@sequenceholdings/studio-cli 0.1.24 → 0.1.26
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 +32 -6
- package/dist/app/commands.js +5 -2
- package/dist/app/readme.d.ts +33 -0
- package/dist/app/readme.js +294 -0
- package/dist/app/scaffold.js +116 -13
- package/dist/auth.d.ts +9 -5
- package/dist/auth.js +121 -8
- package/dist/config.d.ts +7 -3
- package/dist/config.js +77 -5
- package/dist/envs/commands.js +9 -6
- package/dist/functions/bundle.js +3 -3
- package/dist/functions/commands.js +18 -5
- package/dist/functions/manifest.d.ts +3 -0
- package/dist/functions/manifest.js +48 -0
- package/dist/login.js +3 -3
- package/dist/pipeline/commands.js +6 -2
- package/dist/pipeline/lifecycle.js +93 -16
- package/dist/preview.d.ts +4 -3
- package/dist/preview.js +5 -4
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +1 -1
- package/package.json +7 -7
|
@@ -9,6 +9,7 @@ const LOG = '[seq-studio]';
|
|
|
9
9
|
const TERMINAL = new Set(['active', 'failed', 'retired']);
|
|
10
10
|
const POLL_INTERVAL_MS = 2000;
|
|
11
11
|
const HEARTBEAT_INTERVAL_MS = 20_000;
|
|
12
|
+
const PLAN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
12
13
|
const FIRST_PARTY_TARGETS = [
|
|
13
14
|
{ id: 'dev', label: 'Development', requiresApproval: false },
|
|
14
15
|
{ id: 'staging', label: 'Staging', requiresApproval: false },
|
|
@@ -56,6 +57,9 @@ function flagString(flags, key) {
|
|
|
56
57
|
throw new Error(`--${key} requires a value`);
|
|
57
58
|
return value;
|
|
58
59
|
}
|
|
60
|
+
function flagOn(flags, key) {
|
|
61
|
+
return flags[key] === true || flags[key] === 'true';
|
|
62
|
+
}
|
|
59
63
|
export function deployEnvironmentForEnv(env) {
|
|
60
64
|
return env.name === 'local' ? 'dev' : env.name;
|
|
61
65
|
}
|
|
@@ -101,19 +105,17 @@ export async function pipelinePlanCommand(args) {
|
|
|
101
105
|
}
|
|
102
106
|
let response;
|
|
103
107
|
try {
|
|
104
|
-
response = await
|
|
108
|
+
response = await enqueueAndWaitForPlan({
|
|
105
109
|
baseUrl: env.url,
|
|
106
110
|
token,
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
repo,
|
|
112
|
+
ref,
|
|
113
|
+
environment: target.id,
|
|
109
114
|
});
|
|
110
115
|
}
|
|
111
116
|
catch (error) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return 1;
|
|
115
|
-
}
|
|
116
|
-
throw error;
|
|
117
|
+
console.error(`${LOG} plan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
return 1;
|
|
117
119
|
}
|
|
118
120
|
if (json) {
|
|
119
121
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -129,6 +131,7 @@ export async function pipelineDeployCommand(args) {
|
|
|
129
131
|
const ref = flagString(args.flags, 'ref');
|
|
130
132
|
const deploymentId = flagString(args.flags, 'deployment-id');
|
|
131
133
|
const approvedBy = flagString(args.flags, 'approved-by');
|
|
134
|
+
const runNow = flagOn(args.flags, 'run-now');
|
|
132
135
|
const { env, token } = await envAndToken(args);
|
|
133
136
|
let target;
|
|
134
137
|
try {
|
|
@@ -144,27 +147,33 @@ export async function pipelineDeployCommand(args) {
|
|
|
144
147
|
// Direct execute of a persisted plan.
|
|
145
148
|
const stageId = flagString(args.flags, 'stage-id');
|
|
146
149
|
if (!stageId) {
|
|
147
|
-
console.error('usage: seq-studio pipeline deploy --deployment-id <id> --stage-id <id> -e <env> [--target <id>] [--approved-by <sub>] [--no-wait]');
|
|
150
|
+
console.error('usage: seq-studio pipeline deploy --deployment-id <id> --stage-id <id> -e <env> [--target <id>] [--approved-by <sub>] [--run-now] [--no-wait]');
|
|
148
151
|
return 1;
|
|
149
152
|
}
|
|
150
153
|
enqueue = await postJson({
|
|
151
154
|
baseUrl: env.url,
|
|
152
155
|
token,
|
|
153
156
|
path: `/api/data-pipelines/stages/${stageId}/deploy`,
|
|
154
|
-
body: {
|
|
157
|
+
body: {
|
|
158
|
+
deploymentId,
|
|
159
|
+
environment: target.id,
|
|
160
|
+
...(approvedBy ? { approvedBy } : {}),
|
|
161
|
+
runNow,
|
|
162
|
+
},
|
|
155
163
|
});
|
|
156
164
|
}
|
|
157
165
|
else {
|
|
158
166
|
if (!repo || !ref) {
|
|
159
|
-
console.error('usage: seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha|branch> -e <env> [--target <id>] [--approved-by <sub>] [--no-wait]');
|
|
167
|
+
console.error('usage: seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha|branch> -e <env> [--target <id>] [--approved-by <sub>] [--run-now] [--no-wait]');
|
|
160
168
|
return 1;
|
|
161
169
|
}
|
|
162
170
|
// Plan then deploy the first planning deployment.
|
|
163
|
-
const plan = await
|
|
171
|
+
const plan = await enqueueAndWaitForPlan({
|
|
164
172
|
baseUrl: env.url,
|
|
165
173
|
token,
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
repo,
|
|
175
|
+
ref,
|
|
176
|
+
environment: target.id,
|
|
168
177
|
});
|
|
169
178
|
if (plan.plan.hasDestructive) {
|
|
170
179
|
console.error(`${LOG} plan has destructive findings — refusing to deploy`);
|
|
@@ -190,6 +199,7 @@ export async function pipelineDeployCommand(args) {
|
|
|
190
199
|
deploymentId: firstId,
|
|
191
200
|
environment: target.id,
|
|
192
201
|
...(approvedBy ? { approvedBy } : {}),
|
|
202
|
+
runNow,
|
|
193
203
|
},
|
|
194
204
|
});
|
|
195
205
|
}
|
|
@@ -250,10 +260,20 @@ export async function pipelinePromoteCommand(args) {
|
|
|
250
260
|
}
|
|
251
261
|
throw error;
|
|
252
262
|
}
|
|
253
|
-
console.log(`${LOG} enqueued promote ${enqueue.
|
|
263
|
+
console.log(`${LOG} enqueued promote plan ${enqueue.planRequestId} (trigger=${enqueue.triggerRunId ?? 'n/a'})`);
|
|
254
264
|
if (args.flags['no-wait'] === true || args.flags['no-wait'] === 'true')
|
|
255
265
|
return 0;
|
|
256
|
-
|
|
266
|
+
const plan = await waitForPlanRequest({
|
|
267
|
+
baseUrl: env.url,
|
|
268
|
+
token,
|
|
269
|
+
planRequestId: enqueue.planRequestId,
|
|
270
|
+
});
|
|
271
|
+
const deploymentId = plan.deploymentIds[plan.stageIds.indexOf(stageId)] ?? plan.deploymentIds[0];
|
|
272
|
+
if (!deploymentId) {
|
|
273
|
+
console.error(`${LOG} promote plan completed without a deployment id`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
return pollDeployment({ baseUrl: env.url, token, deploymentId });
|
|
257
277
|
}
|
|
258
278
|
export async function pipelineRunNowCommand(args) {
|
|
259
279
|
const stage = flagString(args.flags, 'stage');
|
|
@@ -404,6 +424,63 @@ async function pollDeployment({ baseUrl, token, deploymentId, }) {
|
|
|
404
424
|
console.error(`${LOG} timed out waiting for deployment ${deploymentId} (${formatElapsed(Date.now() - started)})`);
|
|
405
425
|
return 1;
|
|
406
426
|
}
|
|
427
|
+
async function enqueueAndWaitForPlan({ baseUrl, token, repo, ref, environment, }) {
|
|
428
|
+
const enqueue = await postJson({
|
|
429
|
+
baseUrl,
|
|
430
|
+
token,
|
|
431
|
+
path: '/api/data-pipelines/pipelines/plan',
|
|
432
|
+
body: { repo, ref, environment },
|
|
433
|
+
});
|
|
434
|
+
console.log(`${LOG} enqueued plan ${enqueue.planRequestId} (trigger=${enqueue.triggerRunId ?? 'n/a'})`);
|
|
435
|
+
const detail = await waitForPlanRequest({
|
|
436
|
+
baseUrl,
|
|
437
|
+
token,
|
|
438
|
+
planRequestId: enqueue.planRequestId,
|
|
439
|
+
});
|
|
440
|
+
if (!detail.plan || !detail.text) {
|
|
441
|
+
throw new Error(`plan request ${detail.planRequestId} succeeded without a plan result`);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
plan: detail.plan,
|
|
445
|
+
deploymentIds: detail.deploymentIds,
|
|
446
|
+
stageIds: detail.stageIds,
|
|
447
|
+
text: detail.text,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async function waitForPlanRequest({ baseUrl, token, planRequestId, }) {
|
|
451
|
+
const started = Date.now();
|
|
452
|
+
let lastStatus = null;
|
|
453
|
+
let lastLoggedAt = started;
|
|
454
|
+
while (Date.now() - started < PLAN_TIMEOUT_MS) {
|
|
455
|
+
const detail = await getJson({
|
|
456
|
+
baseUrl,
|
|
457
|
+
token,
|
|
458
|
+
path: `/api/data-pipelines/plan-requests/${planRequestId}`,
|
|
459
|
+
});
|
|
460
|
+
const elapsed = Date.now() - started;
|
|
461
|
+
const statusLabel = detail.statusDetail
|
|
462
|
+
? `${detail.status} — ${detail.statusDetail}`
|
|
463
|
+
: detail.status;
|
|
464
|
+
if (detail.status === 'failed') {
|
|
465
|
+
throw new Error(`plan request ${detail.planRequestId} failed after ${formatElapsed(elapsed)}: ` +
|
|
466
|
+
`${detail.statusDetail ?? 'unknown error'}`);
|
|
467
|
+
}
|
|
468
|
+
if (detail.status === 'succeeded')
|
|
469
|
+
return detail;
|
|
470
|
+
if (statusLabel !== lastStatus) {
|
|
471
|
+
console.log(`${LOG} plan ${detail.planRequestId}: ${statusLabel} (${formatElapsed(elapsed)})`);
|
|
472
|
+
lastStatus = statusLabel;
|
|
473
|
+
lastLoggedAt = Date.now();
|
|
474
|
+
}
|
|
475
|
+
else if (Date.now() - lastLoggedAt >= HEARTBEAT_INTERVAL_MS) {
|
|
476
|
+
console.log(`${LOG} plan ${detail.planRequestId}: still ${statusLabel} (${formatElapsed(elapsed)})`);
|
|
477
|
+
lastLoggedAt = Date.now();
|
|
478
|
+
}
|
|
479
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
480
|
+
}
|
|
481
|
+
throw new Error(`timed out waiting for plan request ${planRequestId} ` +
|
|
482
|
+
`(${formatElapsed(Date.now() - started)})`);
|
|
483
|
+
}
|
|
407
484
|
export async function pipelineAdoptCommand(args) {
|
|
408
485
|
const stage = flagString(args.flags, 'stage');
|
|
409
486
|
const ref = flagString(args.flags, 'ref');
|
package/dist/preview.d.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* hand-editing `~/.config/lattice/config.toml`. See
|
|
7
7
|
* `docs/preview-environments.md` for the full lifecycle.
|
|
8
8
|
*
|
|
9
|
-
* SLUG ALGORITHM — must stay byte-for-byte in sync with the
|
|
9
|
+
* SLUG ALGORITHM — must stay byte-for-byte in sync with the other
|
|
10
10
|
* places that compute the same slug from a branch name:
|
|
11
|
-
* - `.github/workflows/preview-deploy.yml` (
|
|
12
|
-
* -
|
|
11
|
+
* - `.github/workflows/preview-deploy.yml` (replace non-alnum, lowercase, trim hyphens)
|
|
12
|
+
* - `.github/workflows/trigger-preview.yml` (same bash pipeline)
|
|
13
|
+
* - `atlas/src/server/db.ts:sanitize` (`.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase().replace(/^-+|-+$/g, '')`)
|
|
13
14
|
* - `databricks/lakebase/atlas-db/pr-lifecycle.ts:sanitize`
|
|
14
15
|
* Changing it here without changing those produces a host that does NOT
|
|
15
16
|
* match what `preview-deploy.yml` actually deployed.
|
package/dist/preview.js
CHANGED
|
@@ -8,10 +8,11 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
* hand-editing `~/.config/lattice/config.toml`. See
|
|
9
9
|
* `docs/preview-environments.md` for the full lifecycle.
|
|
10
10
|
*
|
|
11
|
-
* SLUG ALGORITHM — must stay byte-for-byte in sync with the
|
|
11
|
+
* SLUG ALGORITHM — must stay byte-for-byte in sync with the other
|
|
12
12
|
* places that compute the same slug from a branch name:
|
|
13
|
-
* - `.github/workflows/preview-deploy.yml` (
|
|
14
|
-
* -
|
|
13
|
+
* - `.github/workflows/preview-deploy.yml` (replace non-alnum, lowercase, trim hyphens)
|
|
14
|
+
* - `.github/workflows/trigger-preview.yml` (same bash pipeline)
|
|
15
|
+
* - `atlas/src/server/db.ts:sanitize` (`.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase().replace(/^-+|-+$/g, '')`)
|
|
15
16
|
* - `databricks/lakebase/atlas-db/pr-lifecycle.ts:sanitize`
|
|
16
17
|
* Changing it here without changing those produces a host that does NOT
|
|
17
18
|
* match what `preview-deploy.yml` actually deployed.
|
|
@@ -45,7 +46,7 @@ export const PREVIEW_PROTECTED_SLUGS = [
|
|
|
45
46
|
export const MAX_LABEL_LENGTH = 63;
|
|
46
47
|
/** Apply the canonical branch → slug transform. */
|
|
47
48
|
export function previewSlug(branchOrSlug) {
|
|
48
|
-
return branchOrSlug.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
|
|
49
|
+
return branchOrSlug.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase().replace(/^-+|-+$/g, '');
|
|
49
50
|
}
|
|
50
51
|
/** The `studio-atlas-git-<slug>` DNS label Vercel assigns the preview. */
|
|
51
52
|
export function previewLabel(slug) {
|
package/dist/repos/commands.d.ts
CHANGED
|
@@ -96,6 +96,6 @@ export declare function reposCiRequireCommand(args: ParsedArgs): Promise<number>
|
|
|
96
96
|
export declare function reposCiImportCommand(args: ParsedArgs): Promise<number>;
|
|
97
97
|
export declare function reposCiCommand(args: ParsedArgs): Promise<number>;
|
|
98
98
|
export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
|
|
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
|
|
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 a repo seeded with a Sequence app README\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";
|
|
100
100
|
export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
|
101
101
|
export {};
|
package/dist/repos/commands.js
CHANGED
|
@@ -748,7 +748,7 @@ export const REPOS_USAGE = `usage:
|
|
|
748
748
|
seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment
|
|
749
749
|
seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces
|
|
750
750
|
seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)
|
|
751
|
-
seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create
|
|
751
|
+
seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create a repo seeded with a Sequence app README
|
|
752
752
|
seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]
|
|
753
753
|
seq-studio repos clone --url <https://…/repos/<id>/git> -e <env> [--ref r] [--out dir]
|
|
754
754
|
seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequenceholdings/studio-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
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": {
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"README.md"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"js-yaml": "^4.
|
|
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/
|
|
44
|
-
"@sequenceholdings/
|
|
45
|
-
"@sequenceholdings/
|
|
43
|
+
"@sequenceholdings/agent-spec": "0.1.2",
|
|
44
|
+
"@sequenceholdings/artifact-studio": "0.2.2",
|
|
45
|
+
"@sequenceholdings/lattice": "0.1.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@sequenceholdings/orm": "0.1.3",
|
|
@@ -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/orm": "0.1.3",
|
|
65
|
+
"@sequenceholdings/pipeline-spec": "0.1.0"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
|
68
68
|
"node": ">=20"
|