@storystick/cli 0.0.0-stage → 0.2.0-rc.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.
Files changed (123) hide show
  1. package/CONTENT-IMPORT.md +349 -0
  2. package/LICENSE +112 -0
  3. package/PUBLIC-API.md +30 -0
  4. package/README.md +263 -2
  5. package/assets/next-v1/app/[[...pathname]]/error.tsx +10 -0
  6. package/assets/next-v1/app/[[...pathname]]/not-found.tsx +8 -0
  7. package/assets/next-v1/app/[[...pathname]]/page.tsx +38 -0
  8. package/assets/next-v1/app/api/storystick/[...action]/route.ts +4 -0
  9. package/assets/next-v1/app/layout.tsx +28 -0
  10. package/assets/next-v1/app/sitemap.ts +3 -0
  11. package/assets/next-v1/env.local.example +13 -0
  12. package/assets/next-v1/globals.css +17 -0
  13. package/assets/next-v1/lib/site.ts +10 -0
  14. package/assets/next-v1/next.config.ts +4 -0
  15. package/assets/skills/storystick-development/SKILL.md +79 -0
  16. package/assets/skills/storystick-development/references/setup.md +152 -0
  17. package/assets/skills/storystick-development/references/story-and-next.md +49 -0
  18. package/dist/_backfill-recommendation.d.ts +12 -0
  19. package/dist/_backfill-recommendation.js +18 -0
  20. package/dist/asset-import.d.ts +50 -0
  21. package/dist/asset-import.js +160 -0
  22. package/dist/assistant-read-client.d.ts +3 -0
  23. package/dist/assistant-read-client.js +8 -0
  24. package/dist/auth-device.d.ts +153 -0
  25. package/dist/auth-device.js +390 -0
  26. package/dist/bootstrap-client.d.ts +52 -0
  27. package/dist/bootstrap-client.js +19 -0
  28. package/dist/cli-error.d.ts +54 -0
  29. package/dist/cli-error.js +107 -0
  30. package/dist/cli.d.ts +68 -0
  31. package/dist/cli.js +1520 -0
  32. package/dist/commands/archive.d.ts +17 -0
  33. package/dist/commands/archive.js +86 -0
  34. package/dist/commands/backfill-refs.d.ts +54 -0
  35. package/dist/commands/backfill-refs.js +217 -0
  36. package/dist/commands/connect.d.ts +11 -0
  37. package/dist/commands/connect.js +124 -0
  38. package/dist/commands/connection.d.ts +57 -0
  39. package/dist/commands/connection.js +83 -0
  40. package/dist/commands/content.d.ts +11 -0
  41. package/dist/commands/content.js +107 -0
  42. package/dist/commands/delivery.d.ts +22 -0
  43. package/dist/commands/delivery.js +99 -0
  44. package/dist/commands/info.d.ts +35 -0
  45. package/dist/commands/info.js +62 -0
  46. package/dist/commands/list.d.ts +28 -0
  47. package/dist/commands/list.js +116 -0
  48. package/dist/commands/migrate.d.ts +80 -0
  49. package/dist/commands/migrate.js +550 -0
  50. package/dist/commands/release.d.ts +8 -0
  51. package/dist/commands/release.js +60 -0
  52. package/dist/commands/setup.d.ts +6 -0
  53. package/dist/commands/setup.js +58 -0
  54. package/dist/commands/slice-previews.d.ts +17 -0
  55. package/dist/commands/slice-previews.js +233 -0
  56. package/dist/commands/source.d.ts +82 -0
  57. package/dist/commands/source.js +192 -0
  58. package/dist/content-import.d.ts +78 -0
  59. package/dist/content-import.js +210 -0
  60. package/dist/generated-transaction.d.ts +22 -0
  61. package/dist/generated-transaction.js +330 -0
  62. package/dist/hub-client.d.ts +72 -0
  63. package/dist/hub-client.js +178 -0
  64. package/dist/init-operation.d.ts +98 -0
  65. package/dist/init-operation.js +427 -0
  66. package/dist/init-readiness.d.ts +10 -0
  67. package/dist/init-readiness.js +50 -0
  68. package/dist/init-scaffold.d.ts +54 -0
  69. package/dist/init-scaffold.js +439 -0
  70. package/dist/init.d.ts +51 -0
  71. package/dist/init.js +789 -0
  72. package/dist/mcp-execution.d.ts +15 -0
  73. package/dist/mcp-execution.js +90 -0
  74. package/dist/mcp.d.ts +7 -0
  75. package/dist/mcp.js +41 -0
  76. package/dist/migration-artifact.d.ts +13 -0
  77. package/dist/migration-artifact.js +154 -0
  78. package/dist/operations-client.d.ts +137 -0
  79. package/dist/operations-client.js +130 -0
  80. package/dist/output.d.ts +347 -0
  81. package/dist/output.js +771 -0
  82. package/dist/project-access.d.ts +18 -0
  83. package/dist/project-access.js +1 -0
  84. package/dist/project-connection.d.ts +62 -0
  85. package/dist/project-connection.js +347 -0
  86. package/dist/project-execution-client.d.ts +2 -0
  87. package/dist/project-execution-client.js +33 -0
  88. package/dist/push-client.d.ts +100 -0
  89. package/dist/push-client.js +160 -0
  90. package/dist/release-context.d.ts +13 -0
  91. package/dist/release-context.js +39 -0
  92. package/dist/release-schema-client.d.ts +16 -0
  93. package/dist/release-schema-client.js +33 -0
  94. package/dist/release-sync.d.ts +36 -0
  95. package/dist/release-sync.js +31 -0
  96. package/dist/resolve.d.ts +15 -0
  97. package/dist/resolve.js +80 -0
  98. package/dist/runtime-config.d.ts +18 -0
  99. package/dist/runtime-config.js +86 -0
  100. package/dist/schema-export.d.ts +10 -0
  101. package/dist/schema-export.js +28 -0
  102. package/dist/setup.d.ts +65 -0
  103. package/dist/setup.js +135 -0
  104. package/dist/source-git.d.ts +36 -0
  105. package/dist/source-git.js +197 -0
  106. package/dist/starter-resolver.d.ts +82 -0
  107. package/dist/starter-resolver.js +495 -0
  108. package/dist/sync-transaction.d.ts +29 -0
  109. package/dist/sync-transaction.js +159 -0
  110. package/dist/transport.d.ts +8 -0
  111. package/dist/transport.js +73 -0
  112. package/examples/sanity/README.md +79 -0
  113. package/examples/sanity/convert.mjs +228 -0
  114. package/examples/sanity/fixture/data.ndjson +7 -0
  115. package/examples/sanity/fixture/mapping.json +44 -0
  116. package/examples/sanity/fixture/originals/mark.svg +1 -0
  117. package/examples/sanity/stories/articleBody.story +23 -0
  118. package/examples/sanity/stories/copy.story +7 -0
  119. package/examples/sanity/stories/page.story +19 -0
  120. package/examples/sanity/stories/picture.story +7 -0
  121. package/examples/sanity/stories/settings.story +9 -0
  122. package/examples/sanity/stories/stack.story +10 -0
  123. package/package.json +75 -3
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `storystick archive <shortId>` (SPEC-1 Item #4).
3
+ *
4
+ * Soft-deletes the given project via `/api/cli/projects/archive`. The Hub
5
+ * patches `deletedAt: now()`; the existing daily GC cron hard-deletes 30d
6
+ * later. `deletedAt` + 30d is echoed back so the CLI can show the user the
7
+ * hard-delete date without baking the grace constant into CLI code.
8
+ *
9
+ * Always prompts for confirmation (no `--yes` flag for first ship — CI
10
+ * scripts can come later).
11
+ */
12
+ export interface ArchiveOptions {
13
+ json?: boolean;
14
+ /** Skip the interactive confirmation prompt (ADR-072 §F6 / T6). */
15
+ yes?: boolean;
16
+ }
17
+ export declare function runArchive(shortId: string, options?: ArchiveOptions): Promise<void>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `storystick archive <shortId>` (SPEC-1 Item #4).
3
+ *
4
+ * Soft-deletes the given project via `/api/cli/projects/archive`. The Hub
5
+ * patches `deletedAt: now()`; the existing daily GC cron hard-deletes 30d
6
+ * later. `deletedAt` + 30d is echoed back so the CLI can show the user the
7
+ * hard-delete date without baking the grace constant into CLI code.
8
+ *
9
+ * Always prompts for confirmation (no `--yes` flag for first ship — CI
10
+ * scripts can come later).
11
+ */
12
+ import pc from 'picocolors';
13
+ import { callHubApi } from '../hub-client.js';
14
+ import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logFinishedStep, logMessage, maybeExitOnCancel, promptConfirm, promptDeviceAuth, withInterrupt, } from '../output.js';
15
+ export async function runArchive(shortId, options = {}) {
16
+ configureOutput({ json: options.json === true });
17
+ if (!shortId || shortId.trim().length === 0) {
18
+ exitWithError('Missing shortId. Usage: storystick archive <shortId>');
19
+ }
20
+ // ADR-072 §F6 / T6: in non-interactive contexts (no TTY OR --json) the
21
+ // prompt cannot reach a human — require --yes so the user opts into
22
+ // the destructive action explicitly. Single rule covers CI scripts,
23
+ // AI agents, piped stdin, and npm-script headless environments.
24
+ const interactive = process.stdin.isTTY && getOutputMode() !== 'json';
25
+ if (!interactive && options.yes !== true) {
26
+ if (getOutputMode() === 'json') {
27
+ exitJsonFailure({
28
+ command: 'archive',
29
+ err: new Error('Cannot prompt for confirmation in non-interactive mode.'),
30
+ hint: 'Pass --yes to skip confirmation in non-interactive mode.',
31
+ });
32
+ }
33
+ exitWithError('Cannot prompt for confirmation in non-interactive mode.', 'Pass --yes to skip confirmation in non-interactive mode.');
34
+ }
35
+ // Confirm before talking to the Hub — a cancelled archive should cost
36
+ // zero network round-trips. --yes skips the prompt in interactive mode
37
+ // too (the user opted in via flag).
38
+ if (!options.yes) {
39
+ let ok;
40
+ try {
41
+ ok = await promptConfirm(`Archive '${shortId}'? Data is recoverable for 30 days.`, { default: false });
42
+ }
43
+ catch (err) {
44
+ // ADR-072 §A5 / D16: Ctrl-C / Esc out of prompt routes through
45
+ // maybeExitOnCancel — exit 130 (was exit 0 pre-ADR-072).
46
+ maybeExitOnCancel(err);
47
+ throw err;
48
+ }
49
+ if (!ok) {
50
+ // ADR-072 §A4: "n" at promptConfirm is an intentional decline → exit 0.
51
+ logMessage(pc.dim('Cancelled.'));
52
+ return;
53
+ }
54
+ }
55
+ const { signal, dispose } = withInterrupt();
56
+ try {
57
+ const result = await callHubApi('/api/cli/projects/archive', {
58
+ method: 'POST',
59
+ body: { shortId },
60
+ onAuthPrompt: promptDeviceAuth,
61
+ signal,
62
+ });
63
+ if (getOutputMode() === 'json') {
64
+ emitJsonResult({
65
+ command: 'archive',
66
+ ok: true,
67
+ data: {
68
+ shortId: result.shortId,
69
+ deletedAt: result.deletedAt,
70
+ hardDeleteAt: result.hardDeleteAt,
71
+ },
72
+ });
73
+ return;
74
+ }
75
+ const hardDate = new Date(result.hardDeleteAt).toISOString().slice(0, 10);
76
+ logFinishedStep(`Archived ${pc.bold(shortId)} (hard-deleted on ${hardDate})`);
77
+ logMessage(pc.dim(' Your .env.local was not touched — run `storystick connect <other>` ' +
78
+ 'to point it at a different project.'));
79
+ }
80
+ catch (err) {
81
+ exitWithError(err);
82
+ }
83
+ finally {
84
+ dispose();
85
+ }
86
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `storystick backfill-refs` — Layer 2 of the simplified backfill design
3
+ * (ADR-062 §Q7 + §D21).
4
+ *
5
+ * Re-extracts live references for the active project — the CLI-only path
6
+ * a user runs after a schema change that affects ref-relevant field types
7
+ * (the diff-gate on `storystick push` surfaces the recommendation).
8
+ *
9
+ * Granularity flags:
10
+ * --storyType <name> Limit to one story type.
11
+ * --document <shortId> Limit to one document.
12
+ * --dry-run Count what would change without writing.
13
+ *
14
+ * The endpoint is chunked: a single mutation call processes one page and
15
+ * returns a continuation cursor. This command loops until the server
16
+ * reports `hasMore === false`, surfacing per-call progress via the
17
+ * spinner so a backfill of a large project shows liveness.
18
+ *
19
+ * Token + endpoint resolution mirrors `storystick push`: the complete local
20
+ * ProjectConnection is validated, its secret key is exchanged for a
21
+ * short-lived project JWT, then the token is read back before the request to
22
+ * `<gateway>/api/m/operations/backfill-refs`. See ADR-058 §Pattern D.
23
+ */
24
+ import { type BackfillRefsBody, type BackfillRefsResult } from '../operations-client.js';
25
+ import type { ProjectRequestOptions } from '../push-client.js';
26
+ export interface BackfillRefsOptions {
27
+ /** Limit scan to one story type. Mutually exclusive with `documentId`. */
28
+ storyType?: string;
29
+ /** Limit scan to one document. Mutually exclusive with `storyType`. */
30
+ documentId?: string;
31
+ /** Don't write — count what would change. */
32
+ dryRun?: boolean;
33
+ /** Emit JSONL events (one per chunk + a final event) on stdout. */
34
+ json?: boolean;
35
+ /** Render info-level diagnostics. */
36
+ verbose?: boolean;
37
+ /** Suppress success and warning output (failures still emit). */
38
+ quiet?: boolean;
39
+ /** Project root directory. Defaults to cwd. */
40
+ root?: string;
41
+ /**
42
+ * Override gateway URL — tests inject a mock server here. Production
43
+ * resolves the URL from `.env.local` / falls back to the well-known
44
+ * Storystick Central endpoint.
45
+ */
46
+ gatewayUrl?: string;
47
+ /**
48
+ * Override token + the entire HTTP transport. Tests bypass the real
49
+ * `fetchSchemaPushToken` + `backfillRefs` calls by providing both.
50
+ */
51
+ fetchTokenImpl?: (gatewayUrl: string, apiKey: string, options?: ProjectRequestOptions) => Promise<string>;
52
+ fetchBackfillImpl?: (gatewayUrl: string, token: string, body: BackfillRefsBody, options?: ProjectRequestOptions) => Promise<BackfillRefsResult>;
53
+ }
54
+ export declare function runBackfillRefs(options?: BackfillRefsOptions): Promise<void>;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * `storystick backfill-refs` — Layer 2 of the simplified backfill design
3
+ * (ADR-062 §Q7 + §D21).
4
+ *
5
+ * Re-extracts live references for the active project — the CLI-only path
6
+ * a user runs after a schema change that affects ref-relevant field types
7
+ * (the diff-gate on `storystick push` surfaces the recommendation).
8
+ *
9
+ * Granularity flags:
10
+ * --storyType <name> Limit to one story type.
11
+ * --document <shortId> Limit to one document.
12
+ * --dry-run Count what would change without writing.
13
+ *
14
+ * The endpoint is chunked: a single mutation call processes one page and
15
+ * returns a continuation cursor. This command loops until the server
16
+ * reports `hasMore === false`, surfacing per-call progress via the
17
+ * spinner so a backfill of a large project shows liveness.
18
+ *
19
+ * Token + endpoint resolution mirrors `storystick push`: the complete local
20
+ * ProjectConnection is validated, its secret key is exchanged for a
21
+ * short-lived project JWT, then the token is read back before the request to
22
+ * `<gateway>/api/m/operations/backfill-refs`. See ADR-058 §Pattern D.
23
+ */
24
+ import { resolve } from 'node:path';
25
+ import pc from 'picocolors';
26
+ import { backfillRefs, } from '../operations-client.js';
27
+ import { fetchSchemaPushToken } from '../push-client.js';
28
+ import { changeSpinner, configureOutput, emitJsonlFailure, emitJsonlEvent, exitWithError, failSpinner, getOutputMode, logFinishedStep, logMessage, showSpinner, succeedSpinner, withInterrupt, } from '../output.js';
29
+ import { resolveCliRuntimeConfig } from '../runtime-config.js';
30
+ import { requireProjectConnection, validateProjectTokenReadback, } from '../project-connection.js';
31
+ // ─────────────────────────────────────────────────────────────
32
+ // Entry point
33
+ // ─────────────────────────────────────────────────────────────
34
+ export async function runBackfillRefs(options = {}) {
35
+ configureOutput({
36
+ json: options.json === true,
37
+ verbose: options.verbose === true,
38
+ quiet: options.quiet === true,
39
+ });
40
+ if (options.storyType !== undefined && options.documentId !== undefined) {
41
+ exitWithError('`--storyType` and `--document` are mutually exclusive. Pass one or the other.');
42
+ }
43
+ const rootDir = resolve(options.root ?? '.');
44
+ const scope = describeScope(options);
45
+ const dryRunSuffix = options.dryRun ? pc.dim(' (dry-run)') : '';
46
+ logMessage(`${pc.bold('Storystick')} — backfilling refs${dryRunSuffix}...`);
47
+ logMessage(pc.dim(`Scope: ${scope}`));
48
+ const { signal, dispose } = withInterrupt();
49
+ try {
50
+ const connection = requireProjectConnection(rootDir);
51
+ const gatewayUrl = resolveCliRuntimeConfig({
52
+ rootDir,
53
+ gatewayUrl: options.gatewayUrl,
54
+ }).gatewayUrl;
55
+ const fetchTokenFn = options.fetchTokenImpl ?? fetchSchemaPushToken;
56
+ const fetchBackfillFn = options.fetchBackfillImpl ?? backfillRefs;
57
+ const token = await fetchTokenFn(gatewayUrl, connection.apiKey, { signal });
58
+ validateProjectTokenReadback(token, connection);
59
+ let cursor = undefined;
60
+ let totalScanned = 0;
61
+ let totalAdded = 0;
62
+ let totalRemoved = 0;
63
+ let totalRoutabilityPatched = 0;
64
+ let totalDisplayNamesPatched = 0;
65
+ let calls = 0;
66
+ let hashUpdated = false;
67
+ const startedAt = Date.now();
68
+ await showSpinner('Starting backfill...');
69
+ while (true) {
70
+ if (signal.aborted) {
71
+ // ADR-072 §A5 / D16: Ctrl-C mid-loop must exit 130, not return
72
+ // (which would silently exit 0 — the pre-ADR-072 behaviour).
73
+ failSpinner('Backfill cancelled.');
74
+ process.exit(130);
75
+ }
76
+ const body = {
77
+ storyType: options.storyType,
78
+ documentId: options.documentId,
79
+ dryRun: options.dryRun,
80
+ cursor,
81
+ };
82
+ const result = await fetchBackfillFn(gatewayUrl, token, body, { signal });
83
+ calls += 1;
84
+ totalScanned += result.docsScanned;
85
+ totalAdded += result.refsAdded;
86
+ totalRemoved += result.refsRemoved;
87
+ totalRoutabilityPatched += result.routabilityPatched ?? 0;
88
+ totalDisplayNamesPatched += result.displayNamesPatched ?? 0;
89
+ hashUpdated = hashUpdated || result.hashUpdated;
90
+ cursor = result.continueCursor;
91
+ changeSpinner(`Scanned ${totalScanned} doc${totalScanned === 1 ? '' : 's'}` +
92
+ ` · ${totalAdded} added · ${totalRemoved} removed` +
93
+ (totalRoutabilityPatched > 0
94
+ ? ` · ${totalRoutabilityPatched} routability patched`
95
+ : '') +
96
+ (totalDisplayNamesPatched > 0
97
+ ? ` · ${totalDisplayNamesPatched} names patched`
98
+ : ''));
99
+ // ADR-072 §A3 / D15 / L5: under --json, emit one progress event
100
+ // per chunk so streaming consumers can show liveness without
101
+ // parsing the human spinner.
102
+ if (getOutputMode() === 'json') {
103
+ emitJsonlEvent({
104
+ command: 'backfill-refs',
105
+ event: 'progress',
106
+ data: {
107
+ docsScanned: totalScanned,
108
+ refsAdded: totalAdded,
109
+ refsRemoved: totalRemoved,
110
+ routabilityPatched: totalRoutabilityPatched,
111
+ displayNamesPatched: totalDisplayNamesPatched,
112
+ calls,
113
+ hasMore: result.hasMore,
114
+ },
115
+ });
116
+ }
117
+ if (!result.hasMore)
118
+ break;
119
+ // Hard safety cap so a stuck server can't loop us forever.
120
+ if (calls > 10_000) {
121
+ failSpinner('Backfill aborted: too many continuation calls.');
122
+ exitWithError('Backfill did not converge after 10,000 chunks. Please report this.');
123
+ }
124
+ }
125
+ const elapsedMs = Date.now() - startedAt;
126
+ const elapsed = formatElapsed(elapsedMs);
127
+ if (getOutputMode() === 'json') {
128
+ emitJsonlEvent({
129
+ command: 'backfill-refs',
130
+ event: 'final',
131
+ ok: true,
132
+ data: {
133
+ docsScanned: totalScanned,
134
+ refsAdded: totalAdded,
135
+ refsRemoved: totalRemoved,
136
+ routabilityPatched: totalRoutabilityPatched,
137
+ displayNamesPatched: totalDisplayNamesPatched,
138
+ calls,
139
+ hashUpdated,
140
+ dryRun: options.dryRun === true,
141
+ elapsedMs,
142
+ },
143
+ });
144
+ return;
145
+ }
146
+ if (options.dryRun) {
147
+ const hasChanges = totalAdded > 0 ||
148
+ totalRemoved > 0 ||
149
+ totalRoutabilityPatched > 0 ||
150
+ totalDisplayNamesPatched > 0;
151
+ const verb = hasChanges ? 'Would' : 'No changes';
152
+ const summary = verb === 'No changes'
153
+ ? `${verb} (scanned ${totalScanned} doc${totalScanned === 1 ? '' : 's'} in ${elapsed})`
154
+ : `${verb} add ${totalAdded}, remove ${totalRemoved} ref` +
155
+ (totalAdded + totalRemoved === 1 ? '' : 's') +
156
+ (totalRoutabilityPatched > 0
157
+ ? `, patch ${totalRoutabilityPatched} routability flag` +
158
+ (totalRoutabilityPatched === 1 ? '' : 's')
159
+ : '') +
160
+ (totalDisplayNamesPatched > 0
161
+ ? `, patch ${totalDisplayNamesPatched} document name` +
162
+ (totalDisplayNamesPatched === 1 ? '' : 's')
163
+ : '') +
164
+ ` across ${totalScanned} doc${totalScanned === 1 ? '' : 's'} (dry-run, ${elapsed})`;
165
+ succeedSpinner(summary);
166
+ return;
167
+ }
168
+ const summary = `${totalScanned} doc${totalScanned === 1 ? '' : 's'} scanned, ` +
169
+ `${totalAdded} ref${totalAdded === 1 ? '' : 's'} added, ` +
170
+ `${totalRemoved} ref${totalRemoved === 1 ? '' : 's'} removed` +
171
+ (totalRoutabilityPatched > 0
172
+ ? `, ${totalRoutabilityPatched} routability flag` +
173
+ (totalRoutabilityPatched === 1 ? '' : 's') +
174
+ ' patched'
175
+ : '') +
176
+ (totalDisplayNamesPatched > 0
177
+ ? `, ${totalDisplayNamesPatched} document name` +
178
+ (totalDisplayNamesPatched === 1 ? '' : 's') +
179
+ ' patched'
180
+ : '') +
181
+ ` (${elapsed})`;
182
+ succeedSpinner(summary);
183
+ if (hashUpdated) {
184
+ logFinishedStep('Project marked as fully reconciled.');
185
+ }
186
+ }
187
+ catch (err) {
188
+ failSpinner('Backfill failed.');
189
+ if (getOutputMode() === 'json') {
190
+ emitJsonlFailure({ command: 'backfill-refs', err });
191
+ }
192
+ exitWithError(err);
193
+ }
194
+ finally {
195
+ dispose();
196
+ }
197
+ }
198
+ // ─────────────────────────────────────────────────────────────
199
+ // Helpers
200
+ // ─────────────────────────────────────────────────────────────
201
+ function describeScope(options) {
202
+ if (options.documentId)
203
+ return `document ${options.documentId}`;
204
+ if (options.storyType)
205
+ return `storyType ${options.storyType}`;
206
+ return 'project-wide';
207
+ }
208
+ function formatElapsed(ms) {
209
+ if (ms < 1000)
210
+ return `${ms}ms`;
211
+ const s = ms / 1000;
212
+ if (s < 60)
213
+ return `${s.toFixed(1)}s`;
214
+ const m = Math.floor(s / 60);
215
+ const rem = s - m * 60;
216
+ return `${m}m ${rem.toFixed(0)}s`;
217
+ }
@@ -0,0 +1,11 @@
1
+ export interface ConnectOptions {
2
+ root?: string;
3
+ origin?: string;
4
+ dryRun?: boolean;
5
+ json?: boolean;
6
+ /** @internal deterministic idempotency seam. */
7
+ operationId?: string;
8
+ /** @internal deterministic filesystem failure seam. */
9
+ connectionFaultAt?: 'before-rename';
10
+ }
11
+ export declare function runConnect(requestedShortId: string, options?: ConnectOptions): Promise<void>;
@@ -0,0 +1,124 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import pc from 'picocolors';
5
+ import { StorystickError } from '../cli-error.js';
6
+ import { acknowledgeCliBootstrap, prepareCliBootstrap, } from '../bootstrap-client.js';
7
+ import { callHubApi } from '../hub-client.js';
8
+ import { commitProjectConnection, findProjectConnectionEnvFile, parseProjectConnection, parseProjectShortId, planProjectConnection, requireProjectConnection, validateProjectTokenReadback, } from '../project-connection.js';
9
+ import { fetchSchemaPushToken } from '../push-client.js';
10
+ import { resolveCliRuntimeConfig } from '../runtime-config.js';
11
+ import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logFinishedStep, logMessage, logOutput, promptDeviceAuth, withInterrupt, } from '../output.js';
12
+ export async function runConnect(requestedShortId, options = {}) {
13
+ configureOutput({ json: options.json === true });
14
+ const { signal, dispose } = withInterrupt();
15
+ try {
16
+ const shortId = parseProjectShortId(requestedShortId);
17
+ const rootDir = resolve(options.root ?? '.');
18
+ if (!existsSync(rootDir) || !statSync(rootDir).isDirectory()) {
19
+ throw new StorystickError(`Project root does not exist: ${rootDir}`, {
20
+ code: 'PROJECT_ROOT_NOT_FOUND',
21
+ category: 'validation',
22
+ });
23
+ }
24
+ const project = await callHubApi(`/api/cli/projects/for-user-by-shortId?shortId=${encodeURIComponent(shortId)}`, { onAuthPrompt: promptDeviceAuth, signal, rootDir });
25
+ if (parseProjectShortId(project.shortId) !== shortId) {
26
+ throw new StorystickError('Gateway resolved a different project than the requested shortId.', {
27
+ code: 'PROJECT_RESOLUTION_MISMATCH',
28
+ category: 'system',
29
+ });
30
+ }
31
+ const envFile = findProjectConnectionEnvFile(rootDir) ?? join(rootDir, '.env.local');
32
+ // A public dry-run has no remote mutation: it proves access and renders
33
+ // only the target path + redacted four-field plan.
34
+ if (options.dryRun === true) {
35
+ renderConnectResult(shortId, project.name, planProjectConnection(envFile, shortId), true);
36
+ return;
37
+ }
38
+ const operationId = options.operationId ?? randomUUID();
39
+ const intent = {
40
+ kind: 'link',
41
+ requestedShortId: shortId,
42
+ initialAllowedOrigins: [options.origin ?? 'http://localhost:3000'],
43
+ originMode: options.origin ? 'append' : 'default-if-empty',
44
+ };
45
+ const prepared = await prepareCliBootstrap(operationId, intent, {
46
+ rootDir,
47
+ signal,
48
+ onAuthPrompt: promptDeviceAuth,
49
+ });
50
+ if (prepared.receipt.projectShortId !== shortId) {
51
+ throw new StorystickError('Bootstrap receipt points to a different project.', { code: 'BOOTSTRAP_PROJECT_MISMATCH', category: 'system' });
52
+ }
53
+ const connection = prepared.credentials
54
+ ? parseProjectConnection({
55
+ projectId: shortId,
56
+ publicProjectId: shortId,
57
+ apiKey: prepared.credentials.apiKey,
58
+ publishableKey: prepared.credentials.publishableKey,
59
+ })
60
+ : requireProjectConnection(rootDir);
61
+ if (connection.shortId !== shortId) {
62
+ throw new StorystickError('Acknowledged bootstrap has no matching local ProjectConnection.', { code: 'BOOTSTRAP_LOCAL_CONNECTION_MISSING', category: 'validation' });
63
+ }
64
+ const gatewayUrl = resolveCliRuntimeConfig({
65
+ rootDir,
66
+ gatewayUrl: project.endpoint,
67
+ }).gatewayUrl;
68
+ const token = await fetchSchemaPushToken(gatewayUrl, connection.apiKey, {
69
+ signal,
70
+ });
71
+ validateProjectTokenReadback(token, connection);
72
+ const result = commitProjectConnection(envFile, connection, {
73
+ faultAt: options.connectionFaultAt,
74
+ });
75
+ const receipt = prepared.credentials
76
+ ? (await acknowledgeCliBootstrap(operationId, intent, prepared.receipt.keyPairVersion, { rootDir, signal, onAuthPrompt: promptDeviceAuth })).receipt
77
+ : prepared.receipt;
78
+ if (receipt.state !== 'connection_acknowledged') {
79
+ throw new StorystickError('Bootstrap acknowledgement was not committed.', {
80
+ code: 'BOOTSTRAP_ACK_INCOMPLETE',
81
+ category: 'system',
82
+ });
83
+ }
84
+ renderConnectResult(shortId, project.name, result, false, receipt);
85
+ }
86
+ catch (error) {
87
+ const hint = error instanceof StorystickError && error.status === 404
88
+ ? 'Run `storystick list` to see your projects.'
89
+ : undefined;
90
+ if (getOutputMode() === 'json') {
91
+ exitJsonFailure({ command: 'connect', err: error, hint });
92
+ }
93
+ exitWithError(error, hint);
94
+ }
95
+ finally {
96
+ dispose();
97
+ }
98
+ }
99
+ function renderConnectResult(shortId, projectName, result, dryRun, receipt) {
100
+ const data = {
101
+ shortId,
102
+ envFile: result.envFile,
103
+ created: result.created,
104
+ changed: result.changed,
105
+ dryRun,
106
+ changes: result.changes,
107
+ ...(receipt ? { receipt } : {}),
108
+ };
109
+ if (getOutputMode() === 'json') {
110
+ emitJsonResult({ command: 'connect', ok: true, data });
111
+ return;
112
+ }
113
+ if (dryRun) {
114
+ const lines = [
115
+ `Would connect ${projectName} (${shortId})`,
116
+ `Env file: ${result.envFile}`,
117
+ ...result.changes.map((change) => `${change.name}=${change.value} (${change.action})`),
118
+ ];
119
+ logOutput(lines.join('\n') + '\n');
120
+ return;
121
+ }
122
+ logFinishedStep(`Connected to ${pc.bold(projectName)} (${shortId})`);
123
+ logMessage(pc.dim(` ${result.envFile}${result.created ? ' created' : ' updated'}`));
124
+ }
@@ -0,0 +1,57 @@
1
+ type Options = {
2
+ root?: string;
3
+ project?: string;
4
+ provider?: string;
5
+ attempt?: string;
6
+ cursor?: string;
7
+ installation?: string;
8
+ repository?: string;
9
+ fullName?: string;
10
+ branch?: string;
11
+ destination?: string;
12
+ json?: boolean;
13
+ };
14
+ export declare function connectionRequest(operation: string, options: Options): {
15
+ operation: string;
16
+ provider?: undefined;
17
+ attemptId?: undefined;
18
+ selection?: undefined;
19
+ } | {
20
+ operation: string;
21
+ provider: string | undefined;
22
+ attemptId?: undefined;
23
+ selection?: undefined;
24
+ } | {
25
+ installationId?: string | undefined;
26
+ cursor?: string | undefined;
27
+ operation: string;
28
+ attemptId: string;
29
+ provider?: undefined;
30
+ selection?: undefined;
31
+ } | {
32
+ operation: string;
33
+ attemptId: string;
34
+ selection: {
35
+ provider: string;
36
+ destinationId: string | undefined;
37
+ installationId?: undefined;
38
+ repositoryId?: undefined;
39
+ fullName?: undefined;
40
+ branch?: undefined;
41
+ };
42
+ provider?: undefined;
43
+ } | {
44
+ operation: string;
45
+ attemptId: string;
46
+ selection: {
47
+ provider: string;
48
+ installationId: number;
49
+ repositoryId: number;
50
+ fullName: string;
51
+ branch: string;
52
+ destinationId?: undefined;
53
+ };
54
+ provider?: undefined;
55
+ };
56
+ export declare function runConnection(operation: string, options: Options): Promise<void>;
57
+ export {};
@@ -0,0 +1,83 @@
1
+ import { resolve } from 'node:path';
2
+ import { callHubApi } from '../hub-client.js';
3
+ import { resolveCliRuntimeConfig } from '../runtime-config.js';
4
+ import { resolveProjectConnection } from '../project-connection.js';
5
+ import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logMessage, withInterrupt, } from '../output.js';
6
+ export function connectionRequest(operation, options) {
7
+ if (operation === 'status')
8
+ return { operation };
9
+ if (['begin', 'disconnect'].includes(operation)) {
10
+ if (!['github', 'vercel'].includes(options.provider ?? ''))
11
+ throw Error('Specify --provider github or vercel.');
12
+ return { operation, provider: options.provider };
13
+ }
14
+ if (!options.attempt)
15
+ throw Error('Specify the --attempt returned by connection status or begin.');
16
+ if (operation === 'resources')
17
+ return {
18
+ operation,
19
+ attemptId: options.attempt,
20
+ ...(options.cursor ? { cursor: options.cursor } : {}),
21
+ ...(options.installation ? { installationId: options.installation } : {}),
22
+ };
23
+ if (operation === 'select') {
24
+ if (options.provider === 'vercel' && /^prj_[a-zA-Z0-9]+$/.test(options.destination ?? ''))
25
+ return {
26
+ operation,
27
+ attemptId: options.attempt,
28
+ selection: { provider: 'vercel', destinationId: options.destination },
29
+ };
30
+ if (options.provider === 'github' &&
31
+ /^[1-9][0-9]*$/.test(options.installation ?? '') &&
32
+ /^[1-9][0-9]*$/.test(options.repository ?? '') &&
33
+ options.fullName &&
34
+ options.branch)
35
+ return {
36
+ operation,
37
+ attemptId: options.attempt,
38
+ selection: {
39
+ provider: 'github',
40
+ installationId: Number(options.installation),
41
+ repositoryId: Number(options.repository),
42
+ fullName: options.fullName,
43
+ branch: options.branch,
44
+ },
45
+ };
46
+ throw Error('Select a returned resource: Vercel needs --destination; GitHub needs --installation, --repository, --full-name and --branch.');
47
+ }
48
+ throw Error('Use connection status, begin, resources, select or disconnect.');
49
+ }
50
+ export async function runConnection(operation, options) {
51
+ configureOutput({ json: options.json === true });
52
+ const { signal, dispose } = withInterrupt();
53
+ try {
54
+ if (process.env.STORYSTICK_RELEASE_SESSION)
55
+ throw Error('Account connections require your CLI login outside a release sandbox.');
56
+ const request = connectionRequest(operation, options);
57
+ const rootDir = resolve(options.root ?? '.');
58
+ const projectShortId = options.project ?? resolveProjectConnection(rootDir)?.shortId;
59
+ if (!projectShortId)
60
+ throw Error('Connect a project or specify --project <shortId>.');
61
+ const data = await callHubApi('/api/cli/project-setup', {
62
+ rootDir,
63
+ gatewayUrl: resolveCliRuntimeConfig({ rootDir }).gatewayUrl,
64
+ method: 'POST',
65
+ body: { projectShortId, operation: 'connection', request },
66
+ signal,
67
+ interactiveAuth: false,
68
+ timeoutMs: 90_000,
69
+ });
70
+ if (getOutputMode() === 'json')
71
+ emitJsonResult({ command: 'connection', ok: true, data });
72
+ else
73
+ logMessage(JSON.stringify(data, null, 2));
74
+ }
75
+ catch (err) {
76
+ if (getOutputMode() === 'json')
77
+ exitJsonFailure({ command: 'connection', err });
78
+ exitWithError(err);
79
+ }
80
+ finally {
81
+ dispose();
82
+ }
83
+ }
@@ -0,0 +1,11 @@
1
+ export declare function runContent(action: string, target: string | undefined, options: {
2
+ root?: string;
3
+ project?: string;
4
+ json?: boolean;
5
+ dryRun?: boolean;
6
+ apply?: boolean;
7
+ plan?: string;
8
+ resume?: string;
9
+ newRun?: boolean;
10
+ renew?: string;
11
+ }): Promise<void>;