@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.
- 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
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { isContentShapeBuildId } from '@storystick/types';
|
|
4
|
+
import { querySchemaFingerprintFromStories } from '@storystick/types/query-schema-fingerprint';
|
|
5
|
+
import { canonicalConversationJson } from '@storystick/core/canonical-json';
|
|
6
|
+
import { abortSchemaChange, activateSchemaChange, analyzeSchemaChange, attestSchemaChange, getSchemaChange, getSchemaChangeIssues, getSchemaChangeReceipt, listSchemaChanges, nextSchemaChangeProposalBatch, pairCompatibleBuild, prepareSchemaChange, resumeSchemaChange, submitSchemaChangeProposalBatch, } from '../operations-client.js';
|
|
7
|
+
import { fetchSchemaChangePlan, schemaToPushBody } from '../push-client.js';
|
|
8
|
+
import { callHubApi } from '../hub-client.js';
|
|
9
|
+
import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logFinishedStep, logMessage, logWarning, promptConfirm, promptText, withInterrupt, } from '../output.js';
|
|
10
|
+
import { resolveCliRuntimeConfig } from '../runtime-config.js';
|
|
11
|
+
import { requireProjectConnection, validateProjectTokenReadback, } from '../project-connection.js';
|
|
12
|
+
import { buildExternalMigrationArtifact, executeExternalMigrationBatch, } from '../migration-artifact.js';
|
|
13
|
+
async function session(options, signal) {
|
|
14
|
+
const rootDir = resolve(options.root ?? '.');
|
|
15
|
+
const connection = requireProjectConnection(rootDir);
|
|
16
|
+
const gatewayUrl = resolveCliRuntimeConfig({
|
|
17
|
+
rootDir,
|
|
18
|
+
gatewayUrl: options.gatewayUrl,
|
|
19
|
+
}).gatewayUrl;
|
|
20
|
+
const issued = await callHubApi('/api/cli/projects/issue-access-token', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
body: {
|
|
23
|
+
projectShortId: connection.shortId,
|
|
24
|
+
purpose: 'schema-migration',
|
|
25
|
+
},
|
|
26
|
+
gatewayUrl,
|
|
27
|
+
rootDir,
|
|
28
|
+
signal,
|
|
29
|
+
fetchImpl: options.fetchImpl,
|
|
30
|
+
});
|
|
31
|
+
const token = issued.token;
|
|
32
|
+
validateProjectTokenReadback(token, connection);
|
|
33
|
+
return { gatewayUrl, token, projectId: connection.shortId };
|
|
34
|
+
}
|
|
35
|
+
function configure(options) {
|
|
36
|
+
configureOutput({
|
|
37
|
+
json: options.json === true,
|
|
38
|
+
verbose: options.verbose === true,
|
|
39
|
+
quiet: options.quiet === true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function renderRun(command, run) {
|
|
43
|
+
if (getOutputMode() === 'json') {
|
|
44
|
+
emitJsonResult({ command, ok: true, data: run });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
logFinishedStep(`${run.runId} · ${run.phase} · ${run.status}`);
|
|
48
|
+
const progress = run.progress;
|
|
49
|
+
logMessage(pc.dim(`${progress.rootsScanned}/${progress.rootsExpected ?? '?'} roots · ` +
|
|
50
|
+
`${progress.occurrencesChanged}/${progress.occurrencesScanned} occurrences changed · ` +
|
|
51
|
+
`${progress.issuesTotal} issue(s) · ${progress.dirtyRoots} dirty`));
|
|
52
|
+
if (run.preparationId)
|
|
53
|
+
logMessage(pc.dim(`Preparation: ${run.preparationId}`));
|
|
54
|
+
if (run.availableCommands.length > 0) {
|
|
55
|
+
logMessage(pc.dim(`Next: ${run.availableCommands.join(', ')}`));
|
|
56
|
+
}
|
|
57
|
+
if (run.statusUrl)
|
|
58
|
+
logMessage(run.statusUrl);
|
|
59
|
+
if (run.lastError)
|
|
60
|
+
logWarning(`${run.lastError.code}: ${run.lastError.message}`);
|
|
61
|
+
}
|
|
62
|
+
async function shell(command, options, fn, render) {
|
|
63
|
+
configure(options);
|
|
64
|
+
const { signal, dispose } = withInterrupt();
|
|
65
|
+
try {
|
|
66
|
+
const active = await session(options, signal);
|
|
67
|
+
const result = await fn(active, signal);
|
|
68
|
+
render(result);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (signal.aborted)
|
|
72
|
+
process.exit(130);
|
|
73
|
+
if (getOutputMode() === 'json')
|
|
74
|
+
exitJsonFailure({ command, err: error });
|
|
75
|
+
exitWithError(error);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
dispose();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function newMigrationRequestId() {
|
|
82
|
+
return `cli_${crypto.randomUUID()}`;
|
|
83
|
+
}
|
|
84
|
+
export async function runMigrateBuild(migrationPath, options = {}) {
|
|
85
|
+
configure(options);
|
|
86
|
+
try {
|
|
87
|
+
const artifact = await buildExternalMigrationArtifact(resolve(options.root ?? '.'), migrationPath);
|
|
88
|
+
const data = {
|
|
89
|
+
entryPath: artifact.entryPath,
|
|
90
|
+
artifactDigest: artifact.artifactDigest,
|
|
91
|
+
manifest: artifact.manifest,
|
|
92
|
+
bytes: artifact.bytes.byteLength,
|
|
93
|
+
};
|
|
94
|
+
if (getOutputMode() === 'json') {
|
|
95
|
+
emitJsonResult({ command: 'migrate build', ok: true, data });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
logFinishedStep(`${artifact.manifest.id}@${artifact.manifest.version} · ${artifact.artifactDigest}`);
|
|
99
|
+
logMessage(pc.dim(`${artifact.bytes.byteLength} bytes · ${artifact.entryPath}`));
|
|
100
|
+
}
|
|
101
|
+
return artifact;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (getOutputMode() === 'json') {
|
|
105
|
+
exitJsonFailure({ command: 'migrate build', err: error });
|
|
106
|
+
}
|
|
107
|
+
return exitWithError(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function delay(ms, signal) {
|
|
111
|
+
await new Promise((resolveDelay, reject) => {
|
|
112
|
+
const onAbort = () => {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
reject(signal.reason ?? new Error('Migration interrupted.'));
|
|
115
|
+
};
|
|
116
|
+
const timeout = setTimeout(() => {
|
|
117
|
+
signal.removeEventListener('abort', onAbort);
|
|
118
|
+
resolveDelay();
|
|
119
|
+
}, ms);
|
|
120
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function driveExternalProposals(args) {
|
|
124
|
+
const admitted = await getSchemaChange(args.gatewayUrl, args.token, args.runId, { signal: args.signal, fetchImpl: args.fetchImpl });
|
|
125
|
+
assertExternalMigrationArtifact(admitted, args.artifact);
|
|
126
|
+
for (;;) {
|
|
127
|
+
const batch = await nextSchemaChangeProposalBatch(args.gatewayUrl, args.token, args.runId, { signal: args.signal, fetchImpl: args.fetchImpl });
|
|
128
|
+
if (batch) {
|
|
129
|
+
if (batch.programDigest !== admitted.programDigest) {
|
|
130
|
+
throw new Error('SCHEMA_MIGRATION_ARTIFACT_MISMATCH');
|
|
131
|
+
}
|
|
132
|
+
const results = await executeExternalMigrationBatch(args.artifact.rootDir, args.artifact, batch);
|
|
133
|
+
await submitSchemaChangeProposalBatch(args.gatewayUrl, args.token, {
|
|
134
|
+
runId: args.runId,
|
|
135
|
+
batchId: batch.batchId,
|
|
136
|
+
inputDigest: batch.inputDigest,
|
|
137
|
+
programDigest: batch.programDigest,
|
|
138
|
+
results,
|
|
139
|
+
requestId: newMigrationRequestId(),
|
|
140
|
+
}, { signal: args.signal, fetchImpl: args.fetchImpl });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const run = await getSchemaChange(args.gatewayUrl, args.token, args.runId, { signal: args.signal, fetchImpl: args.fetchImpl });
|
|
144
|
+
if (run.status !== 'running') {
|
|
145
|
+
if ((run.proposalBatches?.open ?? 0) === 0)
|
|
146
|
+
return run;
|
|
147
|
+
}
|
|
148
|
+
if (args.deadline !== undefined && Date.now() >= args.deadline)
|
|
149
|
+
return run;
|
|
150
|
+
await delay(250, args.signal);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function assertExternalMigrationArtifact(run, artifact) {
|
|
154
|
+
if (run.program?.kind !== 'external' ||
|
|
155
|
+
canonicalConversationJson(run.program.manifest) !==
|
|
156
|
+
canonicalConversationJson(artifact.manifest)) {
|
|
157
|
+
throw new Error('SCHEMA_MIGRATION_ARTIFACT_MISMATCH');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function assertGuidedProgram(run, target, program) {
|
|
161
|
+
if (run.targetVersion !== target.version)
|
|
162
|
+
throw new Error('The selected run targets a different schema. Re-run preview with the intended source files.');
|
|
163
|
+
if (program.artifact) {
|
|
164
|
+
assertExternalMigrationArtifact(run, program.artifact);
|
|
165
|
+
}
|
|
166
|
+
else if (!program.recipe || canonicalConversationJson(run.recipe) !== canonicalConversationJson(program.recipe)) {
|
|
167
|
+
throw new Error('The selected run uses a different native recipe.');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function findGuidedRun(args) {
|
|
171
|
+
const options = { signal: args.signal, fetchImpl: args.fetchImpl };
|
|
172
|
+
if (args.program.runId) {
|
|
173
|
+
const run = await getSchemaChange(args.gatewayUrl, args.token, args.program.runId, options);
|
|
174
|
+
assertGuidedProgram(run, args.target, args.program);
|
|
175
|
+
return run;
|
|
176
|
+
}
|
|
177
|
+
const recent = await listSchemaChanges(args.gatewayUrl, args.token, 100, options);
|
|
178
|
+
const matches = recent.filter((run) => {
|
|
179
|
+
try {
|
|
180
|
+
assertGuidedProgram(run, args.target, args.program);
|
|
181
|
+
return run.status !== 'aborted';
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
const active = matches.filter((run) => run.status !== 'succeeded');
|
|
188
|
+
if (active.length > 1) {
|
|
189
|
+
throw new Error(`Several matching runs exist (${active.map((run) => run.runId).join(', ')}). Select one with --run.`);
|
|
190
|
+
}
|
|
191
|
+
return active[0] ?? matches[0] ?? null;
|
|
192
|
+
}
|
|
193
|
+
async function waitForGuidedRun(args) {
|
|
194
|
+
const deadline = Date.now() + 180_000;
|
|
195
|
+
for (;;) {
|
|
196
|
+
const run = args.artifact
|
|
197
|
+
? await driveExternalProposals({ ...args, artifact: args.artifact, deadline })
|
|
198
|
+
: await getSchemaChange(args.gatewayUrl, args.token, args.runId, {
|
|
199
|
+
signal: args.signal, fetchImpl: args.fetchImpl,
|
|
200
|
+
});
|
|
201
|
+
if (run.status !== 'running' || Date.now() >= deadline)
|
|
202
|
+
return run;
|
|
203
|
+
await delay(750, args.signal);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Compose the existing durable analysis and preparation; no second server state machine. */
|
|
207
|
+
export async function runMigratePreview(target, program, options = {}) {
|
|
208
|
+
const command = 'migrate preview';
|
|
209
|
+
return shell(command, options, async ({ gatewayUrl, token }, signal) => {
|
|
210
|
+
const transport = { signal, fetchImpl: options.fetchImpl };
|
|
211
|
+
let run = await findGuidedRun({ gatewayUrl, token, target, program, ...transport });
|
|
212
|
+
if (!run) {
|
|
213
|
+
const plan = await fetchSchemaChangePlan(gatewayUrl, token, schemaToPushBody(target), transport);
|
|
214
|
+
if (!plan.baseVersion)
|
|
215
|
+
throw new Error('A migration requires an existing active source schema.');
|
|
216
|
+
run = await analyzeSchemaChange(gatewayUrl, token, {
|
|
217
|
+
expectedBaseVersion: plan.baseVersion,
|
|
218
|
+
targetSchema: target,
|
|
219
|
+
planDigest: plan.planDigest,
|
|
220
|
+
...(program.artifact
|
|
221
|
+
? { program: { kind: 'external', manifest: program.artifact.manifest } }
|
|
222
|
+
: { recipe: program.recipe }),
|
|
223
|
+
requestId: options.requestId ?? newMigrationRequestId(),
|
|
224
|
+
}, transport);
|
|
225
|
+
}
|
|
226
|
+
if (run.status === 'running' || (program.artifact && run.status === 'awaiting_action' && (run.proposalBatches?.open ?? 0) > 0)) {
|
|
227
|
+
run = await waitForGuidedRun({ gatewayUrl, token, runId: run.runId, artifact: program.artifact, ...transport });
|
|
228
|
+
}
|
|
229
|
+
if (run.phase === 'analysis' && run.status === 'awaiting_action' && run.progress.issuesTotal === 0 && run.analysisId) {
|
|
230
|
+
run = await prepareSchemaChange(gatewayUrl, token, {
|
|
231
|
+
runId: run.runId, analysisId: run.analysisId,
|
|
232
|
+
requestId: newMigrationRequestId(),
|
|
233
|
+
}, transport);
|
|
234
|
+
if (run.status === 'running')
|
|
235
|
+
run = await waitForGuidedRun({ gatewayUrl, token, runId: run.runId, artifact: program.artifact, ...transport });
|
|
236
|
+
}
|
|
237
|
+
return run;
|
|
238
|
+
}, (run) => {
|
|
239
|
+
renderRun(command, run);
|
|
240
|
+
if (getOutputMode() === 'json')
|
|
241
|
+
return;
|
|
242
|
+
if (run.phase === 'verification' && run.status === 'awaiting_action' && run.preparationId) {
|
|
243
|
+
logMessage('B is verified in isolation. The application candidate and its Preview still require a human check before activation.');
|
|
244
|
+
}
|
|
245
|
+
else if (run.progress.issuesTotal > 0 && run.analysisId) {
|
|
246
|
+
logWarning(`Resolve the issues in A or the module, then resume: storystick migrate issues ${run.runId} ${run.analysisId}`);
|
|
247
|
+
}
|
|
248
|
+
else if (run.status === 'running') {
|
|
249
|
+
logMessage(`The server is still working. Re-run this command or inspect: storystick migrate status ${run.runId}`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function migrationReadbackEndpoint(appUrl) {
|
|
254
|
+
const app = new URL(appUrl);
|
|
255
|
+
if (!['http:', 'https:'].includes(app.protocol) || app.username || app.password || app.search || app.hash) {
|
|
256
|
+
throw new Error('The application URL must be HTTP(S), without credentials, query or fragment.');
|
|
257
|
+
}
|
|
258
|
+
const base = new URL(app.toString().replace(/\/?$/, '/'));
|
|
259
|
+
return new URL('api/storystick/migration-readback', base).toString();
|
|
260
|
+
}
|
|
261
|
+
export async function readMigrationApplication(args) {
|
|
262
|
+
const url = migrationReadbackEndpoint(args.appUrl);
|
|
263
|
+
const response = await (args.fetchImpl ?? fetch)(url, {
|
|
264
|
+
redirect: 'error',
|
|
265
|
+
cache: 'no-store',
|
|
266
|
+
signal: args.signal ? AbortSignal.any([args.signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000),
|
|
267
|
+
});
|
|
268
|
+
if (!response.ok)
|
|
269
|
+
throw new Error(`Application readback failed (${response.status}) at ${url}.`);
|
|
270
|
+
const body = await response.json();
|
|
271
|
+
const value = body && typeof body === 'object' && !Array.isArray(body)
|
|
272
|
+
? body.migration : undefined;
|
|
273
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
274
|
+
throw new Error('The application did not expose a migration readback. Set STORYSTICK_APP_BUILD_ID and STORYSTICK_APP_BUILD_DIGEST in that build.');
|
|
275
|
+
const readback = value;
|
|
276
|
+
if (readback.projectId !== args.projectId || !isContentShapeBuildId(readback.artifactId) ||
|
|
277
|
+
!/^[a-f0-9]{64}$/.test(readback.artifactDigest) ||
|
|
278
|
+
(readback.schemaVersion !== null && typeof readback.schemaVersion !== 'string') ||
|
|
279
|
+
typeof readback.querySchemaFingerprint !== 'string') {
|
|
280
|
+
throw new Error('Application readback has invalid project or build identity.');
|
|
281
|
+
}
|
|
282
|
+
const expectedFingerprint = querySchemaFingerprintFromStories(args.target.stories);
|
|
283
|
+
if (readback.querySchemaFingerprint !== expectedFingerprint) {
|
|
284
|
+
throw new Error('The deployed application does not contain the target query schema. Deploy the candidate build first.');
|
|
285
|
+
}
|
|
286
|
+
return { url, readback };
|
|
287
|
+
}
|
|
288
|
+
export async function runMigratePairBuild(runId, target, input, options = {}) {
|
|
289
|
+
const command = 'migrate pair-build';
|
|
290
|
+
return shell(command, options, async ({ gatewayUrl, token, projectId }, signal) => {
|
|
291
|
+
const run = await getSchemaChange(gatewayUrl, token, runId, { signal, fetchImpl: options.fetchImpl });
|
|
292
|
+
if (run.status !== 'succeeded')
|
|
293
|
+
throw new Error('Complete the migration before pairing a compatible build.');
|
|
294
|
+
const observed = await readMigrationApplication({
|
|
295
|
+
appUrl: input.appUrl, projectId, target, signal, fetchImpl: options.fetchImpl,
|
|
296
|
+
});
|
|
297
|
+
if (input.phase === 'serving' && !input.previewProof?.trim()) {
|
|
298
|
+
throw new Error('A manual Preview proof reference is required before recording serving evidence.');
|
|
299
|
+
}
|
|
300
|
+
if (input.phase === 'serving' && observed.readback.schemaVersion !== target.version) {
|
|
301
|
+
throw new Error(`The application still reads ${observed.readback.schemaVersion ?? 'no schema'}; expected ${target.version}.`);
|
|
302
|
+
}
|
|
303
|
+
const paired = await pairCompatibleBuild(gatewayUrl, token, {
|
|
304
|
+
runId,
|
|
305
|
+
phase: input.phase,
|
|
306
|
+
artifactId: observed.readback.artifactId,
|
|
307
|
+
artifactDigest: observed.readback.artifactDigest,
|
|
308
|
+
schemaVersion: target.version,
|
|
309
|
+
querySchemaFingerprint: observed.readback.querySchemaFingerprint,
|
|
310
|
+
references: input.phase === 'prepared'
|
|
311
|
+
? [observed.url] : [observed.url, input.previewProof.trim()],
|
|
312
|
+
requestId: input.requestId ?? newMigrationRequestId(),
|
|
313
|
+
}, { signal, fetchImpl: options.fetchImpl });
|
|
314
|
+
return { ...paired, artifactId: observed.readback.artifactId };
|
|
315
|
+
}, (paired) => {
|
|
316
|
+
if (getOutputMode() === 'json') {
|
|
317
|
+
emitJsonResult({ command, ok: true, data: paired });
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
logFinishedStep(`${input.phase} · ${paired.artifactId} · ${paired.schemaVersion.slice(0, 8)}`);
|
|
321
|
+
logMessage(pc.dim(`Evidence: ${paired.evidenceId}`));
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
export async function runMigrateGuidedActivate(target, program, options = {}) {
|
|
326
|
+
const command = 'migrate activate';
|
|
327
|
+
return shell(command, options, async ({ gatewayUrl, token, projectId }, signal) => {
|
|
328
|
+
const transport = { signal, fetchImpl: options.fetchImpl };
|
|
329
|
+
let run = await findGuidedRun({ gatewayUrl, token, target, program, ...transport });
|
|
330
|
+
if (!run)
|
|
331
|
+
throw new Error('No matching migration run. Run `storystick migrate preview` first, or select an older run with --run.');
|
|
332
|
+
if (run.status === 'succeeded')
|
|
333
|
+
return { run, receipt: await getSchemaChangeReceipt(gatewayUrl, token, run.runId, transport), pending: '' };
|
|
334
|
+
if (run.phase !== 'verification' && run.phase !== 'convergence') {
|
|
335
|
+
throw new Error(`Run ${run.runId} is ${run.phase}/${run.status}; complete or resume preparation before activation.`);
|
|
336
|
+
}
|
|
337
|
+
if (run.phase === 'verification' && (run.status !== 'awaiting_action' || !run.preparationId || run.progress.issuesTotal !== 0 || run.progress.dirtyRoots !== 0)) {
|
|
338
|
+
throw new Error(`Run ${run.runId} has no current verified preparation.`);
|
|
339
|
+
}
|
|
340
|
+
if (run.phase === 'verification' && !program.yes && (getOutputMode() === 'json' || !process.stdin.isTTY)) {
|
|
341
|
+
throw new Error('Pass --yes to explicitly approve authority transfer in non-interactive mode.');
|
|
342
|
+
}
|
|
343
|
+
const servingUrl = program.servingUrl ?? (getOutputMode() !== 'json' && process.stdin.isTTY
|
|
344
|
+
? await promptText('Public serving application URL') : undefined);
|
|
345
|
+
if (!servingUrl)
|
|
346
|
+
throw new Error('Pass --serving-url <url> for the public application. The candidate URL is not assumed to be public.');
|
|
347
|
+
migrationReadbackEndpoint(servingUrl);
|
|
348
|
+
const appUrl = program.appUrl ?? (run.phase === 'verification' && getOutputMode() !== 'json' && process.stdin.isTTY
|
|
349
|
+
? await promptText('Candidate application URL') : undefined);
|
|
350
|
+
if (run.phase === 'verification') {
|
|
351
|
+
if (!appUrl)
|
|
352
|
+
throw new Error('Pass --app-url <url> to identify the candidate application.');
|
|
353
|
+
const observed = await readMigrationApplication({ appUrl, projectId, target, ...transport });
|
|
354
|
+
const proof = program.previewProof ?? (getOutputMode() !== 'json' && process.stdin.isTTY
|
|
355
|
+
? await promptText('Reference to your manual B Preview check') : undefined);
|
|
356
|
+
if (!proof?.trim())
|
|
357
|
+
throw new Error('A manual B Preview proof reference is required (--preview-proof).');
|
|
358
|
+
if (!run.activationEvidenceId) {
|
|
359
|
+
run = await attestSchemaChange(gatewayUrl, token, {
|
|
360
|
+
runId: run.runId, preparationId: run.preparationId, phase: 'prepared',
|
|
361
|
+
level: 'manual-attestation', artifactId: observed.readback.artifactId,
|
|
362
|
+
artifactDigest: observed.readback.artifactDigest,
|
|
363
|
+
schemaVersion: run.targetVersion, generationId: run.targetGenerationId,
|
|
364
|
+
surfaces: ['api', 'preview'], references: [observed.url, proof.trim()],
|
|
365
|
+
requestId: newMigrationRequestId(),
|
|
366
|
+
}, transport);
|
|
367
|
+
}
|
|
368
|
+
if (!program.yes) {
|
|
369
|
+
logMessage(`${run.progress.rootsScanned} roots · ${run.progress.occurrencesChanged} changes · ${run.progress.issuesTotal} issues · ${run.progress.dirtyRoots} dirty`);
|
|
370
|
+
logMessage(`A → B · ${run.baseVersion} → ${run.targetVersion} · ${observed.readback.artifactId}`);
|
|
371
|
+
if (!await promptConfirm('Transfer editing authority to B? A promoted candidate build may already read B before the final receipt.', { default: false })) {
|
|
372
|
+
return { run, receipt: null, pending: 'Activation cancelled; the verified preparation remains available.' };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
run = await activateSchemaChange(gatewayUrl, token, {
|
|
376
|
+
runId: run.runId, preparationId: run.preparationId,
|
|
377
|
+
evidenceId: run.activationEvidenceId, requestId: options.requestId ?? newMigrationRequestId(),
|
|
378
|
+
}, transport);
|
|
379
|
+
}
|
|
380
|
+
if (!run.servingEvidenceId) {
|
|
381
|
+
// The endpoint is a real, no-cache query through the deployed SDK. It
|
|
382
|
+
// does not attest every CDN instance or the client's Preview session.
|
|
383
|
+
const serving = await readMigrationApplication({ appUrl: servingUrl, projectId, target, ...transport });
|
|
384
|
+
if (serving.readback.schemaVersion !== run.targetVersion) {
|
|
385
|
+
return { run, receipt: null, pending: `B is active for editing, but ${serving.url} still reads ${serving.readback.schemaVersion ?? 'no schema'}; promote the same build and re-run this command.` };
|
|
386
|
+
}
|
|
387
|
+
run = await attestSchemaChange(gatewayUrl, token, {
|
|
388
|
+
runId: run.runId, preparationId: run.preparationId, phase: 'serving',
|
|
389
|
+
level: 'adapter-readback', artifactId: serving.readback.artifactId,
|
|
390
|
+
artifactDigest: serving.readback.artifactDigest,
|
|
391
|
+
schemaVersion: run.targetVersion, generationId: run.targetGenerationId,
|
|
392
|
+
surfaces: ['api'], references: [serving.url],
|
|
393
|
+
requestId: newMigrationRequestId(),
|
|
394
|
+
}, transport);
|
|
395
|
+
}
|
|
396
|
+
const deadline = Date.now() + 180_000;
|
|
397
|
+
while (run.status === 'running' && Date.now() < deadline) {
|
|
398
|
+
await delay(750, signal);
|
|
399
|
+
run = await getSchemaChange(gatewayUrl, token, run.runId, transport);
|
|
400
|
+
}
|
|
401
|
+
const receipt = await getSchemaChangeReceipt(gatewayUrl, token, run.runId, transport);
|
|
402
|
+
return { run, receipt, pending: receipt ? '' : 'Serving was observed, but projection ACK and the final server receipt are still pending. Re-run this command later.' };
|
|
403
|
+
}, ({ run, receipt, pending }) => {
|
|
404
|
+
if (getOutputMode() === 'json') {
|
|
405
|
+
emitJsonResult({ command, ok: true, data: { run, receipt, pending: pending || null, complete: receipt !== null } });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
renderRun(command, run);
|
|
409
|
+
if (receipt)
|
|
410
|
+
logFinishedStep(`Server receipt: ${receipt.receiptId} · projection ACK ${receipt.projectionAckedThroughSeq}`);
|
|
411
|
+
else if (pending)
|
|
412
|
+
logWarning(pending);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
export async function runMigrateAnalyze(targetSchema, recipe, options = {}) {
|
|
416
|
+
const command = 'migrate analyze';
|
|
417
|
+
return shell(command, options, async ({ gatewayUrl, token }, signal) => {
|
|
418
|
+
const plan = await fetchSchemaChangePlan(gatewayUrl, token, schemaToPushBody(targetSchema), { signal, fetchImpl: options.fetchImpl });
|
|
419
|
+
if (!plan.baseVersion) {
|
|
420
|
+
throw new Error('A migration requires an existing active source schema.');
|
|
421
|
+
}
|
|
422
|
+
return analyzeSchemaChange(gatewayUrl, token, {
|
|
423
|
+
expectedBaseVersion: plan.baseVersion,
|
|
424
|
+
targetSchema,
|
|
425
|
+
planDigest: plan.planDigest,
|
|
426
|
+
recipe,
|
|
427
|
+
requestId: options.requestId ?? newMigrationRequestId(),
|
|
428
|
+
}, { signal, fetchImpl: options.fetchImpl });
|
|
429
|
+
}, (run) => renderRun(command, run));
|
|
430
|
+
}
|
|
431
|
+
export async function runMigrateAnalyzeExternal(targetSchema, artifact, options = {}) {
|
|
432
|
+
const command = 'migrate analyze';
|
|
433
|
+
return shell(command, options, async ({ gatewayUrl, token }, signal) => {
|
|
434
|
+
const plan = await fetchSchemaChangePlan(gatewayUrl, token, schemaToPushBody(targetSchema), { signal, fetchImpl: options.fetchImpl });
|
|
435
|
+
if (!plan.baseVersion) {
|
|
436
|
+
throw new Error('A migration requires an existing active source schema.');
|
|
437
|
+
}
|
|
438
|
+
const started = await analyzeSchemaChange(gatewayUrl, token, {
|
|
439
|
+
expectedBaseVersion: plan.baseVersion,
|
|
440
|
+
targetSchema,
|
|
441
|
+
planDigest: plan.planDigest,
|
|
442
|
+
program: { kind: 'external', manifest: artifact.manifest },
|
|
443
|
+
requestId: options.requestId ?? newMigrationRequestId(),
|
|
444
|
+
}, { signal, fetchImpl: options.fetchImpl });
|
|
445
|
+
return driveExternalProposals({
|
|
446
|
+
gatewayUrl,
|
|
447
|
+
token,
|
|
448
|
+
runId: started.runId,
|
|
449
|
+
artifact,
|
|
450
|
+
signal,
|
|
451
|
+
fetchImpl: options.fetchImpl,
|
|
452
|
+
});
|
|
453
|
+
}, (run) => renderRun(command, run));
|
|
454
|
+
}
|
|
455
|
+
export async function runMigratePrepare(runId, analysisId, options = {}) {
|
|
456
|
+
const command = 'migrate prepare';
|
|
457
|
+
return shell(command, options, async ({ gatewayUrl, token }, signal) => {
|
|
458
|
+
const started = await prepareSchemaChange(gatewayUrl, token, {
|
|
459
|
+
runId,
|
|
460
|
+
analysisId,
|
|
461
|
+
requestId: options.requestId ?? newMigrationRequestId(),
|
|
462
|
+
}, { signal, fetchImpl: options.fetchImpl });
|
|
463
|
+
if (!options.artifact)
|
|
464
|
+
return started;
|
|
465
|
+
return driveExternalProposals({
|
|
466
|
+
gatewayUrl,
|
|
467
|
+
token,
|
|
468
|
+
runId: started.runId,
|
|
469
|
+
artifact: options.artifact,
|
|
470
|
+
signal,
|
|
471
|
+
fetchImpl: options.fetchImpl,
|
|
472
|
+
});
|
|
473
|
+
}, (run) => renderRun(command, run));
|
|
474
|
+
}
|
|
475
|
+
export async function runMigrateRunCommand(action, runId, options = {}) {
|
|
476
|
+
const command = `migrate ${action}`;
|
|
477
|
+
return shell(command, options, ({ gatewayUrl, token }, signal) => (action === 'resume' ? resumeSchemaChange : abortSchemaChange)(gatewayUrl, token, { runId, requestId: options.requestId ?? newMigrationRequestId() }, { signal, fetchImpl: options.fetchImpl }), (run) => renderRun(command, run));
|
|
478
|
+
}
|
|
479
|
+
export async function runMigrateAttest(body, options = {}) {
|
|
480
|
+
const command = 'migrate attest';
|
|
481
|
+
return shell(command, options, ({ gatewayUrl, token }, signal) => attestSchemaChange(gatewayUrl, token, { ...body, requestId: options.requestId ?? newMigrationRequestId() }, { signal, fetchImpl: options.fetchImpl }), (run) => renderRun(command, run));
|
|
482
|
+
}
|
|
483
|
+
export async function runMigrateActivate(runId, preparationId, evidenceId, options = {}) {
|
|
484
|
+
const command = 'migrate activate';
|
|
485
|
+
return shell(command, options, ({ gatewayUrl, token }, signal) => activateSchemaChange(gatewayUrl, token, {
|
|
486
|
+
runId,
|
|
487
|
+
preparationId,
|
|
488
|
+
evidenceId,
|
|
489
|
+
requestId: options.requestId ?? newMigrationRequestId(),
|
|
490
|
+
}, { signal, fetchImpl: options.fetchImpl }), (run) => renderRun(command, run));
|
|
491
|
+
}
|
|
492
|
+
export async function runMigrateStatus(runId, options = {}) {
|
|
493
|
+
const command = 'migrate status';
|
|
494
|
+
return shell(command, options, ({ gatewayUrl, token }, signal) => runId
|
|
495
|
+
? getSchemaChange(gatewayUrl, token, runId, {
|
|
496
|
+
signal,
|
|
497
|
+
fetchImpl: options.fetchImpl,
|
|
498
|
+
})
|
|
499
|
+
: listSchemaChanges(gatewayUrl, token, 20, {
|
|
500
|
+
signal,
|
|
501
|
+
fetchImpl: options.fetchImpl,
|
|
502
|
+
}), (result) => {
|
|
503
|
+
if (Array.isArray(result)) {
|
|
504
|
+
if (getOutputMode() === 'json') {
|
|
505
|
+
emitJsonResult({ command, ok: true, data: result });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (result.length === 0)
|
|
509
|
+
logMessage('No schema-change runs.');
|
|
510
|
+
for (const run of result)
|
|
511
|
+
renderRun(command, run);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
renderRun(command, result);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
export async function runMigrateIssues(runId, analysisId, options = {}) {
|
|
519
|
+
const command = 'migrate issues';
|
|
520
|
+
return shell(command, options, ({ gatewayUrl, token }, signal) => getSchemaChangeIssues(gatewayUrl, token, { runId, analysisId, cursor: options.cursor, limit: options.limit }, { signal, fetchImpl: options.fetchImpl }), (result) => {
|
|
521
|
+
if (getOutputMode() === 'json') {
|
|
522
|
+
emitJsonResult({ command, ok: true, data: result });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
for (const entry of result.page) {
|
|
526
|
+
logWarning(`${entry.code} · ${entry.corpus}/${entry.rootKey}: ${entry.message}`);
|
|
527
|
+
}
|
|
528
|
+
logMessage(pc.dim(`${result.page.length}/${result.total} issue(s) shown`));
|
|
529
|
+
if (result.continueCursor)
|
|
530
|
+
logMessage(pc.dim(`Cursor: ${result.continueCursor}`));
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
export async function runMigrateReceipt(runId, options = {}) {
|
|
534
|
+
const command = 'migrate receipt';
|
|
535
|
+
return shell(command, options, ({ gatewayUrl, token }, signal) => getSchemaChangeReceipt(gatewayUrl, token, runId, {
|
|
536
|
+
signal,
|
|
537
|
+
fetchImpl: options.fetchImpl,
|
|
538
|
+
}), (receipt) => {
|
|
539
|
+
if (getOutputMode() === 'json') {
|
|
540
|
+
emitJsonResult({ command, ok: true, data: receipt });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (!receipt) {
|
|
544
|
+
logMessage('No terminal server receipt is available yet.');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
logFinishedStep(`${receipt.receiptId} · run ${receipt.runId}`);
|
|
548
|
+
logMessage(pc.dim(`Projection ACK: ${receipt.projectionAckedThroughSeq}`));
|
|
549
|
+
});
|
|
550
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { callHubApi } from '../hub-client.js';
|
|
4
|
+
import { resolveCliRuntimeConfig } from '../runtime-config.js';
|
|
5
|
+
import { readReleaseContext, writeReleaseContext } from '../release-context.js';
|
|
6
|
+
import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logMessage, withInterrupt } from '../output.js';
|
|
7
|
+
export async function runRelease(operation, value, options) {
|
|
8
|
+
configureOutput({ json: options.json === true });
|
|
9
|
+
const { signal, dispose } = withInterrupt();
|
|
10
|
+
try {
|
|
11
|
+
const rootDir = resolve(options.root ?? '.'), runtime = resolveCliRuntimeConfig({ rootDir });
|
|
12
|
+
const stored = readReleaseContext(rootDir);
|
|
13
|
+
if (operation === 'use' && value === 'main') {
|
|
14
|
+
if (process.env.STORYSTICK_RELEASE_SESSION)
|
|
15
|
+
throw Error('A sandbox cannot select Main.');
|
|
16
|
+
writeReleaseContext(rootDir, undefined);
|
|
17
|
+
if (getOutputMode() === 'json')
|
|
18
|
+
emitJsonResult({ command: 'release', ok: true, data: { releaseId: null } });
|
|
19
|
+
else
|
|
20
|
+
logMessage('CLI context: Main');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const projectShortId = options.project ?? stored?.projectShortId ?? process.env.STORYSTICK_PROJECT_ID;
|
|
24
|
+
if (!projectShortId)
|
|
25
|
+
throw Error('Specify --project <shortId>.');
|
|
26
|
+
const delivery = ['prepare-publication', 'publication-status', 'publish', 'resume-publication', 'cancel-publication'].includes(operation);
|
|
27
|
+
if (!delivery && !['list', 'read', 'create', 'use'].includes(operation))
|
|
28
|
+
throw Error('Use release list, read <id>, create <name> or use <id|main>.');
|
|
29
|
+
if (operation !== 'list' && !value)
|
|
30
|
+
throw Error('A release ID or name is required.');
|
|
31
|
+
if (operation === 'publish' && !options.approval)
|
|
32
|
+
throw Error('Review release publication-status, then pass --approval <digest> to publish that exact prepared release.');
|
|
33
|
+
if (delivery && process.env.STORYSTICK_RELEASE_SESSION)
|
|
34
|
+
throw Error('Publication requires a human CLI session outside the coding sandbox.');
|
|
35
|
+
const deliveryOperation = { 'prepare-publication': 'prepare', 'publication-status': 'status', publish: 'confirm', 'resume-publication': 'resume', 'cancel-publication': 'cancel' }[operation];
|
|
36
|
+
const request = delivery ? { operation: deliveryOperation, releaseId: value, ...(deliveryOperation === 'prepare' ? { requestId: options.requestId ?? randomUUID() } : {}), ...(deliveryOperation === 'confirm' ? { approval: options.approval } : {}) } : operation === 'list' ? { operation } : operation === 'create'
|
|
37
|
+
? { operation, releaseId: randomUUID(), name: options.name ?? value }
|
|
38
|
+
: { operation: 'read', releaseId: value };
|
|
39
|
+
const result = await callHubApi(delivery ? '/api/cli/project-delivery' : '/api/cli/project-release', {
|
|
40
|
+
rootDir, gatewayUrl: runtime.gatewayUrl, method: 'POST', body: { projectShortId, request }, signal, interactiveAuth: false,
|
|
41
|
+
});
|
|
42
|
+
if (operation === 'use') {
|
|
43
|
+
if (result.status !== 'draft')
|
|
44
|
+
throw Error('Select an editable draft release.');
|
|
45
|
+
writeReleaseContext(rootDir, { projectShortId, releaseId: value, gatewayUrl: runtime.gatewayUrl });
|
|
46
|
+
}
|
|
47
|
+
if (getOutputMode() === 'json')
|
|
48
|
+
emitJsonResult({ command: 'release', ok: true, data: result });
|
|
49
|
+
else
|
|
50
|
+
logMessage(JSON.stringify(result, null, 2));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (getOutputMode() === 'json')
|
|
54
|
+
exitJsonFailure({ command: 'release', err });
|
|
55
|
+
exitWithError(err);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
dispose();
|
|
59
|
+
}
|
|
60
|
+
}
|