@sequenceholdings/studio-cli 0.1.22 → 0.1.25

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.
@@ -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.22",
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.25",
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",
@@ -36,16 +36,16 @@
36
36
  "README.md"
37
37
  ],
38
38
  "dependencies": {
39
- "js-yaml": "^4.1.1",
39
+ "js-yaml": "^4.3.1",
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",
43
+ "@sequenceholdings/agent-spec": "0.1.1",
44
+ "@sequenceholdings/artifact-studio": "0.2.2",
45
45
  "@sequenceholdings/lattice": "0.1.2"
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,7 +61,7 @@
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",
64
+ "@sequenceholdings/orm": "0.1.3",
65
65
  "@sequenceholdings/pipeline-spec": "0.1.0"
66
66
  },
67
67
  "engines": {
@@ -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
- }