@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.
@@ -16,7 +16,8 @@ import { existsSync, readdirSync, rmSync } from 'node:fs';
16
16
  import { homedir } from 'node:os';
17
17
  import { resolve, sep } from 'node:path';
18
18
  import { materializeRepo, resolveCommitSha, resolveRepo, } from '@sequenceholdings/artifact-studio/git-service-client';
19
- import { deleteNoContent, getJson, postJson } from '../atlas-client.js';
19
+ import { z } from 'zod';
20
+ import { AtlasApiError, deleteNoContent, getJson, getJsonOr404, postJson, } from '../atlas-client.js';
20
21
  import { printCliError } from '../cli-errors.js';
21
22
  import { PREVIEW_DOMAIN } from '../preview.js';
22
23
  import { confirmYes } from '../prompt.js';
@@ -468,6 +469,257 @@ function defaultOutFromCloneTarget({ url, id, }) {
468
469
  const match = /\/repos\/([^/]+)\/git\/?$/.exec(new URL(url).pathname);
469
470
  return match?.[1] ?? 'repo';
470
471
  }
472
+ /**
473
+ * Mirror of atlas `ci-discover` Zod contract so `repos ci show` fails closed
474
+ * on the same invalid `.seq/ci.json` shapes the runner rejects (`ci/config`).
475
+ * Keep in sync with `atlas/src/server/services/git-service/ci-discover.ts`.
476
+ */
477
+ const CiPhaseSchema = z.enum(['check', 'test']);
478
+ const CiJsonEntrySchema = z
479
+ .object({
480
+ phase: CiPhaseSchema,
481
+ name: z
482
+ .string()
483
+ .min(1)
484
+ .max(64)
485
+ .regex(/^[a-z][a-z0-9._/-]*$/)
486
+ .optional(),
487
+ script: z.string().min(1).max(128).optional(),
488
+ command: z.array(z.string().min(1).max(256)).min(1).max(32).optional(),
489
+ })
490
+ .superRefine((entry, ctx) => {
491
+ const hasScript = entry.script != null;
492
+ const hasCommand = entry.command != null;
493
+ if (hasScript === hasCommand) {
494
+ ctx.addIssue({
495
+ code: z.ZodIssueCode.custom,
496
+ message: 'each check must specify exactly one of script or command',
497
+ path: hasScript ? ['command'] : ['script'],
498
+ });
499
+ }
500
+ });
501
+ const CiJsonSchema = z.object({
502
+ checks: z.array(CiJsonEntrySchema).max(16),
503
+ });
504
+ /** Match atlas `ci-discover` defaultNameForPhase (per-phase index, not array index). */
505
+ function defaultNameForPhase(phase, indexInPhase) {
506
+ if (indexInPhase === 0)
507
+ return `ci/${phase}`;
508
+ return `ci/${phase}-${indexInPhase + 1}`;
509
+ }
510
+ /**
511
+ * Pure discovery shared by `repos ci show` / `import`. Mirrors atlas
512
+ * `discoverCiChecks` / `parseCiJson` so CLI preview matches execution.
513
+ */
514
+ export function discoverCliChecks({ ciJson, packageScripts, }) {
515
+ if (ciJson !== undefined) {
516
+ const parsed = CiJsonSchema.safeParse(ciJson);
517
+ if (!parsed.success) {
518
+ const detail = parsed.error.issues
519
+ .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
520
+ .slice(0, 3)
521
+ .join('; ');
522
+ return { ok: false, error: `Invalid .seq/ci.json: ${detail}` };
523
+ }
524
+ const phaseCounts = new Map();
525
+ const seenNames = new Set();
526
+ const checks = [];
527
+ for (const entry of parsed.data.checks) {
528
+ const indexInPhase = phaseCounts.get(entry.phase) ?? 0;
529
+ phaseCounts.set(entry.phase, indexInPhase + 1);
530
+ const name = entry.name ?? defaultNameForPhase(entry.phase, indexInPhase);
531
+ if (seenNames.has(name)) {
532
+ return {
533
+ ok: false,
534
+ error: `Invalid .seq/ci.json: duplicate check name "${name}"`,
535
+ };
536
+ }
537
+ seenNames.add(name);
538
+ const check = {
539
+ name,
540
+ source: 'ci.json',
541
+ };
542
+ if (entry.script != null)
543
+ check.script = entry.script;
544
+ if (entry.command != null)
545
+ check.command = entry.command;
546
+ checks.push(check);
547
+ }
548
+ return { ok: true, checks };
549
+ }
550
+ const checks = [];
551
+ if (packageScripts) {
552
+ for (const candidate of ['lint', 'typecheck', 'check']) {
553
+ if (packageScripts[candidate]?.trim()) {
554
+ checks.push({ name: 'ci/check', script: candidate, source: 'autodiscover' });
555
+ break;
556
+ }
557
+ }
558
+ if (packageScripts.test?.trim()) {
559
+ checks.push({ name: 'ci/test', script: 'test', source: 'autodiscover' });
560
+ }
561
+ }
562
+ return { ok: true, checks };
563
+ }
564
+ function decodeFileContent(file) {
565
+ return file.encoding === 'base64'
566
+ ? Buffer.from(file.content, 'base64').toString('utf8')
567
+ : file.content;
568
+ }
569
+ /**
570
+ * Load `.seq/ci.json` / `package.json` for CI preview.
571
+ *
572
+ * Matches the runner: only a true 404 means "file absent". Invalid JSON and
573
+ * non-404 API errors fail closed (no silent package.json fallback over a bad
574
+ * `.seq/ci.json`).
575
+ */
576
+ export async function loadRemoteCiSources({ ctx, repoId, ref, }) {
577
+ let ciJson;
578
+ try {
579
+ const file = await getJsonOr404({
580
+ ...clientOptions(ctx),
581
+ path: `/api/git-service/repos/${repoId}/contents/.seq/ci.json?ref=${encodeURIComponent(ref)}`,
582
+ });
583
+ if (file != null) {
584
+ try {
585
+ ciJson = JSON.parse(decodeFileContent(file));
586
+ }
587
+ catch {
588
+ return { ok: false, error: 'invalid .seq/ci.json: not valid JSON' };
589
+ }
590
+ }
591
+ }
592
+ catch (err) {
593
+ if (err instanceof AtlasApiError) {
594
+ return { ok: false, error: `failed to fetch .seq/ci.json: ${err.message}` };
595
+ }
596
+ throw err;
597
+ }
598
+ // When ci.json is present (including empty checks), skip package.json —
599
+ // discovery never falls through to autodiscover in that case.
600
+ if (ciJson !== undefined) {
601
+ return { ok: true, ciJson };
602
+ }
603
+ let packageScripts;
604
+ try {
605
+ const file = await getJsonOr404({
606
+ ...clientOptions(ctx),
607
+ path: `/api/git-service/repos/${repoId}/contents/package.json?ref=${encodeURIComponent(ref)}`,
608
+ });
609
+ if (file != null) {
610
+ let pkg;
611
+ try {
612
+ pkg = JSON.parse(decodeFileContent(file));
613
+ }
614
+ catch {
615
+ return { ok: false, error: 'invalid package.json: not valid JSON' };
616
+ }
617
+ if (pkg.scripts && typeof pkg.scripts === 'object') {
618
+ packageScripts = {};
619
+ for (const [key, value] of Object.entries(pkg.scripts)) {
620
+ if (typeof value === 'string')
621
+ packageScripts[key] = value;
622
+ }
623
+ }
624
+ }
625
+ }
626
+ catch (err) {
627
+ if (err instanceof AtlasApiError) {
628
+ return { ok: false, error: `failed to fetch package.json: ${err.message}` };
629
+ }
630
+ throw err;
631
+ }
632
+ return { ok: true, packageScripts };
633
+ }
634
+ /**
635
+ * Discover CI checks from a remote repo tip (pure preview — no Settings write).
636
+ * Uses the same autodiscovery rules as the platform runner (PLA-378).
637
+ */
638
+ export async function reposCiShowCommand(args) {
639
+ const { namespace, name } = parseRepoPath(args.positional[0]);
640
+ const ctx = await reposContext(args);
641
+ const resolved = await resolveRepo({ ...clientOptions(ctx), namespace, name });
642
+ const repo = await getJson({
643
+ ...clientOptions(ctx),
644
+ path: `/api/git-service/repos/${resolved.id}`,
645
+ });
646
+ const ref = stringFlag(args.flags, 'ref') ?? repo.defaultBranch;
647
+ const required = new Set(repo.reviewPolicy?.requiredChecks ?? []);
648
+ const sources = await loadRemoteCiSources({ ctx, repoId: resolved.id, ref });
649
+ if (!sources.ok) {
650
+ console.error(`${LOG} ${sources.error} on ${namespace}/${name}@${ref}`);
651
+ return 1;
652
+ }
653
+ const discovered = discoverCliChecks(sources);
654
+ if (!discovered.ok) {
655
+ console.error(`${LOG} ${discovered.error} on ${namespace}/${name}@${ref}`);
656
+ return 1;
657
+ }
658
+ console.log(`${LOG} CI for ${namespace}/${name}@${ref} on ${ctx.env.name}:`);
659
+ if (discovered.checks.length === 0) {
660
+ console.log(' (no checks discovered)');
661
+ return 0;
662
+ }
663
+ for (const check of discovered.checks) {
664
+ const how = check.script != null
665
+ ? `script=${check.script}`
666
+ : check.command != null
667
+ ? `command=${JSON.stringify(check.command)}`
668
+ : '';
669
+ console.log(` ${check.name} ${how} source=${check.source} required=${required.has(check.name)}`);
670
+ }
671
+ return 0;
672
+ }
673
+ const REQUIRED_CHECKS_WRITE_DISABLED = `${LOG} writing requiredChecks is disabled until the sandboxed CI runner is live ` +
674
+ `(discovery posts neutral check-runs that cannot satisfy the merge gate). ` +
675
+ `Use \`seq-studio repos ci show\` to preview discovered names.`;
676
+ /** Add a check-run name to Settings `requiredChecks` (merge-blocking). */
677
+ export async function reposCiRequireCommand(args) {
678
+ const check = stringFlag(args.flags, 'check');
679
+ if (!check) {
680
+ console.error('usage: seq-studio repos ci require <ns>/<name> --check <name> -e <env>');
681
+ return 1;
682
+ }
683
+ console.error(REQUIRED_CHECKS_WRITE_DISABLED);
684
+ return 1;
685
+ }
686
+ /**
687
+ * Set requiredChecks to every check discovered on the tip (opt-in bulk require).
688
+ */
689
+ export async function reposCiImportCommand(args) {
690
+ // Still run show so authors can preview names, then refuse the write.
691
+ const code = await reposCiShowCommand(args);
692
+ if (code !== 0)
693
+ return code;
694
+ console.error(REQUIRED_CHECKS_WRITE_DISABLED);
695
+ return 1;
696
+ }
697
+ export async function reposCiCommand(args) {
698
+ const [action] = args.positional;
699
+ const rest = {
700
+ ...args,
701
+ positional: args.positional.slice(1),
702
+ };
703
+ switch (action) {
704
+ case 'show':
705
+ return reposCiShowCommand(rest);
706
+ case 'require':
707
+ return reposCiRequireCommand(rest);
708
+ case 'import':
709
+ return reposCiImportCommand(rest);
710
+ case 'help':
711
+ case undefined:
712
+ console.log(`usage:
713
+ seq-studio repos ci show <ns>/<name> -e <env> [--ref r] preview discovered CI checks
714
+ seq-studio repos ci require <ns>/<name> --check <name> -e <env> reserved (refuses write until sandboxed runner)
715
+ seq-studio repos ci import <ns>/<name> -e <env> [--ref r] reserved (refuses write until sandboxed runner)
716
+ `);
717
+ return action ? 0 : 1;
718
+ default:
719
+ console.error(`unknown repos ci command: ${action}`);
720
+ return 1;
721
+ }
722
+ }
471
723
  export async function reposDeleteCommand(args) {
472
724
  const { namespace, name } = parseRepoPath(args.positional[0]);
473
725
  const ctx = await reposContext(args);
@@ -505,6 +757,9 @@ export const REPOS_USAGE = `usage:
505
757
  seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]
506
758
  materialize the tree at a ref (JSON API)
507
759
  seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)
760
+ seq-studio repos ci show <ns>/<name> -e <env> [--ref r] preview discovered CI checks
761
+ seq-studio repos ci require <ns>/<name> --check <name> -e <env> reserved (refuses write until sandboxed runner)
762
+ seq-studio repos ci import <ns>/<name> -e <env> [--ref r] reserved (refuses write until sandboxed runner)
508
763
 
509
764
  Flags: -e/--env <env> (required; see: seq-studio envs list)
510
765
 
@@ -534,6 +789,8 @@ export async function runReposCommand(sub, args) {
534
789
  return await reposPullCommand(args);
535
790
  case 'delete':
536
791
  return await reposDeleteCommand(args);
792
+ case 'ci':
793
+ return await reposCiCommand(args);
537
794
  case 'help':
538
795
  case '--help':
539
796
  case '-h':
@@ -1,5 +1,7 @@
1
1
  import type { ParsedArgs } from '../process/commands.js';
2
2
  export declare const LOG = "[seq-studio]";
3
+ /** Drop one trailing LF or CRLF so Windows-authored --from-file values match Unix. */
4
+ export declare function stripOneTrailingNewline(value: string): string;
3
5
  export declare function secretsCreateCommand(args: ParsedArgs): Promise<number>;
4
6
  export declare function secretsSetCommand(args: ParsedArgs): Promise<number>;
5
7
  export declare function secretsListCommand(args: ParsedArgs): Promise<number>;
@@ -20,5 +22,5 @@ export declare function secretsSetDefaultCommand(args: ParsedArgs): Promise<numb
20
22
  * UI: pin + redeploy the function's active version / pin only / cancel.
21
23
  */
22
24
  export declare function secretsPinCommand(args: ParsedArgs): Promise<number>;
23
- export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> -e <env> [--description text] register an org-owned secret\n seq-studio secrets set <NAME> -e <env> set the shared default value (write-only)\n seq-studio secrets list -e <env> secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> -e <env> mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> -e <env> remove attachment\n seq-studio secrets apply --from-env-file .env -e <env> push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> -e <env> value version history (never values)\n seq-studio secrets set-default <NAME> [version] -e <env> point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
25
+ export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> -e <env> [--org <slug>] [--description text]\n register an org-owned secret\n seq-studio secrets set <NAME> -e <env> [--org <slug>] [--from-file <path>]\n set the shared default value (write-only)\n seq-studio secrets list -e <env> secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> -e <env> [--org <slug>]\n mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> -e <env> [--org <slug>]\n remove attachment\n seq-studio secrets apply --from-env-file .env -e <env> push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> -e <env> [--org <slug>] value version history (never values)\n seq-studio secrets set-default <NAME> [version] -e <env> [--org <slug>]\n point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> [--org <slug>]\n pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n --org <slug> (target/select managed-scope org; required when a name exists in multiple orgs)\n set: --from-file <path> (non-interactive; file is not echoed)\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
24
26
  export declare function runSecretsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
@@ -41,6 +41,25 @@ function parseCommaList(value) {
41
41
  .map((part) => part.trim())
42
42
  .filter(Boolean);
43
43
  }
44
+ function orgFlag(args) {
45
+ return typeof args.flags.org === 'string' ? args.flags.org.trim().toLowerCase() : undefined;
46
+ }
47
+ /** First-party Sequence Atlas envs — their Lakebase registry is org=sequence. */
48
+ const SEQUENCE_OWNED_ENVS = new Set(['local', 'staging', 'production']);
49
+ /** Refuse --org that would write a tenant row into the wrong deployment registry. */
50
+ function orgDeploymentError(org, envName) {
51
+ if (SEQUENCE_OWNED_ENVS.has(envName) && org !== 'sequence') {
52
+ return `--org ${org} must be used with -e ${org} (${envName} only holds the sequence secret registry)`;
53
+ }
54
+ if (!SEQUENCE_OWNED_ENVS.has(envName) && org === 'sequence') {
55
+ return `--org sequence is not valid on -e ${envName} (that deployment does not own the Sequence registry)`;
56
+ }
57
+ return null;
58
+ }
59
+ /** Drop one trailing LF or CRLF so Windows-authored --from-file values match Unix. */
60
+ export function stripOneTrailingNewline(value) {
61
+ return value.replace(/\r?\n$/, '');
62
+ }
44
63
  async function listSecrets(ctx) {
45
64
  const { secrets } = await getJson({
46
65
  ...clientOptions(ctx),
@@ -48,11 +67,24 @@ async function listSecrets(ctx) {
48
67
  });
49
68
  return secrets;
50
69
  }
51
- async function resolveSecretByName({ ctx, name, }) {
70
+ async function resolveSecretByName({ ctx, name, org, }) {
52
71
  const secrets = await listSecrets(ctx);
53
- const secret = secrets.find((row) => row.name === name);
72
+ const matches = secrets.filter((row) => row.name === name);
73
+ const scoped = org ? matches.filter((row) => row.orgId === org) : matches;
74
+ if (scoped.length === 0) {
75
+ throw new Error(`Managed secret "${name}" not found on ${ctx.env.name}` +
76
+ (org ? ` org=${org}` : '') +
77
+ ` (or you don't have access).`);
78
+ }
79
+ if (scoped.length > 1) {
80
+ const orgs = scoped.map((row) => row.orgId).join(', ');
81
+ throw new Error(`Managed secret "${name}" exists in multiple orgs (${orgs}). Pass --org <slug>.`);
82
+ }
83
+ const [secret] = scoped;
54
84
  if (!secret) {
55
- throw new Error(`Managed secret "${name}" not found on ${ctx.env.name} (or you don't have access).`);
85
+ throw new Error(`Managed secret "${name}" not found on ${ctx.env.name}` +
86
+ (org ? ` org=${org}` : '') +
87
+ ` (or you don't have access).`);
56
88
  }
57
89
  return secret;
58
90
  }
