@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,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
+ }
@@ -9,6 +9,7 @@ import { PREVIEW_DOMAIN } from './preview.js';
9
9
  const MAX_503_RETRIES = 5;
10
10
  const DEFAULT_RETRY_AFTER_SECONDS = 2;
11
11
  const LOG_PREFIX = '[seq-studio]';
12
+ const CF_ACCESS_DOMAIN = 'seqholdings.com';
12
13
  export class AtlasApiError extends Error {
13
14
  status;
14
15
  path;
@@ -42,6 +43,7 @@ async function authenticatedFetch({ baseUrl, init = {}, path, token, }) {
42
43
  redirect: 'manual',
43
44
  headers: {
44
45
  ...previewAccessHeaders(baseUrl),
46
+ ...cfAccessHeaders(baseUrl),
45
47
  ...init.headers,
46
48
  Authorization: `Bearer ${token}`,
47
49
  },
@@ -63,6 +65,33 @@ function previewAccessHeaders(baseUrl) {
63
65
  return {};
64
66
  }
65
67
  }
68
+ /**
69
+ * Cloudflare Access fronts *.seqholdings.com and only bypasses the company
70
+ * network, so a CI runner is turned away at the edge with an HTML "Access
71
+ * Restricted" page before Atlas ever sees the bearer token. A service token
72
+ * gets through the edge; the bearer still authenticates at the app.
73
+ *
74
+ * Scoped to seqholdings.com so the token is never sent to localhost, a tenant
75
+ * domain, or any other host the CLI can be pointed at.
76
+ */
77
+ function cfAccessHeaders(baseUrl) {
78
+ const clientId = process.env.CF_ACCESS_CLIENT_ID?.trim();
79
+ const clientSecret = process.env.CF_ACCESS_CLIENT_SECRET?.trim();
80
+ if (!clientId || !clientSecret)
81
+ return {};
82
+ try {
83
+ const parsed = new URL(baseUrl);
84
+ if (parsed.protocol !== 'https:')
85
+ return {};
86
+ const hostname = parsed.hostname.toLowerCase();
87
+ if (hostname !== CF_ACCESS_DOMAIN && !hostname.endsWith(`.${CF_ACCESS_DOMAIN}`))
88
+ return {};
89
+ return { 'CF-Access-Client-Id': clientId, 'CF-Access-Client-Secret': clientSecret };
90
+ }
91
+ catch {
92
+ return {};
93
+ }
94
+ }
66
95
  function messageFromBody(body, status, statusText) {
67
96
  if (body && typeof body === 'object') {
68
97
  const record = body;
package/dist/auth.js CHANGED
@@ -43,6 +43,8 @@ const SEQUENCE_BUILTIN_ENVS = new Set(['local', 'staging', 'production', 'bankso
43
43
  export function isSequenceAuthEnvName(envName) {
44
44
  return (envName === SEQUENCE_REALM ||
45
45
  SEQUENCE_BUILTIN_ENVS.has(envName) ||
46
+ envName === 'worktree' ||
47
+ envName.startsWith('local:') ||
46
48
  envName === 'preview' ||
47
49
  envName.startsWith('preview:'));
48
50
  }
@@ -198,6 +198,7 @@ entrypoint: handler
198
198
  limits:
199
199
  memory_mb: 256
200
200
  timeout_seconds: 60
201
+ min_instances: 0
201
202
  max_instances: 3
202
203
  invoke_rate_per_minute: 60
203
204
 
@@ -5,6 +5,20 @@ import { z } from 'zod';
5
5
  * as artifact-studio's duplicated manifest. Keep the two in sync.
6
6
  */
7
7
  export declare const MF_MANIFEST_FILENAME = "managed-function.yml";
8
+ export declare const managedFunctionAuthorizationAdapterSchema: z.ZodEnum<{
9
+ "encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
10
+ "encompass.loan-read-by-number": "encompass.loan-read-by-number";
11
+ "encompass.loan-search": "encompass.loan-search";
12
+ }>;
13
+ export type ManagedFunctionAuthorizationAdapter = z.infer<typeof managedFunctionAuthorizationAdapterSchema>;
14
+ export declare const managedFunctionAuthorizationSchema: z.ZodObject<{
15
+ version: z.ZodLiteral<1>;
16
+ adapter: z.ZodEnum<{
17
+ "encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
18
+ "encompass.loan-read-by-number": "encompass.loan-read-by-number";
19
+ "encompass.loan-search": "encompass.loan-search";
20
+ }>;
21
+ }, z.core.$strict>;
8
22
  /**
9
23
  * CLI-side copy of the server's egress entry validation (see
10
24
  * atlas/src/server/services/managed-functions/egress.ts). Returns the
@@ -52,12 +66,21 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
52
66
  limits: z.ZodDefault<z.ZodObject<{
53
67
  memory_mb: z.ZodDefault<z.ZodNumber>;
54
68
  timeout_seconds: z.ZodDefault<z.ZodNumber>;
69
+ min_instances: z.ZodDefault<z.ZodNumber>;
55
70
  max_instances: z.ZodDefault<z.ZodNumber>;
56
71
  invoke_rate_per_minute: z.ZodDefault<z.ZodNumber>;
57
72
  }, z.core.$strip>>;
58
73
  secrets: z.ZodDefault<z.ZodArray<z.ZodString>>;
59
74
  egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
60
75
  service_account: z.ZodOptional<z.ZodString>;
76
+ authorization: z.ZodOptional<z.ZodObject<{
77
+ version: z.ZodLiteral<1>;
78
+ adapter: z.ZodEnum<{
79
+ "encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
80
+ "encompass.loan-read-by-number": "encompass.loan-read-by-number";
81
+ "encompass.loan-search": "encompass.loan-search";
82
+ }>;
83
+ }, z.core.$strict>>;
61
84
  input_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
62
85
  output_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
63
86
  capabilities: z.ZodDefault<z.ZodObject<{
@@ -83,6 +106,7 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
83
106
  data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
84
107
  tables: z.ZodDefault<z.ZodArray<z.ZodString>>;
85
108
  actions: z.ZodDefault<z.ZodArray<z.ZodString>>;
109
+ operations: z.ZodDefault<z.ZodArray<z.ZodString>>;
86
110
  query: z.ZodDefault<z.ZodBoolean>;
87
111
  }, z.core.$strip>>>;
88
112
  }, z.core.$strip>>;
@@ -6,6 +6,22 @@ import { z } from 'zod';
6
6
  */
7
7
  export const MF_MANIFEST_FILENAME = 'managed-function.yml';
8
8
  const SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
9
+ export const managedFunctionAuthorizationAdapterSchema = z.enum([
10
+ 'encompass.loan-read-by-guid',
11
+ 'encompass.loan-read-by-number',
12
+ 'encompass.loan-search',
13
+ ]);
14
+ export const managedFunctionAuthorizationSchema = z
15
+ .object({
16
+ version: z.literal(1),
17
+ adapter: managedFunctionAuthorizationAdapterSchema,
18
+ })
19
+ .strict();
20
+ const AUTHORIZATION_ADAPTER_BY_FUNCTION_ID = {
21
+ 'encompass-get-common-loan-fields': 'encompass.loan-read-by-guid',
22
+ 'encompass-get-loan-by-number': 'encompass.loan-read-by-number',
23
+ 'encompass-search-pipeline': 'encompass.loan-search',
24
+ };
9
25
  /** RFC 1035 label: alnum, optional inner dashes, max 63 chars. */
10
26
  const DNS_LABEL_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
11
27
  /**
@@ -493,6 +509,12 @@ function validateCapabilityGates({ gates, uses, roles, }) {
493
509
  * Mirrors atlas/src/server/services/managed-functions/manifest.ts.
494
510
  */
495
511
  const SERVICE_ACCOUNT_REF_RE = /^([a-z][a-z0-9-]{1,98}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
512
+ /**
513
+ * GraphQL Name grammar — persisted-operation names exactly as they appear in
514
+ * a namespace's operations manifest. Mirrors
515
+ * atlas/src/server/services/managed-functions/manifest.ts.
516
+ */
517
+ const GRAPHQL_OPERATION_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
496
518
  export const managedFunctionManifestSchema = z.object({
497
519
  schema_version: z.literal(1).default(1),
498
520
  function: z.object({
@@ -510,12 +532,23 @@ export const managedFunctionManifestSchema = z.object({
510
532
  .object({
511
533
  memory_mb: z.number().int().min(128).max(2048).default(256),
512
534
  timeout_seconds: z.number().int().min(1).max(540).default(60),
535
+ min_instances: z.number().int().min(0).max(10).default(0),
513
536
  max_instances: z.number().int().min(1).max(10).default(3),
514
537
  invoke_rate_per_minute: z.number().int().min(1).max(600).default(60),
538
+ })
539
+ .superRefine((limits, ctx) => {
540
+ if (limits.min_instances > limits.max_instances) {
541
+ ctx.addIssue({
542
+ code: 'custom',
543
+ message: 'min_instances cannot exceed max_instances',
544
+ path: ['min_instances'],
545
+ });
546
+ }
515
547
  })
516
548
  .default({
517
549
  memory_mb: 256,
518
550
  timeout_seconds: 60,
551
+ min_instances: 0,
519
552
  max_instances: 3,
520
553
  invoke_rate_per_minute: 60,
521
554
  }),
@@ -554,6 +587,11 @@ export const managedFunctionManifestSchema = z.object({
554
587
  .string()
555
588
  .regex(SERVICE_ACCOUNT_REF_RE, 'service_account must be a platform service-account slug (lowercase alphanumeric with hyphens) or uuid')
556
589
  .optional(),
590
+ /**
591
+ * Server-owned resource authorization. Authors select a reviewed adapter;
592
+ * they cannot provide input/output paths or executable policy.
593
+ */
594
+ authorization: managedFunctionAuthorizationSchema.optional(),
557
595
  input_schema: z.record(z.string(), z.unknown()).optional(),
558
596
  output_schema: z.record(z.string(), z.unknown()).optional(),
559
597
  /**
@@ -567,12 +605,13 @@ export const managedFunctionManifestSchema = z.object({
567
605
  gates: capabilityGatesSchema,
568
606
  /**
569
607
  * ORM Data API consumer reach, grouped by namespace: the `tables` this
570
- * function may read, the `actions` it may invoke, and whether raw `query`
571
- * (arbitrary read SQL over the namespace) is allowed — all ON BEHALF OF the
572
- * invoking user. The invoke proxy mints a short-lived token scoped to
573
- * exactly these refs; the Data API re-resolves the user's claims per call,
574
- * so declaring a table never widens what the user could see. Writes go
575
- * through declared actions only raw table writes don't exist.
608
+ * function may read, the v1 `actions` and v2 persisted `operations` it may
609
+ * invoke, and whether raw `query` (arbitrary read SQL over the namespace)
610
+ * is allowed — all ON BEHALF OF the invoking user. The invoke proxy mints
611
+ * a short-lived token scoped to exactly these refs; the Data API
612
+ * re-resolves the user's claims per call, so declaring a table never
613
+ * widens what the user could see. Writes go through declared actions and
614
+ * operations only — raw table writes don't exist.
576
615
  *
577
616
  * Mirrors atlas/src/server/services/managed-functions/manifest.ts — must
578
617
  * stay in sync so `seq-studio` doesn't strip the block before deploy.
@@ -581,11 +620,47 @@ export const managedFunctionManifestSchema = z.object({
581
620
  .record(z.string().regex(/^[a-z][a-z0-9_]{0,40}$/, 'capabilities.data keys are ORM namespace names'), z.object({
582
621
  tables: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'table names')).max(64).default([]),
583
622
  actions: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'action names')).max(64).default([]),
623
+ /**
624
+ * ORM v2 persisted operations (GraphQL mutations/actions by name)
625
+ * this function may execute — each mints a `<ns>/ops/<Name>` write
626
+ * ref on the invoke token.
627
+ */
628
+ operations: z
629
+ .array(z
630
+ .string()
631
+ .max(128)
632
+ .regex(GRAPHQL_OPERATION_NAME_RE, 'operation names must match the GraphQL Name grammar'))
633
+ .max(64)
634
+ .default([]),
584
635
  query: z.boolean().default(false),
585
636
  }))
586
637
  .default({}),
587
638
  }).default({ uses: [], gates: [], data: {} }),
588
639
  }).superRefine((manifest, ctx) => {
640
+ const expectedAdapter = AUTHORIZATION_ADAPTER_BY_FUNCTION_ID[manifest.function.id];
641
+ if (expectedAdapter !== undefined && manifest.authorization === undefined) {
642
+ ctx.addIssue({
643
+ code: 'custom',
644
+ message: `function "${manifest.function.id}" requires authorization adapter "${expectedAdapter}"`,
645
+ path: ['authorization'],
646
+ });
647
+ }
648
+ else if (expectedAdapter !== undefined &&
649
+ manifest.authorization?.adapter !== expectedAdapter) {
650
+ ctx.addIssue({
651
+ code: 'custom',
652
+ message: `function "${manifest.function.id}" must use authorization adapter "${expectedAdapter}"`,
653
+ path: ['authorization', 'adapter'],
654
+ });
655
+ }
656
+ else if (expectedAdapter === undefined &&
657
+ manifest.authorization !== undefined) {
658
+ ctx.addIssue({
659
+ code: 'custom',
660
+ message: `function "${manifest.function.id}" is not registered for a server-owned authorization adapter`,
661
+ path: ['authorization'],
662
+ });
663
+ }
589
664
  for (const message of validateCapabilityPins({
590
665
  uses: manifest.capabilities.uses,
591
666
  roles: manifest.capabilities.roles,
@@ -600,9 +675,9 @@ export const managedFunctionManifestSchema = z.object({
600
675
  ctx.addIssue({ code: 'custom', message, path: ['capabilities', 'gates'] });
601
676
  }
602
677
  // A namespace is "reached" when its block declares at least one table,
603
- // action, or raw query — the same rule the server's floor reconciliation
604
- // uses (readManifestDataNamespaces).
605
- const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.query);
678
+ // action, operation, or raw query — the same rule the server's floor
679
+ // reconciliation uses (readManifestDataNamespaces).
680
+ const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.operations.length > 0 || block.query);
606
681
  if (reachesData && manifest.service_account === undefined) {
607
682
  ctx.addIssue({
608
683
  code: 'custom',
package/dist/main.d.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  * seq-studio argv router.
3
3
  *
4
4
  * Top-level commands:
5
+ * seq-studio init scaffold an app monorepo (sequence.app.yml)
6
+ * seq-studio add add a primitive to an app monorepo
7
+ * seq-studio deploy deploy all primitives from sequence.app.yml
5
8
  * seq-studio process <sub> manage Lattice processes
6
9
  * seq-studio artifact <sub> manage Artifact Studio apps
7
10
  * seq-studio functions <sub> manage Managed Functions
package/dist/main.js CHANGED
@@ -2,6 +2,9 @@
2
2
  * seq-studio argv router.
3
3
  *
4
4
  * Top-level commands:
5
+ * seq-studio init scaffold an app monorepo (sequence.app.yml)
6
+ * seq-studio add add a primitive to an app monorepo
7
+ * seq-studio deploy deploy all primitives from sequence.app.yml
5
8
  * seq-studio process <sub> manage Lattice processes
6
9
  * seq-studio artifact <sub> manage Artifact Studio apps
7
10
  * seq-studio functions <sub> manage Managed Functions
@@ -21,6 +24,9 @@ import { applyCommand, bundleCommand, doctorCommand, initCommand, lintCommand, p
21
24
  // Lazy-load artifact delegate: published @sequenceholdings/artifact-studio/cli
22
25
  // still auto-runs runCli() at module load; importing it here breaks doctor/process.
23
26
  const TOP_LEVEL_USAGE = `usage:
27
+ seq-studio init <dir> --with <kinds> scaffold an app monorepo (sequence.app.yml)
28
+ seq-studio add <kind> <name> add a primitive to the current app monorepo
29
+ seq-studio deploy -e <env> deploy primitives from sequence.app.yml
24
30
  seq-studio process <sub> [args] lint | plan | apply | test | simulate | bundle | init
25
31
  seq-studio artifact <sub> [args] init | build | plan | deploy | dev | list | show | pull | promote | rollback
26
32
  seq-studio functions <sub> [args] init | build | deploy | list | show | logs | promote | rollback | delete
@@ -65,6 +71,21 @@ export async function run(argv = process.argv.slice(2)) {
65
71
  return namespace ? 0 : 1;
66
72
  }
67
73
  switch (namespace) {
74
+ case 'init': {
75
+ const { runAppInitCommand } = await import('./app/commands.js');
76
+ return runAppInitCommand(parseArgs([sub, ...rest].filter((a) => Boolean(a))));
77
+ }
78
+ case 'add': {
79
+ const { runAppAddCommand } = await import('./app/commands.js');
80
+ return runAppAddCommand({
81
+ kindArg: sub,
82
+ args: parseArgs(rest),
83
+ });
84
+ }
85
+ case 'deploy': {
86
+ const { runAppDeployCommand } = await import('./app/commands.js');
87
+ return runAppDeployCommand(parseArgs([sub, ...rest].filter((a) => Boolean(a))));
88
+ }
68
89
  case 'process':
69
90
  return runProcessNamespace(sub, rest);
70
91
  case 'artifact': {
@@ -0,0 +1,2 @@
1
+ import type { ParsedArgs } from '../process/commands.js';
2
+ export declare function pipelineCodegenTablesCommand(args: ParsedArgs): Promise<number>;