@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.
- package/README.md +184 -16
- package/dist/agents/commands.d.ts +1 -1
- package/dist/agents/commands.js +24 -4
- package/dist/agents/source.d.ts +3 -1
- package/dist/agents/source.js +27 -4
- 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/atlas-client.js +29 -0
- package/dist/auth.js +2 -0
- package/dist/functions/commands.js +1 -0
- package/dist/functions/manifest.d.ts +24 -0
- package/dist/functions/manifest.js +84 -9
- 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 +105 -10
- package/dist/pipeline/lifecycle.d.ts +3 -12
- package/dist/pipeline/lifecycle.js +245 -33
- package/dist/pipeline/templates.js +3 -1
- package/dist/repos/commands.d.ts +53 -1
- package/dist/repos/commands.js +258 -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
package/dist/repos/commands.js
CHANGED
|
@@ -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 {
|
|
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]
|
|
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>;
|
package/dist/secrets/commands.js
CHANGED
|
@@ -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
|
|
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}
|
|
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
|
|
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]
|
|
647
|
-
|
|
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>
|
|
650
|
-
|
|
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>
|
|
653
|
-
seq-studio secrets set-default <NAME> [version] -e <env>
|
|
654
|
-
|
|
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.
|
|
4
|
-
"description": "Unified Sequence Studio CLI — `seq-studio
|
|
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/
|
|
44
|
-
"@sequenceholdings/
|
|
45
|
-
"@sequenceholdings/
|
|
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.
|
|
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/
|
|
65
|
-
"@sequenceholdings/
|
|
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;
|
package/dist/pipeline/pinning.js
DELETED
|
@@ -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
|
-
}
|