@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
package/dist/setup.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, realpathSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
3
|
+
import { sourceConfiguration, nextSourceConfiguration } from '@storystick/source/recipe';
|
|
4
|
+
import { resolveProjectConnection } from './project-connection.js';
|
|
5
|
+
import { inspectRepository } from './source-git.js';
|
|
6
|
+
/** Inspection is local, read-only and never invokes package scripts. Only names
|
|
7
|
+
* and identities escape: no environment values, remote credentials or scripts. */
|
|
8
|
+
export function inspectSetup(root) {
|
|
9
|
+
root = resolve(root);
|
|
10
|
+
const issues = [];
|
|
11
|
+
const readJson = (file) => {
|
|
12
|
+
if (!existsSync(file))
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
issues.push({ path: relative(root, file), message: 'Invalid JSON' });
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const pkg = readJson(resolve(root, 'package.json'));
|
|
23
|
+
const repository = inspectRepository(root);
|
|
24
|
+
const configPath = resolve(root, '.storystick/source.json');
|
|
25
|
+
const raw = readJson(configPath);
|
|
26
|
+
const config = raw === null ? null : sourceConfiguration.safeParse(raw);
|
|
27
|
+
if (config && !config.success)
|
|
28
|
+
issues.push(...config.error.issues.map((issue) => ({
|
|
29
|
+
path: `.storystick/source.json:${issue.path.join('.')}`,
|
|
30
|
+
message: issue.message,
|
|
31
|
+
})));
|
|
32
|
+
let connection = null;
|
|
33
|
+
try {
|
|
34
|
+
const value = resolveProjectConnection(root);
|
|
35
|
+
if (value)
|
|
36
|
+
connection = { projectShortId: value.shortId };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
issues.push({
|
|
40
|
+
path: '.env.local',
|
|
41
|
+
message: 'Incomplete or inconsistent project connection. Run storystick connect for this project.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const vercel = readJson(resolve(root, '.vercel/project.json'));
|
|
45
|
+
const next = [];
|
|
46
|
+
if (!connection)
|
|
47
|
+
next.push({
|
|
48
|
+
reason: 'Connect an existing project, or use init for a new one.',
|
|
49
|
+
command: 'storystick',
|
|
50
|
+
args: ['connect', '--help'],
|
|
51
|
+
});
|
|
52
|
+
if (!config?.success)
|
|
53
|
+
next.push({
|
|
54
|
+
reason: 'Configure the source scope and commands before enabling Studio code editing.',
|
|
55
|
+
command: 'storystick',
|
|
56
|
+
args: ['source', 'configure', '--help'],
|
|
57
|
+
});
|
|
58
|
+
else
|
|
59
|
+
next.push({
|
|
60
|
+
reason: 'Review the committed source before transferring it.',
|
|
61
|
+
command: 'storystick',
|
|
62
|
+
args: ['source', 'prepare', '--root', root, '--dry-run', '--json'],
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
root,
|
|
66
|
+
connection,
|
|
67
|
+
repository,
|
|
68
|
+
application: {
|
|
69
|
+
manifest: pkg !== null,
|
|
70
|
+
framework: pkg?.dependencies?.next || pkg?.devDependencies?.next ? 'nextjs' : null,
|
|
71
|
+
scripts: pkg?.scripts && typeof pkg.scripts === 'object' ? Object.keys(pkg.scripts).sort() : [],
|
|
72
|
+
packageManager: typeof pkg?.packageManager === 'string' &&
|
|
73
|
+
/^(pnpm|npm|yarn|bun)@[\w.+-]+$/.test(pkg.packageManager)
|
|
74
|
+
? pkg.packageManager
|
|
75
|
+
: null,
|
|
76
|
+
},
|
|
77
|
+
source: config?.success
|
|
78
|
+
? {
|
|
79
|
+
configured: true,
|
|
80
|
+
scope: config.data.scope,
|
|
81
|
+
schemaVersion: config.data.schemaVersion,
|
|
82
|
+
checks: config.data.recipe.checks.map((check) => check.name),
|
|
83
|
+
previewPort: config.data.recipe.preview.port,
|
|
84
|
+
}
|
|
85
|
+
: { configured: false },
|
|
86
|
+
hosting: vercel &&
|
|
87
|
+
/^prj_[a-zA-Z0-9]+$/.test(vercel.projectId) &&
|
|
88
|
+
/^team_[a-zA-Z0-9]+$/.test(vercel.orgId)
|
|
89
|
+
? {
|
|
90
|
+
provider: 'vercel',
|
|
91
|
+
projectId: vercel.projectId,
|
|
92
|
+
teamId: vercel.orgId,
|
|
93
|
+
status: 'local-link-only',
|
|
94
|
+
}
|
|
95
|
+
: null,
|
|
96
|
+
issues,
|
|
97
|
+
next,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** A reviewed proposal becomes the local configuration. Never overwrite another
|
|
101
|
+
* configuration on retry, including one belonging to an active release. */
|
|
102
|
+
export function configureSource(root, proposalPath) {
|
|
103
|
+
return saveSourceConfiguration(root, JSON.parse(readFileSync(resolve(root, proposalPath), 'utf8')));
|
|
104
|
+
}
|
|
105
|
+
function saveSourceConfiguration(root, proposal) {
|
|
106
|
+
const configuration = sourceConfiguration.parse(proposal);
|
|
107
|
+
const destination = resolve(root, '.storystick/source.json');
|
|
108
|
+
if (existsSync(destination)) {
|
|
109
|
+
const existing = sourceConfiguration.parse(JSON.parse(readFileSync(destination, 'utf8')));
|
|
110
|
+
if (JSON.stringify(existing) !== JSON.stringify(configuration))
|
|
111
|
+
throw Error('Source configuration already exists. Review and edit .storystick/source.json directly; it was not overwritten.');
|
|
112
|
+
return { path: destination, configured: true, changed: false };
|
|
113
|
+
}
|
|
114
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
115
|
+
writeFileSync(destination, JSON.stringify(configuration, null, 2) + '\n', {
|
|
116
|
+
flag: 'wx',
|
|
117
|
+
mode: 0o600,
|
|
118
|
+
});
|
|
119
|
+
return { path: destination, configured: true, changed: true };
|
|
120
|
+
}
|
|
121
|
+
/** A deliberately narrow preset. Monorepos keep their explicit dependency scope. */
|
|
122
|
+
export function configureNextSource(root, schemaFile) {
|
|
123
|
+
root = resolve(root);
|
|
124
|
+
const repo = inspectRepository(root);
|
|
125
|
+
if (!repo || realpathSync(repo.root) !== realpathSync(root))
|
|
126
|
+
throw Error('Run the Next.js preset at the Git root. For a monorepo, use --config with an explicit source scope.');
|
|
127
|
+
const compiled = JSON.parse(readFileSync(resolve(root, schemaFile), 'utf8'));
|
|
128
|
+
if (compiled.formatVersion !== 1 || typeof compiled.version !== 'string' || !compiled.stories)
|
|
129
|
+
throw Error('Use the output of storystick schema for --schema.');
|
|
130
|
+
return saveSourceConfiguration(root, nextSourceConfiguration({
|
|
131
|
+
manifest: JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
|
|
132
|
+
files: readdirSync(root),
|
|
133
|
+
schemaVersion: compiled.version,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type SourceEntry } from '@storystick/source/archive';
|
|
2
|
+
import { type SourceScope, type SourceChange } from '@storystick/source/manifest';
|
|
3
|
+
export declare const repositoryRoot: (root: string) => string;
|
|
4
|
+
export declare function inspectRepository(root: string): {
|
|
5
|
+
root: string;
|
|
6
|
+
commit: string | null;
|
|
7
|
+
dirty: boolean;
|
|
8
|
+
remotes: string[];
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function captureCommit(root: string, revision: string, rawScope: SourceScope): {
|
|
11
|
+
commit: string;
|
|
12
|
+
scope: {
|
|
13
|
+
appRoot: string;
|
|
14
|
+
include: string[];
|
|
15
|
+
exclude: string[];
|
|
16
|
+
};
|
|
17
|
+
files: {
|
|
18
|
+
path: string;
|
|
19
|
+
mode: number;
|
|
20
|
+
content: Buffer<ArrayBuffer>;
|
|
21
|
+
digest: string;
|
|
22
|
+
}[];
|
|
23
|
+
revision: string;
|
|
24
|
+
};
|
|
25
|
+
/** Schema provenance uses this same byte/mode identity. It is a working capture,
|
|
26
|
+
* not a claim that those bytes have been imported into a remote workspace. */
|
|
27
|
+
export declare function captureWorkingSource(root: string, scope: SourceScope): SourceEntry[];
|
|
28
|
+
/** Produces an ordinary independent Git checkout. The original index, refs,
|
|
29
|
+
* untracked files and dirty worktree are never modified, including on conflict. */
|
|
30
|
+
export declare function integrateSource(root: string, out: string, change: SourceChange, base: SourceEntry[], result: SourceEntry[], localRef?: string): {
|
|
31
|
+
status: string;
|
|
32
|
+
localCommit: string;
|
|
33
|
+
changeId: string;
|
|
34
|
+
conflicts: string[];
|
|
35
|
+
destination: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, renameSync, writeFileSync, readFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { sourceDigest, sourceRevision } from '@storystick/source/archive';
|
|
6
|
+
import { sourceChangeIdentity, inSourceScope, sourcePath, sourceScope, sourceManifest, MAX_SOURCE_BYTES, MAX_SOURCE_FILES } from '@storystick/source/manifest';
|
|
7
|
+
// No user-global hooks, filters, external diff drivers or inherited Git target.
|
|
8
|
+
const gitEnv = () => ({ ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_'))),
|
|
9
|
+
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null',
|
|
10
|
+
GIT_AUTHOR_NAME: 'Storystick', GIT_AUTHOR_EMAIL: 'source@storystick.invalid',
|
|
11
|
+
GIT_COMMITTER_NAME: 'Storystick', GIT_COMMITTER_EMAIL: 'source@storystick.invalid',
|
|
12
|
+
});
|
|
13
|
+
function git(root, args, input, env, allowFailure = false) {
|
|
14
|
+
const result = spawnSync('git', ['-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-c', 'core.autocrlf=false', '-c', 'core.attributesFile=/dev/null', ...args], {
|
|
15
|
+
cwd: root, env: { ...gitEnv(), ...env }, input, maxBuffer: MAX_SOURCE_BYTES + 16 * 1024 * 1024, timeout: 60_000,
|
|
16
|
+
});
|
|
17
|
+
if (result.error)
|
|
18
|
+
throw result.error;
|
|
19
|
+
if (result.status !== 0 && !allowFailure)
|
|
20
|
+
throw Error(`Git operation failed (${args[0]}): ${result.stderr.toString().slice(0, 2000)}`);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
const text = (root, args, input, env) => git(root, args, input, env).stdout.toString().trim();
|
|
24
|
+
export const repositoryRoot = (root) => text(root, ['rev-parse', '--show-toplevel']);
|
|
25
|
+
export function inspectRepository(root) {
|
|
26
|
+
const probe = git(root, ['rev-parse', '--show-toplevel'], undefined, undefined, true);
|
|
27
|
+
if (probe.status !== 0)
|
|
28
|
+
return null;
|
|
29
|
+
const directory = probe.stdout.toString().trim();
|
|
30
|
+
const head = git(directory, ['rev-parse', '--verify', 'HEAD'], undefined, undefined, true);
|
|
31
|
+
return {
|
|
32
|
+
root: directory,
|
|
33
|
+
commit: head.status === 0 ? head.stdout.toString().trim() : null,
|
|
34
|
+
dirty: git(directory, ['status', '--porcelain', '-z']).stdout.length > 0,
|
|
35
|
+
// Remote URLs may embed credentials. Their names are enough for discovery.
|
|
36
|
+
remotes: text(directory, ['remote']).split('\n').filter(Boolean),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function treeEntries(root, commit) {
|
|
40
|
+
const raw = git(root, ['ls-tree', '-rz', '--full-tree', commit]).stdout;
|
|
41
|
+
if (!Buffer.from(raw.toString()).equals(raw))
|
|
42
|
+
throw Error('Source paths must be UTF-8.');
|
|
43
|
+
return raw.toString().split('\0').filter(Boolean).map(line => {
|
|
44
|
+
const match = /^(\d{6}) (blob|commit) ([a-f0-9]+)\t(.+)$/s.exec(line);
|
|
45
|
+
if (!match)
|
|
46
|
+
throw Error('Unsupported Git tree entry.');
|
|
47
|
+
return { path: match[4], mode: parseInt(match[1], 8), oid: match[3] };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function checkedEntries(root, commit, scope) {
|
|
51
|
+
const entries = treeEntries(root, commit).filter(file => inSourceScope(file.path, scope));
|
|
52
|
+
if (entries.length > MAX_SOURCE_FILES)
|
|
53
|
+
throw Error('Source contains too many files.');
|
|
54
|
+
for (const file of entries) {
|
|
55
|
+
sourcePath(file.path);
|
|
56
|
+
if (![0o100644, 0o100755].includes(file.mode))
|
|
57
|
+
throw Error(`Unsupported link or submodule: ${file.path}`);
|
|
58
|
+
}
|
|
59
|
+
const scratch = mkdtempSync(join(tmpdir(), 'storystick-attributes-'));
|
|
60
|
+
try {
|
|
61
|
+
const env = { GIT_INDEX_FILE: join(scratch, 'index') };
|
|
62
|
+
git(root, ['read-tree', commit], undefined, env);
|
|
63
|
+
const attrs = git(root, ['check-attr', '--cached', '-z', '--stdin', 'filter', 'working-tree-encoding', 'eol', 'text'], entries.map(f => f.path + '\0').join(''), env).stdout.toString().split('\0');
|
|
64
|
+
for (let i = 0; i + 2 < attrs.length; i += 3) {
|
|
65
|
+
if (!['unspecified', 'unset'].includes(attrs[i + 2]))
|
|
66
|
+
throw Error(`Unsupported Git attribute ${attrs[i + 1]} on ${attrs[i]}.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
export function captureCommit(root, revision, rawScope) {
|
|
75
|
+
root = repositoryRoot(root);
|
|
76
|
+
const scope = sourceScope.parse(rawScope);
|
|
77
|
+
const commit = text(root, ['rev-parse', '--verify', '--end-of-options', `${revision}^{commit}`]);
|
|
78
|
+
const entries = checkedEntries(root, commit, scope);
|
|
79
|
+
const result = entries.length ? git(root, ['cat-file', '--batch'], entries.map(file => file.oid + '\n').join('')).stdout : Buffer.alloc(0);
|
|
80
|
+
let offset = 0, total = 0;
|
|
81
|
+
const files = entries.map(file => {
|
|
82
|
+
const newline = result.indexOf(10, offset);
|
|
83
|
+
const header = result.subarray(offset, newline).toString().split(' ');
|
|
84
|
+
const size = Number(header[2]);
|
|
85
|
+
total += size;
|
|
86
|
+
if (header[0] !== file.oid || header[1] !== 'blob' || !Number.isSafeInteger(size) || size < 0 || total > MAX_SOURCE_BYTES)
|
|
87
|
+
throw Error('Invalid or oversized Git source.');
|
|
88
|
+
const content = result.subarray(newline + 1, newline + 1 + size);
|
|
89
|
+
offset = newline + size + 2;
|
|
90
|
+
if (content.length !== size || result[offset - 1] !== 10)
|
|
91
|
+
throw Error('Truncated Git source.');
|
|
92
|
+
if (content.subarray(0, 100).toString().startsWith('version https://git-lfs.github.com/spec/'))
|
|
93
|
+
throw Error(`Git LFS is not supported: ${file.path}`);
|
|
94
|
+
return { path: file.path, mode: file.mode === 0o100755 ? 0o755 : 0o644, content, digest: sourceDigest(content) };
|
|
95
|
+
});
|
|
96
|
+
sourceManifest(files);
|
|
97
|
+
return { commit, scope, files, revision: sourceRevision(files) };
|
|
98
|
+
}
|
|
99
|
+
/** Schema provenance uses this same byte/mode identity. It is a working capture,
|
|
100
|
+
* not a claim that those bytes have been imported into a remote workspace. */
|
|
101
|
+
export function captureWorkingSource(root, scope) {
|
|
102
|
+
root = repositoryRoot(root);
|
|
103
|
+
const paths = [...new Set(git(root, ['ls-files', '-z', '--cached', '--others', '--exclude-standard']).stdout.toString().split('\0').filter(Boolean))];
|
|
104
|
+
let total = 0;
|
|
105
|
+
const files = paths.filter(path => inSourceScope(path, scope)).flatMap(path => {
|
|
106
|
+
// Connection/runtime files are never part of source provenance.
|
|
107
|
+
if (path.split('/').some(part => part === '.storystick' || part.startsWith('.env')))
|
|
108
|
+
return [];
|
|
109
|
+
sourcePath(path);
|
|
110
|
+
const absolute = resolve(root, path);
|
|
111
|
+
if (!existsSync(absolute))
|
|
112
|
+
return [];
|
|
113
|
+
for (let parent = absolute; parent !== root; parent = dirname(parent))
|
|
114
|
+
if (lstatSync(parent).isSymbolicLink())
|
|
115
|
+
throw Error(`Unsupported symlink: ${path}`);
|
|
116
|
+
const stat = lstatSync(absolute);
|
|
117
|
+
if (!stat.isFile() || stat.nlink !== 1)
|
|
118
|
+
throw Error(`Unsupported source file: ${path}`);
|
|
119
|
+
total += stat.size;
|
|
120
|
+
if (total > MAX_SOURCE_BYTES)
|
|
121
|
+
throw Error('Source is too large.');
|
|
122
|
+
const content = readFileSync(absolute);
|
|
123
|
+
return [{ path, content, mode: stat.mode & 0o111 ? 0o755 : 0o644, digest: sourceDigest(content) }];
|
|
124
|
+
});
|
|
125
|
+
sourceManifest(files);
|
|
126
|
+
return files;
|
|
127
|
+
}
|
|
128
|
+
function projectedTree(root, local, files, scope, index) {
|
|
129
|
+
const env = { GIT_INDEX_FILE: index };
|
|
130
|
+
git(root, ['read-tree', local], undefined, env);
|
|
131
|
+
const current = treeEntries(root, local).filter(f => inSourceScope(f.path, scope));
|
|
132
|
+
const zero = '0'.repeat(text(root, ['rev-parse', '--show-object-format']) === 'sha256' ? 64 : 40);
|
|
133
|
+
const updates = current.map(file => `0 ${zero}\t${file.path}\0`);
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
if (!inSourceScope(file.path, scope))
|
|
136
|
+
throw Error('Artifact contains a path outside its scope.');
|
|
137
|
+
const oid = text(root, ['hash-object', '-w', '--stdin'], file.content);
|
|
138
|
+
updates.push(`${file.mode === 0o755 ? '100755' : '100644'} ${oid}\t${file.path}\0`);
|
|
139
|
+
}
|
|
140
|
+
git(root, ['update-index', '-z', '--index-info'], updates.join(''), env);
|
|
141
|
+
return text(root, ['write-tree'], undefined, env);
|
|
142
|
+
}
|
|
143
|
+
/** Produces an ordinary independent Git checkout. The original index, refs,
|
|
144
|
+
* untracked files and dirty worktree are never modified, including on conflict. */
|
|
145
|
+
export function integrateSource(root, out, change, base, result, localRef = 'HEAD') {
|
|
146
|
+
root = realpathSync(repositoryRoot(root));
|
|
147
|
+
out = join(realpathSync(dirname(resolve(out))), basename(out));
|
|
148
|
+
if (existsSync(out))
|
|
149
|
+
throw Error(`Destination already exists; inspect it before retrying: ${out}`);
|
|
150
|
+
if (out === root || out.startsWith(root + '/'))
|
|
151
|
+
throw Error('Choose a destination outside the original checkout.');
|
|
152
|
+
const version = text(root, ['--version']).match(/(\d+)\.(\d+)/);
|
|
153
|
+
if (!version || Number(version[1]) < 2 || Number(version[1]) === 2 && Number(version[2]) < 41)
|
|
154
|
+
throw Error('Source integration requires Git 2.41 or later.');
|
|
155
|
+
if (sourceRevision(base) !== change.base.revision || sourceRevision(result) !== change.result.revision)
|
|
156
|
+
throw Error('Artifact source revision mismatch.');
|
|
157
|
+
const local = captureCommit(root, localRef, change.scope).commit;
|
|
158
|
+
const stage = mkdtempSync(join(dirname(out), '.storystick-integrate-'));
|
|
159
|
+
try {
|
|
160
|
+
git(root, ['-c', 'init.templateDir=', 'clone', '--local', '--no-hardlinks', '--no-checkout', '--', root, stage]);
|
|
161
|
+
const index = join(stage, '.git', 'source-index');
|
|
162
|
+
const baseTree = projectedTree(stage, local, base, change.scope, index);
|
|
163
|
+
const resultTree = projectedTree(stage, local, result, change.scope, index);
|
|
164
|
+
checkedEntries(stage, baseTree, change.scope);
|
|
165
|
+
checkedEntries(stage, resultTree, change.scope);
|
|
166
|
+
const b = text(stage, ['commit-tree', baseTree, '-m', 'Source import base']);
|
|
167
|
+
const r = text(stage, ['commit-tree', resultTree, '-p', b, '-m', 'Remote source result']);
|
|
168
|
+
const merge = git(stage, ['merge-tree', '--write-tree', '-z', '--no-messages', `--merge-base=${b}`, local, r], undefined, undefined, true);
|
|
169
|
+
if (merge.status !== 0 && merge.status !== 1)
|
|
170
|
+
throw Error('Git could not compute the source integration.');
|
|
171
|
+
const parts = merge.stdout.toString().split('\0'), tree = parts.shift();
|
|
172
|
+
if (!/^[a-f0-9]{40,64}$/.test(tree))
|
|
173
|
+
throw Error('Git returned an invalid merge tree.');
|
|
174
|
+
const stages = parts.filter(line => /^\d{6} [a-f0-9]+ [123]\t/.test(line));
|
|
175
|
+
const conflicts = [...new Set(stages.map(line => line.slice(line.indexOf('\t') + 1)))].sort();
|
|
176
|
+
if (merge.status === 1 && !conflicts.length)
|
|
177
|
+
throw Error('Git returned a conflict without file stages.');
|
|
178
|
+
git(stage, ['symbolic-ref', 'HEAD', 'refs/heads/storystick/source']);
|
|
179
|
+
git(stage, ['update-ref', 'HEAD', local]);
|
|
180
|
+
git(stage, ['read-tree', tree]);
|
|
181
|
+
git(stage, ['checkout-index', '-a']);
|
|
182
|
+
if (conflicts.length) {
|
|
183
|
+
const zero = '0'.repeat(local.length);
|
|
184
|
+
git(stage, ['update-index', '-z', '--index-info'], conflicts.map(path => `0 ${zero}\t${path}\0`).join('') + stages.map(line => line + '\0').join(''));
|
|
185
|
+
}
|
|
186
|
+
rmSync(index, { force: true });
|
|
187
|
+
const receipt = { status: conflicts.length ? 'conflicted' : 'ready', localCommit: local,
|
|
188
|
+
changeId: sourceDigest(sourceChangeIdentity(change)), conflicts, destination: out };
|
|
189
|
+
writeFileSync(join(stage, '.git', 'storystick-source.json'), JSON.stringify(receipt, null, 2) + '\n');
|
|
190
|
+
renameSync(stage, out);
|
|
191
|
+
return receipt;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
if (existsSync(stage))
|
|
195
|
+
rmSync(stage, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export declare const DEFAULT_STARTER_ID: "minimal-next";
|
|
2
|
+
export declare const DEFAULT_STARTER_CATALOG_URL = "https://starters.storystick.app/catalog.json";
|
|
3
|
+
export declare const STARTER_CATALOG_ENV = "STORYSTICK_STARTER_CATALOG_URL";
|
|
4
|
+
export declare const PREPARED_STARTER_DIRECTORY: string;
|
|
5
|
+
export declare const MATERIALIZED_STARTER_MARKER: string;
|
|
6
|
+
export interface StarterOverlayPath {
|
|
7
|
+
readonly from: string;
|
|
8
|
+
readonly to: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StarterManifest {
|
|
11
|
+
readonly version: 2;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly release: string;
|
|
14
|
+
readonly framework: 'next' | 'astro';
|
|
15
|
+
readonly integration: {
|
|
16
|
+
readonly contract: 'next-app-router' | 'astro-native';
|
|
17
|
+
readonly version: 1;
|
|
18
|
+
readonly routeStoryTypes: readonly string[];
|
|
19
|
+
readonly fields: {
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly description: string;
|
|
22
|
+
readonly slices: string;
|
|
23
|
+
readonly siteSettings: string;
|
|
24
|
+
readonly siteName: string;
|
|
25
|
+
readonly siteDescription: string;
|
|
26
|
+
};
|
|
27
|
+
readonly existingNext?: readonly StarterOverlayPath[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface StarterArtifactIdentity {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly release: string;
|
|
33
|
+
readonly framework: 'next' | 'astro';
|
|
34
|
+
readonly source: {
|
|
35
|
+
readonly provider: 'archive';
|
|
36
|
+
readonly url: string;
|
|
37
|
+
readonly integrity: string;
|
|
38
|
+
};
|
|
39
|
+
readonly integrity: string;
|
|
40
|
+
readonly localDirectory?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface StarterCatalogEntry extends StarterArtifactIdentity {
|
|
43
|
+
readonly label: string;
|
|
44
|
+
readonly description: string;
|
|
45
|
+
readonly capabilities: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
export interface StarterCatalog {
|
|
48
|
+
readonly version: 2;
|
|
49
|
+
readonly default: string;
|
|
50
|
+
readonly starters: readonly StarterCatalogEntry[];
|
|
51
|
+
}
|
|
52
|
+
export interface LoadStarterCatalogOptions {
|
|
53
|
+
readonly startDir?: string;
|
|
54
|
+
readonly catalogUrl?: string;
|
|
55
|
+
readonly fetchImpl?: typeof fetch;
|
|
56
|
+
readonly signal?: AbortSignal;
|
|
57
|
+
}
|
|
58
|
+
export interface PrepareStarterOptions {
|
|
59
|
+
readonly entry: StarterArtifactIdentity;
|
|
60
|
+
readonly targetDir: string;
|
|
61
|
+
readonly startDir?: string;
|
|
62
|
+
readonly fetchImpl?: typeof fetch;
|
|
63
|
+
readonly signal?: AbortSignal;
|
|
64
|
+
readonly onManifest?: (manifest: StarterManifest) => void;
|
|
65
|
+
}
|
|
66
|
+
export declare function loadStarterCatalog(options?: LoadStarterCatalogOptions): Promise<StarterCatalog>;
|
|
67
|
+
export declare function readStarterCatalog(path: string): StarterCatalog;
|
|
68
|
+
export declare function selectStarter(catalog: StarterCatalog, requestedId?: string): StarterCatalogEntry;
|
|
69
|
+
/** A required archive digest pins the bytes independently of the hosting URL. */
|
|
70
|
+
export declare function resolveStarterSource(entry: StarterArtifactIdentity): string;
|
|
71
|
+
export declare function findMonorepoRoot(startDir?: string): string | null;
|
|
72
|
+
export declare function findLocalStarterCatalog(startDir?: string): string | null;
|
|
73
|
+
export declare function findLocalStarter(entry: StarterArtifactIdentity, startDir?: string): string | null;
|
|
74
|
+
/** Fetch, validate, and hash a starter before publishing any of its bytes. */
|
|
75
|
+
export declare function prepareStarter(options: PrepareStarterOptions): Promise<StarterManifest>;
|
|
76
|
+
/** Publish only previously validated bytes into a blank application tree. */
|
|
77
|
+
export declare function ensurePreparedStarterMaterialized(targetDir: string, entry: StarterArtifactIdentity): StarterManifest;
|
|
78
|
+
export declare function readStarterManifest(starterRoot: string): StarterManifest;
|
|
79
|
+
export declare function computeStarterIntegrity(starterRoot: string): string;
|
|
80
|
+
export declare function preparedStarterRoot(targetDir: string): string;
|
|
81
|
+
export declare function listStarterFiles(root: string): string[];
|
|
82
|
+
export declare function copyDirRecursive(source: string, destination: string): void;
|