@sequenceholdings/studio-cli 0.1.13 → 0.1.18

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.
Files changed (53) hide show
  1. package/README.md +94 -30
  2. package/dist/artifact/delegate.d.ts +2 -2
  3. package/dist/artifact/delegate.js +31 -73
  4. package/dist/atlas-client.js +52 -37
  5. package/dist/auth-cmds/commands.d.ts +1 -1
  6. package/dist/auth-cmds/commands.js +12 -7
  7. package/dist/auth.d.ts +97 -19
  8. package/dist/auth.js +376 -81
  9. package/dist/config.d.ts +3 -3
  10. package/dist/config.js +18 -13
  11. package/dist/env-catalog.js +13 -3
  12. package/dist/env-flags.d.ts +2 -0
  13. package/dist/env-flags.js +2 -0
  14. package/dist/env-registry.d.ts +27 -0
  15. package/dist/env-registry.js +204 -0
  16. package/dist/envs/commands.d.ts +1 -1
  17. package/dist/envs/commands.js +41 -3
  18. package/dist/file-lock.d.ts +5 -0
  19. package/dist/file-lock.js +187 -0
  20. package/dist/functions/commands.d.ts +9 -1
  21. package/dist/functions/commands.js +71 -29
  22. package/dist/functions/manifest.d.ts +1 -0
  23. package/dist/functions/manifest.js +36 -0
  24. package/dist/login.d.ts +8 -3
  25. package/dist/login.js +46 -34
  26. package/dist/main.d.ts +2 -1
  27. package/dist/main.js +35 -12
  28. package/dist/orm/delegate.js +15 -2
  29. package/dist/pat-hints.js +2 -2
  30. package/dist/pipeline/commands.d.ts +58 -0
  31. package/dist/pipeline/commands.js +330 -0
  32. package/dist/pipeline/lifecycle.d.ts +58 -0
  33. package/dist/pipeline/lifecycle.js +348 -0
  34. package/dist/pipeline/pinning.d.ts +5 -0
  35. package/dist/pipeline/pinning.js +9 -0
  36. package/dist/pipeline/templates.d.ts +11 -0
  37. package/dist/pipeline/templates.js +166 -0
  38. package/dist/process/build.d.ts +4 -0
  39. package/dist/process/build.js +31 -1
  40. package/dist/process/codegen.js +19 -1
  41. package/dist/process/commands.js +97 -47
  42. package/dist/process/compiler-subprocess.d.ts +29 -0
  43. package/dist/process/compiler-subprocess.js +99 -0
  44. package/dist/process/compiler-worker.d.ts +1 -0
  45. package/dist/process/compiler-worker.js +38 -0
  46. package/dist/process/lint.d.ts +8 -0
  47. package/dist/process/lint.js +76 -29
  48. package/dist/process/repo-install.js +18 -2
  49. package/dist/repos/commands.d.ts +1 -1
  50. package/dist/repos/commands.js +17 -12
  51. package/dist/secrets/commands.d.ts +1 -1
  52. package/dist/secrets/commands.js +18 -18
  53. package/package.json +8 -3
