@catalyst-cloud/cli 0.13.0 → 0.14.0
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/CHANGELOG.md +10 -0
- package/README.md +27 -30
- package/dist/args.js +299 -61
- package/dist/ask.js +30 -18
- package/dist/capabilities.js +7 -0
- package/dist/cli.js +125 -20
- package/dist/config.js +3 -1
- package/dist/env/draft.js +134 -0
- package/dist/env/legacy.js +54 -0
- package/dist/env/parse-env.js +50 -0
- package/dist/env/settings-toml.js +66 -0
- package/dist/env.js +126 -26
- package/dist/environment.js +1 -1
- package/dist/legacy.js +224 -0
- package/dist/onboard-consent.js +65 -0
- package/dist/onboard.js +836 -0
- package/dist/project.js +191 -0
- package/dist/ready.js +11 -1
- package/dist/release.js +1 -1
- package/dist/repo.js +297 -0
- package/dist/runtime-verb.js +1 -1
- package/dist/secret.js +25 -4
- package/dist/skill-shape.js +21 -13
- package/dist/skills.js +44 -4
- package/dist/transport.js +26 -1
- package/dist/var.js +118 -0
- package/package.json +6 -4
- package/skills/catalyst-github/SKILL.md +7 -7
- package/skills/catalyst-github/references/is-it-mergeable.md +8 -2
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +3 -3
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +3 -3
- package/skills/catalyst-github/scripts/lib/cli.mjs +9 -7
- package/skills/catalyst-github/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-github/scripts/read-pr.mjs +2 -2
- package/skills/catalyst-linear/SKILL.md +17 -19
- package/skills/catalyst-linear/agents/openai.yaml +1 -1
- package/skills/catalyst-linear/agents/portability.yaml +1 -1
- package/skills/catalyst-linear/references/reading-a-ticket.md +9 -26
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +2 -2
- package/skills/catalyst-linear/references/writing-to-linear.md +7 -9
- package/skills/catalyst-linear/scripts/comment.mjs +1 -1
- package/skills/catalyst-linear/scripts/create-ticket.mjs +1 -1
- package/skills/catalyst-linear/scripts/label.mjs +1 -1
- package/skills/catalyst-linear/scripts/lib/cli.mjs +9 -7
- package/skills/catalyst-linear/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-linear/scripts/move.mjs +1 -1
- package/skills/catalyst-linear/scripts/read-ticket.mjs +1 -1
- package/skills/catalyst-linear/scripts/search.mjs +1 -1
- package/skills/catalyst-onboard/SKILL.md +37 -57
- package/skills/catalyst-onboard/agents/openai.yaml +2 -2
- package/skills/catalyst-onboard/agents/portability.yaml +1 -1
- package/skills/catalyst-onboard/references/connecting-this-machine.md +35 -0
- package/skills/catalyst-onboard/references/declaring-a-repository.md +18 -21
- package/skills/catalyst-onboard/references/local-sync.md +19 -7
- package/skills/catalyst-onboard/references/reading-ready.md +79 -0
- package/skills/catalyst-onboard/references/replacing-a-credential.md +8 -33
- package/skills/catalyst-onboard/references/repository-agent-setup.md +23 -0
- package/skills/catalyst-onboard/references/skill-sources.md +13 -19
- package/skills/catalyst-onboard/references/the-one-path.md +18 -74
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +21 -28
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +20 -37
- package/skills/{catalyst-setup → catalyst-onboard}/scripts/check.mjs +10 -9
- package/skills/catalyst-onboard/scripts/lib/cli.mjs +6 -4
- package/skills/catalyst-onboard/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-onboard/scripts/lib/project-coverage.mjs +53 -0
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +929 -184
- package/skills/run-this-project/SKILL.md +23 -25
- package/skills/run-this-project/agents/openai.yaml +1 -1
- package/skills/run-this-project/agents/portability.yaml +1 -1
- package/skills/run-this-project/references/making-work-ready.md +20 -37
- package/skills/run-this-project/references/reacting-to-events.md +25 -48
- package/skills/run-this-project/references/stalls-and-escalation.md +20 -42
- package/skills/run-this-project/scripts/lib/cli.mjs +7 -5
- package/skills/run-this-project/scripts/lib/credential.mjs +2 -2
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +1 -1
- package/skills/unstick/SKILL.md +12 -12
- package/skills/unstick/agents/openai.yaml +1 -1
- package/skills/unstick/agents/portability.yaml +1 -1
- package/skills/unstick/references/playbook.md +13 -13
- package/skills/unstick/scripts/lib/cli.mjs +6 -4
- package/skills/unstick/scripts/lib/credential.mjs +2 -2
- package/skills/unstick/scripts/unstick.mjs +1 -1
- package/skills/what-needs-me/SKILL.md +12 -14
- package/skills/what-needs-me/agents/openai.yaml +2 -2
- package/skills/what-needs-me/agents/portability.yaml +1 -1
- package/skills/what-needs-me/references/raising-a-decision.md +8 -16
- package/skills/what-needs-me/references/reading-the-inbox.md +6 -13
- package/skills/what-needs-me/references/settling-an-answer.md +6 -12
- package/skills/what-needs-me/scripts/inbox.mjs +14 -13
- package/skills/what-needs-me/scripts/lib/cli.mjs +6 -4
- package/skills/what-needs-me/scripts/lib/credential.mjs +2 -2
- package/skills/what-needs-me/scripts/raise.mjs +1 -1
- package/skills/what-needs-me/scripts/settle.mjs +1 -1
- package/skills/what-this-repo-needs/SKILL.md +10 -19
- package/skills/what-this-repo-needs/references/what-each-group-means.md +14 -44
- package/skills/what-this-repo-needs/scripts/check.mjs +13 -14
- package/skills/what-this-repo-needs/scripts/inventory.mjs +2 -2
- package/skills/what-this-repo-needs/scripts/lib/cli.mjs +11 -9
- package/skills/what-this-repo-needs/scripts/lib/credential.mjs +2 -2
- package/skills/whats-happening/SKILL.md +20 -30
- package/skills/whats-happening/agents/openai.yaml +1 -1
- package/skills/whats-happening/references/coding-accounts.md +11 -0
- package/skills/whats-happening/references/routing-work.md +4 -33
- package/skills/whats-happening/references/settings-and-where-they-live.md +17 -0
- package/skills/whats-happening/references/stages-and-mapping.md +19 -0
- package/skills/whats-happening/references/status-reply.md +11 -31
- package/skills/whats-happening/references/the-ladder.md +20 -0
- package/skills/whats-happening/references/what-runs-next.md +13 -0
- package/skills/whats-happening/references/when-a-phase-fails.md +21 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +38 -62
- package/skills/whats-happening/scripts/explain.mjs +1 -1
- package/skills/whats-happening/scripts/lib/cli.mjs +7 -5
- package/skills/whats-happening/scripts/lib/credential.mjs +2 -2
- package/skills/{how-catalyst-works → whats-happening}/scripts/show-my-map.mjs +18 -32
- package/skills/whats-happening/scripts/snapshot.mjs +10 -6
- package/dist/env/declaration-rules.js +0 -163
- package/skills/catalyst-onboard/references/choosing-a-coding-account.md +0 -52
- package/skills/catalyst-onboard/references/who-fixes-what.md +0 -64
- package/skills/catalyst-setup/SKILL.md +0 -36
- package/skills/catalyst-setup/agents/openai.yaml +0 -6
- package/skills/catalyst-setup/agents/portability.yaml +0 -4
- package/skills/catalyst-setup/references/what-each-check-means.md +0 -88
- package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
- package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
- package/skills/connect-me/SKILL.md +0 -63
- package/skills/connect-me/agents/openai.yaml +0 -6
- package/skills/connect-me/agents/portability.yaml +0 -5
- package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
- package/skills/connect-me/scripts/lib/cli.mjs +0 -185
- package/skills/connect-me/scripts/lib/credential.mjs +0 -29
- package/skills/connect-me/scripts/verify-connection.mjs +0 -68
- package/skills/how-catalyst-works/SKILL.md +0 -45
- package/skills/how-catalyst-works/agents/openai.yaml +0 -6
- package/skills/how-catalyst-works/agents/portability.yaml +0 -4
- package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
- package/skills/how-catalyst-works/references/settings-and-where-they-live.md +0 -20
- package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -56
- package/skills/how-catalyst-works/references/the-ladder.md +0 -41
- package/skills/how-catalyst-works/references/what-catalyst-is.md +0 -30
- package/skills/how-catalyst-works/references/what-runs-next.md +0 -95
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
- package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
- package/skills/whats-happening/references/reading-the-board.md +0 -43
- package/skills/whats-happening/references/reprioritising.md +0 -37
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli.ts — the `catalyst
|
|
1
|
+
// cli.ts — the `catalyst` dispatcher (npm: @catalyst-cloud/cli). Verbs live in
|
|
2
2
|
// their own modules; this file keeps every export the 0.1 tests and the bin import.
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { parseArgs, positionals, verbHelp } from "./args.js";
|
|
@@ -8,14 +8,14 @@ import { CliError, MeError, UsageError } from "./errors.js";
|
|
|
8
8
|
import { fetchMe } from "./transport.js";
|
|
9
9
|
import { bearerFor, deviceFlowLogin } from "./oauth.js";
|
|
10
10
|
import { openBrowser as defaultOpenBrowser } from "./browser.js";
|
|
11
|
-
import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning } from "./execution.js";
|
|
11
|
+
import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning, } from "./execution.js";
|
|
12
12
|
import { cmdQuery } from "./query.js";
|
|
13
13
|
import { cmdReady } from "./ready.js";
|
|
14
14
|
import { cmdReplica } from "./replica.js";
|
|
15
15
|
import { cmdRuntime } from "./runtime-verb.js";
|
|
16
16
|
import { cmdEvents } from "./events.js";
|
|
17
17
|
import { stdinIsTty } from "./prompt.js";
|
|
18
|
-
import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion } from "./skill-shape.js";
|
|
18
|
+
import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion, } from "./skill-shape.js";
|
|
19
19
|
import { installedBundleVersion, installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine, } from "./skills.js";
|
|
20
20
|
import { cmdWatch } from "./watch.js";
|
|
21
21
|
import { cmdWrite } from "./write.js";
|
|
@@ -25,18 +25,20 @@ import { cmdRelease } from "./release.js";
|
|
|
25
25
|
import { cmdEnvironment } from "./environment.js";
|
|
26
26
|
import { cmdEnv } from "./env.js";
|
|
27
27
|
import { cmdSecret } from "./secret.js";
|
|
28
|
+
import { cmdVar } from "./var.js";
|
|
28
29
|
import { cmdTeam } from "./team.js";
|
|
29
30
|
import { cmdCapabilities } from "./capabilities.js";
|
|
31
|
+
import { cmdProject } from "./project.js";
|
|
32
|
+
import { cmdRepo } from "./repo.js";
|
|
33
|
+
import { cmdLegacy, findLegacy } from "./legacy.js";
|
|
30
34
|
import { cmdIdentity } from "./identity.js";
|
|
31
35
|
import { cmdConnections } from "./connections.js";
|
|
36
|
+
import { cmdOnboard } from "./onboard.js";
|
|
32
37
|
export { CONFIG_MODE, DEFAULT_BASE_URL, FIRST_STAMPED_VERSION, LEGACY_PACKAGE_NAME, PACKAGE_NAME, PROVENANCE_MARKER, CliError, MeError, UsageError, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, fetchMe, formatMode, installedBundleVersion, installSkills, loadConfig, modernCliPath, normalizeBaseUrl, parseArgs, parseChangelogEntry, parseProvenanceVersion, readChangelog, readManifest, resolveSkillsDir, saveConfig, skillsSourceDir, updateNoticeLine, writeConfig, };
|
|
33
38
|
export const CUSTOMER_SKILLS = [
|
|
34
39
|
"catalyst-github",
|
|
35
40
|
"catalyst-linear",
|
|
36
41
|
"catalyst-onboard",
|
|
37
|
-
"catalyst-setup",
|
|
38
|
-
"connect-me",
|
|
39
|
-
"how-catalyst-works",
|
|
40
42
|
"run-this-project",
|
|
41
43
|
"unstick",
|
|
42
44
|
"what-needs-me",
|
|
@@ -57,11 +59,12 @@ export function usageText() {
|
|
|
57
59
|
" catalyst login --key <personal-key> [--base-url <url>] (or CATALYST_CLOUD_TOKEN, for a key)",
|
|
58
60
|
" catalyst join ... (deprecated alias of login; removed in the next minor version)",
|
|
59
61
|
" catalyst install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
|
|
62
|
+
" catalyst onboard [--resume-from <step>] [--only <step>] [--yes] [--dry-run] [--json]",
|
|
60
63
|
" catalyst status | notice | me | ready | accounts",
|
|
61
64
|
" catalyst mcp add|list|remove (vault references only)",
|
|
62
65
|
" catalyst contract [--refresh] [--path <a.b.c>]",
|
|
63
66
|
" catalyst query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
|
|
64
|
-
|
|
67
|
+
' catalyst replica <start [--detach]|stop|status [--probe]|sql "<select>"|schema [table]>',
|
|
65
68
|
" catalyst runtime <status [--json]|install|path|uninstall> (a pinned Node this CLI manages itself)",
|
|
66
69
|
" catalyst events <tail|wait-for|query|status [--probe]> [--type NAME] [--ticket CTC-N] [--after SEQUENCE]",
|
|
67
70
|
" catalyst explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
|
|
@@ -70,10 +73,15 @@ export function usageText() {
|
|
|
70
73
|
" catalyst ask <raise|accept|list> ...",
|
|
71
74
|
" catalyst release <ticket> --because <what changed> [--retry-unchanged] [--dry-run] | release --class <c> --team <K> ...",
|
|
72
75
|
" catalyst environment [read] | environment propose --file <path>|--stdin [--approve] | environment approve (your account; needs login)",
|
|
73
|
-
" catalyst env <inventory [path] | check <file
|
|
74
|
-
" catalyst secret set <NAME> --repo <owner/name> [--command '<cmd>'] | secret import <file> --repo <owner/name>",
|
|
76
|
+
" catalyst env <inventory [path] | check <file> | migrate [catalyst.env.json] | draft [--root DIR] [--diff] [--write]> [--json] (this repository, offline — no login, no network)",
|
|
77
|
+
" catalyst secret set <NAME> --repo <owner/name> [--command '<cmd>'] | secret import <file> --repo <owner/name> [--names NAME[,NAME...]]",
|
|
78
|
+
" catalyst var set <NAME> | var import <file> [--repo <owner/name>] [--names NAME[,NAME...]] (plain environment variables)",
|
|
75
79
|
" catalyst team <list|check|map|adopt|migrate|checklist> ...",
|
|
76
80
|
" catalyst capabilities [--json] (what this CLI can do, the role each verb needs, and whether this cloud serves it)",
|
|
81
|
+
" catalyst project list [--json] (all tenant projects, including those with no stage mapping)",
|
|
82
|
+
" catalyst project wip-limit <get|set <n>|set default> [--team K] (a project's new-start WIP limit; members read, owners and admins set)",
|
|
83
|
+
" catalyst repo <agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> (a checkout's AGENTS.md block and portable agent layout; working tree only)",
|
|
84
|
+
" catalyst legacy [--remove [--data] [--yes]] (leftovers of the old local Catalyst runtime: list them; remove them only on a yes)",
|
|
77
85
|
" catalyst identity linear <status|options|set> [<linearUserId>] [--json]",
|
|
78
86
|
" catalyst connections personal <linear|github> <start|status> [--wait <seconds>] [--json]",
|
|
79
87
|
"",
|
|
@@ -111,7 +119,10 @@ function migrateLegacyCliPath(ctx) {
|
|
|
111
119
|
// An upgrade from @catalyst-cloud/catalyst-skills to @catalyst-cloud/cli removes the old package
|
|
112
120
|
// folder, so the recorded launcher no longer exists and has no sibling. Point it at the launcher
|
|
113
121
|
// that is running now, so `ready` and every skill script work without another login.
|
|
114
|
-
if (next === null &&
|
|
122
|
+
if (next === null &&
|
|
123
|
+
typeof cfg.cliPath === "string" &&
|
|
124
|
+
cfg.cliPath !== "" &&
|
|
125
|
+
!existsSync(cfg.cliPath)) {
|
|
115
126
|
next = cliPath();
|
|
116
127
|
}
|
|
117
128
|
if (next === null || next === cfg.cliPath)
|
|
@@ -168,6 +179,8 @@ function maybePrintUpdateNotice(args, ctx) {
|
|
|
168
179
|
}
|
|
169
180
|
if (refreshed.installed.length > 0)
|
|
170
181
|
say(`[catalyst] refreshed ${refreshed.installed.join(", ")} at ${skillsDir} to ${manifest.version}`);
|
|
182
|
+
if (refreshed.removed.length > 0)
|
|
183
|
+
say(`[catalyst] removed retired skills ${refreshed.removed.join(", ")} from ${skillsDir}; their work moved into the skills this bundle ships`);
|
|
171
184
|
for (const s of refreshed.skipped) {
|
|
172
185
|
say(`[catalyst] left "${s.name}" alone: ${skillsDir}/${s.name} was not installed by this package (catalyst install --force to replace)`);
|
|
173
186
|
}
|
|
@@ -208,6 +221,36 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
208
221
|
return await cmdLogin(args, ctx, deps);
|
|
209
222
|
case "install":
|
|
210
223
|
return cmdInstall(args, ctx);
|
|
224
|
+
case "onboard":
|
|
225
|
+
return await cmdOnboard(args, ctx, {
|
|
226
|
+
runStep: async (id, stepCtx) => {
|
|
227
|
+
if (id !== "legacy")
|
|
228
|
+
return { state: "skipped" };
|
|
229
|
+
const before = findLegacy(stepCtx.home, process.platform).filter((item) => !item.data).length;
|
|
230
|
+
const safeCtx = args.json
|
|
231
|
+
? { ...stepCtx, stdout: stepCtx.stderr }
|
|
232
|
+
: stepCtx;
|
|
233
|
+
const legacyArgs = {
|
|
234
|
+
...args,
|
|
235
|
+
command: "legacy",
|
|
236
|
+
subcommand: null,
|
|
237
|
+
rest: [],
|
|
238
|
+
flags: { remove: true, yes: true },
|
|
239
|
+
json: false,
|
|
240
|
+
help: false,
|
|
241
|
+
version: false,
|
|
242
|
+
};
|
|
243
|
+
const code = await cmdLegacy(legacyArgs, safeCtx, deps.legacy ?? {});
|
|
244
|
+
if (code !== 0)
|
|
245
|
+
throw new CliError("legacy cleanup did not finish", "onboard-legacy-failed", 10);
|
|
246
|
+
const after = findLegacy(stepCtx.home, process.platform).filter((item) => !item.data).length;
|
|
247
|
+
return {
|
|
248
|
+
state: after === 0 ? "done" : "skipped",
|
|
249
|
+
evidence: { found: before, remaining: after },
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
isTty: deps.isTty ?? stdinIsTty,
|
|
253
|
+
}, readManifest().version);
|
|
211
254
|
case "notice":
|
|
212
255
|
return 0;
|
|
213
256
|
case "status":
|
|
@@ -217,7 +260,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
217
260
|
case "contract":
|
|
218
261
|
return await cmdContract(args, ctx);
|
|
219
262
|
case "query":
|
|
220
|
-
return await cmdQuery(args, ctx, {
|
|
263
|
+
return await cmdQuery(args, ctx, {
|
|
264
|
+
engineDeps: deps.replica?.engineDeps,
|
|
265
|
+
});
|
|
221
266
|
case "replica":
|
|
222
267
|
return await cmdReplica(args, ctx, deps.replica);
|
|
223
268
|
case "runtime":
|
|
@@ -239,7 +284,11 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
239
284
|
case "ask":
|
|
240
285
|
return await cmdAsk(args, ctx);
|
|
241
286
|
case "ready":
|
|
242
|
-
return await cmdReady(args, ctx, {
|
|
287
|
+
return await cmdReady(args, ctx, {
|
|
288
|
+
skillNames: CUSTOMER_SKILLS,
|
|
289
|
+
loadSdk: deps.loadSdk,
|
|
290
|
+
offline: args.flags.offline === true,
|
|
291
|
+
});
|
|
243
292
|
case "mcp":
|
|
244
293
|
return await cmdMcp(args, ctx);
|
|
245
294
|
case "accounts":
|
|
@@ -252,10 +301,18 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
252
301
|
return await cmdEnv(args, ctx, deps.env ?? {});
|
|
253
302
|
case "secret":
|
|
254
303
|
return await cmdSecret(args, ctx, deps.secret ?? {});
|
|
304
|
+
case "var":
|
|
305
|
+
return await cmdVar(args, ctx, deps.var ?? {});
|
|
255
306
|
case "team":
|
|
256
307
|
return await cmdTeam(args, ctx, deps.team);
|
|
257
308
|
case "capabilities":
|
|
258
309
|
return await cmdCapabilities(args, ctx);
|
|
310
|
+
case "project":
|
|
311
|
+
return await cmdProject(args, ctx);
|
|
312
|
+
case "repo":
|
|
313
|
+
return await cmdRepo(args, ctx);
|
|
314
|
+
case "legacy":
|
|
315
|
+
return await cmdLegacy(args, ctx, deps.legacy ?? {});
|
|
259
316
|
case "identity":
|
|
260
317
|
return await cmdIdentity(args, ctx, deps.identity ?? {});
|
|
261
318
|
case "connections":
|
|
@@ -269,7 +326,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
269
326
|
catch (err) {
|
|
270
327
|
if (err instanceof UsageError) {
|
|
271
328
|
ctx.stderr(err.message);
|
|
272
|
-
ctx.stderr(args.command && args.command in VERB_HELP_KNOWN
|
|
329
|
+
ctx.stderr(args.command && args.command in VERB_HELP_KNOWN
|
|
330
|
+
? verbHelp(args.command)
|
|
331
|
+
: usageText());
|
|
273
332
|
return 1;
|
|
274
333
|
}
|
|
275
334
|
if (err instanceof CliError) {
|
|
@@ -283,7 +342,40 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
283
342
|
throw err;
|
|
284
343
|
}
|
|
285
344
|
}
|
|
286
|
-
const VERB_HELP_KNOWN = Object.fromEntries([
|
|
345
|
+
const VERB_HELP_KNOWN = Object.fromEntries([
|
|
346
|
+
"login",
|
|
347
|
+
"join",
|
|
348
|
+
"install",
|
|
349
|
+
"status",
|
|
350
|
+
"notice",
|
|
351
|
+
"me",
|
|
352
|
+
"contract",
|
|
353
|
+
"query",
|
|
354
|
+
"replica",
|
|
355
|
+
"runtime",
|
|
356
|
+
"events",
|
|
357
|
+
"explain",
|
|
358
|
+
"running",
|
|
359
|
+
"queue",
|
|
360
|
+
"watch",
|
|
361
|
+
"write",
|
|
362
|
+
"ask",
|
|
363
|
+
"ready",
|
|
364
|
+
"accounts",
|
|
365
|
+
"release",
|
|
366
|
+
"secret",
|
|
367
|
+
"env",
|
|
368
|
+
"environment",
|
|
369
|
+
"connections",
|
|
370
|
+
"identity",
|
|
371
|
+
"mcp",
|
|
372
|
+
"team",
|
|
373
|
+
"capabilities",
|
|
374
|
+
"project",
|
|
375
|
+
"repo",
|
|
376
|
+
"legacy",
|
|
377
|
+
"onboard",
|
|
378
|
+
].map((v) => [v, true]));
|
|
287
379
|
async function cmdLogin(args, ctx, deps) {
|
|
288
380
|
const manifest = readManifest();
|
|
289
381
|
const key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
|
|
@@ -327,19 +419,24 @@ async function cmdLogin(args, ctx, deps) {
|
|
|
327
419
|
const written = writeConfig(ctx.home, config);
|
|
328
420
|
ctx.stdout(`Connected to ${me.name} (${me.slug}) — account ${me.account}`);
|
|
329
421
|
if (me.user) {
|
|
330
|
-
ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId
|
|
422
|
+
ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId
|
|
423
|
+
? ""
|
|
424
|
+
: ' — your Linear identity is not matched yet, so "what needs me" will show everyone\'s asks until an admin matches it in Settings'}`);
|
|
331
425
|
}
|
|
332
426
|
else {
|
|
333
427
|
ctx.stderr(`[catalyst] this is the tenant's account key (a host credential), not your own — the skills work, but nothing your agent writes will carry your name and "what needs me" cannot mean you. Mint a personal key at Settings → API keys and log in with that.`);
|
|
334
428
|
}
|
|
335
|
-
ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your ${auth ? "login session" : "key"} and the CLI path)${written.mode === CONFIG_MODE
|
|
429
|
+
ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your ${auth ? "login session" : "key"} and the CLI path)${written.mode === CONFIG_MODE
|
|
430
|
+
? ""
|
|
431
|
+
: ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
|
|
336
432
|
ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
|
|
337
433
|
try {
|
|
338
434
|
const loaded = await loadContract(ctx, config, { refresh: true });
|
|
339
435
|
ctx.stdout(`Tenant contract ${loaded.doc.contractVersion} cached at ${loaded.path}`);
|
|
340
436
|
}
|
|
341
437
|
catch (err) {
|
|
342
|
-
if (err instanceof CliError &&
|
|
438
|
+
if (err instanceof CliError &&
|
|
439
|
+
(err.code === "contract-forbidden" || err.code === "contract-version")) {
|
|
343
440
|
ctx.stderr(`[catalyst] ${err.message}`);
|
|
344
441
|
}
|
|
345
442
|
else {
|
|
@@ -376,6 +473,8 @@ function cmdInstall(args, ctx) {
|
|
|
376
473
|
for (const s of result.skipped) {
|
|
377
474
|
ctx.stdout(`Skipped "${s.name}": ${skillsDir}/${s.name} exists and was not installed by this package (use --force to replace)`);
|
|
378
475
|
}
|
|
476
|
+
if (result.removed.length > 0)
|
|
477
|
+
ctx.stdout(`Removed retired skills from ${skillsDir}: ${result.removed.join(", ")}`);
|
|
379
478
|
return 0;
|
|
380
479
|
}
|
|
381
480
|
function cmdStatus(ctx) {
|
|
@@ -387,8 +486,12 @@ function cmdStatus(ctx) {
|
|
|
387
486
|
}
|
|
388
487
|
ctx.stdout(`Tenant: ${cfg.name} (${cfg.slug}) — account ${cfg.account}`);
|
|
389
488
|
ctx.stdout(`API: ${cfg.baseUrl} (principal: ${cfg.principal})`);
|
|
390
|
-
ctx.stdout(cfg.user
|
|
391
|
-
|
|
489
|
+
ctx.stdout(cfg.user
|
|
490
|
+
? `As: ${cfg.user.label} (${cfg.user.role})`
|
|
491
|
+
: "As: the tenant's account key (a host credential — no person)");
|
|
492
|
+
ctx.stdout(cfg.auth
|
|
493
|
+
? `Credential: your login (expires ${relativeExpiry(cfg.auth.expiresAt, ctx.now())})`
|
|
494
|
+
: "Credential: personal key");
|
|
392
495
|
ctx.stdout(`Bundle: ${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
|
|
393
496
|
if (cfg.cliPath)
|
|
394
497
|
ctx.stdout(`CLI: ${cfg.cliPath}${existsSync(cfg.cliPath) ? "" : " (missing — re-run login)"}`);
|
|
@@ -433,6 +536,8 @@ async function cmdContract(args, ctx) {
|
|
|
433
536
|
const value = path ? pickPath(loaded.doc, path) : loaded.doc;
|
|
434
537
|
if (path && value === undefined)
|
|
435
538
|
throw new CliError(`the contract has nothing at "${path}"`, "contract-path");
|
|
436
|
-
ctx.stdout(typeof value === "string"
|
|
539
|
+
ctx.stdout(typeof value === "string"
|
|
540
|
+
? value
|
|
541
|
+
: JSON.stringify(value, null, args.json ? 0 : 2));
|
|
437
542
|
return 0;
|
|
438
543
|
}
|
package/dist/config.js
CHANGED
|
@@ -111,7 +111,9 @@ export function modernCliPath(recorded) {
|
|
|
111
111
|
return existsSync(sibling) ? sibling : null;
|
|
112
112
|
}
|
|
113
113
|
export function normalizeBaseUrl(url) {
|
|
114
|
-
|
|
114
|
+
while (url.endsWith("/"))
|
|
115
|
+
url = url.slice(0, -1);
|
|
116
|
+
return url;
|
|
115
117
|
}
|
|
116
118
|
/** The origin WITH the versioned prefix — what the SDK and every API read/write take. */
|
|
117
119
|
export function apiBase(cfg) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { convertLegacyEnvironmentJson } from "./legacy.js";
|
|
4
|
+
import { inventoryRepo } from "./inventory.js";
|
|
5
|
+
import { walkRepo } from "./walk.js";
|
|
6
|
+
const SECRET_NAME_RE = /(?:^|_)(?:API_?KEY|AUTH|CREDENTIALS?|PASSWORD|PRIVATE_?KEY|SECRET|TOKEN)(?:_|$)/i;
|
|
7
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
+
const MAX_VARIABLES = 128;
|
|
9
|
+
/** Build a stable, value-free environment table from repository evidence. `.env` is never read:
|
|
10
|
+
* inventoryRepo's walker only opens supported examples and source/config files. The legacy
|
|
11
|
+
* declaration is parsed for names and explicit secret flags, and no legacy value escapes. */
|
|
12
|
+
export function draftEnvironment(root, deps = {}) {
|
|
13
|
+
const inventory = inventoryRepo(root, deps);
|
|
14
|
+
const files = (deps.listFiles ?? walkRepo)(root);
|
|
15
|
+
const names = new Set();
|
|
16
|
+
const secrets = new Set();
|
|
17
|
+
const sources = new Set();
|
|
18
|
+
for (const entry of inventory.entries) {
|
|
19
|
+
if (entry.group === "bindings" || !entry.needsDeclaration || !ENV_NAME_RE.test(entry.name))
|
|
20
|
+
continue;
|
|
21
|
+
names.add(entry.name);
|
|
22
|
+
if (SECRET_NAME_RE.test(entry.name))
|
|
23
|
+
secrets.add(entry.name);
|
|
24
|
+
for (const finding of entry.found)
|
|
25
|
+
sources.add(finding.file);
|
|
26
|
+
}
|
|
27
|
+
const legacyPath = join(root, "catalyst.env.json");
|
|
28
|
+
const legacy = deps.readFile !== undefined ? deps.readFile(legacyPath) : readOptional(legacyPath);
|
|
29
|
+
if (legacy !== undefined) {
|
|
30
|
+
const converted = convertLegacyEnvironmentJson(legacy);
|
|
31
|
+
if (converted.state === "invalid")
|
|
32
|
+
return converted;
|
|
33
|
+
for (const name of converted.variableNames)
|
|
34
|
+
names.add(name);
|
|
35
|
+
for (const name of converted.secretNames)
|
|
36
|
+
secrets.add(name);
|
|
37
|
+
if (converted.variableNames.length > 0)
|
|
38
|
+
sources.add("catalyst.env.json");
|
|
39
|
+
}
|
|
40
|
+
const sortedNames = [...names].sort((a, b) => a.localeCompare(b, "en"));
|
|
41
|
+
if (sortedNames.length > MAX_VARIABLES) {
|
|
42
|
+
return { state: "invalid", errors: [`environment has more than ${MAX_VARIABLES} names`] };
|
|
43
|
+
}
|
|
44
|
+
const secretNames = [...secrets].filter((name) => names.has(name)).sort((a, b) => a.localeCompare(b, "en"));
|
|
45
|
+
const standard = standardSteps(root, files, deps);
|
|
46
|
+
if (standard.error !== undefined)
|
|
47
|
+
return { state: "invalid", errors: [standard.error] };
|
|
48
|
+
for (const source of standard.sources)
|
|
49
|
+
sources.add(source);
|
|
50
|
+
for (const path of files)
|
|
51
|
+
if (path.startsWith(".github/workflows/"))
|
|
52
|
+
sources.add(path);
|
|
53
|
+
const chunks = ["[environment]"];
|
|
54
|
+
for (const name of sortedNames)
|
|
55
|
+
chunks.push([
|
|
56
|
+
"[[environment.variables]]",
|
|
57
|
+
`name = ${JSON.stringify(name)}`,
|
|
58
|
+
...(secrets.has(name) ? ["secret = true"] : []),
|
|
59
|
+
"required = false",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n"));
|
|
62
|
+
chunks.push(...standard.setup, ...standard.verify);
|
|
63
|
+
const toml = `${chunks.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
64
|
+
return {
|
|
65
|
+
state: "valid",
|
|
66
|
+
names: sortedNames,
|
|
67
|
+
secretNames,
|
|
68
|
+
setup: standard.setup,
|
|
69
|
+
verify: standard.verify,
|
|
70
|
+
toml,
|
|
71
|
+
sources: [...sources].sort((a, b) => a.localeCompare(b, "en")),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Conservative package.json/lockfile inference. No repository script is executed. */
|
|
75
|
+
function standardSteps(root, files, deps) {
|
|
76
|
+
if (!files.includes("package.json"))
|
|
77
|
+
return { setup: [], verify: [], sources: [] };
|
|
78
|
+
const raw = deps.readFile !== undefined ? deps.readFile(join(root, "package.json")) : readOptional(join(root, "package.json"));
|
|
79
|
+
if (raw === undefined)
|
|
80
|
+
return { setup: [], verify: [], sources: [] };
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(raw);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { setup: [], verify: [], sources: [], error: "package.json is not valid JSON; refusing to guess setup commands" };
|
|
87
|
+
}
|
|
88
|
+
if (!isRecord(parsed))
|
|
89
|
+
return { setup: [], verify: [], sources: [], error: "package.json must contain a JSON object; refusing to guess setup commands" };
|
|
90
|
+
const scripts = isRecord(parsed.scripts) ? parsed.scripts : {};
|
|
91
|
+
const lock = ["bun.lock", "bun.lockb", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"].find((name) => files.includes(name));
|
|
92
|
+
const manager = lock?.startsWith("bun.") ? "bun"
|
|
93
|
+
: lock === "pnpm-lock.yaml" ? "pnpm"
|
|
94
|
+
: lock === "yarn.lock" ? "yarn"
|
|
95
|
+
: lock === "package-lock.json" ? "npm" : undefined;
|
|
96
|
+
const setup = [];
|
|
97
|
+
const verify = [];
|
|
98
|
+
const sources = ["package.json"];
|
|
99
|
+
if (manager !== undefined && lock !== undefined) {
|
|
100
|
+
const install = manager === "npm" ? ["npm", "ci"]
|
|
101
|
+
: manager === "yarn" ? ["yarn", "install", "--immutable"]
|
|
102
|
+
: [manager, "install", "--frozen-lockfile"];
|
|
103
|
+
setup.push(stepToml("setup", "install-dependencies", install, [lock]));
|
|
104
|
+
sources.push(lock);
|
|
105
|
+
}
|
|
106
|
+
const selected = typeof scripts["check"] === "string"
|
|
107
|
+
? ["check"]
|
|
108
|
+
: ["typecheck", "test"].filter((name) => typeof scripts[name] === "string");
|
|
109
|
+
if (manager !== undefined) {
|
|
110
|
+
for (const name of selected)
|
|
111
|
+
verify.push(stepToml("verify", name, [manager, "run", name]));
|
|
112
|
+
}
|
|
113
|
+
return { setup, verify, sources };
|
|
114
|
+
}
|
|
115
|
+
function stepToml(section, name, run, cacheKeyFiles = []) {
|
|
116
|
+
return [
|
|
117
|
+
`[[environment.${section}]]`,
|
|
118
|
+
`name = ${JSON.stringify(name)}`,
|
|
119
|
+
`run = [${run.map((arg) => JSON.stringify(arg)).join(", ")}]`,
|
|
120
|
+
...(cacheKeyFiles.length ? [`cache_key_files = [${cacheKeyFiles.map((path) => JSON.stringify(path)).join(", ")}]`] : []),
|
|
121
|
+
"",
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
function readOptional(path) {
|
|
125
|
+
try {
|
|
126
|
+
return readFileSync(path, "utf8");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function isRecord(value) {
|
|
133
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
134
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
const SECRET_NAME_RE = /(?:^|_)(?:API_?KEY|AUTH|CREDENTIALS?|PASSWORD|PRIVATE_?KEY|SECRET|TOKEN)(?:_|$)/i;
|
|
3
|
+
/** Convert legacy catalyst.env.json names only. Values and provenance are never copied. */
|
|
4
|
+
export function convertLegacyEnvironmentJson(text) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(text);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return { state: "invalid", errors: ["legacy declaration is not valid JSON"] };
|
|
11
|
+
}
|
|
12
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.environment)) {
|
|
13
|
+
return {
|
|
14
|
+
state: "invalid",
|
|
15
|
+
errors: ["legacy declaration must contain an environment array"],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const errors = [];
|
|
19
|
+
const names = new Set();
|
|
20
|
+
const secretNames = [];
|
|
21
|
+
for (const [index, entry] of parsed.environment.entries()) {
|
|
22
|
+
if (!isRecord(entry) ||
|
|
23
|
+
typeof entry.name !== "string" ||
|
|
24
|
+
!ENV_NAME_RE.test(entry.name)) {
|
|
25
|
+
errors.push(`environment[${index}].name is not a valid environment variable name`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
names.add(entry.name);
|
|
29
|
+
const isSecret = entry.secret === true || SECRET_NAME_RE.test(entry.name);
|
|
30
|
+
if (isSecret && !secretNames.includes(entry.name)) {
|
|
31
|
+
secretNames.push(entry.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const variableNames = [...names].sort();
|
|
35
|
+
if (variableNames.length > 128)
|
|
36
|
+
errors.push("environment has more than 128 names");
|
|
37
|
+
if (errors.length)
|
|
38
|
+
return { state: "invalid", errors };
|
|
39
|
+
const toml = variableNames.length === 0
|
|
40
|
+
? "[environment]\n"
|
|
41
|
+
: variableNames
|
|
42
|
+
.map((name) => [
|
|
43
|
+
"[[environment.variables]]",
|
|
44
|
+
`name = ${JSON.stringify(name)}`,
|
|
45
|
+
...(secretNames.includes(name) ? ["secret = true"] : []),
|
|
46
|
+
"required = false",
|
|
47
|
+
"",
|
|
48
|
+
].join("\n"))
|
|
49
|
+
.join("\n");
|
|
50
|
+
return { state: "valid", variableNames, secretNames, toml };
|
|
51
|
+
}
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Select dotenv assignment lines by name while keeping their original bytes for the cloud's parser.
|
|
2
|
+
* Unselected values never cross the network boundary. This mirrors the cloud parser's line grammar. */
|
|
3
|
+
export function filterEnvFileNames(text, selected) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
const found = new Set();
|
|
6
|
+
for (const line of text.split(/\r?\n/)) {
|
|
7
|
+
const trimmed = line.trim();
|
|
8
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
9
|
+
continue;
|
|
10
|
+
const assignment = trimmed.startsWith("export ") ? trimmed.slice("export ".length) : trimmed;
|
|
11
|
+
const equals = assignment.indexOf("=");
|
|
12
|
+
if (equals < 0)
|
|
13
|
+
continue;
|
|
14
|
+
const name = assignment.slice(0, equals).trim();
|
|
15
|
+
if (!selected.has(name))
|
|
16
|
+
continue;
|
|
17
|
+
lines.push(line);
|
|
18
|
+
found.add(name);
|
|
19
|
+
}
|
|
20
|
+
return { text: lines.length === 0 ? "" : `${lines.join("\n")}\n`, found: [...found].sort((a, b) => a.localeCompare(b, "en")) };
|
|
21
|
+
}
|
|
22
|
+
/** Parse dotenv assignments using the mirror import grammar, after the caller has locally selected
|
|
23
|
+
* names. Values stay in this process and are posted one at a time to the documented write route. */
|
|
24
|
+
export function parseEnvAssignments(text, selected) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
for (const rawLine of text.split("\n")) {
|
|
27
|
+
const trimmed = rawLine.trim();
|
|
28
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
29
|
+
continue;
|
|
30
|
+
const assignment = trimmed.startsWith("export ") ? trimmed.slice("export ".length) : trimmed;
|
|
31
|
+
const equals = assignment.indexOf("=");
|
|
32
|
+
if (equals < 0)
|
|
33
|
+
continue;
|
|
34
|
+
const name = assignment.slice(0, equals).trim();
|
|
35
|
+
if (name === "" || (selected !== undefined && !selected.has(name)))
|
|
36
|
+
continue;
|
|
37
|
+
const rawValue = assignment.slice(equals + 1);
|
|
38
|
+
let value = rawValue.trim();
|
|
39
|
+
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
40
|
+
value = value.slice(1, -1);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const comment = rawValue.search(/\s#/);
|
|
44
|
+
if (comment >= 0)
|
|
45
|
+
value = rawValue.slice(0, comment).trim();
|
|
46
|
+
}
|
|
47
|
+
entries.push({ name, value });
|
|
48
|
+
}
|
|
49
|
+
return entries;
|
|
50
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parse, TomlError } from "smol-toml";
|
|
2
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
|
+
/** Validate TOML syntax and the repo declaration's environment variable table without printing values. */
|
|
4
|
+
export function validateSettingsToml(text) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = parse(text);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const line = error instanceof TomlError && typeof error.line === "number"
|
|
11
|
+
? ` at line ${error.line}`
|
|
12
|
+
: "";
|
|
13
|
+
return { state: "invalid", errors: [`TOML could not be parsed${line}`] };
|
|
14
|
+
}
|
|
15
|
+
if (!isRecord(parsed))
|
|
16
|
+
return {
|
|
17
|
+
state: "invalid",
|
|
18
|
+
errors: ["the settings file must have a TOML table at its root"],
|
|
19
|
+
};
|
|
20
|
+
if (parsed.environment === undefined)
|
|
21
|
+
return { state: "no-source", errors: [] };
|
|
22
|
+
if (!isRecord(parsed.environment))
|
|
23
|
+
return { state: "invalid", errors: ["environment must be a table"] };
|
|
24
|
+
const section = parsed.environment;
|
|
25
|
+
if (section.variables === undefined)
|
|
26
|
+
return { state: "valid", errors: [], variableNames: [] };
|
|
27
|
+
if (!Array.isArray(section.variables))
|
|
28
|
+
return {
|
|
29
|
+
state: "invalid",
|
|
30
|
+
errors: ["environment.variables must be an array of tables"],
|
|
31
|
+
};
|
|
32
|
+
const errors = [];
|
|
33
|
+
const names = new Set();
|
|
34
|
+
const variableNames = [];
|
|
35
|
+
if (section.variables.length > 128)
|
|
36
|
+
errors.push("environment.variables has more than 128 entries");
|
|
37
|
+
for (const [index, raw] of section.variables.entries()) {
|
|
38
|
+
const prefix = `environment.variables[${index}]`;
|
|
39
|
+
if (!isRecord(raw)) {
|
|
40
|
+
errors.push(`${prefix} must be a table`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const name = raw.name;
|
|
44
|
+
if (typeof name !== "string" || !ENV_NAME_RE.test(name)) {
|
|
45
|
+
errors.push(`${prefix}.name is not a valid environment variable name`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
variableNames.push(name);
|
|
49
|
+
if (names.has(name))
|
|
50
|
+
errors.push(`${prefix}.name duplicates ${name}`);
|
|
51
|
+
names.add(name);
|
|
52
|
+
}
|
|
53
|
+
if (raw.required !== undefined && typeof raw.required !== "boolean")
|
|
54
|
+
errors.push(`${prefix}.required must be a boolean`);
|
|
55
|
+
if (raw.secret !== undefined && typeof raw.secret !== "boolean")
|
|
56
|
+
errors.push(`${prefix}.secret must be a boolean`);
|
|
57
|
+
if ("value" in raw)
|
|
58
|
+
errors.push(`${prefix}.value is not allowed; store values in the cloud`);
|
|
59
|
+
}
|
|
60
|
+
return errors.length > 0
|
|
61
|
+
? { state: "invalid", errors }
|
|
62
|
+
: { state: "valid", errors: [], variableNames };
|
|
63
|
+
}
|
|
64
|
+
function isRecord(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|