@catalyst-cloud/cli 0.8.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 +75 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bin/catalyst-skills.js +8 -0
- package/bin/catalyst.js +5 -0
- package/bin/launch.js +154 -0
- package/dist/args.js +280 -0
- package/dist/ask.js +161 -0
- package/dist/browser.js +20 -0
- package/dist/cli.js +397 -0
- package/dist/config.js +241 -0
- package/dist/contract-types.js +4 -0
- package/dist/contract.js +184 -0
- package/dist/detach.js +10 -0
- package/dist/environment.js +207 -0
- package/dist/errors.js +27 -0
- package/dist/events.js +106 -0
- package/dist/execution.js +451 -0
- package/dist/oauth.js +300 -0
- package/dist/pagination.js +76 -0
- package/dist/prompt.js +35 -0
- package/dist/published.js +79 -0
- package/dist/query.js +248 -0
- package/dist/ready.js +380 -0
- package/dist/release.js +142 -0
- package/dist/replica.js +614 -0
- package/dist/runtime-store.js +135 -0
- package/dist/runtime-verb.js +66 -0
- package/dist/runtime.js +87 -0
- package/dist/sdk.js +29 -0
- package/dist/secret.js +190 -0
- package/dist/semver.js +18 -0
- package/dist/skill-shape.js +189 -0
- package/dist/skills.js +129 -0
- package/dist/transport.js +205 -0
- package/dist/ts-deps-loader.js +113 -0
- package/dist/watch/consumer.js +141 -0
- package/dist/watch/cursor-file.js +62 -0
- package/dist/watch.js +175 -0
- package/dist/write.js +224 -0
- package/package.json +60 -0
- package/skills/catalyst-github/SKILL.md +35 -0
- package/skills/catalyst-github/agents/openai.yaml +6 -0
- package/skills/catalyst-github/agents/portability.yaml +4 -0
- package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
- package/skills/catalyst-github/scripts/lib/cli.mjs +103 -0
- package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
- package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
- package/skills/catalyst-linear/SKILL.md +43 -0
- package/skills/catalyst-linear/agents/openai.yaml +6 -0
- package/skills/catalyst-linear/agents/portability.yaml +5 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +52 -0
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
- package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
- package/skills/catalyst-linear/scripts/comment.mjs +59 -0
- package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
- package/skills/catalyst-linear/scripts/label.mjs +48 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +164 -0
- package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-linear/scripts/move.mjs +41 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
- package/skills/catalyst-linear/scripts/search.mjs +49 -0
- package/skills/catalyst-onboard/SKILL.md +57 -0
- package/skills/catalyst-onboard/agents/openai.yaml +6 -0
- package/skills/catalyst-onboard/agents/portability.yaml +5 -0
- package/skills/catalyst-onboard/references/declaring-a-repository.md +23 -0
- package/skills/catalyst-onboard/references/skill-sources.md +35 -0
- package/skills/catalyst-onboard/references/the-one-path.md +149 -0
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +46 -0
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +50 -0
- package/skills/catalyst-onboard/references/who-fixes-what.md +44 -0
- package/skills/catalyst-onboard/scripts/lib/cli.mjs +117 -0
- package/skills/catalyst-onboard/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +345 -0
- package/skills/catalyst-setup/SKILL.md +36 -0
- package/skills/catalyst-setup/agents/openai.yaml +6 -0
- package/skills/catalyst-setup/agents/portability.yaml +4 -0
- package/skills/catalyst-setup/references/what-each-check-means.md +88 -0
- package/skills/catalyst-setup/scripts/check.mjs +75 -0
- package/skills/catalyst-setup/scripts/lib/cli.mjs +103 -0
- package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
- package/skills/connect-me/SKILL.md +63 -0
- package/skills/connect-me/agents/openai.yaml +6 -0
- package/skills/connect-me/agents/portability.yaml +5 -0
- package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
- package/skills/connect-me/scripts/lib/cli.mjs +185 -0
- package/skills/connect-me/scripts/lib/credential.mjs +29 -0
- package/skills/connect-me/scripts/verify-connection.mjs +68 -0
- package/skills/how-catalyst-works/SKILL.md +43 -0
- package/skills/how-catalyst-works/agents/openai.yaml +6 -0
- package/skills/how-catalyst-works/agents/portability.yaml +4 -0
- package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +56 -0
- package/skills/how-catalyst-works/references/the-ladder.md +41 -0
- package/skills/how-catalyst-works/references/what-catalyst-is.md +30 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +164 -0
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
- package/skills/how-catalyst-works/scripts/whats-running.mjs +65 -0
- package/skills/run-this-project/SKILL.md +45 -0
- package/skills/run-this-project/agents/openai.yaml +6 -0
- package/skills/run-this-project/agents/portability.yaml +5 -0
- package/skills/run-this-project/assets/stall-policy.json +15 -0
- package/skills/run-this-project/references/making-work-ready.md +60 -0
- package/skills/run-this-project/references/reacting-to-events.md +76 -0
- package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
- package/skills/run-this-project/scripts/lib/cli.mjs +185 -0
- package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
- package/skills/run-this-project/scripts/make-ready.mjs +64 -0
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
- package/skills/unstick/SKILL.md +41 -0
- package/skills/unstick/agents/openai.yaml +6 -0
- package/skills/unstick/agents/portability.yaml +5 -0
- package/skills/unstick/references/playbook.md +51 -0
- package/skills/unstick/scripts/lib/cli.mjs +135 -0
- package/skills/unstick/scripts/lib/credential.mjs +29 -0
- package/skills/unstick/scripts/unstick.mjs +57 -0
- package/skills/what-needs-me/SKILL.md +41 -0
- package/skills/what-needs-me/agents/openai.yaml +6 -0
- package/skills/what-needs-me/agents/portability.yaml +5 -0
- package/skills/what-needs-me/references/raising-a-decision.md +41 -0
- package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
- package/skills/what-needs-me/references/settling-an-answer.md +37 -0
- package/skills/what-needs-me/scripts/inbox.mjs +56 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +135 -0
- package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
- package/skills/what-needs-me/scripts/raise.mjs +53 -0
- package/skills/what-needs-me/scripts/settle.mjs +73 -0
- package/skills/whats-happening/SKILL.md +43 -0
- package/skills/whats-happening/agents/openai.yaml +6 -0
- package/skills/whats-happening/agents/portability.yaml +4 -0
- package/skills/whats-happening/assets/status-reply.json +77 -0
- package/skills/whats-happening/references/reading-the-board.md +43 -0
- package/skills/whats-happening/references/reprioritising.md +37 -0
- package/skills/whats-happening/references/routing-work.md +36 -0
- package/skills/whats-happening/references/status-reply.md +34 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +62 -0
- package/skills/whats-happening/scripts/explain.mjs +28 -0
- package/skills/whats-happening/scripts/lib/cli.mjs +135 -0
- package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/scripts/snapshot.mjs +149 -0
- package/vendor/README.md +9 -0
- package/vendor/paths/index.d.ts +85 -0
- package/vendor/paths/index.js +148 -0
- package/vendor/paths/legacy-installer.d.ts +36 -0
- package/vendor/paths/legacy-installer.js +154 -0
- package/vendor/paths/node.d.ts +18 -0
- package/vendor/paths/node.js +102 -0
- package/vendor/paths/provenance.json +17 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// cli.ts — the `catalyst-skills` dispatcher (npm: @catalyst-cloud/catalyst-skills). Verbs live in
|
|
2
|
+
// their own modules; this file keeps every export the 0.1 tests and the bin import.
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { parseArgs, positionals, verbHelp } from "./args.js";
|
|
5
|
+
import { CONFIG_MODE, DEFAULT_BASE_URL, LEGACY_PACKAGE_NAME, PACKAGE_NAME, cliPath, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, formatMode, loadConfig, modernCliPath, normalizeBaseUrl, readManifest, requireConfig, saveConfig, writeConfig, } from "./config.js";
|
|
6
|
+
import { loadContract, pickPath } from "./contract.js";
|
|
7
|
+
import { CliError, MeError, UsageError } from "./errors.js";
|
|
8
|
+
import { fetchMe } from "./transport.js";
|
|
9
|
+
import { bearerFor, deviceFlowLogin } from "./oauth.js";
|
|
10
|
+
import { openBrowser as defaultOpenBrowser } from "./browser.js";
|
|
11
|
+
import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning } from "./execution.js";
|
|
12
|
+
import { cmdQuery } from "./query.js";
|
|
13
|
+
import { cmdReady } from "./ready.js";
|
|
14
|
+
import { cmdReplica } from "./replica.js";
|
|
15
|
+
import { cmdRuntime } from "./runtime-verb.js";
|
|
16
|
+
import { cmdEvents } from "./events.js";
|
|
17
|
+
import { stdinIsTty } from "./prompt.js";
|
|
18
|
+
import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion } from "./skill-shape.js";
|
|
19
|
+
import { installedBundleVersion, installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine, } from "./skills.js";
|
|
20
|
+
import { cmdWatch } from "./watch.js";
|
|
21
|
+
import { cmdWrite } from "./write.js";
|
|
22
|
+
import { cmdAsk } from "./ask.js";
|
|
23
|
+
import { cmdRelease } from "./release.js";
|
|
24
|
+
import { cmdEnvironment } from "./environment.js";
|
|
25
|
+
import { cmdSecret } from "./secret.js";
|
|
26
|
+
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, };
|
|
27
|
+
export const CUSTOMER_SKILLS = [
|
|
28
|
+
"catalyst-github",
|
|
29
|
+
"catalyst-linear",
|
|
30
|
+
"catalyst-onboard",
|
|
31
|
+
"catalyst-setup",
|
|
32
|
+
"connect-me",
|
|
33
|
+
"how-catalyst-works",
|
|
34
|
+
"run-this-project",
|
|
35
|
+
"unstick",
|
|
36
|
+
"what-needs-me",
|
|
37
|
+
"whats-happening",
|
|
38
|
+
];
|
|
39
|
+
/** `login` is the verb; `join` is the 0.1 name, kept as a deprecated alias for one minor version. */
|
|
40
|
+
const VERB_ALIASES = { join: "login" };
|
|
41
|
+
/** Verbs whose stdout is for a human, so the update notice may share it. Every other verb's stdout
|
|
42
|
+
* is machine-read by a skill script and the notice goes to stderr. */
|
|
43
|
+
const HUMAN_VERBS = new Set(["login", "install", "status", "notice"]);
|
|
44
|
+
export function usageText() {
|
|
45
|
+
return [
|
|
46
|
+
`${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your tenant, read through the SDK, write through the agent proxy`,
|
|
47
|
+
"",
|
|
48
|
+
"Usage:",
|
|
49
|
+
" catalyst-skills login [--base-url <url>] [--start-replica] (keyless: logs you in as yourself)",
|
|
50
|
+
" catalyst-skills login --key <personal-key> [--base-url <url>] (or CATALYST_CLOUD_TOKEN, for a key)",
|
|
51
|
+
" catalyst-skills join ... (deprecated alias of login; removed in the next minor version)",
|
|
52
|
+
" catalyst-skills install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
|
|
53
|
+
" catalyst-skills status | notice | me | ready | accounts",
|
|
54
|
+
" catalyst-skills contract [--refresh] [--path <a.b.c>]",
|
|
55
|
+
" catalyst-skills query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
|
|
56
|
+
" catalyst-skills replica <start [--detach]|stop|status [--probe]|sql \"<select>\"|schema [table]>",
|
|
57
|
+
" catalyst-skills runtime <status [--json]|install|path|uninstall> (a pinned Node this CLI manages itself)",
|
|
58
|
+
" catalyst-skills events <tail|wait-for|query> [--type NAME] [--ticket CTC-N] [--after SEQUENCE]",
|
|
59
|
+
" catalyst-skills explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
|
|
60
|
+
" catalyst-skills watch [--team K] [--ticket T]... [--project P] [--exec CMD]",
|
|
61
|
+
" catalyst-skills write <comment|state|label|create|reaction|attachment|session> ...",
|
|
62
|
+
" catalyst-skills ask <raise|accept|list> ...",
|
|
63
|
+
" catalyst-skills release <ticket> --because <what changed> [--retry-unchanged] [--dry-run] | release --class <c> --team <K> ...",
|
|
64
|
+
" catalyst-skills environment [read] | environment propose --file <path>|--stdin [--approve] | environment approve",
|
|
65
|
+
" catalyst-skills secret set <NAME> --repo <owner/name> [--command '<cmd>'] | secret import <file> --repo <owner/name>",
|
|
66
|
+
"",
|
|
67
|
+
"Every verb takes --help. --json makes the output machine-readable.",
|
|
68
|
+
"",
|
|
69
|
+
"With no key, login is keyless: it opens a WorkOS device-code flow, prints a short code and a URL,",
|
|
70
|
+
"and connects you as yourself once you approve in the browser — nothing to mint or paste. Pass",
|
|
71
|
+
"--key (or set CATALYST_CLOUD_TOKEN) to use a personal or account key instead. The base URL defaults",
|
|
72
|
+
`to CATALYST_CLOUD_BASE_URL or ${DEFAULT_BASE_URL}.`,
|
|
73
|
+
"",
|
|
74
|
+
"login discovers your tenant from GET /api/v1/me, writes ~/.config/catalyst-cloud/customer.json",
|
|
75
|
+
"(0600) holding your login session (or key) and this CLI's path, and caches the tenant contract",
|
|
76
|
+
"beside it. A keyless session's short-lived token refreshes silently on every request. login does",
|
|
77
|
+
"not install skills: your agent's own install command does that, and `install` is only here to",
|
|
78
|
+
"repair a copy this package made.",
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* CTC-3479 — a login before the rename recorded bin/catalyst-skills.js as cliPath. Skill scripts
|
|
83
|
+
* spawn that path, and it now prints the deprecated-name notice on every call. Move the record onto
|
|
84
|
+
* the `catalyst` launcher beside it, once, on the next run of either name. Read and write back to
|
|
85
|
+
* back, so the window for racing a concurrent OAuth refresh is as small as the update notice's.
|
|
86
|
+
*/
|
|
87
|
+
function migrateLegacyCliPath(ctx) {
|
|
88
|
+
let cfg;
|
|
89
|
+
try {
|
|
90
|
+
cfg = loadConfig(ctx.home);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!cfg)
|
|
96
|
+
return;
|
|
97
|
+
const next = modernCliPath(cfg.cliPath);
|
|
98
|
+
if (next === null)
|
|
99
|
+
return;
|
|
100
|
+
cfg.cliPath = next;
|
|
101
|
+
try {
|
|
102
|
+
saveConfig(ctx.home, cfg);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Best effort: the old launcher still works, it only prints the notice.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The Tier-2 update path: when the bundle on disk is newer than the version the config last
|
|
110
|
+
* recorded, refresh the copied skills FIRST and record the new version only once that succeeded.
|
|
111
|
+
*/
|
|
112
|
+
function maybePrintUpdateNotice(args, ctx) {
|
|
113
|
+
const say = HUMAN_VERBS.has(args.command ?? "") ? ctx.stdout : ctx.stderr;
|
|
114
|
+
let cfg;
|
|
115
|
+
try {
|
|
116
|
+
cfg = loadConfig(ctx.home);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!cfg)
|
|
122
|
+
return;
|
|
123
|
+
const manifest = readManifest();
|
|
124
|
+
if (cfg.lastSkillBundleVersion === manifest.version)
|
|
125
|
+
return;
|
|
126
|
+
const previous = cfg.lastSkillBundleVersion;
|
|
127
|
+
if (previous) {
|
|
128
|
+
const entry = parseChangelogEntry(readChangelog(), manifest.version);
|
|
129
|
+
say(updateNoticeLine(previous, manifest.version, entry));
|
|
130
|
+
}
|
|
131
|
+
const skillsDir = resolveSkillsDir(args, ctx, cfg);
|
|
132
|
+
let refreshed;
|
|
133
|
+
try {
|
|
134
|
+
refreshed = installSkills(skillsDir, { force: false, onlyExisting: true });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
ctx.stderr(`[catalyst-skills] could not refresh the skills at ${skillsDir} (${err instanceof Error ? err.message : String(err)}) — the ${manifest.version} skills are not installed yet; run: catalyst-skills install`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (refreshed.installed.length > 0)
|
|
141
|
+
say(`[catalyst-skills] refreshed ${refreshed.installed.join(", ")} at ${skillsDir} to ${manifest.version}`);
|
|
142
|
+
for (const s of refreshed.skipped) {
|
|
143
|
+
say(`[catalyst-skills] left "${s.name}" alone: ${skillsDir}/${s.name} was not installed by this package (catalyst-skills install --force to replace)`);
|
|
144
|
+
}
|
|
145
|
+
cfg.lastSkillBundleVersion = manifest.version;
|
|
146
|
+
cfg.skillsDir = skillsDir;
|
|
147
|
+
saveConfig(ctx.home, cfg);
|
|
148
|
+
}
|
|
149
|
+
export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
150
|
+
let args;
|
|
151
|
+
try {
|
|
152
|
+
args = parseArgs(argv.map((a, i) => (i === 0 && VERB_ALIASES[a] ? VERB_ALIASES[a] : a)));
|
|
153
|
+
if (args.command && VERB_ALIASES[args.command])
|
|
154
|
+
args.command = VERB_ALIASES[args.command];
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
ctx.stderr(err instanceof Error ? err.message : String(err));
|
|
158
|
+
ctx.stderr(usageText());
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
const manifest = readManifest();
|
|
162
|
+
if (args.version) {
|
|
163
|
+
ctx.stdout(`${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (args.command === null) {
|
|
167
|
+
ctx.stdout(usageText());
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
if (args.help) {
|
|
171
|
+
ctx.stdout(verbHelp(args.command));
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
migrateLegacyCliPath(ctx);
|
|
176
|
+
maybePrintUpdateNotice(args, ctx);
|
|
177
|
+
switch (args.command) {
|
|
178
|
+
case "login":
|
|
179
|
+
return await cmdLogin(args, ctx, deps);
|
|
180
|
+
case "install":
|
|
181
|
+
return cmdInstall(args, ctx);
|
|
182
|
+
case "notice":
|
|
183
|
+
return 0;
|
|
184
|
+
case "status":
|
|
185
|
+
return cmdStatus(ctx);
|
|
186
|
+
case "me":
|
|
187
|
+
return await cmdMe(args, ctx);
|
|
188
|
+
case "contract":
|
|
189
|
+
return await cmdContract(args, ctx);
|
|
190
|
+
case "query":
|
|
191
|
+
return await cmdQuery(args, ctx, { engineDeps: deps.replica?.engineDeps });
|
|
192
|
+
case "replica":
|
|
193
|
+
return await cmdReplica(args, ctx, deps.replica);
|
|
194
|
+
case "runtime":
|
|
195
|
+
return await cmdRuntime(args, ctx, deps.runtime);
|
|
196
|
+
case "events":
|
|
197
|
+
return await cmdEvents(args, ctx, deps.events);
|
|
198
|
+
case "explain":
|
|
199
|
+
return await cmdExplain(args, ctx);
|
|
200
|
+
case "history":
|
|
201
|
+
return await cmdHistory(args, ctx);
|
|
202
|
+
case "running":
|
|
203
|
+
return await cmdRunning(args, ctx);
|
|
204
|
+
case "queue":
|
|
205
|
+
return await cmdQueue(args, ctx);
|
|
206
|
+
case "watch":
|
|
207
|
+
return await cmdWatch(args, ctx, deps.watch);
|
|
208
|
+
case "write":
|
|
209
|
+
return await cmdWrite(args, ctx, deps.write);
|
|
210
|
+
case "ask":
|
|
211
|
+
return await cmdAsk(args, ctx);
|
|
212
|
+
case "ready":
|
|
213
|
+
return await cmdReady(args, ctx, { skillNames: CUSTOMER_SKILLS, loadSdk: deps.loadSdk, offline: args.flags.offline === true });
|
|
214
|
+
case "accounts":
|
|
215
|
+
return await cmdAccounts(args, ctx);
|
|
216
|
+
case "release":
|
|
217
|
+
return await cmdRelease(args, ctx);
|
|
218
|
+
case "environment":
|
|
219
|
+
return await cmdEnvironment(args, ctx, deps.environment ?? {});
|
|
220
|
+
case "secret":
|
|
221
|
+
return await cmdSecret(args, ctx, deps.secret ?? {});
|
|
222
|
+
default:
|
|
223
|
+
ctx.stderr(`unknown command: ${args.command}`);
|
|
224
|
+
ctx.stderr(usageText());
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
if (err instanceof UsageError) {
|
|
230
|
+
ctx.stderr(err.message);
|
|
231
|
+
ctx.stderr(args.command && args.command in VERB_HELP_KNOWN ? verbHelp(args.command) : usageText());
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
if (err instanceof CliError) {
|
|
235
|
+
ctx.stderr(`catalyst-skills: ${err.message}`);
|
|
236
|
+
return err.exitCode;
|
|
237
|
+
}
|
|
238
|
+
if (err instanceof MeError) {
|
|
239
|
+
ctx.stderr(`catalyst-skills: ${err.message}`);
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const VERB_HELP_KNOWN = Object.fromEntries(["login", "join", "install", "status", "notice", "me", "contract", "query", "replica", "runtime", "events", "explain", "running", "queue", "watch", "write", "ask", "ready", "accounts", "release", "secret"].map((v) => [v, true]));
|
|
246
|
+
async function cmdLogin(args, ctx, deps) {
|
|
247
|
+
const manifest = readManifest();
|
|
248
|
+
const key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
|
|
249
|
+
const baseUrl = normalizeBaseUrl(args.baseUrl ?? ctx.env.CATALYST_CLOUD_BASE_URL ?? DEFAULT_BASE_URL);
|
|
250
|
+
// Keyless is the preferred rail: with no key in flag or env, run the WorkOS device flow and log in
|
|
251
|
+
// as the person. A key (flag or CATALYST_CLOUD_TOKEN) still takes the token rail unchanged.
|
|
252
|
+
let auth;
|
|
253
|
+
const bearer = key
|
|
254
|
+
? key
|
|
255
|
+
: (auth = await deviceFlowLogin(ctx, baseUrl, {
|
|
256
|
+
isTty: deps.isTty ?? stdinIsTty,
|
|
257
|
+
openBrowser: deps.openBrowser ?? defaultOpenBrowser,
|
|
258
|
+
sleep: deps.sleep,
|
|
259
|
+
})).accessToken;
|
|
260
|
+
const me = await fetchMe(baseUrl, bearer, ctx.fetch);
|
|
261
|
+
let existing;
|
|
262
|
+
try {
|
|
263
|
+
existing = loadConfig(ctx.home);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
existing = null;
|
|
267
|
+
}
|
|
268
|
+
const previous = existing?.lastSkillBundleVersion ?? null;
|
|
269
|
+
const skillsDir = resolveSkillsDir(args, ctx, existing);
|
|
270
|
+
const config = {
|
|
271
|
+
baseUrl,
|
|
272
|
+
...(key ? { key } : {}),
|
|
273
|
+
...(auth ? { auth } : {}),
|
|
274
|
+
account: me.account,
|
|
275
|
+
slug: me.slug,
|
|
276
|
+
name: me.name,
|
|
277
|
+
permissions: me.permissions,
|
|
278
|
+
principal: me.principal,
|
|
279
|
+
...(me.user ? { user: me.user } : {}),
|
|
280
|
+
joinedAt: ctx.now().toISOString(),
|
|
281
|
+
lastSkillBundleVersion: manifest.version,
|
|
282
|
+
skillsDir,
|
|
283
|
+
cliPath: cliPath(),
|
|
284
|
+
replicaDb: existing?.replicaDb ?? defaultReplicaDbFor(ctx.home),
|
|
285
|
+
};
|
|
286
|
+
const written = writeConfig(ctx.home, config);
|
|
287
|
+
ctx.stdout(`Connected to ${me.name} (${me.slug}) — account ${me.account}`);
|
|
288
|
+
if (me.user) {
|
|
289
|
+
ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId ? "" : " — your Linear identity is not matched yet, so \"what needs me\" will show everyone's asks until an admin matches it in Settings"}`);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
ctx.stderr(`[catalyst-skills] 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.`);
|
|
293
|
+
}
|
|
294
|
+
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 ? "" : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
|
|
295
|
+
ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
|
|
296
|
+
try {
|
|
297
|
+
const loaded = await loadContract(ctx, config, { refresh: true });
|
|
298
|
+
ctx.stdout(`Tenant contract ${loaded.doc.contractVersion} cached at ${loaded.path}`);
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
if (err instanceof CliError && (err.code === "contract-forbidden" || err.code === "contract-version")) {
|
|
302
|
+
ctx.stderr(`[catalyst-skills] ${err.message}`);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (previous && previous !== manifest.version) {
|
|
309
|
+
const entry = parseChangelogEntry(readChangelog(), manifest.version);
|
|
310
|
+
ctx.stdout(updateNoticeLine(previous, manifest.version, entry));
|
|
311
|
+
}
|
|
312
|
+
if (args.flags["start-replica"] === true) {
|
|
313
|
+
const code = await cmdReplica(parseArgs(["replica", "start", "--detach"]), ctx, {
|
|
314
|
+
...deps.replica,
|
|
315
|
+
argv: deps.replica?.argv ?? [cliPath(), "replica", "start"],
|
|
316
|
+
});
|
|
317
|
+
if (code !== 0)
|
|
318
|
+
return code;
|
|
319
|
+
}
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
function cmdInstall(args, ctx) {
|
|
323
|
+
let cfg;
|
|
324
|
+
try {
|
|
325
|
+
cfg = loadConfig(ctx.home);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
cfg = null;
|
|
329
|
+
}
|
|
330
|
+
const skillsDir = resolveSkillsDir(args, ctx, cfg);
|
|
331
|
+
const result = installSkills(skillsDir, { force: args.force });
|
|
332
|
+
ctx.stdout(result.installed.length > 0
|
|
333
|
+
? `Skills installed to ${skillsDir}: ${result.installed.join(", ")}`
|
|
334
|
+
: `No skills installed — ${skillsDir} already had them`);
|
|
335
|
+
for (const s of result.skipped) {
|
|
336
|
+
ctx.stdout(`Skipped "${s.name}": ${skillsDir}/${s.name} exists and was not installed by this package (use --force to replace)`);
|
|
337
|
+
}
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
function cmdStatus(ctx) {
|
|
341
|
+
const manifest = readManifest();
|
|
342
|
+
const cfg = loadConfig(ctx.home);
|
|
343
|
+
if (!cfg) {
|
|
344
|
+
ctx.stdout(`Not connected yet — run: npx ${LEGACY_PACKAGE_NAME} login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN)`);
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
ctx.stdout(`Tenant: ${cfg.name} (${cfg.slug}) — account ${cfg.account}`);
|
|
348
|
+
ctx.stdout(`API: ${cfg.baseUrl} (principal: ${cfg.principal})`);
|
|
349
|
+
ctx.stdout(cfg.user ? `As: ${cfg.user.label} (${cfg.user.role})` : "As: the tenant's account key (a host credential — no person)");
|
|
350
|
+
ctx.stdout(cfg.auth ? `Credential: your login (expires ${relativeExpiry(cfg.auth.expiresAt, ctx.now())})` : "Credential: personal key");
|
|
351
|
+
ctx.stdout(`Bundle: ${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
|
|
352
|
+
if (cfg.cliPath)
|
|
353
|
+
ctx.stdout(`CLI: ${cfg.cliPath}${existsSync(cfg.cliPath) ? "" : " (missing — re-run login)"}`);
|
|
354
|
+
ctx.stdout(`Contract: ${existsSync(contractPathFor(ctx.home)) ? contractPathFor(ctx.home) : "not cached (run: catalyst-skills contract --refresh)"}`);
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
/** A short human relative time for an ISO expiry, e.g. "in 14m", "in 2h", "in 6d", or "now" once past. */
|
|
358
|
+
function relativeExpiry(iso, now) {
|
|
359
|
+
const ms = Date.parse(iso) - now.getTime();
|
|
360
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
361
|
+
return "now";
|
|
362
|
+
const mins = Math.round(ms / 60_000);
|
|
363
|
+
if (mins < 60)
|
|
364
|
+
return `in ${mins}m`;
|
|
365
|
+
const hours = Math.round(mins / 60);
|
|
366
|
+
if (hours < 48)
|
|
367
|
+
return `in ${hours}h`;
|
|
368
|
+
return `in ${Math.round(hours / 24)}d`;
|
|
369
|
+
}
|
|
370
|
+
async function cmdMe(args, ctx) {
|
|
371
|
+
const cfg = requireConfig(ctx);
|
|
372
|
+
const me = await fetchMe(cfg.baseUrl, await bearerFor(ctx, cfg), ctx.fetch);
|
|
373
|
+
if (args.json)
|
|
374
|
+
ctx.stdout(JSON.stringify(me));
|
|
375
|
+
else {
|
|
376
|
+
ctx.stdout(`${me.name} (${me.slug}) — account ${me.account}`);
|
|
377
|
+
ctx.stdout(`principal: ${me.principal}; permissions: ${me.permissions ? me.permissions.join(", ") : "unrestricted"}`);
|
|
378
|
+
if (me.user)
|
|
379
|
+
ctx.stdout(`user: ${me.user.label} (${me.user.role}); linear: ${me.user.linearUserId ?? "unmatched"}`);
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
async function cmdContract(args, ctx) {
|
|
384
|
+
const cfg = requireConfig(ctx);
|
|
385
|
+
const [sub] = positionals(args);
|
|
386
|
+
if (sub)
|
|
387
|
+
throw new UsageError(`contract takes no positional argument (got "${sub}"); use --path <a.b.c>`);
|
|
388
|
+
const refresh = args.flags.refresh === true;
|
|
389
|
+
const loaded = await loadContract(ctx, cfg, { refresh });
|
|
390
|
+
ctx.stderr(`contract: ${loaded.doc.contractVersion} from ${loaded.source}${loaded.source === "cache" ? ` (${loaded.ageSeconds}s old)` : ""}`);
|
|
391
|
+
const path = typeof args.flags.path === "string" ? args.flags.path : undefined;
|
|
392
|
+
const value = path ? pickPath(loaded.doc, path) : loaded.doc;
|
|
393
|
+
if (path && value === undefined)
|
|
394
|
+
throw new CliError(`the contract has nothing at "${path}"`, "contract-path");
|
|
395
|
+
ctx.stdout(typeof value === "string" ? value : JSON.stringify(value, null, args.json ? 0 : 2));
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// config.ts — ~/.config/catalyst-cloud/customer.json and its siblings. The ONLY place the base URL
|
|
2
|
+
// and the `/api/v1` prefix are joined: the SDK wants the base with the prefix, GET /me without.
|
|
3
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { CliError } from "./errors.js";
|
|
8
|
+
import { parseMachinePaths, resolveCatalystPath } from "../vendor/paths/index.js";
|
|
9
|
+
import { machinePathsFile } from "../vendor/paths/node.js";
|
|
10
|
+
/** CTC-3479 — the package this CLI publishes as. Its command is `catalyst`. */
|
|
11
|
+
export const PACKAGE_NAME = "@catalyst-cloud/cli";
|
|
12
|
+
/** The old package name, now a thin forwarder that depends on PACKAGE_NAME at the same version. The
|
|
13
|
+
* npx login hints keep naming it until CTC-3480/3481 move them; upgrade hints name whichever package
|
|
14
|
+
* is installed (updatePackageName). `npx @catalyst-cloud/cli`
|
|
15
|
+
* cannot run a verb: that package has two bins and neither is named `cli`, so npx cannot pick one.
|
|
16
|
+
* The forwarder has a bin named after itself, so `npx @catalyst-cloud/catalyst-skills` still works. */
|
|
17
|
+
export const LEGACY_PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
|
|
18
|
+
export const DEFAULT_BASE_URL = "https://staging.catalystcloud.dev";
|
|
19
|
+
export const CONFIG_MODE = 0o600;
|
|
20
|
+
export function defaultCtx() {
|
|
21
|
+
return {
|
|
22
|
+
env: process.env,
|
|
23
|
+
home: process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/",
|
|
24
|
+
stdout: (line) => console.log(line),
|
|
25
|
+
stderr: (line) => console.error(line),
|
|
26
|
+
fetch: fetch,
|
|
27
|
+
now: () => new Date(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function configDirFor(home) {
|
|
31
|
+
return join(home, ".config", "catalyst-cloud");
|
|
32
|
+
}
|
|
33
|
+
export function configPathFor(home) {
|
|
34
|
+
return join(configDirFor(home), "customer.json");
|
|
35
|
+
}
|
|
36
|
+
export function contractPathFor(home) {
|
|
37
|
+
return join(configDirFor(home), "contract.json");
|
|
38
|
+
}
|
|
39
|
+
/** CTC-2160 — the cached answer to "what is the newest published release" (src/published.ts). */
|
|
40
|
+
export function publishedCachePathFor(home) {
|
|
41
|
+
return join(configDirFor(home), "published.json");
|
|
42
|
+
}
|
|
43
|
+
export function watchCursorPathFor(home) {
|
|
44
|
+
return join(configDirFor(home), "watch-cursor.json");
|
|
45
|
+
}
|
|
46
|
+
/** CTC-2112 — where the cached CLI-auth discovery document lives (the `/api/v1/auth/cli` answer). */
|
|
47
|
+
export function discoveryCachePathFor(home) {
|
|
48
|
+
return join(configDirFor(home), "auth-discovery.json");
|
|
49
|
+
}
|
|
50
|
+
export function defaultReplicaDbFor(home) {
|
|
51
|
+
return join(configDirFor(home), "replica.db");
|
|
52
|
+
}
|
|
53
|
+
export function defaultSkillsDirFor(home) {
|
|
54
|
+
return join(home, ".claude", "skills");
|
|
55
|
+
}
|
|
56
|
+
/** The CLI launcher this very package ships, recorded by login so skill scripts can spawn it. It is
|
|
57
|
+
* the `catalyst` launcher, so a skill script's call does not print the deprecated-name line. */
|
|
58
|
+
export function cliPath() {
|
|
59
|
+
return fileURLToPath(new URL("../bin/catalyst.js", import.meta.url));
|
|
60
|
+
}
|
|
61
|
+
/** This package's root directory (the one holding package.json, bin/ and dist/). */
|
|
62
|
+
export function packageRoot() {
|
|
63
|
+
return fileURLToPath(new URL("..", import.meta.url));
|
|
64
|
+
}
|
|
65
|
+
function packageNameAt(dir) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
68
|
+
return typeof raw.name === "string" ? raw.name : null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* CTC-3479 — the package whose `npm install -g <name>@latest` updates this install. Both packages
|
|
76
|
+
* ship the `catalyst` and `catalyst-skills` bins, so npm refuses to install one over the other
|
|
77
|
+
* (EEXIST). An upgrade hint must therefore name the package this machine already has.
|
|
78
|
+
*
|
|
79
|
+
* The forwarder carries this package as its dependency, in one of two layouts:
|
|
80
|
+
* - nested, as `npm install -g` lays it out: `<forwarder>/node_modules/@catalyst-cloud/cli`;
|
|
81
|
+
* - flat, as npx and a project install lay it out: `node_modules/@catalyst-cloud/catalyst-skills`
|
|
82
|
+
* sits beside `node_modules/@catalyst-cloud/cli`.
|
|
83
|
+
* Anything else is a direct install of this package (or a checkout of this repository).
|
|
84
|
+
*/
|
|
85
|
+
export function updatePackageName(root = packageRoot()) {
|
|
86
|
+
const scopeDir = dirname(root);
|
|
87
|
+
const nested = dirname(dirname(scopeDir));
|
|
88
|
+
if (packageNameAt(nested) === LEGACY_PACKAGE_NAME)
|
|
89
|
+
return LEGACY_PACKAGE_NAME;
|
|
90
|
+
if (packageNameAt(join(scopeDir, "catalyst-skills")) === LEGACY_PACKAGE_NAME)
|
|
91
|
+
return LEGACY_PACKAGE_NAME;
|
|
92
|
+
return PACKAGE_NAME;
|
|
93
|
+
}
|
|
94
|
+
/** The one upgrade command every hint prints: update the installed package, then log in again so
|
|
95
|
+
* customer.json records the new launcher path. */
|
|
96
|
+
export function upgradeCommand(root) {
|
|
97
|
+
return `npm install -g ${updatePackageName(root)}@latest && catalyst-skills login`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* CTC-3479 — logins before this release recorded bin/catalyst-skills.js as cliPath. That launcher is
|
|
101
|
+
* now the deprecated name and prints a notice, so every skill script call through it would carry
|
|
102
|
+
* the notice. When the recorded launcher has a `catalyst.js` beside it (any 0.8.0-or-later install of
|
|
103
|
+
* either package), point cliPath at that instead. Returns the new path, or null when nothing changes.
|
|
104
|
+
*/
|
|
105
|
+
export function modernCliPath(recorded) {
|
|
106
|
+
if (typeof recorded !== "string" || !recorded.endsWith("catalyst-skills.js"))
|
|
107
|
+
return null;
|
|
108
|
+
const sibling = join(dirname(recorded), "catalyst.js");
|
|
109
|
+
return existsSync(sibling) ? sibling : null;
|
|
110
|
+
}
|
|
111
|
+
export function normalizeBaseUrl(url) {
|
|
112
|
+
return url.replace(/\/+$/, "");
|
|
113
|
+
}
|
|
114
|
+
/** The origin WITH the versioned prefix — what the SDK and every API read/write take. */
|
|
115
|
+
export function apiBase(cfg) {
|
|
116
|
+
return `${normalizeBaseUrl(cfg.baseUrl)}/api/v1`;
|
|
117
|
+
}
|
|
118
|
+
function machineFilePresent(file) {
|
|
119
|
+
try {
|
|
120
|
+
lstatSync(file);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
125
|
+
return false;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function replicaDbPath(cfg, home, env = {}) {
|
|
130
|
+
if (env.CATALYST_REPLICA_DB !== undefined)
|
|
131
|
+
return resolveCatalystPath("replicaDb", { env });
|
|
132
|
+
const file = machinePathsFile({ env: { ...env, HOME: home } });
|
|
133
|
+
if (file && (env.CATALYST_PATHS_FILE !== undefined || machineFilePresent(file))) {
|
|
134
|
+
const machine = parseMachinePaths(JSON.parse(readFileSync(file, "utf8")));
|
|
135
|
+
if (machine.paths.replicaDb === undefined)
|
|
136
|
+
throw new CliError("optional replica is not configured; set CATALYST_REPLICA_DB or declare replicaDb in the machine paths file", "replica-not-configured");
|
|
137
|
+
return resolveCatalystPath("replicaDb", { env, machine });
|
|
138
|
+
}
|
|
139
|
+
// Compatibility for machines that have not run paths setup. Keep the existing DB in place.
|
|
140
|
+
return resolveCatalystPath("replicaDb", { overrides: { replicaDb: cfg.replicaDb ?? defaultReplicaDbFor(home) } });
|
|
141
|
+
}
|
|
142
|
+
export function loadConfig(home) {
|
|
143
|
+
const path = configPathFor(home);
|
|
144
|
+
if (!existsSync(path))
|
|
145
|
+
return null;
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
throw new CliError(`config at ${path} is not valid JSON — re-run login to rewrite it`, "config-corrupt");
|
|
152
|
+
}
|
|
153
|
+
const cfg = parsed;
|
|
154
|
+
if (typeof cfg.account !== "string" || typeof cfg.baseUrl !== "string") {
|
|
155
|
+
throw new CliError(`config at ${path} is missing required fields — re-run login to rewrite it`, "config-corrupt");
|
|
156
|
+
}
|
|
157
|
+
const hasKey = typeof cfg.key === "string" && cfg.key !== "";
|
|
158
|
+
const hasOauth = isOauthAuth(cfg.auth);
|
|
159
|
+
if (hasKey === hasOauth) {
|
|
160
|
+
throw new CliError(`config at ${path} must hold exactly one of a personal key or an OAuth session — re-run login to rewrite it`, "config-corrupt");
|
|
161
|
+
}
|
|
162
|
+
return cfg;
|
|
163
|
+
}
|
|
164
|
+
/** Load the config or refuse with the one line every verb prints when the machine is not connected. */
|
|
165
|
+
export function requireConfig(ctx) {
|
|
166
|
+
const cfg = loadConfig(ctx.home);
|
|
167
|
+
if (!cfg) {
|
|
168
|
+
throw new CliError(`not connected yet — run: npx ${LEGACY_PACKAGE_NAME} login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN for a key)`, "not-configured");
|
|
169
|
+
}
|
|
170
|
+
return cfg;
|
|
171
|
+
}
|
|
172
|
+
export function saveConfig(home, cfg) {
|
|
173
|
+
return writeConfig(home, cfg).path;
|
|
174
|
+
}
|
|
175
|
+
/** True when `v` is a well-formed OAuth session block. */
|
|
176
|
+
export function isOauthAuth(v) {
|
|
177
|
+
if (typeof v !== "object" || v === null)
|
|
178
|
+
return false;
|
|
179
|
+
const a = v;
|
|
180
|
+
return (a.kind === "oauth" &&
|
|
181
|
+
typeof a.accessToken === "string" &&
|
|
182
|
+
typeof a.refreshToken === "string" &&
|
|
183
|
+
typeof a.expiresAt === "string" &&
|
|
184
|
+
typeof a.sessionId === "string");
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Write the config atomically (a rotated token must never be half-written under a crash) and return
|
|
188
|
+
* the mode the file ACTUALLY carries afterwards. Write to a sibling `.tmp` at 0600, then rename over
|
|
189
|
+
* the target — rename is atomic within a directory — and chmod once more (a pre-existing target keeps
|
|
190
|
+
* its own mode through the rename on some platforms).
|
|
191
|
+
*/
|
|
192
|
+
export function writeConfig(home, cfg) {
|
|
193
|
+
const path = configPathFor(home);
|
|
194
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
195
|
+
// A UNIQUE sibling temp per write (pid + randomness): two concurrent CLI processes reaching the
|
|
196
|
+
// OAuth refresh window must not share `customer.json.tmp`, or one renames/removes it out from under
|
|
197
|
+
// the other and the second write fails with ENOENT or lands the wrong contents (Codex P2).
|
|
198
|
+
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
199
|
+
try {
|
|
200
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: CONFIG_MODE });
|
|
201
|
+
chmodSync(tmp, CONFIG_MODE);
|
|
202
|
+
renameSync(tmp, path);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
try {
|
|
206
|
+
rmSync(tmp, { force: true });
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// best-effort cleanup of the temp file
|
|
210
|
+
}
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
chmodSync(path, CONFIG_MODE);
|
|
214
|
+
return { path, mode: statSync(path).mode & 0o777 };
|
|
215
|
+
}
|
|
216
|
+
export function formatMode(mode) {
|
|
217
|
+
return "0" + mode.toString(8);
|
|
218
|
+
}
|
|
219
|
+
let manifestCache = null;
|
|
220
|
+
export function readManifest() {
|
|
221
|
+
if (manifestCache)
|
|
222
|
+
return manifestCache;
|
|
223
|
+
const raw = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
224
|
+
if (!raw.engines?.node) {
|
|
225
|
+
throw new CliError("package.json is missing engines.node — the supported Node range is not optional", "manifest-corrupt");
|
|
226
|
+
}
|
|
227
|
+
if (!raw.catalystCloud?.pinnedNode) {
|
|
228
|
+
throw new CliError("package.json is missing catalystCloud.pinnedNode — the pinned runtime version is not optional", "manifest-corrupt");
|
|
229
|
+
}
|
|
230
|
+
manifestCache = {
|
|
231
|
+
version: raw.version,
|
|
232
|
+
tenantContractRange: raw.catalystCloud?.tenantContractRange ?? "unpinned",
|
|
233
|
+
enginesNode: raw.engines.node,
|
|
234
|
+
pinnedNode: raw.catalystCloud.pinnedNode,
|
|
235
|
+
};
|
|
236
|
+
return manifestCache;
|
|
237
|
+
}
|
|
238
|
+
/** Test seam: forget the cached manifest. */
|
|
239
|
+
export function resetManifestCache() {
|
|
240
|
+
manifestCache = null;
|
|
241
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// contract-types.ts — the SHAPE of GET /api/v1/agent/contract as this bundle reads it. Typed here
|
|
2
|
+
// (the cloud's own declaration lives in another repository) and filled only from the wire. No field
|
|
3
|
+
// below carries a default: a tenant value the contract does not serve is refused, never guessed.
|
|
4
|
+
export {};
|