@@ -0,0 +1,330 @@
1
+ /**
2
+ * `seq-studio pipeline <sub>` — the Data Pipelines authoring interface.
3
+ *
4
+ * `init` scaffolds a typed stage spec (`<name>.stage.yml` + entrypoint stub);
5
+ * `validate` runs the full offline SDK gate (envelope + body + schema_ref
6
+ * resolution + graph validation) over a Pipeline directory. Both are also the
7
+ * CI gate for `pipelines/**` repos (`.github/workflows/pipeline-verify.yml`).
8
+ *
9
+ * All schema logic lives in @sequenceholdings/pipeline-spec (single source of truth) —
10
+ * this module only handles argv, filesystem scaffolding, and output shaping.
11
+ * The SDK is lazy-imported like `@sequenceholdings/orm` so public installs of
12
+ * the CLI without the package get a clear install hint instead of a crash.
13
+ */
14
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
15
+ import { existsSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { authenticatedRequestUrl, validateDeploymentBaseUrl, } from '@sequenceholdings/artifact-studio/deployment-validation';
18
+ import { tryGetAccessToken } from '../auth.js';
19
+ import { readConfig } from '../config.js';
20
+ import { renderEntrypointStub, renderStageTemplate, STAGE_TYPES, } from './templates.js';
21
+ const LOG = '[seq-studio]';
22
+ const PIPELINE_USAGE = `usage:
23
+ seq-studio pipeline init --type ingestion|transformation|serving <name> [--dir <dir>]
24
+ scaffold <name>.stage.yml (commented template) + src/ entrypoint stub
25
+
26
+ seq-studio pipeline validate [dir] [--assets <file|url>] [--json]
27
+ run the offline spec gate: envelope + body validation, schema_ref
28
+ resolution, and repo-level graph validation (references, single-writer,
29
+ cycles, column subsets). --assets supplies a registry asset export for
30
+ cross-Pipeline references. Exits 1 on any error-severity finding.
31
+
32
+ seq-studio pipeline plan --repo pipelines/<slug> --ref <sha|branch> -e <env> [--json]
33
+ materialize + SDK/graph + compile + live-diff + provision findings
34
+ (no Databricks CLI / DAB validate on Atlas); exit 1 on destructive findings
35
+
36
+ seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env>
37
+ [--approved-by <sub>] [--no-wait] [--json]
38
+ plan then enqueue Trigger deploy (DAB bundle validate hard-gates before
39
+ bundle deploy); production/banksouth require a pinned 40-hex SHA
40
+
41
+ seq-studio pipeline promote --stage <slug> --version <v> -e <env>
42
+ [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]
43
+ promote a validated version to the next environment (approvals are
44
+ self-recorded: --approved-by must name the authenticated caller)
45
+
46
+ seq-studio pipeline rollback --stage <slug> -e <env> [--repo pipelines/<slug>]
47
+ [--approved-by <you>] [--no-wait]
48
+ redeploy the previously retired deployment's version (production/
49
+ banksouth require --approved-by — approvals are explicit even for
50
+ rollbacks)
51
+
52
+ seq-studio pipeline run-now --stage <slug> -e <env> [--repo pipelines/<slug>] [--json]
53
+ fire the stage's active deployment resource (job run-now / DLT
54
+ start_update) and print the Databricks run URL
55
+ `;
56
+ /**
57
+ * Same classification as the orm delegate: is the pipeline-spec package
58
+ * itself absent (installable) or did one of its dependencies fail to load?
59
+ */
60
+ export function isPipelineSpecMissing(message) {
61
+ const specifier = message.match(/Cannot find (?:package|module) '([^']+)'/)?.[1];
62
+ return specifier === '@sequenceholdings/pipeline-spec' || (specifier?.startsWith('@sequenceholdings/pipeline-spec/') ?? false);
63
+ }
64
+ async function importPipelineSpec() {
65
+ try {
66
+ return await import('@sequenceholdings/pipeline-spec');
67
+ }
68
+ catch (err) {
69
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ if (isPipelineSpecMissing(message)) {
72
+ console.error('seq-studio pipeline needs the @sequenceholdings/pipeline-spec package. Install it alongside the CLI:\n' +
73
+ ' npm install @sequenceholdings/pipeline-spec');
74
+ }
75
+ else {
76
+ console.error('@sequenceholdings/pipeline-spec is installed but a dependency failed to load. Reinstall dependencies, then retry.\n' +
77
+ ` ${message}`);
78
+ }
79
+ return null;
80
+ }
81
+ throw err;
82
+ }
83
+ }
84
+ const isStageTemplateType = (value) => STAGE_TYPES.includes(value);
85
+ export async function pipelineInitCommand(args) {
86
+ const name = args.positional[0];
87
+ const type = typeof args.flags.type === 'string' ? args.flags.type : undefined;
88
+ if (!name || !type) {
89
+ console.error('usage: seq-studio pipeline init --type ingestion|transformation|serving <name> [--dir <dir>]');
90
+ return 1;
91
+ }
92
+ if (!isStageTemplateType(type)) {
93
+ console.error(`${LOG} unknown stage type '${type}' — expected one of: ${STAGE_TYPES.join(', ')}`);
94
+ return 1;
95
+ }
96
+ const spec = await importPipelineSpec();
97
+ if (!spec)
98
+ return 1;
99
+ if (!spec.STAGE_ID_PATTERN.test(name)) {
100
+ console.error(`${LOG} stage name '${name}' must be a kebab-case slug (a-z, 0-9, -)`);
101
+ return 1;
102
+ }
103
+ const dir = resolve(typeof args.flags.dir === 'string' ? args.flags.dir : '.');
104
+ const specPath = join(dir, `${name}.stage.yml`);
105
+ if (existsSync(specPath)) {
106
+ console.error(`${LOG} ${specPath} already exists — refusing to overwrite`);
107
+ return 1;
108
+ }
109
+ await mkdir(dir, { recursive: true });
110
+ await writeFile(specPath, renderStageTemplate(name, type), 'utf8');
111
+ console.log(`${LOG} wrote ${specPath}`);
112
+ // Serving stages are declarative — no entrypoint stub. The template's
113
+ // `entrypoint:` and this stub path share the snake-cased name.
114
+ if (type !== 'serving') {
115
+ const stubPath = join(dir, 'src', `${name.replace(/-/g, '_')}.py`);
116
+ if (existsSync(stubPath)) {
117
+ console.log(`${LOG} ${stubPath} already exists — left untouched`);
118
+ }
119
+ else {
120
+ await mkdir(join(dir, 'src'), { recursive: true });
121
+ await writeFile(stubPath, renderEntrypointStub(name), 'utf8');
122
+ console.log(`${LOG} wrote ${stubPath}`);
123
+ }
124
+ }
125
+ const displayDir = typeof args.flags.dir === 'string' ? args.flags.dir : '.';
126
+ if (type === 'serving') {
127
+ // A serving stage inherently references its producer — the scaffold's
128
+ // placeholder cannot resolve until the author points it at a real stage.
129
+ console.log(`${LOG} next: point source.stage and trigger.after at your producing transformation stage — ` +
130
+ `\`seq-studio pipeline validate ${displayDir}\` will fail with unresolved references until then`);
131
+ }
132
+ else {
133
+ console.log(`${LOG} next: fill in the TODOs, then run: seq-studio pipeline validate ${displayDir}`);
134
+ }
135
+ return 0;
136
+ }
137
+ function graphFindingToReport(finding) {
138
+ return {
139
+ severity: finding.severity,
140
+ code: finding.code,
141
+ message: finding.message,
142
+ ...(finding.stage !== undefined ? { stage: finding.stage } : {}),
143
+ ...(finding.asset !== undefined ? { asset: finding.asset } : {}),
144
+ ...(finding.consumers !== undefined ? { consumers: finding.consumers } : {}),
145
+ ...(finding.suggestions !== undefined ? { suggestions: finding.suggestions } : {}),
146
+ ...(finding.cycle_path !== undefined ? { cycle_path: finding.cycle_path } : {}),
147
+ };
148
+ }
149
+ export function buildExternalAssetsRequest({ source, knownOrigins, token, }) {
150
+ const unauthenticated = {
151
+ url: source,
152
+ init: {
153
+ headers: {},
154
+ redirect: 'manual',
155
+ },
156
+ };
157
+ if (!token)
158
+ return unauthenticated;
159
+ let parsed;
160
+ try {
161
+ parsed = new URL(source);
162
+ }
163
+ catch {
164
+ return unauthenticated;
165
+ }
166
+ if (!knownOrigins.includes(parsed.origin))
167
+ return unauthenticated;
168
+ const path = `${parsed.pathname}${parsed.search}`;
169
+ const url = authenticatedRequestUrl({
170
+ baseUrl: parsed.origin,
171
+ path,
172
+ });
173
+ return {
174
+ url,
175
+ init: {
176
+ headers: { authorization: `Bearer ${token}` },
177
+ redirect: 'manual',
178
+ },
179
+ };
180
+ }
181
+ /** Trusted origins of every environment the CLI knows about. */
182
+ export function configuredEnvOrigins(envs) {
183
+ const origins = [];
184
+ for (const env of Object.values(envs)) {
185
+ try {
186
+ origins.push(validateDeploymentBaseUrl(env.url));
187
+ }
188
+ catch {
189
+ // A malformed or untrusted config.toml URL contributes no auth origin.
190
+ }
191
+ }
192
+ return origins;
193
+ }
194
+ async function loadExternalAssets(spec, source) {
195
+ let raw;
196
+ if (/^https?:\/\//.test(source)) {
197
+ const config = await readConfig();
198
+ const token = await tryGetAccessToken();
199
+ const request = buildExternalAssetsRequest({
200
+ source,
201
+ knownOrigins: configuredEnvOrigins(config.envs),
202
+ token,
203
+ });
204
+ const response = await fetch(request.url, request.init);
205
+ if (!response.ok) {
206
+ throw new Error(`fetching assets export failed: ${response.status} ${response.statusText}`);
207
+ }
208
+ raw = await response.text();
209
+ }
210
+ else {
211
+ raw = await readFile(resolve(source), 'utf8');
212
+ }
213
+ return spec.externalAssetsExportSchema.parse(JSON.parse(raw));
214
+ }
215
+ function printHumanReport(report) {
216
+ for (const finding of report.findings) {
217
+ const prefix = finding.severity === 'error' ? 'error' : 'warning';
218
+ const where = finding.stage ? ` [${finding.stage}]` : '';
219
+ console.log(`${prefix}(${finding.code})${where}: ${finding.message}`);
220
+ }
221
+ const errors = report.findings.filter((finding) => finding.severity === 'error').length;
222
+ const warnings = report.findings.length - errors;
223
+ console.log(`${LOG} ${report.stages.length} stage${report.stages.length === 1 ? '' : 's'} in ${report.dir} — ` +
224
+ `${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}`);
225
+ console.log(report.ok ? `${LOG} validation passed` : `${LOG} validation failed`);
226
+ }
227
+ export async function pipelineValidateCommand(args) {
228
+ const spec = await importPipelineSpec();
229
+ if (!spec)
230
+ return 1;
231
+ const dirArg = args.positional[0] ?? '.';
232
+ const dir = resolve(dirArg);
233
+ const json = args.flags.json === true || args.flags.json === 'true';
234
+ const emit = (report) => {
235
+ if (json) {
236
+ console.log(JSON.stringify(report, null, 2));
237
+ }
238
+ else {
239
+ printHumanReport(report);
240
+ }
241
+ return report.ok ? 0 : 1;
242
+ };
243
+ let pipeline;
244
+ try {
245
+ pipeline = await spec.loadSpecDirectory(dir);
246
+ }
247
+ catch (error) {
248
+ if (error instanceof spec.SpecDefinitionError) {
249
+ return emit({
250
+ ok: false,
251
+ dir: dirArg,
252
+ stages: [],
253
+ findings: [
254
+ {
255
+ severity: 'error',
256
+ code: 'spec_definition_error',
257
+ message: error.message,
258
+ ...(error.file !== undefined ? { file: error.file } : {}),
259
+ ...(error.path !== undefined ? { path: error.path } : {}),
260
+ },
261
+ ],
262
+ });
263
+ }
264
+ throw error;
265
+ }
266
+ let externalAssets;
267
+ if (typeof args.flags.assets === 'string') {
268
+ try {
269
+ externalAssets = await loadExternalAssets(spec, args.flags.assets);
270
+ }
271
+ catch (error) {
272
+ return emit({
273
+ ok: false,
274
+ dir: dirArg,
275
+ stages: pipeline.stages.map((stage) => stage.stage),
276
+ findings: [
277
+ {
278
+ severity: 'error',
279
+ code: 'assets_export_unreadable',
280
+ message: error instanceof Error ? error.message : String(error),
281
+ },
282
+ ],
283
+ });
284
+ }
285
+ }
286
+ const result = spec.validateSpecGraph(pipeline.stages, externalAssets);
287
+ return emit({
288
+ ok: result.ok,
289
+ dir: dirArg,
290
+ stages: pipeline.stages.map((stage) => stage.stage),
291
+ findings: result.findings.map(graphFindingToReport),
292
+ });
293
+ }
294
+ export async function runPipelineCommand(sub, args) {
295
+ switch (sub) {
296
+ case 'init':
297
+ return pipelineInitCommand(args);
298
+ case 'validate':
299
+ return pipelineValidateCommand(args);
300
+ case 'plan': {
301
+ const { pipelinePlanCommand } = await import('./lifecycle.js');
302
+ return pipelinePlanCommand(args);
303
+ }
304
+ case 'deploy': {
305
+ const { pipelineDeployCommand } = await import('./lifecycle.js');
306
+ return pipelineDeployCommand(args);
307
+ }
308
+ case 'promote': {
309
+ const { pipelinePromoteCommand } = await import('./lifecycle.js');
310
+ return pipelinePromoteCommand(args);
311
+ }
312
+ case 'rollback': {
313
+ const { pipelineRollbackCommand } = await import('./lifecycle.js');
314
+ return pipelineRollbackCommand(args);
315
+ }
316
+ case 'run-now': {
317
+ const { pipelineRunNowCommand } = await import('./lifecycle.js');
318
+ return pipelineRunNowCommand(args);
319
+ }
320
+ case 'help':
321
+ case '--help':
322
+ case '-h':
323
+ case undefined:
324
+ console.log(PIPELINE_USAGE);
325
+ return sub ? 0 : 1;
326
+ default:
327
+ console.error(`${LOG} unknown pipeline subcommand '${sub}'\n${PIPELINE_USAGE}`);
328
+ return 1;
329
+ }
330
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Deploy lifecycle verbs for `seq-studio pipeline plan|deploy|promote|rollback`
3
+ * (SEQ-2450). Wraps the /api/data-pipelines plan/deploy APIs.
4
+ */
5
+ import { type ResolvedEnv } from '../config.js';
6
+ import type { ParsedArgs } from '../process/commands.js';
7
+ export interface PlanApiResponse {
8
+ plan: {
9
+ hasDestructive: boolean;
10
+ findings: Array<{
11
+ resourceKey: string;
12
+ classification: 'create' | 'update' | 'destructive';
13
+ destructiveReason?: string;
14
+ message: string;
15
+ managedTables?: string[];
16
+ }>;
17
+ unmodeled: Array<{
18
+ resourceKey: string;
19
+ reason: string;
20
+ sourcePath: string;
21
+ }>;
22
+ planHash: string;
23
+ commit: string;
24
+ environment: string;
25
+ bundleName: string;
26
+ };
27
+ deploymentIds: string[];
28
+ stageIds: string[];
29
+ text: string;
30
+ }
31
+ export interface EnqueueResponse {
32
+ deploymentId: string;
33
+ triggerRunId: string | null;
34
+ status: string;
35
+ }
36
+ export interface DeploymentDetail {
37
+ id: string;
38
+ status: string;
39
+ statusDetail: string | null;
40
+ }
41
+ /**
42
+ * The CLI's local dev-loop env is named `local` — chosen so `-e local` lines
43
+ * up with `seqapi -e local` / `artifact-studio --env local` (see `config.ts`
44
+ * `BUILT_IN_ENV_URLS`). There is no `local` in the server's deploy-lifecycle
45
+ * enum (`DEPLOY_ENVIRONMENTS` in `atlas/src/server/services/data-pipelines/schema.ts`
46
+ * is `dev | staging | production | banksouth`); the server's name for that
47
+ * same developer-loop target is `dev` (`targetFactsForEnvironment` treats
48
+ * `dev` as the one target exempt from a `lakebase_branch` binding). Map at
49
+ * this one boundary — every lifecycle request body funnels through here —
50
+ * so the CLI never sends the wire-invalid `local` and every other env name
51
+ * passes through unchanged.
52
+ */
53
+ export declare function deployEnvironmentForEnv(env: ResolvedEnv): string;
54
+ export declare function pipelinePlanCommand(args: ParsedArgs): Promise<number>;
55
+ export declare function pipelineDeployCommand(args: ParsedArgs): Promise<number>;
56
+ export declare function pipelinePromoteCommand(args: ParsedArgs): Promise<number>;
57
+ export declare function pipelineRunNowCommand(args: ParsedArgs): Promise<number>;
58
+ export declare function pipelineRollbackCommand(args: ParsedArgs): Promise<number>;