@sequenceholdings/studio-cli 0.1.21 → 0.1.22
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 +71 -6
- 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/atlas-client.js +29 -0
- package/dist/auth.js +2 -0
- package/dist/functions/commands.js +1 -0
- package/dist/functions/manifest.d.ts +2 -0
- package/dist/functions/manifest.js +39 -9
- package/dist/pipeline/commands.js +21 -0
- package/dist/pipeline/lifecycle.d.ts +2 -0
- package/dist/pipeline/lifecycle.js +107 -0
- package/dist/repos/commands.d.ts +53 -1
- package/dist/repos/commands.js +258 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,13 +145,17 @@ bundle; it never implicitly deletes agents.
|
|
|
145
145
|
| Command | What it does |
|
|
146
146
|
|---------|--------------|
|
|
147
147
|
| `seq-studio agents init <dir>` | Scaffold a standalone typed agent repository |
|
|
148
|
-
| `seq-studio agents validate [--dir <dir>] [--target <APP_ENV>]` | Compile and validate locally, without API access |
|
|
149
|
-
| `seq-studio agents plan [--dir <dir>]` | Offline compile/hash plan |
|
|
150
|
-
| `seq-studio agents plan [--dir <dir>] -e <env> [--target <APP_ENV>]` | Diff creates, updates, and unchanged definitions against an environment |
|
|
151
|
-
| `seq-studio agents apply [--dir <dir>] -e <env> [--target <APP_ENV>] [--yes]` | Apply creates and updates after showing the plan |
|
|
148
|
+
| `seq-studio agents validate [--dir <dir>] [--target <APP_ENV>] [--only <id1,id2>]` | Compile and validate locally, without API access |
|
|
149
|
+
| `seq-studio agents plan [--dir <dir>] [--only <id1,id2>]` | Offline compile/hash plan |
|
|
150
|
+
| `seq-studio agents plan [--dir <dir>] -e <env> [--target <APP_ENV>] [--only <id1,id2>]` | Diff creates, updates, and unchanged definitions against an environment |
|
|
151
|
+
| `seq-studio agents apply [--dir <dir>] -e <env> [--target <APP_ENV>] [--only <id1,id2>] [--yes]` | Apply creates and updates after showing the plan |
|
|
152
152
|
| `seq-studio agents list -e <env>` | List visible runtime agents |
|
|
153
153
|
| `seq-studio agents show <id> -e <env>` | Show one runtime agent |
|
|
154
154
|
|
|
155
|
+
For `validate`, `plan`, and `apply`, `--only` accepts a comma-separated list of
|
|
156
|
+
agent IDs after deployment-environment selection; every requested ID must be
|
|
157
|
+
selected.
|
|
158
|
+
|
|
155
159
|
An optional `deploy-manifest.json` targets definitions by deployment identity:
|
|
156
160
|
|
|
157
161
|
```json
|
|
@@ -187,8 +191,10 @@ Use `--repo agents/<name> [--ref <ref>]` or `--git-url <url>` instead of
|
|
|
187
191
|
## ORM commands
|
|
188
192
|
|
|
189
193
|
`seq-studio orm` authors and deploys governed ORM v2 namespaces: TypeScript
|
|
190
|
-
table definitions and policies plus named
|
|
191
|
-
persisted operations.
|
|
194
|
+
table definitions, Drizzle-authored read-only views, and policies plus named
|
|
195
|
+
GraphQL documents compiled into persisted operations. Import Drizzle query
|
|
196
|
+
helpers from `@sequenceholdings/orm/drizzle`; managed view builders are
|
|
197
|
+
compiled to canonical SQL before registration.
|
|
192
198
|
|
|
193
199
|
| Command | What it does |
|
|
194
200
|
|---------|--------------|
|
|
@@ -335,6 +341,40 @@ same repo share Git review and commit provenance, but keep separate manifests,
|
|
|
335
341
|
versions, runtime resources, secrets, and permissions. `--path` accepts only a
|
|
336
342
|
canonical relative directory inside a remote repo; use `--dir` for local source.
|
|
337
343
|
|
|
344
|
+
### Scaling
|
|
345
|
+
|
|
346
|
+
Managed functions scale to zero by default. Set a bounded warm pool in
|
|
347
|
+
`managed-function.yml` when first-request latency matters:
|
|
348
|
+
|
|
349
|
+
```yaml
|
|
350
|
+
limits:
|
|
351
|
+
min_instances: 1
|
|
352
|
+
max_instances: 3
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`min_instances` defaults to `0`, cannot exceed `max_instances`, and incurs
|
|
356
|
+
Cloud Run idle-instance charges while warm.
|
|
357
|
+
|
|
358
|
+
### ORM data access
|
|
359
|
+
|
|
360
|
+
A function declares its ORM Data API reach in `capabilities.data`, grouped by
|
|
361
|
+
namespace: `tables` it may read, v1 `actions` and ORM v2 persisted `operations`
|
|
362
|
+
it may invoke, and whether raw read `query` is allowed. At invoke time the
|
|
363
|
+
platform mints a short-lived data token scoped to exactly these refs — an
|
|
364
|
+
operation is scoped as `<namespace>/ops/<OperationName>` (the GraphQL operation
|
|
365
|
+
name from the namespace's `graphql/` documents, case-sensitive), and anything
|
|
366
|
+
undeclared is denied by the Data API. A function that reaches any namespace
|
|
367
|
+
must also attach a top-level `service_account`:
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
service_account: lucky-svc
|
|
371
|
+
capabilities:
|
|
372
|
+
data:
|
|
373
|
+
lucky:
|
|
374
|
+
tables: [lucky_draws]
|
|
375
|
+
operations: [RollLuckyNumber]
|
|
376
|
+
```
|
|
377
|
+
|
|
338
378
|
## Artifact commands
|
|
339
379
|
|
|
340
380
|
`seq-studio artifact <sub>` is the entry point for Artifact Studio. It runs
|
|
@@ -467,11 +507,34 @@ JSON API — the same repos `--repo <ns>/<name>` sources build from.
|
|
|
467
507
|
| `seq-studio repos clone <ns>/<name> \| --url <clone-url> \| --id <uuid> -e <env> [--ref <r>] [--out <dir>] [--force]` | smart-HTTP `git clone` when `ATLAS_GIT_PAT` is set (`--url`/`--id` need no seqapi); otherwise JSON materialize + PAT hint |
|
|
468
508
|
| `seq-studio repos pull <ns>/<name> -e <env> [--ref <r>] [--out <dir>] [--force]` | always materialize via JSON API (no `.git` dir); refuses a non-empty destination unless `--force` |
|
|
469
509
|
| `seq-studio repos delete <ns>/<name> -e <env> [--yes]` | delete a repo — interactive confirm unless `--yes` |
|
|
510
|
+
| `seq-studio repos ci show <ns>/<name> -e <env> [--ref <r>]` | preview CI checks discovered from the ref |
|
|
511
|
+
| `seq-studio repos ci require <ns>/<name> --check <name> -e <env>` | reserved for requiring a named CI check; currently refuses to write until the sandboxed runner is live |
|
|
512
|
+
| `seq-studio repos ci import <ns>/<name> -e <env> [--ref <r>]` | reserved for requiring every discovered check; currently refuses to write until the sandboxed runner is live |
|
|
470
513
|
|
|
471
514
|
`show` prints the smart-HTTP clone URL (`…/repos/<id>/git`). Basic auth:
|
|
472
515
|
any username, PAT as password. Prefer `repos clone` over hand-rolling the
|
|
473
516
|
tree API.
|
|
474
517
|
|
|
518
|
+
PR CI discovers `ci/check` from the first available `lint`, `typecheck`, or
|
|
519
|
+
`check` script and `ci/test` from `test`. A `.seq/ci.json` takes precedence;
|
|
520
|
+
its `checks` array can declare script/argv checks or be empty to opt out.
|
|
521
|
+
|
|
522
|
+
For example:
|
|
523
|
+
|
|
524
|
+
{ "checks": [
|
|
525
|
+
{ "phase": "check", "script": "lint" },
|
|
526
|
+
{ "phase": "test", "command": ["pnpm", "test"] }
|
|
527
|
+
] }
|
|
528
|
+
|
|
529
|
+
Each check needs `phase` (`check` or `test`) and exactly one of `script` or
|
|
530
|
+
`command`; `name` is optional and otherwise defaults to `ci/<phase>`
|
|
531
|
+
(`-2`, etc. for additional checks in that phase).
|
|
532
|
+
Discovery alone never blocks a merge. Until the sandboxed executor is live,
|
|
533
|
+
`repos ci show` is preview-only, Settings controls are disabled, and
|
|
534
|
+
`require`/`import` refuse to write (discovery currently posts `neutral`
|
|
535
|
+
check-runs). Once the executor is live, repo owners can opt in by requiring
|
|
536
|
+
check names in Settings or with `repos ci require`/`repos ci import`.
|
|
537
|
+
|
|
475
538
|
## Pipeline commands
|
|
476
539
|
|
|
477
540
|
`seq-studio pipeline <sub>` authors and validates Data Pipelines **stage
|
|
@@ -486,6 +549,8 @@ install it alongside the CLI to use this family.
|
|
|
486
549
|
| `seq-studio pipeline validate [dir] [--assets <file\|url>] [--json]` | Run the full offline spec gate: envelope + body validation, `schema_ref` resolution, and repo-level graph validation (reference resolution, single-writer, cycles, column subsets, serving projection checks). Exit 0/1 |
|
|
487
550
|
| `seq-studio pipeline plan --repo pipelines/<slug> --ref <sha\|branch> -e <env> [--json]` | Plan a Pipeline deploy (materialize → SDK/`validateSpecGraph` → compile → live-diff → provision findings). Does **not** run Databricks `bundle validate` (that is a Trigger deploy-path hard gate). Exit 1 on destructive findings (CI-safe). `--json` emits the stable plan envelope |
|
|
488
551
|
| `seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env> [--approved-by <sub>] [--no-wait]` | Plan then enqueue deploy; Trigger runs `bundle validate` then `bundle deploy` against reviewed bytes. Polls to terminal unless `--no-wait`. Production/banksouth require a pinned 40-hex SHA (client + server) |
|
|
552
|
+
| `seq-studio pipeline adopt --stage <slug> --ref <sha\|branch> -e <env> --native-id <id> --approved-by <you> [--resource-key <key>] [--kind job\|dlt_pipeline] [--old-source-removal-pr <url>] [--repo pipelines/<slug>]` | Bind a live Databricks job/pipeline into the stage without recreation (`bundle deployment bind` on Trigger). Always requires `--approved-by` naming the caller. When the key is still in the monorepo DAB, pass `--old-source-removal-pr` and follow the returned cutover checklist: unbind the old bundle state without deleting the remote, then remove its DAB declaration and add the target-specific adopted-resource entry in the same PR before redeploying. |
|
|
553
|
+
| `seq-studio pipeline unbind --stage <slug> --ref <sha\|branch> -e <env> --approved-by <you> [--resource-key <key>] [--repo pipelines/<slug>]` | Release an adopted binding on Trigger; the remote object stays live (never deleted) |
|
|
489
554
|
| `seq-studio pipeline run-now --stage <slug> -e <env> [--repo pipelines/<slug>] [--json]` | Run the stage's active job or DLT pipeline immediately and print its Databricks run URL |
|
|
490
555
|
| `seq-studio pipeline promote --stage <slug> --version <v> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Promote a validated version to another environment. Prod/banksouth require `--approved-by` naming the authenticated caller (approvals are self-recorded); `--repo` disambiguates a slug that exists in multiple Pipelines |
|
|
491
556
|
| `seq-studio pipeline rollback --stage <slug> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Redeploy the previously retired deployment's version. Prod/banksouth require `--approved-by` — approvals are explicit even for rollbacks |
|
|
@@ -6,5 +6,5 @@ export declare function agentsPlanCommand(args: ParsedArgs): Promise<number>;
|
|
|
6
6
|
export declare function agentsApplyCommand(args: ParsedArgs): Promise<number>;
|
|
7
7
|
export declare function agentsListCommand(args: ParsedArgs): Promise<number>;
|
|
8
8
|
export declare function agentsShowCommand(args: ParsedArgs): Promise<number>;
|
|
9
|
-
export declare const AGENTS_USAGE = "usage:\n seq-studio agents init <dir> scaffold a typed agent\n seq-studio agents validate [--dir d] [--target app] offline compile + validation\n seq-studio agents plan [--dir d] [-e <env>] offline bundle plan or live diff\n seq-studio agents apply [--dir d] -e <env> [--yes] apply creates/updates; never deletes\n seq-studio agents list -e <env> list visible agents\n seq-studio agents show <id> -e <env> show one agent\n\n Source: local --dir (default .), --repo agents/<name>, or --git-url <url>.\n Use --ref for remote sources. --target selects the deployment APP_ENV when it\n differs from the CLI environment alias (notably OpCo registrations).\n";
|
|
9
|
+
export declare const AGENTS_USAGE = "usage:\n seq-studio agents init <dir> scaffold a typed agent\n seq-studio agents validate [--dir d] [--target app] [--only ids] offline compile + validation\n seq-studio agents plan [--dir d] [-e <env>] [--only ids] offline bundle plan or live diff\n seq-studio agents apply [--dir d] -e <env> [--only ids] [--yes] apply creates/updates; never deletes\n seq-studio agents list -e <env> list visible agents\n seq-studio agents show <id> -e <env> show one agent\n\n Source: local --dir (default .), --repo agents/<name>, or --git-url <url>.\n Use --ref for remote sources. --target selects the deployment APP_ENV when it\n differs from the CLI environment alias (notably OpCo registrations). --only\n accepts a comma-separated list of agent IDs after environment selection.\n";
|
|
10
10
|
export declare function runAgentsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
package/dist/agents/commands.js
CHANGED
|
@@ -7,6 +7,22 @@ import { compileAgentSource as compileSource, materializeAgentSource as material
|
|
|
7
7
|
import { agentsInitCommand } from './scaffold.js';
|
|
8
8
|
export { agentsInitCommand };
|
|
9
9
|
const LOG = '[seq-studio]';
|
|
10
|
+
function requestedAgentIds(args) {
|
|
11
|
+
const only = args.flags.only;
|
|
12
|
+
if (only === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (typeof only !== 'string') {
|
|
15
|
+
throw new Error('--only must be a comma-separated list of agent IDs');
|
|
16
|
+
}
|
|
17
|
+
const ids = only
|
|
18
|
+
.split(',')
|
|
19
|
+
.map((id) => id.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
if (ids.length === 0) {
|
|
22
|
+
throw new Error('--only must include at least one agent ID');
|
|
23
|
+
}
|
|
24
|
+
return [...new Set(ids)];
|
|
25
|
+
}
|
|
10
26
|
export async function agentsValidateCommand(args) {
|
|
11
27
|
const { source } = await materialize({ args, requireEnvironment: false });
|
|
12
28
|
try {
|
|
@@ -15,6 +31,7 @@ export async function agentsValidateCommand(args) {
|
|
|
15
31
|
directory: source.dir,
|
|
16
32
|
targetEnvironment: target,
|
|
17
33
|
deployEnvironments: await deployEnvironmentNames(),
|
|
34
|
+
onlyIds: requestedAgentIds(args),
|
|
18
35
|
});
|
|
19
36
|
if (bundle.definitions.length === 0) {
|
|
20
37
|
console.error(`${LOG} no named agent.ts definitions found`);
|
|
@@ -34,6 +51,7 @@ async function deploymentBundle({ args, source, context, }) {
|
|
|
34
51
|
directory: source.dir,
|
|
35
52
|
targetEnvironment: target,
|
|
36
53
|
deployEnvironments: await deployEnvironmentNames(),
|
|
54
|
+
onlyIds: requestedAgentIds(args),
|
|
37
55
|
});
|
|
38
56
|
}
|
|
39
57
|
/**
|
|
@@ -75,6 +93,7 @@ export async function agentsPlanCommand(args) {
|
|
|
75
93
|
directory: source.dir,
|
|
76
94
|
targetEnvironment: target,
|
|
77
95
|
deployEnvironments: await deployEnvironmentNames(),
|
|
96
|
+
onlyIds: requestedAgentIds(args),
|
|
78
97
|
});
|
|
79
98
|
console.log(`${LOG} offline plan: ${bundle.definitions.length} valid definition${bundle.definitions.length === 1 ? '' : 's'}, bundle ${bundle.hash}`);
|
|
80
99
|
console.log(`${LOG} pass -e <env> for create/update/unchanged live diff`);
|
|
@@ -174,15 +193,16 @@ export async function agentsShowCommand(args) {
|
|
|
174
193
|
}
|
|
175
194
|
export const AGENTS_USAGE = `usage:
|
|
176
195
|
seq-studio agents init <dir> scaffold a typed agent
|
|
177
|
-
seq-studio agents validate [--dir d] [--target app] offline compile + validation
|
|
178
|
-
seq-studio agents plan [--dir d] [-e <env>] offline bundle plan or live diff
|
|
179
|
-
seq-studio agents apply [--dir d] -e <env> [--yes] apply creates/updates; never deletes
|
|
196
|
+
seq-studio agents validate [--dir d] [--target app] [--only ids] offline compile + validation
|
|
197
|
+
seq-studio agents plan [--dir d] [-e <env>] [--only ids] offline bundle plan or live diff
|
|
198
|
+
seq-studio agents apply [--dir d] -e <env> [--only ids] [--yes] apply creates/updates; never deletes
|
|
180
199
|
seq-studio agents list -e <env> list visible agents
|
|
181
200
|
seq-studio agents show <id> -e <env> show one agent
|
|
182
201
|
|
|
183
202
|
Source: local --dir (default .), --repo agents/<name>, or --git-url <url>.
|
|
184
203
|
Use --ref for remote sources. --target selects the deployment APP_ENV when it
|
|
185
|
-
differs from the CLI environment alias (notably OpCo registrations).
|
|
204
|
+
differs from the CLI environment alias (notably OpCo registrations). --only
|
|
205
|
+
accepts a comma-separated list of agent IDs after environment selection.
|
|
186
206
|
`;
|
|
187
207
|
export async function runAgentsCommand(sub, args) {
|
|
188
208
|
try {
|
package/dist/agents/source.d.ts
CHANGED
|
@@ -10,9 +10,11 @@ export declare function materializeAgentSource({ args, requireEnvironment, }: {
|
|
|
10
10
|
source: ResolvedSource;
|
|
11
11
|
context: CommandContext | null;
|
|
12
12
|
}>;
|
|
13
|
-
export declare function compileAgentSource({ directory, targetEnvironment, deployEnvironments, }: {
|
|
13
|
+
export declare function compileAgentSource({ directory, targetEnvironment, deployEnvironments, onlyIds, }: {
|
|
14
14
|
directory: string;
|
|
15
15
|
targetEnvironment?: string;
|
|
16
16
|
/** Registered deployment environments, so a real env absent from the manifest is not read as a typo. */
|
|
17
17
|
deployEnvironments?: readonly string[];
|
|
18
|
+
/** Restrict a plan or apply to explicit agent IDs after environment selection. */
|
|
19
|
+
onlyIds?: readonly string[];
|
|
18
20
|
}): Promise<CompiledAgentBundle>;
|
package/dist/agents/source.js
CHANGED
|
@@ -87,10 +87,25 @@ async function compileManifestEntries({ directory, entries, allowDuplicateIds =
|
|
|
87
87
|
sources: compiled.sources,
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
-
export async function compileAgentSource({ directory, targetEnvironment, deployEnvironments = [], }) {
|
|
90
|
+
export async function compileAgentSource({ directory, targetEnvironment, deployEnvironments = [], onlyIds, }) {
|
|
91
91
|
const manifest = await readManifest(directory);
|
|
92
|
-
if (!manifest)
|
|
93
|
-
|
|
92
|
+
if (!manifest) {
|
|
93
|
+
const compiled = await compileAgentDirectory({ rootDir: directory });
|
|
94
|
+
if (!onlyIds || onlyIds.length === 0)
|
|
95
|
+
return compiled;
|
|
96
|
+
const requested = new Set(onlyIds);
|
|
97
|
+
const definitions = compiled.definitions.filter((definition) => requested.has(definition.id));
|
|
98
|
+
const missing = [...requested].filter((id) => !definitions.some((definition) => definition.id === id));
|
|
99
|
+
if (missing.length > 0) {
|
|
100
|
+
throw new Error(`Requested agent IDs were not selected: ${missing.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...compiled,
|
|
104
|
+
definitions,
|
|
105
|
+
hash: hashAgentBundle({ definitions }),
|
|
106
|
+
sources: compiled.sources.filter((source) => requested.has(source.definition.id)),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
94
109
|
if (targetEnvironment !== undefined) {
|
|
95
110
|
assertKnownTargetEnvironment({
|
|
96
111
|
manifest,
|
|
@@ -109,10 +124,18 @@ export async function compileAgentSource({ directory, targetEnvironment, deployE
|
|
|
109
124
|
}
|
|
110
125
|
// With a target: last-wins by id (tenant overrides). Without: every distinct
|
|
111
126
|
// path, so validate still compiles override sources that lose a collapse.
|
|
112
|
-
const
|
|
127
|
+
const selectedEntries = selectAgentEntries({
|
|
113
128
|
manifest,
|
|
114
129
|
environment: targetEnvironment,
|
|
115
130
|
});
|
|
131
|
+
const requested = new Set(onlyIds);
|
|
132
|
+
const entries = requested.size === 0
|
|
133
|
+
? selectedEntries
|
|
134
|
+
: selectedEntries.filter((entry) => requested.has(entry.id));
|
|
135
|
+
const missing = [...requested].filter((id) => !entries.some((entry) => entry.id === id));
|
|
136
|
+
if (missing.length > 0) {
|
|
137
|
+
throw new Error(`Requested agent IDs were not selected: ${missing.join(', ')}`);
|
|
138
|
+
}
|
|
116
139
|
return compileManifestEntries({
|
|
117
140
|
directory,
|
|
118
141
|
entries,
|
package/dist/atlas-client.js
CHANGED
|
@@ -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
|
}
|
|
@@ -52,6 +52,7 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
|
|
|
52
52
|
limits: z.ZodDefault<z.ZodObject<{
|
|
53
53
|
memory_mb: z.ZodDefault<z.ZodNumber>;
|
|
54
54
|
timeout_seconds: z.ZodDefault<z.ZodNumber>;
|
|
55
|
+
min_instances: z.ZodDefault<z.ZodNumber>;
|
|
55
56
|
max_instances: z.ZodDefault<z.ZodNumber>;
|
|
56
57
|
invoke_rate_per_minute: z.ZodDefault<z.ZodNumber>;
|
|
57
58
|
}, z.core.$strip>>;
|
|
@@ -83,6 +84,7 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
|
|
|
83
84
|
data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
84
85
|
tables: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
85
86
|
actions: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
87
|
+
operations: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
86
88
|
query: z.ZodDefault<z.ZodBoolean>;
|
|
87
89
|
}, z.core.$strip>>>;
|
|
88
90
|
}, z.core.$strip>>;
|
|
@@ -493,6 +493,12 @@ function validateCapabilityGates({ gates, uses, roles, }) {
|
|
|
493
493
|
* Mirrors atlas/src/server/services/managed-functions/manifest.ts.
|
|
494
494
|
*/
|
|
495
495
|
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})$/;
|
|
496
|
+
/**
|
|
497
|
+
* GraphQL Name grammar — persisted-operation names exactly as they appear in
|
|
498
|
+
* a namespace's operations manifest. Mirrors
|
|
499
|
+
* atlas/src/server/services/managed-functions/manifest.ts.
|
|
500
|
+
*/
|
|
501
|
+
const GRAPHQL_OPERATION_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
496
502
|
export const managedFunctionManifestSchema = z.object({
|
|
497
503
|
schema_version: z.literal(1).default(1),
|
|
498
504
|
function: z.object({
|
|
@@ -510,12 +516,23 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
510
516
|
.object({
|
|
511
517
|
memory_mb: z.number().int().min(128).max(2048).default(256),
|
|
512
518
|
timeout_seconds: z.number().int().min(1).max(540).default(60),
|
|
519
|
+
min_instances: z.number().int().min(0).max(10).default(0),
|
|
513
520
|
max_instances: z.number().int().min(1).max(10).default(3),
|
|
514
521
|
invoke_rate_per_minute: z.number().int().min(1).max(600).default(60),
|
|
522
|
+
})
|
|
523
|
+
.superRefine((limits, ctx) => {
|
|
524
|
+
if (limits.min_instances > limits.max_instances) {
|
|
525
|
+
ctx.addIssue({
|
|
526
|
+
code: 'custom',
|
|
527
|
+
message: 'min_instances cannot exceed max_instances',
|
|
528
|
+
path: ['min_instances'],
|
|
529
|
+
});
|
|
530
|
+
}
|
|
515
531
|
})
|
|
516
532
|
.default({
|
|
517
533
|
memory_mb: 256,
|
|
518
534
|
timeout_seconds: 60,
|
|
535
|
+
min_instances: 0,
|
|
519
536
|
max_instances: 3,
|
|
520
537
|
invoke_rate_per_minute: 60,
|
|
521
538
|
}),
|
|
@@ -567,12 +584,13 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
567
584
|
gates: capabilityGatesSchema,
|
|
568
585
|
/**
|
|
569
586
|
* ORM Data API consumer reach, grouped by namespace: the `tables` this
|
|
570
|
-
* function may read, the `actions`
|
|
571
|
-
* (arbitrary read SQL over the namespace)
|
|
572
|
-
* invoking user. The invoke proxy mints
|
|
573
|
-
* exactly these refs; the Data API
|
|
574
|
-
*
|
|
575
|
-
*
|
|
587
|
+
* function may read, the v1 `actions` and v2 persisted `operations` it may
|
|
588
|
+
* invoke, and whether raw `query` (arbitrary read SQL over the namespace)
|
|
589
|
+
* is allowed — all ON BEHALF OF the invoking user. The invoke proxy mints
|
|
590
|
+
* a short-lived token scoped to exactly these refs; the Data API
|
|
591
|
+
* re-resolves the user's claims per call, so declaring a table never
|
|
592
|
+
* widens what the user could see. Writes go through declared actions and
|
|
593
|
+
* operations only — raw table writes don't exist.
|
|
576
594
|
*
|
|
577
595
|
* Mirrors atlas/src/server/services/managed-functions/manifest.ts — must
|
|
578
596
|
* stay in sync so `seq-studio` doesn't strip the block before deploy.
|
|
@@ -581,6 +599,18 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
581
599
|
.record(z.string().regex(/^[a-z][a-z0-9_]{0,40}$/, 'capabilities.data keys are ORM namespace names'), z.object({
|
|
582
600
|
tables: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'table names')).max(64).default([]),
|
|
583
601
|
actions: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'action names')).max(64).default([]),
|
|
602
|
+
/**
|
|
603
|
+
* ORM v2 persisted operations (GraphQL mutations/actions by name)
|
|
604
|
+
* this function may execute — each mints a `<ns>/ops/<Name>` write
|
|
605
|
+
* ref on the invoke token.
|
|
606
|
+
*/
|
|
607
|
+
operations: z
|
|
608
|
+
.array(z
|
|
609
|
+
.string()
|
|
610
|
+
.max(128)
|
|
611
|
+
.regex(GRAPHQL_OPERATION_NAME_RE, 'operation names must match the GraphQL Name grammar'))
|
|
612
|
+
.max(64)
|
|
613
|
+
.default([]),
|
|
584
614
|
query: z.boolean().default(false),
|
|
585
615
|
}))
|
|
586
616
|
.default({}),
|
|
@@ -600,9 +630,9 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
600
630
|
ctx.addIssue({ code: 'custom', message, path: ['capabilities', 'gates'] });
|
|
601
631
|
}
|
|
602
632
|
// A namespace is "reached" when its block declares at least one table,
|
|
603
|
-
// action, or raw query — the same rule the server's floor
|
|
604
|
-
// uses (readManifestDataNamespaces).
|
|
605
|
-
const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.query);
|
|
633
|
+
// action, operation, or raw query — the same rule the server's floor
|
|
634
|
+
// reconciliation uses (readManifestDataNamespaces).
|
|
635
|
+
const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.operations.length > 0 || block.query);
|
|
606
636
|
if (reachesData && manifest.service_account === undefined) {
|
|
607
637
|
ctx.addIssue({
|
|
608
638
|
code: 'custom',
|
|
@@ -49,6 +49,19 @@ const PIPELINE_USAGE = `usage:
|
|
|
49
49
|
banksouth require --approved-by — approvals are explicit even for
|
|
50
50
|
rollbacks)
|
|
51
51
|
|
|
52
|
+
seq-studio pipeline adopt --stage <slug> --ref <sha|branch> -e <env>
|
|
53
|
+
--native-id <id> [--resource-key <key>] [--kind job|dlt_pipeline]
|
|
54
|
+
--approved-by <you> [--old-source-removal-pr <url>]
|
|
55
|
+
[--repo pipelines/<slug>] [--json]
|
|
56
|
+
bind a live Databricks job/pipeline to the stage without recreation
|
|
57
|
+
(SEQ-2449). Always requires --approved-by. Monorepo-declared keys also
|
|
58
|
+
require --old-source-removal-pr.
|
|
59
|
+
|
|
60
|
+
seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env>
|
|
61
|
+
--approved-by <you> [--resource-key <key>]
|
|
62
|
+
[--repo pipelines/<slug>] [--json]
|
|
63
|
+
release an adopted binding; the remote object stays live (never deleted)
|
|
64
|
+
|
|
52
65
|
seq-studio pipeline run-now --stage <slug> -e <env> [--repo pipelines/<slug>] [--json]
|
|
53
66
|
fire the stage's active deployment resource (job run-now / DLT
|
|
54
67
|
start_update) and print the Databricks run URL
|
|
@@ -297,6 +310,14 @@ export async function runPipelineCommand(sub, args) {
|
|
|
297
310
|
return pipelineInitCommand(args);
|
|
298
311
|
case 'validate':
|
|
299
312
|
return pipelineValidateCommand(args);
|
|
313
|
+
case 'adopt': {
|
|
314
|
+
const { pipelineAdoptCommand } = await import('./lifecycle.js');
|
|
315
|
+
return pipelineAdoptCommand(args);
|
|
316
|
+
}
|
|
317
|
+
case 'unbind': {
|
|
318
|
+
const { pipelineUnbindCommand } = await import('./lifecycle.js');
|
|
319
|
+
return pipelineUnbindCommand(args);
|
|
320
|
+
}
|
|
300
321
|
case 'plan': {
|
|
301
322
|
const { pipelinePlanCommand } = await import('./lifecycle.js');
|
|
302
323
|
return pipelinePlanCommand(args);
|
|
@@ -56,3 +56,5 @@ export declare function pipelineDeployCommand(args: ParsedArgs): Promise<number>
|
|
|
56
56
|
export declare function pipelinePromoteCommand(args: ParsedArgs): Promise<number>;
|
|
57
57
|
export declare function pipelineRunNowCommand(args: ParsedArgs): Promise<number>;
|
|
58
58
|
export declare function pipelineRollbackCommand(args: ParsedArgs): Promise<number>;
|
|
59
|
+
export declare function pipelineAdoptCommand(args: ParsedArgs): Promise<number>;
|
|
60
|
+
export declare function pipelineUnbindCommand(args: ParsedArgs): Promise<number>;
|
|
@@ -315,6 +315,113 @@ async function pollDeployment({ baseUrl, token, deploymentId, }) {
|
|
|
315
315
|
console.error(`${LOG} timed out waiting for deployment ${deploymentId}`);
|
|
316
316
|
return 1;
|
|
317
317
|
}
|
|
318
|
+
export async function pipelineAdoptCommand(args) {
|
|
319
|
+
const stage = flagString(args.flags, 'stage');
|
|
320
|
+
const ref = flagString(args.flags, 'ref');
|
|
321
|
+
const nativeId = flagString(args.flags, 'native-id');
|
|
322
|
+
const approvedBy = flagString(args.flags, 'approved-by');
|
|
323
|
+
if (!stage || !ref || !nativeId || !approvedBy) {
|
|
324
|
+
console.error('usage: seq-studio pipeline adopt --stage <slug> --ref <sha|branch> -e <env> ' +
|
|
325
|
+
'--native-id <id> --approved-by <you> [--resource-key <key>] [--kind job|dlt_pipeline] ' +
|
|
326
|
+
'[--old-source-removal-pr <url>] [--repo pipelines/<slug>] [--json]');
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
const { env, token } = await envAndToken(args);
|
|
330
|
+
const json = args.flags.json === true || args.flags.json === 'true';
|
|
331
|
+
const repo = flagString(args.flags, 'repo');
|
|
332
|
+
const resourceKey = flagString(args.flags, 'resource-key') ?? stage.replace(/-/g, '_');
|
|
333
|
+
const kind = flagString(args.flags, 'kind') ?? 'job';
|
|
334
|
+
if (kind !== 'job' && kind !== 'dlt_pipeline') {
|
|
335
|
+
console.error(`${LOG} --kind must be job or dlt_pipeline`);
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
const oldSourceRemovalPr = flagString(args.flags, 'old-source-removal-pr');
|
|
339
|
+
const stageId = await resolveStageIdBySlug({
|
|
340
|
+
baseUrl: env.url,
|
|
341
|
+
token,
|
|
342
|
+
slug: stage,
|
|
343
|
+
repo,
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
const response = await postJson({
|
|
347
|
+
baseUrl: env.url,
|
|
348
|
+
token,
|
|
349
|
+
path: `/api/data-pipelines/stages/${stageId}/adopt`,
|
|
350
|
+
body: {
|
|
351
|
+
environment: deployEnvironmentForEnv(env),
|
|
352
|
+
ref,
|
|
353
|
+
bindings: [{ resourceKey, nativeId, kind }],
|
|
354
|
+
approvedBy,
|
|
355
|
+
...(oldSourceRemovalPr ? { oldSourceRemovalPr } : {}),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
if (json) {
|
|
359
|
+
console.log(JSON.stringify(response, null, 2));
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
console.log(`${LOG} enqueued adopt for ${stage} → ${resourceKey}=${nativeId} ` +
|
|
363
|
+
`(trigger=${response.triggerRunId}). Worker binds on Trigger; ` +
|
|
364
|
+
`confirm with plan or stage_resources once the run completes.`);
|
|
365
|
+
}
|
|
366
|
+
return 0;
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
if (error instanceof AtlasApiError) {
|
|
370
|
+
console.error(`${LOG} adopt failed: ${error.message}`);
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
export async function pipelineUnbindCommand(args) {
|
|
377
|
+
const stage = flagString(args.flags, 'stage');
|
|
378
|
+
const ref = flagString(args.flags, 'ref');
|
|
379
|
+
const approvedBy = flagString(args.flags, 'approved-by');
|
|
380
|
+
if (!stage || !ref || !approvedBy) {
|
|
381
|
+
console.error('usage: seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env> ' +
|
|
382
|
+
'--approved-by <you> [--resource-key <key>] [--repo pipelines/<slug>] [--json]');
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
const { env, token } = await envAndToken(args);
|
|
386
|
+
const json = args.flags.json === true || args.flags.json === 'true';
|
|
387
|
+
const repo = flagString(args.flags, 'repo');
|
|
388
|
+
const resourceKey = flagString(args.flags, 'resource-key');
|
|
389
|
+
const stageId = await resolveStageIdBySlug({
|
|
390
|
+
baseUrl: env.url,
|
|
391
|
+
token,
|
|
392
|
+
slug: stage,
|
|
393
|
+
repo,
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
const response = await postJson({
|
|
397
|
+
baseUrl: env.url,
|
|
398
|
+
token,
|
|
399
|
+
path: `/api/data-pipelines/stages/${stageId}/unbind`,
|
|
400
|
+
body: {
|
|
401
|
+
environment: deployEnvironmentForEnv(env),
|
|
402
|
+
ref,
|
|
403
|
+
approvedBy,
|
|
404
|
+
...(resourceKey ? { resourceKeys: [resourceKey] } : {}),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
if (json) {
|
|
408
|
+
console.log(JSON.stringify(response, null, 2));
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
console.log(`${LOG} enqueued unbind for ${stage}` +
|
|
412
|
+
(resourceKey ? ` (${resourceKey})` : '') +
|
|
413
|
+
` (trigger=${response.triggerRunId}). Remote object is never deleted.`);
|
|
414
|
+
}
|
|
415
|
+
return 0;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (error instanceof AtlasApiError) {
|
|
419
|
+
console.error(`${LOG} unbind failed: ${error.message}`);
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
318
425
|
/**
|
|
319
426
|
* Stage identity is `(repo, slug)` — a bare slug can be ambiguous across
|
|
320
427
|
* Pipelines. `--repo pipelines/<domain>` scopes the lookup server-side; an
|
package/dist/repos/commands.d.ts
CHANGED
|
@@ -44,6 +44,58 @@ export declare function reposCloneCommand(args: ParsedArgs, deps?: {
|
|
|
44
44
|
* so askpass never sends ATLAS_GIT_PAT to an attacker-controlled host.
|
|
45
45
|
*/
|
|
46
46
|
export declare function normalizeCloneUrl(raw: string, env: ResolvedEnv): string;
|
|
47
|
+
type DiscoveredCliCheck = {
|
|
48
|
+
name: string;
|
|
49
|
+
script?: string;
|
|
50
|
+
command?: string[];
|
|
51
|
+
source: 'autodiscover' | 'ci.json';
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Pure discovery shared by `repos ci show` / `import`. Mirrors atlas
|
|
55
|
+
* `discoverCiChecks` / `parseCiJson` so CLI preview matches execution.
|
|
56
|
+
*/
|
|
57
|
+
export declare function discoverCliChecks({ ciJson, packageScripts, }: {
|
|
58
|
+
ciJson?: unknown;
|
|
59
|
+
packageScripts?: Record<string, string>;
|
|
60
|
+
}): {
|
|
61
|
+
ok: true;
|
|
62
|
+
checks: DiscoveredCliCheck[];
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Load `.seq/ci.json` / `package.json` for CI preview.
|
|
69
|
+
*
|
|
70
|
+
* Matches the runner: only a true 404 means "file absent". Invalid JSON and
|
|
71
|
+
* non-404 API errors fail closed (no silent package.json fallback over a bad
|
|
72
|
+
* `.seq/ci.json`).
|
|
73
|
+
*/
|
|
74
|
+
export declare function loadRemoteCiSources({ ctx, repoId, ref, }: {
|
|
75
|
+
ctx: CommandContext;
|
|
76
|
+
repoId: string;
|
|
77
|
+
ref: string;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
ok: true;
|
|
80
|
+
ciJson?: unknown;
|
|
81
|
+
packageScripts?: Record<string, string>;
|
|
82
|
+
} | {
|
|
83
|
+
ok: false;
|
|
84
|
+
error: string;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Discover CI checks from a remote repo tip (pure preview — no Settings write).
|
|
88
|
+
* Uses the same autodiscovery rules as the platform runner (PLA-378).
|
|
89
|
+
*/
|
|
90
|
+
export declare function reposCiShowCommand(args: ParsedArgs): Promise<number>;
|
|
91
|
+
/** Add a check-run name to Settings `requiredChecks` (merge-blocking). */
|
|
92
|
+
export declare function reposCiRequireCommand(args: ParsedArgs): Promise<number>;
|
|
93
|
+
/**
|
|
94
|
+
* Set requiredChecks to every check discovered on the tip (opt-in bulk require).
|
|
95
|
+
*/
|
|
96
|
+
export declare function reposCiImportCommand(args: ParsedArgs): Promise<number>;
|
|
97
|
+
export declare function reposCiCommand(args: ParsedArgs): Promise<number>;
|
|
47
98
|
export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
|
|
48
|
-
export declare const REPOS_USAGE = "usage:\n seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces\n seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> -e <env> [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
|
|
99
|
+
export declare const REPOS_USAGE = "usage:\n seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces\n seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> -e <env> [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)\n seq-studio repos ci show <ns>/<name> -e <env> [--ref r] preview discovered CI checks\n seq-studio repos ci require <ns>/<name> --check <name> -e <env> reserved (refuses write until sandboxed runner)\n seq-studio repos ci import <ns>/<name> -e <env> [--ref r] reserved (refuses write until sandboxed runner)\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
|
|
49
100
|
export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
|
101
|
+
export {};
|
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':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequenceholdings/studio-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
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.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|