@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
package/dist/init.js
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { serializeSchema } from '@storystick/core/serializer';
|
|
5
|
+
import { canonicalLocaleCode } from '@storystick/core/locale-routing';
|
|
6
|
+
import { whoami } from './auth-device.js';
|
|
7
|
+
import { acknowledgeCliBootstrap, prepareCliBootstrap, } from './bootstrap-client.js';
|
|
8
|
+
import { StorystickError } from './cli-error.js';
|
|
9
|
+
import { commitGeneratedFiles } from './generated-transaction.js';
|
|
10
|
+
import { callHubApi } from './hub-client.js';
|
|
11
|
+
import { readInitOperation, runInitOrchestrator, shellQuote, } from './init-operation.js';
|
|
12
|
+
import { assertInitScaffoldReady, isNextProject, planInitScaffold, writeInitScaffold, } from './init-scaffold.js';
|
|
13
|
+
import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logFinishedStep, logMessage, maybeExitOnCancel, promptDeviceAuth, promptSelect, promptText, withInterrupt, } from './output.js';
|
|
14
|
+
import { PROJECT_CONNECTION_ENV_KEYS, commitProjectConnection, findProjectConnectionEnvFile, parseEnvAssignment, parseProjectConnection, parseProjectShortId, resolveProjectConnection, validateProjectTokenReadback, } from './project-connection.js';
|
|
15
|
+
import { assertSchemaPlanAllowed, fetchSchemaChangePlan, fetchSchemaPushToken, pushSchema, schemaPlanFence, schemaToPushBody, } from './push-client.js';
|
|
16
|
+
import { resolveCliRuntimeConfig } from './runtime-config.js';
|
|
17
|
+
import { ensurePreparedStarterMaterialized, loadStarterCatalog, preparedStarterRoot, prepareStarter, readStarterManifest, selectStarter, } from './starter-resolver.js';
|
|
18
|
+
export async function runInit(projectNameArg, options = {}) {
|
|
19
|
+
configureOutput({ json: options.json === true });
|
|
20
|
+
try {
|
|
21
|
+
if (projectNameArg && options.link) {
|
|
22
|
+
throw new StorystickError('Choose either a project name or --link, not both.', { code: 'INIT_INTENT_AMBIGUOUS', category: 'validation' });
|
|
23
|
+
}
|
|
24
|
+
let intent;
|
|
25
|
+
if (options.link) {
|
|
26
|
+
intent = {
|
|
27
|
+
kind: 'link',
|
|
28
|
+
requestedShortId: parseProjectShortId(options.link),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
else if (projectNameArg) {
|
|
32
|
+
intent = createIntent(projectNameArg, options.team);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (options.json) {
|
|
36
|
+
throw new StorystickError('Pass a project name or --link when using --json.', { code: 'INIT_INTENT_REQUIRED', category: 'validation' });
|
|
37
|
+
}
|
|
38
|
+
intent = await promptForIntent(options.team);
|
|
39
|
+
}
|
|
40
|
+
if (options.newTeam !== undefined) {
|
|
41
|
+
if (!options.newTeam.trim())
|
|
42
|
+
throw new StorystickError('The new team name cannot be empty.', { code: 'INVALID_TEAM_NAME', category: 'validation' });
|
|
43
|
+
if (options.team || intent.kind !== 'create')
|
|
44
|
+
throw new StorystickError('--new-team requires project creation and cannot be combined with --team.', { code: 'INIT_TEAM_CONFLICT', category: 'validation' });
|
|
45
|
+
intent = { ...intent, requestedNewTeamName: options.newTeam.trim() };
|
|
46
|
+
}
|
|
47
|
+
const defaultDir = intent.kind === 'create'
|
|
48
|
+
? toSlug(intent.projectName)
|
|
49
|
+
: intent.requestedShortId;
|
|
50
|
+
if (!defaultDir) {
|
|
51
|
+
throw new StorystickError('Project name must contain a letter or number.', {
|
|
52
|
+
code: 'INVALID_PROJECT_NAME',
|
|
53
|
+
category: 'validation',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const targetDir = resolve(options.into ?? defaultDir);
|
|
57
|
+
const existingReceipt = readInitOperation(targetDir);
|
|
58
|
+
const locale = await resolveSiteLocale(options.locale, existingReceipt);
|
|
59
|
+
const siteUrl = await resolveSiteUrl(options.siteUrl, existingReceipt);
|
|
60
|
+
const catalogInterrupt = withInterrupt();
|
|
61
|
+
let starter;
|
|
62
|
+
try {
|
|
63
|
+
starter = await resolveInitStarter({
|
|
64
|
+
requested: options.starter,
|
|
65
|
+
existingReceipt,
|
|
66
|
+
signal: catalogInterrupt.signal,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
catalogInterrupt.dispose();
|
|
71
|
+
}
|
|
72
|
+
const targetExists = existsSync(targetDir);
|
|
73
|
+
const strategy = existingReceipt?.strategy ??
|
|
74
|
+
(targetExists && isNextProject(targetDir) ? 'existing-next' : 'starter');
|
|
75
|
+
if (targetExists && !existingReceipt && strategy === 'starter') {
|
|
76
|
+
throw new StorystickError(`Target already exists and is not a Next application: ${targetDir}`, { code: 'INIT_TARGET_EXISTS', category: 'conflict' });
|
|
77
|
+
}
|
|
78
|
+
if (strategy === 'existing-next' && starter.framework !== 'next') {
|
|
79
|
+
throw new StorystickError('An Astro starter cannot be overlaid on a Next application. Choose a new directory.', { code: 'INIT_FRAMEWORK_MISMATCH', category: 'validation' });
|
|
80
|
+
}
|
|
81
|
+
if (strategy === 'existing-next' && !isNextProject(targetDir)) {
|
|
82
|
+
throw new StorystickError(`Target is not a Next application: ${targetDir}`, {
|
|
83
|
+
code: 'INIT_TARGET_NOT_NEXT',
|
|
84
|
+
category: 'validation',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (strategy === 'starter' && options.force) {
|
|
88
|
+
throw new StorystickError('--force applies only to known Storystick files in an existing Next application.', { code: 'INIT_FORCE_NOT_APPLICABLE', category: 'validation' });
|
|
89
|
+
}
|
|
90
|
+
if (!existingReceipt && findProjectConnectionEnvFile(targetDir)) {
|
|
91
|
+
throw new StorystickError('This application already has a ProjectConnection. Use `storystick connect` to replace it.', { code: 'INIT_CONNECTION_EXISTS', category: 'conflict' });
|
|
92
|
+
}
|
|
93
|
+
const origin = normalizeOrigin(options.origin ?? 'http://localhost:3000');
|
|
94
|
+
const identity = {
|
|
95
|
+
targetDir,
|
|
96
|
+
intent,
|
|
97
|
+
strategy,
|
|
98
|
+
starter: {
|
|
99
|
+
catalogVersion: 2,
|
|
100
|
+
id: starter.id,
|
|
101
|
+
release: starter.release,
|
|
102
|
+
framework: starter.framework,
|
|
103
|
+
source: starter.source,
|
|
104
|
+
integrity: starter.integrity,
|
|
105
|
+
...(starter.localDirectory
|
|
106
|
+
? { localDirectory: starter.localDirectory }
|
|
107
|
+
: {}),
|
|
108
|
+
},
|
|
109
|
+
origins: [origin],
|
|
110
|
+
originMode: intent.kind === 'create' || options.origin
|
|
111
|
+
? 'append'
|
|
112
|
+
: 'default-if-empty',
|
|
113
|
+
locale,
|
|
114
|
+
...(siteUrl ? { siteUrl } : {}),
|
|
115
|
+
force: options.force === true,
|
|
116
|
+
};
|
|
117
|
+
const { signal, dispose } = withInterrupt();
|
|
118
|
+
let receipt;
|
|
119
|
+
try {
|
|
120
|
+
receipt = await runInitOrchestrator({
|
|
121
|
+
identity,
|
|
122
|
+
operationId: options.operationId,
|
|
123
|
+
dependencies: createInitDependencies(signal),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
dispose();
|
|
128
|
+
}
|
|
129
|
+
printInitOutro({
|
|
130
|
+
dirName: relativeTarget(targetDir),
|
|
131
|
+
shortId: receipt.projectShortId,
|
|
132
|
+
projectName: receipt.projectName ??
|
|
133
|
+
(receipt.intent.kind === 'create'
|
|
134
|
+
? receipt.intent.projectName
|
|
135
|
+
: receipt.intent.requestedShortId),
|
|
136
|
+
team: receipt.selectedTeam,
|
|
137
|
+
linked: receipt.intent.kind === 'link',
|
|
138
|
+
receipt,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
maybeExitOnCancel(error);
|
|
143
|
+
if (getOutputMode() === 'json') {
|
|
144
|
+
exitJsonFailure({ command: 'init', err: error });
|
|
145
|
+
}
|
|
146
|
+
exitWithError(error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function createInitDependencies(signal) {
|
|
150
|
+
return {
|
|
151
|
+
async planLocal(receipt) {
|
|
152
|
+
assertInitSiteEnvironmentCompatible(receipt);
|
|
153
|
+
await prepareStarter({
|
|
154
|
+
entry: receipt.starter,
|
|
155
|
+
targetDir: receipt.targetDir,
|
|
156
|
+
});
|
|
157
|
+
if (receipt.strategy === 'existing-next') {
|
|
158
|
+
const starterRoot = preparedStarterRoot(receipt.targetDir);
|
|
159
|
+
const manifest = readStarterManifest(starterRoot);
|
|
160
|
+
assertInitScaffoldReady(planInitScaffold({
|
|
161
|
+
rootDir: receipt.targetDir,
|
|
162
|
+
starterRoot,
|
|
163
|
+
manifest,
|
|
164
|
+
force: receipt.force,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
async materializeAndInstall(receipt) {
|
|
169
|
+
const starterRoot = preparedStarterRoot(receipt.targetDir);
|
|
170
|
+
const manifest = readStarterManifest(starterRoot);
|
|
171
|
+
if (receipt.strategy === 'existing-next') {
|
|
172
|
+
writeInitScaffold(planInitScaffold({
|
|
173
|
+
rootDir: receipt.targetDir,
|
|
174
|
+
starterRoot,
|
|
175
|
+
manifest,
|
|
176
|
+
force: receipt.force,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
ensurePreparedStarterMaterialized(receipt.targetDir, receipt.starter);
|
|
181
|
+
// Astro starters already contain their complete native integration.
|
|
182
|
+
// Keep the Next kernel scoped to its own framework.
|
|
183
|
+
if (manifest.framework === 'next') {
|
|
184
|
+
writeInitScaffold(planInitScaffold({
|
|
185
|
+
rootDir: receipt.targetDir,
|
|
186
|
+
starterRoot,
|
|
187
|
+
manifest,
|
|
188
|
+
force: true,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
const packageName = receipt.intent.kind === 'create'
|
|
192
|
+
? toSlug(receipt.intent.projectName)
|
|
193
|
+
: receipt.intent.requestedShortId;
|
|
194
|
+
substituteInFile(join(receipt.targetDir, 'package.json'), {
|
|
195
|
+
'{{PROJECT_NAME}}': packageName,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
writeInitSiteEnvironment(receipt);
|
|
199
|
+
ensureInitReceiptIgnored(receipt.targetDir);
|
|
200
|
+
await installDependencies(receipt.targetDir, signal);
|
|
201
|
+
},
|
|
202
|
+
async prepareGeneration(receipt) {
|
|
203
|
+
const { findCorePath, preparePipeline } = await import('./cli.js');
|
|
204
|
+
const pipeline = await preparePipeline({
|
|
205
|
+
rootDir: receipt.targetDir,
|
|
206
|
+
storyDirs: ['stories'],
|
|
207
|
+
corePath: findCorePath(receipt.targetDir),
|
|
208
|
+
});
|
|
209
|
+
if (!pipeline.success) {
|
|
210
|
+
const errors = pipeline.diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;
|
|
211
|
+
throw new StorystickError(`Story preparation failed with ${errors} error(s).`, { code: 'INIT_STORY_PREPARATION_FAILED', category: 'validation' });
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
pipeline,
|
|
215
|
+
schema: serializeSchema(pipeline.stories, pipeline.diagnostics),
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
async authorizeAndSelect(receipt) {
|
|
219
|
+
if (receipt.intent.kind === 'create') {
|
|
220
|
+
const teams = await fetchEligibleProjectTeams(signal, receipt.targetDir);
|
|
221
|
+
await logAuthenticatedUser(receipt.targetDir);
|
|
222
|
+
const selectedTeam = await selectEligibleProjectTeam(teams, {
|
|
223
|
+
requestedShortId: receipt.intent.requestedTeamShortId,
|
|
224
|
+
newTeamName: receipt.intent.requestedNewTeamName,
|
|
225
|
+
interactive: process.stdin.isTTY === true && getOutputMode() !== 'json',
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
projectName: receipt.intent.projectName,
|
|
229
|
+
...('newTeamName' in selectedTeam ? { newTeamName: selectedTeam.newTeamName } : {
|
|
230
|
+
selectedTeam: { shortId: selectedTeam.shortId, name: selectedTeam.name },
|
|
231
|
+
}),
|
|
232
|
+
endpoint: resolveCliRuntimeConfig({ rootDir: receipt.targetDir })
|
|
233
|
+
.gatewayUrl,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const project = await callHubApi(`/api/cli/projects/for-user-by-shortId?shortId=${encodeURIComponent(receipt.intent.requestedShortId)}`, {
|
|
237
|
+
rootDir: receipt.targetDir,
|
|
238
|
+
onAuthPrompt: promptDeviceAuth,
|
|
239
|
+
signal,
|
|
240
|
+
});
|
|
241
|
+
await logAuthenticatedUser(receipt.targetDir);
|
|
242
|
+
return {
|
|
243
|
+
projectName: project.name,
|
|
244
|
+
endpoint: project.endpoint,
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
async recoverCommittedConnection(receipt) {
|
|
248
|
+
const connection = resolveProjectConnection(receipt.targetDir, {});
|
|
249
|
+
if (!connection || connection.shortId !== receipt.projectShortId) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const token = await fetchSchemaPushToken(gatewayFor(receipt), connection.apiKey, { signal });
|
|
253
|
+
validateProjectTokenReadback(token, connection);
|
|
254
|
+
return true;
|
|
255
|
+
},
|
|
256
|
+
async prepareBootstrap(receipt) {
|
|
257
|
+
const intent = remoteIntent(receipt);
|
|
258
|
+
const prepared = await prepareCliBootstrap(receipt.operationId, intent, {
|
|
259
|
+
rootDir: receipt.targetDir,
|
|
260
|
+
onAuthPrompt: promptDeviceAuth,
|
|
261
|
+
signal,
|
|
262
|
+
});
|
|
263
|
+
if (!prepared.credentials) {
|
|
264
|
+
throw new StorystickError('Bootstrap returned no credentials before local connection commit.', { code: 'BOOTSTRAP_CREDENTIALS_MISSING', category: 'system' });
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
receiptPatch: {
|
|
268
|
+
selectedTeam: prepared.receipt.team ?? receipt.selectedTeam,
|
|
269
|
+
projectShortId: prepared.receipt.projectShortId,
|
|
270
|
+
keyPairVersion: prepared.receipt.keyPairVersion,
|
|
271
|
+
},
|
|
272
|
+
credentials: prepared.credentials,
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
async commitConnection(receipt, prepared) {
|
|
276
|
+
const shortId = prepared.receiptPatch.projectShortId;
|
|
277
|
+
const connection = parseProjectConnection({
|
|
278
|
+
projectId: shortId,
|
|
279
|
+
publicProjectId: shortId,
|
|
280
|
+
apiKey: prepared.credentials.apiKey,
|
|
281
|
+
publishableKey: prepared.credentials.publishableKey,
|
|
282
|
+
});
|
|
283
|
+
const token = await fetchSchemaPushToken(gatewayFor(receipt), connection.apiKey, { signal });
|
|
284
|
+
validateProjectTokenReadback(token, connection);
|
|
285
|
+
commitProjectConnection(join(receipt.targetDir, '.env.local'), connection);
|
|
286
|
+
},
|
|
287
|
+
async pushSchema(receipt, prepared) {
|
|
288
|
+
const connection = requireLocalConnection(receipt);
|
|
289
|
+
const token = await fetchSchemaPushToken(gatewayFor(receipt), connection.apiKey, { signal });
|
|
290
|
+
validateProjectTokenReadback(token, connection);
|
|
291
|
+
const body = schemaToPushBody(prepared.schema);
|
|
292
|
+
const plan = await fetchSchemaChangePlan(gatewayFor(receipt), token, body, { signal });
|
|
293
|
+
assertSchemaPlanAllowed(plan);
|
|
294
|
+
const pushed = await pushSchema(gatewayFor(receipt), token, body, schemaPlanFence(plan), { signal });
|
|
295
|
+
return { schemaVersion: pushed.version, projectionSeq: pushed.projectionSeq };
|
|
296
|
+
},
|
|
297
|
+
async commitGeneration(receipt, prepared) {
|
|
298
|
+
await commitGeneratedFiles(join(receipt.targetDir, 'generated', 'storystick'), prepared.pipeline.generated);
|
|
299
|
+
},
|
|
300
|
+
async acknowledge(receipt) {
|
|
301
|
+
if (!receipt.keyPairVersion) {
|
|
302
|
+
throw new StorystickError('Bootstrap key-pair version is missing.', {
|
|
303
|
+
code: 'BOOTSTRAP_VERSION_MISSING',
|
|
304
|
+
category: 'system',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const result = await acknowledgeCliBootstrap(receipt.operationId, remoteIntent(receipt), receipt.keyPairVersion, {
|
|
308
|
+
rootDir: receipt.targetDir,
|
|
309
|
+
onAuthPrompt: promptDeviceAuth,
|
|
310
|
+
signal,
|
|
311
|
+
});
|
|
312
|
+
if (result.receipt.state !== 'connection_acknowledged') {
|
|
313
|
+
throw new StorystickError('Bootstrap acknowledgement is incomplete.', {
|
|
314
|
+
code: 'BOOTSTRAP_ACK_INCOMPLETE',
|
|
315
|
+
category: 'system',
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const connection = requireLocalConnection(receipt);
|
|
319
|
+
const token = await fetchSchemaPushToken(gatewayFor(receipt), connection.apiKey, { signal });
|
|
320
|
+
const { waitForProjection } = await import('./init-readiness.js');
|
|
321
|
+
await waitForProjection({ endpoint: gatewayFor(receipt), token,
|
|
322
|
+
version: receipt.schemaVersion, seq: receipt.projectionSeq, signal });
|
|
323
|
+
},
|
|
324
|
+
async initializeGit(receipt) {
|
|
325
|
+
if (receipt.strategy === 'existing-next')
|
|
326
|
+
return;
|
|
327
|
+
if (existsSync(join(receipt.targetDir, '.git')))
|
|
328
|
+
return;
|
|
329
|
+
const { execa } = await import('execa');
|
|
330
|
+
await execa('git', ['init'], {
|
|
331
|
+
cwd: receipt.targetDir,
|
|
332
|
+
stdio: 'pipe',
|
|
333
|
+
cancelSignal: signal,
|
|
334
|
+
});
|
|
335
|
+
await execa('git', ['add', '-A'], {
|
|
336
|
+
cwd: receipt.targetDir,
|
|
337
|
+
stdio: 'pipe',
|
|
338
|
+
cancelSignal: signal,
|
|
339
|
+
});
|
|
340
|
+
await execa('git', ['commit', '-m', 'Initial Storystick project'], {
|
|
341
|
+
cwd: receipt.targetDir,
|
|
342
|
+
stdio: 'pipe',
|
|
343
|
+
cancelSignal: signal,
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function remoteIntent(receipt) {
|
|
349
|
+
if (receipt.intent.kind === 'create') {
|
|
350
|
+
if (!receipt.selectedTeam && !receipt.newTeamName) {
|
|
351
|
+
throw new StorystickError('No eligible Team was selected.', {
|
|
352
|
+
code: 'TEAM_REQUIRED',
|
|
353
|
+
category: 'validation',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
kind: 'create',
|
|
358
|
+
projectName: receipt.intent.projectName,
|
|
359
|
+
...(receipt.newTeamName ? { newTeamName: receipt.newTeamName } : { teamShortId: receipt.selectedTeam.shortId }),
|
|
360
|
+
initialLocale: receipt.locale,
|
|
361
|
+
initialLocaleLabel: localeDisplayName(receipt.locale),
|
|
362
|
+
initialAllowedOrigins: receipt.origins,
|
|
363
|
+
originMode: 'append',
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
kind: 'link',
|
|
368
|
+
requestedShortId: receipt.intent.requestedShortId,
|
|
369
|
+
initialAllowedOrigins: receipt.origins,
|
|
370
|
+
originMode: receipt.originMode,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function requireLocalConnection(receipt) {
|
|
374
|
+
const connection = resolveProjectConnection(receipt.targetDir, {});
|
|
375
|
+
if (!connection || connection.shortId !== receipt.projectShortId) {
|
|
376
|
+
throw new StorystickError('Local ProjectConnection does not match bootstrap.', {
|
|
377
|
+
code: 'BOOTSTRAP_LOCAL_CONNECTION_MISSING',
|
|
378
|
+
category: 'validation',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return connection;
|
|
382
|
+
}
|
|
383
|
+
function gatewayFor(receipt) {
|
|
384
|
+
return resolveCliRuntimeConfig({
|
|
385
|
+
rootDir: receipt.targetDir,
|
|
386
|
+
gatewayUrl: receipt.endpoint,
|
|
387
|
+
}).gatewayUrl;
|
|
388
|
+
}
|
|
389
|
+
export function sanitizedInstallEnvironment(env = process.env) {
|
|
390
|
+
const clean = { ...env };
|
|
391
|
+
for (const key of PROJECT_CONNECTION_ENV_KEYS)
|
|
392
|
+
delete clean[key];
|
|
393
|
+
return clean;
|
|
394
|
+
}
|
|
395
|
+
export async function installDependencies(targetDir, signal) {
|
|
396
|
+
const manager = detectPackageManager(targetDir);
|
|
397
|
+
const { execa } = await import('execa');
|
|
398
|
+
await execa(manager, ['install'], {
|
|
399
|
+
cwd: targetDir,
|
|
400
|
+
stdio: 'pipe',
|
|
401
|
+
env: sanitizedInstallEnvironment(),
|
|
402
|
+
cancelSignal: signal,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function detectPackageManager(targetDir) {
|
|
406
|
+
if (existsSync(join(targetDir, 'pnpm-lock.yaml')))
|
|
407
|
+
return 'pnpm';
|
|
408
|
+
if (existsSync(join(targetDir, 'yarn.lock')))
|
|
409
|
+
return 'yarn';
|
|
410
|
+
if (existsSync(join(targetDir, 'bun.lockb')))
|
|
411
|
+
return 'bun';
|
|
412
|
+
return 'npm';
|
|
413
|
+
}
|
|
414
|
+
function ensureInitReceiptIgnored(targetDir) {
|
|
415
|
+
const path = join(targetDir, '.gitignore');
|
|
416
|
+
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
417
|
+
if (/^\.storystick\/$/mu.test(current))
|
|
418
|
+
return;
|
|
419
|
+
const prefix = current.length > 0 && !current.endsWith('\n') ? '\n' : '';
|
|
420
|
+
writeFileSync(path, `${current}${prefix}.storystick/\n`, 'utf8');
|
|
421
|
+
}
|
|
422
|
+
function substituteInFile(path, values) {
|
|
423
|
+
if (!existsSync(path))
|
|
424
|
+
return;
|
|
425
|
+
let content = readFileSync(path, 'utf8');
|
|
426
|
+
for (const [from, to] of Object.entries(values)) {
|
|
427
|
+
content = content.replaceAll(from, to);
|
|
428
|
+
}
|
|
429
|
+
writeFileSync(path, content, 'utf8');
|
|
430
|
+
}
|
|
431
|
+
async function resolveInitStarter({ requested, existingReceipt, signal, }) {
|
|
432
|
+
if (existingReceipt) {
|
|
433
|
+
if (requested && requested.trim() !== existingReceipt.starter.id) {
|
|
434
|
+
throw new StorystickError(`Initialization is already pinned to starter "${existingReceipt.starter.id}".`, { code: 'INIT_INTENT_MISMATCH', category: 'conflict' });
|
|
435
|
+
}
|
|
436
|
+
return existingReceipt.starter;
|
|
437
|
+
}
|
|
438
|
+
const catalog = await loadStarterCatalog({ signal });
|
|
439
|
+
if (requested?.trim())
|
|
440
|
+
return selectStarter(catalog, requested);
|
|
441
|
+
if (catalog.starters.length > 1 &&
|
|
442
|
+
process.stdin.isTTY === true &&
|
|
443
|
+
getOutputMode() !== 'json') {
|
|
444
|
+
return selectStarter(catalog, await promptStarter(catalog));
|
|
445
|
+
}
|
|
446
|
+
return selectStarter(catalog);
|
|
447
|
+
}
|
|
448
|
+
async function promptStarter(catalog) {
|
|
449
|
+
try {
|
|
450
|
+
return await promptSelect('Starter', catalog.starters.map((starter) => ({
|
|
451
|
+
value: starter.id,
|
|
452
|
+
name: starter.label,
|
|
453
|
+
description: starter.description,
|
|
454
|
+
})));
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
maybeExitOnCancel(error);
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function resolveSiteLocale(requested, existingReceipt) {
|
|
462
|
+
const existing = existingReceipt?.locale;
|
|
463
|
+
if (requested?.trim())
|
|
464
|
+
return normalizeLocale(requested);
|
|
465
|
+
if (existing)
|
|
466
|
+
return existing;
|
|
467
|
+
if (process.stdin.isTTY !== true || getOutputMode() === 'json') {
|
|
468
|
+
throw new StorystickError('Pass --locale with the primary BCP 47 language tag outside an interactive terminal.', { code: 'INIT_LOCALE_REQUIRED', category: 'validation' });
|
|
469
|
+
}
|
|
470
|
+
const detected = normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale || 'en');
|
|
471
|
+
try {
|
|
472
|
+
const locale = await promptText('Primary site locale', {
|
|
473
|
+
placeholder: detected,
|
|
474
|
+
validate: (value) => {
|
|
475
|
+
try {
|
|
476
|
+
normalizeLocale(value);
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
return error instanceof Error ? error.message : String(error);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
return normalizeLocale(locale);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
maybeExitOnCancel(error);
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function resolveSiteUrl(requested, existingReceipt) {
|
|
492
|
+
if (requested?.trim())
|
|
493
|
+
return normalizeSiteUrl(requested);
|
|
494
|
+
if (existingReceipt?.siteUrl)
|
|
495
|
+
return existingReceipt.siteUrl;
|
|
496
|
+
if (process.stdin.isTTY !== true || getOutputMode() === 'json')
|
|
497
|
+
return undefined;
|
|
498
|
+
try {
|
|
499
|
+
const value = await promptText('Production site URL (optional)', {
|
|
500
|
+
validate: (candidate) => {
|
|
501
|
+
if (!candidate.trim())
|
|
502
|
+
return undefined;
|
|
503
|
+
try {
|
|
504
|
+
normalizeSiteUrl(candidate);
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
return error instanceof Error ? error.message : String(error);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
return value.trim() ? normalizeSiteUrl(value) : undefined;
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
maybeExitOnCancel(error);
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function normalizeLocale(raw) {
|
|
520
|
+
try {
|
|
521
|
+
return canonicalLocaleCode(raw);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
throw new StorystickError('--locale must be a BCP 47 language tag such as fr, en-GB or zh-Hant-TW.', { code: 'INVALID_LOCALE', category: 'validation' });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function localeDisplayName(locale) {
|
|
528
|
+
try {
|
|
529
|
+
const label = new Intl.DisplayNames(['en'], { type: 'language' }).of(locale);
|
|
530
|
+
return label && label.length <= 80 ? label : locale;
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
return locale;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function normalizeSiteUrl(raw) {
|
|
537
|
+
let url;
|
|
538
|
+
try {
|
|
539
|
+
url = new URL(raw);
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
throw new StorystickError('--site-url must be an HTTP(S) site origin.', {
|
|
543
|
+
code: 'INVALID_SITE_URL',
|
|
544
|
+
category: 'validation',
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if (!['http:', 'https:'].includes(url.protocol) ||
|
|
548
|
+
url.pathname !== '/' ||
|
|
549
|
+
url.search ||
|
|
550
|
+
url.hash ||
|
|
551
|
+
url.username ||
|
|
552
|
+
url.password) {
|
|
553
|
+
throw new StorystickError('--site-url must be an HTTP(S) origin without a path.', { code: 'INVALID_SITE_URL', category: 'validation' });
|
|
554
|
+
}
|
|
555
|
+
return url.origin;
|
|
556
|
+
}
|
|
557
|
+
function writeInitSiteEnvironment(receipt) {
|
|
558
|
+
assertInitSiteEnvironmentCompatible(receipt);
|
|
559
|
+
const path = join(receipt.targetDir, '.env.local');
|
|
560
|
+
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
561
|
+
const values = desiredSiteEnvironment(receipt);
|
|
562
|
+
const newline = current.includes('\r\n') ? '\r\n' : '\n';
|
|
563
|
+
const body = current.replace(/(?:\r?\n)+$/u, '');
|
|
564
|
+
const lines = body ? body.split(/\r?\n/u) : [];
|
|
565
|
+
const missing = [];
|
|
566
|
+
for (const [key, value] of Object.entries(values)) {
|
|
567
|
+
const index = lines.findIndex((line) => parseEnvAssignment(line)?.key === key);
|
|
568
|
+
if (index >= 0) {
|
|
569
|
+
lines[index] = `${key}=${value}`;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
missing.push(`${key}=${value}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (missing.length > 0) {
|
|
576
|
+
if (lines.length > 0 && lines.at(-1) !== '')
|
|
577
|
+
lines.push('');
|
|
578
|
+
lines.push(...missing);
|
|
579
|
+
}
|
|
580
|
+
const content = `${lines.join(newline)}${newline}`;
|
|
581
|
+
writeFileSync(path, content, { encoding: 'utf8', mode: 0o600 });
|
|
582
|
+
}
|
|
583
|
+
function assertInitSiteEnvironmentCompatible(receipt) {
|
|
584
|
+
const path = join(receipt.targetDir, '.env.local');
|
|
585
|
+
if (!existsSync(path))
|
|
586
|
+
return;
|
|
587
|
+
const assignments = readFileSync(path, 'utf8')
|
|
588
|
+
.split(/\r?\n/u)
|
|
589
|
+
.map(parseEnvAssignment)
|
|
590
|
+
.filter((value) => value !== null);
|
|
591
|
+
for (const [key, value] of Object.entries(desiredSiteEnvironment(receipt))) {
|
|
592
|
+
const matches = assignments.filter((entry) => entry.key === key);
|
|
593
|
+
if (matches.length > 1) {
|
|
594
|
+
throw new StorystickError(`${key} is declared more than once in .env.local.`, { code: 'INIT_SITE_CONFIG_CONFLICT', category: 'conflict' });
|
|
595
|
+
}
|
|
596
|
+
const existing = matches[0]?.value;
|
|
597
|
+
if (existing && existing !== value) {
|
|
598
|
+
throw new StorystickError(`${key} already has a different value in .env.local.`, { code: 'INIT_SITE_CONFIG_CONFLICT', category: 'conflict' });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function desiredSiteEnvironment(receipt) {
|
|
603
|
+
return {
|
|
604
|
+
...(receipt.siteUrl ? { STORYSTICK_SITE_URL: receipt.siteUrl } : {}),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
async function promptForIntent(team) {
|
|
608
|
+
try {
|
|
609
|
+
const choice = await promptSelect('What do you want to do?', [
|
|
610
|
+
{ value: 'create', name: 'Create a new Storystick project' },
|
|
611
|
+
{ value: 'link', name: 'Link to an existing project' },
|
|
612
|
+
]);
|
|
613
|
+
if (choice === 'link') {
|
|
614
|
+
const shortId = await promptText('Project shortId', {
|
|
615
|
+
validate: (value) => {
|
|
616
|
+
try {
|
|
617
|
+
parseProjectShortId(value);
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
return error instanceof Error ? error.message : String(error);
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
return { kind: 'link', requestedShortId: parseProjectShortId(shortId) };
|
|
626
|
+
}
|
|
627
|
+
const projectName = await promptText('Project name', {
|
|
628
|
+
placeholder: 'my-project',
|
|
629
|
+
validate: (value) => value.trim().length > 0 ? undefined : 'Project name is required',
|
|
630
|
+
});
|
|
631
|
+
return createIntent(projectName, team);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
maybeExitOnCancel(error);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function createIntent(projectName, team) {
|
|
639
|
+
const normalized = projectName.trim();
|
|
640
|
+
if (!normalized) {
|
|
641
|
+
throw new StorystickError('Project name is required.', {
|
|
642
|
+
code: 'INVALID_PROJECT_NAME',
|
|
643
|
+
category: 'validation',
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
kind: 'create',
|
|
648
|
+
projectName: normalized,
|
|
649
|
+
...(team?.trim() ? { requestedTeamShortId: team.trim() } : {}),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function normalizeOrigin(raw) {
|
|
653
|
+
let url;
|
|
654
|
+
try {
|
|
655
|
+
url = new URL(raw);
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
throw new StorystickError('--origin must be an HTTP(S) origin.', {
|
|
659
|
+
code: 'INVALID_ORIGIN',
|
|
660
|
+
category: 'validation',
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') ||
|
|
664
|
+
url.pathname !== '/' ||
|
|
665
|
+
url.search ||
|
|
666
|
+
url.hash ||
|
|
667
|
+
url.username ||
|
|
668
|
+
url.password) {
|
|
669
|
+
throw new StorystickError('--origin must be an HTTP(S) origin without a path.', {
|
|
670
|
+
code: 'INVALID_ORIGIN',
|
|
671
|
+
category: 'validation',
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return url.origin;
|
|
675
|
+
}
|
|
676
|
+
function toSlug(name) {
|
|
677
|
+
return name
|
|
678
|
+
.toLowerCase()
|
|
679
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
680
|
+
.replace(/^-|-$/g, '');
|
|
681
|
+
}
|
|
682
|
+
export function relativeTarget(targetDir, cwd = process.cwd()) {
|
|
683
|
+
const target = resolve(targetDir);
|
|
684
|
+
const current = resolve(cwd);
|
|
685
|
+
if (target === current)
|
|
686
|
+
return '.';
|
|
687
|
+
const candidate = relative(current, target);
|
|
688
|
+
return candidate === '..' || candidate.startsWith(`..${sep}`)
|
|
689
|
+
? target
|
|
690
|
+
: candidate;
|
|
691
|
+
}
|
|
692
|
+
async function logAuthenticatedUser(rootDir) {
|
|
693
|
+
const currentUser = await whoami({ rootDir });
|
|
694
|
+
if (currentUser?.email) {
|
|
695
|
+
logMessage(`Authenticated as ${pc.bold(currentUser.email)}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function eligibleTeamChoices(teams) {
|
|
699
|
+
return teams
|
|
700
|
+
.map((team) => ` ${team.name} (${team.shortId}) — ${team.role}${team.isPersonal ? ', personal' : ''}`)
|
|
701
|
+
.join('\n');
|
|
702
|
+
}
|
|
703
|
+
export async function selectEligibleProjectTeam(teams, options) {
|
|
704
|
+
const requestedShortId = options.requestedShortId?.trim();
|
|
705
|
+
if (requestedShortId) {
|
|
706
|
+
const requested = teams.find((team) => team.shortId === requestedShortId);
|
|
707
|
+
if (requested)
|
|
708
|
+
return requested;
|
|
709
|
+
const suffix = teams.length > 0 ? `\nEligible Teams:\n${eligibleTeamChoices(teams)}` : '';
|
|
710
|
+
throw new StorystickError(`Team "${requestedShortId}" is not eligible for project creation.${suffix}`, { code: 'TEAM_NOT_ELIGIBLE', category: 'validation' });
|
|
711
|
+
}
|
|
712
|
+
if (options.newTeamName)
|
|
713
|
+
return { newTeamName: options.newTeamName };
|
|
714
|
+
if (!options.interactive) {
|
|
715
|
+
throw new StorystickError('Choose --team <teamShortId> or --new-team <name> before creating a project.', { code: 'TEAM_REQUIRED', category: 'validation' });
|
|
716
|
+
}
|
|
717
|
+
const selectedShortId = await (options.prompt ?? promptSelect)('Select the Team that will own this project:', [...teams.map((team) => ({
|
|
718
|
+
value: team.shortId,
|
|
719
|
+
name: `${team.name} (${team.shortId})`,
|
|
720
|
+
description: `${team.role}${team.isPersonal ? ', personal' : ''}`,
|
|
721
|
+
})), { value: '__create_team__', name: 'Create a new team', description: 'You will own the new team' }]);
|
|
722
|
+
if (selectedShortId === '__create_team__') {
|
|
723
|
+
const name = (await promptText('Team name')).trim();
|
|
724
|
+
if (!name || name.length > 120)
|
|
725
|
+
throw new StorystickError('Team name must contain between 1 and 120 characters.', { code: 'TEAM_NAME_INVALID', category: 'validation' });
|
|
726
|
+
return { newTeamName: name };
|
|
727
|
+
}
|
|
728
|
+
const selected = teams.find((team) => team.shortId === selectedShortId);
|
|
729
|
+
if (!selected) {
|
|
730
|
+
throw new StorystickError('The selected Team is no longer eligible.', {
|
|
731
|
+
code: 'TEAM_NOT_ELIGIBLE',
|
|
732
|
+
category: 'validation',
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
return selected;
|
|
736
|
+
}
|
|
737
|
+
export async function fetchEligibleProjectTeams(signal, rootDir) {
|
|
738
|
+
return await callHubApi('/api/cli/teams/eligible-for-project-creation', {
|
|
739
|
+
rootDir,
|
|
740
|
+
onAuthPrompt: promptDeviceAuth,
|
|
741
|
+
signal,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
export function printInitOutro(args) {
|
|
745
|
+
if (getOutputMode() === 'json') {
|
|
746
|
+
emitJsonResult({
|
|
747
|
+
command: 'init',
|
|
748
|
+
ok: true,
|
|
749
|
+
data: {
|
|
750
|
+
outcome: 'complete',
|
|
751
|
+
projectName: args.projectName,
|
|
752
|
+
shortId: args.shortId,
|
|
753
|
+
dirName: args.dirName,
|
|
754
|
+
team: args.team,
|
|
755
|
+
receipt: args.receipt,
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
logFinishedStep(args.linked
|
|
761
|
+
? `Linked to ${pc.bold(args.projectName)} (${args.shortId})`
|
|
762
|
+
: `Project ${pc.bold(args.projectName)} (${args.shortId}) created`);
|
|
763
|
+
if (args.team) {
|
|
764
|
+
logMessage(`Team: ${pc.bold(args.team.name)} (${args.team.shortId})`);
|
|
765
|
+
}
|
|
766
|
+
logMessage('');
|
|
767
|
+
logMessage('Next steps:');
|
|
768
|
+
if (args.dirName !== '.') {
|
|
769
|
+
logMessage(` ${pc.bold(`cd ${shellArgument(args.dirName)}`)}`);
|
|
770
|
+
}
|
|
771
|
+
const devOrigin = args.receipt?.origins[0] ? new URL(args.receipt.origins[0]) : null;
|
|
772
|
+
const local = devOrigin && ['localhost', '127.0.0.1', '[::1]'].includes(devOrigin.hostname) && devOrigin.protocol === 'http:';
|
|
773
|
+
logMessage(` ${pc.bold(local ? `npm run dev -- --port ${devOrigin.port || '80'}` : 'npm run dev')}`);
|
|
774
|
+
if (!args.linked) {
|
|
775
|
+
logMessage('Your project has content types but no pages yet. Create a page in the Studio, then open Preview.');
|
|
776
|
+
logMessage(` ${resolveCliRuntimeConfig().hubUrl}/p/${args.shortId}`);
|
|
777
|
+
}
|
|
778
|
+
logMessage('');
|
|
779
|
+
logMessage(`${pc.yellow('!')} Your ${pc.bold('.env.local')} contains a secret API key (STORYSTICK_API_KEY).`);
|
|
780
|
+
logMessage(' It is in .gitignore — keep it that way. Never commit it.');
|
|
781
|
+
logMessage('');
|
|
782
|
+
const hubUrl = resolveCliRuntimeConfig().hubUrl;
|
|
783
|
+
logMessage(`${pc.yellow('!')} Manage keys in the Hub:`);
|
|
784
|
+
logMessage(` ${hubUrl}/p/${args.shortId}/settings/api-keys`);
|
|
785
|
+
logMessage('');
|
|
786
|
+
}
|
|
787
|
+
function shellArgument(value) {
|
|
788
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(value) ? value : shellQuote(value);
|
|
789
|
+
}
|