@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// runtime-store.ts — CTC-2158, Tier 2. The pinned runtime this CLI manages itself.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS AND NOT A POSTINSTALL. A postinstall download is skipped by `--ignore-scripts` (which is
|
|
4
|
+
// how this very container's own install ran: `bun install --frozen-lockfile --ignore-scripts`), is
|
|
5
|
+
// skipped entirely by `npx`, and fetches-and-executes a binary the customer never asked for. This is
|
|
6
|
+
// one explicit command instead — the command every unsupported-runtime message prints — and it never
|
|
7
|
+
// touches the machine's default Node.
|
|
8
|
+
//
|
|
9
|
+
// The checksum is verified BEFORE anything is unpacked. `.tar.gz`, never `.tar.xz`: `tar -xzf` works
|
|
10
|
+
// everywhere including macOS bsdtar, which cannot be assumed to carry liblzma.
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { CliError } from "./errors.js";
|
|
16
|
+
const PLATFORM_ARCH = {
|
|
17
|
+
darwin: ["arm64", "x64"],
|
|
18
|
+
linux: ["x64", "arm64"],
|
|
19
|
+
};
|
|
20
|
+
/** Map a version/platform/arch onto a real nodejs.org `.tar.gz` name. win32 refuses by name — Windows
|
|
21
|
+
* needs `.zip` extraction, a separate mechanism this ticket does not build (D7). */
|
|
22
|
+
export function tarballFor(version, platform, arch) {
|
|
23
|
+
if (platform === "win32") {
|
|
24
|
+
throw new CliError("catalyst-skills runtime install does not support Windows yet — supported: darwin (arm64, x64) and linux (x64, arm64). Use whatever Node 22.15+ or bun 1.4+ you already have on Windows.", "runtime-unsupported-platform");
|
|
25
|
+
}
|
|
26
|
+
const arches = PLATFORM_ARCH[platform];
|
|
27
|
+
if (!arches || !arches.includes(arch)) {
|
|
28
|
+
throw new CliError(`catalyst-skills runtime install does not support ${platform}/${arch} — supported: darwin (arm64, x64) and linux (x64, arm64)`, "runtime-unsupported-platform");
|
|
29
|
+
}
|
|
30
|
+
const file = `node-v${version}-${platform}-${arch}.tar.gz`;
|
|
31
|
+
return { file, url: `https://nodejs.org/dist/v${version}/${file}`, shasums: `https://nodejs.org/dist/v${version}/SHASUMS256.txt` };
|
|
32
|
+
}
|
|
33
|
+
/** Pick our file's line out of a real SHASUMS256.txt body. A missing line is a named error — never a
|
|
34
|
+
* skipped verification. */
|
|
35
|
+
export function shasumFor(shasumsBody, file) {
|
|
36
|
+
for (const line of shasumsBody.split("\n")) {
|
|
37
|
+
const [sum, name] = line.trim().split(/\s+/);
|
|
38
|
+
if (name === file && sum)
|
|
39
|
+
return sum;
|
|
40
|
+
}
|
|
41
|
+
throw new CliError(`no line for ${file} in SHASUMS256.txt — refusing to install without a checksum to verify against`, "runtime-checksum-missing");
|
|
42
|
+
}
|
|
43
|
+
export function runtimesDir(home) {
|
|
44
|
+
return join(home, ".cache", "catalyst-cloud", "catalyst-skills", "runtimes");
|
|
45
|
+
}
|
|
46
|
+
export function pinPath(home) {
|
|
47
|
+
return join(runtimesDir(home), "pin.json");
|
|
48
|
+
}
|
|
49
|
+
export function pinnedNodePath(home, version, platform, arch) {
|
|
50
|
+
return join(runtimesDir(home), `node-v${version}-${platform}-${arch}`, "bin", platform === "win32" ? "node.exe" : "node");
|
|
51
|
+
}
|
|
52
|
+
export function readPin(home) {
|
|
53
|
+
const p = pinPath(home);
|
|
54
|
+
if (!existsSync(p))
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function writePin(home, pin) {
|
|
64
|
+
mkdirSync(runtimesDir(home), { recursive: true });
|
|
65
|
+
writeFileSync(pinPath(home), JSON.stringify(pin, null, 2) + "\n", { mode: 0o600 });
|
|
66
|
+
}
|
|
67
|
+
async function defaultFetchText(url) {
|
|
68
|
+
const res = await fetch(url);
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
throw new CliError(`GET ${url} -> ${res.status}`, "runtime-fetch-failed");
|
|
71
|
+
return res.text();
|
|
72
|
+
}
|
|
73
|
+
async function defaultFetchBytes(url) {
|
|
74
|
+
const res = await fetch(url);
|
|
75
|
+
if (!res.ok)
|
|
76
|
+
throw new CliError(`GET ${url} -> ${res.status}`, "runtime-fetch-failed");
|
|
77
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
78
|
+
}
|
|
79
|
+
async function defaultExtract(tarPath, dest) {
|
|
80
|
+
mkdirSync(dest, { recursive: true });
|
|
81
|
+
const res = spawnSync("tar", ["-xzf", tarPath, "-C", dest, "--strip-components=1"]);
|
|
82
|
+
if (res.status !== 0) {
|
|
83
|
+
throw new CliError(`tar -xzf ${tarPath} failed: ${res.stderr?.toString().trim() || `exit ${String(res.status)}`}`, "runtime-extract-failed");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Download the pinned Node, verify it against that release's own SHASUMS256.txt, and unpack it
|
|
87
|
+
* under the CLI's own cache. Refuses (never unpacks) on a checksum mismatch. Re-installing a
|
|
88
|
+
* version already present short-circuits without touching the network. */
|
|
89
|
+
export async function installPinnedRuntime(o) {
|
|
90
|
+
const tb = tarballFor(o.version, o.platform, o.arch);
|
|
91
|
+
const nodePath = pinnedNodePath(o.home, o.version, o.platform, o.arch);
|
|
92
|
+
const existing = readPin(o.home);
|
|
93
|
+
if (existing?.version === o.version && existsSync(nodePath)) {
|
|
94
|
+
return { version: o.version, nodePath, alreadyPresent: true };
|
|
95
|
+
}
|
|
96
|
+
const fetchText = o.fetchText ?? defaultFetchText;
|
|
97
|
+
const fetchBytes = o.fetchBytes ?? defaultFetchBytes;
|
|
98
|
+
const extract = o.extract ?? defaultExtract;
|
|
99
|
+
const shasumsBody = await fetchText(tb.shasums);
|
|
100
|
+
const want = shasumFor(shasumsBody, tb.file);
|
|
101
|
+
const bytes = await fetchBytes(tb.url);
|
|
102
|
+
const got = createHash("sha256").update(bytes).digest("hex");
|
|
103
|
+
if (got !== want) {
|
|
104
|
+
throw new CliError(`checksum mismatch for ${tb.file}: expected ${want}, got ${got} — refusing to unpack`, "runtime-checksum-mismatch");
|
|
105
|
+
}
|
|
106
|
+
const tmpDir = join(runtimesDir(o.home), ".tmp");
|
|
107
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
108
|
+
const tmpTar = join(tmpDir, tb.file);
|
|
109
|
+
writeFileSync(tmpTar, bytes);
|
|
110
|
+
const dest = join(runtimesDir(o.home), `node-v${o.version}-${o.platform}-${o.arch}`);
|
|
111
|
+
try {
|
|
112
|
+
await extract(tmpTar, dest);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
rmSync(tmpTar, { force: true });
|
|
116
|
+
}
|
|
117
|
+
if (!existsSync(nodePath)) {
|
|
118
|
+
throw new CliError(`extraction did not produce ${nodePath} — the tarball's layout may have changed`, "runtime-extract-failed");
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
chmodSync(nodePath, 0o755);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// best-effort: platforms without exec bits (e.g. a fake in tests) still count as installed
|
|
125
|
+
}
|
|
126
|
+
writePin(o.home, { version: o.version, nodePath });
|
|
127
|
+
return { version: o.version, nodePath, alreadyPresent: false };
|
|
128
|
+
}
|
|
129
|
+
export function uninstallPinnedRuntime(home) {
|
|
130
|
+
const pin = readPin(home);
|
|
131
|
+
if (!pin)
|
|
132
|
+
return false;
|
|
133
|
+
rmSync(runtimesDir(home), { recursive: true, force: true });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// runtime-verb.ts — CTC-2158, Tier 2. `catalyst-skills runtime status|install|path|uninstall`: the
|
|
2
|
+
// verb every unsupported-runtime message points at. It downloads the pinned Node named by
|
|
3
|
+
// package.json's `catalystCloud.pinnedNode`, verifies it, and unpacks it under this CLI's own
|
|
4
|
+
// cache — it never touches the machine's default Node and needs no admin rights.
|
|
5
|
+
import { positionals } from "./args.js";
|
|
6
|
+
import { readManifest } from "./config.js";
|
|
7
|
+
import { UsageError } from "./errors.js";
|
|
8
|
+
import { FIX_COMMAND, detectRuntime, runtimeVerdict } from "./runtime.js";
|
|
9
|
+
import { installPinnedRuntime, readPin, uninstallPinnedRuntime } from "./runtime-store.js";
|
|
10
|
+
export async function cmdRuntime(args, ctx, deps = {}) {
|
|
11
|
+
const [sub = "status"] = positionals(args);
|
|
12
|
+
const home = deps.home ?? ctx.home;
|
|
13
|
+
const platform = deps.platform ?? process.platform;
|
|
14
|
+
const arch = deps.arch ?? process.arch;
|
|
15
|
+
const manifest = readManifest();
|
|
16
|
+
switch (sub) {
|
|
17
|
+
case "status": {
|
|
18
|
+
const pin = readPin(home);
|
|
19
|
+
const ambient = detectRuntime();
|
|
20
|
+
const ambientVerdict = runtimeVerdict(ambient, manifest.enginesNode);
|
|
21
|
+
const supported = pin ? true : ambientVerdict.supported;
|
|
22
|
+
if (args.json) {
|
|
23
|
+
ctx.stdout(JSON.stringify({ pinned: pin, ambient, supported }));
|
|
24
|
+
return supported ? 0 : 1;
|
|
25
|
+
}
|
|
26
|
+
if (!pin) {
|
|
27
|
+
ctx.stdout(`no pinned runtime installed — ambient: ${ambientVerdict.line}`);
|
|
28
|
+
if (!ambientVerdict.supported)
|
|
29
|
+
ctx.stdout(`fix: ${FIX_COMMAND}`);
|
|
30
|
+
return ambientVerdict.supported ? 0 : 1;
|
|
31
|
+
}
|
|
32
|
+
ctx.stdout(`pinned: Node ${pin.version} at ${pin.nodePath}`);
|
|
33
|
+
ctx.stdout(`ambient: ${ambientVerdict.line}`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
case "install": {
|
|
37
|
+
const result = await installPinnedRuntime({
|
|
38
|
+
home,
|
|
39
|
+
version: manifest.pinnedNode,
|
|
40
|
+
platform,
|
|
41
|
+
arch,
|
|
42
|
+
fetchText: deps.fetchText,
|
|
43
|
+
fetchBytes: deps.fetchBytes,
|
|
44
|
+
extract: deps.extract,
|
|
45
|
+
});
|
|
46
|
+
ctx.stdout(result.alreadyPresent
|
|
47
|
+
? `Node ${result.version} is already installed at ${result.nodePath}`
|
|
48
|
+
: `Installed Node ${result.version} at ${result.nodePath} — this CLI now runs on it regardless of your machine's default Node`);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
case "path": {
|
|
52
|
+
const pin = readPin(home);
|
|
53
|
+
if (!pin)
|
|
54
|
+
return 1;
|
|
55
|
+
ctx.stdout(pin.nodePath);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
case "uninstall": {
|
|
59
|
+
const removed = uninstallPinnedRuntime(home);
|
|
60
|
+
ctx.stdout(removed ? "removed the pinned runtime" : "no pinned runtime to remove");
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
throw new UsageError(`unknown runtime subcommand: ${sub} (expected: status | install | path | uninstall)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// runtime.ts — CTC-2158. The ONE place that answers "can this runtime run this CLI, and if not,
|
|
2
|
+
// what is the one command that fixes it without touching the machine's default Node?"
|
|
3
|
+
//
|
|
4
|
+
// Two floors, and they come from different places on purpose:
|
|
5
|
+
// • Node: `engines.node` in package.json, read through readManifest(). Declared and publishable.
|
|
6
|
+
// MEASURED: Node 22.14.0 has no node:module.registerHooks, so the type-stripping loader cannot
|
|
7
|
+
// install and the SDK cannot load — `>=22` admitted a runtime this package does not work on.
|
|
8
|
+
// • bun: BUN_MIN below. `engines` has no bun key bun enforces, so it lives here with its own
|
|
9
|
+
// measurement. MEASURED: bun 1.3.14 has no `node:sqlite` at all (`bun -e 'import "node:sqlite"'`
|
|
10
|
+
// -> "Could not resolve"), so the CLI could not even finish loading; bun 1.4.2 has it and runs
|
|
11
|
+
// everything including the SDK. Anything that offers bun as a fallback MUST name this floor —
|
|
12
|
+
// an unqualified bun recommendation sent a customer to a runtime that cannot even start.
|
|
13
|
+
export const BUN_MIN = "1.4.0";
|
|
14
|
+
/** The ONE copy-pasteable command. Installs a pinned Node under the CLI's own cache and pins it;
|
|
15
|
+
* it never touches the machine's default Node and needs no admin rights. See src/runtime-store.ts. */
|
|
16
|
+
export const FIX_COMMAND = "npx -y @catalyst-cloud/catalyst-skills runtime install";
|
|
17
|
+
/** Detect the runtime this process is running on. `process.versions.bun` is present only under bun. */
|
|
18
|
+
export function detectRuntime(versions = process.versions) {
|
|
19
|
+
if (versions.bun)
|
|
20
|
+
return { kind: "bun", version: versions.bun, nodeCompat: versions.node ?? "0.0.0" };
|
|
21
|
+
if (versions.node)
|
|
22
|
+
return { kind: "node", version: versions.node, nodeCompat: versions.node };
|
|
23
|
+
return { kind: "unknown", version: "0", nodeCompat: "0" };
|
|
24
|
+
}
|
|
25
|
+
const RANGE_RE = /^>=\s*(\d+)\.(\d+)(?:\.(\d+))?$/;
|
|
26
|
+
/** Parse the one range form this package declares (">=X.Y" or ">=X.Y.Z"). A range this cannot parse
|
|
27
|
+
* is a loud, named error — a CLI that cannot read its own declared floor must not conclude
|
|
28
|
+
* "everything is supported". */
|
|
29
|
+
export function parseNodeFloor(range) {
|
|
30
|
+
const m = RANGE_RE.exec(range.trim());
|
|
31
|
+
if (!m) {
|
|
32
|
+
throw new Error(`engines.node is "${range}", which this package's own range parser does not understand (it only reads ">=X.Y" or ">=X.Y.Z")`);
|
|
33
|
+
}
|
|
34
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
35
|
+
}
|
|
36
|
+
function parseVersionTriple(v) {
|
|
37
|
+
const core = v.split("-")[0].split("+")[0];
|
|
38
|
+
const [maj, min, pat] = core.split(".");
|
|
39
|
+
return [Number(maj) || 0, Number(min) || 0, Number(pat) || 0];
|
|
40
|
+
}
|
|
41
|
+
export function versionAtLeast(version, floor) {
|
|
42
|
+
const [a, b, c] = parseVersionTriple(version);
|
|
43
|
+
if (a !== floor.major)
|
|
44
|
+
return a > floor.major;
|
|
45
|
+
if (b !== floor.minor)
|
|
46
|
+
return b > floor.minor;
|
|
47
|
+
return c >= floor.patch;
|
|
48
|
+
}
|
|
49
|
+
/** The one range text every message reads, instead of writing "22.15" or "1.4" a second time. */
|
|
50
|
+
export function supportedRangeText(range) {
|
|
51
|
+
const floor = parseNodeFloor(range);
|
|
52
|
+
return `Node ${floor.major}.${floor.minor} or newer, or bun ${BUN_MIN} or newer`;
|
|
53
|
+
}
|
|
54
|
+
/** Can this runtime run the CLI, and if not, what is the one command? */
|
|
55
|
+
export function runtimeVerdict(facts, range) {
|
|
56
|
+
const floor = parseNodeFloor(range);
|
|
57
|
+
const rangeText = supportedRangeText(range);
|
|
58
|
+
if (facts.kind === "node") {
|
|
59
|
+
if (versionAtLeast(facts.version, floor)) {
|
|
60
|
+
return { supported: true, line: `runtime: Node ${facts.version} (supported: ${rangeText})` };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
supported: false,
|
|
64
|
+
line: `runtime: Node ${facts.version} is too old (supported: ${rangeText})`,
|
|
65
|
+
reason: `node:module has no registerHooks on Node ${facts.version}; Node ${floor.major}.${floor.minor} or newer is required`,
|
|
66
|
+
fix: FIX_COMMAND,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (facts.kind === "bun") {
|
|
70
|
+
const bunFloor = parseNodeFloor(`>=${BUN_MIN}`);
|
|
71
|
+
if (versionAtLeast(facts.version, bunFloor)) {
|
|
72
|
+
return { supported: true, line: `runtime: bun ${facts.version} (supported: ${rangeText})` };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
supported: false,
|
|
76
|
+
line: `runtime: bun ${facts.version} is too old (supported: ${rangeText})`,
|
|
77
|
+
reason: `bun ${facts.version} has no node:sqlite, which the replica needs; bun ${BUN_MIN} or newer is required`,
|
|
78
|
+
fix: FIX_COMMAND,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
supported: false,
|
|
83
|
+
line: `runtime: unrecognised runtime (supported: ${rangeText})`,
|
|
84
|
+
reason: "could not detect a Node or bun version on this process",
|
|
85
|
+
fix: FIX_COMMAND,
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
2
|
+
import { installTsDepsLoader } from "./ts-deps-loader.js";
|
|
3
|
+
import { readManifest } from "./config.js";
|
|
4
|
+
import { FIX_COMMAND, supportedRangeText } from "./runtime.js";
|
|
5
|
+
let cached = null;
|
|
6
|
+
const realImport = () => import("@catalyst-cloud/sdk/node");
|
|
7
|
+
/** Import the SDK's node entry, installing the type-stripping loader first. Cached per process. */
|
|
8
|
+
export function loadSdk(importer = realImport) {
|
|
9
|
+
if (!cached) {
|
|
10
|
+
cached = (async () => {
|
|
11
|
+
const verdict = installTsDepsLoader();
|
|
12
|
+
try {
|
|
13
|
+
return await importer();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
cached = null;
|
|
17
|
+
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
18
|
+
const why = verdict.installed ? "" : ` (${verdict.reason})`;
|
|
19
|
+
throw new CliError(`the Catalyst Cloud SDK could not be loaded on Node ${process.version}${why}: ${detail} — this verb needs the SDK. ` +
|
|
20
|
+
`Supported: ${supportedRangeText(readManifest().enginesNode)}. One command fixes it without changing your default Node: ${FIX_COMMAND}`, "sdk-unavailable");
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
}
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
/** Test seam: forget the cached import. */
|
|
27
|
+
export function resetSdkCache() {
|
|
28
|
+
cached = null;
|
|
29
|
+
}
|
package/dist/secret.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// secret.ts — `catalyst-skills secret set|import`: put a repository's secrets into the cloud from the
|
|
2
|
+
// terminal, as the person who is logged in (CTC-3549). The cloud gates both on an admin or owner seat,
|
|
3
|
+
// encrypts the value, and records who wrote it and where it came from, never the value itself.
|
|
4
|
+
//
|
|
5
|
+
// ⛔ A VALUE NEVER REACHES STDOUT, STDERR OR AN ERROR MESSAGE. It is read (stdin, a hidden prompt, or
|
|
6
|
+
// the output of a local command), sent in one request body, and dropped. The cloud's responses carry
|
|
7
|
+
// names and versions only, and every line this verb prints is built from those. A command's own
|
|
8
|
+
// stdout is the value, so it is captured and never echoed; its stderr passes through untouched (a
|
|
9
|
+
// sign-in prompt from `op` has to reach the person).
|
|
10
|
+
//
|
|
11
|
+
// ⭐ `--command` RUNS ON THIS MACHINE, in the person's own shell, with their own environment. That is
|
|
12
|
+
// the point: `op read op://…` resolves against the 1Password session on the laptop, and the cloud
|
|
13
|
+
// only ever sees the result. The command TEXT goes to the cloud as the audit `source`, so the log can
|
|
14
|
+
// say where a value came from without holding it. Do not put a value in the command line itself.
|
|
15
|
+
//
|
|
16
|
+
// The routes are the browser's own (`/me/secrets`, `/me/secrets/import`): the cloud admits a personal
|
|
17
|
+
// key or CLI login on them under the same gates as a session. They are not in the contract's
|
|
18
|
+
// `routes[]` table (that table is the /api/v1/agent/* family), so the path is fixed here, and an
|
|
19
|
+
// older cloud that still wants a session answers 401 — which this verb names as "needs a newer cloud".
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { basename } from "node:path";
|
|
23
|
+
import { flagList, flagString, positionals } from "./args.js";
|
|
24
|
+
import { requireConfig } from "./config.js";
|
|
25
|
+
import { CliError, UsageError } from "./errors.js";
|
|
26
|
+
import { promptSecret, stdinIsTty } from "./prompt.js";
|
|
27
|
+
import { apiClient } from "./transport.js";
|
|
28
|
+
export const SECRETS_PATH = "/me/secrets";
|
|
29
|
+
export const SECRETS_IMPORT_PATH = "/me/secrets/import";
|
|
30
|
+
const NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
31
|
+
const REPO_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
32
|
+
/** Why an import line was not stored, in words a person can act on. */
|
|
33
|
+
const IMPORT_REASONS = {
|
|
34
|
+
name_exists: "already set for this repository; pass --rotate <NAME> to replace it",
|
|
35
|
+
name_taken_by_env_var: "already a plain environment variable; a name is a secret or a variable, never both",
|
|
36
|
+
invalid_name: "not an env-style name (A-Z, 0-9 and _, starting with a letter)",
|
|
37
|
+
invalid_value: "the value is empty",
|
|
38
|
+
config_rotation_unconfirmed: "the existing entry is a readable config value; change it in the app",
|
|
39
|
+
probe_refused: "the value failed this kind's check",
|
|
40
|
+
};
|
|
41
|
+
/** A command's stdout, minus the one trailing newline almost every command prints after a value. */
|
|
42
|
+
export function trimOneNewline(s) {
|
|
43
|
+
return s.endsWith("\r\n") ? s.slice(0, -2) : s.endsWith("\n") ? s.slice(0, -1) : s;
|
|
44
|
+
}
|
|
45
|
+
/** Run `command` through the person's shell. Stdout is captured (it is the value); stderr passes
|
|
46
|
+
* through so an interactive sign-in can reach them. A non-zero exit stores nothing. */
|
|
47
|
+
export function runLocalCommand(command, env) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const shell = env.SHELL && env.SHELL.length > 0 ? env.SHELL : "/bin/sh";
|
|
50
|
+
const child = spawn(shell, ["-c", command], { env, stdio: ["inherit", "pipe", "inherit"] });
|
|
51
|
+
const chunks = [];
|
|
52
|
+
child.stdout.on("data", (c) => chunks.push(c));
|
|
53
|
+
child.on("error", (err) => reject(new CliError(`could not run the command: ${err.message}; nothing was stored`, "command-failed", 1)));
|
|
54
|
+
child.on("close", (code, signal) => {
|
|
55
|
+
if (code === 0)
|
|
56
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
57
|
+
else
|
|
58
|
+
reject(new CliError(`the command ${signal ? `was killed by ${signal}` : `exited ${code}`}; nothing was stored`, "command-failed", 1));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function requireRepo(args, verb) {
|
|
63
|
+
const repo = flagString(args, "repo");
|
|
64
|
+
if (repo === undefined)
|
|
65
|
+
throw new UsageError(`secret ${verb} needs --repo <owner/name>`);
|
|
66
|
+
if (!REPO_RE.test(repo))
|
|
67
|
+
throw new UsageError(`--repo must be owner/name (got "${repo}")`);
|
|
68
|
+
return repo;
|
|
69
|
+
}
|
|
70
|
+
/** The cloud's refusal, in its own words, plus what to do for the two we can explain. */
|
|
71
|
+
function refusalLine(status, body, repo) {
|
|
72
|
+
if (status === 401) {
|
|
73
|
+
return "refused (401): the cloud did not accept your credential here. If `catalyst-skills me` works, this cloud predates secret writes from the CLI and needs a newer release; otherwise log in again.";
|
|
74
|
+
}
|
|
75
|
+
if (status === 404)
|
|
76
|
+
return `refused (404): ${repo} is not a repository of this tenant (register it in the app first)`;
|
|
77
|
+
if (status === 409 && body.error === "registry_not_migrated") {
|
|
78
|
+
return "refused (409): the cloud's secret audit is not set up yet, so nothing was stored. Tell your Catalyst operator.";
|
|
79
|
+
}
|
|
80
|
+
return `refused (${status}): ${body.message ?? body.error ?? "no reason given"}`;
|
|
81
|
+
}
|
|
82
|
+
function renderDeclared(declared, repo) {
|
|
83
|
+
if (declared === undefined)
|
|
84
|
+
return [];
|
|
85
|
+
if (declared === null)
|
|
86
|
+
return [`${repo} has no approved environment declaration, so there is no declared list to check against`];
|
|
87
|
+
if (declared.declared.length === 0)
|
|
88
|
+
return [`${repo}'s approved declaration names no secrets`];
|
|
89
|
+
if (declared.missing.length === 0)
|
|
90
|
+
return [`every declared secret has a value (${declared.declared.length})`];
|
|
91
|
+
return [`declared but still without a value (${declared.missing.length}): ${declared.missing.join(", ")}`];
|
|
92
|
+
}
|
|
93
|
+
const ACCEPT = [400, 401, 403, 404, 409, 422];
|
|
94
|
+
async function readStdinText() {
|
|
95
|
+
const chunks = [];
|
|
96
|
+
for await (const chunk of process.stdin)
|
|
97
|
+
chunks.push(Buffer.from(chunk));
|
|
98
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
99
|
+
}
|
|
100
|
+
export async function cmdSecret(args, ctx, deps = {}) {
|
|
101
|
+
const [sub, ...rest] = positionals(args);
|
|
102
|
+
if (sub !== "set" && sub !== "import")
|
|
103
|
+
throw new UsageError(`unknown secret subcommand "${sub ?? ""}": set | import`);
|
|
104
|
+
if (rest.length !== 1) {
|
|
105
|
+
throw new UsageError(sub === "set" ? "secret set takes one NAME" : "secret import takes one file (a .env file)");
|
|
106
|
+
}
|
|
107
|
+
const repo = requireRepo(args, sub);
|
|
108
|
+
return sub === "set" ? setOne(args, ctx, deps, rest[0], repo) : importFile(args, ctx, rest[0], repo);
|
|
109
|
+
}
|
|
110
|
+
async function setOne(args, ctx, deps, name, repo) {
|
|
111
|
+
if (!NAME_RE.test(name))
|
|
112
|
+
throw new UsageError(`"${name}" is not an env-style name (A-Z, 0-9 and _, starting with a letter)`);
|
|
113
|
+
if (flagList(args, "rotate").length > 0)
|
|
114
|
+
throw new UsageError("--rotate belongs to secret import; secret set always replaces the value");
|
|
115
|
+
const command = flagString(args, "command");
|
|
116
|
+
const cfg = requireConfig(ctx);
|
|
117
|
+
let value;
|
|
118
|
+
let source;
|
|
119
|
+
if (command !== undefined) {
|
|
120
|
+
if (command.trim() === "")
|
|
121
|
+
throw new UsageError("--command is empty");
|
|
122
|
+
value = trimOneNewline(await (deps.runCommand ?? runLocalCommand)(command, ctx.env));
|
|
123
|
+
source = `command: ${command}`;
|
|
124
|
+
}
|
|
125
|
+
else if ((deps.isTty ?? stdinIsTty)()) {
|
|
126
|
+
value = await (deps.prompt ?? ((q) => promptSecret(q)))(`Value for ${name} (input hidden): `);
|
|
127
|
+
source = "prompt";
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
value = trimOneNewline(await (deps.readStdin ?? readStdinText)());
|
|
131
|
+
source = "stdin";
|
|
132
|
+
}
|
|
133
|
+
if (value === "")
|
|
134
|
+
throw new CliError(`the value for ${name} is empty; nothing was stored`, "empty-value", 1);
|
|
135
|
+
const res = await apiClient(cfg, ctx).postJson(SECRETS_PATH, { name, value, scope: "repo", repo, source }, { accept: ACCEPT });
|
|
136
|
+
const body = res.body;
|
|
137
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
138
|
+
if (args.json)
|
|
139
|
+
ctx.stdout(JSON.stringify({ status: res.status, error: body.error ?? null, message: body.message ?? null }));
|
|
140
|
+
else
|
|
141
|
+
ctx.stdout(refusalLine(res.status, body, repo));
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
if (args.json) {
|
|
145
|
+
ctx.stdout(JSON.stringify({ name, repo, version: body.version ?? null, created: body.created ?? null, declared: body.declared ?? null }));
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
ctx.stdout(`stored ${name} for ${repo} (version ${body.version ?? "?"}, ${body.created ? "new" : "replaced"}; from ${command !== undefined ? "the command" : source})`);
|
|
149
|
+
for (const line of renderDeclared(body.declared, repo))
|
|
150
|
+
ctx.stdout(line);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
async function importFile(args, ctx, file, repo) {
|
|
154
|
+
if (flagString(args, "command") !== undefined)
|
|
155
|
+
throw new UsageError("--command belongs to secret set");
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = readFileSync(file, "utf8");
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
throw new UsageError(`could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
const cfg = requireConfig(ctx);
|
|
164
|
+
const rotate = flagList(args, "rotate");
|
|
165
|
+
const res = await apiClient(cfg, ctx).postJson(SECRETS_IMPORT_PATH, { repo, text, rotateExisting: rotate, source: `import: ${basename(file)}` }, { accept: ACCEPT });
|
|
166
|
+
const body = res.body;
|
|
167
|
+
if (res.status !== 200) {
|
|
168
|
+
if (args.json)
|
|
169
|
+
ctx.stdout(JSON.stringify({ status: res.status, error: body.error ?? null, message: body.message ?? null }));
|
|
170
|
+
else
|
|
171
|
+
ctx.stdout(refusalLine(res.status, body, repo));
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
const created = body.created ?? [];
|
|
175
|
+
const rotated = body.rotated ?? [];
|
|
176
|
+
const errors = body.errors ?? [];
|
|
177
|
+
if (args.json) {
|
|
178
|
+
ctx.stdout(JSON.stringify({ repo, created, rotated, errors, declared: body.declared ?? null }));
|
|
179
|
+
return errors.length > 0 ? 1 : 0;
|
|
180
|
+
}
|
|
181
|
+
const stored = created.length + rotated.length;
|
|
182
|
+
ctx.stdout(stored === 0 ? `stored nothing for ${repo}` : `stored ${stored} for ${repo}: ${[...created, ...rotated].join(", ")}`);
|
|
183
|
+
if (rotated.length > 0)
|
|
184
|
+
ctx.stdout(` replaced: ${rotated.join(", ")}`);
|
|
185
|
+
for (const e of errors)
|
|
186
|
+
ctx.stdout(`not stored: ${e.name} (${IMPORT_REASONS[e.reason] ?? e.reason})`);
|
|
187
|
+
for (const line of renderDeclared(body.declared, repo))
|
|
188
|
+
ctx.stdout(line);
|
|
189
|
+
return errors.length > 0 ? 1 : 0;
|
|
190
|
+
}
|
package/dist/semver.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// semver.ts — the one three-field comparator every version-drift check in this package shares
|
|
2
|
+
// (the tenant-minimum bundle check, and CTC-2160's published-release checks).
|
|
3
|
+
/** True when `a` is a semver-older release than `b`. Prerelease/build metadata is ignored; a segment
|
|
4
|
+
* that does not parse counts as 0. A tiny compare on purpose — no dependency for three fields. */
|
|
5
|
+
export function semverOlder(a, b) {
|
|
6
|
+
const parse = (v) => {
|
|
7
|
+
const core = v.split("-")[0].split("+")[0];
|
|
8
|
+
const p = core.split(".");
|
|
9
|
+
return [Number(p[0]) || 0, Number(p[1]) || 0, Number(p[2]) || 0];
|
|
10
|
+
};
|
|
11
|
+
const [a0, a1, a2] = parse(a);
|
|
12
|
+
const [b0, b1, b2] = parse(b);
|
|
13
|
+
if (a0 !== b0)
|
|
14
|
+
return a0 < b0;
|
|
15
|
+
if (a1 !== b1)
|
|
16
|
+
return a1 < b1;
|
|
17
|
+
return a2 < b2;
|
|
18
|
+
}
|