@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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern D schema push — plain `fetch()` client used by the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Post ADR-058 + audit FIX-1 (Pattern D bridge): `schemas.push` no longer
|
|
5
|
+
* exists at Central's top-level Convex API. It lives inside the backend
|
|
6
|
+
* Component and is only reachable via the Pattern D mutation endpoint
|
|
7
|
+
* `POST <gatewayUrl>/api/m/schemas/push`. This module hides the
|
|
8
|
+
* endpoint shape behind a single function used by both `storystick push`
|
|
9
|
+
* / `sync` (cli.ts) and `storystick init` (init.ts) — one wire format,
|
|
10
|
+
* one place to maintain it.
|
|
11
|
+
*
|
|
12
|
+
* We avoid `ConvexHttpClient` on purpose: the Pattern D contract is plain
|
|
13
|
+
* HTTP so the edge Worker (follow-up PR) can cache+rewrite without
|
|
14
|
+
* understanding Convex's protocol, and so the SDK has zero runtime
|
|
15
|
+
* Convex dependency.
|
|
16
|
+
*
|
|
17
|
+
* ADR-072 §D3: failures throw `StorystickError` with `code` + `status` so
|
|
18
|
+
* the CLI's exit-code router (Batch 7) can route HTTP 5xx and network
|
|
19
|
+
* errors uniformly. Network errors preserve their original cause via
|
|
20
|
+
* `cause` (lock L4) so `TypeError(cause: ECONNREFUSED)` survives wrapping.
|
|
21
|
+
*/
|
|
22
|
+
import { StorystickError, storystickErrorFromResponse } from './cli-error.js';
|
|
23
|
+
import { contentShapeCapabilityHeaders, } from '@storystick/types';
|
|
24
|
+
import { abortableSleep, fetchWithDeadline } from './transport.js';
|
|
25
|
+
/**
|
|
26
|
+
* Convert the core serializer output into the exact Pattern D push body.
|
|
27
|
+
*
|
|
28
|
+
* Keep this as the only schema → wire-shape mapper used by push/sync/init/link
|
|
29
|
+
* so new schema sidecars cannot be dropped by one call site.
|
|
30
|
+
*/
|
|
31
|
+
export function schemaToPushBody(schema) {
|
|
32
|
+
return {
|
|
33
|
+
formatVersion: schema.formatVersion,
|
|
34
|
+
version: schema.version,
|
|
35
|
+
stories: schema.stories,
|
|
36
|
+
storyCount: schema.storyCount,
|
|
37
|
+
zoneMappings: schema.zoneMappings,
|
|
38
|
+
fieldBindings: schema.fieldBindings,
|
|
39
|
+
diagnostics: schema.diagnostics,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ─────────────────────────────────────────────
|
|
43
|
+
// Request
|
|
44
|
+
// ─────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* POST `schemas.push` via Pattern D. Throws with a readable message on
|
|
47
|
+
* failure — caller decides whether to exit / warn.
|
|
48
|
+
*
|
|
49
|
+
* @param siteUrl Central's `.convex.site` base URL (no trailing slash).
|
|
50
|
+
* @param token Short-lived Storystick JWT with `scope: 'write' | 'full'`.
|
|
51
|
+
* @param body Resolved schema payload.
|
|
52
|
+
*/
|
|
53
|
+
export async function pushSchema(siteUrl, token, body, fence, options = {}) {
|
|
54
|
+
const endpoint = `${siteUrl.replace(/\/$/, '')}/api/m/schemas/push`;
|
|
55
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
56
|
+
const res = await fetchWithDeadline(endpoint, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'X-Storystick-Client': 'cli',
|
|
62
|
+
'X-Storystick-Idempotency-Key': body.version,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ ...body, ...fence }),
|
|
65
|
+
}, options);
|
|
66
|
+
if (res.ok)
|
|
67
|
+
return (await res.json());
|
|
68
|
+
const error = actionablePushError(await storystickErrorFromResponse(res));
|
|
69
|
+
if (error.retryable !== true || attempt === 3)
|
|
70
|
+
throw error;
|
|
71
|
+
await abortableSleep(Math.min(250 * 2 ** (attempt - 1), 1_000), options.signal);
|
|
72
|
+
}
|
|
73
|
+
throw new StorystickError('Schema push exhausted its retry budget.');
|
|
74
|
+
}
|
|
75
|
+
export async function fetchSchemaChangePlan(siteUrl, token, body, options = {}) {
|
|
76
|
+
const response = await fetchWithDeadline(`${siteUrl.replace(/\/$/, '')}/api/m/schemas/plan`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'X-Storystick-Client': 'cli',
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
}, options);
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw actionablePushError(await storystickErrorFromResponse(response));
|
|
87
|
+
}
|
|
88
|
+
return (await response.json());
|
|
89
|
+
}
|
|
90
|
+
export function schemaPlanFence(plan) {
|
|
91
|
+
return {
|
|
92
|
+
expectedBaseVersion: plan.baseVersion,
|
|
93
|
+
planDigest: plan.planDigest,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function assertSchemaPlanAllowed(plan) {
|
|
97
|
+
if (plan.classification !== 'breaking' && plan.classification !== 'unknown') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
throw new StorystickError(`Schema change is ${plan.classification}; no schema or generated pointer was published.`, {
|
|
101
|
+
code: 'SCHEMA_CHANGE_BLOCKED',
|
|
102
|
+
category: 'validation',
|
|
103
|
+
recoverable: true,
|
|
104
|
+
retryable: false,
|
|
105
|
+
payload: { plan },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function actionablePushError(error) {
|
|
109
|
+
const payload = error.payload &&
|
|
110
|
+
typeof error.payload === 'object' &&
|
|
111
|
+
!Array.isArray(error.payload)
|
|
112
|
+
? error.payload
|
|
113
|
+
: undefined;
|
|
114
|
+
const docShortId = typeof payload?.docShortId === 'string' ? payload.docShortId : undefined;
|
|
115
|
+
const context = [error.code, error.message].filter(Boolean).join(': ');
|
|
116
|
+
const message = docShortId
|
|
117
|
+
? `${context} (docShortId: ${docShortId})`
|
|
118
|
+
: context;
|
|
119
|
+
return new StorystickError(message, {
|
|
120
|
+
code: error.code,
|
|
121
|
+
status: error.status,
|
|
122
|
+
payload: error.payload,
|
|
123
|
+
category: error.category,
|
|
124
|
+
recoverable: error.recoverable,
|
|
125
|
+
retryable: error.retryable,
|
|
126
|
+
messageKey: error.messageKey,
|
|
127
|
+
params: error.params,
|
|
128
|
+
ref: error.ref,
|
|
129
|
+
docUrl: error.docUrl,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// ─────────────────────────────────────────────
|
|
133
|
+
// Token exchange
|
|
134
|
+
// ─────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* Exchange a long-lived API key for a short-lived Storystick JWT. The CLI
|
|
137
|
+
* is a server-to-server consumer: `X-Storystick-Client: sdk` signals
|
|
138
|
+
* that path to Central's CSRF check (see `apps/studio/convex/_csrf.ts`).
|
|
139
|
+
*
|
|
140
|
+
* @param siteUrl Central's `.convex.site` base URL (no trailing slash).
|
|
141
|
+
* @param apiKey Secret (`ss_sk_*`) API key.
|
|
142
|
+
*/
|
|
143
|
+
export async function fetchSchemaPushToken(siteUrl, apiKey, options = {}) {
|
|
144
|
+
const res = await fetchWithDeadline(`${siteUrl.replace(/\/$/, '')}/api/tokens/issue`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {
|
|
147
|
+
Authorization: `Bearer ${apiKey}`,
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
'X-Storystick-Client': 'cli',
|
|
150
|
+
...contentShapeCapabilityHeaders('cli'),
|
|
151
|
+
},
|
|
152
|
+
}, options);
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
throw await storystickErrorFromResponse(res, `Gateway rejected token issuance (HTTP ${res.status}).`);
|
|
155
|
+
}
|
|
156
|
+
const body = (await res.json());
|
|
157
|
+
if (!body.token)
|
|
158
|
+
throw new StorystickError('Gateway returned no token.');
|
|
159
|
+
return body.token;
|
|
160
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const contextSchema: z.ZodObject<{
|
|
3
|
+
projectShortId: z.ZodString;
|
|
4
|
+
releaseId: z.ZodString;
|
|
5
|
+
gatewayUrl: z.ZodString;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
export type ReleaseContext = z.infer<typeof contextSchema>;
|
|
8
|
+
export declare function readReleaseContext(rootDir: string): ReleaseContext | undefined;
|
|
9
|
+
export declare function writeReleaseContext(rootDir: string, context: ReleaseContext | undefined): void;
|
|
10
|
+
/** A sandbox's destination is supplied by its server-held credential. Neither a
|
|
11
|
+
* local context file nor --release can retarget that credential to Main. */
|
|
12
|
+
export declare function resolveReleaseContext(rootDir: string, gatewayUrl: string, explicit?: string, env?: NodeJS.ProcessEnv): ReleaseContext | undefined;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { readCliEnvValue } from './runtime-config.js';
|
|
6
|
+
const contextSchema = z.object({ projectShortId: z.string().regex(/^[a-z0-9]{8}$/), releaseId: z.string().min(1).max(120), gatewayUrl: z.string().url() }).strict();
|
|
7
|
+
const file = (rootDir) => join(rootDir, '.storystick', 'release-context.json');
|
|
8
|
+
export function readReleaseContext(rootDir) {
|
|
9
|
+
return existsSync(file(rootDir)) ? contextSchema.parse(JSON.parse(readFileSync(file(rootDir), 'utf8'))) : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function writeReleaseContext(rootDir, context) {
|
|
12
|
+
if (!context) {
|
|
13
|
+
rmSync(file(rootDir), { force: true });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const path = file(rootDir), temporary = `${path}.${randomUUID()}.tmp`;
|
|
17
|
+
mkdirSync(join(rootDir, '.storystick'), { recursive: true });
|
|
18
|
+
writeFileSync(temporary, JSON.stringify(contextSchema.parse(context)) + '\n', { mode: 0o600 });
|
|
19
|
+
renameSync(temporary, path);
|
|
20
|
+
}
|
|
21
|
+
/** A sandbox's destination is supplied by its server-held credential. Neither a
|
|
22
|
+
* local context file nor --release can retarget that credential to Main. */
|
|
23
|
+
export function resolveReleaseContext(rootDir, gatewayUrl, explicit, env = process.env) {
|
|
24
|
+
if (env.STORYSTICK_RELEASE_SESSION) {
|
|
25
|
+
const scoped = contextSchema.parse({ projectShortId: env.STORYSTICK_PROJECT_ID, releaseId: env.STORYSTICK_RELEASE_ID, gatewayUrl: env.STORYSTICK_RELEASE_GATEWAY });
|
|
26
|
+
if (explicit && explicit !== scoped.releaseId)
|
|
27
|
+
throw Error('This workspace can only write its own release.');
|
|
28
|
+
return scoped;
|
|
29
|
+
}
|
|
30
|
+
const stored = readReleaseContext(rootDir);
|
|
31
|
+
if (stored && new URL(stored.gatewayUrl).origin !== new URL(gatewayUrl).origin)
|
|
32
|
+
throw Error('Release context belongs to a different gateway. Select the release again.');
|
|
33
|
+
if (!explicit)
|
|
34
|
+
return stored;
|
|
35
|
+
const projectShortId = stored?.projectShortId ?? readCliEnvValue(rootDir, 'STORYSTICK_PROJECT_ID', env);
|
|
36
|
+
if (!projectShortId)
|
|
37
|
+
throw Error('Select a project with storystick release use <id> --project <id> first.');
|
|
38
|
+
return contextSchema.parse({ projectShortId, releaseId: explicit, gatewayUrl });
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SerializedSchema } from '@storystick/types';
|
|
2
|
+
import type { ReleaseContext } from './release-context.js';
|
|
3
|
+
export declare function releaseSchemaRequest<T>(context: ReleaseContext, request: Record<string, unknown>, rootDir: string, signal?: AbortSignal): Promise<T>;
|
|
4
|
+
export declare function localSourceRevision(rootDir: string): string;
|
|
5
|
+
export declare function pushReleaseSchema(args: {
|
|
6
|
+
context: ReleaseContext;
|
|
7
|
+
schema: SerializedSchema;
|
|
8
|
+
rootDir: string;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
status: "pushed" | "unchanged" | "planned";
|
|
13
|
+
version: string;
|
|
14
|
+
releaseId: string;
|
|
15
|
+
published: false;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { callHubApi } from './hub-client.js';
|
|
4
|
+
import { fetchWithDeadline } from './transport.js';
|
|
5
|
+
import { storystickErrorFromResponse } from './cli-error.js';
|
|
6
|
+
import { captureWorkingSource } from './source-git.js';
|
|
7
|
+
import { sourceRevision } from '@storystick/source/archive';
|
|
8
|
+
import { sourceScope, defaultSourceScope } from '@storystick/source/manifest';
|
|
9
|
+
export async function releaseSchemaRequest(context, request, rootDir, signal) {
|
|
10
|
+
const path = '/api/cli/release-schema';
|
|
11
|
+
const body = { projectShortId: context.projectShortId, releaseId: context.releaseId, ...request };
|
|
12
|
+
const token = process.env.STORYSTICK_RELEASE_SESSION;
|
|
13
|
+
if (!token)
|
|
14
|
+
return callHubApi(path, { rootDir, gatewayUrl: context.gatewayUrl, method: 'POST', body, signal, interactiveAuth: false });
|
|
15
|
+
// Restricted workspace credential, never a user login or project API key.
|
|
16
|
+
const response = await fetchWithDeadline(`${context.gatewayUrl.replace(/\/$/, '')}${path}`, {
|
|
17
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Storystick-Workspace-Session': token }, body: JSON.stringify(body), signal,
|
|
18
|
+
}, { timeoutMs: 60_000, signal });
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
throw await storystickErrorFromResponse(response);
|
|
21
|
+
return response.json();
|
|
22
|
+
}
|
|
23
|
+
export function localSourceRevision(rootDir) {
|
|
24
|
+
const config = resolve(rootDir, '.storystick/source.json');
|
|
25
|
+
const scope = existsSync(config) ? sourceScope.parse(JSON.parse(readFileSync(config, 'utf8')).scope) : defaultSourceScope;
|
|
26
|
+
return sourceRevision(captureWorkingSource(rootDir, scope));
|
|
27
|
+
}
|
|
28
|
+
export async function pushReleaseSchema(args) {
|
|
29
|
+
const current = await releaseSchemaRequest(args.context, { operation: 'read' }, args.rootDir, args.signal);
|
|
30
|
+
return releaseSchemaRequest(args.context, {
|
|
31
|
+
operation: 'push', schema: args.schema, sourceRevision: localSourceRevision(args.rootDir), expectedVersion: current.schemaVersion, dryRun: args.dryRun === true,
|
|
32
|
+
}, args.rootDir, args.signal);
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { GeneratedFile } from '@storystick/core';
|
|
2
|
+
import type { SerializedSchema } from '@storystick/types';
|
|
3
|
+
import type { ReleaseContext } from './release-context.js';
|
|
4
|
+
export declare function syncRelease(args: {
|
|
5
|
+
context: ReleaseContext;
|
|
6
|
+
rootDir: string;
|
|
7
|
+
outputDirectory: string;
|
|
8
|
+
prepare: () => Promise<{
|
|
9
|
+
schema: SerializedSchema;
|
|
10
|
+
generated: readonly GeneratedFile[];
|
|
11
|
+
storyCount: number;
|
|
12
|
+
}>;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
status: "pushed" | "unchanged" | "planned";
|
|
17
|
+
version: string;
|
|
18
|
+
releaseId: string;
|
|
19
|
+
published: false;
|
|
20
|
+
} | {
|
|
21
|
+
releaseId: string;
|
|
22
|
+
published: boolean;
|
|
23
|
+
generatedFiles: readonly string[];
|
|
24
|
+
resumed: boolean;
|
|
25
|
+
status?: "pushed" | "unchanged" | undefined;
|
|
26
|
+
version?: string | undefined;
|
|
27
|
+
newBindingNotes?: import("./push-client.js").NewBindingNote[] | undefined;
|
|
28
|
+
recommendBackfill?: import("./push-client.js").BackfillRecommendation;
|
|
29
|
+
projectionSeq?: number;
|
|
30
|
+
} | {
|
|
31
|
+
generatedFiles: readonly string[];
|
|
32
|
+
status: "pushed" | "unchanged" | "planned";
|
|
33
|
+
version: string;
|
|
34
|
+
releaseId: string;
|
|
35
|
+
published: false;
|
|
36
|
+
}>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { commitPreparedGeneration, prepareGeneratedFiles } from './generated-transaction.js';
|
|
2
|
+
import { pushReleaseSchema } from './release-schema-client.js';
|
|
3
|
+
import { clearSyncOperation, markSyncOperationPushed, readSyncOperation, writePreparedSyncOperation } from './sync-transaction.js';
|
|
4
|
+
export async function syncRelease(args) {
|
|
5
|
+
const target = `${args.context.gatewayUrl}|${args.context.projectShortId}|${args.context.releaseId}`;
|
|
6
|
+
const pending = readSyncOperation(args.rootDir);
|
|
7
|
+
if (pending && (pending.target !== target || pending.outputDirectory !== args.outputDirectory))
|
|
8
|
+
throw Error('A sync is pending for another destination. Resume it before changing release.');
|
|
9
|
+
if (pending?.state === 'pushed') {
|
|
10
|
+
if (args.dryRun)
|
|
11
|
+
throw Error('Resume the pending sync without --dry-run.');
|
|
12
|
+
const committed = await commitPreparedGeneration(pending.outputDirectory, pending.generationId);
|
|
13
|
+
clearSyncOperation(args.rootDir);
|
|
14
|
+
return { ...pending.pushResult, releaseId: args.context.releaseId, published: false, generatedFiles: committed.files, resumed: true };
|
|
15
|
+
}
|
|
16
|
+
const prepared = await args.prepare();
|
|
17
|
+
const resolved = { ...args, ...prepared };
|
|
18
|
+
// Plan validates the exact same admission without changing the remote scope.
|
|
19
|
+
const plan = await pushReleaseSchema({ ...resolved, dryRun: true });
|
|
20
|
+
if (args.dryRun)
|
|
21
|
+
return plan;
|
|
22
|
+
const staged = await prepareGeneratedFiles(args.outputDirectory, prepared.generated);
|
|
23
|
+
const receipt = writePreparedSyncOperation({ ...resolved, schemaVersion: prepared.schema.version, generationId: staged.generationId, target });
|
|
24
|
+
const result = await pushReleaseSchema(resolved);
|
|
25
|
+
if (result.status === 'planned')
|
|
26
|
+
throw Error('Release sync did not apply its schema.');
|
|
27
|
+
markSyncOperationPushed(receipt, { status: result.status, version: result.version, newBindingNotes: [] });
|
|
28
|
+
const committed = await commitPreparedGeneration(args.outputDirectory, staged.generationId);
|
|
29
|
+
clearSyncOperation(args.rootDir);
|
|
30
|
+
return { ...result, generatedFiles: committed.files };
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Diagnostic, ResolvedStory } from '@storystick/core';
|
|
2
|
+
export interface ResolveResult {
|
|
3
|
+
stories: Map<string, ResolvedStory>;
|
|
4
|
+
diagnostics: Diagnostic[];
|
|
5
|
+
success: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse and resolve all .story files. No side effects (no file writes).
|
|
9
|
+
* Shared by generate, push, sync, and deterministic build tooling.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runResolve(options: {
|
|
12
|
+
rootDir: string;
|
|
13
|
+
storyDirs: string[];
|
|
14
|
+
corePath: string;
|
|
15
|
+
}): Promise<ResolveResult>;
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { access, readFile, readdir } from 'fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { convertToParsedStory, createStoryRegistry, readStoryFile, resolveStories, } from '@storystick/core';
|
|
4
|
+
/**
|
|
5
|
+
* Parse and resolve all .story files. No side effects (no file writes).
|
|
6
|
+
* Shared by generate, push, sync, and deterministic build tooling.
|
|
7
|
+
*/
|
|
8
|
+
export async function runResolve(options) {
|
|
9
|
+
const { rootDir, storyDirs, corePath } = options;
|
|
10
|
+
const allDiagnostics = [];
|
|
11
|
+
const nodeFs = {
|
|
12
|
+
async exists(path) {
|
|
13
|
+
try {
|
|
14
|
+
await access(path);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
readdir: (path, opts) => readdir(path, opts),
|
|
22
|
+
resolve,
|
|
23
|
+
join,
|
|
24
|
+
dirname,
|
|
25
|
+
};
|
|
26
|
+
const registry = await createStoryRegistry({ rootDir, storyDirs, corePath }, nodeFs);
|
|
27
|
+
const parsed = new Map();
|
|
28
|
+
for (const id of registry.allIds()) {
|
|
29
|
+
const filePath = registry.resolve(id);
|
|
30
|
+
if (!filePath)
|
|
31
|
+
continue;
|
|
32
|
+
const content = await readFile(filePath, 'utf-8');
|
|
33
|
+
const readResult = readStoryFile(content, filePath);
|
|
34
|
+
allDiagnostics.push(...readResult.diagnostics);
|
|
35
|
+
if (!readResult.documents)
|
|
36
|
+
continue;
|
|
37
|
+
const convertResult = convertToParsedStory(readResult.documents);
|
|
38
|
+
allDiagnostics.push(...convertResult.diagnostics);
|
|
39
|
+
if (convertResult.story) {
|
|
40
|
+
const expectedId = registry.idFromPath(filePath);
|
|
41
|
+
if (expectedId !== convertResult.story.id) {
|
|
42
|
+
allDiagnostics.push({
|
|
43
|
+
source: 'storystick',
|
|
44
|
+
severity: 'error',
|
|
45
|
+
code: 'STORY_ID_FILENAME_MISMATCH',
|
|
46
|
+
message: `Story identifier '${convertResult.story.id}' does not match filename identity ` +
|
|
47
|
+
`'${expectedId ?? '<unknown>'}'. Rename the file or update 'story:' so they match.`,
|
|
48
|
+
file: filePath,
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const existing = parsed.get(convertResult.story.id);
|
|
53
|
+
if (existing) {
|
|
54
|
+
allDiagnostics.push({
|
|
55
|
+
source: 'storystick',
|
|
56
|
+
severity: 'error',
|
|
57
|
+
code: 'DUPLICATE_STORY_ID',
|
|
58
|
+
message: `Story identifier '${convertResult.story.id}' is declared more than once.`,
|
|
59
|
+
file: filePath,
|
|
60
|
+
related: [
|
|
61
|
+
{
|
|
62
|
+
file: existing.filePath,
|
|
63
|
+
message: 'First declaration is here.',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
parsed.set(convertResult.story.id, convertResult.story);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const resolveResult = resolveStories(parsed, registry);
|
|
73
|
+
allDiagnostics.push(...resolveResult.diagnostics);
|
|
74
|
+
const success = !allDiagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
75
|
+
return {
|
|
76
|
+
stories: resolveResult.stories,
|
|
77
|
+
diagnostics: allDiagnostics,
|
|
78
|
+
success,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const DEFAULT_HUB_URL = "https://storystick.app";
|
|
2
|
+
export declare const DEFAULT_GATEWAY_URL = "https://api.storystick.app";
|
|
3
|
+
export interface CliRuntimeConfig {
|
|
4
|
+
hubUrl: string;
|
|
5
|
+
gatewayUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CliRuntimeConfigOptions {
|
|
8
|
+
rootDir?: string;
|
|
9
|
+
hubUrl?: string;
|
|
10
|
+
gatewayUrl?: string;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveCliRuntimeConfig(options?: CliRuntimeConfigOptions): CliRuntimeConfig;
|
|
14
|
+
export declare function readCliEnvValue(rootDir: string, name: string, env?: NodeJS.ProcessEnv): string | null;
|
|
15
|
+
/** Credential issuers are origins, never project-controlled URL prefixes. */
|
|
16
|
+
export declare function normalizeCliIssuer(raw: string, name?: string): string;
|
|
17
|
+
/** Check the effective request URL before attaching a human credential. */
|
|
18
|
+
export declare function cliIssuerRequestUrl(issuer: string, requestPath: string): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { STORYSTICK_GATEWAY_ORIGIN, STORYSTICK_STUDIO_ORIGIN } from '@storystick/types/public-endpoints';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { StorystickError } from './cli-error.js';
|
|
5
|
+
export const DEFAULT_HUB_URL = STORYSTICK_STUDIO_ORIGIN;
|
|
6
|
+
export const DEFAULT_GATEWAY_URL = STORYSTICK_GATEWAY_ORIGIN;
|
|
7
|
+
export function resolveCliRuntimeConfig(options = {}) {
|
|
8
|
+
const rootDir = resolve(options.rootDir ?? '.');
|
|
9
|
+
const env = options.env ?? process.env;
|
|
10
|
+
return {
|
|
11
|
+
hubUrl: normalizeCliIssuer(options.hubUrl ??
|
|
12
|
+
env.STORYSTICK_HUB_URL ??
|
|
13
|
+
readCliEnvValue(rootDir, 'STORYSTICK_HUB_URL', env) ??
|
|
14
|
+
DEFAULT_HUB_URL, 'STORYSTICK_HUB_URL'),
|
|
15
|
+
gatewayUrl: normalizeCliIssuer(options.gatewayUrl ??
|
|
16
|
+
env.STORYSTICK_GATEWAY_URL ??
|
|
17
|
+
readCliEnvValue(rootDir, 'STORYSTICK_GATEWAY_URL', env) ??
|
|
18
|
+
DEFAULT_GATEWAY_URL, 'STORYSTICK_GATEWAY_URL'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function readCliEnvValue(rootDir, name, env = process.env) {
|
|
22
|
+
if (Object.prototype.hasOwnProperty.call(env, name))
|
|
23
|
+
return env[name] ?? '';
|
|
24
|
+
for (const filename of ['.env.local', '.env']) {
|
|
25
|
+
const path = join(rootDir, filename);
|
|
26
|
+
if (!existsSync(path))
|
|
27
|
+
continue;
|
|
28
|
+
const content = readFileSync(path, 'utf8');
|
|
29
|
+
for (const line of content.split(/\r?\n/)) {
|
|
30
|
+
const match = line.match(new RegExp(`^${escapeRegExp(name)}\\s*=\\s*(.*)$`));
|
|
31
|
+
if (!match)
|
|
32
|
+
continue;
|
|
33
|
+
return unquote(match[1].trim());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** Credential issuers are origins, never project-controlled URL prefixes. */
|
|
39
|
+
export function normalizeCliIssuer(raw, name = 'STORYSTICK_GATEWAY_URL') {
|
|
40
|
+
const value = raw.trim();
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = new URL(value);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new StorystickError(`${name} must be a valid HTTP(S) URL.`, {
|
|
47
|
+
code: 'INVALID_RUNTIME_URL',
|
|
48
|
+
category: 'validation',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const localHttp = parsed.protocol === 'http:' &&
|
|
52
|
+
['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname);
|
|
53
|
+
if ((parsed.protocol !== 'https:' && !localHttp) ||
|
|
54
|
+
parsed.username || parsed.password || parsed.search || parsed.hash ||
|
|
55
|
+
parsed.pathname !== '/') {
|
|
56
|
+
throw new StorystickError(`${name} must be an HTTPS origin (HTTP loopback is allowed for local development).`, {
|
|
57
|
+
code: 'INVALID_RUNTIME_URL',
|
|
58
|
+
category: 'validation',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return parsed.origin;
|
|
62
|
+
}
|
|
63
|
+
/** Check the effective request URL before attaching a human credential. */
|
|
64
|
+
export function cliIssuerRequestUrl(issuer, requestPath) {
|
|
65
|
+
const origin = normalizeCliIssuer(issuer);
|
|
66
|
+
const target = new URL(requestPath, `${origin}/`);
|
|
67
|
+
if (target.origin !== origin || target.username || target.password || target.hash) {
|
|
68
|
+
throw new StorystickError('The CLI request must stay on its credential issuer.', {
|
|
69
|
+
code: 'AUTH_ISSUER_MISMATCH', category: 'auth', retryable: false,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return target.href;
|
|
73
|
+
}
|
|
74
|
+
function unquote(value) {
|
|
75
|
+
if (value.length >= 2) {
|
|
76
|
+
const first = value[0];
|
|
77
|
+
const last = value[value.length - 1];
|
|
78
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
79
|
+
return value.slice(1, -1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function escapeRegExp(value) {
|
|
85
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, writeFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { sourcePath } from '@storystick/source/manifest';
|
|
5
|
+
import { serializeSchema } from '@storystick/core/serializer';
|
|
6
|
+
import { runResolve } from './resolve.js';
|
|
7
|
+
export async function exportSchema(options) {
|
|
8
|
+
sourcePath(options.out);
|
|
9
|
+
const resolved = await runResolve(options);
|
|
10
|
+
if (!resolved.success)
|
|
11
|
+
throw Error('Schema validation failed: ' +
|
|
12
|
+
resolved.diagnostics
|
|
13
|
+
.filter((d) => d.severity === 'error')
|
|
14
|
+
.map((d) => d.message)
|
|
15
|
+
.join('\n'));
|
|
16
|
+
const schema = serializeSchema(resolved.stories, resolved.diagnostics);
|
|
17
|
+
const destination = resolve(options.rootDir, options.out);
|
|
18
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
19
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
20
|
+
try {
|
|
21
|
+
await writeFile(temporary, JSON.stringify(schema, null, 2) + '\n', { flag: 'wx' });
|
|
22
|
+
await rename(temporary, destination);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await rm(temporary, { force: true });
|
|
26
|
+
}
|
|
27
|
+
return { path: destination, schemaVersion: schema.version, storyCount: schema.storyCount };
|
|
28
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Inspection is local, read-only and never invokes package scripts. Only names
|
|
2
|
+
* and identities escape: no environment values, remote credentials or scripts. */
|
|
3
|
+
export declare function inspectSetup(root: string): {
|
|
4
|
+
root: string;
|
|
5
|
+
connection: {
|
|
6
|
+
projectShortId: string;
|
|
7
|
+
} | null;
|
|
8
|
+
repository: {
|
|
9
|
+
root: string;
|
|
10
|
+
commit: string | null;
|
|
11
|
+
dirty: boolean;
|
|
12
|
+
remotes: string[];
|
|
13
|
+
} | null;
|
|
14
|
+
application: {
|
|
15
|
+
manifest: boolean;
|
|
16
|
+
framework: string | null;
|
|
17
|
+
scripts: string[];
|
|
18
|
+
packageManager: any;
|
|
19
|
+
};
|
|
20
|
+
source: {
|
|
21
|
+
configured: boolean;
|
|
22
|
+
scope: {
|
|
23
|
+
appRoot: string;
|
|
24
|
+
include: string[];
|
|
25
|
+
exclude: string[];
|
|
26
|
+
};
|
|
27
|
+
schemaVersion: string;
|
|
28
|
+
checks: string[];
|
|
29
|
+
previewPort: number;
|
|
30
|
+
} | {
|
|
31
|
+
configured: boolean;
|
|
32
|
+
scope?: undefined;
|
|
33
|
+
schemaVersion?: undefined;
|
|
34
|
+
checks?: undefined;
|
|
35
|
+
previewPort?: undefined;
|
|
36
|
+
};
|
|
37
|
+
hosting: {
|
|
38
|
+
provider: string;
|
|
39
|
+
projectId: any;
|
|
40
|
+
teamId: any;
|
|
41
|
+
status: string;
|
|
42
|
+
} | null;
|
|
43
|
+
issues: {
|
|
44
|
+
path: string;
|
|
45
|
+
message: string;
|
|
46
|
+
}[];
|
|
47
|
+
next: {
|
|
48
|
+
reason: string;
|
|
49
|
+
command: string;
|
|
50
|
+
args: string[];
|
|
51
|
+
}[];
|
|
52
|
+
};
|
|
53
|
+
/** A reviewed proposal becomes the local configuration. Never overwrite another
|
|
54
|
+
* configuration on retry, including one belonging to an active release. */
|
|
55
|
+
export declare function configureSource(root: string, proposalPath: string): {
|
|
56
|
+
path: string;
|
|
57
|
+
configured: boolean;
|
|
58
|
+
changed: boolean;
|
|
59
|
+
};
|
|
60
|
+
/** A deliberately narrow preset. Monorepos keep their explicit dependency scope. */
|
|
61
|
+
export declare function configureNextSource(root: string, schemaFile: string): {
|
|
62
|
+
path: string;
|
|
63
|
+
configured: boolean;
|
|
64
|
+
changed: boolean;
|
|
65
|
+
};
|