@storystick/cli 0.0.0-stage → 0.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CONTENT-IMPORT.md +349 -0
  2. package/LICENSE +112 -0
  3. package/PUBLIC-API.md +30 -0
  4. package/README.md +263 -2
  5. package/assets/next-v1/app/[[...pathname]]/error.tsx +10 -0
  6. package/assets/next-v1/app/[[...pathname]]/not-found.tsx +8 -0
  7. package/assets/next-v1/app/[[...pathname]]/page.tsx +38 -0
  8. package/assets/next-v1/app/api/storystick/[...action]/route.ts +4 -0
  9. package/assets/next-v1/app/layout.tsx +28 -0
  10. package/assets/next-v1/app/sitemap.ts +3 -0
  11. package/assets/next-v1/env.local.example +13 -0
  12. package/assets/next-v1/globals.css +17 -0
  13. package/assets/next-v1/lib/site.ts +10 -0
  14. package/assets/next-v1/next.config.ts +4 -0
  15. package/assets/skills/storystick-development/SKILL.md +79 -0
  16. package/assets/skills/storystick-development/references/setup.md +152 -0
  17. package/assets/skills/storystick-development/references/story-and-next.md +49 -0
  18. package/dist/_backfill-recommendation.d.ts +12 -0
  19. package/dist/_backfill-recommendation.js +18 -0
  20. package/dist/asset-import.d.ts +50 -0
  21. package/dist/asset-import.js +160 -0
  22. package/dist/assistant-read-client.d.ts +3 -0
  23. package/dist/assistant-read-client.js +8 -0
  24. package/dist/auth-device.d.ts +153 -0
  25. package/dist/auth-device.js +390 -0
  26. package/dist/bootstrap-client.d.ts +52 -0
  27. package/dist/bootstrap-client.js +19 -0
  28. package/dist/cli-error.d.ts +54 -0
  29. package/dist/cli-error.js +107 -0
  30. package/dist/cli.d.ts +68 -0
  31. package/dist/cli.js +1520 -0
  32. package/dist/commands/archive.d.ts +17 -0
  33. package/dist/commands/archive.js +86 -0
  34. package/dist/commands/backfill-refs.d.ts +54 -0
  35. package/dist/commands/backfill-refs.js +217 -0
  36. package/dist/commands/connect.d.ts +11 -0
  37. package/dist/commands/connect.js +124 -0
  38. package/dist/commands/connection.d.ts +57 -0
  39. package/dist/commands/connection.js +83 -0
  40. package/dist/commands/content.d.ts +11 -0
  41. package/dist/commands/content.js +107 -0
  42. package/dist/commands/delivery.d.ts +22 -0
  43. package/dist/commands/delivery.js +99 -0
  44. package/dist/commands/info.d.ts +35 -0
  45. package/dist/commands/info.js +62 -0
  46. package/dist/commands/list.d.ts +28 -0
  47. package/dist/commands/list.js +116 -0
  48. package/dist/commands/migrate.d.ts +80 -0
  49. package/dist/commands/migrate.js +550 -0
  50. package/dist/commands/release.d.ts +8 -0
  51. package/dist/commands/release.js +60 -0
  52. package/dist/commands/setup.d.ts +6 -0
  53. package/dist/commands/setup.js +58 -0
  54. package/dist/commands/slice-previews.d.ts +17 -0
  55. package/dist/commands/slice-previews.js +233 -0
  56. package/dist/commands/source.d.ts +82 -0
  57. package/dist/commands/source.js +192 -0
  58. package/dist/content-import.d.ts +78 -0
  59. package/dist/content-import.js +210 -0
  60. package/dist/generated-transaction.d.ts +22 -0
  61. package/dist/generated-transaction.js +330 -0
  62. package/dist/hub-client.d.ts +72 -0
  63. package/dist/hub-client.js +178 -0
  64. package/dist/init-operation.d.ts +98 -0
  65. package/dist/init-operation.js +427 -0
  66. package/dist/init-readiness.d.ts +10 -0
  67. package/dist/init-readiness.js +50 -0
  68. package/dist/init-scaffold.d.ts +54 -0
  69. package/dist/init-scaffold.js +439 -0
  70. package/dist/init.d.ts +51 -0
  71. package/dist/init.js +789 -0
  72. package/dist/mcp-execution.d.ts +15 -0
  73. package/dist/mcp-execution.js +90 -0
  74. package/dist/mcp.d.ts +7 -0
  75. package/dist/mcp.js +41 -0
  76. package/dist/migration-artifact.d.ts +13 -0
  77. package/dist/migration-artifact.js +154 -0
  78. package/dist/operations-client.d.ts +137 -0
  79. package/dist/operations-client.js +130 -0
  80. package/dist/output.d.ts +347 -0
  81. package/dist/output.js +771 -0
  82. package/dist/project-access.d.ts +18 -0
  83. package/dist/project-access.js +1 -0
  84. package/dist/project-connection.d.ts +62 -0
  85. package/dist/project-connection.js +347 -0
  86. package/dist/project-execution-client.d.ts +2 -0
  87. package/dist/project-execution-client.js +33 -0
  88. package/dist/push-client.d.ts +100 -0
  89. package/dist/push-client.js +160 -0
  90. package/dist/release-context.d.ts +13 -0
  91. package/dist/release-context.js +39 -0
  92. package/dist/release-schema-client.d.ts +16 -0
  93. package/dist/release-schema-client.js +33 -0
  94. package/dist/release-sync.d.ts +36 -0
  95. package/dist/release-sync.js +31 -0
  96. package/dist/resolve.d.ts +15 -0
  97. package/dist/resolve.js +80 -0
  98. package/dist/runtime-config.d.ts +18 -0
  99. package/dist/runtime-config.js +86 -0
  100. package/dist/schema-export.d.ts +10 -0
  101. package/dist/schema-export.js +28 -0
  102. package/dist/setup.d.ts +65 -0
  103. package/dist/setup.js +135 -0
  104. package/dist/source-git.d.ts +36 -0
  105. package/dist/source-git.js +197 -0
  106. package/dist/starter-resolver.d.ts +82 -0
  107. package/dist/starter-resolver.js +495 -0
  108. package/dist/sync-transaction.d.ts +29 -0
  109. package/dist/sync-transaction.js +159 -0
  110. package/dist/transport.d.ts +8 -0
  111. package/dist/transport.js +73 -0
  112. package/examples/sanity/README.md +79 -0
  113. package/examples/sanity/convert.mjs +228 -0
  114. package/examples/sanity/fixture/data.ndjson +7 -0
  115. package/examples/sanity/fixture/mapping.json +44 -0
  116. package/examples/sanity/fixture/originals/mark.svg +1 -0
  117. package/examples/sanity/stories/articleBody.story +23 -0
  118. package/examples/sanity/stories/copy.story +7 -0
  119. package/examples/sanity/stories/page.story +19 -0
  120. package/examples/sanity/stories/picture.story +7 -0
  121. package/examples/sanity/stories/settings.story +9 -0
  122. package/examples/sanity/stories/stack.story +10 -0
  123. package/package.json +75 -3
