@design-drafts/cli 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Craigory Coppola
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @design-drafts/cli
2
+
3
+ Push static site previews as branches to a **design-drafts** repo, and have
4
+ them deployed to GitHub Pages automatically.
5
+
6
+ A *host* repo collects many *draft* previews. You scaffold a host once, then
7
+ push any built static directory as a draft branch; a GitHub Actions workflow
8
+ publishes each branch under its own path on GitHub Pages and an index site
9
+ links them all together.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ # one-off
15
+ npx @design-drafts/cli --help
16
+
17
+ # or install globally for the `design-drafts` binary
18
+ npm i -g @design-drafts/cli
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### `design-drafts push [path]`
24
+
25
+ Push a built static directory (default `.`) as a draft preview branch. This is
26
+ the default command, so `design-drafts ./dist` works too.
27
+
28
+ ```sh
29
+ design-drafts push ./dist --repo my-org/design-previews --site-name homepage-v2
30
+ ```
31
+
32
+ - `--repo <org/repo>` — the host repo to push to (remembered after the first run).
33
+ - `--site-name <name>` — branch/preview name (prompted if omitted).
34
+ - `--prefix <prefix>` — branch prefix for previews (default `drafts/`; pass `""` to push without one).
35
+
36
+ ### `design-drafts init host`
37
+
38
+ Scaffold a new GitHub repo configured to host draft previews (deploy workflow,
39
+ index site, Pages setup).
40
+
41
+ ```sh
42
+ design-drafts init host --repo my-org/design-previews
43
+ ```
44
+
45
+ - `--private` — create the GitHub repo as private.
46
+ - `--yes` — skip the confirmation prompt before GitHub setup.
47
+
48
+ ### `design-drafts init draft [directory]`
49
+
50
+ Scaffold a new draft directory locally.
51
+
52
+ ```sh
53
+ design-drafts init draft ./my-draft
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ Shared options (`--repo`, `--site-name`, `--template-ref`) can be supplied via
59
+ flags, the `DESIGN_DRAFTS_*` environment variables, or a JSON config file. The
60
+ `--repo` value is persisted to a per-user config after the first successful
61
+ push, so subsequent runs don't need it.
62
+
63
+ ## License
64
+
65
+ MIT
package/bin.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('./dist/index.mjs');
package/dist/index.mjs ADDED
@@ -0,0 +1,964 @@
1
+ import { createHash } from "node:crypto";
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { ConfigurationProviders, cli } from "cli-forge";
6
+ import { confirm, isCancel, select, text } from "@clack/prompts";
7
+ import { execSync } from "node:child_process";
8
+ //#region package.json
9
+ var version = "0.1.0";
10
+ //#endregion
11
+ //#region src/config.ts
12
+ const CONFIG_FILENAME = "design-drafts.config.json";
13
+ const DEFAULT_PREFIX = "drafts/";
14
+ const homeConfigPath = join(homedir(), CONFIG_FILENAME);
15
+ const localConfigPath = join(process.cwd(), CONFIG_FILENAME);
16
+ const homeJsonProvider = {
17
+ resolve: () => existsSync(homeConfigPath) ? homeConfigPath : void 0,
18
+ load: (filename) => JSON.parse(readFileSync(filename, "utf-8"))
19
+ };
20
+ function readConfig(configPath) {
21
+ return existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : {};
22
+ }
23
+ /** Reads a single key from the home config without prompting. Used by the magic
24
+ * `init` to decide whether the host has already been set up. */
25
+ function readHomeConfigValue(key) {
26
+ const value = readConfig(homeConfigPath)[key];
27
+ return typeof value === "string" ? value : void 0;
28
+ }
29
+ /** Resolves a value, prompting (with optional validation) when it's missing.
30
+ * Does NOT persist — callers that want the answer remembered should call
31
+ * `persistHomeConfigValue` only after the operation it feeds succeeds, so a
32
+ * value that led nowhere (e.g. a repo that failed to push) never becomes a
33
+ * sticky default. */
34
+ async function promptForValue(existing, label, promptMessage, validate) {
35
+ if (existing) return existing;
36
+ const value = await text({
37
+ message: promptMessage,
38
+ validate: (v) => {
39
+ if (!v?.trim()) return `${label} is required`;
40
+ return validate?.(v);
41
+ }
42
+ });
43
+ if (typeof value !== "string") process.exit(1);
44
+ return value;
45
+ }
46
+ async function promptAndPersist(existing, argKey, configPath, promptMessage, validate) {
47
+ if (existing) return existing;
48
+ const value = await promptForValue(existing, argKey, promptMessage, validate);
49
+ writeFileSync(configPath, JSON.stringify({
50
+ ...readConfig(configPath),
51
+ [argKey]: value
52
+ }, null, 2) + "\n");
53
+ return value;
54
+ }
55
+ function persistHomeConfigValue(key, value) {
56
+ const previousFile = readConfig(homeConfigPath);
57
+ if (previousFile[key] === value) return;
58
+ writeFileSync(homeConfigPath, JSON.stringify({
59
+ ...previousFile,
60
+ [key]: value
61
+ }, null, 2) + "\n");
62
+ }
63
+ function resolvePrefix(existing) {
64
+ if (typeof existing === "string") {
65
+ persistHomeConfigValue("prefix", existing);
66
+ return existing;
67
+ }
68
+ persistHomeConfigValue("prefix", DEFAULT_PREFIX);
69
+ return DEFAULT_PREFIX;
70
+ }
71
+ //#endregion
72
+ //#region src/errors.ts
73
+ /**
74
+ * An expected, user-facing failure. The top-level handler prints its message
75
+ * verbatim (no stack trace) and exits non-zero. Use it for situations the user
76
+ * can act on — a push that was rejected, a template that couldn't be fetched —
77
+ * as opposed to programmer errors, which should surface as ordinary Errors.
78
+ */
79
+ var CliError = class extends Error {
80
+ constructor(message) {
81
+ super(message);
82
+ this.name = "CliError";
83
+ }
84
+ };
85
+ /** Prints a failure as a single clean message and exits non-zero. A CliError's
86
+ * message is shown verbatim; anything else is an unexpected bug, labelled as
87
+ * such (with the stack only under DESIGN_DRAFTS_DEBUG). */
88
+ function reportError(error) {
89
+ if (error instanceof CliError) console.error(`\n${error.message}`);
90
+ else if (process.env.DESIGN_DRAFTS_DEBUG) console.error(error);
91
+ else {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ console.error(`\nUnexpected error: ${message}\nRe-run with DESIGN_DRAFTS_DEBUG=1 for details.`);
94
+ }
95
+ process.exit(1);
96
+ }
97
+ /**
98
+ * Runs a command handler, converting any thrown failure into a clean message +
99
+ * non-zero exit. We catch and exit here rather than letting the error escape,
100
+ * because cli-forge's own runCommand catch prints the raw error (with stack)
101
+ * and the help text — exiting first keeps the output clean.
102
+ */
103
+ async function runHandler(fn) {
104
+ try {
105
+ await fn();
106
+ } catch (error) {
107
+ reportError(error);
108
+ }
109
+ }
110
+ //#endregion
111
+ //#region src/exec.ts
112
+ /** Runs a command, streaming its output to the user's terminal. Throws on a
113
+ * non-zero exit. */
114
+ function exec(command, cwd) {
115
+ execSync(command, {
116
+ cwd,
117
+ stdio: "inherit"
118
+ });
119
+ }
120
+ /** Runs a command and returns its trimmed stdout, or undefined if it fails or
121
+ * produces no output. Stderr is suppressed — callers use this to probe state
122
+ * (does this tag exist? is gh authed?) where failure is an expected answer. */
123
+ function capture(command, cwd) {
124
+ try {
125
+ return execSync(command, {
126
+ cwd,
127
+ stdio: [
128
+ "ignore",
129
+ "pipe",
130
+ "ignore"
131
+ ]
132
+ }).toString("utf-8").trim() || void 0;
133
+ } catch {
134
+ return;
135
+ }
136
+ }
137
+ /** True when the command exits zero. */
138
+ function succeeds(command, cwd) {
139
+ try {
140
+ execSync(command, {
141
+ cwd,
142
+ stdio: "ignore"
143
+ });
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ //#endregion
150
+ //#region src/github.ts
151
+ /**
152
+ * Picks a remote URL for a GitHub repo using auth that actually works.
153
+ *
154
+ * `gh config get git_protocol` reflects the transport the user authenticated
155
+ * with — if they set up gh over HTTPS, gh installs a git credential helper and
156
+ * HTTPS pushes succeed while SSH would fail (no key), and vice versa. We honor
157
+ * that signal and fall back to SSH (git's traditional default) when gh isn't
158
+ * present to ask.
159
+ */
160
+ function githubRemoteUrl(repo, cwd) {
161
+ if (capture("gh config get git_protocol", cwd) === "https") return `https://github.com/${repo}.git`;
162
+ return `git@github.com:${repo}.git`;
163
+ }
164
+ //#endregion
165
+ //#region src/site-name.ts
166
+ const SITE_NAME_PATTERN = /^[a-z0-9][a-z0-9-_]{0,62}$/;
167
+ function slugifySiteName(input) {
168
+ return input.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").replace(/^[-_]+/, "").slice(0, 63);
169
+ }
170
+ function validateSiteName(name) {
171
+ if (!name || !name.trim()) return {
172
+ ok: false,
173
+ reason: "site-name must not be empty"
174
+ };
175
+ if (name.length > 63) return {
176
+ ok: false,
177
+ reason: "site-name must be 63 characters or fewer",
178
+ suggestion: slugifySiteName(name) || void 0
179
+ };
180
+ if (!SITE_NAME_PATTERN.test(name)) return {
181
+ ok: false,
182
+ reason: "site-name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, or underscores",
183
+ suggestion: slugifySiteName(name) || void 0
184
+ };
185
+ return { ok: true };
186
+ }
187
+ //#endregion
188
+ //#region src/init/templates.ts
189
+ const HOST_MARKER = "# design-drafts: host deploy workflow";
190
+ const DEPLOY_WORKFLOW = `${HOST_MARKER}
191
+ name: Deploy Preview
192
+
193
+ on:
194
+ push:
195
+ branches:
196
+ - 'drafts/**'
197
+ pull_request:
198
+ types: [opened, synchronize, reopened]
199
+ branches: [main]
200
+ delete:
201
+ workflow_dispatch:
202
+ inputs:
203
+ branch:
204
+ description: 'Branch to deploy a preview for'
205
+ required: true
206
+ type: string
207
+
208
+ # The gh-pages branch is the durable store of every preview (NOT the Pages
209
+ # source — Pages serves the artifact uploaded below). Each run reconstructs the
210
+ # full site from gh-pages, swaps in the changed preview, and republishes.
211
+ concurrency:
212
+ group: deploy-preview
213
+ cancel-in-progress: false
214
+
215
+ jobs:
216
+ build:
217
+ runs-on: ubuntu-latest
218
+ if: |
219
+ (github.event_name == 'push' && startsWith(github.ref_name, 'drafts/')) ||
220
+ (github.event_name == 'pull_request' && startsWith(github.head_ref, 'drafts/')) ||
221
+ (github.event_name == 'delete' && github.event.ref_type == 'branch' && startsWith(github.event.ref, 'drafts/')) ||
222
+ github.event_name == 'workflow_dispatch'
223
+ permissions:
224
+ contents: write
225
+ env:
226
+ BRANCH_NAME: \${{ inputs.branch || github.head_ref || github.ref_name }}
227
+ DRAFT_BRANCH_PREFIX: drafts/
228
+ steps:
229
+ - name: Checkout main
230
+ uses: actions/checkout@v4
231
+ with:
232
+ ref: main
233
+
234
+ - name: Setup pnpm
235
+ uses: pnpm/action-setup@v4
236
+
237
+ - name: Setup Node
238
+ uses: actions/setup-node@v4
239
+ with:
240
+ node-version: 20
241
+ cache: pnpm
242
+
243
+ - name: Install dependencies
244
+ run: pnpm install
245
+
246
+ - name: Configure git identity
247
+ run: |
248
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
249
+ git config --global user.name "github-actions[bot]"
250
+
251
+ - name: Checkout gh-pages store to staging
252
+ run: |
253
+ if git fetch origin gh-pages:gh-pages 2>/dev/null; then
254
+ git worktree add .gh-pages-staging gh-pages
255
+ else
256
+ echo "gh-pages store does not exist yet, starting fresh"
257
+ git worktree add --orphan -b gh-pages .gh-pages-staging
258
+ fi
259
+
260
+ - name: Resolve preview directory name
261
+ run: |
262
+ if [ "\${{ github.event_name }}" = "delete" ]; then
263
+ BRANCH_NAME="\${{ github.event.ref }}"
264
+ fi
265
+ # Strip the draft prefix so branch "drafts/my-site" maps to "/my-site/".
266
+ PREVIEW_DIR="\${BRANCH_NAME#\${DRAFT_BRANCH_PREFIX}}"
267
+ if [ -z "$PREVIEW_DIR" ] || [ "$PREVIEW_DIR" = "$BRANCH_NAME" ]; then
268
+ PREVIEW_DIR="$BRANCH_NAME"
269
+ fi
270
+ echo "PREVIEW_DIR=$PREVIEW_DIR" >> "$GITHUB_ENV"
271
+
272
+ - name: Stage branch content
273
+ if: github.event_name != 'delete'
274
+ run: |
275
+ git clone --depth 1 --branch "$BRANCH_NAME" \\
276
+ "https://x-access-token:\${{ secrets.GITHUB_TOKEN }}@github.com/\${{ github.repository }}.git" \\
277
+ /tmp/branch-content
278
+ rm -rf /tmp/branch-content/.git
279
+ # The CLI embeds this workflow on the draft branch so the push event
280
+ # can resolve it. Strip it back out so it never ships into the preview.
281
+ rm -rf /tmp/branch-content/.github
282
+ rm -rf ".gh-pages-staging/\${PREVIEW_DIR}"
283
+ mkdir -p ".gh-pages-staging/\${PREVIEW_DIR}"
284
+ cp -a /tmp/branch-content/. ".gh-pages-staging/\${PREVIEW_DIR}/"
285
+
286
+ - name: Remove deleted branch's directory
287
+ if: github.event_name == 'delete'
288
+ run: rm -rf ".gh-pages-staging/\${PREVIEW_DIR}"
289
+
290
+ - name: Build index site
291
+ run: pnpm build
292
+ env:
293
+ PAGES_DIR: \${{ github.workspace }}/.gh-pages-staging
294
+ DESIGN_DRAFTS_PREFIX: drafts/
295
+ DESIGN_DRAFTS_REPO: \${{ github.repository }}
296
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
297
+
298
+ - name: Copy index site into staging
299
+ run: |
300
+ cp -r dist/client/* .gh-pages-staging/ 2>/dev/null || true
301
+ touch .gh-pages-staging/.nojekyll
302
+
303
+ - name: Persist preview store to gh-pages
304
+ run: |
305
+ cd .gh-pages-staging
306
+ git add -A
307
+ git commit -m "deploy: \${PREVIEW_DIR}" || echo "no changes to persist"
308
+ git push origin HEAD:gh-pages
309
+
310
+ - name: Upload Pages artifact
311
+ uses: actions/upload-pages-artifact@v3
312
+ with:
313
+ path: .gh-pages-staging
314
+
315
+ deploy:
316
+ needs: build
317
+ runs-on: ubuntu-latest
318
+ permissions:
319
+ pages: write
320
+ id-token: write
321
+ environment:
322
+ name: github-pages
323
+ url: \${{ steps.deployment.outputs.page_url }}
324
+ steps:
325
+ - name: Deploy to GitHub Pages
326
+ id: deployment
327
+ uses: actions/deploy-pages@v4
328
+ `;
329
+ const GITIGNORE = `node_modules
330
+ dist
331
+ .gh-pages-staging
332
+ `;
333
+ /**
334
+ * The starter manifest written by \`init draft\`. The push command reads the
335
+ * \`prompt\` field for the commit trailer.
336
+ */
337
+ function draftConfig(siteName) {
338
+ return JSON.stringify({
339
+ siteName,
340
+ prompt: ""
341
+ }, null, 2) + "\n";
342
+ }
343
+ const DRAFT_INDEX_HTML = `<!doctype html>
344
+ <html lang="en">
345
+ <head>
346
+ <meta charset="utf-8" />
347
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
348
+ <title>Draft preview</title>
349
+ </head>
350
+ <body>
351
+ <main>
352
+ <h1>Draft preview</h1>
353
+ <p>Replace this with your design draft, then run <code>design-drafts</code> to publish.</p>
354
+ </main>
355
+ </body>
356
+ </html>
357
+ `;
358
+ //#endregion
359
+ //#region src/init/draft.ts
360
+ /** Writes the file only if it is absent, logging which path it created and
361
+ * which it left untouched. Returns true when a file was written. */
362
+ function writeIfAbsent(filePath, contents) {
363
+ const name = basename(filePath);
364
+ if (existsSync(filePath)) {
365
+ console.log(` skipped ${name} (already exists)`);
366
+ return false;
367
+ }
368
+ writeFileSync(filePath, contents);
369
+ console.log(` created ${name}`);
370
+ return true;
371
+ }
372
+ async function initDraft(opts) {
373
+ const targetDir = resolve(opts.path);
374
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
375
+ let siteName = await promptAndPersist(opts.siteName, "site-name", localConfigPath, "Site name for this draft:");
376
+ const validation = validateSiteName(siteName);
377
+ if (!validation.ok) {
378
+ const fixed = validation.suggestion ?? slugifySiteName(siteName);
379
+ console.warn(`"${siteName}" is not a valid site-name (${validation.reason}); using "${fixed}".`);
380
+ siteName = fixed;
381
+ }
382
+ console.log(`\nScaffolding draft "${siteName}" in ${targetDir}:`);
383
+ const wroteManifest = writeIfAbsent(join(targetDir, "draft.config.json"), draftConfig(siteName));
384
+ const wroteIndex = writeIfAbsent(join(targetDir, "index.html"), DRAFT_INDEX_HTML);
385
+ if (!wroteManifest && !wroteIndex) {
386
+ console.log(`\nDraft "${siteName}" was already scaffolded; nothing to write.`);
387
+ return;
388
+ }
389
+ console.log(`\nDraft "${siteName}" ready.\nEdit index.html, then run \`design-drafts\` here to publish a preview.`);
390
+ }
391
+ //#endregion
392
+ //#region src/validate.ts
393
+ const REPO_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
394
+ const PREFIX_PATTERN = /^[A-Za-z0-9._/-]*$/;
395
+ const REF_PATTERN = /^[A-Za-z0-9._/-]+$/;
396
+ function validateRepo(repo) {
397
+ if (!repo.trim()) return {
398
+ ok: false,
399
+ reason: "repo must not be empty"
400
+ };
401
+ if (!REPO_PATTERN.test(repo)) return {
402
+ ok: false,
403
+ reason: "repo must be in \"owner/name\" form using only letters, digits, \".\", \"_\", or \"-\""
404
+ };
405
+ return { ok: true };
406
+ }
407
+ function validatePrefix(prefix) {
408
+ if (!PREFIX_PATTERN.test(prefix)) return {
409
+ ok: false,
410
+ reason: "prefix may contain only letters, digits, \"/\", \".\", \"_\", or \"-\""
411
+ };
412
+ return { ok: true };
413
+ }
414
+ function validateTemplateRef(ref) {
415
+ if (!REF_PATTERN.test(ref)) return {
416
+ ok: false,
417
+ reason: "template-ref may contain only letters, digits, \"/\", \".\", \"_\", or \"-\""
418
+ };
419
+ return { ok: true };
420
+ }
421
+ //#endregion
422
+ //#region src/init/rewrites.ts
423
+ const CATALOG_PREFIX = "catalog:";
424
+ function resolveDepBlock(block, catalog) {
425
+ if (!block) return block;
426
+ const resolved = {};
427
+ for (const [name, spec] of Object.entries(block)) {
428
+ if (!spec.startsWith(CATALOG_PREFIX)) {
429
+ resolved[name] = spec;
430
+ continue;
431
+ }
432
+ if (spec.slice(8)) throw new Error(`Cannot resolve "${name}": named catalogs ("${spec}") are not supported`);
433
+ const version = catalog[name];
434
+ if (!version) throw new Error(`Cannot resolve "${name}": "${spec}" but no catalog entry exists for it`);
435
+ resolved[name] = version;
436
+ }
437
+ return resolved;
438
+ }
439
+ /**
440
+ * Rewrites a site package.json for life in a standalone host repo: every
441
+ * `catalog:` dependency specifier is replaced with the concrete version from
442
+ * the canonical workspace catalog, the nx project config is removed, and a
443
+ * `packageManager` field is set (so `pnpm/action-setup` can resolve a version
444
+ * in the generated repo — without it the first deploy fails).
445
+ */
446
+ function resolveSitePackageJson(raw, catalog, packageManager) {
447
+ const pkg = JSON.parse(raw);
448
+ const next = { ...pkg };
449
+ next.dependencies = resolveDepBlock(pkg.dependencies, catalog);
450
+ next.devDependencies = resolveDepBlock(pkg.devDependencies, catalog);
451
+ delete next.nx;
452
+ if (packageManager) next.packageManager = packageManager;
453
+ if (next.dependencies && Object.keys(next.dependencies).length === 0) delete next.dependencies;
454
+ if (next.devDependencies && Object.keys(next.devDependencies).length === 0) delete next.devDependencies;
455
+ return JSON.stringify(next, null, 2) + "\n";
456
+ }
457
+ /**
458
+ * Parses the `catalog:` block out of a canonical pnpm-workspace.yaml. We keep
459
+ * the parser intentionally small (flat `key: value` pairs under `catalog:`)
460
+ * rather than pulling in a YAML dependency — the catalog is always a flat map
461
+ * of package name to version string.
462
+ */
463
+ function parseCatalog(workspaceYaml) {
464
+ const lines = workspaceYaml.split("\n");
465
+ const catalog = {};
466
+ let inCatalog = false;
467
+ let catalogIndent = 0;
468
+ for (const line of lines) {
469
+ if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
470
+ const indent = line.length - line.trimStart().length;
471
+ if (!inCatalog) {
472
+ if (line.trim() === "catalog:") {
473
+ inCatalog = true;
474
+ catalogIndent = indent;
475
+ }
476
+ continue;
477
+ }
478
+ if (indent <= catalogIndent) break;
479
+ const match = line.trim().match(/^['"]?([^'":]+)['"]?\s*:\s*(.+)$/);
480
+ if (match) {
481
+ const name = match[1].trim();
482
+ catalog[name] = match[2].trim().replace(/^['"]|['"]$/g, "");
483
+ }
484
+ }
485
+ return catalog;
486
+ }
487
+ /**
488
+ * Rewrites the Vite `base` so asset URLs resolve under the host repo's Pages
489
+ * subpath (https://<owner>.github.io/<repo>/). Accepts `org/repo` or a bare
490
+ * repo name and uses just the repo segment.
491
+ */
492
+ function rewriteViteBase(source, repo) {
493
+ const base = `/${repo.includes("/") ? repo.split("/").pop() : repo}/`;
494
+ const replaced = source.replace(/base:\s*(['"])(?:[^'"]*)\1/, `base: '${base}'`);
495
+ if (replaced === source) throw new Error("Could not find a `base:` option in vite.config to rewrite");
496
+ return replaced;
497
+ }
498
+ /**
499
+ * Folds the canonical base tsconfig's compilerOptions into the site tsconfig and
500
+ * drops `extends`, so the host needs no `tsconfig.base.json` at its root. The
501
+ * site's own compilerOptions win on conflict.
502
+ */
503
+ function inlineTsconfig(siteRaw, baseRaw) {
504
+ const site = JSON.parse(siteRaw);
505
+ const base = JSON.parse(baseRaw);
506
+ const merged = { ...site };
507
+ delete merged.extends;
508
+ merged.compilerOptions = {
509
+ ...base.compilerOptions ?? {},
510
+ ...site.compilerOptions ?? {}
511
+ };
512
+ return JSON.stringify(merged, null, 2) + "\n";
513
+ }
514
+ /**
515
+ * Picks the template ref to check the site out from. An explicit override wins;
516
+ * otherwise prefer the release tag matching the CLI version when it exists,
517
+ * falling back to the default branch.
518
+ */
519
+ function resolveTemplateRef(opts) {
520
+ if (opts.override) return opts.override;
521
+ const tag = `v${opts.cliVersion}`;
522
+ if (opts.tagExists(tag)) return tag;
523
+ return opts.defaultBranch;
524
+ }
525
+ //#endregion
526
+ //#region src/init/host.ts
527
+ const CANONICAL_REPO = "AgentEnder/design-drafts";
528
+ const CANONICAL_URL = `https://github.com/${CANONICAL_REPO}.git`;
529
+ const DEFAULT_BRANCH$1 = "main";
530
+ const SITE_SUBDIR = "packages/site";
531
+ const WORKFLOW_PATH$1 = ".github/workflows/deploy-preview.yml";
532
+ const TRANSFORMED = new Set([
533
+ "package.json",
534
+ "vite.config.ts",
535
+ "tsconfig.json"
536
+ ]);
537
+ function repoName(repo) {
538
+ return repo.includes("/") ? repo.split("/").pop() : repo;
539
+ }
540
+ function detectRemoteRepo(targetDir) {
541
+ const url = capture("git remote get-url origin", targetDir);
542
+ if (!url) return void 0;
543
+ return url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/)?.[1];
544
+ }
545
+ /** True when the target already carries our generated workflow. */
546
+ function alreadyConfigured(targetDir) {
547
+ const workflow = join(targetDir, WORKFLOW_PATH$1);
548
+ if (!existsSync(workflow)) return false;
549
+ return readFileSync(workflow, "utf-8").includes(HOST_MARKER);
550
+ }
551
+ /** Sparse-checks-out the canonical site source into a temp dir, returning that
552
+ * path. Cone mode (the recommended default) always materialises top-level
553
+ * files, so the root files we read to resolve the site (pnpm-workspace.yaml's
554
+ * catalog and tsconfig.base.json) come along without listing them, while the
555
+ * sibling packages stay excluded. */
556
+ function sparseCheckout(ref) {
557
+ const tmp = mkdtempSync(join(tmpdir(), "design-drafts-host-"));
558
+ try {
559
+ exec(`git clone --filter=blob:none --no-checkout ${CANONICAL_URL} .`, tmp);
560
+ exec(`git sparse-checkout set ${SITE_SUBDIR}`, tmp);
561
+ exec(`git checkout ${ref}`, tmp);
562
+ } catch {
563
+ rmSync(tmp, {
564
+ recursive: true,
565
+ force: true
566
+ });
567
+ throw new CliError(`Could not fetch the host template from ${CANONICAL_REPO}@${ref}.\nCheck your network connection and that the ref exists (try --template-ref main).`);
568
+ }
569
+ return tmp;
570
+ }
571
+ function writeScaffold(checkout, targetDir, repo) {
572
+ const siteDir = join(checkout, SITE_SUBDIR);
573
+ cpSync(siteDir, targetDir, {
574
+ recursive: true,
575
+ filter: (src) => !TRANSFORMED.has(src.slice(siteDir.length + 1))
576
+ });
577
+ const catalog = parseCatalog(readFileSync(join(checkout, "pnpm-workspace.yaml"), "utf-8"));
578
+ const rootPkg = JSON.parse(readFileSync(join(checkout, "package.json"), "utf-8"));
579
+ writeFileSync(join(targetDir, "package.json"), resolveSitePackageJson(readFileSync(join(siteDir, "package.json"), "utf-8"), catalog, rootPkg.packageManager));
580
+ const viteRaw = readFileSync(join(siteDir, "vite.config.ts"), "utf-8");
581
+ writeFileSync(join(targetDir, "vite.config.ts"), rewriteViteBase(viteRaw, repo));
582
+ writeFileSync(join(targetDir, "tsconfig.json"), inlineTsconfig(readFileSync(join(siteDir, "tsconfig.json"), "utf-8"), readFileSync(join(checkout, "tsconfig.base.json"), "utf-8")));
583
+ const workflowDest = join(targetDir, WORKFLOW_PATH$1);
584
+ mkdirSync(dirname(workflowDest), { recursive: true });
585
+ writeFileSync(workflowDest, DEPLOY_WORKFLOW);
586
+ writeFileSync(join(targetDir, ".nojekyll"), "");
587
+ writeFileSync(join(targetDir, ".gitignore"), GITIGNORE);
588
+ }
589
+ function commitScaffold(targetDir) {
590
+ if (!existsSync(join(targetDir, ".git"))) exec(`git init -b ${DEFAULT_BRANCH$1}`, targetDir);
591
+ exec("git add .", targetDir);
592
+ if (capture("git status --porcelain", targetDir)) exec("git commit -m \"chore: scaffold design-drafts host\"", targetDir);
593
+ }
594
+ /** Best-effort GitHub wiring. Never hard-fails: the local scaffold already
595
+ * succeeded, so any gh gap becomes printed manual steps. Returns true when the
596
+ * repo is set up and pushed (so the caller can discard a throwaway scaffold). */
597
+ function configureGitHub(targetDir, repo, isPrivate) {
598
+ if (!succeeds("gh auth status", targetDir)) {
599
+ printManualSteps(repo, targetDir, isPrivate, "GitHub CLI (`gh`) is not installed or authenticated");
600
+ return false;
601
+ }
602
+ const visibility = isPrivate ? "--private" : "--public";
603
+ if (!succeeds(`gh repo view ${repo}`, targetDir)) {
604
+ if (!succeeds(`gh repo create ${repo} ${visibility} --source ${JSON.stringify(targetDir)} --remote origin --push`, targetDir)) {
605
+ printManualSteps(repo, targetDir, isPrivate, `could not create ${repo}`);
606
+ return false;
607
+ }
608
+ enablePages(targetDir, repo, isPrivate);
609
+ return true;
610
+ }
611
+ if (!detectRemoteRepo(targetDir)) exec(`git remote add origin ${githubRemoteUrl(repo, targetDir)}`, targetDir);
612
+ if (!succeeds(`git push -u origin ${DEFAULT_BRANCH$1}`, targetDir)) {
613
+ printManualSteps(repo, targetDir, isPrivate, `could not push to ${repo} (does its ${DEFAULT_BRANCH$1} already have commits?)`);
614
+ return false;
615
+ }
616
+ enablePages(targetDir, repo, isPrivate);
617
+ return true;
618
+ }
619
+ /** Sets the Pages source to "GitHub Actions" (build_type=workflow). */
620
+ function enablePages(targetDir, repo, isPrivate) {
621
+ const create = `gh api -X POST /repos/${repo}/pages -f build_type=workflow`;
622
+ const update = `gh api -X PUT /repos/${repo}/pages -f build_type=workflow`;
623
+ if (succeeds(create, targetDir) || succeeds(update, targetDir)) {
624
+ console.log(`Enabled GitHub Pages (Actions source) on ${repo}.`);
625
+ return;
626
+ }
627
+ console.warn(`Warning: could not enable GitHub Pages on ${repo}.${isPrivate ? " Private repos need GitHub Pro/Team for Pages — make the repo public or upgrade." : ""}\nEnable it manually: Settings -> Pages -> Build and deployment -> Source: GitHub Actions.`);
628
+ }
629
+ function printManualSteps(repo, targetDir, isPrivate, reason) {
630
+ console.warn(`\nScaffold is at ${targetDir}, but GitHub setup was skipped (${reason}).\nFinish manually:\n gh repo create ${repo} ${isPrivate ? "--private" : "--public"} --source ${JSON.stringify(targetDir)} --remote origin --push\n Settings -> Pages -> Source: GitHub Actions\n`);
631
+ }
632
+ /** Resolves repo visibility, prompting interactively when not preset. */
633
+ async function resolveVisibility(opts) {
634
+ if (typeof opts.private === "boolean") return opts.private;
635
+ const choice = await select({
636
+ message: "Repository visibility:",
637
+ options: [{
638
+ value: false,
639
+ label: "Public",
640
+ hint: "required for free GitHub Pages"
641
+ }, {
642
+ value: true,
643
+ label: "Private",
644
+ hint: "Pages needs GitHub Pro/Team"
645
+ }],
646
+ initialValue: false
647
+ });
648
+ if (isCancel(choice)) process.exit(1);
649
+ return choice;
650
+ }
651
+ /** Confirmation gate before the outward GitHub actions (create/push/Pages). */
652
+ async function confirmGitHub(repo, isPrivate) {
653
+ const answer = await confirm({ message: `Create ${isPrivate ? "private" : "public"} repo ${repo} on GitHub, push the scaffold, and enable Pages?` });
654
+ if (isCancel(answer)) process.exit(1);
655
+ return answer === true;
656
+ }
657
+ async function initHost(opts) {
658
+ const usingTmp = !opts.path;
659
+ const targetDir = opts.path ? resolve(opts.path) : mkdtempSync(join(tmpdir(), "design-drafts-host-scaffold-"));
660
+ if (opts.path && !existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
661
+ const repo = await promptForValue(opts.repo ?? readHomeConfigValue("repo") ?? (opts.path ? detectRemoteRepo(targetDir) : void 0), "repo", "GitHub repo for the host (org/repo):", (v) => validateRepo(v).ok ? void 0 : "use \"owner/name\" form");
662
+ const repoCheck = validateRepo(repo);
663
+ if (!repoCheck.ok) {
664
+ console.error(`Invalid repo "${repo}": ${repoCheck.reason}`);
665
+ process.exit(1);
666
+ }
667
+ if (opts.templateRef) {
668
+ const refCheck = validateTemplateRef(opts.templateRef);
669
+ if (!refCheck.ok) {
670
+ console.error(`Invalid template-ref "${opts.templateRef}": ${refCheck.reason}`);
671
+ process.exit(1);
672
+ }
673
+ }
674
+ if (opts.path && alreadyConfigured(targetDir)) {
675
+ console.log(`Host already configured at ${targetDir}; re-ensuring Pages.`);
676
+ enablePages(targetDir, repo, opts.private ?? false);
677
+ return;
678
+ }
679
+ const ref = resolveTemplateRef({
680
+ override: opts.templateRef,
681
+ cliVersion: opts.cliVersion,
682
+ defaultBranch: DEFAULT_BRANCH$1,
683
+ tagExists: (tag) => Boolean(capture(`git ls-remote --tags ${CANONICAL_URL} ${tag}`, targetDir))
684
+ });
685
+ console.log(`Scaffolding host "${repoName(repo)}" from ${CANONICAL_REPO}@${ref}...`);
686
+ const checkout = sparseCheckout(ref);
687
+ try {
688
+ writeScaffold(checkout, targetDir, repo);
689
+ } finally {
690
+ rmSync(checkout, {
691
+ recursive: true,
692
+ force: true
693
+ });
694
+ }
695
+ commitScaffold(targetDir);
696
+ const isPrivate = await resolveVisibility(opts);
697
+ if (!(opts.yes || await confirmGitHub(repo, isPrivate))) {
698
+ console.log(`\nStopped before GitHub setup. The scaffold is at ${targetDir}.`);
699
+ printManualSteps(repo, targetDir, isPrivate, "cancelled");
700
+ return;
701
+ }
702
+ const pushed = configureGitHub(targetDir, repo, isPrivate);
703
+ if (pushed) persistHomeConfigValue("repo", repo);
704
+ if (!usingTmp) {
705
+ console.log(`\nHost scaffolded at ${targetDir}.`);
706
+ return;
707
+ }
708
+ if (pushed) {
709
+ rmSync(targetDir, {
710
+ recursive: true,
711
+ force: true
712
+ });
713
+ console.log(`\nHost ready at https://github.com/${repo} (local scaffold discarded).`);
714
+ } else console.log(`\nKept the scaffold at ${targetDir} so you can finish the push.`);
715
+ }
716
+ //#endregion
717
+ //#region src/init/init.ts
718
+ /**
719
+ * The "from nothing to ready-to-publish" path. Scaffolds a host the first time
720
+ * (detected by the absence of a configured `repo`), then scaffolds a draft in
721
+ * the target directory. Stops short of auto-pushing a placeholder — it prints
722
+ * the single command to publish instead.
723
+ */
724
+ async function init(opts) {
725
+ if (!Boolean(opts.repo ?? readHomeConfigValue("repo"))) {
726
+ console.log("No host configured yet — setting one up first.\n");
727
+ await initHost({
728
+ repo: opts.repo,
729
+ templateRef: opts.templateRef,
730
+ cliVersion: opts.cliVersion
731
+ });
732
+ console.log("");
733
+ }
734
+ await initDraft({
735
+ path: opts.path,
736
+ siteName: opts.siteName
737
+ });
738
+ console.log("\nWhen the draft looks right, run `design-drafts` to publish.");
739
+ }
740
+ //#endregion
741
+ //#region src/index.ts
742
+ const CLI_VERSION = version;
743
+ const DEFAULT_BRANCH = "main";
744
+ const WORKFLOW_PATH = ".github/workflows/deploy-preview.yml";
745
+ async function embedDeployWorkflow(repo, tmpDir) {
746
+ const url = `https://raw.githubusercontent.com/${repo}/${DEFAULT_BRANCH}/${WORKFLOW_PATH}`;
747
+ let contents;
748
+ try {
749
+ const response = await fetch(url);
750
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
751
+ contents = await response.text();
752
+ } catch (error) {
753
+ const reason = error instanceof Error ? error.message : String(error);
754
+ console.warn(`Warning: could not fetch deploy workflow from ${url} (${reason}).\nThe preview will not auto-deploy. Trigger it manually with:\n gh workflow run deploy-preview.yml -f branch=<branch> --repo ${repo}`);
755
+ return;
756
+ }
757
+ const destination = join(tmpDir, WORKFLOW_PATH);
758
+ mkdirSync(dirname(destination), { recursive: true });
759
+ writeFileSync(destination, contents);
760
+ }
761
+ function getSourceMetadata(sourcePath) {
762
+ return {
763
+ sha: capture("git rev-parse HEAD", sourcePath),
764
+ repo: capture("git remote get-url origin", sourcePath),
765
+ authorName: capture("git config user.name", sourcePath),
766
+ authorEmail: capture("git config user.email", sourcePath)
767
+ };
768
+ }
769
+ function hashManifest(manifestPath) {
770
+ try {
771
+ const content = readFileSync(manifestPath, "utf-8");
772
+ return createHash("sha256").update(content).digest("hex");
773
+ } catch {
774
+ return;
775
+ }
776
+ }
777
+ function readManifestPrompt(manifestPath, sourcePath) {
778
+ let raw;
779
+ try {
780
+ raw = readFileSync(manifestPath, "utf-8");
781
+ } catch {
782
+ return;
783
+ }
784
+ let parsed;
785
+ try {
786
+ parsed = JSON.parse(raw);
787
+ } catch {
788
+ return;
789
+ }
790
+ if (!parsed || typeof parsed !== "object" || !("prompt" in parsed)) return;
791
+ const prompt = parsed.prompt;
792
+ if (typeof prompt !== "string" || !prompt.trim()) return;
793
+ const candidatePath = isAbsolute(prompt) ? prompt : join(sourcePath, prompt);
794
+ try {
795
+ const stat = statSync(candidatePath);
796
+ if (stat.isFile() && stat.size < 200) {
797
+ const inline = readFileSync(candidatePath, "utf-8").trim();
798
+ if (inline) return inline.replace(/\s+/g, " ");
799
+ }
800
+ } catch {}
801
+ return prompt.replace(/\s+/g, " ");
802
+ }
803
+ function sanitizeAuthorName(name) {
804
+ return name.replace(/</g, "(").replace(/>/g, ")");
805
+ }
806
+ /**
807
+ * Builds the structured commit message used for every draft push.
808
+ *
809
+ * Format:
810
+ *
811
+ * push: <site-name>
812
+ *
813
+ * source-sha: <sha>
814
+ * source-repo: <repo>
815
+ * author: <Name <email>>
816
+ * prompt: <one-line summary or path>
817
+ * draft-config-sha: <sha256 of draft.config.json>
818
+ *
819
+ * Each trailer line is omitted when its data is unavailable (no source git
820
+ * repo, no manifest, no `prompt` field, etc.). The site-name subject line is
821
+ * always present.
822
+ */
823
+ function buildCommitMessage(opts) {
824
+ const lines = [`push: ${opts.siteName}`, ""];
825
+ const { metadata } = opts;
826
+ if (metadata.sha) lines.push(`source-sha: ${metadata.sha}`);
827
+ if (metadata.repo) lines.push(`source-repo: ${metadata.repo}`);
828
+ if (metadata.authorName && metadata.authorEmail) {
829
+ const safeName = sanitizeAuthorName(metadata.authorName);
830
+ lines.push(`author: ${safeName} <${metadata.authorEmail}>`);
831
+ }
832
+ if (opts.prompt) lines.push(`prompt: ${opts.prompt}`);
833
+ if (opts.draftConfigSha) lines.push(`draft-config-sha: ${opts.draftConfigSha}`);
834
+ if (lines.length === 2) lines.pop();
835
+ return lines.join("\n") + "\n";
836
+ }
837
+ async function pushHandler(args) {
838
+ const repo = await promptForValue(args.repo, "repo", "GitHub repo (org/repo):", (v) => validateRepo(v).ok ? void 0 : "use \"owner/name\" form");
839
+ const siteName = await promptAndPersist(args["site-name"], "site-name", localConfigPath, "Site name for this preview:");
840
+ const prefix = resolvePrefix(args.prefix);
841
+ const sourcePath = resolve(args.path ?? ".");
842
+ const validation = validateSiteName(siteName);
843
+ if (!validation.ok) {
844
+ console.error(`Invalid site-name "${siteName}": ${validation.reason}`);
845
+ if (validation.suggestion) console.error(`Try: ${validation.suggestion}`);
846
+ process.exit(1);
847
+ }
848
+ const repoCheck = validateRepo(repo);
849
+ if (!repoCheck.ok) {
850
+ console.error(`Invalid repo "${repo}": ${repoCheck.reason}`);
851
+ process.exit(1);
852
+ }
853
+ const prefixCheck = validatePrefix(prefix);
854
+ if (!prefixCheck.ok) {
855
+ console.error(`Invalid prefix "${prefix}": ${prefixCheck.reason}`);
856
+ process.exit(1);
857
+ }
858
+ if (!existsSync(sourcePath)) {
859
+ console.error(`Path does not exist: ${sourcePath}`);
860
+ process.exit(1);
861
+ }
862
+ const manifestPath = join(sourcePath, "draft.config.json");
863
+ const metadata = getSourceMetadata(sourcePath);
864
+ const draftConfigSha = hashManifest(manifestPath);
865
+ const commitMessage = buildCommitMessage({
866
+ siteName,
867
+ metadata,
868
+ prompt: readManifestPrompt(manifestPath, sourcePath),
869
+ draftConfigSha
870
+ });
871
+ const branchName = `${prefix}${siteName}`;
872
+ const tmpDir = mkdtempSync(join(tmpdir(), "design-drafts-"));
873
+ const messageFile = join(tmpdir(), `design-drafts-msg-${process.pid}.txt`);
874
+ writeFileSync(messageFile, commitMessage);
875
+ try {
876
+ cpSync(sourcePath, tmpDir, { recursive: true });
877
+ await embedDeployWorkflow(repo, tmpDir);
878
+ exec(`git init -b ${branchName}`, tmpDir);
879
+ exec(`git remote add origin ${githubRemoteUrl(repo, tmpDir)}`, tmpDir);
880
+ exec("git add .", tmpDir);
881
+ exec(`git commit -F ${JSON.stringify(messageFile)}`, tmpDir);
882
+ try {
883
+ exec(`git push --force origin ${branchName}`, tmpDir);
884
+ } catch {
885
+ throw new CliError(`Failed to push "${branchName}" to ${repo}.\nCheck that the repo exists and you have push access (\`gh auth status\`),\nthen re-run. Nothing was saved as a default.`);
886
+ }
887
+ persistHomeConfigValue("repo", repo);
888
+ console.log(`\nPushed "${branchName}" to ${repo}`);
889
+ } finally {
890
+ rmSync(tmpDir, {
891
+ recursive: true,
892
+ force: true
893
+ });
894
+ rmSync(messageFile, { force: true });
895
+ }
896
+ }
897
+ await cli("design-drafts", {
898
+ description: "Push static site previews as branches to a design-drafts repo",
899
+ builder: (args) => args.option("repo", {
900
+ type: "string",
901
+ description: "GitHub repo in org/repo form"
902
+ }).option("site-name", {
903
+ type: "string",
904
+ description: "Name for this site preview (becomes the branch name)"
905
+ }).option("template-ref", {
906
+ type: "string",
907
+ description: "Ref of the canonical repo to scaffold the host site from (default: matching version tag, else main)"
908
+ }).env({ prefix: "DESIGN_DRAFTS" }).config(homeJsonProvider).config(ConfigurationProviders.JsonFile(CONFIG_FILENAME)).command("init", {
909
+ description: "Set up a host (if needed) and scaffold a draft, ready to publish",
910
+ builder: (initArgs) => initArgs.command("host", {
911
+ description: "Scaffold a GitHub repo to host draft previews",
912
+ builder: (b) => b.option("path", {
913
+ type: "string",
914
+ description: "Persist the scaffold to this directory instead of a throwaway tmpdir"
915
+ }).option("private", {
916
+ type: "boolean",
917
+ description: "Create a private repo (default: prompt; Pages needs Pro/Team for private)"
918
+ }).option("yes", {
919
+ type: "boolean",
920
+ description: "Skip the confirmation prompt before GitHub setup"
921
+ }),
922
+ handler: (a) => runHandler(() => initHost({
923
+ path: a.path,
924
+ repo: a.repo,
925
+ templateRef: a["template-ref"],
926
+ private: a.private,
927
+ yes: a.yes,
928
+ cliVersion: CLI_VERSION
929
+ }))
930
+ }).command("draft", {
931
+ description: "Scaffold a new draft directory",
932
+ builder: (b) => b.positional("path", {
933
+ type: "string",
934
+ default: ".",
935
+ description: "Directory to scaffold the draft into"
936
+ }),
937
+ handler: (a) => runHandler(() => initDraft({
938
+ path: a.path,
939
+ siteName: a["site-name"]
940
+ }))
941
+ }),
942
+ handler: (a) => runHandler(() => init({
943
+ path: ".",
944
+ repo: a.repo,
945
+ siteName: a["site-name"],
946
+ templateRef: a["template-ref"],
947
+ cliVersion: CLI_VERSION
948
+ }))
949
+ }).command("push", {
950
+ alias: ["$0"],
951
+ description: "Push a built directory as a draft preview branch",
952
+ builder: (b) => b.positional("path", {
953
+ type: "string",
954
+ default: ".",
955
+ description: "Directory to push as a site preview"
956
+ }).option("prefix", {
957
+ type: "string",
958
+ description: "Branch prefix used when pushing previews (default: \"drafts/\"). Pass an empty string to push without a prefix."
959
+ }),
960
+ handler: (a) => runHandler(() => pushHandler(a))
961
+ })
962
+ }).forge();
963
+ //#endregion
964
+ export {};
package/package.json CHANGED
@@ -1,8 +1,56 @@
1
1
  {
2
2
  "name": "@design-drafts/cli",
3
- "version": "0.0.0",
4
- "description": "Shell package for @design-drafts/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for scaffolding and deploying design-drafts previews to GitHub Pages.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "design-drafts": "./bin.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "bin.js",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/AgentEnder/design-drafts.git",
21
+ "directory": "packages/cli"
22
+ },
23
+ "homepage": "https://github.com/AgentEnder/design-drafts/tree/main/packages/cli#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/AgentEnder/design-drafts/issues"
26
+ },
27
+ "keywords": [
28
+ "design-drafts",
29
+ "cli",
30
+ "preview",
31
+ "github-pages",
32
+ "static-site"
33
+ ],
5
34
  "publishConfig": {
6
35
  "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@clack/prompts": "^1.1.0",
39
+ "cli-forge": "^1.9.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "18.16.9",
43
+ "tsdown": "^0.21.7",
44
+ "typescript": "5.9.3",
45
+ "vitest": "^4.1.4",
46
+ "yaml": "^2.8.3"
47
+ },
48
+ "nx": {
49
+ "name": "cli"
50
+ },
51
+ "scripts": {
52
+ "build": "tsdown src/index.ts --format esm --out-dir dist",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run"
7
55
  }
8
56
  }