@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
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seq-studio process * commands. Each command is a small function with
|
|
3
|
+
* its own argv slice, called by the argv router in main.ts. Network I/O
|
|
4
|
+
* happens through `atlas-client.ts` and `getAccessToken()`; commands
|
|
5
|
+
* themselves do not know about the token file or config TOML.
|
|
6
|
+
*/
|
|
7
|
+
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
|
|
12
|
+
import { getJson, getJsonOr404, postJson, AtlasApiError } from '../atlas-client.js';
|
|
13
|
+
import { generateProcessFiles } from './codegen.js';
|
|
14
|
+
import { getAccessToken, NotLoggedInError } from '../auth.js';
|
|
15
|
+
import { ENV_NAMES, readConfig, resolveEnv } from '../config.js';
|
|
16
|
+
import { buildBundleFromProcesses, summarizeBundle } from './build.js';
|
|
17
|
+
import { buildResolveProcessPinFromEnv } from './resolve-process-pin.js';
|
|
18
|
+
import { loadBundleForPublish } from './local-bundle.js';
|
|
19
|
+
import { loadProcessDefinitions } from './discover.js';
|
|
20
|
+
import { lintProcesses, formatLintResult } from './lint.js';
|
|
21
|
+
import { diffBundleAgainstActive, formatPlanDiff, } from './plan-diff.js';
|
|
22
|
+
import { simulateProcess, formatSimulateResult } from './simulate.js';
|
|
23
|
+
import { buildAgentSchemaLoader } from './agent-loader.js';
|
|
24
|
+
export function parseArgs(rest) {
|
|
25
|
+
const positional = [];
|
|
26
|
+
const flags = {};
|
|
27
|
+
for (let i = 0; i < rest.length; i++) {
|
|
28
|
+
const arg = rest[i];
|
|
29
|
+
if (arg === '--') {
|
|
30
|
+
positional.push(...rest.slice(i + 1));
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (arg.startsWith('--')) {
|
|
34
|
+
const body = arg.slice(2);
|
|
35
|
+
// Accept `--key=value` (POSIX-style). Without this, `--env=staging`
|
|
36
|
+
// would land in flags as `env=staging` → true and resolveEnv
|
|
37
|
+
// would fall back to default_env, silently deploying to the
|
|
38
|
+
// wrong target.
|
|
39
|
+
const eqIdx = body.indexOf('=');
|
|
40
|
+
if (eqIdx >= 0) {
|
|
41
|
+
flags[body.slice(0, eqIdx)] = body.slice(eqIdx + 1);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const key = body;
|
|
45
|
+
const next = rest[i + 1];
|
|
46
|
+
// Same sentinel as the short-flag branch below: any leading `-`
|
|
47
|
+
// is another flag, not a value. Without this `--out -e staging`
|
|
48
|
+
// would consume `-e` as the value of `--out` and silently swallow
|
|
49
|
+
// the `-e` flag.
|
|
50
|
+
if (next && !next.startsWith('-')) {
|
|
51
|
+
flags[key] = next;
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
flags[key] = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (arg.startsWith('-') && arg.length >= 2 && !arg.startsWith('--')) {
|
|
59
|
+
// single-letter shorthand: -e, -o, -e=foo, -o=path/to/file
|
|
60
|
+
// Without supporting the `-e=value` form, a user running
|
|
61
|
+
// `seq-studio process apply -e=production` would silently fall
|
|
62
|
+
// back to the configured default env and deploy to local.
|
|
63
|
+
const eqIdx = arg.indexOf('=');
|
|
64
|
+
const shortName = eqIdx >= 0 ? arg.slice(0, eqIdx) : arg;
|
|
65
|
+
const longName = SHORT_FLAG_ALIASES[shortName];
|
|
66
|
+
if (!longName || shortName.length !== 2) {
|
|
67
|
+
positional.push(arg);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (eqIdx >= 0) {
|
|
71
|
+
flags[longName] = arg.slice(eqIdx + 1);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const next = rest[i + 1];
|
|
75
|
+
if (next && !next.startsWith('-')) {
|
|
76
|
+
flags[longName] = next;
|
|
77
|
+
i += 1;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
flags[longName] = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
positional.push(arg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { positional, flags };
|
|
88
|
+
}
|
|
89
|
+
const SHORT_FLAG_ALIASES = {
|
|
90
|
+
'-e': 'env',
|
|
91
|
+
'-m': 'message',
|
|
92
|
+
'-o': 'out',
|
|
93
|
+
};
|
|
94
|
+
async function getEnvAndToken(args) {
|
|
95
|
+
const config = await readConfig();
|
|
96
|
+
const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
97
|
+
const env = resolveEnv({ config, requested });
|
|
98
|
+
let token;
|
|
99
|
+
try {
|
|
100
|
+
token = await getAccessToken();
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (err instanceof NotLoggedInError) {
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
return { env, token };
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// init
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
export async function initCommand(args) {
|
|
114
|
+
const target = args.positional[0];
|
|
115
|
+
if (!target) {
|
|
116
|
+
console.error('usage: seq-studio process init <name-or-dir>');
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
const dir = resolve(target);
|
|
120
|
+
const name = dir.split('/').filter(Boolean).at(-1) ?? 'my-process';
|
|
121
|
+
if (existsSync(dir)) {
|
|
122
|
+
console.error(`${dir} already exists`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
126
|
+
// After build, templates/ is at the package root, two levels up from
|
|
127
|
+
// dist/process/commands.js. During tsx dev runs, two levels up from
|
|
128
|
+
// src/process/commands.ts lands at the package root too.
|
|
129
|
+
const candidates = [
|
|
130
|
+
resolve(moduleDir, '..', '..', 'templates', 'process'),
|
|
131
|
+
resolve(moduleDir, '..', 'templates', 'process'),
|
|
132
|
+
];
|
|
133
|
+
const templateRoot = candidates.find((p) => existsSync(p));
|
|
134
|
+
if (!templateRoot) {
|
|
135
|
+
console.error('seq-studio install is missing templates/process/');
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
const slug = slugify(name);
|
|
139
|
+
await mkdir(dir, { recursive: true });
|
|
140
|
+
await cp(templateRoot, dir, { recursive: true });
|
|
141
|
+
// Rename the placeholder per-process folder to the user's slug so
|
|
142
|
+
// `<slug>/process.ts` matches the cwd discovery contract.
|
|
143
|
+
const placeholderDir = join(dir, 'example-process');
|
|
144
|
+
const targetDir = join(dir, slug);
|
|
145
|
+
if (existsSync(placeholderDir) && placeholderDir !== targetDir) {
|
|
146
|
+
const { rename } = await import('node:fs/promises');
|
|
147
|
+
await rename(placeholderDir, targetDir);
|
|
148
|
+
}
|
|
149
|
+
for (const relPath of ['package.json', join(slug, 'process.ts')]) {
|
|
150
|
+
const path = join(dir, relPath);
|
|
151
|
+
if (existsSync(path)) {
|
|
152
|
+
const body = (await readFile(path, 'utf8'))
|
|
153
|
+
.replace(/\{\{name\}\}/g, name)
|
|
154
|
+
.replace(/\{\{slug\}\}/g, slug);
|
|
155
|
+
await writeFile(path, body, 'utf8');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
console.log(`[seq-studio] scaffolded process repo at ${relative(process.cwd(), dir)}`);
|
|
159
|
+
console.log('Next:');
|
|
160
|
+
// pnpm is recommended over npm so the scaffolded
|
|
161
|
+
// pnpm-workspace.yaml's `minimumReleaseAge: 10080` supply-chain
|
|
162
|
+
// gate actually takes effect (npm has no equivalent setting).
|
|
163
|
+
console.log(` cd ${relative(process.cwd(), dir) || '.'} && pnpm install`);
|
|
164
|
+
console.log(' seqapi login');
|
|
165
|
+
console.log(' seq-studio process plan -e local');
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
function slugify(value) {
|
|
169
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'process';
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// lint
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
export async function lintCommand(args) {
|
|
175
|
+
const defs = await loadProcessDefinitions();
|
|
176
|
+
const loadAgentSchemaEdgeIdEnum = await tryBuildAgentLoader(args);
|
|
177
|
+
const result = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
|
|
178
|
+
const formatted = formatLintResult(result);
|
|
179
|
+
if (formatted.text)
|
|
180
|
+
console.log(formatted.text);
|
|
181
|
+
if (formatted.hasErrors) {
|
|
182
|
+
console.error(`\nlint FAILED — ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
console.log(`\nlint ok — ${defs.length} process(es) checked, ${result.warnings.length} warning(s)`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
async function tryBuildAgentLoader(args) {
|
|
189
|
+
// Agent loader is only meaningful when we have a target env + token.
|
|
190
|
+
// If either fails to resolve, skip the check; lint will emit a warning
|
|
191
|
+
// for multi-edge agent nodes instead of failing.
|
|
192
|
+
try {
|
|
193
|
+
const { env, token } = await getEnvAndToken(args);
|
|
194
|
+
return buildAgentSchemaLoader({ baseUrl: env.url, token });
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// plan
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
async function buildBundleForPublish(args, defs) {
|
|
204
|
+
const hasSubprocess = defs.some((d) => d.process.nodes.some((n) => n.kind === 'subprocess'));
|
|
205
|
+
try {
|
|
206
|
+
const { env, token } = await getEnvAndToken(args);
|
|
207
|
+
const resolveProcessPin = buildResolveProcessPinFromEnv(env, token);
|
|
208
|
+
return await buildBundleFromProcesses(defs, { resolveProcessPin });
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (hasSubprocess) {
|
|
212
|
+
throw new Error('subprocess nodes require -e <env> and `seqapi login` to resolve child process versions', { cause: err });
|
|
213
|
+
}
|
|
214
|
+
return await buildBundleFromProcesses(defs);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export async function planCommand(args) {
|
|
218
|
+
const defs = await loadProcessDefinitions();
|
|
219
|
+
const bundle = await buildBundleForPublish(args, defs);
|
|
220
|
+
const summary = summarizeBundle(bundle);
|
|
221
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
222
|
+
const loadActiveProcess = await tryBuildActiveProcessLoader(args);
|
|
223
|
+
if (!loadActiveProcess) {
|
|
224
|
+
console.log('\n(no diff — env or auth unavailable; pass --env <name> and run `seqapi login`)');
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
const diff = await diffBundleAgainstActive({ newBundle: bundle, loadActiveProcess });
|
|
228
|
+
console.log('\n--- diff against active ---');
|
|
229
|
+
console.log(formatPlanDiff(diff));
|
|
230
|
+
if (diff.has_breaking_change) {
|
|
231
|
+
console.log('\nthis plan includes BREAKING changes for new runs (in-flight runs are pinned and unaffected)');
|
|
232
|
+
}
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
async function tryBuildActiveProcessLoader(args) {
|
|
236
|
+
try {
|
|
237
|
+
const { env, token } = await getEnvAndToken(args);
|
|
238
|
+
return async (processId) => {
|
|
239
|
+
// GET /api/lattice/processes/:id returns
|
|
240
|
+
// `{ process_id, activeVersion, activeBundleHash }` flat at the
|
|
241
|
+
// top level — NOT a nested `{ process: { ... } }`, NOT snake_case.
|
|
242
|
+
// See atlas/src/app/api/lattice/processes/[processId]/route.ts.
|
|
243
|
+
const meta = await getJsonOr404({
|
|
244
|
+
baseUrl: env.url,
|
|
245
|
+
token,
|
|
246
|
+
path: `/api/lattice/processes/${encodeURIComponent(processId)}`,
|
|
247
|
+
});
|
|
248
|
+
if (!meta?.activeVersion || !meta.activeBundleHash)
|
|
249
|
+
return null;
|
|
250
|
+
const bundleData = await getJsonOr404({
|
|
251
|
+
baseUrl: env.url,
|
|
252
|
+
token,
|
|
253
|
+
path: `/api/lattice/bundles/${encodeURIComponent(meta.activeBundleHash)}`,
|
|
254
|
+
});
|
|
255
|
+
if (!bundleData?.bundle)
|
|
256
|
+
return null;
|
|
257
|
+
const proc = bundleData.bundle.processes.find((p) => p.id === processId);
|
|
258
|
+
if (!proc)
|
|
259
|
+
return null;
|
|
260
|
+
return { version: meta.activeVersion, process: proc };
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// test (CI wrapper)
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
export async function testCommand(args) {
|
|
271
|
+
const defs = await loadProcessDefinitions();
|
|
272
|
+
const loadAgentSchemaEdgeIdEnum = await tryBuildAgentLoader(args);
|
|
273
|
+
const lintResult = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
|
|
274
|
+
const formattedLint = formatLintResult(lintResult);
|
|
275
|
+
if (formattedLint.text)
|
|
276
|
+
console.log(formattedLint.text);
|
|
277
|
+
if (formattedLint.hasErrors) {
|
|
278
|
+
console.error(`\nlint FAILED — ${lintResult.errors.length} error(s), ${lintResult.warnings.length} warning(s)`);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
console.log(`\nlint ok — ${defs.length} process(es) checked, ${lintResult.warnings.length} warning(s)`);
|
|
282
|
+
const bundle = await buildBundleForPublish(args, defs);
|
|
283
|
+
const offline = args.flags.offline === true;
|
|
284
|
+
const loadActiveProcess = await tryBuildActiveProcessLoader(args);
|
|
285
|
+
if (!loadActiveProcess) {
|
|
286
|
+
// `process test` is the CI gate — silently skipping the diff would
|
|
287
|
+
// let a misconfigured CI job ship BREAKING changes as a green
|
|
288
|
+
// build. Require explicit `--offline` to opt out.
|
|
289
|
+
if (offline) {
|
|
290
|
+
console.log('\n(skipped diff — --offline flag set)');
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
console.error('\nFAIL — could not load the active version for diff (env or auth missing).\n' +
|
|
294
|
+
' Either pass -e <env> with valid seqapi auth (run `seq-studio doctor` to debug),\n' +
|
|
295
|
+
' or pass --offline if you intentionally want CI to skip the diff.');
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
const diff = await diffBundleAgainstActive({ newBundle: bundle, loadActiveProcess });
|
|
299
|
+
console.log('\n--- diff against active ---');
|
|
300
|
+
console.log(formatPlanDiff(diff));
|
|
301
|
+
if (diff.has_breaking_change) {
|
|
302
|
+
console.error('\nFAIL — plan includes BREAKING changes');
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// apply (build → register → promote)
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
/**
|
|
311
|
+
* Parse + validate the `--only <id1,id2,...>` promote filter for `apply`.
|
|
312
|
+
* The bundle is still built and registered from the full discovery root
|
|
313
|
+
* (registration is content-addressed and inert — only the promote flips
|
|
314
|
+
* active versions), so CI can target a subset of a shared root per
|
|
315
|
+
* environment. Returns `ids: null` when the flag is absent (promote all).
|
|
316
|
+
*/
|
|
317
|
+
export function resolveOnlyIds({ only, knownIds, }) {
|
|
318
|
+
if (only === undefined)
|
|
319
|
+
return { ids: null };
|
|
320
|
+
if (typeof only !== 'string' || !only.trim()) {
|
|
321
|
+
return { ids: null, error: 'usage: --only <process-id>[,<process-id>...]' };
|
|
322
|
+
}
|
|
323
|
+
const ids = new Set(only
|
|
324
|
+
.split(',')
|
|
325
|
+
.map((id) => id.trim())
|
|
326
|
+
.filter(Boolean));
|
|
327
|
+
const unknown = [...ids].filter((id) => !knownIds.has(id));
|
|
328
|
+
if (unknown.length > 0) {
|
|
329
|
+
return {
|
|
330
|
+
ids: null,
|
|
331
|
+
error: `--only names process(es) not found in this root: ${unknown.join(', ')}\n` +
|
|
332
|
+
`known: ${[...knownIds].sort().join(', ')}`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return { ids };
|
|
336
|
+
}
|
|
337
|
+
export async function applyCommand(args) {
|
|
338
|
+
const { env, token } = await getEnvAndToken(args);
|
|
339
|
+
const defs = await loadProcessDefinitions();
|
|
340
|
+
const onlyResult = resolveOnlyIds({
|
|
341
|
+
only: args.flags.only,
|
|
342
|
+
knownIds: new Set(defs.map((d) => d.process.id)),
|
|
343
|
+
});
|
|
344
|
+
if (onlyResult.error) {
|
|
345
|
+
console.error(onlyResult.error);
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
const onlyIds = onlyResult.ids;
|
|
349
|
+
// Layer-3 lint before touching the registry. Server-side lint runs
|
|
350
|
+
// again on POST /bundles; this is a local fast-fail so we don't ship
|
|
351
|
+
// a bundle that will be rejected.
|
|
352
|
+
const loadAgentSchemaEdgeIdEnum = buildAgentSchemaLoader({ baseUrl: env.url, token });
|
|
353
|
+
const lintResult = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
|
|
354
|
+
const formatted = formatLintResult(lintResult);
|
|
355
|
+
if (formatted.text)
|
|
356
|
+
console.log(formatted.text);
|
|
357
|
+
if (formatted.hasErrors) {
|
|
358
|
+
console.error('\napply FAILED — lint rejected the bundle');
|
|
359
|
+
return 1;
|
|
360
|
+
}
|
|
361
|
+
const bundle = await buildBundleForPublish(args, defs);
|
|
362
|
+
console.log(`[seq-studio] built bundle ${bundle.bundle_hash.slice(0, 12)} ` +
|
|
363
|
+
`(${bundle.processes.length} process(es))`);
|
|
364
|
+
// We send only the bundle. `created_by` (and `promoted_by` for the
|
|
365
|
+
// promote call below) are derived server-side from the authenticated
|
|
366
|
+
// user — we deliberately do NOT forward `bundle.metadata.created_by`
|
|
367
|
+
// (which is built from local `process.env.USER`) as authoritative
|
|
368
|
+
// attribution. The descriptive value still lives inside the bundle
|
|
369
|
+
// JSON for local debugging; the server-side audit log uses the JWT
|
|
370
|
+
// sub.
|
|
371
|
+
const registered = await postJson({
|
|
372
|
+
baseUrl: env.url,
|
|
373
|
+
token,
|
|
374
|
+
path: '/api/lattice/bundles',
|
|
375
|
+
body: { bundle },
|
|
376
|
+
});
|
|
377
|
+
console.log(`[seq-studio] registered bundle ${registered.bundleHash.slice(0, 12)} → ${registered.storedUri} (written=${registered.written})`);
|
|
378
|
+
for (const pv of registered.processVersions) {
|
|
379
|
+
if (onlyIds && !onlyIds.has(pv.processId)) {
|
|
380
|
+
console.log(`[seq-studio] skipped promote of ${pv.processId} (not in --only)`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
await postJson({
|
|
384
|
+
baseUrl: env.url,
|
|
385
|
+
token,
|
|
386
|
+
path: `/api/lattice/processes/${encodeURIComponent(pv.processId)}/promote`,
|
|
387
|
+
body: { version: pv.version },
|
|
388
|
+
});
|
|
389
|
+
console.log(`[seq-studio] promoted ${pv.processId} → ${pv.version}`);
|
|
390
|
+
}
|
|
391
|
+
console.log(`\napply OK against ${env.name} (${env.url})`);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// bundle build|pull
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
/**
|
|
398
|
+
* Pull a published process's source back into a local tree. Resolves the
|
|
399
|
+
* requested (or active) version to a bundle, then regenerates `process.ts`
|
|
400
|
+
* (+ `recommendations.ts`) under `<out>/<processId>/`. Pulls the process and
|
|
401
|
+
* its co-located subprocess children by default (`--no-children` for just the
|
|
402
|
+
* root). This closes the loop: apply a change in the UI → publish an inactive
|
|
403
|
+
* version → pull it here → review + commit.
|
|
404
|
+
*/
|
|
405
|
+
export async function pullCommand(args) {
|
|
406
|
+
const processId = args.positional[0];
|
|
407
|
+
if (!processId) {
|
|
408
|
+
console.error('usage: seq-studio process pull <processId> [--version <v>] [-e <env>] [--out <dir>] [--no-children]');
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
const { env, token } = await getEnvAndToken(args);
|
|
412
|
+
const version = typeof args.flags.version === 'string' ? args.flags.version : undefined;
|
|
413
|
+
const outDir = typeof args.flags.out === 'string' ? resolve(args.flags.out) : process.cwd();
|
|
414
|
+
const noChildren = args.flags['no-children'] === true;
|
|
415
|
+
// Resolve the bundle hash: a specific version, or the active pointer.
|
|
416
|
+
let bundleHash;
|
|
417
|
+
let resolvedVersion;
|
|
418
|
+
if (version) {
|
|
419
|
+
const v = await getJson({
|
|
420
|
+
baseUrl: env.url,
|
|
421
|
+
token,
|
|
422
|
+
path: `/api/lattice/processes/${encodeURIComponent(processId)}/versions/${encodeURIComponent(version)}`,
|
|
423
|
+
});
|
|
424
|
+
bundleHash = v.bundleHash;
|
|
425
|
+
resolvedVersion = version;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const meta = await getJson({
|
|
429
|
+
baseUrl: env.url,
|
|
430
|
+
token,
|
|
431
|
+
path: `/api/lattice/processes/${encodeURIComponent(processId)}`,
|
|
432
|
+
});
|
|
433
|
+
bundleHash = meta.activeBundleHash;
|
|
434
|
+
resolvedVersion = meta.activeVersion;
|
|
435
|
+
}
|
|
436
|
+
const { bundle } = await getJson({
|
|
437
|
+
baseUrl: env.url,
|
|
438
|
+
token,
|
|
439
|
+
path: `/api/lattice/bundles/${encodeURIComponent(bundleHash)}`,
|
|
440
|
+
});
|
|
441
|
+
const root = bundle.processes.find((p) => p.id === processId);
|
|
442
|
+
if (!root) {
|
|
443
|
+
console.error(`[seq-studio] process "${processId}" not found in bundle ${resolvedVersion}`);
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
446
|
+
const targets = noChildren ? [root] : reachableProcesses(root, bundle);
|
|
447
|
+
for (const proc of targets) {
|
|
448
|
+
const dir = join(outDir, proc.id);
|
|
449
|
+
await mkdir(dir, { recursive: true });
|
|
450
|
+
const files = generateProcessFiles(proc, bundle);
|
|
451
|
+
for (const [name, content] of Object.entries(files)) {
|
|
452
|
+
await writeFile(join(dir, name), content, 'utf8');
|
|
453
|
+
}
|
|
454
|
+
// This version has no recommendations — clear a stale file from a prior pull.
|
|
455
|
+
if (!files['recommendations.ts'])
|
|
456
|
+
await rm(join(dir, 'recommendations.ts'), { force: true });
|
|
457
|
+
console.log(`[seq-studio] wrote ${relative(process.cwd(), join(dir, 'process.ts'))}`);
|
|
458
|
+
}
|
|
459
|
+
console.log(`[seq-studio] pulled ${targets.length} process(es) at version ${resolvedVersion}`);
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
/** A process + its transitive co-located subprocess children (those bundled here). */
|
|
463
|
+
function reachableProcesses(root, bundle) {
|
|
464
|
+
const byId = new Map(bundle.processes.map((p) => [p.id, p]));
|
|
465
|
+
const seen = new Set();
|
|
466
|
+
const out = [];
|
|
467
|
+
const visit = (proc) => {
|
|
468
|
+
if (seen.has(proc.id))
|
|
469
|
+
return;
|
|
470
|
+
seen.add(proc.id);
|
|
471
|
+
out.push(proc);
|
|
472
|
+
forEachSerializedNode(proc.nodes, (node) => {
|
|
473
|
+
if (node.kind !== 'subprocess')
|
|
474
|
+
return;
|
|
475
|
+
const childId = node.metadata.subprocess
|
|
476
|
+
?.process_id;
|
|
477
|
+
const child = childId ? byId.get(childId) : undefined;
|
|
478
|
+
if (child)
|
|
479
|
+
visit(child);
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
visit(root);
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Make a published version the active one — the same promote `apply` runs, as a
|
|
487
|
+
* standalone command (and the same endpoint the UI version menu hits).
|
|
488
|
+
*/
|
|
489
|
+
export async function promoteCommand(args) {
|
|
490
|
+
const processId = args.positional[0];
|
|
491
|
+
const version = typeof args.flags.version === 'string' ? args.flags.version : undefined;
|
|
492
|
+
if (!processId || !version) {
|
|
493
|
+
console.error('usage: seq-studio process promote <processId> --version <v> [-e <env>]');
|
|
494
|
+
return 1;
|
|
495
|
+
}
|
|
496
|
+
const { env, token } = await getEnvAndToken(args);
|
|
497
|
+
const result = await postJson({
|
|
498
|
+
baseUrl: env.url,
|
|
499
|
+
token,
|
|
500
|
+
path: `/api/lattice/processes/${encodeURIComponent(processId)}/promote`,
|
|
501
|
+
body: { version },
|
|
502
|
+
});
|
|
503
|
+
console.log(`[seq-studio] promoted ${result.process_id} → ${result.active_version}`);
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
export async function bundleCommand(args) {
|
|
507
|
+
const sub = args.positional[0];
|
|
508
|
+
switch (sub) {
|
|
509
|
+
case 'build': {
|
|
510
|
+
const defs = await loadProcessDefinitions();
|
|
511
|
+
const bundle = await buildBundleForPublish(args, defs);
|
|
512
|
+
const out = typeof args.flags.out === 'string' ? resolve(args.flags.out) : null;
|
|
513
|
+
if (out) {
|
|
514
|
+
await mkdir(dirname(out), { recursive: true });
|
|
515
|
+
await writeFile(out, JSON.stringify(bundle, null, 2), 'utf8');
|
|
516
|
+
console.log(`[seq-studio] wrote ${out}`);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
console.log(JSON.stringify(summarizeBundle(bundle), null, 2));
|
|
520
|
+
}
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
case 'pull': {
|
|
524
|
+
const hash = args.positional[1];
|
|
525
|
+
if (!hash) {
|
|
526
|
+
console.error('usage: seq-studio process bundle pull <hash> [-o file.json] [-e <env>]');
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
const { env, token } = await getEnvAndToken(args);
|
|
530
|
+
const data = await getJson({
|
|
531
|
+
baseUrl: env.url,
|
|
532
|
+
token,
|
|
533
|
+
path: `/api/lattice/bundles/${encodeURIComponent(hash)}`,
|
|
534
|
+
});
|
|
535
|
+
const body = JSON.stringify(data.bundle, null, 2);
|
|
536
|
+
const out = typeof args.flags.out === 'string' ? resolve(args.flags.out) : null;
|
|
537
|
+
if (out) {
|
|
538
|
+
await mkdir(dirname(out), { recursive: true });
|
|
539
|
+
await writeFile(out, body, 'utf8');
|
|
540
|
+
console.log(`[seq-studio] wrote ${out} (${body.length} bytes)`);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
process.stdout.write(body);
|
|
544
|
+
if (process.stdout.isTTY)
|
|
545
|
+
process.stdout.write('\n');
|
|
546
|
+
}
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
case 'archive': {
|
|
550
|
+
const hash = args.positional[1];
|
|
551
|
+
if (!hash) {
|
|
552
|
+
console.error('usage: seq-studio process bundle archive <hash> [-e <env>]');
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
const { env, token } = await getEnvAndToken(args);
|
|
556
|
+
const result = await postJson({
|
|
557
|
+
baseUrl: env.url,
|
|
558
|
+
token,
|
|
559
|
+
path: `/api/lattice/bundles/${encodeURIComponent(hash)}/archive`,
|
|
560
|
+
body: {},
|
|
561
|
+
});
|
|
562
|
+
console.log(result.archived
|
|
563
|
+
? `[seq-studio] archived bundle ${hash} (${result.processVersions.length} process version(s) hidden)`
|
|
564
|
+
: `[seq-studio] bundle ${hash} was not present (no-op)`);
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
case 'inspect': {
|
|
568
|
+
const path = args.positional[1];
|
|
569
|
+
if (!path) {
|
|
570
|
+
console.error('usage: seq-studio process bundle inspect <bundle.json>');
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
const body = await readFile(path, 'utf8');
|
|
574
|
+
const bundle = JSON.parse(body);
|
|
575
|
+
console.log(JSON.stringify(summarizeBundle(bundle), null, 2));
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
case 'list': {
|
|
579
|
+
const { env, token } = await getEnvAndToken(args);
|
|
580
|
+
const pageLimit = typeof args.flags.limit === 'string'
|
|
581
|
+
? Math.min(Math.max(Number.parseInt(args.flags.limit, 10) || 100, 1), 500)
|
|
582
|
+
: 500;
|
|
583
|
+
let cursor = typeof args.flags.cursor === 'string' ? args.flags.cursor : undefined;
|
|
584
|
+
let pages = 0;
|
|
585
|
+
const maxPages = 100;
|
|
586
|
+
do {
|
|
587
|
+
pages += 1;
|
|
588
|
+
if (pages > maxPages) {
|
|
589
|
+
console.error(`[seq-studio] bundle list stopped after ${maxPages} pages — use --cursor to continue`);
|
|
590
|
+
return 1;
|
|
591
|
+
}
|
|
592
|
+
const params = new URLSearchParams({ limit: String(pageLimit) });
|
|
593
|
+
if (cursor)
|
|
594
|
+
params.set('cursor', cursor);
|
|
595
|
+
const data = await getJson({
|
|
596
|
+
baseUrl: env.url,
|
|
597
|
+
token,
|
|
598
|
+
path: `/api/lattice/bundles?${params.toString()}`,
|
|
599
|
+
});
|
|
600
|
+
for (const entry of data.bundles) {
|
|
601
|
+
console.log(`${entry.bundleHash}\t${entry.uri}`);
|
|
602
|
+
}
|
|
603
|
+
cursor = data.next_cursor ?? undefined;
|
|
604
|
+
} while (cursor);
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
case 'publish': {
|
|
608
|
+
const hashOrPath = args.positional[1];
|
|
609
|
+
if (!hashOrPath) {
|
|
610
|
+
console.error('usage: seq-studio process bundle publish <hash|bundle.json> [-e <env>]');
|
|
611
|
+
return 1;
|
|
612
|
+
}
|
|
613
|
+
const { env, token } = await getEnvAndToken(args);
|
|
614
|
+
let bundle;
|
|
615
|
+
try {
|
|
616
|
+
bundle = await loadBundleForPublish({ hashOrPath });
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
620
|
+
return 1;
|
|
621
|
+
}
|
|
622
|
+
const registered = await postJson({
|
|
623
|
+
baseUrl: env.url,
|
|
624
|
+
token,
|
|
625
|
+
path: '/api/lattice/bundles',
|
|
626
|
+
body: { bundle },
|
|
627
|
+
});
|
|
628
|
+
console.log(`[seq-studio] published ${registered.bundleHash.slice(0, 12)} → ${registered.storedUri} (written=${registered.written})`);
|
|
629
|
+
console.log(` registered ${registered.processVersions.length} process version(s); run process apply to promote`);
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
default:
|
|
633
|
+
console.error('usage: seq-studio process bundle build|pull|inspect|list|publish ...');
|
|
634
|
+
return 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// simulate
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
export async function simulateCommand(args) {
|
|
641
|
+
const processId = args.positional[0];
|
|
642
|
+
if (!processId) {
|
|
643
|
+
console.error('usage: seq-studio process simulate <process-id>');
|
|
644
|
+
return 1;
|
|
645
|
+
}
|
|
646
|
+
const defs = await loadProcessDefinitions();
|
|
647
|
+
const def = defs.find((d) => d.process.id === processId);
|
|
648
|
+
if (!def) {
|
|
649
|
+
console.error(`process "${processId}" not found in lattice process roots`);
|
|
650
|
+
return 1;
|
|
651
|
+
}
|
|
652
|
+
const processesById = new Map(defs.map((d) => [d.process.id, d.process]));
|
|
653
|
+
const result = await simulateProcess({ process: def.process, processesById });
|
|
654
|
+
console.log(formatSimulateResult(result));
|
|
655
|
+
return result.status === 'failed' ? 1 : 0;
|
|
656
|
+
}
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
// doctor (token + env + FGA gate)
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
export async function doctorCommand(args) {
|
|
661
|
+
const lines = [];
|
|
662
|
+
let ok = true;
|
|
663
|
+
const config = await readConfig().catch(() => null);
|
|
664
|
+
if (!config) {
|
|
665
|
+
lines.push('config: FAIL — could not read ~/.config/lattice/config.toml');
|
|
666
|
+
ok = false;
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
lines.push(`config: ok — default_env=${config.defaultEnv}, envs=${Object.keys(config.envs).join(',')}`);
|
|
670
|
+
}
|
|
671
|
+
const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
672
|
+
if (requested && !ENV_NAMES.includes(requested) && config && !(requested in config.envs)) {
|
|
673
|
+
lines.push(`env "${requested}": FAIL — not in built-ins (${ENV_NAMES.join(',')}) or config`);
|
|
674
|
+
ok = false;
|
|
675
|
+
}
|
|
676
|
+
let env = null;
|
|
677
|
+
if (config) {
|
|
678
|
+
try {
|
|
679
|
+
env = resolveEnv({ config, requested });
|
|
680
|
+
lines.push(`env: ok — ${env.name} (${env.url})`);
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
lines.push(`env: FAIL — ${err.message}`);
|
|
684
|
+
ok = false;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
let token = null;
|
|
688
|
+
try {
|
|
689
|
+
token = await getAccessToken();
|
|
690
|
+
lines.push('auth: ok — seqapi token present and not expired');
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
lines.push(`auth: FAIL — ${err instanceof Error ? err.message.split('\n')[0] : String(err)}`);
|
|
694
|
+
ok = false;
|
|
695
|
+
}
|
|
696
|
+
if (env && token) {
|
|
697
|
+
// Hit a writer-gated endpoint with a fake hash so the request
|
|
698
|
+
// either:
|
|
699
|
+
// - 404 NOT_FOUND → writer gate passed (we're a Sequence-org
|
|
700
|
+
// member, the hash just doesn't exist), OR
|
|
701
|
+
// - 403 FORBIDDEN → not a Sequence-org member.
|
|
702
|
+
// The GET /processes/:id endpoint uses plain `authenticate` so it
|
|
703
|
+
// cannot distinguish these cases. GET /bundles/:hash is gated by
|
|
704
|
+
// `authenticateLatticeWriter` and is the right probe.
|
|
705
|
+
// Use a syntactically-valid-but-nonexistent 64-char hex hash so the
|
|
706
|
+
// bundle store's HASH_PATTERN regex passes (atlas/lattice/src/bundle/
|
|
707
|
+
// store.ts) and we hit BundleNotFoundError → 404. A non-hex probe like
|
|
708
|
+
// "__doctor__" trips the regex first and lands in respondToError as
|
|
709
|
+
// 500 instead, which would surface as a misleading WARN.
|
|
710
|
+
const PROBE_HASH = '0'.repeat(64);
|
|
711
|
+
try {
|
|
712
|
+
await getJson({
|
|
713
|
+
baseUrl: env.url,
|
|
714
|
+
token,
|
|
715
|
+
path: `/api/lattice/bundles/${PROBE_HASH}`,
|
|
716
|
+
});
|
|
717
|
+
lines.push('writer gate: ok — Sequence-org member (probe via GET /bundles/<zeros>)');
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
if (err instanceof AtlasApiError) {
|
|
721
|
+
if (err.status === 404) {
|
|
722
|
+
lines.push('writer gate: ok — Sequence-org member (GET /bundles/<zeros> → 404 means we passed the writer gate, the probe hash is fake)');
|
|
723
|
+
}
|
|
724
|
+
else if (err.status === 403) {
|
|
725
|
+
lines.push('writer gate: FAIL — 403 from Atlas. You need to be a member of organization:sequence in OpenFGA. ' +
|
|
726
|
+
'Log in to Atlas at least once to materialize the membership tuple.');
|
|
727
|
+
ok = false;
|
|
728
|
+
}
|
|
729
|
+
else if (err.status === 401) {
|
|
730
|
+
lines.push('writer gate: FAIL — 401. Token is rejected by Atlas — run `seqapi login`.');
|
|
731
|
+
ok = false;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
lines.push(`writer gate: WARN — ${err.status} ${err.message}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
lines.push(`writer gate: WARN — could not probe: ${err instanceof Error ? err.message : String(err)}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Git-service discovery: empty current env often means the repos live on
|
|
742
|
+
// another deployment (agents historically probed banksouth → production →
|
|
743
|
+
// staging). Hint when staging has repos and the current env does not.
|
|
744
|
+
try {
|
|
745
|
+
const current = await getJson({
|
|
746
|
+
baseUrl: env.url,
|
|
747
|
+
token,
|
|
748
|
+
path: '/api/git-service/repos?limit=1&offset=0',
|
|
749
|
+
});
|
|
750
|
+
const currentTotal = current.total ?? current.items.length;
|
|
751
|
+
if (currentTotal === 0 && env.name !== 'staging' && config?.envs.staging?.url) {
|
|
752
|
+
try {
|
|
753
|
+
const staging = await getJson({
|
|
754
|
+
baseUrl: config.envs.staging.url,
|
|
755
|
+
token,
|
|
756
|
+
path: '/api/git-service/repos?limit=1&offset=0',
|
|
757
|
+
});
|
|
758
|
+
const stagingTotal = staging.total ?? staging.items.length;
|
|
759
|
+
if (stagingTotal > 0) {
|
|
760
|
+
lines.push(`git-service: ${env.name} has 0 visible repos, but staging has ${stagingTotal} — try \`seq-studio repos list -e staging\``);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
lines.push(`git-service: ok — 0 visible repos on ${env.name} (staging also empty)`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
lines.push(`git-service: ok — 0 visible repos on ${env.name}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
lines.push(`git-service: ok — ${currentTotal} visible repo(s) on ${env.name}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
catch (err) {
|
|
775
|
+
if (err instanceof AtlasApiError) {
|
|
776
|
+
lines.push(`git-service: WARN — ${err.status} ${err.message}`);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
lines.push(`git-service: WARN — could not probe: ${err instanceof Error ? err.message : String(err)}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
for (const l of lines)
|
|
784
|
+
console.log(l);
|
|
785
|
+
return ok ? 0 : 1;
|
|
786
|
+
}
|