@sequenceholdings/studio-cli 0.1.9

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 (74) hide show
  1. package/README.md +258 -0
  2. package/dist/artifact/delegate.d.ts +25 -0
  3. package/dist/artifact/delegate.js +263 -0
  4. package/dist/atlas-client.d.ts +44 -0
  5. package/dist/atlas-client.js +173 -0
  6. package/dist/auth-cmds/commands.d.ts +15 -0
  7. package/dist/auth-cmds/commands.js +249 -0
  8. package/dist/auth.d.ts +26 -0
  9. package/dist/auth.js +171 -0
  10. package/dist/bin.d.ts +2 -0
  11. package/dist/bin.js +8 -0
  12. package/dist/cli-errors.d.ts +5 -0
  13. package/dist/cli-errors.js +78 -0
  14. package/dist/config.d.ts +44 -0
  15. package/dist/config.js +103 -0
  16. package/dist/env-flags.d.ts +8 -0
  17. package/dist/env-flags.js +47 -0
  18. package/dist/functions/bundle.d.ts +30 -0
  19. package/dist/functions/bundle.js +137 -0
  20. package/dist/functions/commands.d.ts +86 -0
  21. package/dist/functions/commands.js +999 -0
  22. package/dist/functions/egress-preview.d.ts +32 -0
  23. package/dist/functions/egress-preview.js +54 -0
  24. package/dist/functions/lockfile-origin.d.ts +16 -0
  25. package/dist/functions/lockfile-origin.js +45 -0
  26. package/dist/functions/manifest.d.ts +89 -0
  27. package/dist/functions/manifest.js +586 -0
  28. package/dist/functions/secret-reconcile.d.ts +79 -0
  29. package/dist/functions/secret-reconcile.js +86 -0
  30. package/dist/main.d.ts +14 -0
  31. package/dist/main.js +129 -0
  32. package/dist/orm/delegate.d.ts +8 -0
  33. package/dist/orm/delegate.js +61 -0
  34. package/dist/pat-hints.d.ts +17 -0
  35. package/dist/pat-hints.js +28 -0
  36. package/dist/preview.d.ts +89 -0
  37. package/dist/preview.js +291 -0
  38. package/dist/process/agent-loader.d.ts +24 -0
  39. package/dist/process/agent-loader.js +57 -0
  40. package/dist/process/build.d.ts +14 -0
  41. package/dist/process/build.js +368 -0
  42. package/dist/process/codegen.d.ts +18 -0
  43. package/dist/process/codegen.js +270 -0
  44. package/dist/process/commands.d.ts +47 -0
  45. package/dist/process/commands.js +786 -0
  46. package/dist/process/discover.d.ts +32 -0
  47. package/dist/process/discover.js +131 -0
  48. package/dist/process/lint.d.ts +39 -0
  49. package/dist/process/lint.js +485 -0
  50. package/dist/process/local-bundle.d.ts +17 -0
  51. package/dist/process/local-bundle.js +65 -0
  52. package/dist/process/plan-diff.d.ts +82 -0
  53. package/dist/process/plan-diff.js +333 -0
  54. package/dist/process/resolve-process-pin.d.ts +11 -0
  55. package/dist/process/resolve-process-pin.js +63 -0
  56. package/dist/process/simulate.d.ts +50 -0
  57. package/dist/process/simulate.js +328 -0
  58. package/dist/prompt.d.ts +35 -0
  59. package/dist/prompt.js +65 -0
  60. package/dist/repos/commands.d.ts +49 -0
  61. package/dist/repos/commands.js +548 -0
  62. package/dist/repos/git-clone.d.ts +10 -0
  63. package/dist/repos/git-clone.js +49 -0
  64. package/dist/secrets/commands.d.ts +24 -0
  65. package/dist/secrets/commands.js +704 -0
  66. package/dist/templates/process/example-process/process.ts +43 -0
  67. package/dist/templates/process/package.json +23 -0
  68. package/dist/templates/process/pnpm-workspace.yaml +21 -0
  69. package/dist/templates/process/tsconfig.json +17 -0
  70. package/package.json +78 -0
  71. package/templates/process/example-process/process.ts +43 -0
  72. package/templates/process/package.json +23 -0
  73. package/templates/process/pnpm-workspace.yaml +21 -0
  74. package/templates/process/tsconfig.json +17 -0
