@storystick/cli 0.0.0-stage → 0.2.0-rc.1
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/CONTENT-IMPORT.md +349 -0
- package/LICENSE +112 -0
- package/PUBLIC-API.md +30 -0
- package/README.md +263 -2
- package/assets/next-v1/app/[[...pathname]]/error.tsx +10 -0
- package/assets/next-v1/app/[[...pathname]]/not-found.tsx +8 -0
- package/assets/next-v1/app/[[...pathname]]/page.tsx +38 -0
- package/assets/next-v1/app/api/storystick/[...action]/route.ts +4 -0
- package/assets/next-v1/app/layout.tsx +28 -0
- package/assets/next-v1/app/sitemap.ts +3 -0
- package/assets/next-v1/env.local.example +13 -0
- package/assets/next-v1/globals.css +17 -0
- package/assets/next-v1/lib/site.ts +10 -0
- package/assets/next-v1/next.config.ts +4 -0
- package/assets/skills/storystick-development/SKILL.md +79 -0
- package/assets/skills/storystick-development/references/setup.md +152 -0
- package/assets/skills/storystick-development/references/story-and-next.md +49 -0
- package/dist/_backfill-recommendation.d.ts +12 -0
- package/dist/_backfill-recommendation.js +18 -0
- package/dist/asset-import.d.ts +50 -0
- package/dist/asset-import.js +160 -0
- package/dist/assistant-read-client.d.ts +3 -0
- package/dist/assistant-read-client.js +8 -0
- package/dist/auth-device.d.ts +153 -0
- package/dist/auth-device.js +390 -0
- package/dist/bootstrap-client.d.ts +52 -0
- package/dist/bootstrap-client.js +19 -0
- package/dist/cli-error.d.ts +54 -0
- package/dist/cli-error.js +107 -0
- package/dist/cli.d.ts +68 -0
- package/dist/cli.js +1520 -0
- package/dist/commands/archive.d.ts +17 -0
- package/dist/commands/archive.js +86 -0
- package/dist/commands/backfill-refs.d.ts +54 -0
- package/dist/commands/backfill-refs.js +217 -0
- package/dist/commands/connect.d.ts +11 -0
- package/dist/commands/connect.js +124 -0
- package/dist/commands/connection.d.ts +57 -0
- package/dist/commands/connection.js +83 -0
- package/dist/commands/content.d.ts +11 -0
- package/dist/commands/content.js +107 -0
- package/dist/commands/delivery.d.ts +22 -0
- package/dist/commands/delivery.js +99 -0
- package/dist/commands/info.d.ts +35 -0
- package/dist/commands/info.js +62 -0
- package/dist/commands/list.d.ts +28 -0
- package/dist/commands/list.js +116 -0
- package/dist/commands/migrate.d.ts +80 -0
- package/dist/commands/migrate.js +550 -0
- package/dist/commands/release.d.ts +8 -0
- package/dist/commands/release.js +60 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.js +58 -0
- package/dist/commands/slice-previews.d.ts +17 -0
- package/dist/commands/slice-previews.js +233 -0
- package/dist/commands/source.d.ts +82 -0
- package/dist/commands/source.js +192 -0
- package/dist/content-import.d.ts +78 -0
- package/dist/content-import.js +210 -0
- package/dist/generated-transaction.d.ts +22 -0
- package/dist/generated-transaction.js +330 -0
- package/dist/hub-client.d.ts +72 -0
- package/dist/hub-client.js +178 -0
- package/dist/init-operation.d.ts +98 -0
- package/dist/init-operation.js +427 -0
- package/dist/init-readiness.d.ts +10 -0
- package/dist/init-readiness.js +50 -0
- package/dist/init-scaffold.d.ts +54 -0
- package/dist/init-scaffold.js +439 -0
- package/dist/init.d.ts +51 -0
- package/dist/init.js +789 -0
- package/dist/mcp-execution.d.ts +15 -0
- package/dist/mcp-execution.js +90 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +41 -0
- package/dist/migration-artifact.d.ts +13 -0
- package/dist/migration-artifact.js +154 -0
- package/dist/operations-client.d.ts +137 -0
- package/dist/operations-client.js +130 -0
- package/dist/output.d.ts +347 -0
- package/dist/output.js +771 -0
- package/dist/project-access.d.ts +18 -0
- package/dist/project-access.js +1 -0
- package/dist/project-connection.d.ts +62 -0
- package/dist/project-connection.js +347 -0
- package/dist/project-execution-client.d.ts +2 -0
- package/dist/project-execution-client.js +33 -0
- package/dist/push-client.d.ts +100 -0
- package/dist/push-client.js +160 -0
- package/dist/release-context.d.ts +13 -0
- package/dist/release-context.js +39 -0
- package/dist/release-schema-client.d.ts +16 -0
- package/dist/release-schema-client.js +33 -0
- package/dist/release-sync.d.ts +36 -0
- package/dist/release-sync.js +31 -0
- package/dist/resolve.d.ts +15 -0
- package/dist/resolve.js +80 -0
- package/dist/runtime-config.d.ts +18 -0
- package/dist/runtime-config.js +86 -0
- package/dist/schema-export.d.ts +10 -0
- package/dist/schema-export.js +28 -0
- package/dist/setup.d.ts +65 -0
- package/dist/setup.js +135 -0
- package/dist/source-git.d.ts +36 -0
- package/dist/source-git.js +197 -0
- package/dist/starter-resolver.d.ts +82 -0
- package/dist/starter-resolver.js +495 -0
- package/dist/sync-transaction.d.ts +29 -0
- package/dist/sync-transaction.js +159 -0
- package/dist/transport.d.ts +8 -0
- package/dist/transport.js +73 -0
- package/examples/sanity/README.md +79 -0
- package/examples/sanity/convert.mjs +228 -0
- package/examples/sanity/fixture/data.ndjson +7 -0
- package/examples/sanity/fixture/mapping.json +44 -0
- package/examples/sanity/fixture/originals/mark.svg +1 -0
- package/examples/sanity/stories/articleBody.story +23 -0
- package/examples/sanity/stories/copy.story +7 -0
- package/examples/sanity/stories/page.story +19 -0
- package/examples/sanity/stories/picture.story +7 -0
- package/examples/sanity/stories/settings.story +9 -0
- package/examples/sanity/stories/stack.story +10 -0
- package/package.json +75 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1520 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Storystick CLI
|
|
4
|
+
*
|
|
5
|
+
* Phase 0: `storystick generate`
|
|
6
|
+
* Reads .story files, resolves composition, generates TypeScript types.
|
|
7
|
+
*
|
|
8
|
+
* Future: `storystick dev` will add file watching + incremental regeneration.
|
|
9
|
+
* The pipeline function below is designed to be callable from both.
|
|
10
|
+
*/
|
|
11
|
+
import { resolveReleaseContext } from './release-context.js';
|
|
12
|
+
import { pushReleaseSchema } from './release-schema-client.js';
|
|
13
|
+
import { syncRelease } from './release-sync.js';
|
|
14
|
+
import { cac } from 'cac';
|
|
15
|
+
import pc from 'picocolors';
|
|
16
|
+
import { existsSync, statSync } from 'fs';
|
|
17
|
+
import { resolve, relative, dirname } from 'path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { parseArgs } from 'node:util';
|
|
21
|
+
import { generateTypes, generateQueryTypes, generatePreviewProvenanceManifest, } from '@storystick/core';
|
|
22
|
+
import { serializeSchema } from '@storystick/core/serializer';
|
|
23
|
+
import { rewrapStorystickError } from './cli-error.js';
|
|
24
|
+
import { assertSchemaPlanAllowed, fetchSchemaChangePlan, fetchSchemaPushToken, pushSchema, schemaPlanFence, schemaToPushBody, } from './push-client.js';
|
|
25
|
+
import { printBackfillRecommendation } from './_backfill-recommendation.js';
|
|
26
|
+
import { changeSpinner, configureOutput, diagnosticToJson, emitJsonResult, exitJsonFailure, exitWithError, formatDiagnostic, getOutputMode, isVerbose, logFinishedStep, logMessage, logOutput, logWarning, promptDeviceAuth, succeedSpinner, withInterrupt, withSlowSpinner, } from './output.js';
|
|
27
|
+
import { commitPreparedGeneration, commitGeneratedFiles, prepareGeneratedFiles, } from './generated-transaction.js';
|
|
28
|
+
import { clearSyncOperation, markSyncOperationPushed, readSyncOperation, writePreparedSyncOperation, } from './sync-transaction.js';
|
|
29
|
+
import { resolveCliRuntimeConfig } from './runtime-config.js';
|
|
30
|
+
import { buildExternalMigrationArtifact } from './migration-artifact.js';
|
|
31
|
+
import { requireProjectConnection, validateProjectTokenReadback, } from './project-connection.js';
|
|
32
|
+
import { runResolve } from './resolve.js';
|
|
33
|
+
export { runResolve } from './resolve.js';
|
|
34
|
+
// ─── Resolve pipeline (shared by generate, push, sync) ───
|
|
35
|
+
/**
|
|
36
|
+
* Run the full pipeline: discover → parse → resolve → generate → write.
|
|
37
|
+
*
|
|
38
|
+
* ADR-072 §A1 (lock T3 / L7): writes are gated on `resolved.success`. A
|
|
39
|
+
* failed resolve no longer writes a partial / stale `dist/` to disk; init
|
|
40
|
+
* and link pick up the gap via `result.success === false` and surface
|
|
41
|
+
* "run manually" hints instead of silent half-bootstrapped projects.
|
|
42
|
+
*/
|
|
43
|
+
export async function runPipeline(options) {
|
|
44
|
+
const { rootDir, storyDirs, outputDir, corePath } = options;
|
|
45
|
+
const prepared = await preparePipeline({ rootDir, storyDirs, corePath });
|
|
46
|
+
if (!prepared.success) {
|
|
47
|
+
return {
|
|
48
|
+
storyCount: prepared.stories.size,
|
|
49
|
+
diagnostics: prepared.diagnostics,
|
|
50
|
+
success: false,
|
|
51
|
+
generatedFiles: [],
|
|
52
|
+
stories: prepared.stories,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const absOutputDir = resolve(rootDir, outputDir);
|
|
56
|
+
const committed = await commitGeneratedFiles(absOutputDir, prepared.generated, {
|
|
57
|
+
check: options.check,
|
|
58
|
+
faultAt: options.generationFaultAt,
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
storyCount: prepared.stories.size,
|
|
62
|
+
diagnostics: prepared.diagnostics,
|
|
63
|
+
success: prepared.success,
|
|
64
|
+
generatedFiles: [...committed.files],
|
|
65
|
+
stories: prepared.stories,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve and generate entirely in memory. `init` and `sync` use this seam to
|
|
70
|
+
* prove the local project is viable before either a remote effect or a local
|
|
71
|
+
* generation pointer is published.
|
|
72
|
+
*/
|
|
73
|
+
export async function preparePipeline(options) {
|
|
74
|
+
const resolved = await runResolve(options);
|
|
75
|
+
return {
|
|
76
|
+
...resolved,
|
|
77
|
+
generated: resolved.success
|
|
78
|
+
? [
|
|
79
|
+
...generateTypes(resolved.stories),
|
|
80
|
+
generateQueryTypes(resolved.stories),
|
|
81
|
+
generatePreviewProvenanceManifest(resolved.stories),
|
|
82
|
+
]
|
|
83
|
+
: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Spinner threshold for push / sync / generate (ADR-072 §D11). Shorter
|
|
88
|
+
* than `init`'s 2000ms — steady-state schema work is sub-second on a
|
|
89
|
+
* small project, so a 800ms gate keeps the UX silent for the common
|
|
90
|
+
* case while still surfacing liveness on the slow paths.
|
|
91
|
+
*/
|
|
92
|
+
const SPINNER_DELAY_MS = 800;
|
|
93
|
+
function renderSchemaPlan(plan) {
|
|
94
|
+
const base = plan.baseVersion?.slice(0, 8) ?? 'none';
|
|
95
|
+
logMessage(`${pc.bold('Schema plan')}: ${plan.classification} (${base} → ${plan.nextVersion.slice(0, 8)})`);
|
|
96
|
+
for (const change of plan.changes) {
|
|
97
|
+
logMessage(pc.dim(` ${change.path}: ${change.message}`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function renderNewBindingNotes(notes) {
|
|
101
|
+
for (const note of notes)
|
|
102
|
+
logWarning(note.message);
|
|
103
|
+
}
|
|
104
|
+
// ─────────────────────────────────────────────
|
|
105
|
+
// CLI
|
|
106
|
+
// ─────────────────────────────────────────────
|
|
107
|
+
export function findCorePath(rootDir) {
|
|
108
|
+
// `--root` selects the consumer project. Resolve its installed core stories
|
|
109
|
+
// before considering the CLI process CWD so monorepo and CI invocations are
|
|
110
|
+
// equivalent to running the command from inside that project.
|
|
111
|
+
const projectPath = resolve(rootDir, 'node_modules', '@storystick', 'core', 'stories', 'core');
|
|
112
|
+
if (existsSync(projectPath))
|
|
113
|
+
return projectPath;
|
|
114
|
+
// Resolve the CLI's declared ESM dependency, including strict pnpm installs.
|
|
115
|
+
// package.json is private to core and is not part of its exports contract.
|
|
116
|
+
try {
|
|
117
|
+
const coreEntry = fileURLToPath(import.meta.resolve('@storystick/core'));
|
|
118
|
+
const coreRoot = resolve(dirname(coreEntry), '..');
|
|
119
|
+
const corePath = resolve(coreRoot, 'stories', 'core');
|
|
120
|
+
if (existsSync(corePath))
|
|
121
|
+
return corePath;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Fall through to the CWD-relative fallback.
|
|
125
|
+
}
|
|
126
|
+
// Fallback: cwd-relative node_modules lookup.
|
|
127
|
+
const nmPath = resolve('node_modules', '@storystick', 'core', 'stories', 'core');
|
|
128
|
+
if (existsSync(nmPath))
|
|
129
|
+
return nmPath;
|
|
130
|
+
// Warn but don't fail — core stories enhance but aren't required.
|
|
131
|
+
logWarning('Core stories not found. Built-in types may be unavailable.');
|
|
132
|
+
return nmPath;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Scan for directories containing .story files.
|
|
136
|
+
* If specific dirs are given, use those. Otherwise scan common locations.
|
|
137
|
+
*/
|
|
138
|
+
function findStoryDirs(rootDir, explicit) {
|
|
139
|
+
if (explicit && explicit.length > 0)
|
|
140
|
+
return explicit;
|
|
141
|
+
// Convention-based discovery (avoid scanning entire project with '.')
|
|
142
|
+
const candidates = [
|
|
143
|
+
'stories',
|
|
144
|
+
'src/stories',
|
|
145
|
+
'content/stories',
|
|
146
|
+
'src',
|
|
147
|
+
'components',
|
|
148
|
+
];
|
|
149
|
+
const found = candidates.filter((dir) => {
|
|
150
|
+
const full = resolve(rootDir, dir);
|
|
151
|
+
return existsSync(full) && statSync(full).isDirectory();
|
|
152
|
+
});
|
|
153
|
+
// Default to 'stories' if nothing found (clearer error than scanning everything)
|
|
154
|
+
return found.length > 0 ? found : ['stories'];
|
|
155
|
+
}
|
|
156
|
+
function envSection(entries) {
|
|
157
|
+
// Two columns + purpose. Column widths sized once based on entries.
|
|
158
|
+
const nameW = Math.max(...entries.map(([n]) => n.length));
|
|
159
|
+
const kindW = Math.max(...entries.map(([, k]) => k.length));
|
|
160
|
+
const lines = entries.map(([n, k, p]) => ` ${n.padEnd(nameW)} ${k.padEnd(kindW)} ${p}`);
|
|
161
|
+
return { title: 'Environment', body: lines.join('\n') };
|
|
162
|
+
}
|
|
163
|
+
const ENV_SCHEMA_PUSH = [
|
|
164
|
+
['STORYSTICK_PROJECT_ID', 'required', 'Project shortId'],
|
|
165
|
+
[
|
|
166
|
+
'NEXT_PUBLIC_STORYSTICK_PROJECT_ID',
|
|
167
|
+
'required',
|
|
168
|
+
'Matching public project shortId',
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
'STORYSTICK_API_KEY',
|
|
172
|
+
'required',
|
|
173
|
+
'Pattern D auth — exchange for short-lived JWT',
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
'NEXT_PUBLIC_STORYSTICK_PUBLISHABLE_KEY',
|
|
177
|
+
'required',
|
|
178
|
+
'Matching browser-safe project key',
|
|
179
|
+
],
|
|
180
|
+
['STORYSTICK_GATEWAY_URL', 'optional', 'Override Pattern D gateway URL'],
|
|
181
|
+
];
|
|
182
|
+
const ENV_INIT = [
|
|
183
|
+
[
|
|
184
|
+
'STORYSTICK_STARTER_CATALOG_URL',
|
|
185
|
+
'optional',
|
|
186
|
+
'Override the HTTPS starter catalog',
|
|
187
|
+
],
|
|
188
|
+
['STORYSTICK_HUB_URL', 'optional', 'Override the account and project Hub'],
|
|
189
|
+
['STORYSTICK_GATEWAY_URL', 'optional', 'Override the project gateway'],
|
|
190
|
+
];
|
|
191
|
+
const ENV_BACKFILL = [
|
|
192
|
+
['STORYSTICK_PROJECT_ID', 'required', 'Project shortId'],
|
|
193
|
+
[
|
|
194
|
+
'NEXT_PUBLIC_STORYSTICK_PROJECT_ID',
|
|
195
|
+
'required',
|
|
196
|
+
'Matching public project shortId',
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
'STORYSTICK_API_KEY',
|
|
200
|
+
'required',
|
|
201
|
+
'Pattern D auth — exchange for short-lived JWT',
|
|
202
|
+
],
|
|
203
|
+
[
|
|
204
|
+
'NEXT_PUBLIC_STORYSTICK_PUBLISHABLE_KEY',
|
|
205
|
+
'required',
|
|
206
|
+
'Matching browser-safe project key',
|
|
207
|
+
],
|
|
208
|
+
['STORYSTICK_GATEWAY_URL', 'optional', 'Override Pattern D gateway URL'],
|
|
209
|
+
];
|
|
210
|
+
const ENV_SLICE_PREVIEWS = [
|
|
211
|
+
['STORYSTICK_PROJECT_ID', 'required', 'Project shortId (or pass --project)'],
|
|
212
|
+
['STORYSTICK_HUB_URL', 'optional', 'Override the account and project Hub'],
|
|
213
|
+
['STORYSTICK_GATEWAY_URL', 'optional', 'Override the project gateway'],
|
|
214
|
+
];
|
|
215
|
+
const cli = cac('storystick');
|
|
216
|
+
cli.command('setup', 'Inspect local project setup without changing files or running scripts')
|
|
217
|
+
.option('--root <dir>', 'Application directory', { default: '.' })
|
|
218
|
+
.option('--remote', 'Also read source and delivery setup using your CLI login')
|
|
219
|
+
.option('--project <shortId>', 'Project to inspect remotely (defaults to local connection)')
|
|
220
|
+
.option('--json', 'Emit structured facts and next actions')
|
|
221
|
+
.action(async (options) => {
|
|
222
|
+
const { runSetup } = await import('./commands/setup.js');
|
|
223
|
+
await runSetup(options);
|
|
224
|
+
});
|
|
225
|
+
cli.command('schema', 'Validate and export the compiled schema locally; does not push or publish')
|
|
226
|
+
.option('--root <dir>', 'Application directory', { default: '.' })
|
|
227
|
+
.option('--stories <dirs>', 'Directories to scan (comma-separated)')
|
|
228
|
+
.option('--out <file>', 'Project-relative schema output', { default: 'generated/storystick-schema.json' })
|
|
229
|
+
.option('--json', 'Emit the output identity as JSON')
|
|
230
|
+
.action(async (options) => {
|
|
231
|
+
configureOutput({ json: options.json === true });
|
|
232
|
+
const { dispose } = withInterrupt();
|
|
233
|
+
try {
|
|
234
|
+
const { exportSchema } = await import('./schema-export.js');
|
|
235
|
+
const rootDir = resolve(options.root);
|
|
236
|
+
const result = await exportSchema({ rootDir, storyDirs: findStoryDirs(rootDir, options.stories?.split(',').map((s) => s.trim())), corePath: findCorePath(rootDir), out: options.out });
|
|
237
|
+
if (getOutputMode() === 'json')
|
|
238
|
+
emitJsonResult({ command: 'schema', ok: true, data: result });
|
|
239
|
+
else
|
|
240
|
+
logFinishedStep(`Exported ${result.storyCount} stories to ${result.path}`);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (getOutputMode() === 'json')
|
|
244
|
+
exitJsonFailure({ command: 'schema', err });
|
|
245
|
+
exitWithError(err);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
dispose();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
cli
|
|
252
|
+
.command('generate', 'Generate TypeScript types from .story files')
|
|
253
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
254
|
+
.option('--output <dir>', 'Output directory for generated types', {
|
|
255
|
+
default: 'generated/storystick',
|
|
256
|
+
})
|
|
257
|
+
.option('--stories <dirs>', 'Directories to scan (comma-separated)')
|
|
258
|
+
.option('--verbose', 'Render info-level diagnostics and warning details')
|
|
259
|
+
.option('--quiet', 'Suppress success and warning output (failures still emit)')
|
|
260
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
261
|
+
.option('--check', 'Check generated artifacts without modifying files')
|
|
262
|
+
.example(' $ storystick generate')
|
|
263
|
+
.example(' $ storystick generate --root apps/web --output src/types/storystick')
|
|
264
|
+
.example(' $ storystick generate --stories stories,storybook-stories')
|
|
265
|
+
.action(async (options) => {
|
|
266
|
+
configureOutput({
|
|
267
|
+
json: options.json === true,
|
|
268
|
+
verbose: options.verbose === true,
|
|
269
|
+
quiet: options.quiet === true,
|
|
270
|
+
});
|
|
271
|
+
// Canonical command shell (ADR-072 §D2). Inline by design — `dispose()`
|
|
272
|
+
// is semantic-critical and stays visible at the call site. `signal`
|
|
273
|
+
// becomes load-bearing once Batch 7 wires exit 130 (D13) into the
|
|
274
|
+
// parse path; pre-D13 it's just a no-op handle that disposes cleanly.
|
|
275
|
+
const { signal, dispose } = withInterrupt();
|
|
276
|
+
void signal;
|
|
277
|
+
try {
|
|
278
|
+
const rootDir = resolve(options.root);
|
|
279
|
+
const storyDirs = findStoryDirs(rootDir, options.stories?.split(',').map((s) => s.trim()));
|
|
280
|
+
const corePath = findCorePath(rootDir);
|
|
281
|
+
logMessage(`${pc.bold('Storystick')} — generating types...`);
|
|
282
|
+
logMessage(pc.dim(`Root: ${rootDir}`));
|
|
283
|
+
logMessage(pc.dim(`Stories: ${storyDirs.join(', ')}`));
|
|
284
|
+
logMessage(pc.dim(`Core: ${relative(rootDir, corePath) || corePath}`));
|
|
285
|
+
// ADR-072 §D11: spinner only when the work is actually slow
|
|
286
|
+
// (>800ms). Async I/O foundation (Batch 0) is what makes the
|
|
287
|
+
// setTimeout fire — pre-Batch-0 the parse path blocked the event
|
|
288
|
+
// loop and the spinner never rendered.
|
|
289
|
+
const result = await withSlowSpinner('Generating types…', SPINNER_DELAY_MS, () => runPipeline({
|
|
290
|
+
rootDir,
|
|
291
|
+
storyDirs,
|
|
292
|
+
outputDir: options.output,
|
|
293
|
+
corePath,
|
|
294
|
+
check: options.check === true,
|
|
295
|
+
}));
|
|
296
|
+
// Print diagnostics
|
|
297
|
+
const errors = result.diagnostics.filter((d) => d.severity === 'error');
|
|
298
|
+
const warnings = result.diagnostics.filter((d) => d.severity === 'warning');
|
|
299
|
+
const infos = result.diagnostics.filter((d) => d.severity === 'info');
|
|
300
|
+
// ADR-072 §D15: under --json, emit the structured envelope and skip
|
|
301
|
+
// the human-readable diagnostic / success block (those go to stderr
|
|
302
|
+
// under --json anyway via the muted log primitives).
|
|
303
|
+
if (getOutputMode() === 'json') {
|
|
304
|
+
if (result.success) {
|
|
305
|
+
emitJsonResult({
|
|
306
|
+
command: 'generate',
|
|
307
|
+
ok: true,
|
|
308
|
+
data: {
|
|
309
|
+
storyCount: result.storyCount,
|
|
310
|
+
generatedFiles: result.generatedFiles,
|
|
311
|
+
},
|
|
312
|
+
diagnostics: result.diagnostics.map(diagnosticToJson),
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
exitJsonFailure({
|
|
317
|
+
command: 'generate',
|
|
318
|
+
err: new Error(`Generation failed with ${errors.length} error(s)`),
|
|
319
|
+
diagnostics: result.diagnostics.map(diagnosticToJson),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// ADR-072 §D5 / D12: under --verbose, info-level diagnostics are
|
|
323
|
+
// rendered inline; default mode shows them as a "suppressed" count.
|
|
324
|
+
const renderable = isVerbose()
|
|
325
|
+
? [...errors, ...warnings, ...infos]
|
|
326
|
+
: [...errors, ...warnings];
|
|
327
|
+
for (const d of renderable) {
|
|
328
|
+
logMessage(formatDiagnostic(d, rootDir));
|
|
329
|
+
}
|
|
330
|
+
if (!isVerbose() &&
|
|
331
|
+
infos.length > 0 &&
|
|
332
|
+
(errors.length > 0 || warnings.length > 0)) {
|
|
333
|
+
logMessage(pc.dim(` + ${infos.length} info message(s) suppressed`));
|
|
334
|
+
}
|
|
335
|
+
if (result.success) {
|
|
336
|
+
logFinishedStep(`Generated types for ${pc.bold(String(result.storyCount))} stories`);
|
|
337
|
+
for (const f of result.generatedFiles) {
|
|
338
|
+
logMessage(pc.dim(` ${relative(rootDir, f)}`));
|
|
339
|
+
}
|
|
340
|
+
// ADR-072 §D5 + F3: post-success warning count with discoverability
|
|
341
|
+
// hint pointing the user at --verbose for full details. Under
|
|
342
|
+
// --verbose, list the details inline instead of just counting.
|
|
343
|
+
if (warnings.length > 0) {
|
|
344
|
+
if (isVerbose()) {
|
|
345
|
+
for (const w of warnings) {
|
|
346
|
+
logWarning(formatDiagnostic(w, rootDir));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
logWarning(`${warnings.length} warning(s) (use --verbose to see)`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
exitWithError(`Generation failed with ${pc.bold(String(errors.length))} error(s)`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
if (getOutputMode() === 'json') {
|
|
360
|
+
exitJsonFailure({ command: 'generate', err });
|
|
361
|
+
}
|
|
362
|
+
exitWithError(err);
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
dispose();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
// ─────────────────────────────────────────────
|
|
369
|
+
// Push command
|
|
370
|
+
// ─────────────────────────────────────────────
|
|
371
|
+
cli
|
|
372
|
+
.command('push', 'Push resolved schemas to Storystick')
|
|
373
|
+
.option('--release <id>', 'Target an unpublished release (or use the selected CLI release)')
|
|
374
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
375
|
+
.option('--stories <dirs>', 'Directories to scan (comma-separated)')
|
|
376
|
+
.option('--verbose', 'Show endpoint URL, info-level diagnostics, warning details')
|
|
377
|
+
.option('--quiet', 'Suppress success and warning output (failures still emit)')
|
|
378
|
+
.option('--dry-run', 'Plan schema changes without publishing them')
|
|
379
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
380
|
+
.example(' $ storystick push')
|
|
381
|
+
.example(' $ storystick push --dry-run')
|
|
382
|
+
.example(' $ storystick push --verbose')
|
|
383
|
+
.example(' $ storystick push --root apps/blog')
|
|
384
|
+
.action(async (options) => {
|
|
385
|
+
configureOutput({
|
|
386
|
+
json: options.json === true,
|
|
387
|
+
verbose: options.verbose === true,
|
|
388
|
+
quiet: options.quiet === true,
|
|
389
|
+
});
|
|
390
|
+
// Canonical command shell (ADR-072 §D2).
|
|
391
|
+
const { signal, dispose } = withInterrupt();
|
|
392
|
+
void signal;
|
|
393
|
+
try {
|
|
394
|
+
const rootDir = resolve(options.root);
|
|
395
|
+
const storyDirs = findStoryDirs(rootDir, options.stories?.split(',').map((s) => s.trim()));
|
|
396
|
+
const corePath = findCorePath(rootDir);
|
|
397
|
+
const runtime = resolveCliRuntimeConfig({ rootDir });
|
|
398
|
+
const release = resolveReleaseContext(rootDir, runtime.gatewayUrl, options.release);
|
|
399
|
+
if (release) {
|
|
400
|
+
const resolved = await runResolve({ rootDir, storyDirs, corePath });
|
|
401
|
+
if (!resolved.success)
|
|
402
|
+
throw Error(resolved.diagnostics.filter(d => d.severity === 'error').map(d => formatDiagnostic(d, rootDir)).join('\n'));
|
|
403
|
+
const result = await pushReleaseSchema({ context: release, schema: serializeSchema(resolved.stories, resolved.diagnostics), rootDir, dryRun: options.dryRun, signal });
|
|
404
|
+
if (getOutputMode() === 'json')
|
|
405
|
+
emitJsonResult({ command: 'push', ok: true, data: result });
|
|
406
|
+
else
|
|
407
|
+
logMessage(`Release ${release.releaseId}: ${result.status} (${result.version.slice(0, 8)}). Main unchanged.`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const connection = requireProjectConnection(rootDir);
|
|
411
|
+
logMessage(`${pc.bold('Storystick')} — pushing schemas...`);
|
|
412
|
+
logMessage(pc.dim(`Root: ${rootDir}`));
|
|
413
|
+
if (options.verbose) {
|
|
414
|
+
logMessage(pc.dim(`Gateway: ${runtime.gatewayUrl}`));
|
|
415
|
+
}
|
|
416
|
+
// ADR-072 §D11: single spinner that mutates across phases.
|
|
417
|
+
// "Resolving stories…" while the parse path runs, switching to
|
|
418
|
+
// "Pushing schemas…" once the network leg starts. The auto-✔ is
|
|
419
|
+
// suppressed (withSlowSpinner stops cleanly) — the structured
|
|
420
|
+
// success line below is the canonical UX.
|
|
421
|
+
//
|
|
422
|
+
// Diagnostics rendering happens AFTER the spinner stops. ora's
|
|
423
|
+
// concurrent-write handling is robust, but rendering inside the
|
|
424
|
+
// spinner closure interleaves with frame redraws and is harder
|
|
425
|
+
// to reason about. Render once the spinner is gone.
|
|
426
|
+
const phased = await withSlowSpinner('Resolving stories…', SPINNER_DELAY_MS, async () => {
|
|
427
|
+
const resolved = await runResolve({ rootDir, storyDirs, corePath });
|
|
428
|
+
if (!resolved.success) {
|
|
429
|
+
return { kind: 'resolve-failed', resolved };
|
|
430
|
+
}
|
|
431
|
+
changeSpinner('Planning schema changes…');
|
|
432
|
+
const schema = serializeSchema(resolved.stories, resolved.diagnostics);
|
|
433
|
+
const gatewayUrl = runtime.gatewayUrl;
|
|
434
|
+
const token = await fetchSchemaPushToken(gatewayUrl, connection.apiKey, {
|
|
435
|
+
signal,
|
|
436
|
+
});
|
|
437
|
+
validateProjectTokenReadback(token, connection);
|
|
438
|
+
const body = schemaToPushBody(schema);
|
|
439
|
+
const plan = await fetchSchemaChangePlan(gatewayUrl, token, body, {
|
|
440
|
+
signal,
|
|
441
|
+
});
|
|
442
|
+
if (options.dryRun === true) {
|
|
443
|
+
return { kind: 'planned', resolved, schema, plan };
|
|
444
|
+
}
|
|
445
|
+
assertSchemaPlanAllowed(plan);
|
|
446
|
+
changeSpinner('Pushing schemas…');
|
|
447
|
+
const pushResult = await pushSchema(gatewayUrl, token, body, schemaPlanFence(plan), { signal });
|
|
448
|
+
return { kind: 'ok', resolved, schema, plan, pushResult };
|
|
449
|
+
});
|
|
450
|
+
// Resolve outcomes — diagnostics first, then either error-exit or
|
|
451
|
+
// continue with the success path.
|
|
452
|
+
const errors = phased.resolved.diagnostics.filter((d) => d.severity === 'error');
|
|
453
|
+
const warnings = phased.resolved.diagnostics.filter((d) => d.severity === 'warning');
|
|
454
|
+
const infos = phased.resolved.diagnostics.filter((d) => d.severity === 'info');
|
|
455
|
+
const renderable = isVerbose()
|
|
456
|
+
? [...errors, ...warnings, ...infos]
|
|
457
|
+
: [...errors, ...warnings];
|
|
458
|
+
for (const d of renderable) {
|
|
459
|
+
logMessage(formatDiagnostic(d, rootDir));
|
|
460
|
+
}
|
|
461
|
+
if (phased.kind === 'resolve-failed') {
|
|
462
|
+
if (getOutputMode() === 'json') {
|
|
463
|
+
exitJsonFailure({
|
|
464
|
+
command: 'push',
|
|
465
|
+
err: new Error(`Push failed: ${errors.length} error(s). Fix errors and try again.`),
|
|
466
|
+
diagnostics: phased.resolved.diagnostics.map(diagnosticToJson),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
exitWithError(`Push failed: ${pc.bold(String(errors.length))} error(s). Fix errors and try again.`);
|
|
470
|
+
}
|
|
471
|
+
// Type narrowing for the rest of the success path.
|
|
472
|
+
const { schema, plan } = phased;
|
|
473
|
+
if (phased.kind === 'planned') {
|
|
474
|
+
if (getOutputMode() === 'json') {
|
|
475
|
+
emitJsonResult({
|
|
476
|
+
command: 'push',
|
|
477
|
+
ok: true,
|
|
478
|
+
data: {
|
|
479
|
+
status: 'planned',
|
|
480
|
+
version: schema.version,
|
|
481
|
+
storyCount: schema.storyCount,
|
|
482
|
+
plan,
|
|
483
|
+
},
|
|
484
|
+
diagnostics: phased.resolved.diagnostics.map(diagnosticToJson),
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
renderSchemaPlan(plan);
|
|
489
|
+
logFinishedStep('Dry run complete; no schema was published.');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const result = phased.pushResult;
|
|
493
|
+
if (getOutputMode() === 'json') {
|
|
494
|
+
emitJsonResult({
|
|
495
|
+
command: 'push',
|
|
496
|
+
ok: true,
|
|
497
|
+
data: {
|
|
498
|
+
status: result.status,
|
|
499
|
+
version: schema.version,
|
|
500
|
+
storyCount: schema.storyCount,
|
|
501
|
+
plan,
|
|
502
|
+
newBindingNotes: result.newBindingNotes,
|
|
503
|
+
recommendBackfill: result.recommendBackfill,
|
|
504
|
+
},
|
|
505
|
+
diagnostics: phased.resolved.diagnostics.map(diagnosticToJson),
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (result.status === 'unchanged') {
|
|
510
|
+
logMessage(pc.dim(`Schemas unchanged (${schema.version.slice(0, 8)})`));
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
logFinishedStep(`Pushed ${pc.bold(String(schema.storyCount))} stories (${schema.version.slice(0, 8)})`);
|
|
514
|
+
}
|
|
515
|
+
renderNewBindingNotes(result.newBindingNotes);
|
|
516
|
+
printBackfillRecommendation(result.recommendBackfill);
|
|
517
|
+
// ADR-072 §D5 + F3: post-success warning count + discoverability hint;
|
|
518
|
+
// under --verbose, list the warning details inline instead.
|
|
519
|
+
if (warnings.length > 0) {
|
|
520
|
+
if (isVerbose()) {
|
|
521
|
+
for (const w of warnings) {
|
|
522
|
+
logWarning(formatDiagnostic(w, rootDir));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
logWarning(`${warnings.length} warning(s) (use --verbose to see)`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
if (getOutputMode() === 'json') {
|
|
532
|
+
exitJsonFailure({ command: 'push', err });
|
|
533
|
+
}
|
|
534
|
+
exitWithError(err);
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
dispose();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
// ─────────────────────────────────────────────
|
|
541
|
+
// Sync command (generate + push)
|
|
542
|
+
// ─────────────────────────────────────────────
|
|
543
|
+
cli
|
|
544
|
+
.command('sync', 'Generate types and push schemas (generate + push in one step)')
|
|
545
|
+
.option('--release <id>', 'Target an unpublished release (or use the selected CLI release)')
|
|
546
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
547
|
+
.option('--output <dir>', 'Output directory for generated types', {
|
|
548
|
+
default: 'generated/storystick',
|
|
549
|
+
})
|
|
550
|
+
.option('--stories <dirs>', 'Directories to scan (comma-separated)')
|
|
551
|
+
.option('--verbose', 'Show endpoint URL, info-level diagnostics, warning details')
|
|
552
|
+
.option('--quiet', 'Suppress success and warning output (failures still emit)')
|
|
553
|
+
.option('--dry-run', 'Plan without staging, publishing, or moving the pointer')
|
|
554
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
555
|
+
.example(' $ storystick sync')
|
|
556
|
+
.example(' $ storystick sync --dry-run')
|
|
557
|
+
.example(' $ storystick sync --verbose')
|
|
558
|
+
.action(async (options) => {
|
|
559
|
+
configureOutput({
|
|
560
|
+
json: options.json === true,
|
|
561
|
+
verbose: options.verbose === true,
|
|
562
|
+
quiet: options.quiet === true,
|
|
563
|
+
});
|
|
564
|
+
const { signal, dispose } = withInterrupt();
|
|
565
|
+
let failureStep = 'generate';
|
|
566
|
+
try {
|
|
567
|
+
const rootDir = resolve(options.root);
|
|
568
|
+
const outputDirectory = resolve(rootDir, options.output);
|
|
569
|
+
const release = resolveReleaseContext(rootDir, resolveCliRuntimeConfig({ rootDir }).gatewayUrl, options.release);
|
|
570
|
+
if (release) {
|
|
571
|
+
const result = await syncRelease({ context: release, rootDir, outputDirectory, dryRun: options.dryRun, signal, prepare: async () => {
|
|
572
|
+
const prepared = await preparePipeline({ rootDir, storyDirs: findStoryDirs(rootDir, options.stories?.split(',').map((s) => s.trim())), corePath: findCorePath(rootDir) });
|
|
573
|
+
if (!prepared.success)
|
|
574
|
+
throw Error(prepared.diagnostics.filter(d => d.severity === 'error').map(d => formatDiagnostic(d, rootDir)).join('\n'));
|
|
575
|
+
return { schema: serializeSchema(prepared.stories, prepared.diagnostics), generated: prepared.generated, storyCount: prepared.stories.size };
|
|
576
|
+
} });
|
|
577
|
+
if (getOutputMode() === 'json')
|
|
578
|
+
emitJsonResult({ command: 'sync', ok: true, data: result });
|
|
579
|
+
else
|
|
580
|
+
logMessage(`Synced release ${release.releaseId}. Main unchanged.`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// A confirmed remote push is the transaction's point of no return. If
|
|
584
|
+
// the previous process stopped after that point, finish the local
|
|
585
|
+
// pointer commit from the durable receipt without touching the network.
|
|
586
|
+
const pending = readSyncOperation(rootDir);
|
|
587
|
+
if (pending?.target)
|
|
588
|
+
throw Error('A release sync is pending. Select its release before resuming.');
|
|
589
|
+
if (pending?.state === 'prepared' &&
|
|
590
|
+
pending.outputDirectory !== outputDirectory) {
|
|
591
|
+
throw new Error(`A prepared sync exists for output ${pending.outputDirectory}. Resume with the same --output value.`);
|
|
592
|
+
}
|
|
593
|
+
if (pending?.state === 'pushed') {
|
|
594
|
+
if (options.dryRun === true) {
|
|
595
|
+
throw new Error('A pushed sync is waiting for its local commit. Run `storystick sync` without --dry-run to resume it.');
|
|
596
|
+
}
|
|
597
|
+
if (pending.outputDirectory !== outputDirectory) {
|
|
598
|
+
throw new Error(`A pushed sync is waiting for output ${pending.outputDirectory}. Resume with the same --output value.`);
|
|
599
|
+
}
|
|
600
|
+
failureStep = 'commit';
|
|
601
|
+
const committed = await commitPreparedGeneration(pending.outputDirectory, pending.generationId);
|
|
602
|
+
clearSyncOperation(rootDir);
|
|
603
|
+
const pushResult = pending.pushResult;
|
|
604
|
+
if (getOutputMode() === 'json') {
|
|
605
|
+
emitJsonResult({
|
|
606
|
+
command: 'sync',
|
|
607
|
+
ok: true,
|
|
608
|
+
data: {
|
|
609
|
+
resumed: true,
|
|
610
|
+
generate: {
|
|
611
|
+
storyCount: pending.storyCount,
|
|
612
|
+
generatedFiles: committed.files,
|
|
613
|
+
},
|
|
614
|
+
push: {
|
|
615
|
+
status: pushResult.status,
|
|
616
|
+
version: pushResult.version,
|
|
617
|
+
storyCount: pending.storyCount,
|
|
618
|
+
newBindingNotes: pushResult.newBindingNotes,
|
|
619
|
+
recommendBackfill: pushResult.recommendBackfill,
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
logFinishedStep(`Resumed sync for ${pc.bold(String(pending.storyCount))} stories (${pending.schemaVersion.slice(0, 8)})`);
|
|
626
|
+
renderNewBindingNotes(pushResult.newBindingNotes);
|
|
627
|
+
printBackfillRecommendation(pushResult.recommendBackfill);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const storyDirs = findStoryDirs(rootDir, options.stories?.split(',').map((s) => s.trim()));
|
|
631
|
+
const corePath = findCorePath(rootDir);
|
|
632
|
+
const runtime = resolveCliRuntimeConfig({ rootDir });
|
|
633
|
+
const connection = requireProjectConnection(rootDir);
|
|
634
|
+
logMessage(`${pc.bold('Storystick')} — sync (generate + push)...`);
|
|
635
|
+
logMessage(pc.dim(`Root: ${rootDir}`));
|
|
636
|
+
if (options.verbose) {
|
|
637
|
+
logMessage(pc.dim(`Gateway: ${runtime.gatewayUrl}`));
|
|
638
|
+
}
|
|
639
|
+
const phased = await withSlowSpinner('Resolving stories…', SPINNER_DELAY_MS, async () => {
|
|
640
|
+
const prepared = await preparePipeline({
|
|
641
|
+
rootDir,
|
|
642
|
+
storyDirs,
|
|
643
|
+
corePath,
|
|
644
|
+
});
|
|
645
|
+
if (!prepared.success) {
|
|
646
|
+
return { kind: 'generate-failed', prepared };
|
|
647
|
+
}
|
|
648
|
+
const schema = serializeSchema(prepared.stories, prepared.diagnostics);
|
|
649
|
+
const gatewayUrl = runtime.gatewayUrl;
|
|
650
|
+
failureStep = 'plan';
|
|
651
|
+
changeSpinner('Planning schema changes…');
|
|
652
|
+
const token = await fetchSchemaPushToken(gatewayUrl, connection.apiKey, {
|
|
653
|
+
signal,
|
|
654
|
+
});
|
|
655
|
+
validateProjectTokenReadback(token, connection);
|
|
656
|
+
const body = schemaToPushBody(schema);
|
|
657
|
+
const plan = await fetchSchemaChangePlan(gatewayUrl, token, body, {
|
|
658
|
+
signal,
|
|
659
|
+
});
|
|
660
|
+
if (options.dryRun === true) {
|
|
661
|
+
return { kind: 'planned', prepared, schema, plan };
|
|
662
|
+
}
|
|
663
|
+
assertSchemaPlanAllowed(plan);
|
|
664
|
+
failureStep = 'generate';
|
|
665
|
+
changeSpinner('Staging generated types…');
|
|
666
|
+
const staged = await prepareGeneratedFiles(outputDirectory, prepared.generated);
|
|
667
|
+
const receipt = writePreparedSyncOperation({
|
|
668
|
+
rootDir,
|
|
669
|
+
outputDirectory,
|
|
670
|
+
schemaVersion: schema.version,
|
|
671
|
+
generationId: staged.generationId,
|
|
672
|
+
storyCount: prepared.stories.size,
|
|
673
|
+
});
|
|
674
|
+
failureStep = 'push';
|
|
675
|
+
changeSpinner('Pushing schemas…');
|
|
676
|
+
const pushResult = await pushSchema(gatewayUrl, token, body, schemaPlanFence(plan), { signal });
|
|
677
|
+
markSyncOperationPushed(receipt, pushResult);
|
|
678
|
+
failureStep = 'commit';
|
|
679
|
+
changeSpinner('Committing generated types…');
|
|
680
|
+
const committed = await commitPreparedGeneration(outputDirectory, staged.generationId);
|
|
681
|
+
clearSyncOperation(rootDir);
|
|
682
|
+
return {
|
|
683
|
+
kind: 'ok',
|
|
684
|
+
prepared,
|
|
685
|
+
schema,
|
|
686
|
+
plan,
|
|
687
|
+
committed,
|
|
688
|
+
pushResult,
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
const errors = phased.prepared.diagnostics.filter((d) => d.severity === 'error');
|
|
692
|
+
const warnings = phased.prepared.diagnostics.filter((d) => d.severity === 'warning');
|
|
693
|
+
const infos = phased.prepared.diagnostics.filter((d) => d.severity === 'info');
|
|
694
|
+
const renderable = isVerbose()
|
|
695
|
+
? [...errors, ...warnings, ...infos]
|
|
696
|
+
: [...errors, ...warnings];
|
|
697
|
+
for (const d of renderable) {
|
|
698
|
+
logMessage(formatDiagnostic(d, rootDir));
|
|
699
|
+
}
|
|
700
|
+
if (phased.kind === 'generate-failed') {
|
|
701
|
+
// ADR-072 §D7: disambiguate which step in the sync pipeline failed.
|
|
702
|
+
if (getOutputMode() === 'json') {
|
|
703
|
+
exitJsonFailure({
|
|
704
|
+
command: 'sync',
|
|
705
|
+
err: new Error(`Sync failed at generate step: ${errors.length} error(s)`),
|
|
706
|
+
diagnostics: phased.prepared.diagnostics.map(diagnosticToJson),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
exitWithError(`Sync failed at generate step: ${pc.bold(String(errors.length))} error(s)`);
|
|
710
|
+
}
|
|
711
|
+
const { prepared, schema, plan } = phased;
|
|
712
|
+
if (phased.kind === 'planned') {
|
|
713
|
+
if (getOutputMode() === 'json') {
|
|
714
|
+
emitJsonResult({
|
|
715
|
+
command: 'sync',
|
|
716
|
+
ok: true,
|
|
717
|
+
data: {
|
|
718
|
+
status: 'planned',
|
|
719
|
+
generate: { storyCount: prepared.stories.size },
|
|
720
|
+
push: {
|
|
721
|
+
version: schema.version,
|
|
722
|
+
storyCount: schema.storyCount,
|
|
723
|
+
plan,
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
diagnostics: prepared.diagnostics.map(diagnosticToJson),
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
renderSchemaPlan(plan);
|
|
731
|
+
logFinishedStep('Dry run complete; no generated artifacts or schema were published.');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const { committed, pushResult } = phased;
|
|
735
|
+
if (getOutputMode() === 'json') {
|
|
736
|
+
emitJsonResult({
|
|
737
|
+
command: 'sync',
|
|
738
|
+
ok: true,
|
|
739
|
+
data: {
|
|
740
|
+
generate: {
|
|
741
|
+
storyCount: prepared.stories.size,
|
|
742
|
+
generatedFiles: committed.files,
|
|
743
|
+
},
|
|
744
|
+
push: {
|
|
745
|
+
status: pushResult.status,
|
|
746
|
+
version: schema.version,
|
|
747
|
+
storyCount: schema.storyCount,
|
|
748
|
+
plan,
|
|
749
|
+
newBindingNotes: pushResult.newBindingNotes,
|
|
750
|
+
recommendBackfill: pushResult.recommendBackfill,
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
diagnostics: prepared.diagnostics.map(diagnosticToJson),
|
|
754
|
+
});
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
logFinishedStep(`Generated types for ${pc.bold(String(prepared.stories.size))} stories`);
|
|
758
|
+
for (const file of committed.files) {
|
|
759
|
+
logMessage(pc.dim(` ${relative(rootDir, file)}`));
|
|
760
|
+
}
|
|
761
|
+
if (pushResult.status === 'unchanged') {
|
|
762
|
+
logMessage(pc.dim(`Schemas unchanged (${schema.version.slice(0, 8)})`));
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
logFinishedStep(`Pushed ${pc.bold(String(schema.storyCount))} stories (${schema.version.slice(0, 8)})`);
|
|
766
|
+
}
|
|
767
|
+
renderNewBindingNotes(pushResult.newBindingNotes);
|
|
768
|
+
printBackfillRecommendation(pushResult.recommendBackfill);
|
|
769
|
+
if (warnings.length > 0) {
|
|
770
|
+
if (isVerbose()) {
|
|
771
|
+
for (const warning of warnings) {
|
|
772
|
+
logWarning(formatDiagnostic(warning, rootDir));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
logWarning(`${warnings.length} warning(s) (use --verbose to see)`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
const wrapped = err instanceof Error && !/^Sync failed at /.test(err.message)
|
|
782
|
+
? rewrapStorystickError(`Sync failed at ${failureStep} step: ${err.message}`, err)
|
|
783
|
+
: err;
|
|
784
|
+
if (getOutputMode() === 'json') {
|
|
785
|
+
exitJsonFailure({ command: 'sync', err: wrapped });
|
|
786
|
+
}
|
|
787
|
+
exitWithError(wrapped);
|
|
788
|
+
}
|
|
789
|
+
finally {
|
|
790
|
+
dispose();
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
// ─────────────────────────────────────────────
|
|
794
|
+
// Auth commands (device-auth — SPEC-1 Item #2)
|
|
795
|
+
// ─────────────────────────────────────────────
|
|
796
|
+
cli
|
|
797
|
+
.command('login', 'Log in to Storystick via your browser (device-auth flow)')
|
|
798
|
+
.option('--hub-url <url>', 'Hub base URL (defaults to https://storystick.app)')
|
|
799
|
+
.option('--gateway-url <url>', 'Credential issuer origin (HTTPS; HTTP loopback for local development)')
|
|
800
|
+
.example(' $ storystick login')
|
|
801
|
+
.action(async (options) => {
|
|
802
|
+
configureOutput({});
|
|
803
|
+
const { deviceAuthLogin, loadAuth } = await import('./auth-device.js');
|
|
804
|
+
// Default device name to the machine's hostname so the approval page
|
|
805
|
+
// reads "Authorize CLI on tanguy-mbp?" rather than an opaque id.
|
|
806
|
+
const { hostname } = await import('node:os');
|
|
807
|
+
const deviceName = hostname();
|
|
808
|
+
const abortController = new AbortController();
|
|
809
|
+
const onInt = () => abortController.abort();
|
|
810
|
+
process.on('SIGINT', onInt);
|
|
811
|
+
try {
|
|
812
|
+
await deviceAuthLogin({
|
|
813
|
+
hubUrl: options.hubUrl,
|
|
814
|
+
gatewayUrl: options.gatewayUrl,
|
|
815
|
+
deviceName,
|
|
816
|
+
signal: abortController.signal,
|
|
817
|
+
onPrompt: promptDeviceAuth,
|
|
818
|
+
});
|
|
819
|
+
const stored = loadAuth(resolveCliRuntimeConfig({ gatewayUrl: options.gatewayUrl }).gatewayUrl);
|
|
820
|
+
const who = stored?.email ?? 'your account';
|
|
821
|
+
// Lands ✔ in place of the "Waiting for authorization…" spinner
|
|
822
|
+
// frame — one active line per ADR-061 rule 1, no flicker.
|
|
823
|
+
succeedSpinner(`Logged in as ${pc.bold(who)}`);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
exitWithError(err, 'Login failed.');
|
|
827
|
+
}
|
|
828
|
+
finally {
|
|
829
|
+
process.off('SIGINT', onInt);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
cli
|
|
833
|
+
.command('logout', 'Revoke the current Storystick CLI session and remove local auth')
|
|
834
|
+
.option('--local', 'Forget local auth without revoking the remote session')
|
|
835
|
+
.option('--gateway-url <url>', 'Issuer whose CLI session should be revoked')
|
|
836
|
+
.option('--root <dir>', 'Project directory for environment selection')
|
|
837
|
+
.example(' $ storystick logout')
|
|
838
|
+
.example(' $ storystick logout --local')
|
|
839
|
+
.action(async (options) => {
|
|
840
|
+
configureOutput({});
|
|
841
|
+
const { signal, dispose } = withInterrupt();
|
|
842
|
+
try {
|
|
843
|
+
const { logoutAuthSession } = await import('./auth-device.js');
|
|
844
|
+
const result = await logoutAuthSession({
|
|
845
|
+
signal,
|
|
846
|
+
localOnly: options.local === true,
|
|
847
|
+
gatewayUrl: options.gatewayUrl,
|
|
848
|
+
rootDir: options.root,
|
|
849
|
+
});
|
|
850
|
+
if (result === 'local-only') {
|
|
851
|
+
logFinishedStep('Local CLI session forgotten.');
|
|
852
|
+
logWarning('The remote session was not revoked.');
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (result === 'revoked') {
|
|
856
|
+
logFinishedStep('Logged out and CLI session revoked.');
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
logFinishedStep('No local CLI session found.');
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
catch (err) {
|
|
863
|
+
exitWithError(err, 'The local session was kept so revocation can be retried.');
|
|
864
|
+
}
|
|
865
|
+
finally {
|
|
866
|
+
dispose();
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
cli
|
|
870
|
+
.command('whoami', 'Show the currently logged-in user')
|
|
871
|
+
.option('--gateway-url <url>', 'Issuer whose CLI session should be inspected')
|
|
872
|
+
.option('--root <dir>', 'Project directory for environment selection')
|
|
873
|
+
.option('--json', 'Emit JSON to stdout')
|
|
874
|
+
.example(' $ storystick whoami')
|
|
875
|
+
.example(' $ storystick whoami --json')
|
|
876
|
+
.action(async (options) => {
|
|
877
|
+
configureOutput({
|
|
878
|
+
json: options.json === true,
|
|
879
|
+
verbose: options.verbose === true,
|
|
880
|
+
quiet: options.quiet === true,
|
|
881
|
+
});
|
|
882
|
+
const { whoami } = await import('./auth-device.js');
|
|
883
|
+
const info = await whoami({ gatewayUrl: options.gatewayUrl, rootDir: options.root });
|
|
884
|
+
if (getOutputMode() === 'json') {
|
|
885
|
+
// ADR-072 §D15: { userId, email } | null on success.
|
|
886
|
+
emitJsonResult({
|
|
887
|
+
command: 'whoami',
|
|
888
|
+
ok: true,
|
|
889
|
+
data: info ? { userId: info.userId, email: info.email } : null,
|
|
890
|
+
});
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (info) {
|
|
894
|
+
const who = info.email ?? info.userId;
|
|
895
|
+
logMessage(`Logged in as ${pc.bold(who)}`);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
logMessage(`Not logged in. Run ${pc.bold('storystick login')} to authenticate.`);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
// ─────────────────────────────────────────────
|
|
902
|
+
// Init command (SPEC-1 Item #1 — create / --link <shortId> / interactive)
|
|
903
|
+
// ─────────────────────────────────────────────
|
|
904
|
+
cli
|
|
905
|
+
.command('init [project-name]', 'Create a new Storystick project or link an existing one')
|
|
906
|
+
.option('--link <shortId>', 'Link to an existing project instead of creating a new one')
|
|
907
|
+
.option('--team <teamShortId>', 'Create the project in this Team (required for multiple Teams outside a TTY)')
|
|
908
|
+
.option('--json', 'Emit a JSON envelope on stdout; never prompt')
|
|
909
|
+
.option('--starter <name>', 'Starter ID from the configured Storystick catalog')
|
|
910
|
+
.option('--into <dir>', 'Create or scaffold into this directory')
|
|
911
|
+
.option('--origin <origin>', 'Allowed HTTP(S) application origin')
|
|
912
|
+
.option('--locale <tag>', 'Primary BCP 47 site locale')
|
|
913
|
+
.option('--site-url <origin>', 'Production origin for canonical URLs')
|
|
914
|
+
.option('--force', 'Overwrite known Storystick files in an existing Next app')
|
|
915
|
+
.example(' $ storystick init my-blog # create new project')
|
|
916
|
+
.option('--new-team <name>', 'Create a new team for this project')
|
|
917
|
+
.example(' $ storystick init my-blog --team a3k9m2x7')
|
|
918
|
+
.example(' $ storystick init my-blog --team a3k9m2x7 --locale en --json')
|
|
919
|
+
.example(' $ storystick init --link a3k9m2x7 # link existing project by shortId')
|
|
920
|
+
.example(' $ storystick init my-blog --into . --origin http://localhost:4000')
|
|
921
|
+
.example(' $ storystick init # interactive: new or link')
|
|
922
|
+
.example(' $ storystick init my-blog --starter minimal-next --locale en')
|
|
923
|
+
.action(async (projectName, options) => {
|
|
924
|
+
const { runInit } = await import('./init.js');
|
|
925
|
+
await runInit(projectName, {
|
|
926
|
+
link: options.link,
|
|
927
|
+
starter: options.starter,
|
|
928
|
+
into: options.into,
|
|
929
|
+
origin: options.origin,
|
|
930
|
+
locale: options.locale,
|
|
931
|
+
siteUrl: options.siteUrl,
|
|
932
|
+
force: options.force,
|
|
933
|
+
team: options.team,
|
|
934
|
+
newTeam: options.newTeam,
|
|
935
|
+
json: options.json,
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
// ─────────────────────────────────────────────
|
|
939
|
+
// User-scoped parity commands (SPEC-1 Item #4)
|
|
940
|
+
// ─────────────────────────────────────────────
|
|
941
|
+
cli
|
|
942
|
+
.command('list', 'List all projects you belong to')
|
|
943
|
+
.alias('ls')
|
|
944
|
+
.option('--json', 'Emit JSON to stdout')
|
|
945
|
+
.option('--all', 'Include archived projects')
|
|
946
|
+
.example(' $ storystick list')
|
|
947
|
+
.example(' $ storystick ls --all')
|
|
948
|
+
.example(' $ storystick list --json')
|
|
949
|
+
.action(async (options) => {
|
|
950
|
+
const { runList } = await import('./commands/list.js');
|
|
951
|
+
await runList({ json: options.json === true, all: options.all === true });
|
|
952
|
+
});
|
|
953
|
+
cli
|
|
954
|
+
.command('info [shortId]', 'Show details for the active project, or for the given shortId')
|
|
955
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
956
|
+
.option('--json', 'Emit JSON to stdout')
|
|
957
|
+
.example(' $ storystick info # active project (from .env.local)')
|
|
958
|
+
.example(' $ storystick info a3k9m2x7 # specific project by shortId')
|
|
959
|
+
.example(' $ storystick info --json')
|
|
960
|
+
.action(async (shortId, options) => {
|
|
961
|
+
const { runInfo } = await import('./commands/info.js');
|
|
962
|
+
await runInfo(shortId, {
|
|
963
|
+
root: options.root,
|
|
964
|
+
json: options.json === true,
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
cli
|
|
968
|
+
.command('connect <shortId>', 'Provision and atomically write a complete project connection')
|
|
969
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
970
|
+
.option('--origin <url>', 'Explicit HTTP(S) origin to append')
|
|
971
|
+
.option('--dry-run', 'Show the redacted connection plan without mutating')
|
|
972
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
973
|
+
.example(' $ storystick connect a3k9m2x7')
|
|
974
|
+
.example(' $ storystick connect a3k9m2x7 --root apps/web')
|
|
975
|
+
.example(' $ storystick connect a3k9m2x7 --origin https://app.example.com')
|
|
976
|
+
.example(' $ storystick connect a3k9m2x7 --dry-run')
|
|
977
|
+
.action(async (shortId, options) => {
|
|
978
|
+
const { runConnect } = await import('./commands/connect.js');
|
|
979
|
+
await runConnect(shortId, {
|
|
980
|
+
root: options.root,
|
|
981
|
+
origin: options.origin,
|
|
982
|
+
dryRun: options.dryRun === true,
|
|
983
|
+
json: options.json === true,
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
cli
|
|
987
|
+
.command('archive <shortId>', 'Soft-delete a project (recoverable for 30 days)')
|
|
988
|
+
.option('--yes', 'Skip the confirmation prompt (required when non-interactive or --json)')
|
|
989
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
990
|
+
.example(' $ storystick archive a3k9m2x7')
|
|
991
|
+
.example(' $ storystick archive a3k9m2x7 --yes # CI / non-TTY')
|
|
992
|
+
.action(async (shortId, options) => {
|
|
993
|
+
const { runArchive } = await import('./commands/archive.js');
|
|
994
|
+
await runArchive(shortId, {
|
|
995
|
+
json: options.json === true,
|
|
996
|
+
yes: options.yes === true,
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
// ─────────────────────────────────────────────
|
|
1000
|
+
// Content import and schema migrations
|
|
1001
|
+
// ─────────────────────────────────────────────
|
|
1002
|
+
cli
|
|
1003
|
+
.command('content <action> [target]', 'Plan, apply, resume and verify a content import')
|
|
1004
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
1005
|
+
.option('--project <shortId>', 'Target Storystick project')
|
|
1006
|
+
.option('--dry-run', 'Prepare a server plan without creating or editing content')
|
|
1007
|
+
.option('--apply', 'Apply the previously reviewed plan')
|
|
1008
|
+
.option('--plan <digest>', 'Exact server plan digest')
|
|
1009
|
+
.option('--resume <runId>', 'Resume an existing import run')
|
|
1010
|
+
.option('--new-run', 'Prepare a fresh plan after cancellation or a conflict')
|
|
1011
|
+
.option('--renew <uploadSessionId>', 'Explicitly renew an expired or failed asset upload')
|
|
1012
|
+
.option('--json', 'Emit JSON to stdout')
|
|
1013
|
+
.action(async (action, target, options) => {
|
|
1014
|
+
const { runContent } = await import('./commands/content.js');
|
|
1015
|
+
// cac/mri coerces numeric-looking options before its type transforms.
|
|
1016
|
+
// Native string parsing preserves leading zeroes and long digest values.
|
|
1017
|
+
const names = ['root', 'project', 'plan', 'resume', 'renew'];
|
|
1018
|
+
const raw = parseArgs({ options: Object.fromEntries(names.map(name => [name, { type: 'string' }])), strict: false, allowPositionals: true }).values;
|
|
1019
|
+
await runContent(action, target, { ...options, ...Object.fromEntries(names.filter(name => typeof raw[name] === 'string').map(name => [name, raw[name]])) });
|
|
1020
|
+
});
|
|
1021
|
+
cli
|
|
1022
|
+
.command('migrate <action> [...identifiers]', 'Analyze, prepare, activate, and inspect a schema migration')
|
|
1023
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
1024
|
+
.option('--stories <dirs>', 'Directories to scan (comma-separated)')
|
|
1025
|
+
.option('--recipe <id>', 'Native recipe id', {
|
|
1026
|
+
default: 'text-to-single-line-richtext',
|
|
1027
|
+
})
|
|
1028
|
+
.option('--migration <file>', 'Client-owned migration module for controlled external execution')
|
|
1029
|
+
.option('--bindings <paths>', 'Comma-separated schema field definition paths', {
|
|
1030
|
+
default: 'stories.page.fields.title',
|
|
1031
|
+
})
|
|
1032
|
+
.option('--cursor <cursor>', 'Read cursor returned by the server')
|
|
1033
|
+
.option('--limit <count>', 'Issues per page', { default: 50 })
|
|
1034
|
+
.option('--preparation <id>', 'Exact preparation id')
|
|
1035
|
+
.option('--phase <phase>', 'prepared or serving')
|
|
1036
|
+
.option('--level <level>', 'manual-attestation or adapter-readback', {
|
|
1037
|
+
default: 'manual-attestation',
|
|
1038
|
+
})
|
|
1039
|
+
.option('--artifact <id>', 'Artifact identity')
|
|
1040
|
+
.option('--digest <digest>', 'Artifact digest')
|
|
1041
|
+
.option('--schema-version <version>', 'Target schema version')
|
|
1042
|
+
.option('--generation <id>', 'Target generation id')
|
|
1043
|
+
.option('--surfaces <list>', 'Comma-separated controlled surfaces')
|
|
1044
|
+
.option('--references <list>', 'Comma-separated proof references')
|
|
1045
|
+
.option('--request-id <id>', 'Retry-safe idempotency key')
|
|
1046
|
+
.option('--run <id>', 'Select an existing run when recent-run discovery is ambiguous')
|
|
1047
|
+
.option('--app-url <url>', 'Candidate application origin for build identity readback')
|
|
1048
|
+
.option('--serving-url <url>', 'Public application origin to prove after promotion')
|
|
1049
|
+
.option('--preview-proof <reference>', 'Reference to a manually checked B Preview')
|
|
1050
|
+
.option('--yes', 'Confirm authority transfer without an interactive prompt')
|
|
1051
|
+
.option('--verbose', 'Show additional diagnostics')
|
|
1052
|
+
.option('--quiet', 'Suppress success and warning output')
|
|
1053
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
1054
|
+
.example(' $ storystick migrate analyze --bindings stories.page.fields.title')
|
|
1055
|
+
.example(' $ storystick migrate prepare <runId> <analysisId>')
|
|
1056
|
+
.example(' $ storystick migrate status [runId]')
|
|
1057
|
+
.example(' $ storystick migrate pair-build <runId> --phase prepared --app-url <url>')
|
|
1058
|
+
.action(async (action, identifiers, options) => {
|
|
1059
|
+
configureOutput({
|
|
1060
|
+
json: options.json === true,
|
|
1061
|
+
verbose: options.verbose === true,
|
|
1062
|
+
quiet: options.quiet === true,
|
|
1063
|
+
});
|
|
1064
|
+
const args = Array.isArray(identifiers) ? identifiers : [];
|
|
1065
|
+
const failUsage = (message) => {
|
|
1066
|
+
const error = new Error(message);
|
|
1067
|
+
if (getOutputMode() === 'json') {
|
|
1068
|
+
exitJsonFailure({ command: `migrate ${action}`, err: error });
|
|
1069
|
+
}
|
|
1070
|
+
return exitWithError(error);
|
|
1071
|
+
};
|
|
1072
|
+
const expectArguments = (names, optional = false) => {
|
|
1073
|
+
const minimum = optional ? 0 : names.length;
|
|
1074
|
+
if (args.length < minimum || args.length > names.length) {
|
|
1075
|
+
const rendered = names
|
|
1076
|
+
.map((name) => (optional ? `[${name}]` : `<${name}>`))
|
|
1077
|
+
.join(' ');
|
|
1078
|
+
failUsage(`Usage: storystick migrate ${action}${rendered ? ` ${rendered}` : ''}`);
|
|
1079
|
+
}
|
|
1080
|
+
return args;
|
|
1081
|
+
};
|
|
1082
|
+
const common = {
|
|
1083
|
+
root: options.root,
|
|
1084
|
+
requestId: options.requestId,
|
|
1085
|
+
json: options.json === true,
|
|
1086
|
+
verbose: options.verbose === true,
|
|
1087
|
+
quiet: options.quiet === true,
|
|
1088
|
+
};
|
|
1089
|
+
const migrations = await import('./commands/migrate.js');
|
|
1090
|
+
if (action === 'preview') {
|
|
1091
|
+
const [modulePath] = expectArguments(['module'], true);
|
|
1092
|
+
if (modulePath && options.migration)
|
|
1093
|
+
failUsage('Use either a positional module or --migration, not both.');
|
|
1094
|
+
const rootDir = resolve(options.root);
|
|
1095
|
+
try {
|
|
1096
|
+
const storyDirs = findStoryDirs(rootDir, options.stories?.split(',').map((entry) => entry.trim()));
|
|
1097
|
+
const resolved = await runResolve({ rootDir, storyDirs, corePath: findCorePath(rootDir) });
|
|
1098
|
+
if (!resolved.success)
|
|
1099
|
+
throw new Error('Target schema resolution failed; no migration run was created.');
|
|
1100
|
+
const targetSchema = serializeSchema(resolved.stories, resolved.diagnostics);
|
|
1101
|
+
const migration = modulePath ?? options.migration;
|
|
1102
|
+
const artifact = migration ? await buildExternalMigrationArtifact(rootDir, migration) : undefined;
|
|
1103
|
+
const recipe = artifact ? undefined : {
|
|
1104
|
+
id: options.recipe,
|
|
1105
|
+
version: 1,
|
|
1106
|
+
bindings: String(options.bindings).split(',').map((schemaPath) => ({ schemaPath: schemaPath.trim() }))
|
|
1107
|
+
.filter((binding) => binding.schemaPath.length > 0),
|
|
1108
|
+
};
|
|
1109
|
+
await migrations.runMigratePreview(targetSchema, { artifact, recipe, runId: options.run }, { ...common, root: rootDir });
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
if (getOutputMode() === 'json')
|
|
1113
|
+
exitJsonFailure({ command: 'migrate preview', err: error });
|
|
1114
|
+
exitWithError(error);
|
|
1115
|
+
}
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (action === 'build') {
|
|
1119
|
+
expectArguments([]);
|
|
1120
|
+
if (!options.migration) {
|
|
1121
|
+
failUsage('Usage: storystick migrate build --migration <file>');
|
|
1122
|
+
}
|
|
1123
|
+
await migrations.runMigrateBuild(options.migration, {
|
|
1124
|
+
...common,
|
|
1125
|
+
root: resolve(options.root),
|
|
1126
|
+
});
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (action === 'analyze') {
|
|
1130
|
+
expectArguments([]);
|
|
1131
|
+
const rootDir = resolve(options.root);
|
|
1132
|
+
try {
|
|
1133
|
+
const storyDirs = findStoryDirs(rootDir, options.stories?.split(',').map((entry) => entry.trim()));
|
|
1134
|
+
const resolved = await runResolve({
|
|
1135
|
+
rootDir,
|
|
1136
|
+
storyDirs,
|
|
1137
|
+
corePath: findCorePath(rootDir),
|
|
1138
|
+
});
|
|
1139
|
+
if (!resolved.success) {
|
|
1140
|
+
throw new Error('Target schema resolution failed; no migration run was created.');
|
|
1141
|
+
}
|
|
1142
|
+
const targetSchema = serializeSchema(resolved.stories, resolved.diagnostics);
|
|
1143
|
+
if (options.migration) {
|
|
1144
|
+
const artifact = await buildExternalMigrationArtifact(rootDir, options.migration);
|
|
1145
|
+
await migrations.runMigrateAnalyzeExternal(targetSchema, artifact, {
|
|
1146
|
+
...common,
|
|
1147
|
+
root: rootDir,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
const recipe = {
|
|
1152
|
+
id: options.recipe,
|
|
1153
|
+
version: 1,
|
|
1154
|
+
bindings: String(options.bindings)
|
|
1155
|
+
.split(',')
|
|
1156
|
+
.map((schemaPath) => ({ schemaPath: schemaPath.trim() }))
|
|
1157
|
+
.filter((binding) => binding.schemaPath.length > 0),
|
|
1158
|
+
};
|
|
1159
|
+
await migrations.runMigrateAnalyze(targetSchema, recipe, { ...common, root: rootDir });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
if (getOutputMode() === 'json') {
|
|
1164
|
+
exitJsonFailure({ command: 'migrate analyze', err: error });
|
|
1165
|
+
}
|
|
1166
|
+
exitWithError(error);
|
|
1167
|
+
}
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (action === 'prepare') {
|
|
1171
|
+
const [runId, analysisId] = expectArguments(['runId', 'analysisId']);
|
|
1172
|
+
const artifact = options.migration
|
|
1173
|
+
? await buildExternalMigrationArtifact(resolve(options.root), options.migration)
|
|
1174
|
+
: undefined;
|
|
1175
|
+
await migrations.runMigratePrepare(runId, analysisId, {
|
|
1176
|
+
...common,
|
|
1177
|
+
...(artifact ? { artifact } : {}),
|
|
1178
|
+
});
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (action === 'status') {
|
|
1182
|
+
const [runId] = expectArguments(['runId'], true);
|
|
1183
|
+
await migrations.runMigrateStatus(runId, common);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (action === 'issues') {
|
|
1187
|
+
const [runId, analysisId] = expectArguments(['runId', 'analysisId']);
|
|
1188
|
+
await migrations.runMigrateIssues(runId, analysisId, {
|
|
1189
|
+
...common,
|
|
1190
|
+
cursor: options.cursor,
|
|
1191
|
+
limit: Number(options.limit),
|
|
1192
|
+
});
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (action === 'resume' || action === 'abort') {
|
|
1196
|
+
const [runId] = expectArguments(['runId']);
|
|
1197
|
+
await migrations.runMigrateRunCommand(action, runId, common);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (action === 'pair-build') {
|
|
1201
|
+
const [runId] = expectArguments(['runId']);
|
|
1202
|
+
if (options.phase !== 'prepared' && options.phase !== 'serving') {
|
|
1203
|
+
failUsage('Pass --phase prepared or --phase serving.');
|
|
1204
|
+
}
|
|
1205
|
+
const appUrl = options.phase === 'serving'
|
|
1206
|
+
? options.servingUrl ?? options.appUrl : options.appUrl;
|
|
1207
|
+
if (!appUrl)
|
|
1208
|
+
failUsage('Pass --app-url <url> (or --serving-url for serving).');
|
|
1209
|
+
const rootDir = resolve(options.root);
|
|
1210
|
+
try {
|
|
1211
|
+
const storyDirs = findStoryDirs(rootDir, options.stories?.split(',').map((entry) => entry.trim()));
|
|
1212
|
+
const resolved = await runResolve({ rootDir, storyDirs,
|
|
1213
|
+
corePath: findCorePath(rootDir) });
|
|
1214
|
+
if (!resolved.success)
|
|
1215
|
+
throw new Error('Application schema resolution failed; no build was paired.');
|
|
1216
|
+
await migrations.runMigratePairBuild(runId, serializeSchema(resolved.stories, resolved.diagnostics), { phase: options.phase, appUrl, previewProof: options.previewProof,
|
|
1217
|
+
requestId: options.requestId }, { ...common, root: rootDir });
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
if (getOutputMode() === 'json')
|
|
1221
|
+
exitJsonFailure({ command: 'migrate pair-build', err: error });
|
|
1222
|
+
exitWithError(error);
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (action === 'attest') {
|
|
1227
|
+
const [runId] = expectArguments(['runId']);
|
|
1228
|
+
const required = [
|
|
1229
|
+
'preparation',
|
|
1230
|
+
'phase',
|
|
1231
|
+
'artifact',
|
|
1232
|
+
'digest',
|
|
1233
|
+
'schemaVersion',
|
|
1234
|
+
'generation',
|
|
1235
|
+
'surfaces',
|
|
1236
|
+
'references',
|
|
1237
|
+
];
|
|
1238
|
+
const missing = required.filter((name) => !options[name]);
|
|
1239
|
+
if (missing.length > 0) {
|
|
1240
|
+
failUsage(`Missing required option(s): ${missing.join(', ')}`);
|
|
1241
|
+
}
|
|
1242
|
+
await migrations.runMigrateAttest({
|
|
1243
|
+
runId,
|
|
1244
|
+
preparationId: options.preparation,
|
|
1245
|
+
phase: options.phase,
|
|
1246
|
+
level: options.level,
|
|
1247
|
+
artifactId: options.artifact,
|
|
1248
|
+
artifactDigest: options.digest,
|
|
1249
|
+
schemaVersion: options.schemaVersion,
|
|
1250
|
+
generationId: options.generation,
|
|
1251
|
+
surfaces: String(options.surfaces)
|
|
1252
|
+
.split(',')
|
|
1253
|
+
.map((value) => value.trim()),
|
|
1254
|
+
references: String(options.references)
|
|
1255
|
+
.split(',')
|
|
1256
|
+
.map((value) => value.trim()),
|
|
1257
|
+
}, common);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (action === 'activate') {
|
|
1261
|
+
if (args.length <= 1 && (args[0] || options.migration || options.run)) {
|
|
1262
|
+
const [modulePath] = expectArguments(['module'], true);
|
|
1263
|
+
if (modulePath && options.migration)
|
|
1264
|
+
failUsage('Use either a positional module or --migration, not both.');
|
|
1265
|
+
const rootDir = resolve(options.root);
|
|
1266
|
+
try {
|
|
1267
|
+
const storyDirs = findStoryDirs(rootDir, options.stories?.split(',').map((entry) => entry.trim()));
|
|
1268
|
+
const resolved = await runResolve({ rootDir, storyDirs, corePath: findCorePath(rootDir) });
|
|
1269
|
+
if (!resolved.success)
|
|
1270
|
+
throw new Error('Target schema resolution failed; activation was not attempted.');
|
|
1271
|
+
const targetSchema = serializeSchema(resolved.stories, resolved.diagnostics);
|
|
1272
|
+
const migration = modulePath ?? options.migration;
|
|
1273
|
+
const artifact = migration ? await buildExternalMigrationArtifact(rootDir, migration) : undefined;
|
|
1274
|
+
const recipe = artifact ? undefined : {
|
|
1275
|
+
id: options.recipe,
|
|
1276
|
+
version: 1,
|
|
1277
|
+
bindings: String(options.bindings).split(',').map((schemaPath) => ({ schemaPath: schemaPath.trim() }))
|
|
1278
|
+
.filter((binding) => binding.schemaPath.length > 0),
|
|
1279
|
+
};
|
|
1280
|
+
await migrations.runMigrateGuidedActivate(targetSchema, {
|
|
1281
|
+
artifact, recipe, runId: options.run,
|
|
1282
|
+
appUrl: options.appUrl, servingUrl: options.servingUrl,
|
|
1283
|
+
previewProof: options.previewProof,
|
|
1284
|
+
yes: options.yes === true,
|
|
1285
|
+
}, { ...common, root: rootDir });
|
|
1286
|
+
}
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
if (getOutputMode() === 'json')
|
|
1289
|
+
exitJsonFailure({ command: 'migrate activate', err: error });
|
|
1290
|
+
exitWithError(error);
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const [runId, preparationId, evidenceId] = expectArguments([
|
|
1295
|
+
'runId',
|
|
1296
|
+
'preparationId',
|
|
1297
|
+
'evidenceId',
|
|
1298
|
+
]);
|
|
1299
|
+
await migrations.runMigrateActivate(runId, preparationId, evidenceId, common);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (action === 'receipt') {
|
|
1303
|
+
const [runId] = expectArguments(['runId']);
|
|
1304
|
+
await migrations.runMigrateReceipt(runId, common);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
failUsage(`Unknown migration action "${action}". Expected preview, analyze, prepare, status, issues, resume, abort, attest, activate, or receipt.`);
|
|
1308
|
+
});
|
|
1309
|
+
cli
|
|
1310
|
+
.command('backfill-refs', 'Re-extract live references for the active project')
|
|
1311
|
+
.option('--storyType <name>', 'Limit to one story type')
|
|
1312
|
+
.option('--document <shortId>', 'Limit to one document')
|
|
1313
|
+
.option('--dry-run', 'Count what would change without writing')
|
|
1314
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
1315
|
+
.option('--verbose', 'Render info-level diagnostics')
|
|
1316
|
+
.option('--quiet', 'Suppress success and warning output (failures still emit)')
|
|
1317
|
+
.option('--json', 'Emit JSONL events on stdout (one per chunk + a final event)')
|
|
1318
|
+
.example(' $ storystick backfill-refs # whole project')
|
|
1319
|
+
.example(' $ storystick backfill-refs --storyType blogPost')
|
|
1320
|
+
.example(' $ storystick backfill-refs --document a3k9m2x7')
|
|
1321
|
+
.example(' $ storystick backfill-refs --dry-run')
|
|
1322
|
+
.action(async (options) => {
|
|
1323
|
+
const { runBackfillRefs } = await import('./commands/backfill-refs.js');
|
|
1324
|
+
await runBackfillRefs({
|
|
1325
|
+
storyType: options.storyType,
|
|
1326
|
+
documentId: options.document,
|
|
1327
|
+
dryRun: options.dryRun === true,
|
|
1328
|
+
root: options.root,
|
|
1329
|
+
json: options.json === true,
|
|
1330
|
+
verbose: options.verbose === true,
|
|
1331
|
+
quiet: options.quiet === true,
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
cli
|
|
1335
|
+
.command('source <operation> [workspaceId]', 'Source: configure, prepare, release, status, stop, recover, export, apply, import, github-status, github-inspect, github-prepare, github-import, propose')
|
|
1336
|
+
.option('--root <dir>', 'Local repository or application directory', { default: '.' })
|
|
1337
|
+
.option('--project <shortId>', 'Connected Storystick project')
|
|
1338
|
+
.option('--config <file>', 'Preparation scope and recipe (default .storystick/source.json)')
|
|
1339
|
+
.option('--schema <file>', 'Compiled schema for the single-app Next.js source preset')
|
|
1340
|
+
.option('--ref <commit>', 'Committed local ref (default HEAD), or immutable GitHub commit for github-*')
|
|
1341
|
+
.option('--app-root <path>', 'Repository-relative application folder for github-inspect')
|
|
1342
|
+
.option('--connection-revision <revision>', 'GitHub connection revision returned by github-inspect')
|
|
1343
|
+
.option('--repair-command <command>', 'For recover only: repair unsaved source in the retained sandbox before checkpointing (uses workspace budget)')
|
|
1344
|
+
.option('--repair-cwd <path>', 'Repository-relative directory for the recovery command')
|
|
1345
|
+
.option('--out <directory>', 'New export directory or independent integration checkout')
|
|
1346
|
+
.option('--change <directory>', 'Verified source export to apply locally or propose on GitHub')
|
|
1347
|
+
.option('--expected-revision <revision>', 'Workspace revision observed before export or import')
|
|
1348
|
+
.option('--request-id <id>', 'Stable operation identity for retries')
|
|
1349
|
+
.option('--dry-run', 'Validate and inspect a source preparation locally without uploading')
|
|
1350
|
+
.option('--json', 'Emit a JSON envelope')
|
|
1351
|
+
.action(async (operation, workspaceId, options) => {
|
|
1352
|
+
const { runSource } = await import('./commands/source.js');
|
|
1353
|
+
await runSource(operation, workspaceId, options);
|
|
1354
|
+
});
|
|
1355
|
+
cli
|
|
1356
|
+
.command('slice-previews sync', 'Sync Slice Library captures declared in a repository manifest')
|
|
1357
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
1358
|
+
.option('--manifest <file>', 'Manifest path relative to root', {
|
|
1359
|
+
default: 'slice-previews.json',
|
|
1360
|
+
})
|
|
1361
|
+
.option('--project <shortId>', 'Project shortId (defaults to ProjectConnection)')
|
|
1362
|
+
.option('--dry-run', 'Validate and list captures without uploading')
|
|
1363
|
+
.option('--verbose', 'Show manifest details')
|
|
1364
|
+
.option('--quiet', 'Suppress success and warning output')
|
|
1365
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
1366
|
+
.example(' $ storystick slice-previews sync')
|
|
1367
|
+
.example(' $ storystick slice-previews sync --dry-run --json')
|
|
1368
|
+
.action(async (options) => {
|
|
1369
|
+
const { runSlicePreviewsSync } = await import('./commands/slice-previews.js');
|
|
1370
|
+
await runSlicePreviewsSync({
|
|
1371
|
+
root: options.root,
|
|
1372
|
+
manifest: options.manifest,
|
|
1373
|
+
projectShortId: options.project,
|
|
1374
|
+
dryRun: options.dryRun === true,
|
|
1375
|
+
verbose: options.verbose === true,
|
|
1376
|
+
quiet: options.quiet === true,
|
|
1377
|
+
json: options.json === true,
|
|
1378
|
+
});
|
|
1379
|
+
});
|
|
1380
|
+
cli
|
|
1381
|
+
.command('mcp', 'Serve project capabilities over MCP stdio')
|
|
1382
|
+
.option('--project <shortId>', 'Project this server may access (required)')
|
|
1383
|
+
.option('--write', 'Enable verified draft writes and Undo with your editor role')
|
|
1384
|
+
.option('--root <dir>', 'Directory for gateway configuration', { default: '.' })
|
|
1385
|
+
.action(async (options) => {
|
|
1386
|
+
configureOutput({ json: false });
|
|
1387
|
+
const { signal, dispose } = withInterrupt();
|
|
1388
|
+
try {
|
|
1389
|
+
const { runProjectMcp } = await import('./mcp.js');
|
|
1390
|
+
await runProjectMcp({ project: options.project, root: options.root, write: options.write === true, signal });
|
|
1391
|
+
}
|
|
1392
|
+
catch (err) {
|
|
1393
|
+
exitWithError(err);
|
|
1394
|
+
}
|
|
1395
|
+
finally {
|
|
1396
|
+
dispose();
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
cli.help((sections) => {
|
|
1400
|
+
// cac runs this single global callback for every `outputHelp` —
|
|
1401
|
+
// top-level AND per-command. We detect which by examining the Usage
|
|
1402
|
+
// section's body (set at runtime to `$ <cli> <usageText|rawName>`).
|
|
1403
|
+
const commandsIdx = sections.findIndex((s) => s.title === 'Commands');
|
|
1404
|
+
if (commandsIdx >= 0) {
|
|
1405
|
+
// Top-level `--help`. After the Commands list, splice in a short
|
|
1406
|
+
// categorization so first-time users see the shape of the CLI at a
|
|
1407
|
+
// glance instead of a flat list.
|
|
1408
|
+
sections.splice(commandsIdx + 1, 0, {
|
|
1409
|
+
title: 'Command groups',
|
|
1410
|
+
body: [
|
|
1411
|
+
pc.bold(' Schema') + ' generate, push, sync',
|
|
1412
|
+
pc.bold(' Migrations') +
|
|
1413
|
+
' migrate analyze, prepare, status, issues, resume, abort, attest, pair-build, activate, receipt',
|
|
1414
|
+
pc.bold(' Auth') + ' login, logout, whoami',
|
|
1415
|
+
pc.bold(' Projects') + ' init, list, info, connect, archive',
|
|
1416
|
+
pc.bold(' Refs') + ' backfill-refs',
|
|
1417
|
+
pc.bold(' Slice library') + ' slice-previews sync',
|
|
1418
|
+
pc.bold(' Assistants') + ' mcp',
|
|
1419
|
+
].join('\n'),
|
|
1420
|
+
});
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
// ADR-072 §F1: append an Environment: section for the schema/refs
|
|
1424
|
+
// commands so AI agents and CI scripts see the env-var contract on
|
|
1425
|
+
// `<cmd> --help` without having to read the source. cac doesn't
|
|
1426
|
+
// surface the matched command to its global helpCallback, so we sniff
|
|
1427
|
+
// it off the Usage line ("$ storystick <name> ...").
|
|
1428
|
+
const usage = sections.find((s) => s.title === 'Usage');
|
|
1429
|
+
if (!usage)
|
|
1430
|
+
return;
|
|
1431
|
+
const body = usage.body;
|
|
1432
|
+
if (body.includes(' init')) {
|
|
1433
|
+
sections.push(envSection(ENV_INIT));
|
|
1434
|
+
}
|
|
1435
|
+
else if (body.includes(' push') || body.includes(' sync')) {
|
|
1436
|
+
sections.push(envSection(ENV_SCHEMA_PUSH));
|
|
1437
|
+
}
|
|
1438
|
+
else if (body.includes(' backfill-refs')) {
|
|
1439
|
+
sections.push(envSection(ENV_BACKFILL));
|
|
1440
|
+
}
|
|
1441
|
+
else if (body.includes(' slice-previews sync')) {
|
|
1442
|
+
sections.push(envSection(ENV_SLICE_PREVIEWS));
|
|
1443
|
+
}
|
|
1444
|
+
else if (body.includes(' mcp')) {
|
|
1445
|
+
sections.push(envSection([
|
|
1446
|
+
['STORYSTICK_GATEWAY_URL', 'optional', 'Project gateway override; also read from --root/.env.local'],
|
|
1447
|
+
['XDG_CONFIG_HOME', 'optional', 'Operator session directory; sign in with storystick login first'],
|
|
1448
|
+
]));
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
const cliPackage = createRequire(import.meta.url)('../package.json');
|
|
1452
|
+
cli.version(cliPackage.version);
|
|
1453
|
+
cli.command('release <operation> [value]', 'Manage releases and prepare, review or publish a coordinated release')
|
|
1454
|
+
.option('--approval <digest>', 'Confirm the exact reviewed publication')
|
|
1455
|
+
.option('--request-id <id>', 'Stable request identity for publication preparation')
|
|
1456
|
+
.option('--root <dir>', 'Project root directory', { default: '.' })
|
|
1457
|
+
.option('--project <id>', 'Project short ID')
|
|
1458
|
+
.option('--json', 'Emit a JSON envelope on stdout')
|
|
1459
|
+
.action(async (operation, value, options) => {
|
|
1460
|
+
const { runRelease } = await import('./commands/release.js');
|
|
1461
|
+
await runRelease(operation, value, options);
|
|
1462
|
+
});
|
|
1463
|
+
cli.command('connection <operation>', 'Connect a project to GitHub or Vercel without deploying')
|
|
1464
|
+
.option('--root <dir>', 'Website folder')
|
|
1465
|
+
.option('--project <shortId>', 'Storystick project')
|
|
1466
|
+
.option('--provider <provider>', 'github or vercel')
|
|
1467
|
+
.option('--attempt <id>', 'Authorization attempt returned by begin or status')
|
|
1468
|
+
.option('--cursor <cursor>', 'Next resource page')
|
|
1469
|
+
.option('--installation <id>', 'GitHub installation')
|
|
1470
|
+
.option('--repository <id>', 'GitHub repository numeric ID')
|
|
1471
|
+
.option('--full-name <name>', 'GitHub owner/repository')
|
|
1472
|
+
.option('--branch <branch>', 'GitHub source branch')
|
|
1473
|
+
.option('--destination <id>', 'Vercel project ID')
|
|
1474
|
+
.option('--json', 'Machine-readable output; never opens a browser')
|
|
1475
|
+
.action(async (operation, options) => {
|
|
1476
|
+
const { runConnection } = await import('./commands/connection.js');
|
|
1477
|
+
await runConnection(operation, options);
|
|
1478
|
+
});
|
|
1479
|
+
cli.command('delivery <operation>', 'Configure or inspect release hosting; never deploys or publishes')
|
|
1480
|
+
.option('--root <dir>', 'Local application directory', { default: '.' })
|
|
1481
|
+
.option('--project <id>', 'Storystick project short ID')
|
|
1482
|
+
.option('--from-vercel', 'Read team/project IDs from .vercel/project.json (not credentials)')
|
|
1483
|
+
.option('--team <id>', 'Vercel team ID')
|
|
1484
|
+
.option('--destination <id>', 'Vercel project ID')
|
|
1485
|
+
.option('--site-url <origin>', 'Existing HTTPS production origin')
|
|
1486
|
+
.option('--root-directory <path>', 'Deployment root within the source scope')
|
|
1487
|
+
.option('--build-command <command>', 'Deployment build override')
|
|
1488
|
+
.option('--dry-run', 'Inspect the configuration without saving it')
|
|
1489
|
+
.option('--json', 'Emit structured results')
|
|
1490
|
+
.action(async (operation, options) => {
|
|
1491
|
+
const { runDelivery } = await import('./commands/delivery.js');
|
|
1492
|
+
await runDelivery(operation, options);
|
|
1493
|
+
});
|
|
1494
|
+
cli.command('guide', 'Print the Storystick development skill')
|
|
1495
|
+
.option('--reference <name>', 'Read a focused reference: setup or story-and-next')
|
|
1496
|
+
.action(async (options) => {
|
|
1497
|
+
// Readers such as head may close the pipe before the guide finishes.
|
|
1498
|
+
process.stdout.on('error', (error) => {
|
|
1499
|
+
if (error.code !== 'EPIPE')
|
|
1500
|
+
throw error;
|
|
1501
|
+
});
|
|
1502
|
+
configureOutput({});
|
|
1503
|
+
const { dispose } = withInterrupt();
|
|
1504
|
+
try {
|
|
1505
|
+
if (options.reference && !['setup', 'story-and-next'].includes(options.reference))
|
|
1506
|
+
throw new Error('Unknown reference. Choose setup or story-and-next.');
|
|
1507
|
+
const { readFile } = await import('node:fs/promises');
|
|
1508
|
+
const { fileURLToPath } = await import('node:url');
|
|
1509
|
+
const path = options.reference ? `references/${options.reference}.md` : 'SKILL.md';
|
|
1510
|
+
const guide = new URL(`../assets/skills/storystick-development/${path}`, import.meta.url);
|
|
1511
|
+
logOutput(`# Guide source: ${fileURLToPath(guide)}\n${await readFile(guide, 'utf8')}`);
|
|
1512
|
+
}
|
|
1513
|
+
catch (err) {
|
|
1514
|
+
exitWithError(err);
|
|
1515
|
+
}
|
|
1516
|
+
finally {
|
|
1517
|
+
dispose();
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
cli.parse();
|