feat: add @dashcommerce/create scaffolding CLI (#9) #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Sync starter to flat repo | |
| # Mirrors `packages/starter/` into `emdashCommerce/starter` as a flat, cloneable | |
| # template. Content-mirror approach (not `git subtree split`) — gives us a | |
| # clean history in the target and room for the `workspace:*` → `^x.y.z` | |
| # rewrite without fighting rebases. | |
| # | |
| # Fires on pushes to main that touch the starter or core's version. Also | |
| # triggerable manually via the Actions UI. | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "packages/starter/**" | |
| - "packages/core/package.json" | |
| - ".github/workflows/sync-starter.yml" | |
| workflow_dispatch: | |
| concurrency: sync-starter | |
| permissions: | |
| contents: read | |
| env: | |
| TARGET_REPO: emdashCommerce/starter | |
| TARGET_BRANCH: main | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout monorepo | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| # Handle both "target repo has commits" and "target repo is brand-new and | |
| # empty" cases without failing. actions/checkout refuses empty repos | |
| # because there's no branch to resolve yet, so manage target's .git | |
| # directly instead. | |
| - name: Initialize or fetch target repo | |
| run: | | |
| mkdir -p target | |
| cd target | |
| git init --initial-branch="${TARGET_BRANCH}" --quiet | |
| git remote add origin "https://x-access-token:${TOKEN}@github.com/${TARGET_REPO}.git" | |
| if git fetch --depth=1 origin "${TARGET_BRANCH}" 2>/dev/null; then | |
| echo "::notice::Fetched existing ${TARGET_REPO}@${TARGET_BRANCH}" | |
| git reset --hard "origin/${TARGET_BRANCH}" | |
| else | |
| echo "::notice::${TARGET_REPO} is empty — this run will seed it" | |
| fi | |
| env: | |
| TOKEN: ${{ secrets.STARTER_REPO_TOKEN }} | |
| TARGET_REPO: ${{ env.TARGET_REPO }} | |
| TARGET_BRANCH: ${{ env.TARGET_BRANCH }} | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Mirror starter contents into target | |
| run: | | |
| # Keep target's .git, wipe everything else, copy starter/ in. | |
| find target -mindepth 1 -maxdepth 1 -not -name '.git' -exec rm -rf {} + | |
| cp -R packages/starter/. target/ | |
| # Strip local-dev artifacts that shouldn't ship to the flat repo | |
| rm -rf target/node_modules target/dist target/.astro target/.wrangler \ | |
| target/data.db target/data.db-shm target/data.db-wal target/uploads | |
| mkdir -p target/uploads && touch target/uploads/.gitkeep | |
| # emdash bun-patches live at the monorepo root; propagate to flat repo | |
| # root so patchedDependencies in target/package.json can find them. | |
| if [ -d patches ]; then | |
| cp -R patches target/patches | |
| fi | |
| - name: Rewrite workspace deps + CF-first scripts + wrangler.jsonc | |
| run: | | |
| CORE_VERSION=$(node -p "require('./packages/core/package.json').version") | |
| echo "Pinning @dashcommerce/core to ^${CORE_VERSION}" | |
| node - "$CORE_VERSION" <<'NODE' | |
| const fs = require('fs'); | |
| const [, , version] = process.argv; | |
| // ── package.json ── | |
| const pkgPath = 'target/package.json'; | |
| const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); | |
| // Pin workspace deps to the real published version of core. | |
| for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) { | |
| if (!pkg[field]) continue; | |
| for (const dep of Object.keys(pkg[field])) { | |
| if (pkg[field][dep] === 'workspace:*') { | |
| pkg[field][dep] = `^${version}`; | |
| } | |
| } | |
| } | |
| // Flat repo's primary deploy target is Cloudflare (via the Deploy | |
| // button), so swap scripts so `build` IS the CF build — CF dashboard | |
| // auto-detect will land on `bun run build` and do the right thing. | |
| // `build:node` stays available for contributors who want Node. | |
| if (pkg.scripts?.build && pkg.scripts['build:cf']) { | |
| pkg.scripts['build:node'] = pkg.scripts.build; | |
| pkg.scripts.build = pkg.scripts['build:cf']; | |
| delete pkg.scripts['build:cf']; | |
| } | |
| // Propagate the emdash bun-patch reference from the monorepo root | |
| // so the flat repo applies it on `bun install` too. | |
| const rootPkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| if (rootPkg.patchedDependencies) { | |
| pkg.patchedDependencies = rootPkg.patchedDependencies; | |
| } | |
| fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n'); | |
| // ── wrangler.jsonc ── | |
| // Strip any previously-injected main + assets block. These fields | |
| // must NOT exist at config-load time because @cloudflare/vite-plugin | |
| // (invoked during astro sync, before the build runs) validates that | |
| // main points at an existing file — and the file is the build | |
| // output. @astrojs/cloudflare emits dist/server/wrangler.json at | |
| // build time with these fields filled in correctly; deploy with: | |
| // npx wrangler deploy --config dist/server/wrangler.json | |
| const wranglerPath = 'target/wrangler.jsonc'; | |
| if (fs.existsSync(wranglerPath)) { | |
| let content = fs.readFileSync(wranglerPath, 'utf8'); | |
| const mainBlockRe = /\n\n\t"main":\s*"\.\/dist\/server\/entry\.mjs",\n\t"assets":\s*\{[^}]*\},?/; | |
| if (mainBlockRe.test(content)) { | |
| content = content.replace(mainBlockRe, ''); | |
| fs.writeFileSync(wranglerPath, content); | |
| console.log('Stripped stale main + assets block from wrangler.jsonc'); | |
| } | |
| } | |
| // ── railway.json ── | |
| // Railway auto-detect runs `bun run build`, which on the flat repo | |
| // is the Cloudflare build. Override with build:node so Railway gets | |
| // a Node-standalone server bundle compatible with its runtime. | |
| const railway = { | |
| "$schema": "https://railway.com/railway.schema.json", | |
| build: { | |
| builder: "RAILPACK", | |
| buildCommand: "bun install --frozen-lockfile && bun run build:node", | |
| }, | |
| deploy: { | |
| startCommand: "bun run start", | |
| healthcheckPath: "/", | |
| healthcheckTimeout: 60, | |
| restartPolicyType: "ON_FAILURE", | |
| restartPolicyMaxRetries: 3, | |
| }, | |
| }; | |
| fs.writeFileSync( | |
| 'target/railway.json', | |
| JSON.stringify(railway, null, '\t') + '\n', | |
| ); | |
| console.log('Wrote flat-repo railway.json (uses build:node for Railway)'); | |
| NODE | |
| - name: Regenerate standalone lockfile | |
| working-directory: target | |
| run: | | |
| rm -f bun.lock bun.lockb | |
| bun install --ignore-scripts | |
| - name: Commit + push to target | |
| working-directory: target | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add -A | |
| if git diff --cached --quiet; then | |
| echo "::notice::No starter changes to sync" | |
| exit 0 | |
| fi | |
| SHORT_SHA="${GITHUB_SHA:0:7}" | |
| git commit -m "sync: ${GITHUB_REPOSITORY}@${SHORT_SHA}" \ | |
| -m "Source: https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}" | |
| git push origin "${{ env.TARGET_BRANCH }}" | |
| echo "::notice::Synced ${GITHUB_REPOSITORY}@${SHORT_SHA} → ${TARGET_REPO}" |