@@ -0,0 +1,291 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ /**
4
+ * Per-PR preview-environment resolution for `seq-studio artifact`.
5
+ *
6
+ * Lets an engineer target a specific PR's Vercel preview deployment
7
+ * (`https://studio-atlas-git-<slug>.preview.seqholdings.com`) WITHOUT
8
+ * hand-editing `~/.config/lattice/config.toml`. See
9
+ * `docs/preview-environments.md` for the full lifecycle.
10
+ *
11
+ * SLUG ALGORITHM — must stay byte-for-byte in sync with the three other
12
+ * places that compute the same slug from a branch name:
13
+ * - `.github/workflows/preview-deploy.yml` (`sed 's/[^a-zA-Z0-9-]/-/g' | tr '[:upper:]' '[:lower:]'`)
14
+ * - `atlas/src/server/db.ts:sanitize` (`replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase()`)
15
+ * - `databricks/lakebase/atlas-db/pr-lifecycle.ts:sanitize`
16
+ * Changing it here without changing those produces a host that does NOT
17
+ * match what `preview-deploy.yml` actually deployed.
18
+ */
19
+ /** Cloudflare zone the preview aliases live on. */
20
+ export const PREVIEW_DOMAIN = 'preview.seqholdings.com';
21
+ /** Vercel project name → the `<project>-git-<slug>` alias label prefix. */
22
+ export const PREVIEW_PROJECT = 'studio-atlas';
23
+ const LABEL_PREFIX = `${PREVIEW_PROJECT}-git-`;
24
+ /**
25
+ * Mirror of `preview-deploy.yml`'s `PROTECTED` list. A branch whose slug
26
+ * matches one of these (or starts with `dev-`) never gets a preview — the
27
+ * workflow's guard refuses to deploy it — so the CLI refuses to target it.
28
+ */
29
+ export const PREVIEW_PROTECTED_SLUGS = [
30
+ 'banksouth',
31
+ 'staging',
32
+ 'dev',
33
+ 'production',
34
+ 'main',
35
+ 'development',
36
+ 'master',
37
+ ];
38
+ /**
39
+ * Vercel deploys the preview under the DNS label `studio-atlas-git-<slug>`.
40
+ * A single DNS label can be at most 63 chars; Vercel hash-truncates labels
41
+ * past that, so the resulting host can NOT be computed from the slug and must
42
+ * be read back from the deployment. This is the SAME cutoff the workflow uses
43
+ * (`[ "${#LABEL}" -le 63 ]`).
44
+ */
45
+ export const MAX_LABEL_LENGTH = 63;
46
+ /** Apply the canonical branch → slug transform. */
47
+ export function previewSlug(branchOrSlug) {
48
+ return branchOrSlug.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
49
+ }
50
+ /** The `studio-atlas-git-<slug>` DNS label Vercel assigns the preview. */
51
+ export function previewLabel(slug) {
52
+ return `${LABEL_PREFIX}${slug}`;
53
+ }
54
+ /** True for protected slugs that never get a preview (mirror of the workflow). */
55
+ export function isProtectedPreviewSlug(slug) {
56
+ return (PREVIEW_PROTECTED_SLUGS.includes(slug) ||
57
+ slug.startsWith('dev-'));
58
+ }
59
+ /**
60
+ * True when the slug is short enough that its preview host can be computed
61
+ * without reading the real alias back from Vercel (label ≤ 63 chars).
62
+ */
63
+ export function isComputablePreviewSlug(slug) {
64
+ return previewLabel(slug).length <= MAX_LABEL_LENGTH;
65
+ }
66
+ /** The computed preview host for a (short) slug. */
67
+ export function previewHost(slug) {
68
+ return `${previewLabel(slug)}.${PREVIEW_DOMAIN}`;
69
+ }
70
+ const PROTECTED_HINT = PREVIEW_PROTECTED_SLUGS.join(', ');
71
+ /**
72
+ * Resolve a `preview:<branch-or-slug>` target to its computed preview URL.
73
+ * Throws for protected slugs and for long slugs whose alias can't be computed
74
+ * (the caller should fall back to `--pr <number>` or `--env-url <url>`).
75
+ */
76
+ export function resolvePreviewFromSlug(branchOrSlug) {
77
+ const slug = previewSlug(branchOrSlug);
78
+ if (!slug) {
79
+ throw new Error('Preview env requires a branch or slug, e.g. --env preview:my-branch');
80
+ }
81
+ if (isProtectedPreviewSlug(slug)) {
82
+ throw new Error(`Refusing to target a protected branch slug "${slug}". ` +
83
+ `Protected slugs never get a preview (${PROTECTED_HINT}, or anything starting with "dev-").`);
84
+ }
85
+ if (!isComputablePreviewSlug(slug)) {
86
+ throw new Error(`Branch slug "${slug}" is too long (label "${previewLabel(slug)}" is ` +
87
+ `${previewLabel(slug).length} > ${MAX_LABEL_LENGTH} chars): Vercel ` +
88
+ `hash-truncates the alias, so the preview host can't be computed. ` +
89
+ `Resolve it with --pr <number> (reads the real alias from the PR) or ` +
90
+ `pass --env-url <https://…preview.seqholdings.com>.`);
91
+ }
92
+ return { slug, url: `https://${previewHost(slug)}` };
93
+ }
94
+ const execFileAsync = promisify(execFile);
95
+ const defaultGhRunner = async (args) => {
96
+ try {
97
+ const { stdout } = await execFileAsync('gh', args, { encoding: 'utf8' });
98
+ return stdout;
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ throw new Error(`Failed to run \`gh ${args.join(' ')}\`. Install the GitHub CLI and run ` +
103
+ `\`gh auth login\`, or pass --env-url <url> directly. (${message})`);
104
+ }
105
+ };
106
+ const TRUSTED_PREVIEW_COMMENT_AUTHORS = new Set([
107
+ 'vercel',
108
+ 'vercel[bot]',
109
+ 'github-actions',
110
+ 'github-actions[bot]',
111
+ ]);
112
+ function compareIsoDesc(a, b) {
113
+ if (a === b)
114
+ return 0;
115
+ return a > b ? -1 : 1;
116
+ }
117
+ function commentsNewestFirst(comments) {
118
+ const withDates = comments
119
+ .map((c) => ({ c, d: typeof c.createdAt === 'string' ? c.createdAt : null }))
120
+ .filter((x) => x.d !== null);
121
+ if (withDates.length === 0)
122
+ return comments;
123
+ return [...comments].sort((a, b) => {
124
+ const da = typeof a.createdAt === 'string' ? a.createdAt : '';
125
+ const db = typeof b.createdAt === 'string' ? b.createdAt : '';
126
+ return compareIsoDesc(da, db);
127
+ });
128
+ }
129
+ /**
130
+ * Find the real `*.preview.seqholdings.com` alias the Vercel bot posts on the
131
+ * PR. This is the ONLY reliable way to get the host for a long (hash-truncated)
132
+ * slug — we can't compute it. Returns the newest trusted match or null.
133
+ */
134
+ export function findPreviewUrlInComments(comments, { expectedHost, requireTrustedAuthor = true, } = {}) {
135
+ if (!comments)
136
+ return null;
137
+ const ordered = commentsNewestFirst(comments);
138
+ const expected = expectedHost?.toLowerCase();
139
+ const re = /https:\/\/[a-z0-9-]+\.preview\.seqholdings\.com/gi;
140
+ const labelPrefix = LABEL_PREFIX.toLowerCase();
141
+ const isTrusted = (comment) => TRUSTED_PREVIEW_COMMENT_AUTHORS.has(comment.author?.login ?? '');
142
+ const search = (predicate) => {
143
+ for (const comment of ordered) {
144
+ if (!comment?.body)
145
+ continue;
146
+ if (!predicate(comment))
147
+ continue;
148
+ for (const match of comment.body.matchAll(re)) {
149
+ const url = match[0];
150
+ if (!url)
151
+ continue;
152
+ let host;
153
+ try {
154
+ host = new URL(url).host.toLowerCase();
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ if (!host.endsWith(`.${PREVIEW_DOMAIN}`))
160
+ continue;
161
+ if (!host.startsWith(labelPrefix))
162
+ continue;
163
+ if (expected && host !== expected)
164
+ continue;
165
+ return url;
166
+ }
167
+ }
168
+ return null;
169
+ };
170
+ if (requireTrustedAuthor) {
171
+ return search(isTrusted);
172
+ }
173
+ return search(() => true);
174
+ }
175
+ async function tryResolvePreviewUrlFromDeployments({ runGh, headSha, }) {
176
+ if (!headSha)
177
+ return null;
178
+ const repoStdout = await runGh(['repo', 'view', '--json', 'nameWithOwner']);
179
+ let repo;
180
+ try {
181
+ repo = JSON.parse(repoStdout);
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ const nameWithOwner = repo.nameWithOwner;
187
+ if (!nameWithOwner)
188
+ return null;
189
+ const deploymentsStdout = await runGh([
190
+ 'api',
191
+ `repos/${nameWithOwner}/deployments?sha=${encodeURIComponent(headSha)}&per_page=100`,
192
+ ]);
193
+ let deployments;
194
+ try {
195
+ deployments = JSON.parse(deploymentsStdout);
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ const previewDeployments = deployments
201
+ .filter((d) => typeof d.id === 'number' && /preview/i.test(d.environment ?? ''))
202
+ .sort((a, b) => compareIsoDesc(a.created_at ?? '', b.created_at ?? ''));
203
+ const chosen = previewDeployments[0];
204
+ if (!chosen?.id)
205
+ return null;
206
+ const statusesStdout = await runGh([
207
+ 'api',
208
+ `repos/${nameWithOwner}/deployments/${chosen.id}/statuses?per_page=100`,
209
+ ]);
210
+ let statuses;
211
+ try {
212
+ statuses = JSON.parse(statusesStdout);
213
+ }
214
+ catch {
215
+ return null;
216
+ }
217
+ const candidates = statuses
218
+ .filter((s) => typeof s.environment_url === 'string' && s.environment_url.length > 0)
219
+ .sort((a, b) => compareIsoDesc(a.created_at ?? '', b.created_at ?? ''));
220
+ const isPreviewHost = (url) => {
221
+ try {
222
+ const host = new URL(url).hostname.toLowerCase();
223
+ return host.endsWith(`.${PREVIEW_DOMAIN}`) && host.startsWith(LABEL_PREFIX.toLowerCase());
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ };
229
+ const success = candidates.find((s) => s.state === 'success' && isPreviewHost(s.environment_url));
230
+ if (success?.environment_url)
231
+ return success.environment_url;
232
+ const any = candidates.find((s) => isPreviewHost(s.environment_url));
233
+ return any?.environment_url ?? null;
234
+ }
235
+ /**
236
+ * Resolve a preview env from a PR number. Reads the PR's head branch (and the
237
+ * Vercel bot's preview-URL comment) via `gh`. Prefers the real alias from the
238
+ * comment — which works even for long, hash-truncated slugs — and falls back to
239
+ * computing the host from the branch slug for short slugs. Throws for protected
240
+ * slugs and for long slugs with no resolvable alias.
241
+ */
242
+ export async function resolvePreviewByPr({ pr, runGh = defaultGhRunner, }) {
243
+ if (!Number.isInteger(pr) || pr <= 0) {
244
+ throw new Error(`--pr must be a positive integer (got "${pr}").`);
245
+ }
246
+ const stdout = await runGh([
247
+ 'pr',
248
+ 'view',
249
+ String(pr),
250
+ '--json',
251
+ 'headRefName,headRefOid,comments',
252
+ ]);
253
+ let parsed;
254
+ try {
255
+ parsed = JSON.parse(stdout);
256
+ }
257
+ catch {
258
+ throw new Error(`Could not parse \`gh pr view ${pr}\` output as JSON.`);
259
+ }
260
+ const branch = parsed.headRefName;
261
+ if (!branch) {
262
+ throw new Error(`Could not read the head branch for PR #${pr} from gh.`);
263
+ }
264
+ const slug = previewSlug(branch);
265
+ if (isProtectedPreviewSlug(slug)) {
266
+ throw new Error(`PR #${pr} targets a protected branch slug "${slug}", which never gets a preview.`);
267
+ }
268
+ // If the slug is short, the alias is deterministic; prefer the computed host.
269
+ // This avoids accidentally targeting a stale or user-pasted preview URL.
270
+ if (isComputablePreviewSlug(slug)) {
271
+ return { slug, url: `https://${previewHost(slug)}` };
272
+ }
273
+ // Long, hash-truncated slugs require reading the real alias back from Vercel.
274
+ // Prefer deployment metadata over comment text (comments can be stale or
275
+ // user-authored).
276
+ const fromDeployment = await tryResolvePreviewUrlFromDeployments({
277
+ runGh,
278
+ headSha: parsed.headRefOid ?? '',
279
+ }).catch(() => null);
280
+ if (fromDeployment)
281
+ return { slug, url: fromDeployment };
282
+ // Fall back to the Vercel comment if deployments aren't available.
283
+ const fromComment = findPreviewUrlInComments(parsed.comments);
284
+ if (fromComment)
285
+ return { slug, url: fromComment };
286
+ // No trusted Vercel comment yet — error loudly and ask for an explicit URL.
287
+ throw new Error(`PR #${pr} (branch "${branch}", slug "${slug}") has a long, ` +
288
+ `hash-truncated alias and no preview URL was found in the PR comments ` +
289
+ `yet. Wait for the Vercel "Visit Preview" comment, or pass ` +
290
+ `--env-url <https://…preview.seqholdings.com> directly.`);
291
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Online agent loader for lint's "agent output schema matches
3
+ * outgoing_edges" check. Hits `GET /api/agents/agents/:id` on the
4
+ * resolved env (instead of the previous Mongo `findAgentById` path, which
5
+ * required an Atlas tsx runtime).
6
+ *
7
+ * Outcomes are distinguished so lint can react proportionally: a
8
+ * definitive 404 returns 'not-found' (hard error — a misspelled or
9
+ * unsynced agent id must not pass apply), an existing agent without an
10
+ * edge_id enum returns null (accepted — the generic-agent contract,
11
+ * validated at runtime), and transport failures throw (lint degrades to
12
+ * a warning so seq-studio keeps working without network access).
13
+ */
14
+ export type AgentSchemaLookup = string[] | null | 'not-found';
15
+ export declare function buildAgentSchemaLoader({ baseUrl, token, }: {
16
+ baseUrl: string;
17
+ token: string;
18
+ }): (agentId: string) => Promise<AgentSchemaLookup>;
19
+ /**
20
+ * Walk a JSON Schema looking for an `edge_id` property whose constraint
21
+ * is an enum of strings. Recurses through `anyOf` / `oneOf` / `allOf`.
22
+ * Returns null if the schema has no such enum (or it isn't strings).
23
+ */
24
+ export declare function findEdgeIdEnumInSchema(schema: unknown): string[] | null;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Online agent loader for lint's "agent output schema matches
3
+ * outgoing_edges" check. Hits `GET /api/agents/agents/:id` on the
4
+ * resolved env (instead of the previous Mongo `findAgentById` path, which
5
+ * required an Atlas tsx runtime).
6
+ *
7
+ * Outcomes are distinguished so lint can react proportionally: a
8
+ * definitive 404 returns 'not-found' (hard error — a misspelled or
9
+ * unsynced agent id must not pass apply), an existing agent without an
10
+ * edge_id enum returns null (accepted — the generic-agent contract,
11
+ * validated at runtime), and transport failures throw (lint degrades to
12
+ * a warning so seq-studio keeps working without network access).
13
+ */
14
+ import { getJsonOr404 } from '../atlas-client.js';
15
+ export function buildAgentSchemaLoader({ baseUrl, token, }) {
16
+ return async (agentId) => {
17
+ const agent = await getJsonOr404({
18
+ baseUrl,
19
+ token,
20
+ path: `/api/agents/agents/${encodeURIComponent(agentId)}`,
21
+ });
22
+ if (!agent)
23
+ return 'not-found';
24
+ const schema = agent.structuredOutput?.schema;
25
+ if (!schema)
26
+ return null;
27
+ return findEdgeIdEnumInSchema(schema);
28
+ };
29
+ }
30
+ /**
31
+ * Walk a JSON Schema looking for an `edge_id` property whose constraint
32
+ * is an enum of strings. Recurses through `anyOf` / `oneOf` / `allOf`.
33
+ * Returns null if the schema has no such enum (or it isn't strings).
34
+ */
35
+ export function findEdgeIdEnumInSchema(schema) {
36
+ if (!schema || typeof schema !== 'object')
37
+ return null;
38
+ const s = schema;
39
+ const props = s['properties'];
40
+ if (props && typeof props === 'object') {
41
+ const edgeIdSchema = props['edge_id'];
42
+ if (edgeIdSchema && Array.isArray(edgeIdSchema['enum'])) {
43
+ return edgeIdSchema['enum'].filter((v) => typeof v === 'string');
44
+ }
45
+ }
46
+ for (const key of ['anyOf', 'oneOf', 'allOf']) {
47
+ const arr = s[key];
48
+ if (Array.isArray(arr)) {
49
+ for (const sub of arr) {
50
+ const found = findEdgeIdEnumInSchema(sub);
51
+ if (found)
52
+ return found;
53
+ }
54
+ }
55
+ }
56
+ return null;
57
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Bundle builder. Produces the canonical `bundle_hash` for a process
3
+ * source tree.
4
+ */
5
+ import { type LatticeBundle, type LatticeBundleSummary } from '@sequenceholdings/lattice/bundle';
6
+ import type { LoadedProcess } from './discover.js';
7
+ export type ResolveProcessPin = (processId: string, version?: string) => Promise<{
8
+ version: string;
9
+ bundleHash: string;
10
+ } | null>;
11
+ export declare function buildBundleFromProcesses(defs: readonly LoadedProcess[], options?: {
12
+ resolveProcessPin?: ResolveProcessPin;
13
+ }): Promise<LatticeBundle>;
14
+ export declare function summarizeBundle(bundle: LatticeBundle): LatticeBundleSummary;