@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
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { StorystickError } from './cli-error.js';
|
|
5
|
+
import { redactProjectSecrets } from './project-connection.js';
|
|
6
|
+
import { MATERIALIZED_STARTER_MARKER, PREPARED_STARTER_DIRECTORY, } from './starter-resolver.js';
|
|
7
|
+
export const INIT_OPERATION_RELATIVE_PATH = join('.storystick', 'init-operation.json');
|
|
8
|
+
export const INIT_PHASES = [
|
|
9
|
+
'P0',
|
|
10
|
+
'P1',
|
|
11
|
+
'P2',
|
|
12
|
+
'P3',
|
|
13
|
+
'P4',
|
|
14
|
+
'P5',
|
|
15
|
+
'P6',
|
|
16
|
+
'P7',
|
|
17
|
+
'P8',
|
|
18
|
+
'P9',
|
|
19
|
+
'P10',
|
|
20
|
+
];
|
|
21
|
+
export class InitOrchestrationError extends StorystickError {
|
|
22
|
+
receipt;
|
|
23
|
+
resumeCommand;
|
|
24
|
+
constructor(cause, receipt) {
|
|
25
|
+
const status = receipt.outcome === 'partial' ? 'partial' : 'failed';
|
|
26
|
+
const resumeCommand = initResumeCommand(receipt);
|
|
27
|
+
const message = redactProjectSecrets(cause instanceof Error ? cause.message : String(cause));
|
|
28
|
+
super(`Initialization ${status} at ${receipt.phase}: ${message}\nResume with: ${resumeCommand}`, {
|
|
29
|
+
code: receipt.outcome === 'partial'
|
|
30
|
+
? 'INIT_PARTIAL'
|
|
31
|
+
: 'INIT_LOCAL_PREPARATION_FAILED',
|
|
32
|
+
category: 'system',
|
|
33
|
+
recoverable: true,
|
|
34
|
+
payload: { outcome: receipt.outcome, receipt, resumeCommand },
|
|
35
|
+
});
|
|
36
|
+
this.name = 'InitOrchestrationError';
|
|
37
|
+
this.receipt = receipt;
|
|
38
|
+
this.resumeCommand = resumeCommand;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function runInitOrchestrator(args) {
|
|
42
|
+
let receipt = openInitOperation(args.identity, args.operationId);
|
|
43
|
+
let prepared;
|
|
44
|
+
const run = async (phase, work) => {
|
|
45
|
+
if (receipt.phases[phase] === 'complete')
|
|
46
|
+
return;
|
|
47
|
+
receipt = updateReceipt(receipt, { phase, outcome: 'running' });
|
|
48
|
+
try {
|
|
49
|
+
const patch = await work();
|
|
50
|
+
receipt = updateReceipt(receipt, {
|
|
51
|
+
...patch,
|
|
52
|
+
phase,
|
|
53
|
+
outcome: 'running',
|
|
54
|
+
phases: { ...receipt.phases, [phase]: 'complete' },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const index = INIT_PHASES.indexOf(phase);
|
|
59
|
+
receipt = updateReceipt(receipt, {
|
|
60
|
+
phase,
|
|
61
|
+
outcome: index <= INIT_PHASES.indexOf('P3') ? 'failed' : 'partial',
|
|
62
|
+
phases: { ...receipt.phases, [phase]: 'failed' },
|
|
63
|
+
});
|
|
64
|
+
throw new InitOrchestrationError(error, receipt);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
await run('P1', () => args.dependencies.planLocal(receipt));
|
|
68
|
+
await run('P2', () => args.dependencies.materializeAndInstall(receipt));
|
|
69
|
+
// A prepared generation is deliberately ephemeral. Recompute it before
|
|
70
|
+
// resuming any unfinished P7/P8 work, and always before auth/remote work.
|
|
71
|
+
if (receipt.phases.P8 !== 'complete') {
|
|
72
|
+
await run('P3', async () => {
|
|
73
|
+
prepared = await args.dependencies.prepareGeneration(receipt);
|
|
74
|
+
});
|
|
75
|
+
if (prepared === undefined) {
|
|
76
|
+
try {
|
|
77
|
+
prepared = await args.dependencies.prepareGeneration(receipt);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
receipt = updateReceipt(receipt, {
|
|
81
|
+
phase: 'P3',
|
|
82
|
+
outcome: 'failed',
|
|
83
|
+
phases: { ...receipt.phases, P3: 'failed' },
|
|
84
|
+
});
|
|
85
|
+
throw new InitOrchestrationError(error, receipt);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
await run('P4', async () => args.dependencies.authorizeAndSelect(receipt));
|
|
90
|
+
if (receipt.phases.P6 !== 'complete') {
|
|
91
|
+
let recovered = false;
|
|
92
|
+
if (receipt.projectShortId && receipt.keyPairVersion) {
|
|
93
|
+
try {
|
|
94
|
+
recovered = await args.dependencies.recoverCommittedConnection(receipt);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
receipt = updateReceipt(receipt, {
|
|
98
|
+
phase: 'P6',
|
|
99
|
+
outcome: 'partial',
|
|
100
|
+
phases: { ...receipt.phases, P6: 'failed' },
|
|
101
|
+
});
|
|
102
|
+
throw new InitOrchestrationError(error, receipt);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (recovered) {
|
|
106
|
+
receipt = updateReceipt(receipt, {
|
|
107
|
+
phase: 'P6',
|
|
108
|
+
outcome: 'running',
|
|
109
|
+
phases: { ...receipt.phases, P5: 'complete', P6: 'complete' },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
let bootstrap;
|
|
114
|
+
await run('P5', async () => {
|
|
115
|
+
bootstrap = await args.dependencies.prepareBootstrap(receipt);
|
|
116
|
+
return bootstrap.receiptPatch;
|
|
117
|
+
});
|
|
118
|
+
// P5 must be replayed on a fresh process when P6 is incomplete because
|
|
119
|
+
// its one-time cleartext response is intentionally absent from disk.
|
|
120
|
+
if (!bootstrap) {
|
|
121
|
+
try {
|
|
122
|
+
bootstrap = await args.dependencies.prepareBootstrap(receipt);
|
|
123
|
+
receipt = updateReceipt(receipt, bootstrap.receiptPatch);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
receipt = updateReceipt(receipt, {
|
|
127
|
+
phase: 'P5',
|
|
128
|
+
outcome: 'partial',
|
|
129
|
+
phases: { ...receipt.phases, P5: 'failed' },
|
|
130
|
+
});
|
|
131
|
+
throw new InitOrchestrationError(error, receipt);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await run('P6', () => args.dependencies.commitConnection(receipt, bootstrap));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (receipt.phases.P7 !== 'complete') {
|
|
138
|
+
if (prepared === undefined) {
|
|
139
|
+
throw new InitOrchestrationError(new Error('Prepared generation is unavailable.'), receipt);
|
|
140
|
+
}
|
|
141
|
+
await run('P7', async () => args.dependencies.pushSchema(receipt, prepared));
|
|
142
|
+
}
|
|
143
|
+
if (receipt.phases.P8 !== 'complete') {
|
|
144
|
+
if (prepared === undefined) {
|
|
145
|
+
throw new InitOrchestrationError(new Error('Prepared generation is unavailable.'), receipt);
|
|
146
|
+
}
|
|
147
|
+
await run('P8', () => args.dependencies.commitGeneration(receipt, prepared));
|
|
148
|
+
}
|
|
149
|
+
await run('P9', () => args.dependencies.acknowledge(receipt));
|
|
150
|
+
if (receipt.phases.P10 !== 'complete' && receipt.phases.P10 !== 'skipped') {
|
|
151
|
+
try {
|
|
152
|
+
await args.dependencies.initializeGit(receipt);
|
|
153
|
+
receipt = updateReceipt(receipt, {
|
|
154
|
+
phase: 'P10',
|
|
155
|
+
phases: { ...receipt.phases, P10: 'complete' },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
receipt = updateReceipt(receipt, {
|
|
160
|
+
phase: 'P10',
|
|
161
|
+
phases: { ...receipt.phases, P10: 'skipped' },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
receipt = updateReceipt(receipt, { outcome: 'complete' });
|
|
166
|
+
removeInitOperation(receipt.targetDir);
|
|
167
|
+
return receipt;
|
|
168
|
+
}
|
|
169
|
+
export function openInitOperation(identity, operationId = randomUUID()) {
|
|
170
|
+
const normalized = {
|
|
171
|
+
...identity,
|
|
172
|
+
targetDir: resolve(identity.targetDir),
|
|
173
|
+
};
|
|
174
|
+
const path = receiptPath(normalized.targetDir);
|
|
175
|
+
if (existsSync(path)) {
|
|
176
|
+
const receipt = parseReceipt(readFileSync(path, 'utf8'));
|
|
177
|
+
if (stableIdentity(receipt) !== stableIdentity(normalized)) {
|
|
178
|
+
throw new StorystickError(`An incompatible initialization is already pending in ${normalized.targetDir}.`, {
|
|
179
|
+
code: 'INIT_INTENT_MISMATCH',
|
|
180
|
+
category: 'conflict',
|
|
181
|
+
recoverable: true,
|
|
182
|
+
payload: { receipt, resumeCommand: initResumeCommand(receipt) },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return receipt;
|
|
186
|
+
}
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const phases = Object.fromEntries(INIT_PHASES.map((phase) => [
|
|
189
|
+
phase,
|
|
190
|
+
phase === 'P0' ? 'complete' : 'pending',
|
|
191
|
+
]));
|
|
192
|
+
const receipt = {
|
|
193
|
+
version: 1,
|
|
194
|
+
operationId,
|
|
195
|
+
...normalized,
|
|
196
|
+
phase: 'P0',
|
|
197
|
+
outcome: 'running',
|
|
198
|
+
phases,
|
|
199
|
+
createdAt: now,
|
|
200
|
+
updatedAt: now,
|
|
201
|
+
};
|
|
202
|
+
writeReceipt(receipt);
|
|
203
|
+
return receipt;
|
|
204
|
+
}
|
|
205
|
+
export function readInitOperation(targetDir) {
|
|
206
|
+
const path = receiptPath(targetDir);
|
|
207
|
+
return existsSync(path) ? parseReceipt(readFileSync(path, 'utf8')) : null;
|
|
208
|
+
}
|
|
209
|
+
export function initResumeCommand(receipt) {
|
|
210
|
+
const into = shellQuote(receipt.targetDir);
|
|
211
|
+
const origin = receipt.originMode === 'append' && receipt.origins[0]
|
|
212
|
+
? ` --origin ${shellQuote(receipt.origins[0])}`
|
|
213
|
+
: '';
|
|
214
|
+
const force = receipt.force ? ' --force' : '';
|
|
215
|
+
const starter = ` --starter ${shellQuote(receipt.starter.id)}`;
|
|
216
|
+
const locale = ` --locale ${shellQuote(receipt.locale)}`;
|
|
217
|
+
const siteUrl = receipt.siteUrl
|
|
218
|
+
? ` --site-url ${shellQuote(receipt.siteUrl)}`
|
|
219
|
+
: '';
|
|
220
|
+
if (receipt.intent.kind === 'link') {
|
|
221
|
+
return `storystick init --link ${receipt.intent.requestedShortId} --into ${into}${starter}${locale}${siteUrl}${origin}${force}`;
|
|
222
|
+
}
|
|
223
|
+
const team = receipt.intent.requestedTeamShortId
|
|
224
|
+
? ` --team ${receipt.intent.requestedTeamShortId}`
|
|
225
|
+
: receipt.intent.requestedNewTeamName ? ` --new-team ${shellQuote(receipt.intent.requestedNewTeamName)}` : '';
|
|
226
|
+
return `storystick init ${shellQuote(receipt.intent.projectName)} --into ${into}${team}${starter}${locale}${siteUrl}${origin}${force}`;
|
|
227
|
+
}
|
|
228
|
+
function updateReceipt(receipt, patch) {
|
|
229
|
+
const next = { ...receipt, ...patch, updatedAt: Date.now() };
|
|
230
|
+
writeReceipt(next);
|
|
231
|
+
return next;
|
|
232
|
+
}
|
|
233
|
+
function removeInitOperation(targetDir) {
|
|
234
|
+
rmSync(receiptPath(targetDir), { force: true });
|
|
235
|
+
rmSync(join(resolve(targetDir), PREPARED_STARTER_DIRECTORY), {
|
|
236
|
+
recursive: true,
|
|
237
|
+
force: true,
|
|
238
|
+
});
|
|
239
|
+
rmSync(join(resolve(targetDir), MATERIALIZED_STARTER_MARKER), {
|
|
240
|
+
force: true,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function receiptPath(targetDir) {
|
|
244
|
+
return join(resolve(targetDir), INIT_OPERATION_RELATIVE_PATH);
|
|
245
|
+
}
|
|
246
|
+
function writeReceipt(receipt) {
|
|
247
|
+
const path = receiptPath(receipt.targetDir);
|
|
248
|
+
const directory = dirname(path);
|
|
249
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
250
|
+
const serialized = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
251
|
+
assertSecretFree(serialized);
|
|
252
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
253
|
+
let handle;
|
|
254
|
+
try {
|
|
255
|
+
handle = openSync(temporary, 'wx', 0o600);
|
|
256
|
+
writeFileSync(handle, serialized, 'utf8');
|
|
257
|
+
fsyncSync(handle);
|
|
258
|
+
closeSync(handle);
|
|
259
|
+
handle = undefined;
|
|
260
|
+
renameSync(temporary, path);
|
|
261
|
+
chmodSync(path, 0o600);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (handle !== undefined)
|
|
265
|
+
closeSync(handle);
|
|
266
|
+
try {
|
|
267
|
+
if (existsSync(temporary))
|
|
268
|
+
unlinkSync(temporary);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Preserve the original write failure.
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function parseReceipt(source) {
|
|
277
|
+
assertSecretFree(source);
|
|
278
|
+
let value;
|
|
279
|
+
try {
|
|
280
|
+
value = JSON.parse(source);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
throw new StorystickError('The init operation receipt is malformed.', {
|
|
284
|
+
code: 'INIT_RECEIPT_INVALID',
|
|
285
|
+
category: 'validation',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (!isValidReceipt(value)) {
|
|
289
|
+
throw new StorystickError('The init operation receipt is unsupported.', {
|
|
290
|
+
code: 'INIT_RECEIPT_INVALID',
|
|
291
|
+
category: 'validation',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
function isValidReceipt(value) {
|
|
297
|
+
if (!isRecord(value))
|
|
298
|
+
return false;
|
|
299
|
+
const phases = value.phases;
|
|
300
|
+
const allowedKeys = new Set([
|
|
301
|
+
'version',
|
|
302
|
+
'operationId',
|
|
303
|
+
'targetDir',
|
|
304
|
+
'intent',
|
|
305
|
+
'strategy',
|
|
306
|
+
'starter',
|
|
307
|
+
'origins',
|
|
308
|
+
'originMode',
|
|
309
|
+
'locale',
|
|
310
|
+
'siteUrl',
|
|
311
|
+
'force',
|
|
312
|
+
'phase',
|
|
313
|
+
'outcome',
|
|
314
|
+
'phases',
|
|
315
|
+
'projectShortId',
|
|
316
|
+
'projectName',
|
|
317
|
+
'selectedTeam',
|
|
318
|
+
'newTeamName',
|
|
319
|
+
'projectionSeq',
|
|
320
|
+
'endpoint',
|
|
321
|
+
'keyPairVersion',
|
|
322
|
+
'schemaVersion',
|
|
323
|
+
'createdAt',
|
|
324
|
+
'updatedAt',
|
|
325
|
+
]);
|
|
326
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key)))
|
|
327
|
+
return false;
|
|
328
|
+
if (value.version !== 1 ||
|
|
329
|
+
typeof value.operationId !== 'string' ||
|
|
330
|
+
value.operationId.length === 0 ||
|
|
331
|
+
typeof value.targetDir !== 'string' ||
|
|
332
|
+
!isIntent(value.intent) ||
|
|
333
|
+
(value.strategy !== 'starter' && value.strategy !== 'existing-next') ||
|
|
334
|
+
!isRecord(value.starter) ||
|
|
335
|
+
value.starter.catalogVersion !== 2 ||
|
|
336
|
+
typeof value.starter.id !== 'string' ||
|
|
337
|
+
typeof value.starter.release !== 'string' ||
|
|
338
|
+
(value.starter.framework !== 'next' && value.starter.framework !== 'astro') ||
|
|
339
|
+
(value.strategy === 'existing-next' && value.starter.framework !== 'next') ||
|
|
340
|
+
!isRecord(value.starter.source) ||
|
|
341
|
+
value.starter.source.provider !== 'archive' ||
|
|
342
|
+
typeof value.starter.source.url !== 'string' ||
|
|
343
|
+
typeof value.starter.source.integrity !== 'string' ||
|
|
344
|
+
typeof value.starter.integrity !== 'string' ||
|
|
345
|
+
(value.starter.localDirectory !== undefined &&
|
|
346
|
+
typeof value.starter.localDirectory !== 'string') ||
|
|
347
|
+
!Array.isArray(value.origins) ||
|
|
348
|
+
!value.origins.every((origin) => typeof origin === 'string') ||
|
|
349
|
+
(value.originMode !== 'append' &&
|
|
350
|
+
value.originMode !== 'default-if-empty') ||
|
|
351
|
+
typeof value.locale !== 'string' ||
|
|
352
|
+
(value.siteUrl !== undefined && typeof value.siteUrl !== 'string') ||
|
|
353
|
+
typeof value.force !== 'boolean' ||
|
|
354
|
+
!INIT_PHASES.includes(value.phase) ||
|
|
355
|
+
!['running', 'partial', 'failed', 'complete'].includes(String(value.outcome)) ||
|
|
356
|
+
!isRecord(phases) ||
|
|
357
|
+
!INIT_PHASES.every((phase) => ['pending', 'complete', 'failed', 'skipped'].includes(String(isRecord(phases) ? phases[phase] : undefined))) ||
|
|
358
|
+
typeof value.createdAt !== 'number' ||
|
|
359
|
+
typeof value.updatedAt !== 'number') {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if ((value.projectShortId !== undefined &&
|
|
363
|
+
(typeof value.projectShortId !== 'string' ||
|
|
364
|
+
!/^[0-9a-hjkmnp-tv-z]{8}$/u.test(value.projectShortId))) ||
|
|
365
|
+
(value.projectName !== undefined &&
|
|
366
|
+
typeof value.projectName !== 'string') ||
|
|
367
|
+
(value.endpoint !== undefined && typeof value.endpoint !== 'string') ||
|
|
368
|
+
(value.keyPairVersion !== undefined &&
|
|
369
|
+
(typeof value.keyPairVersion !== 'number' ||
|
|
370
|
+
!Number.isSafeInteger(value.keyPairVersion) ||
|
|
371
|
+
value.keyPairVersion < 1)) ||
|
|
372
|
+
(value.schemaVersion !== undefined &&
|
|
373
|
+
typeof value.schemaVersion !== 'string')) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
if (value.newTeamName !== undefined && typeof value.newTeamName !== 'string')
|
|
377
|
+
return false;
|
|
378
|
+
if (value.projectionSeq !== undefined && (!Number.isSafeInteger(value.projectionSeq) || value.projectionSeq < 0))
|
|
379
|
+
return false;
|
|
380
|
+
if (value.selectedTeam !== undefined) {
|
|
381
|
+
if (!isRecord(value.selectedTeam) ||
|
|
382
|
+
typeof value.selectedTeam.shortId !== 'string' ||
|
|
383
|
+
typeof value.selectedTeam.name !== 'string') {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
function isIntent(value) {
|
|
390
|
+
if (!isRecord(value))
|
|
391
|
+
return false;
|
|
392
|
+
if (value.kind === 'create') {
|
|
393
|
+
return (typeof value.projectName === 'string' &&
|
|
394
|
+
(value.requestedTeamShortId === undefined ||
|
|
395
|
+
typeof value.requestedTeamShortId === 'string') &&
|
|
396
|
+
(value.requestedNewTeamName === undefined || typeof value.requestedNewTeamName === 'string'));
|
|
397
|
+
}
|
|
398
|
+
return value.kind === 'link' && typeof value.requestedShortId === 'string';
|
|
399
|
+
}
|
|
400
|
+
function isRecord(value) {
|
|
401
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
402
|
+
}
|
|
403
|
+
function assertSecretFree(serialized) {
|
|
404
|
+
if (/ss_(?:sk|pk)_(?:live|test)_/u.test(serialized) ||
|
|
405
|
+
/"(?:apiKey|publishableKey)"\s*:/u.test(serialized)) {
|
|
406
|
+
throw new StorystickError('Refusing to persist secrets in init receipt.', {
|
|
407
|
+
code: 'INIT_RECEIPT_SECRET',
|
|
408
|
+
category: 'system',
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function stableIdentity(identity) {
|
|
413
|
+
return JSON.stringify({
|
|
414
|
+
targetDir: resolve(identity.targetDir),
|
|
415
|
+
intent: identity.intent,
|
|
416
|
+
strategy: identity.strategy,
|
|
417
|
+
starter: identity.starter,
|
|
418
|
+
origins: identity.origins,
|
|
419
|
+
originMode: identity.originMode,
|
|
420
|
+
locale: identity.locale,
|
|
421
|
+
siteUrl: identity.siteUrl,
|
|
422
|
+
force: identity.force,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
export function shellQuote(value) {
|
|
426
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
427
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Bounded readback of the receipt's causal checkpoint. Never repeats a remote write. */
|
|
2
|
+
export declare function waitForProjection(args: {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
token: string;
|
|
5
|
+
version: string;
|
|
6
|
+
seq: number;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { StorystickError } from './cli-error.js';
|
|
2
|
+
/** Bounded readback of the receipt's causal checkpoint. Never repeats a remote write. */
|
|
3
|
+
export async function waitForProjection(args) {
|
|
4
|
+
if (!Number.isSafeInteger(args.seq))
|
|
5
|
+
throw new StorystickError('The schema projection checkpoint is missing. Resume schema synchronization.', { code: 'INIT_CHECKPOINT_MISSING', category: 'system' });
|
|
6
|
+
const deadline = Date.now() + (args.timeoutMs ?? 60_000);
|
|
7
|
+
while (Date.now() < deadline) {
|
|
8
|
+
args.signal?.throwIfAborted();
|
|
9
|
+
const response = await (args.fetchImpl ?? fetch)(new URL('/api/m/operations/content-plane/readiness', args.endpoint), {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
authorization: `Bearer ${args.token}`,
|
|
13
|
+
'content-type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({ version: args.version, seq: args.seq }),
|
|
16
|
+
signal: args.signal
|
|
17
|
+
? AbortSignal.any([
|
|
18
|
+
args.signal,
|
|
19
|
+
AbortSignal.timeout(Math.max(1, Math.min(10_000, deadline - Date.now()))),
|
|
20
|
+
])
|
|
21
|
+
: AbortSignal.timeout(Math.max(1, Math.min(10_000, deadline - Date.now()))),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
throw new StorystickError(`Projection verification failed (${response.status}). Resume init to retry.`, { code: 'INIT_PROJECTION_UNAVAILABLE', category: 'system' });
|
|
25
|
+
const { status } = (await response.json());
|
|
26
|
+
if (status === 'ready')
|
|
27
|
+
return;
|
|
28
|
+
if (status === 'superseded')
|
|
29
|
+
throw new StorystickError('The project schema changed during setup. Synchronize the intended schema before resuming.', { code: 'INIT_SCHEMA_SUPERSEDED', category: 'conflict' });
|
|
30
|
+
if (status === 'failed')
|
|
31
|
+
break;
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
const finish = () => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
args.signal?.removeEventListener('abort', abort);
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
const abort = () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
args.signal?.removeEventListener('abort', abort);
|
|
41
|
+
reject(args.signal?.reason);
|
|
42
|
+
};
|
|
43
|
+
const timer = setTimeout(finish, Math.min(1000, Math.max(1, deadline - Date.now())));
|
|
44
|
+
args.signal?.addEventListener('abort', abort, { once: true });
|
|
45
|
+
if (args.signal?.aborted)
|
|
46
|
+
abort();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
throw new StorystickError('The project and connection were created, but its content projection is not ready. Resume init when the Content Plane is available.', { code: 'INIT_PROJECTION_NOT_READY', category: 'system' });
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { StarterManifest } from './starter-resolver.js';
|
|
2
|
+
type ScaffoldStatus = 'create' | 'update' | 'overwrite' | 'skip';
|
|
3
|
+
export interface InitScaffoldPlan {
|
|
4
|
+
readonly rootDir: string;
|
|
5
|
+
readonly appDir: string;
|
|
6
|
+
readonly componentsDir: string;
|
|
7
|
+
readonly actions: readonly InitScaffoldAction[];
|
|
8
|
+
}
|
|
9
|
+
export type InitScaffoldAction = InitScaffoldFileAction | InitScaffoldPackageAction | InitScaffoldGitignoreAction | InitScaffoldDeleteAction;
|
|
10
|
+
export interface InitScaffoldFileAction {
|
|
11
|
+
readonly kind: 'file';
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly status: ScaffoldStatus;
|
|
14
|
+
readonly content: string;
|
|
15
|
+
readonly reason?: string;
|
|
16
|
+
readonly critical?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface InitScaffoldPackageAction {
|
|
19
|
+
readonly kind: 'package';
|
|
20
|
+
readonly path: 'package.json';
|
|
21
|
+
readonly status: Extract<ScaffoldStatus, 'create' | 'update' | 'skip'>;
|
|
22
|
+
readonly content: string;
|
|
23
|
+
readonly reason?: string;
|
|
24
|
+
readonly critical?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface InitScaffoldGitignoreAction {
|
|
27
|
+
readonly kind: 'gitignore';
|
|
28
|
+
readonly path: '.gitignore';
|
|
29
|
+
readonly status: Extract<ScaffoldStatus, 'create' | 'update' | 'skip'>;
|
|
30
|
+
readonly content: string;
|
|
31
|
+
readonly reason?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface InitScaffoldDeleteAction {
|
|
34
|
+
readonly kind: 'delete';
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly status: 'delete';
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
}
|
|
39
|
+
export interface PlanInitScaffoldOptions {
|
|
40
|
+
readonly rootDir: string;
|
|
41
|
+
readonly starterRoot: string;
|
|
42
|
+
readonly manifest: StarterManifest;
|
|
43
|
+
readonly force?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function isNextProject(rootDir: string): boolean;
|
|
46
|
+
export declare function planInitScaffold({ rootDir, starterRoot, manifest, force, }: PlanInitScaffoldOptions): InitScaffoldPlan;
|
|
47
|
+
export declare function assertInitScaffoldReady(plan: InitScaffoldPlan): void;
|
|
48
|
+
export declare function writeInitScaffold(plan: InitScaffoldPlan): void;
|
|
49
|
+
export declare function summarizeInitScaffold(plan: InitScaffoldPlan): {
|
|
50
|
+
readonly changed: number;
|
|
51
|
+
readonly skipped: readonly InitScaffoldAction[];
|
|
52
|
+
};
|
|
53
|
+
export declare function renderKernelAsset(asset: string, manifest: StarterManifest): string;
|
|
54
|
+
export {};
|