@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.
- package/README.md +258 -0
- package/dist/artifact/delegate.d.ts +25 -0
- package/dist/artifact/delegate.js +263 -0
- package/dist/atlas-client.d.ts +44 -0
- package/dist/atlas-client.js +173 -0
- package/dist/auth-cmds/commands.d.ts +15 -0
- package/dist/auth-cmds/commands.js +249 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.js +171 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/cli-errors.d.ts +5 -0
- package/dist/cli-errors.js +78 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +103 -0
- package/dist/env-flags.d.ts +8 -0
- package/dist/env-flags.js +47 -0
- package/dist/functions/bundle.d.ts +30 -0
- package/dist/functions/bundle.js +137 -0
- package/dist/functions/commands.d.ts +86 -0
- package/dist/functions/commands.js +999 -0
- package/dist/functions/egress-preview.d.ts +32 -0
- package/dist/functions/egress-preview.js +54 -0
- package/dist/functions/lockfile-origin.d.ts +16 -0
- package/dist/functions/lockfile-origin.js +45 -0
- package/dist/functions/manifest.d.ts +89 -0
- package/dist/functions/manifest.js +586 -0
- package/dist/functions/secret-reconcile.d.ts +79 -0
- package/dist/functions/secret-reconcile.js +86 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.js +129 -0
- package/dist/orm/delegate.d.ts +8 -0
- package/dist/orm/delegate.js +61 -0
- package/dist/pat-hints.d.ts +17 -0
- package/dist/pat-hints.js +28 -0
- package/dist/preview.d.ts +89 -0
- package/dist/preview.js +291 -0
- package/dist/process/agent-loader.d.ts +24 -0
- package/dist/process/agent-loader.js +57 -0
- package/dist/process/build.d.ts +14 -0
- package/dist/process/build.js +368 -0
- package/dist/process/codegen.d.ts +18 -0
- package/dist/process/codegen.js +270 -0
- package/dist/process/commands.d.ts +47 -0
- package/dist/process/commands.js +786 -0
- package/dist/process/discover.d.ts +32 -0
- package/dist/process/discover.js +131 -0
- package/dist/process/lint.d.ts +39 -0
- package/dist/process/lint.js +485 -0
- package/dist/process/local-bundle.d.ts +17 -0
- package/dist/process/local-bundle.js +65 -0
- package/dist/process/plan-diff.d.ts +82 -0
- package/dist/process/plan-diff.js +333 -0
- package/dist/process/resolve-process-pin.d.ts +11 -0
- package/dist/process/resolve-process-pin.js +63 -0
- package/dist/process/simulate.d.ts +50 -0
- package/dist/process/simulate.js +328 -0
- package/dist/prompt.d.ts +35 -0
- package/dist/prompt.js +65 -0
- package/dist/repos/commands.d.ts +49 -0
- package/dist/repos/commands.js +548 -0
- package/dist/repos/git-clone.d.ts +10 -0
- package/dist/repos/git-clone.js +49 -0
- package/dist/secrets/commands.d.ts +24 -0
- package/dist/secrets/commands.js +704 -0
- package/dist/templates/process/example-process/process.ts +43 -0
- package/dist/templates/process/package.json +23 -0
- package/dist/templates/process/pnpm-workspace.yaml +21 -0
- package/dist/templates/process/tsconfig.json +17 -0
- package/package.json +78 -0
- package/templates/process/example-process/process.ts +43 -0
- package/templates/process/package.json +23 -0
- package/templates/process/pnpm-workspace.yaml +21 -0
- package/templates/process/tsconfig.json +17 -0
package/dist/preview.js
ADDED
|
@@ -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;
|