@@ -0,0 +1,58 @@
1
+ import { resolve } from "node:path";
2
+ import { inspectSetup } from "../setup.js";
3
+ import { callHubApi } from "../hub-client.js";
4
+ import { resolveCliRuntimeConfig } from "../runtime-config.js";
5
+ import { shellQuote } from "../init-operation.js";
6
+ import { StorystickError } from "../cli-error.js";
7
+ import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logMessage, withInterrupt, } from "../output.js";
8
+ export async function runSetup(options) {
9
+ configureOutput({ json: options.json === true });
10
+ const { signal, dispose } = withInterrupt();
11
+ try {
12
+ const root = resolve(options.root ?? ".");
13
+ const local = inspectSetup(root);
14
+ const projectShortId = options.project ?? local.connection?.projectShortId;
15
+ if (options.remote && !projectShortId)
16
+ throw new StorystickError("Connect a project or pass --project <shortId> to inspect its remote setup.", {
17
+ code: "PROJECT_NOT_CONNECTED",
18
+ category: "validation",
19
+ retryable: false,
20
+ });
21
+ const remote = options.remote
22
+ ? await callHubApi("/api/cli/project-setup", {
23
+ rootDir: root,
24
+ gatewayUrl: resolveCliRuntimeConfig({ rootDir: root }).gatewayUrl,
25
+ method: "POST",
26
+ body: { projectShortId },
27
+ signal,
28
+ interactiveAuth: false,
29
+ })
30
+ : null;
31
+ const data = { ...local, remote };
32
+ if (getOutputMode() === "json")
33
+ emitJsonResult({ command: "setup", ok: true, data });
34
+ else {
35
+ logMessage(`Project: ${projectShortId ?? "not connected"}`);
36
+ logMessage(`Website: ${local.application.framework ?? "not detected"}`);
37
+ logMessage(`Source: ${local.source.configured ? "configured locally" : "not configured"}`);
38
+ if (local.repository?.dirty)
39
+ logMessage("Local changes are present. Source transfer uses committed files only.");
40
+ if (local.hosting)
41
+ logMessage(`Hosting: Vercel ${local.hosting.projectId} (local link; access not verified)`);
42
+ for (const issue of local.issues)
43
+ logMessage(`${issue.path}: ${issue.message}`);
44
+ for (const step of local.next)
45
+ logMessage(`${step.reason}\n ${step.command} ${step.args.map((arg) => (/^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : shellQuote(arg))).join(" ")}`);
46
+ if (remote)
47
+ logMessage(JSON.stringify(remote, null, 2));
48
+ }
49
+ }
50
+ catch (err) {
51
+ if (getOutputMode() === "json")
52
+ exitJsonFailure({ command: "setup", err });
53
+ exitWithError(err);
54
+ }
55
+ finally {
56
+ dispose();
57
+ }
58
+ }
@@ -0,0 +1,17 @@
1
+ export type SlicePreviewManifestEntry = {
2
+ sliceType: string;
3
+ variationId?: string;
4
+ file: string;
5
+ };
6
+ export interface SlicePreviewSyncOptions {
7
+ root?: string;
8
+ manifest?: string;
9
+ projectShortId?: string;
10
+ dryRun?: boolean;
11
+ json?: boolean;
12
+ quiet?: boolean;
13
+ verbose?: boolean;
14
+ fetchImpl?: typeof fetch;
15
+ }
16
+ export declare function runSlicePreviewsSync(options?: SlicePreviewSyncOptions): Promise<void>;
17
+ export declare function readSlicePreviewManifest(manifestPath: string): Promise<SlicePreviewManifestEntry[]>;
@@ -0,0 +1,233 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, realpath } from 'node:fs/promises';
3
+ import { basename, relative, resolve, sep } from 'node:path';
4
+ import { StorystickError } from '../cli-error.js';
5
+ import { callHubApi } from '../hub-client.js';
6
+ import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logFinishedStep, logMessage, logWarning, withInterrupt, } from '../output.js';
7
+ import { requireProjectConnection } from '../project-connection.js';
8
+ import { fetchWithDeadline } from '../transport.js';
9
+ export async function runSlicePreviewsSync(options = {}) {
10
+ configureOutput({
11
+ json: options.json === true,
12
+ quiet: options.quiet === true,
13
+ verbose: options.verbose === true,
14
+ });
15
+ const { signal, dispose } = withInterrupt();
16
+ try {
17
+ const rootDir = resolve(options.root ?? '.');
18
+ const canonicalRootDir = await realpath(rootDir);
19
+ const manifestPath = resolve(rootDir, options.manifest ?? 'slice-previews.json');
20
+ const projectShortId = options.projectShortId?.trim() ||
21
+ requireProjectConnection(rootDir).shortId;
22
+ const entries = await readSlicePreviewManifest(manifestPath);
23
+ const planned = await Promise.all(entries.map(async (entry) => {
24
+ const requestedFilePath = resolve(rootDir, entry.file);
25
+ const filePath = await realpath(requestedFilePath);
26
+ const pathFromRoot = relative(canonicalRootDir, filePath);
27
+ if (pathFromRoot === '..' ||
28
+ pathFromRoot.startsWith(`..${sep}`) ||
29
+ resolve(canonicalRootDir, pathFromRoot) !== filePath) {
30
+ throw new StorystickError(`Slice capture must stay inside the project root: ${entry.file}`, { code: 'SLICE_PREVIEW_PATH_OUTSIDE_ROOT' });
31
+ }
32
+ const bytes = new Uint8Array(await readFile(filePath));
33
+ return {
34
+ ...entry,
35
+ filePath,
36
+ bytes,
37
+ mimeType: mimeTypeForPath(filePath),
38
+ checksum: createHash('sha256').update(bytes).digest('hex'),
39
+ };
40
+ }));
41
+ if (options.dryRun === true) {
42
+ const data = {
43
+ status: 'planned',
44
+ projectShortId,
45
+ manifest: manifestPath,
46
+ total: planned.length,
47
+ items: planned.map(({ bytes: _bytes, filePath, checksum, ...entry }) => ({
48
+ ...entry,
49
+ file: filePath,
50
+ checksum,
51
+ })),
52
+ };
53
+ if (getOutputMode() === 'json') {
54
+ emitJsonResult({ command: 'slice-previews sync', ok: true, data });
55
+ }
56
+ else {
57
+ logFinishedStep(`Validated ${planned.length} slice capture(s); no upload performed.`);
58
+ }
59
+ return;
60
+ }
61
+ const results = [];
62
+ for (const item of planned) {
63
+ const begun = await callHubApi('/api/cli/slice-previews/begin-upload', {
64
+ method: 'POST',
65
+ body: {
66
+ projectShortId,
67
+ sliceType: item.sliceType,
68
+ ...(item.variationId !== undefined
69
+ ? { variationId: item.variationId }
70
+ : {}),
71
+ originalFilename: basename(item.filePath),
72
+ declaredMimeType: item.mimeType,
73
+ declaredSizeBytes: item.bytes.byteLength,
74
+ declaredChecksum: item.checksum,
75
+ },
76
+ rootDir,
77
+ signal,
78
+ fetchImpl: options.fetchImpl,
79
+ });
80
+ if (begun.status === 'unchanged') {
81
+ results.push({
82
+ sliceType: item.sliceType,
83
+ ...(item.variationId !== undefined
84
+ ? { variationId: item.variationId }
85
+ : {}),
86
+ status: 'unchanged',
87
+ });
88
+ continue;
89
+ }
90
+ if (typeof begun.contentShapeCapability !== 'string' ||
91
+ !begun.contentShapeCapability.trim()) {
92
+ throw new StorystickError('The upload response is missing its content-shape capability. Restart the sync.', {
93
+ code: 'CONTENT_SHAPE_RELOAD_REQUIRED',
94
+ });
95
+ }
96
+ const response = await fetchWithDeadline(begun.uploadUrl, {
97
+ method: begun.uploadMethod,
98
+ headers: begun.uploadHeaders,
99
+ body: item.bytes,
100
+ }, { fetchImpl: options.fetchImpl, signal });
101
+ if (!response.ok) {
102
+ throw new StorystickError(`Slice capture upload returned HTTP ${response.status}.`, { code: 'SLICE_PREVIEW_UPLOAD_FAILED', status: response.status });
103
+ }
104
+ const finalized = await callHubApi('/api/cli/slice-previews/finalize-upload', {
105
+ method: 'POST',
106
+ body: { projectShortId, uploadSessionId: begun.uploadSessionId },
107
+ contentShapeCapability: begun.contentShapeCapability,
108
+ rootDir,
109
+ signal,
110
+ fetchImpl: options.fetchImpl,
111
+ });
112
+ results.push({
113
+ sliceType: item.sliceType,
114
+ ...(item.variationId !== undefined
115
+ ? { variationId: item.variationId }
116
+ : {}),
117
+ status: finalized.status === 'finalized' ? 'uploaded' : 'superseded',
118
+ });
119
+ }
120
+ const data = {
121
+ status: 'synced',
122
+ projectShortId,
123
+ manifest: manifestPath,
124
+ total: results.length,
125
+ uploaded: results.filter((item) => item.status === 'uploaded').length,
126
+ unchanged: results.filter((item) => item.status === 'unchanged').length,
127
+ superseded: results.filter((item) => item.status === 'superseded').length,
128
+ items: results,
129
+ };
130
+ if (getOutputMode() === 'json') {
131
+ emitJsonResult({ command: 'slice-previews sync', ok: true, data });
132
+ return;
133
+ }
134
+ logFinishedStep(`Synced ${data.total} slice capture(s): ${data.uploaded} uploaded, ${data.unchanged} unchanged.`);
135
+ if (data.superseded > 0) {
136
+ logWarning(`${data.superseded} upload(s) were superseded by a newer capture.`);
137
+ }
138
+ if (options.verbose)
139
+ logMessage(`Manifest: ${manifestPath}`);
140
+ }
141
+ catch (error) {
142
+ if (getOutputMode() === 'json') {
143
+ exitJsonFailure({ command: 'slice-previews sync', err: error });
144
+ }
145
+ exitWithError(error);
146
+ }
147
+ finally {
148
+ dispose();
149
+ }
150
+ }
151
+ export async function readSlicePreviewManifest(manifestPath) {
152
+ let raw;
153
+ try {
154
+ raw = JSON.parse(await readFile(manifestPath, 'utf8'));
155
+ }
156
+ catch (error) {
157
+ throw new StorystickError(`Could not read slice preview manifest at ${manifestPath}.`, { code: 'SLICE_PREVIEW_MANIFEST_INVALID', cause: error });
158
+ }
159
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
160
+ throw manifestError('The manifest root must be an object.');
161
+ const manifest = raw;
162
+ const rootKeys = new Set(['version', 'previews']);
163
+ if (Object.keys(manifest).some((key) => !rootKeys.has(key))) {
164
+ throw manifestError('The manifest contains an unknown root property.');
165
+ }
166
+ if (manifest.version !== 1)
167
+ throw manifestError('The manifest version must be 1.');
168
+ if (!Array.isArray(manifest.previews) || manifest.previews.length === 0) {
169
+ throw manifestError('The manifest must contain at least one preview.');
170
+ }
171
+ const entries = [];
172
+ const subjects = new Set();
173
+ for (const [index, value] of manifest.previews.entries()) {
174
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
175
+ throw manifestError(`Preview at index ${index} must be an object.`);
176
+ }
177
+ const record = value;
178
+ const allowedKeys = new Set(['slice', 'variation', 'file']);
179
+ if (Object.keys(record).some((key) => !allowedKeys.has(key))) {
180
+ throw manifestError(`Preview at index ${index} has an unknown property.`);
181
+ }
182
+ if (typeof record.slice !== 'string' || !isIdentifier(record.slice)) {
183
+ throw manifestError(`Preview at index ${index} has an invalid slice ID.`);
184
+ }
185
+ if (record.variation !== undefined) {
186
+ if (typeof record.variation !== 'string' ||
187
+ !isIdentifier(record.variation) ||
188
+ record.variation === 'default') {
189
+ throw manifestError(`Preview at index ${index} has an invalid variation ID.`);
190
+ }
191
+ }
192
+ pushEntry(entries, subjects, {
193
+ sliceType: record.slice,
194
+ ...(typeof record.variation === 'string'
195
+ ? { variationId: record.variation }
196
+ : {}),
197
+ file: requireFile(record.file),
198
+ });
199
+ }
200
+ return entries;
201
+ }
202
+ function pushEntry(entries, subjects, entry) {
203
+ const key = `${entry.sliceType}:${entry.variationId ?? ''}`;
204
+ if (subjects.has(key))
205
+ throw manifestError(`Duplicate slice subject “${key}”.`);
206
+ subjects.add(key);
207
+ entries.push(entry);
208
+ }
209
+ function requireFile(value) {
210
+ if (typeof value !== 'string' || value.trim().length === 0) {
211
+ throw manifestError('Every capture path must be a non-empty string.');
212
+ }
213
+ return value;
214
+ }
215
+ function isIdentifier(value) {
216
+ return /^[A-Za-z][A-Za-z0-9_-]*$/u.test(value);
217
+ }
218
+ function mimeTypeForPath(path) {
219
+ if (/\.png$/iu.test(path))
220
+ return 'image/png';
221
+ if (/\.jpe?g$/iu.test(path))
222
+ return 'image/jpeg';
223
+ if (/\.webp$/iu.test(path))
224
+ return 'image/webp';
225
+ throw new StorystickError(`Unsupported slice capture format: ${path}`, {
226
+ code: 'SLICE_PREVIEW_FORMAT_UNSUPPORTED',
227
+ });
228
+ }
229
+ function manifestError(message) {
230
+ return new StorystickError(message, {
231
+ code: 'SLICE_PREVIEW_MANIFEST_INVALID',
232
+ });
233
+ }
@@ -0,0 +1,82 @@
1
+ type Options = {
2
+ root?: string;
3
+ project?: string;
4
+ config?: string;
5
+ ref?: string;
6
+ out?: string;
7
+ change?: string;
8
+ expectedRevision?: string;
9
+ requestId?: string;
10
+ json?: boolean;
11
+ dryRun?: boolean;
12
+ schema?: string;
13
+ appRoot?: string;
14
+ connectionRevision?: string;
15
+ repairCommand?: string;
16
+ repairCwd?: string;
17
+ };
18
+ export declare function prepareSource(root: string, configPath?: string, ref?: string): {
19
+ config: {
20
+ scope: {
21
+ appRoot: string;
22
+ include: string[];
23
+ exclude: string[];
24
+ };
25
+ schemaVersion: string;
26
+ recipe: {
27
+ schemaCheck: string;
28
+ install: {
29
+ argv: string[];
30
+ cwd?: string | undefined;
31
+ }[];
32
+ checks: {
33
+ argv: string[];
34
+ name: string;
35
+ cwd?: string | undefined;
36
+ }[];
37
+ preview: {
38
+ argv: string[];
39
+ port: number;
40
+ cwd?: string | undefined;
41
+ readyPath?: string | undefined;
42
+ };
43
+ allowedHosts: string[];
44
+ schemaOutput?: string | undefined;
45
+ env?: Record<string, string> | undefined;
46
+ };
47
+ };
48
+ snapshot: {
49
+ commit: string;
50
+ scope: {
51
+ appRoot: string;
52
+ include: string[];
53
+ exclude: string[];
54
+ };
55
+ files: {
56
+ path: string;
57
+ mode: number;
58
+ content: Buffer<ArrayBuffer>;
59
+ digest: string;
60
+ }[];
61
+ revision: string;
62
+ };
63
+ archive: NonSharedBuffer;
64
+ summary: {
65
+ commit: string;
66
+ revision: string;
67
+ scope: {
68
+ appRoot: string;
69
+ include: string[];
70
+ exclude: string[];
71
+ };
72
+ schemaVersion: string;
73
+ files: string[];
74
+ fileCount: number;
75
+ archiveBytes: number;
76
+ dirty: boolean;
77
+ note: string;
78
+ };
79
+ };
80
+ export declare function readSourceChange(directory: string): any;
81
+ export declare function runSource(operation: string, workspaceId: string | undefined, options: Options): Promise<void>;
82
+ export {};
@@ -0,0 +1,192 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, renameSync, rmSync, existsSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { sourceChangeIdentity, sourceChange, sourceScope, MAX_ARCHIVE_BYTES } from '@storystick/source/manifest';
5
+ import { encodeSourceArchive, sourceDigest, sourceRevision, validateWorkspaceArchive } from '@storystick/source/archive';
6
+ import { captureCommit, integrateSource } from '../source-git.js';
7
+ import { callHubApi } from '../hub-client.js';
8
+ import { fetchWithDeadline } from '../transport.js';
9
+ import { resolveCliRuntimeConfig } from '../runtime-config.js';
10
+ import { resolveProjectConnection } from '../project-connection.js';
11
+ import { sourceConfiguration } from '@storystick/source/recipe';
12
+ import { configureSource, configureNextSource } from '../setup.js';
13
+ import { inspectRepository } from '../source-git.js';
14
+ import { configureOutput, emitJsonResult, exitJsonFailure, exitWithError, getOutputMode, logMessage, withInterrupt } from '../output.js';
15
+ export function prepareSource(root, configPath = '.storystick/source.json', ref = 'HEAD') {
16
+ const config = sourceConfiguration.parse(JSON.parse(readFileSync(resolve(root, configPath), 'utf8')));
17
+ const snapshot = captureCommit(root, ref, config.scope);
18
+ if (!snapshot.files.length)
19
+ throw Error('Source scope contains no committed files. Review the scope and commit the intended source first.');
20
+ const archive = encodeSourceArchive(snapshot.files);
21
+ return { config, snapshot, archive, summary: {
22
+ commit: snapshot.commit, revision: snapshot.revision, scope: snapshot.scope,
23
+ schemaVersion: config.schemaVersion, files: snapshot.files.map(file => file.path),
24
+ fileCount: snapshot.files.length, archiveBytes: archive.length,
25
+ dirty: inspectRepository(root)?.dirty ?? false,
26
+ note: 'Only the selected commit is transferred. Uncommitted changes are not included.',
27
+ } };
28
+ }
29
+ export function readSourceChange(directory) {
30
+ const envelope = JSON.parse(readFileSync(join(directory, 'change.json'), 'utf8'));
31
+ const change = sourceChange.parse(envelope.change);
32
+ if (sourceDigest(sourceChangeIdentity(change)) !== envelope.changeId)
33
+ throw Error('Source change identity mismatch.');
34
+ const load = (part) => {
35
+ const bytes = readFileSync(join(directory, `${part}.tar.gz`));
36
+ const files = validateWorkspaceArchive(bytes, change[part].archiveDigest);
37
+ if (sourceRevision(files) !== change[part].revision)
38
+ throw Error('Source snapshot identity mismatch.');
39
+ return files;
40
+ };
41
+ return { ...envelope, change, base: load('base'), result: load('result') };
42
+ }
43
+ export async function runSource(operation, workspaceId, options) {
44
+ configureOutput({ json: options.json === true });
45
+ const { signal, dispose } = withInterrupt();
46
+ try {
47
+ const root = resolve(options.root ?? '.');
48
+ if (process.env.STORYSTICK_RELEASE_SESSION)
49
+ throw Error('Source transfer requires a human CLI session outside the sandbox.');
50
+ let result;
51
+ if (options.dryRun && operation !== 'prepare')
52
+ throw Error('--dry-run is supported by source prepare only.');
53
+ if (operation === 'configure') {
54
+ if (Boolean(options.config) === Boolean(options.schema))
55
+ throw Error('Use --schema <compiled-schema.json> for a Next.js preset, or --config <reviewed-proposal.json>.');
56
+ result = options.config ? configureSource(root, options.config) : configureNextSource(root, options.schema);
57
+ }
58
+ else if (operation === 'prepare' && options.dryRun) {
59
+ result = { ...prepareSource(root, options.config, options.ref).summary, transferred: false };
60
+ }
61
+ else if (operation === 'apply') {
62
+ if (!options.change || !options.out)
63
+ throw Error('Specify --change <export-directory> and --out <new-checkout>.');
64
+ const artifact = readSourceChange(resolve(options.change));
65
+ result = integrateSource(root, options.out, artifact.change, artifact.base, artifact.result, options.ref);
66
+ if (result.status === 'conflicted')
67
+ process.exitCode = 1;
68
+ }
69
+ else {
70
+ const projectShortId = options.project ?? resolveProjectConnection(root)?.shortId;
71
+ if (!projectShortId)
72
+ throw Error('Specify --project <shortId> or connect this project.');
73
+ const runtime = resolveCliRuntimeConfig({ rootDir: root });
74
+ const transport = { rootDir: root, gatewayUrl: runtime.gatewayUrl, method: 'POST', signal, interactiveAuth: false, timeoutMs: 60_000 };
75
+ const request = (input, bytes = false) => callHubApi('/api/cli/project-workspace', {
76
+ ...transport, body: { projectShortId, request: input }, ...(bytes ? { responseType: 'bytes' } : {}),
77
+ });
78
+ const requestId = options.requestId ?? randomUUID();
79
+ const selection = () => { if (!workspaceId)
80
+ throw Error('Specify a workspace ID.'); return workspaceId; };
81
+ const upload = async (bytes) => {
82
+ const admission = await request({
83
+ operation: 'createUploadUrl', requestId: `source_${requestId}`, bytes: bytes.byteLength, sha256: sourceDigest(bytes),
84
+ });
85
+ if (admission.state === 'stored')
86
+ return admission.storageId;
87
+ const response = await fetchWithDeadline(admission.uploadUrl, { method: 'POST', headers: {
88
+ 'Content-Type': 'application/gzip', 'X-Storystick-Upload': admission.uploadToken,
89
+ }, body: new Uint8Array(bytes) }, { signal, timeoutMs: 60_000 });
90
+ if (!response.ok)
91
+ throw Error(`Source upload failed (${response.status}).`);
92
+ return (await response.json()).storageId;
93
+ };
94
+ if (operation === 'release')
95
+ result = await request({ operation: 'sourceRelease', releaseId: selection(), requestId });
96
+ else if (operation === 'stop')
97
+ result = await request({ operation: 'stop', workspaceId: selection(), requestId });
98
+ else if (operation === 'recover')
99
+ result = await request({ operation: 'recover', workspaceId: selection(), requestId,
100
+ ...(options.repairCommand === undefined ? {} : { command: options.repairCommand }),
101
+ ...(options.repairCwd === undefined ? {} : { cwd: options.repairCwd }) });
102
+ else if (operation === 'github-status')
103
+ result = await request({ operation: 'githubStatus' });
104
+ else if (operation === 'github-inspect')
105
+ result = await request({ operation: 'githubInspect', ...(options.appRoot ? { appRoot: options.appRoot } : {}), ...(options.ref ? { commit: options.ref } : {}), ...(options.config ? { configuration: sourceConfiguration.parse(JSON.parse(readFileSync(resolve(root, options.config), 'utf8'))) } : {}) });
106
+ else if (operation === 'propose') {
107
+ if (!options.change)
108
+ throw Error('Specify --change <export-directory>. This creates a GitHub branch and draft PR and may trigger configured CI.');
109
+ const exported = readSourceChange(resolve(options.change));
110
+ if (exported.change.projectShortId !== projectShortId)
111
+ throw Error('Source artifact belongs to another project.');
112
+ result = await request({ operation: 'githubPropose', workspaceId: exported.change.workspaceId, requestId: exported.requestId, changeId: exported.changeId });
113
+ }
114
+ else if (operation === 'github-import') {
115
+ if (!options.ref || !options.expectedRevision)
116
+ throw Error('Specify --ref <immutable-GitHub-commit> and --expected-revision <workspace-revision>.');
117
+ result = await request({ operation: 'githubImport', workspaceId: selection(), requestId, expectedRevision: options.expectedRevision, commit: options.ref });
118
+ }
119
+ else if (operation === 'github-prepare') {
120
+ if (!options.ref || !options.connectionRevision)
121
+ throw Error('Inspect the repository first, then pass --ref <immutable-GitHub-commit> and --connection-revision <inspected-connection-revision>.');
122
+ const config = sourceConfiguration.parse(JSON.parse(readFileSync(resolve(root, options.config ?? '.storystick/source.json'), 'utf8')));
123
+ result = await request({ operation: 'githubPrepare', connectionRevision: options.connectionRevision, commit: options.ref, scope: config.scope, schemaVersion: config.schemaVersion, recipe: config.recipe });
124
+ }
125
+ else if (operation === 'status')
126
+ result = await request({ operation: 'sourceStatus', workspaceId: selection() });
127
+ else if (operation === 'prepare') {
128
+ const { config, snapshot, archive } = prepareSource(root, options.config, options.ref);
129
+ const scope = config.scope;
130
+ const project = await callHubApi(`/api/cli/projects/for-user-by-shortId?shortId=${encodeURIComponent(projectShortId)}`, { ...transport, method: 'GET' });
131
+ result = await request({ operation: 'prepare', archiveId: await upload(archive), archiveDigest: sourceDigest(archive),
132
+ seed: { projectId: project.internalId, commit: snapshot.commit, schemaVersion: config.schemaVersion, scope }, recipe: config.recipe });
133
+ }
134
+ else if (operation === 'export') {
135
+ if (!options.out || !options.expectedRevision)
136
+ throw Error('Read source status, then specify --expected-revision <revision> and --out <new-directory>.');
137
+ const out = resolve(options.out);
138
+ if (existsSync(out))
139
+ throw Error('Export destination already exists; inspect it before retrying.');
140
+ mkdirSync(dirname(out), { recursive: true });
141
+ const exported = await request({ operation: 'exportSource', workspaceId: selection(), requestId, expectedRevision: options.expectedRevision });
142
+ const stage = mkdtempSync(join(dirname(out), '.storystick-export-'));
143
+ try {
144
+ writeFileSync(join(stage, 'change.json'), JSON.stringify(exported, null, 2) + '\n');
145
+ for (const part of ['base', 'result']) {
146
+ const chunks = [];
147
+ for (let offset = 0; offset <= MAX_ARCHIVE_BYTES; offset += 4 * 1024 * 1024) {
148
+ const chunk = Buffer.from(await request({ operation: 'downloadSource', workspaceId, requestId, part, offset }, true));
149
+ if (chunk.length > 4 * 1024 * 1024 || offset + chunk.length > MAX_ARCHIVE_BYTES)
150
+ throw Error('Source download exceeded its bound.');
151
+ chunks.push(chunk);
152
+ if (chunk.length < 4 * 1024 * 1024)
153
+ break;
154
+ }
155
+ writeFileSync(join(stage, `${part}.tar.gz`), Buffer.concat(chunks));
156
+ }
157
+ readSourceChange(stage);
158
+ renameSync(stage, out);
159
+ }
160
+ finally {
161
+ if (existsSync(stage))
162
+ rmSync(stage, { recursive: true, force: true });
163
+ }
164
+ result = { changeId: exported.changeId, requestId, directory: out, exported: true, applied: false };
165
+ }
166
+ else if (operation === 'import') {
167
+ if (!options.expectedRevision)
168
+ throw Error('Read source status, then pass --expected-revision <revision>. Stop the workspace before importing.');
169
+ const status = await request({ operation: 'sourceStatus', workspaceId: selection() });
170
+ const snapshot = captureCommit(root, options.ref ?? 'HEAD', sourceScope.parse(status.scope)), archive = encodeSourceArchive(snapshot.files);
171
+ result = await request({ operation: 'importSource', workspaceId, requestId, expectedRevision: options.expectedRevision,
172
+ archiveId: await upload(archive), archiveDigest: sourceDigest(archive), commit: snapshot.commit, scope: snapshot.scope });
173
+ }
174
+ else
175
+ throw Error('Use source configure, prepare, release, status, stop, recover, export, apply, import, github-status, github-inspect, github-prepare, github-import or propose.');
176
+ }
177
+ if ((operation === 'stop' || operation === 'recover') && result && typeof result === 'object' && 'error' in result)
178
+ throw Error(typeof result.error === 'string' ? result.error : 'Workspace operation failed.');
179
+ if (getOutputMode() === 'json')
180
+ emitJsonResult({ command: 'source', ok: true, data: result });
181
+ else
182
+ logMessage(JSON.stringify(result, null, 2));
183
+ }
184
+ catch (err) {
185
+ if (getOutputMode() === 'json')
186
+ exitJsonFailure({ command: 'source', err });
187
+ exitWithError(err);
188
+ }
189
+ finally {
190
+ dispose();
191
+ }
192
+ }
@@ -0,0 +1,78 @@
1
+ import { type ImportManifest, type ImportRecord, type ImportStatus } from '@storystick/core/content-import';
2
+ import { type HubApiOptions } from './hub-client.js';
3
+ import { type ImportAssetInput } from './asset-import.js';
4
+ export interface LocalImportAsset {
5
+ file: string;
6
+ input: ImportAssetInput;
7
+ }
8
+ export declare function readContentImport(file: string): Promise<{
9
+ manifest: ImportManifest;
10
+ batches: ImportRecord[][];
11
+ assets: LocalImportAsset[];
12
+ }>;
13
+ /** Reserve source identities without uploading. All local inputs have already been checked. */
14
+ export declare function prepareContentImportAssets(transport: ImportTransport, source: Awaited<ReturnType<typeof readContentImport>>): Promise<{
15
+ manifest: ImportManifest;
16
+ batches: ImportRecord[][];
17
+ assets: {
18
+ plan: import("./asset-import.js").ImportAssetReceipt;
19
+ file: string;
20
+ input: ImportAssetInput;
21
+ }[];
22
+ }>;
23
+ export type ImportTransport = <T>(request: Record<string, unknown>) => Promise<T>;
24
+ export declare function contentImportTransport(projectShortId: string, options: HubApiOptions): ImportTransport;
25
+ export declare function assetImportTransport(projectShortId: string, options: HubApiOptions): ImportTransport;
26
+ export declare function uploadBudgetTransport(projectShortId: string, options: HubApiOptions): ImportTransport;
27
+ /** Receipt identity is written before the first remote call. No content/secrets. */
28
+ export declare function importRunIdentity(input: {
29
+ root: string;
30
+ project: string;
31
+ gateway: string;
32
+ manifest: ImportManifest;
33
+ resume?: string;
34
+ newRun?: boolean;
35
+ }): Promise<string>;
36
+ export declare function prepareContentImport(transport: ImportTransport, input: {
37
+ runId: string;
38
+ manifest: ImportManifest;
39
+ batches: ImportRecord[][];
40
+ }): Promise<{
41
+ plans: unknown[];
42
+ runId: string;
43
+ state: "preparing" | "planned" | "applying" | "complete" | "cancelled" | "expired";
44
+ staged: number;
45
+ planned: number;
46
+ applied: number;
47
+ batches: number;
48
+ planDigest?: string;
49
+ assetCount: number;
50
+ assetsReady: number;
51
+ }>;
52
+ export declare function applyContentImport(transport: ImportTransport, runId: string, planDigest: string): Promise<ImportStatus>;
53
+ export declare function verifyContentImport(transport: ImportTransport, runId: string): Promise<{
54
+ verified: boolean;
55
+ projected: boolean;
56
+ verification: {
57
+ documents: Array<{
58
+ matches: boolean;
59
+ }>;
60
+ assets: Array<{
61
+ matches: boolean;
62
+ }>;
63
+ projection: string;
64
+ }[];
65
+ assetVerification: {
66
+ assetId: string;
67
+ matches: boolean;
68
+ }[];
69
+ runId: string;
70
+ state: "preparing" | "planned" | "applying" | "complete" | "cancelled" | "expired";
71
+ staged: number;
72
+ planned: number;
73
+ applied: number;
74
+ batches: number;
75
+ planDigest?: string;
76
+ assetCount: number;
77
+ assetsReady: number;
78
+ }>;