@sequenceholdings/studio-cli 0.1.21 → 0.1.24

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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Top-level `seq-studio deploy` — fan out from sequence.app.yml in deploy order.
3
+ *
4
+ * Child primitives keep their own deploy/apply verbs; this command only
5
+ * orchestrates them. Injectable runners keep unit tests offline.
6
+ */
7
+ import { type AppManifest, type PrimitiveEntry, type PrimitiveKind } from './manifest.js';
8
+ /**
9
+ * Resolve a manifest primitive path under the app root. Rejects absolute paths,
10
+ * lexical traversal, and symlink escapes (via realpath) before a deployer runs.
11
+ * Missing targets keep the lexical candidate after a lexical containment check.
12
+ */
13
+ export declare function resolveContainedPrimitivePath({ rootDir, entryPath, }: {
14
+ rootDir: string;
15
+ entryPath: string;
16
+ }): Promise<string>;
17
+ export type DeployPrimitiveArgs = {
18
+ entry: PrimitiveEntry;
19
+ /** Absolute path to the primitive directory. */
20
+ path: string;
21
+ env: string;
22
+ yes: boolean;
23
+ };
24
+ export type PrimitiveDeployers = {
25
+ [K in PrimitiveKind]: (args: DeployPrimitiveArgs) => Promise<number>;
26
+ };
27
+ export type DeployAppOptions = {
28
+ rootDir: string;
29
+ env: string;
30
+ /** Restrict to these primitive ids (manifest order still applies). */
31
+ only?: readonly string[];
32
+ yes?: boolean;
33
+ dryRun?: boolean;
34
+ deployers?: PrimitiveDeployers;
35
+ };
36
+ export type DeployAppResult = {
37
+ attempted: string[];
38
+ succeeded: string[];
39
+ failed: string[];
40
+ skipped: string[];
41
+ exitCode: number;
42
+ };
43
+ /** Resolve ordered primitive entries for deploy, validating order / --only ids. */
44
+ export declare function resolveDeployEntries({ manifest, only, }: {
45
+ manifest: AppManifest;
46
+ only?: readonly string[];
47
+ }): PrimitiveEntry[];
48
+ export declare function createDefaultDeployers(): Promise<PrimitiveDeployers>;
49
+ export declare function deployApp(options: DeployAppOptions): Promise<DeployAppResult>;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Top-level `seq-studio deploy` — fan out from sequence.app.yml in deploy order.
3
+ *
4
+ * Child primitives keep their own deploy/apply verbs; this command only
5
+ * orchestrates them. Injectable runners keep unit tests offline.
6
+ */
7
+ import { realpath } from 'node:fs/promises';
8
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
9
+ import { defaultDeployOrder, loadAppManifest, } from './manifest.js';
10
+ const LOG = '[seq-studio]';
11
+ const APP_MANIFEST_HINT = 'sequence.app.yml';
12
+ function assertRelativePrimitivePath(entryPath) {
13
+ const segments = entryPath.split('/');
14
+ if (entryPath.length === 0 ||
15
+ entryPath.trim() !== entryPath ||
16
+ isAbsolute(entryPath) ||
17
+ entryPath.includes('\\') ||
18
+ [...entryPath].some((character) => character.charCodeAt(0) < 0x20) ||
19
+ segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
20
+ throw new Error(`${APP_MANIFEST_HINT}: primitive path must be a relative directory within the app root (got ${JSON.stringify(entryPath)})`);
21
+ }
22
+ }
23
+ function assertPathInsideRoot({ rootDir, candidate, entryPath, }) {
24
+ const relativePath = relative(rootDir, candidate);
25
+ if (relativePath === '..' ||
26
+ relativePath.startsWith(`..${sep}`) ||
27
+ isAbsolute(relativePath)) {
28
+ throw new Error(`${APP_MANIFEST_HINT}: primitive path escapes the app root (got ${JSON.stringify(entryPath)})`);
29
+ }
30
+ }
31
+ /**
32
+ * Resolve a manifest primitive path under the app root. Rejects absolute paths,
33
+ * lexical traversal, and symlink escapes (via realpath) before a deployer runs.
34
+ * Missing targets keep the lexical candidate after a lexical containment check.
35
+ */
36
+ export async function resolveContainedPrimitivePath({ rootDir, entryPath, }) {
37
+ assertRelativePrimitivePath(entryPath);
38
+ const root = resolve(rootDir);
39
+ let canonicalRoot = root;
40
+ try {
41
+ canonicalRoot = await realpath(root);
42
+ }
43
+ catch (error) {
44
+ if (error.code !== 'ENOENT')
45
+ throw error;
46
+ }
47
+ // Resolve against the canonical root so macOS /var → /private/var does not
48
+ // look like an escape when comparing realpath(root) to a lexical candidate.
49
+ const candidate = resolve(canonicalRoot, entryPath);
50
+ assertPathInsideRoot({ rootDir: canonicalRoot, candidate, entryPath });
51
+ try {
52
+ const canonicalCandidate = await realpath(candidate);
53
+ assertPathInsideRoot({
54
+ rootDir: canonicalRoot,
55
+ candidate: canonicalCandidate,
56
+ entryPath,
57
+ });
58
+ return canonicalCandidate;
59
+ }
60
+ catch (error) {
61
+ if (error.code !== 'ENOENT')
62
+ throw error;
63
+ // Target does not exist yet (dry-run / pre-scaffold) — lexical check stands.
64
+ return candidate;
65
+ }
66
+ }
67
+ /** Resolve ordered primitive entries for deploy, validating order / --only ids. */
68
+ export function resolveDeployEntries({ manifest, only, }) {
69
+ const byId = new Map(manifest.primitives.map((entry) => [entry.id, entry]));
70
+ const order = manifest.deploy.order && manifest.deploy.order.length > 0
71
+ ? manifest.deploy.order
72
+ : defaultDeployOrder(manifest.primitives);
73
+ for (const id of order) {
74
+ if (!byId.has(id)) {
75
+ throw new Error(`${APP_MANIFEST_HINT}: deploy.order references unknown primitive id "${id}"`);
76
+ }
77
+ }
78
+ // Include primitives missing from an incomplete explicit order (append by default).
79
+ const orderedIds = [...order];
80
+ for (const entry of manifest.primitives) {
81
+ if (!orderedIds.includes(entry.id))
82
+ orderedIds.push(entry.id);
83
+ }
84
+ if (only && only.length > 0) {
85
+ const unknown = only.filter((id) => !byId.has(id));
86
+ if (unknown.length > 0) {
87
+ throw new Error(`Unknown primitive id(s) in --only: ${unknown.join(', ')}. Known: ${[...byId.keys()].join(', ')}`);
88
+ }
89
+ const onlySet = new Set(only);
90
+ return orderedIds
91
+ .filter((id) => onlySet.has(id))
92
+ .map((id) => byId.get(id));
93
+ }
94
+ return orderedIds.map((id) => byId.get(id));
95
+ }
96
+ export async function createDefaultDeployers() {
97
+ const { runOrmCommand } = await import('../orm/delegate.js');
98
+ const { functionsDeployCommand } = await import('../functions/commands.js');
99
+ const { runArtifactCommand } = await import('../artifact/delegate.js');
100
+ return {
101
+ async orm({ path, env }) {
102
+ return runOrmCommand('apply', [path, '--env', env]);
103
+ },
104
+ async function({ path, env, yes }) {
105
+ return functionsDeployCommand({
106
+ positional: [],
107
+ flags: {
108
+ dir: path,
109
+ env,
110
+ ...(yes ? { yes: true } : {}),
111
+ },
112
+ });
113
+ },
114
+ async artifact({ path, env }) {
115
+ return runArtifactCommand('deploy', [path, '--env', env]);
116
+ },
117
+ };
118
+ }
119
+ export async function deployApp(options) {
120
+ const rootDir = resolve(options.rootDir);
121
+ const manifest = await loadAppManifest(rootDir);
122
+ const entries = resolveDeployEntries({ manifest, only: options.only });
123
+ const onError = manifest.deploy.on_error;
124
+ const deployers = options.deployers ?? (await createDefaultDeployers());
125
+ // Validate every selected path before any deployer runs (including dry-run).
126
+ const resolvedPaths = new Map();
127
+ for (const entry of entries) {
128
+ resolvedPaths.set(entry.id, await resolveContainedPrimitivePath({ rootDir, entryPath: entry.path }));
129
+ }
130
+ console.log(`${LOG} deploying app "${manifest.app.id}" (${entries.length} primitive(s)) → ${options.env}`);
131
+ for (const entry of entries) {
132
+ console.log(`${LOG} - ${entry.kind.padEnd(9)} ${entry.id} (${entry.path})`);
133
+ }
134
+ if (options.dryRun) {
135
+ console.log(`${LOG} dry-run: no primitives deployed`);
136
+ return {
137
+ attempted: [],
138
+ succeeded: [],
139
+ failed: [],
140
+ skipped: entries.map((entry) => entry.id),
141
+ exitCode: 0,
142
+ };
143
+ }
144
+ const attempted = [];
145
+ const succeeded = [];
146
+ const failed = [];
147
+ const skipped = [];
148
+ /** Failed or dependency-skipped ids — blocks transitive `depends_on` edges. */
149
+ const unavailable = new Set();
150
+ let stop = false;
151
+ for (const entry of entries) {
152
+ if (stop) {
153
+ skipped.push(entry.id);
154
+ unavailable.add(entry.id);
155
+ console.log(`${LOG} skip ${entry.id} (stopped after earlier failure)`);
156
+ continue;
157
+ }
158
+ const blockedBy = entry.depends_on.filter((id) => unavailable.has(id));
159
+ if (blockedBy.length > 0) {
160
+ skipped.push(entry.id);
161
+ unavailable.add(entry.id);
162
+ console.log(`${LOG} skip ${entry.id} (depends on failed/skipped: ${blockedBy.join(', ')})`);
163
+ continue;
164
+ }
165
+ const path = resolvedPaths.get(entry.id);
166
+ attempted.push(entry.id);
167
+ console.log(`${LOG} → ${entry.kind} ${entry.id}`);
168
+ let code;
169
+ try {
170
+ code = await deployers[entry.kind]({
171
+ entry,
172
+ path,
173
+ env: options.env,
174
+ yes: options.yes === true,
175
+ });
176
+ }
177
+ catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ console.error(`${LOG} ${entry.id} threw: ${message}`);
180
+ code = 1;
181
+ }
182
+ if (code === 0) {
183
+ succeeded.push(entry.id);
184
+ console.log(`${LOG} ✓ ${entry.id}`);
185
+ }
186
+ else {
187
+ failed.push(entry.id);
188
+ unavailable.add(entry.id);
189
+ console.error(`${LOG} ✗ ${entry.id} exited ${code}`);
190
+ if (onError === 'stop')
191
+ stop = true;
192
+ }
193
+ }
194
+ const exitCode = failed.length > 0 ? 1 : 0;
195
+ console.log(`${LOG} deploy finished: ${succeeded.length} ok, ${failed.length} failed, ${skipped.length} skipped`);
196
+ return { attempted, succeeded, failed, skipped, exitCode };
197
+ }
@@ -0,0 +1,10 @@
1
+ import { type PrimitiveKind } from './manifest.js';
2
+ /**
3
+ * Resolve which primitive kinds to scaffold from CLI flags.
4
+ *
5
+ * Supports:
6
+ * --with orm,function,artifact
7
+ * --with=orm,function
8
+ * --orm --function --artifact (boolean aliases)
9
+ */
10
+ export declare function resolveRequestedKinds(flags: Record<string, string | true>): PrimitiveKind[];
@@ -0,0 +1,36 @@
1
+ import { isPrimitiveKind, PRIMITIVE_KINDS } from './manifest.js';
2
+ /**
3
+ * Resolve which primitive kinds to scaffold from CLI flags.
4
+ *
5
+ * Supports:
6
+ * --with orm,function,artifact
7
+ * --with=orm,function
8
+ * --orm --function --artifact (boolean aliases)
9
+ */
10
+ export function resolveRequestedKinds(flags) {
11
+ const kinds = new Set();
12
+ const withFlag = flags.with;
13
+ if (withFlag === true) {
14
+ throw new Error('--with requires a comma-separated list (e.g. --with orm,function,artifact)');
15
+ }
16
+ if (typeof withFlag === 'string') {
17
+ for (const part of withFlag.split(',')) {
18
+ const trimmed = part.trim().toLowerCase();
19
+ if (!trimmed)
20
+ continue;
21
+ if (!isPrimitiveKind(trimmed)) {
22
+ throw new Error(`unknown primitive kind '${trimmed}' in --with — expected one of: ${PRIMITIVE_KINDS.join(', ')}`);
23
+ }
24
+ kinds.add(trimmed);
25
+ }
26
+ }
27
+ for (const kind of PRIMITIVE_KINDS) {
28
+ if (flags[kind] === true)
29
+ kinds.add(kind);
30
+ else if (typeof flags[kind] === 'string') {
31
+ throw new Error(`--${kind} is a boolean flag (use --with ${kind} or --${kind} with no value)`);
32
+ }
33
+ }
34
+ // Stable order matching deploy defaults.
35
+ return PRIMITIVE_KINDS.filter((kind) => kinds.has(kind));
36
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * App monorepo root manifest (`sequence.app.yml`).
3
+ *
4
+ * Declares which platform primitives live in the repo and where. Required for
5
+ * scaffolding and for the eventual Applications UI. Crawl-only discovery is
6
+ * intentionally not supported — `seq-studio init` / `add` own this file.
7
+ *
8
+ * v0 kinds: orm | function | artifact. Process/agent can return later.
9
+ */
10
+ import { z } from 'zod';
11
+ export declare const APP_MANIFEST_FILENAME = "sequence.app.yml";
12
+ export declare const PRIMITIVE_KINDS: readonly ["orm", "function", "artifact"];
13
+ export type PrimitiveKind = (typeof PRIMITIVE_KINDS)[number];
14
+ export declare function isPrimitiveKind(value: string): value is PrimitiveKind;
15
+ declare const primitiveEntrySchema: z.ZodObject<{
16
+ id: z.ZodString;
17
+ kind: z.ZodEnum<{
18
+ function: "function";
19
+ orm: "orm";
20
+ artifact: "artifact";
21
+ }>;
22
+ path: z.ZodString;
23
+ depends_on: z.ZodDefault<z.ZodArray<z.ZodString>>;
24
+ namespace: z.ZodOptional<z.ZodString>;
25
+ slug: z.ZodOptional<z.ZodString>;
26
+ project_id: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>;
28
+ export declare const appManifestSchema: z.ZodObject<{
29
+ schema_version: z.ZodLiteral<1>;
30
+ app: z.ZodObject<{
31
+ id: z.ZodString;
32
+ title: z.ZodString;
33
+ description: z.ZodOptional<z.ZodString>;
34
+ }, z.core.$strip>;
35
+ studio: z.ZodObject<{
36
+ created_with: z.ZodString;
37
+ min_cli: z.ZodString;
38
+ }, z.core.$strip>;
39
+ primitives: z.ZodArray<z.ZodObject<{
40
+ id: z.ZodString;
41
+ kind: z.ZodEnum<{
42
+ function: "function";
43
+ orm: "orm";
44
+ artifact: "artifact";
45
+ }>;
46
+ path: z.ZodString;
47
+ depends_on: z.ZodDefault<z.ZodArray<z.ZodString>>;
48
+ namespace: z.ZodOptional<z.ZodString>;
49
+ slug: z.ZodOptional<z.ZodString>;
50
+ project_id: z.ZodOptional<z.ZodString>;
51
+ }, z.core.$strip>>;
52
+ deploy: z.ZodDefault<z.ZodObject<{
53
+ order: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ on_error: z.ZodDefault<z.ZodEnum<{
55
+ continue: "continue";
56
+ stop: "stop";
57
+ }>>;
58
+ }, z.core.$strip>>;
59
+ }, z.core.$strip>;
60
+ export type AppManifest = z.infer<typeof appManifestSchema>;
61
+ export type PrimitiveEntry = z.infer<typeof primitiveEntrySchema>;
62
+ /**
63
+ * Fail closed when the installed CLI is older than `studio.min_cli`, so an
64
+ * older binary cannot load / rewrite a manifest that requires newer semantics.
65
+ */
66
+ export declare function assertManifestCliVersion({ manifest, cliVersion, }: {
67
+ manifest: AppManifest;
68
+ cliVersion?: string;
69
+ }): void;
70
+ export declare function appManifestPath(rootDir: string): string;
71
+ export declare function parseAppManifest(raw: unknown): AppManifest;
72
+ export declare function loadAppManifest(rootDir: string): Promise<AppManifest>;
73
+ export declare function writeAppManifest({ rootDir, manifest, }: {
74
+ rootDir: string;
75
+ manifest: AppManifest;
76
+ }): Promise<string>;
77
+ /** Default deploy order: orm → functions → artifact. */
78
+ export declare function defaultDeployOrder(primitives: readonly PrimitiveEntry[]): string[];
79
+ /**
80
+ * Extend deploy.order when adding a primitive. Preserves any explicit custom
81
+ * order; only appends the new id (plus any pre-existing primitives that were
82
+ * missing from an incomplete order). Falls back to the default kind ranking
83
+ * when no order is set.
84
+ */
85
+ export declare function extendDeployOrder({ existingOrder, primitives, newId, }: {
86
+ existingOrder: readonly string[] | undefined;
87
+ primitives: readonly PrimitiveEntry[];
88
+ newId: string;
89
+ }): string[];
90
+ export declare function titleizeSlug(slug: string): string;
91
+ export declare function slugifyAppId(value: string): string;
92
+ /** ORM namespaces use snake_case; map kebab app ids. */
93
+ export declare function ormNamespaceFromAppId(appId: string): string;
94
+ export {};
@@ -0,0 +1,273 @@
1
+ /**
2
+ * App monorepo root manifest (`sequence.app.yml`).
3
+ *
4
+ * Declares which platform primitives live in the repo and where. Required for
5
+ * scaffolding and for the eventual Applications UI. Crawl-only discovery is
6
+ * intentionally not supported — `seq-studio init` / `add` own this file.
7
+ *
8
+ * v0 kinds: orm | function | artifact. Process/agent can return later.
9
+ */
10
+ import { readFile, writeFile } from 'node:fs/promises';
11
+ import { join, resolve } from 'node:path';
12
+ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
13
+ import { z } from 'zod';
14
+ import { currentVersion, isNewerVersion } from '../update-check.js';
15
+ export const APP_MANIFEST_FILENAME = 'sequence.app.yml';
16
+ export const PRIMITIVE_KINDS = ['orm', 'function', 'artifact'];
17
+ export function isPrimitiveKind(value) {
18
+ return PRIMITIVE_KINDS.includes(value);
19
+ }
20
+ const appIdSchema = z
21
+ .string()
22
+ .min(1)
23
+ .max(64)
24
+ .regex(/^[a-z][a-z0-9-]*$/, 'app id must be kebab-case (a-z, 0-9, -)');
25
+ const primitiveEntrySchema = z
26
+ .object({
27
+ id: z.string().min(1).max(128),
28
+ kind: z.enum(PRIMITIVE_KINDS),
29
+ path: z.string().min(1),
30
+ depends_on: z.array(z.string().min(1)).default([]),
31
+ namespace: z.string().min(1).optional(),
32
+ slug: z.string().min(1).optional(),
33
+ project_id: z.string().min(1).optional(),
34
+ })
35
+ .superRefine((entry, ctx) => {
36
+ if (entry.kind === 'orm' && entry.namespace === undefined) {
37
+ ctx.addIssue({ code: 'custom', message: `orm primitive "${entry.id}" requires namespace`, path: ['namespace'] });
38
+ }
39
+ if (entry.kind === 'function' && entry.slug === undefined) {
40
+ ctx.addIssue({ code: 'custom', message: `function primitive "${entry.id}" requires slug`, path: ['slug'] });
41
+ }
42
+ if (entry.kind === 'artifact' && entry.project_id === undefined) {
43
+ ctx.addIssue({
44
+ code: 'custom',
45
+ message: `artifact primitive "${entry.id}" requires project_id`,
46
+ path: ['project_id'],
47
+ });
48
+ }
49
+ });
50
+ export const appManifestSchema = z
51
+ .object({
52
+ schema_version: z.literal(1),
53
+ app: z.object({
54
+ id: appIdSchema,
55
+ title: z.string().min(1),
56
+ description: z.string().optional(),
57
+ }),
58
+ studio: z.object({
59
+ created_with: z.string().min(1),
60
+ min_cli: z.string().min(1),
61
+ }),
62
+ primitives: z.array(primitiveEntrySchema),
63
+ deploy: z
64
+ .object({
65
+ order: z.array(z.string().min(1)).optional(),
66
+ on_error: z.enum(['stop', 'continue']).default('stop'),
67
+ })
68
+ .default({ on_error: 'stop' }),
69
+ })
70
+ .superRefine((manifest, ctx) => {
71
+ const seenIds = new Set();
72
+ const seenPaths = new Set();
73
+ for (const [index, entry] of manifest.primitives.entries()) {
74
+ if (seenIds.has(entry.id)) {
75
+ ctx.addIssue({
76
+ code: 'custom',
77
+ message: `duplicate primitive id "${entry.id}"`,
78
+ path: ['primitives', index, 'id'],
79
+ });
80
+ }
81
+ seenIds.add(entry.id);
82
+ if (seenPaths.has(entry.path)) {
83
+ ctx.addIssue({
84
+ code: 'custom',
85
+ message: `duplicate primitive path "${entry.path}"`,
86
+ path: ['primitives', index, 'path'],
87
+ });
88
+ }
89
+ seenPaths.add(entry.path);
90
+ }
91
+ for (const [index, entry] of manifest.primitives.entries()) {
92
+ for (const [depIndex, dep] of entry.depends_on.entries()) {
93
+ if (!seenIds.has(dep)) {
94
+ ctx.addIssue({
95
+ code: 'custom',
96
+ message: `depends_on references unknown primitive id "${dep}"`,
97
+ path: ['primitives', index, 'depends_on', depIndex],
98
+ });
99
+ }
100
+ }
101
+ }
102
+ const order = manifest.deploy.order;
103
+ if (order !== undefined) {
104
+ const seenOrder = new Set();
105
+ for (const [index, id] of order.entries()) {
106
+ if (seenOrder.has(id)) {
107
+ ctx.addIssue({
108
+ code: 'custom',
109
+ message: `duplicate deploy.order entry "${id}"`,
110
+ path: ['deploy', 'order', index],
111
+ });
112
+ }
113
+ seenOrder.add(id);
114
+ }
115
+ }
116
+ // Effective deploy order (explicit + missing ids, or default kind ranking).
117
+ const effectiveOrder = order && order.length > 0
118
+ ? (() => {
119
+ const ordered = [...order];
120
+ for (const entry of manifest.primitives) {
121
+ if (!ordered.includes(entry.id))
122
+ ordered.push(entry.id);
123
+ }
124
+ return ordered;
125
+ })()
126
+ : defaultDeployOrderForValidation(manifest.primitives);
127
+ const position = new Map(effectiveOrder.map((id, index) => [id, index]));
128
+ for (const entry of manifest.primitives) {
129
+ const entryPos = position.get(entry.id);
130
+ if (entryPos === undefined)
131
+ continue;
132
+ for (const dep of entry.depends_on) {
133
+ const depPos = position.get(dep);
134
+ if (depPos === undefined)
135
+ continue;
136
+ if (depPos >= entryPos) {
137
+ ctx.addIssue({
138
+ code: 'custom',
139
+ message: `deploy order places "${entry.id}" before its dependency "${dep}"`,
140
+ path: ['deploy', 'order'],
141
+ });
142
+ }
143
+ }
144
+ }
145
+ });
146
+ /** Local helper so schema refinement does not depend on later exports. */
147
+ function defaultDeployOrderForValidation(primitives) {
148
+ const rank = (kind) => {
149
+ switch (kind) {
150
+ case 'orm':
151
+ return 0;
152
+ case 'function':
153
+ return 1;
154
+ case 'artifact':
155
+ return 2;
156
+ }
157
+ };
158
+ return [...primitives]
159
+ .sort((a, b) => rank(a.kind) - rank(b.kind) || a.id.localeCompare(b.id))
160
+ .map((entry) => entry.id);
161
+ }
162
+ /**
163
+ * Fail closed when the installed CLI is older than `studio.min_cli`, so an
164
+ * older binary cannot load / rewrite a manifest that requires newer semantics.
165
+ */
166
+ export function assertManifestCliVersion({ manifest, cliVersion = currentVersion(), }) {
167
+ const required = manifest.studio.min_cli;
168
+ if (isNewerVersion(required, cliVersion)) {
169
+ throw new Error(`This app requires seq-studio >= ${required} (sequence.app.yml studio.min_cli), ` +
170
+ `but this CLI is ${cliVersion}. Upgrade with: pnpm add -g @sequenceholdings/studio-cli@latest`);
171
+ }
172
+ }
173
+ export function appManifestPath(rootDir) {
174
+ return join(resolve(rootDir), APP_MANIFEST_FILENAME);
175
+ }
176
+ export function parseAppManifest(raw) {
177
+ return appManifestSchema.parse(raw);
178
+ }
179
+ export async function loadAppManifest(rootDir) {
180
+ const path = appManifestPath(rootDir);
181
+ let text;
182
+ try {
183
+ text = await readFile(path, 'utf8');
184
+ }
185
+ catch (error) {
186
+ if (error.code === 'ENOENT') {
187
+ throw new Error(`No ${APP_MANIFEST_FILENAME} in ${resolve(rootDir)}. Run \`seq-studio init\` to scaffold an app, or cd to the app root.`);
188
+ }
189
+ throw error;
190
+ }
191
+ let parsed;
192
+ try {
193
+ parsed = yamlLoad(text);
194
+ }
195
+ catch (error) {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ throw new Error(`Invalid ${APP_MANIFEST_FILENAME}: ${message}`);
198
+ }
199
+ try {
200
+ const manifest = parseAppManifest(parsed);
201
+ assertManifestCliVersion({ manifest });
202
+ return manifest;
203
+ }
204
+ catch (error) {
205
+ if (error instanceof z.ZodError) {
206
+ const detail = error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ');
207
+ throw new Error(`Invalid ${APP_MANIFEST_FILENAME}: ${detail}`);
208
+ }
209
+ throw error;
210
+ }
211
+ }
212
+ export async function writeAppManifest({ rootDir, manifest, }) {
213
+ const validated = parseAppManifest(manifest);
214
+ const path = appManifestPath(rootDir);
215
+ const body = yamlDump(validated, {
216
+ lineWidth: 100,
217
+ noRefs: true,
218
+ sortKeys: false,
219
+ });
220
+ await writeFile(path, `# App monorepo manifest — owned by seq-studio init / add. Do not hand-author without the CLI.\n${body}`, 'utf8');
221
+ return path;
222
+ }
223
+ /** Default deploy order: orm → functions → artifact. */
224
+ export function defaultDeployOrder(primitives) {
225
+ const rank = (kind) => {
226
+ switch (kind) {
227
+ case 'orm':
228
+ return 0;
229
+ case 'function':
230
+ return 1;
231
+ case 'artifact':
232
+ return 2;
233
+ }
234
+ };
235
+ return [...primitives]
236
+ .sort((a, b) => rank(a.kind) - rank(b.kind) || a.id.localeCompare(b.id))
237
+ .map((entry) => entry.id);
238
+ }
239
+ /**
240
+ * Extend deploy.order when adding a primitive. Preserves any explicit custom
241
+ * order; only appends the new id (plus any pre-existing primitives that were
242
+ * missing from an incomplete order). Falls back to the default kind ranking
243
+ * when no order is set.
244
+ */
245
+ export function extendDeployOrder({ existingOrder, primitives, newId, }) {
246
+ if (existingOrder === undefined || existingOrder.length === 0) {
247
+ return defaultDeployOrder(primitives);
248
+ }
249
+ const knownIds = new Set(primitives.map((entry) => entry.id));
250
+ const preserved = existingOrder.filter((id) => knownIds.has(id) && id !== newId);
251
+ const preservedSet = new Set(preserved);
252
+ const missingExisting = primitives
253
+ .map((entry) => entry.id)
254
+ .filter((id) => id !== newId && !preservedSet.has(id));
255
+ return [...preserved, ...missingExisting, newId];
256
+ }
257
+ export function titleizeSlug(slug) {
258
+ return slug
259
+ .split('-')
260
+ .filter(Boolean)
261
+ .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
262
+ .join(' ');
263
+ }
264
+ export function slugifyAppId(value) {
265
+ return value
266
+ .toLowerCase()
267
+ .replace(/[^a-z0-9]+/g, '-')
268
+ .replace(/^-+|-+$/g, '');
269
+ }
270
+ /** ORM namespaces use snake_case; map kebab app ids. */
271
+ export function ormNamespaceFromAppId(appId) {
272
+ return appId.replace(/-/g, '_');
273
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Scaffold ORM / function / artifact into an app monorepo and keep
3
+ * sequence.app.yml in sync. Delegates to each primitive's real `init`.
4
+ */
5
+ import { type AppManifest, type PrimitiveEntry, type PrimitiveKind } from './manifest.js';
6
+ export interface ScaffoldNames {
7
+ functionName: string;
8
+ }
9
+ export declare function defaultScaffoldNames(_appId: string): ScaffoldNames;
10
+ export declare function initAppMonorepo({ rootDir, appId, kinds, names, description, }: {
11
+ rootDir: string;
12
+ appId: string;
13
+ kinds: readonly PrimitiveKind[];
14
+ names?: ScaffoldNames;
15
+ description?: string;
16
+ }): Promise<AppManifest>;
17
+ export declare function addPrimitiveToApp({ rootDir, kind, name, }: {
18
+ rootDir: string;
19
+ kind: PrimitiveKind;
20
+ name: string;
21
+ }): Promise<{
22
+ manifest: AppManifest;
23
+ entry: PrimitiveEntry;
24
+ }>;
25
+ export declare function printInitNextSteps({ appRoot, manifest, }: {
26
+ appRoot: string;
27
+ manifest: AppManifest;
28
+ }): void;