@sequenceholdings/studio-cli 0.1.22 → 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.
- package/README.md +115 -12
- package/dist/app/commands.d.ts +16 -0
- package/dist/app/commands.js +227 -0
- package/dist/app/deploy.d.ts +49 -0
- package/dist/app/deploy.js +197 -0
- package/dist/app/kinds.d.ts +10 -0
- package/dist/app/kinds.js +36 -0
- package/dist/app/manifest.d.ts +94 -0
- package/dist/app/manifest.js +273 -0
- package/dist/app/scaffold.d.ts +28 -0
- package/dist/app/scaffold.js +263 -0
- package/dist/functions/manifest.d.ts +22 -0
- package/dist/functions/manifest.js +45 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +21 -0
- package/dist/pipeline/codegen.d.ts +2 -0
- package/dist/pipeline/codegen.js +118 -0
- package/dist/pipeline/commands.d.ts +7 -0
- package/dist/pipeline/commands.js +86 -12
- package/dist/pipeline/lifecycle.d.ts +1 -12
- package/dist/pipeline/lifecycle.js +140 -35
- package/dist/pipeline/templates.js +3 -1
- package/dist/secrets/commands.d.ts +3 -1
- package/dist/secrets/commands.js +87 -26
- package/package.json +8 -8
- package/dist/pipeline/pinning.d.ts +0 -5
- package/dist/pipeline/pinning.js +0 -9
|
@@ -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;
|
|
@@ -0,0 +1,263 @@
|
|
|
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 { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
8
|
+
import { functionsInitCommand } from '../functions/commands.js';
|
|
9
|
+
import { parseArgs } from '../process/commands.js';
|
|
10
|
+
import { currentVersion } from '../update-check.js';
|
|
11
|
+
import { defaultDeployOrder, extendDeployOrder, loadAppManifest, ormNamespaceFromAppId, titleizeSlug, writeAppManifest, } from './manifest.js';
|
|
12
|
+
const LOG = '[seq-studio]';
|
|
13
|
+
const APP_MANIFEST_LABEL = 'sequence.app.yml';
|
|
14
|
+
export function defaultScaffoldNames(_appId) {
|
|
15
|
+
return { functionName: 'hello' };
|
|
16
|
+
}
|
|
17
|
+
function primitiveIdFor({ kind, name }) {
|
|
18
|
+
switch (kind) {
|
|
19
|
+
case 'orm':
|
|
20
|
+
return `${name}-orm`;
|
|
21
|
+
case 'artifact':
|
|
22
|
+
return `${name}-ui`;
|
|
23
|
+
case 'function':
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function relativePathFor({ kind, appId, name, }) {
|
|
28
|
+
switch (kind) {
|
|
29
|
+
case 'orm':
|
|
30
|
+
return join('orm', ormNamespaceFromAppId(appId));
|
|
31
|
+
case 'function':
|
|
32
|
+
return join('functions', name);
|
|
33
|
+
case 'artifact':
|
|
34
|
+
return 'artifact';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildPrimitiveEntry({ kind, appId, name, dependsOn, }) {
|
|
38
|
+
const path = relativePathFor({ kind, appId, name }).replace(/\\/g, '/');
|
|
39
|
+
const id = primitiveIdFor({ kind, name: kind === 'orm' || kind === 'artifact' ? appId : name });
|
|
40
|
+
const base = { id, kind, path, depends_on: dependsOn };
|
|
41
|
+
switch (kind) {
|
|
42
|
+
case 'orm':
|
|
43
|
+
return { ...base, namespace: ormNamespaceFromAppId(appId) };
|
|
44
|
+
case 'function':
|
|
45
|
+
return { ...base, slug: name };
|
|
46
|
+
case 'artifact':
|
|
47
|
+
return { ...base, project_id: appId };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function rewriteArtifactIdentity({ artifactDir, appId, }) {
|
|
51
|
+
const title = titleizeSlug(appId);
|
|
52
|
+
const manifestPath = join(artifactDir, 'artifact.bundle.yml');
|
|
53
|
+
if (existsSync(manifestPath)) {
|
|
54
|
+
const body = await readFile(manifestPath, 'utf8');
|
|
55
|
+
const next = body
|
|
56
|
+
.replace(/(project_id:\s*)\S+/g, `$1${appId}`)
|
|
57
|
+
.replace(/^(\s*)title:\s*.*$/m, `$1title: "${title}"`);
|
|
58
|
+
if (next !== body)
|
|
59
|
+
await writeFile(manifestPath, next, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
const pkgPath = join(artifactDir, 'package.json');
|
|
62
|
+
if (existsSync(pkgPath)) {
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
65
|
+
pkg.name = appId;
|
|
66
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// leave non-JSON package.json alone
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function scaffoldOrm({ absPath }) {
|
|
74
|
+
const { runOrmCommand } = await import('../orm/delegate.js');
|
|
75
|
+
const code = await runOrmCommand('init', [absPath]);
|
|
76
|
+
if (code !== 0) {
|
|
77
|
+
throw new Error(`orm init failed for ${absPath} (exit ${code}). ` +
|
|
78
|
+
'`seq-studio init --with orm` requires `@sequenceholdings/orm` ' +
|
|
79
|
+
'(workspace / internal installs today; not yet available via public npm).');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function scaffoldArtifact({ absPath, appId, }) {
|
|
83
|
+
const { runCli } = await import('@sequenceholdings/artifact-studio/cli');
|
|
84
|
+
const code = await runCli(['init', absPath]);
|
|
85
|
+
if (code !== 0) {
|
|
86
|
+
throw new Error(`artifact init failed for ${absPath} (exit ${code})`);
|
|
87
|
+
}
|
|
88
|
+
await rewriteArtifactIdentity({ artifactDir: absPath, appId });
|
|
89
|
+
}
|
|
90
|
+
async function scaffoldFunction({ absPath }) {
|
|
91
|
+
const code = await functionsInitCommand(parseArgs([absPath]));
|
|
92
|
+
if (code !== 0) {
|
|
93
|
+
throw new Error(`functions init failed for ${absPath} (exit ${code})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function scaffoldKind({ kind, appRoot, appId, name, }) {
|
|
97
|
+
const relPath = relativePathFor({ kind, appId, name }).replace(/\\/g, '/');
|
|
98
|
+
const absPath = join(appRoot, relPath);
|
|
99
|
+
const entry = buildPrimitiveEntry({ kind, appId, name, dependsOn: [] });
|
|
100
|
+
switch (kind) {
|
|
101
|
+
case 'orm':
|
|
102
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
103
|
+
await scaffoldOrm({ absPath });
|
|
104
|
+
break;
|
|
105
|
+
case 'function':
|
|
106
|
+
await scaffoldFunction({ absPath });
|
|
107
|
+
break;
|
|
108
|
+
case 'artifact':
|
|
109
|
+
await mkdir(absPath, { recursive: true });
|
|
110
|
+
await scaffoldArtifact({ absPath, appId });
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
function applyDefaultDependsOn(primitives) {
|
|
116
|
+
const orm = primitives.find((entry) => entry.kind === 'orm');
|
|
117
|
+
if (!orm)
|
|
118
|
+
return primitives;
|
|
119
|
+
return primitives.map((entry) => {
|
|
120
|
+
if (entry.kind === 'orm' || entry.depends_on.length > 0)
|
|
121
|
+
return entry;
|
|
122
|
+
return { ...entry, depends_on: [orm.id] };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export async function initAppMonorepo({ rootDir, appId, kinds, names = defaultScaffoldNames(appId), description, }) {
|
|
126
|
+
if (kinds.length === 0) {
|
|
127
|
+
throw new Error('select at least one primitive via --with orm,function,artifact (or --orm --function …)');
|
|
128
|
+
}
|
|
129
|
+
const appRoot = resolve(rootDir);
|
|
130
|
+
if (existsSync(join(appRoot, 'sequence.app.yml'))) {
|
|
131
|
+
throw new Error(`${join(appRoot, 'sequence.app.yml')} already exists — refusing to re-init`);
|
|
132
|
+
}
|
|
133
|
+
if (existsSync(appRoot)) {
|
|
134
|
+
const { readdir } = await import('node:fs/promises');
|
|
135
|
+
const entries = await readdir(appRoot).catch((error) => {
|
|
136
|
+
if (error.code === 'ENOENT')
|
|
137
|
+
return [];
|
|
138
|
+
throw error;
|
|
139
|
+
});
|
|
140
|
+
if (entries.length > 0) {
|
|
141
|
+
throw new Error(`${appRoot} is not empty — init into a new directory, or remove existing files first`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await mkdir(appRoot, { recursive: true });
|
|
145
|
+
try {
|
|
146
|
+
const version = currentVersion();
|
|
147
|
+
const primitives = [];
|
|
148
|
+
for (const kind of kinds) {
|
|
149
|
+
const name = kind === 'function' ? names.functionName : appId;
|
|
150
|
+
primitives.push(await scaffoldKind({ kind, appRoot, appId, name }));
|
|
151
|
+
}
|
|
152
|
+
const withDeps = applyDefaultDependsOn(primitives);
|
|
153
|
+
const manifest = {
|
|
154
|
+
schema_version: 1,
|
|
155
|
+
app: {
|
|
156
|
+
id: appId,
|
|
157
|
+
title: titleizeSlug(appId),
|
|
158
|
+
...(description !== undefined ? { description } : {}),
|
|
159
|
+
},
|
|
160
|
+
studio: {
|
|
161
|
+
created_with: version,
|
|
162
|
+
min_cli: version,
|
|
163
|
+
},
|
|
164
|
+
primitives: withDeps,
|
|
165
|
+
deploy: {
|
|
166
|
+
order: defaultDeployOrder(withDeps),
|
|
167
|
+
on_error: 'stop',
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
await writeAppManifest({ rootDir: appRoot, manifest });
|
|
171
|
+
await writeFile(join(appRoot, '.gitignore'), ['node_modules/', 'dist/', '.env', '.env.*', '!.env.example', '.DS_Store', '*.log'].join('\n') + '\n', 'utf8');
|
|
172
|
+
return manifest;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// Directory was empty (or newly created) — remove partial scaffolds so a
|
|
176
|
+
// retry can `init` into the same path without a manual cleanup.
|
|
177
|
+
await rm(appRoot, { recursive: true, force: true }).catch(() => {
|
|
178
|
+
/* best-effort */
|
|
179
|
+
});
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export async function addPrimitiveToApp({ rootDir, kind, name, }) {
|
|
184
|
+
const appRoot = resolve(rootDir);
|
|
185
|
+
const manifest = await loadAppManifest(appRoot);
|
|
186
|
+
const appId = manifest.app.id;
|
|
187
|
+
if (kind === 'orm') {
|
|
188
|
+
throw new Error('adding a second orm namespace is not supported via `seq-studio add` yet — edit sequence.app.yml and scaffold with `seq-studio orm init` manually');
|
|
189
|
+
}
|
|
190
|
+
if (kind === 'artifact' && manifest.primitives.some((entry) => entry.kind === 'artifact')) {
|
|
191
|
+
throw new Error('this app already has an artifact primitive — remove it from sequence.app.yml before re-adding');
|
|
192
|
+
}
|
|
193
|
+
const entryId = primitiveIdFor({ kind, name: kind === 'artifact' ? appId : name });
|
|
194
|
+
if (manifest.primitives.some((entry) => entry.id === entryId)) {
|
|
195
|
+
throw new Error(`primitive id "${entryId}" already exists in sequence.app.yml`);
|
|
196
|
+
}
|
|
197
|
+
const relPath = relativePathFor({ kind, appId, name }).replace(/\\/g, '/');
|
|
198
|
+
if (manifest.primitives.some((entry) => entry.path === relPath)) {
|
|
199
|
+
throw new Error(`path "${relPath}" is already declared in sequence.app.yml`);
|
|
200
|
+
}
|
|
201
|
+
const absPath = join(appRoot, relPath);
|
|
202
|
+
// Refuse before scaffolding so a failed init cannot roll back pre-existing files.
|
|
203
|
+
if (existsSync(absPath)) {
|
|
204
|
+
throw new Error(`${relPath} already exists on disk — remove it or choose a different name before \`seq-studio add\``);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
let entry = await scaffoldKind({ kind, appRoot, appId, name });
|
|
208
|
+
const orm = manifest.primitives.find((item) => item.kind === 'orm');
|
|
209
|
+
if (orm && entry.depends_on.length === 0) {
|
|
210
|
+
entry = { ...entry, depends_on: [orm.id] };
|
|
211
|
+
}
|
|
212
|
+
const primitives = [...manifest.primitives, entry];
|
|
213
|
+
const next = {
|
|
214
|
+
...manifest,
|
|
215
|
+
primitives,
|
|
216
|
+
deploy: {
|
|
217
|
+
...manifest.deploy,
|
|
218
|
+
order: extendDeployOrder({
|
|
219
|
+
existingOrder: manifest.deploy.order,
|
|
220
|
+
primitives,
|
|
221
|
+
newId: entry.id,
|
|
222
|
+
}),
|
|
223
|
+
on_error: manifest.deploy.on_error,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
await writeAppManifest({ rootDir: appRoot, manifest: next });
|
|
227
|
+
return { manifest: next, entry };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// Safe: absPath did not exist before this invocation.
|
|
231
|
+
await rm(absPath, { recursive: true, force: true }).catch(() => {
|
|
232
|
+
/* best-effort */
|
|
233
|
+
});
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export function printInitNextSteps({ appRoot, manifest, }) {
|
|
238
|
+
const rel = relative(process.cwd(), appRoot);
|
|
239
|
+
const display = !rel || rel.startsWith('..') ? appRoot : rel;
|
|
240
|
+
console.log(`${LOG} scaffolded app "${manifest.app.id}" in ${display}`);
|
|
241
|
+
console.log(`${LOG} wrote ${APP_MANIFEST_LABEL} with ${manifest.primitives.length} primitive(s):`);
|
|
242
|
+
for (const entry of manifest.primitives) {
|
|
243
|
+
console.log(`${LOG} - ${entry.kind.padEnd(9)} ${entry.id} (${entry.path})`);
|
|
244
|
+
}
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log('Next:');
|
|
247
|
+
console.log(` cd ${display}`);
|
|
248
|
+
for (const entry of manifest.primitives) {
|
|
249
|
+
console.log(` (cd ${entry.path} && pnpm install)`);
|
|
250
|
+
}
|
|
251
|
+
console.log(' seq-studio login');
|
|
252
|
+
console.log(' seq-studio deploy -e local --yes');
|
|
253
|
+
console.log(' # or targeted:');
|
|
254
|
+
const fn = manifest.primitives.find((entry) => entry.kind === 'function');
|
|
255
|
+
if (fn) {
|
|
256
|
+
console.log(` seq-studio functions deploy --dir ${fn.path} -e local --yes`);
|
|
257
|
+
}
|
|
258
|
+
if (manifest.primitives.some((entry) => entry.kind === 'artifact')) {
|
|
259
|
+
console.log(' seq-studio artifact deploy artifact -e local');
|
|
260
|
+
}
|
|
261
|
+
console.log(' # add another function later:');
|
|
262
|
+
console.log(' seq-studio add function list-types');
|
|
263
|
+
}
|