@@ -128,20 +160,29 @@ async function functionExistsBySlugEventually({ ctx, slug, attempts = 3, retryDe
128
160
  export async function secretsCreateCommand(args) {
129
161
  const name = args.positional[0];
130
162
  if (!name) {
131
- console.error('usage: seq-studio secrets create <NAME> -e <env> [--description text]');
163
+ console.error('usage: seq-studio secrets create <NAME> -e <env> [--org <slug>] [--description text]');
132
164
  return 1;
133
165
  }
134
166
  const ctx = await buildContext(args);
135
167
  const description = typeof args.flags.description === 'string' ? args.flags.description : undefined;
168
+ const org = orgFlag(args);
169
+ if (org) {
170
+ const mismatch = orgDeploymentError(org, ctx.env.name);
171
+ if (mismatch) {
172
+ console.error(`${LOG} ${mismatch}`);
173
+ return 1;
174
+ }
175
+ }
136
176
  const secret = await postJson({
137
177
  ...clientOptions(ctx),
138
178
  path: '/api/managed-secrets',
139
179
  body: {
140
180
  name,
141
181
  description: description ?? null,
182
+ ...(org ? { orgId: org } : {}),
142
183
  },
143
184
  });
144
- console.log(`${LOG} created managed secret "${secret.name}" (${secret.id}) on ${ctx.env.name}`);
185
+ console.log(`${LOG} created managed secret "${secret.name}" (${secret.id}) org=${secret.orgId} on ${ctx.env.name}`);
145
186
  return 0;
146
187
  }
147
188
  // ---------------------------------------------------------------------------
@@ -150,12 +191,24 @@ export async function secretsCreateCommand(args) {
150
191
  export async function secretsSetCommand(args) {
151
192
  const name = args.positional[0];
152
193
  if (!name) {
153
- console.error('usage: seq-studio secrets set <NAME> -e <env>');
194
+ console.error('usage: seq-studio secrets set <NAME> -e <env> [--org <slug>] [--from-file <path>]');
154
195
  return 1;
155
196
  }
156
197
  const ctx = await buildContext(args);
157
- const secret = await resolveSecretByName({ ctx, name });
158
- const value = await promptHidden(`Default value for ${name} (input hidden): `);
198
+ const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
199
+ const fromFile = typeof args.flags['from-file'] === 'string' ? args.flags['from-file'] : undefined;
200
+ let value;
201
+ if (fromFile) {
202
+ const filePath = isAbsolute(fromFile) ? fromFile : resolve(workDir(args), fromFile);
203
+ if (!existsSync(filePath)) {
204
+ console.error(`${LOG} --from-file not found: ${filePath}`);
205
+ return 1;
206
+ }
207
+ value = stripOneTrailingNewline(await readFile(filePath, 'utf8'));
208
+ }
209
+ else {
210
+ value = await promptHidden(`Default value for ${name} (input hidden): `);
211
+ }
159
212
  if (!value) {
160
213
  console.error(`${LOG} empty value — aborted`);
161
214
  return 1;
@@ -183,7 +236,7 @@ export async function secretsListCommand(args) {
183
236
  for (const secret of secrets) {
184
237
  const defaultState = secret.hasDefaultValue ? 'default set' : 'no default';
185
238
  const access = secret.currentUserAccess ?? 'none';
186
- console.log(` ${secret.name} [${defaultState}] access=${access} attachments=${secret.attachmentCount}`);
239
+ console.log(` ${secret.name} org=${secret.orgId} [${defaultState}] access=${access} attachments=${secret.attachmentCount}`);
187
240
  }
188
241
  return 0;
189
242
  }
@@ -194,11 +247,11 @@ export async function secretsAttachCommand(args) {
194
247
  const name = args.positional[0];
195
248
  const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : args.positional[1];
196
249
  if (!name || !fnSlug) {
197
- console.error('usage: seq-studio secrets attach <NAME> --fn <slug> -e <env> [--env-var NAME]');
250
+ console.error('usage: seq-studio secrets attach <NAME> --fn <slug> -e <env> [--org <slug>] [--env-var NAME]');
198
251
  return 1;
199
252
  }
200
253
  const ctx = await buildContext(args);
201
- const secret = await resolveSecretByName({ ctx, name });
254
+ const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
202
255
  const fn = await resolveFunctionBySlug({ ctx, slug: fnSlug });
203
256
  const envVarName = (typeof args.flags['env-var'] === 'string' ? args.flags['env-var'] : undefined) ?? name;
204
257
  if (secret.hasDefaultValue) {
@@ -229,11 +282,11 @@ export async function secretsDetachCommand(args) {
229
282
  const name = args.positional[0];
230
283
  const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : args.positional[1];
231
284
  if (!name || !fnSlug) {
232
- console.error('usage: seq-studio secrets detach <NAME> --fn <slug> -e <env>');
285
+ console.error('usage: seq-studio secrets detach <NAME> --fn <slug> -e <env> [--org <slug>]');
233
286
  return 1;
234
287
  }
235
288
  const ctx = await buildContext(args);
236
- const secret = await resolveSecretByName({ ctx, name });
289
+ const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
237
290
  const fn = await resolveFunctionBySlug({ ctx, slug: fnSlug });
238
291
  await deleteJson({
239
292
  ...clientOptions(ctx),
@@ -464,11 +517,11 @@ async function attachedFunctionSlugs(ctx, secretId) {
464
517
  export async function secretsVersionsCommand(args) {
465
518
  const name = args.positional[0];
466
519
  if (!name) {
467
- console.error('usage: seq-studio secrets versions <NAME> -e <env>');
520
+ console.error('usage: seq-studio secrets versions <NAME> -e <env> [--org <slug>]');
468
521
  return 1;
469
522
  }
470
523
  const ctx = await buildContext(args);
471
- const secret = await resolveSecretByName({ ctx, name });
524
+ const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
472
525
  const versions = await listSecretVersions(ctx, secret.id);
473
526
  if (versions.length === 0) {
474
527
  console.log(`${LOG} no versions recorded for "${name}" on ${ctx.env.name}`);
@@ -492,11 +545,11 @@ export async function secretsVersionsCommand(args) {
492
545
  export async function secretsSetDefaultCommand(args) {
493
546
  const name = args.positional[0];
494
547
  if (!name) {
495
- console.error('usage: seq-studio secrets set-default <NAME> [version] -e <env> [--yes]');
548
+ console.error('usage: seq-studio secrets set-default <NAME> [version] -e <env> [--org <slug>] [--yes]');
496
549
  return 1;
497
550
  }
498
551
  const ctx = await buildContext(args);
499
- const secret = await resolveSecretByName({ ctx, name });
552
+ const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
500
553
  const versions = await listSecretVersions(ctx, secret.id);
501
554
  if (versions.length === 0) {
502
555
  console.error(`${LOG} "${name}" has no recorded versions`);
@@ -558,11 +611,11 @@ export async function secretsPinCommand(args) {
558
611
  const name = args.positional[0];
559
612
  const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : undefined;
560
613
  if (!name || !fnSlug) {
561
- console.error('usage: seq-studio secrets pin <NAME> --fn <slug> [version] [--unpin] -e <env> [--yes] [--no-redeploy]');
614
+ console.error('usage: seq-studio secrets pin <NAME> --fn <slug> [version] [--unpin] -e <env> [--org <slug>] [--yes] [--no-redeploy]');
562
615
  return 1;
563
616
  }
564
617
  const ctx = await buildContext(args);
565
- const secret = await resolveSecretByName({ ctx, name });
618
+ const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
566
619
  const fn = await resolveFunctionBySlug({ ctx, slug: fnSlug });
567
620
  if (flagBool(args.flags, 'unpin')) {
568
621
  const preview = [
@@ -643,15 +696,21 @@ export async function secretsPinCommand(args) {
643
696
  // dispatcher
644
697
  // ---------------------------------------------------------------------------
645
698
  export const SECRETS_USAGE = `usage:
646
- seq-studio secrets create <NAME> -e <env> [--description text] register an org-owned secret
647
- seq-studio secrets set <NAME> -e <env> set the shared default value (write-only)
699
+ seq-studio secrets create <NAME> -e <env> [--org <slug>] [--description text]
700
+ register an org-owned secret
701
+ seq-studio secrets set <NAME> -e <env> [--org <slug>] [--from-file <path>]
702
+ set the shared default value (write-only)
648
703
  seq-studio secrets list -e <env> secrets you can see (never values)
649
- seq-studio secrets attach <NAME> --fn <slug> -e <env> mount default on a function env var
650
- seq-studio secrets detach <NAME> --fn <slug> -e <env> remove attachment
704
+ seq-studio secrets attach <NAME> --fn <slug> -e <env> [--org <slug>]
705
+ mount default on a function env var
706
+ seq-studio secrets detach <NAME> --fn <slug> -e <env> [--org <slug>]
707
+ remove attachment
651
708
  seq-studio secrets apply --from-env-file .env -e <env> push defaults + attach (keys default from manifest)
652
- seq-studio secrets versions <NAME> -e <env> value version history (never values)
653
- seq-studio secrets set-default <NAME> [version] -e <env> point the default at a prior version (editor)
654
- seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> pin one function to a version (sticky; --unpin clears)
709
+ seq-studio secrets versions <NAME> -e <env> [--org <slug>] value version history (never values)
710
+ seq-studio secrets set-default <NAME> [version] -e <env> [--org <slug>]
711
+ point the default at a prior version (editor)
712
+ seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> [--org <slug>]
713
+ pin one function to a version (sticky; --unpin clears)
655
714
 
656
715
  Note: for the common deploy loop, secrets declared in managed-function.yml are
657
716
  reconciled automatically by \`seq-studio functions deploy\` using a local .env.
@@ -659,6 +718,8 @@ export const SECRETS_USAGE = `usage:
659
718
  value changes without a redeploy.
660
719
 
661
720
  Flags: -e/--env <env> (required; see: seq-studio envs list)
721
+ --org <slug> (target/select managed-scope org; required when a name exists in multiple orgs)
722
+ set: --from-file <path> (non-interactive; file is not echoed)
662
723
  --fn <slug> · --env-var <NAME> · --yes
663
724
  --functions <f1,f2> · --keys <K1,K2> · --all (override key selection)
664
725
  set-default: [version|version-row-id] (defaults to the most recent non-default) · --yes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sequenceholdings/studio-cli",
3
- "version": "0.1.21",
4
- "description": "Unified Sequence Studio CLI — `seq-studio agents` (typed agent definitions), `seq-studio process` (Lattice), `seq-studio artifact` (Artifact Studio), `seq-studio functions` / `secrets`, `seq-studio repos` (platform git-service), and `seq-studio auth pat` (git-service PATs). Includes Auth0 browser login shared with seqapi.",
3
+ "version": "0.1.24",
4
+ "description": "Unified Sequence Studio CLI — `seq-studio init` / `add` / `deploy` (app monorepos), `seq-studio agents`, `seq-studio process` (Lattice), `seq-studio artifact`, `seq-studio functions` / `secrets`, `seq-studio repos`, and `seq-studio auth pat`. Includes Auth0 browser login shared with seqapi.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
@@ -40,12 +40,12 @@
40
40
  "smol-toml": "^1.4.2",
41
41
  "tsx": "^4.20.3",
42
42
  "zod": "^4.1.13",
43
- "@sequenceholdings/agent-spec": "0.1.0",
44
- "@sequenceholdings/artifact-studio": "0.2.0",
45
- "@sequenceholdings/lattice": "0.1.2"
43
+ "@sequenceholdings/artifact-studio": "0.2.1",
44
+ "@sequenceholdings/lattice": "0.1.2",
45
+ "@sequenceholdings/agent-spec": "0.1.1"
46
46
  },
47
47
  "peerDependencies": {
48
- "@sequenceholdings/orm": "0.1.2",
48
+ "@sequenceholdings/orm": "0.1.3",
49
49
  "@sequenceholdings/pipeline-spec": "0.1.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
@@ -61,8 +61,8 @@
61
61
  "@types/node": "^22.0.0",
62
62
  "typescript": "^5.6.0",
63
63
  "vitest": "^4.1.5",
64
- "@sequenceholdings/orm": "0.1.2",
65
- "@sequenceholdings/pipeline-spec": "0.1.0"
64
+ "@sequenceholdings/pipeline-spec": "0.1.0",
65
+ "@sequenceholdings/orm": "0.1.3"
66
66
  },
67
67
  "engines": {
68
68
  "node": ">=20"
@@ -1,5 +0,0 @@
1
- /** Client-side pin rules mirrored from the server deploy adapter (SEQ-2450). */
2
- export declare const FULL_SHA_PATTERN: RegExp;
3
- export declare const PINNED_ENVIRONMENTS: Set<string>;
4
- export declare function isFullSha(ref: string): boolean;
5
- export declare function requiresPinnedSha(environment: string): boolean;
@@ -1,9 +0,0 @@
1
- /** Client-side pin rules mirrored from the server deploy adapter (SEQ-2450). */
2
- export const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
3
- export const PINNED_ENVIRONMENTS = new Set(['production', 'banksouth']);
4
- export function isFullSha(ref) {
5
- return FULL_SHA_PATTERN.test(ref);
6
- }
7
- export function requiresPinnedSha(environment) {
8
- return PINNED_ENVIRONMENTS.has(environment);
9
- }