@catalyst-cloud/cli 0.16.29 → 0.16.31
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 +7 -3
- package/bin/launch.js +28 -0
- package/dist/approved-release.js +210 -0
- package/dist/args.js +21 -0
- package/dist/capabilities.js +5 -0
- package/dist/cli-update-notice.js +7 -4
- package/dist/cli.js +19 -0
- package/dist/lease.js +302 -0
- package/dist/onboard-existing.js +12 -3
- package/dist/onboard-next.js +14 -0
- package/dist/onboard-projects.js +3 -3
- package/dist/onboard-repositories.js +10 -3
- package/dist/onboard-ui.js +5 -1
- package/dist/onboard.js +3 -0
- package/dist/published.js +15 -0
- package/dist/ready.js +11 -5
- package/dist/replica.js +43 -6
- package/dist/runner-auto-update.js +3 -1
- package/dist/runner-command.js +92 -11
- package/dist/runner-update.js +72 -16
- package/dist/setup-onboard-copy.js +41 -2
- package/dist/update-cli.js +234 -0
- package/dist/update-command.js +151 -0
- package/dist/update-inventory.js +96 -0
- package/dist/update-plugin.js +99 -0
- package/dist/update-policy.js +194 -0
- package/dist/update-replica.js +120 -0
- package/dist/update-sdk.js +262 -0
- package/dist/update-skills.js +64 -0
- package/dist/update.js +586 -0
- package/package.json +1 -1
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +1 -1
- package/skills/catalyst-onboard/references/skill-sources.md +8 -0
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/run-this-project/references/stalls-and-escalation.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +1 -1
- package/skills/whats-happening/references/what-runs-next.md +1 -1
- package/skills/whats-happening/references/why-is-it-stuck.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.31
|
|
4
|
+
|
|
5
|
+
New: `catalyst update` is one release check for everything Catalyst installed on this machine. `catalyst update --check` lists each component (the runner images, the CLI, the Cloud and Dev skills, the Claude plugin form of the Cloud skills, the local replica and each registered project's SDK) with its installed and approved versions, and `catalyst update` moves them to the approved release. `catalyst update policy <component> on|off` holds or releases one component. `catalyst runner update --check` and `catalyst runner auto-update on|off` still work and now cover the same components. The CLI updates itself without breaking the command that is running. If that update fails, the report says so, keeps you on the version you have, and tries again at the next release check. Copied skills never move back to an older version. A running local replica comes back on the new CLI with its synced data. When it cannot, `catalyst replica status` says why and where the data is kept. The Claude plugin moves only to the approved version, and after a move the report tells you to run `/reload-plugins` or restart Claude Code. `catalyst update --json` has a new field, `approvedSkills`, naming the version, repository and full commit each skills pack was approved at. It is `null` when no approval applies.
|
|
6
|
+
|
|
7
|
+
A busy self-hosted machine no longer waits to go idle before it takes a release. When the new release can read the work records the current one keeps, `catalyst runner update` restarts the machine's supervisor with its running work left in place. Otherwise it waits for an idle machine, as before. An old runner image that running work still used is removed by a later update.
|
|
8
|
+
|
|
9
|
+
## 0.16.30
|
|
10
|
+
|
|
11
|
+
An agent seat can now hold a ticket while it works on it. `catalyst lease take <ticket> --agent <seat>` takes the ticket for that seat for a set time (`--ttl <minutes>`). `catalyst lease renew <ticket>` keeps it, and each renewal names one piece of progress: a commit (`--commit`), an artifact (`--artifact`), a finding (`--finding`) or a pull request (`--pr`). `catalyst lease release <ticket>` hands it back, and `catalyst lease show <ticket>` prints who holds it. A refused renewal, for example one that names no new progress, prints the reason and exits 1. A renewal or release refused because the lease already lapsed, or because another seat took the ticket, forgets the lease this machine stored. The machine stores only the seat name and the lease generation, in a file only you can read. New: `catalyst work available` lists, per team, the tickets nobody holds, with the last holder and their progress, and the open pull requests no live lease covers, drafts included. Both commands use your login. Setup run unattended now says what it skipped and names the exact flags each skipped choice needs. The skill references now say how to see whether a repository is paused, and who can pause or resume it.
|
|
12
|
+
|
|
3
13
|
## 0.16.29
|
|
4
14
|
|
|
5
15
|
The CLI now tells Catalyst Cloud which release you have installed: each request to Catalyst Cloud carries the installed version, and so does `catalyst watch` when it connects. Catalyst Cloud can then tell an out-of-date CLI from a current one. Nothing about your requests changes otherwise. On a Mac self-hosted machine, setup now installs a small agent that measures the Mac's disk, so the machine can take work instead of showing zero free slots. When a machine still shows zero free slots, setup and `catalyst ready` now say why: its disk has not been measured yet, or its free space is below the floor (25 GiB by default), with the numbers and the fix. An updated disk sampler also takes effect straight away: setup reloads the running disk agent whenever the sampler script changes, not only when its schedule file does.
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ The two packs have different jobs and independent versions. Omit `-g` for a proj
|
|
|
42
42
|
|
|
43
43
|
<details><summary><strong>Alternative for Claude Code: the plugin marketplace</strong></summary>
|
|
44
44
|
|
|
45
|
-
The plugin installs this pack's same `skills/` tree as a managed bundle
|
|
45
|
+
The plugin installs this pack's same `skills/` tree as a managed bundle, and `catalyst update` moves it to the version your workspace approved (see [Updating](#updating)). It needs a GitHub SSH key, and it does not load into the session you are already in — run `/reload-plugins` or restart afterwards. Pick one rail for this pack; installing both leaves you with every skill twice. The development pack has its own optional Claude plugin, `catalyst-dev@catalyst-dev-skills`.
|
|
46
46
|
|
|
47
47
|
```
|
|
48
48
|
/plugin marketplace add coalesce-labs/catalyst-cloud-skills
|
|
@@ -298,13 +298,16 @@ The package pins the contract range `1.x || 2.x`, recorded in `package.json` und
|
|
|
298
298
|
- `~/.config/catalyst-cloud/customer.json`, written with mode `0600`, holding your personal key, who you are, and the CLI path.
|
|
299
299
|
- `~/.config/catalyst-cloud/contract.json`, your account's cached contract.
|
|
300
300
|
- `~/.config/catalyst-cloud/published.json`, the cached answer to "what is the newest release" — no credential in it.
|
|
301
|
+
- After `catalyst update` moves the CLI: `~/.config/catalyst-cloud/cli-runtimes/`, one directory per CLI version it installed and `active.json` naming the one your `catalyst` command runs.
|
|
301
302
|
- Only if you start them: `~/.config/catalyst-cloud/replica.db` with its `.pid`, `.writer.lock` and `.writer.state` sidecars, `$XDG_STATE_HOME/catalyst/events/<tenant>/backbone/` (or the home-directory fallback) with bounded daily event segments, and `~/.config/catalyst-cloud/watch-cursor.json`.
|
|
302
303
|
|
|
303
304
|
The skill files themselves are written by whichever install command you ran, in that tool's own location. Your personal key goes into that one config file and nowhere else.
|
|
304
305
|
|
|
305
306
|
## Updating
|
|
306
307
|
|
|
307
|
-
A
|
|
308
|
+
`catalyst update --check` lists each installed component beside the version your workspace approved, says which one is behind and why, and changes nothing. Add `--project <absolute path>` to include a project's SDK; the report gives the one command that moves it, `catalyst update sdk --project <path>`. A project's files change only when you run that command, or after you turn its SDK updates on with `catalyst update policy sdk on --project <path>`. `catalyst update` moves the runner images, the CLI, the Cloud skills (copied on this machine or installed as the Claude plugin), and a running local replica to the approved versions. The new CLI is installed beside the old one, and your `catalyst` command runs it from then on, at the same path. A running replica restarts on it and keeps its synced data; a stopped one stays stopped. The Claude plugin moves through Claude Code, and only when its marketplace serves the approved version; run `/reload-plugins` or restart Claude Code afterwards. When the release check cannot find the `claude` command, it changes nothing and prints the one command that moves the plugin. The development skills are not on the approved release yet: the report lists their version as unknown and gives no command for them, so update them the way you installed them. On a machine whose runner checks for releases every 15 minutes (`catalyst runner auto-update on`), that check moves the same components by itself. Otherwise they move when you run `catalyst update`. Each component is on by default; turn one off with `catalyst update policy <component> off` (for example `cli`), and `catalyst update policy` lists every setting. If you turned the installer's daily update off, the CLI and both skills packs start held, and `catalyst update policy cli on` turns the CLI back on. Add `--json` for a machine-readable report.
|
|
309
|
+
|
|
310
|
+
A CLI older than `catalyst update` cannot move itself: install it once with `npm install -g @catalyst-cloud/cli@latest && catalyst login`. The re-login rewrites the CLI path the skills spawn, so they stop running the old bundle. To run one command against the latest publish without installing, use `npx -p @catalyst-cloud/cli@latest catalyst login`. The next `catalyst` run prints a one-line notice:
|
|
308
311
|
|
|
309
312
|
```
|
|
310
313
|
[catalyst] updated 0.1.1 → 0.2.0: <that version's CHANGELOG.md summary> · update with: npm install -g @catalyst-cloud/cli@latest && catalyst login
|
|
@@ -312,7 +315,7 @@ A plugin install updates when we ship. Skills copied by `npx skills add` do not.
|
|
|
312
315
|
|
|
313
316
|
A `customer.json` written by an older bundle is still read unchanged; it gains the CLI path and the cached contract the next time you run `catalyst login`.
|
|
314
317
|
|
|
315
|
-
`catalyst ready` also says when either the installed skills or the CLI is behind
|
|
318
|
+
`catalyst ready` also says when either the installed skills or the CLI is behind, naming both versions and the command for each. It compares them with the approved release this machine last read, and only a machine that has not read one yet compares them with the latest publish. It is a note, never a failure, and `--offline` (or `CATALYST_SKILLS_OFFLINE=1`) skips the lookup.
|
|
316
319
|
|
|
317
320
|
## Uninstalling
|
|
318
321
|
|
|
@@ -325,6 +328,7 @@ catalyst replica stop
|
|
|
325
328
|
rm -f ~/.config/catalyst-cloud/customer.json ~/.config/catalyst-cloud/contract.json ~/.config/catalyst-cloud/published.json ~/.config/catalyst-cloud/watch-cursor.json
|
|
326
329
|
rm -f ~/.config/catalyst-cloud/replica.db ~/.config/catalyst-cloud/replica.db.pid ~/.config/catalyst-cloud/replica.db.writer.lock ~/.config/catalyst-cloud/replica.db.writer.state
|
|
327
330
|
rm -rf "${XDG_STATE_HOME:-$HOME/.local/state}/catalyst/events"
|
|
331
|
+
rm -rf ~/.config/catalyst-cloud/cli-runtimes
|
|
328
332
|
npm uninstall -g @catalyst-cloud/cli
|
|
329
333
|
```
|
|
330
334
|
|
package/bin/launch.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// scripts spawn this CLI constantly, and doubling process startup on every call is a real cost.
|
|
33
33
|
// • ambient unsupported, no pin -> print the verdict and the ONE command; exit 1, never a raw
|
|
34
34
|
// module-resolution error.
|
|
35
|
+
import { basename } from "node:path";
|
|
35
36
|
import { fileURLToPath } from "node:url";
|
|
36
37
|
|
|
37
38
|
/** The one stderr line the deprecated name prints in addition to what the program prints. */
|
|
@@ -52,6 +53,7 @@ export const DEPRECATED_NAME_LINE =
|
|
|
52
53
|
export async function launch(invokedAs, binUrl) {
|
|
53
54
|
const argv = process.argv.slice(2);
|
|
54
55
|
const deprecated = invokedAs === "catalyst-skills";
|
|
56
|
+
if (argv[0] !== "runtime" && (await runSelectedVersion(invokedAs, binUrl))) return;
|
|
55
57
|
const handled = argv[0] === "runtime" ? false : await preflight(invokedAs, binUrl, argv, deprecated);
|
|
56
58
|
if (handled) return;
|
|
57
59
|
const noticeFirst = deprecated && process.stderr.isTTY === true;
|
|
@@ -66,6 +68,32 @@ export async function launch(invokedAs, binUrl) {
|
|
|
66
68
|
});
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
/**
|
|
72
|
+
* CTC-6708 — a release check installs the approved CLI in its own directory and records it as the
|
|
73
|
+
* selected version (dist/update-cli.js). This command stays where npm put it, so scheduled jobs and
|
|
74
|
+
* saved launcher paths keep working: when the selected version is newer than its own, it loads that
|
|
75
|
+
* version's bin in this same process (no second process, so a pid, a signal and an exit code are the
|
|
76
|
+
* selected version's own). The selected version's launcher finds nothing newer and runs. Any doubt
|
|
77
|
+
* (no record, an unreadable one, a directory that does not hold that version) runs this version, as
|
|
78
|
+
* before. A release check that runs a staged version to check it sets CATALYST_CLI_ACTIVE_RUNTIME
|
|
79
|
+
* to that bin's path, so that run never selects. Returns true when it already handled the invocation.
|
|
80
|
+
*/
|
|
81
|
+
async function runSelectedVersion(invokedAs, binUrl) {
|
|
82
|
+
if (process.env.CATALYST_CLI_ACTIVE_RUNTIME === fileURLToPath(binUrl)) return false;
|
|
83
|
+
const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/";
|
|
84
|
+
let bin = null;
|
|
85
|
+
try {
|
|
86
|
+
const [updateCli, config] = await Promise.all([import("../dist/update-cli.js"), import("../dist/config.js")]);
|
|
87
|
+
bin = updateCli.activeRuntimeBin(home, config.readManifest().version, basename(fileURLToPath(binUrl)));
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (!bin) return false;
|
|
92
|
+
const { pathToFileURL } = await import("node:url");
|
|
93
|
+
await import(pathToFileURL(bin).href);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
/** Returns true when it already handled (and exited) the invocation. */
|
|
70
98
|
async function preflight(invokedAs, binUrl, args, deprecated) {
|
|
71
99
|
const policy = process.env.CATALYST_SKILLS_RUNTIME ?? "auto";
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// approved-release.ts — the release Catalyst approved for every component an install keeps, read
|
|
2
|
+
// once per release check (CTC-6708). The approved record names each release-train member's exact
|
|
3
|
+
// version together, so the CLI's "newest" answer comes from it, never from npm's `latest` tag.
|
|
4
|
+
//
|
|
5
|
+
// Two transports carry the same record:
|
|
6
|
+
// - a signed-in CLI reads the member route `GET /api/v1/releases/approved`;
|
|
7
|
+
// - an enrolled machine's desired-release answer (`GET /api/v1/hosts/desired-release`, read inside
|
|
8
|
+
// the supervisor container by runner-update.ts) carries it as `release.components`, beside the
|
|
9
|
+
// digest-pinned images it already names. The host credential never leaves Docker.
|
|
10
|
+
//
|
|
11
|
+
// Parsing is strict: exact stable versions only (no tags, ranges or pre-releases), every version on
|
|
12
|
+
// the record's declared train line, skills packs pinned to one of two known repositories at a full
|
|
13
|
+
// commit. A record on another train is reported and never applied: automatic moves stay on the
|
|
14
|
+
// MAJOR.MINOR line this CLI ships on. The last record read is cached for diagnostics only; a failed
|
|
15
|
+
// read never applies a cached record.
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { configDirFor, loadConfig, readManifest } from "./config.js";
|
|
19
|
+
import { CliError, MeError } from "./errors.js";
|
|
20
|
+
import { isRecord, writePrivateAtomic } from "./runner-update.js";
|
|
21
|
+
import { apiClient } from "./transport.js";
|
|
22
|
+
export const APPROVED_RELEASE_PATH = "/api/v1/releases/approved";
|
|
23
|
+
export const APPROVED_RELEASE_CACHE_FILE = "approved-release.json";
|
|
24
|
+
/** Train members the approved record may name. Only some are installed on a customer's machine;
|
|
25
|
+
* the rest are inventory metadata and never something this CLI installs. */
|
|
26
|
+
export const RELEASE_MEMBERS = [
|
|
27
|
+
"cli",
|
|
28
|
+
"forwarder",
|
|
29
|
+
"plugin",
|
|
30
|
+
"sdk",
|
|
31
|
+
"sdk-replica-node",
|
|
32
|
+
"sdk-replica-browser",
|
|
33
|
+
"schema",
|
|
34
|
+
"replicate",
|
|
35
|
+
"read-model",
|
|
36
|
+
"design",
|
|
37
|
+
"installer",
|
|
38
|
+
];
|
|
39
|
+
export const SKILL_PACKS = ["cloud", "dev"];
|
|
40
|
+
/** The only repository each pack may come from. A record naming another is refused whole. */
|
|
41
|
+
export const SKILL_PACK_REPOSITORIES = {
|
|
42
|
+
cloud: "coalesce-labs/catalyst-cloud-skills",
|
|
43
|
+
dev: "coalesce-labs/catalyst-dev-skills",
|
|
44
|
+
};
|
|
45
|
+
const STABLE_VERSION = /^(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})$/;
|
|
46
|
+
const RELEASE_LINE = /^(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})$/;
|
|
47
|
+
const COMMIT = /^[0-9a-f]{40}$/;
|
|
48
|
+
const APPROVAL_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
49
|
+
const posInt = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
50
|
+
/** An exact stable version, or null. Unlike skills.ts's lenient `semverOlder`, this is the authority
|
|
51
|
+
* check: `0.16.28` passes; `latest`, `^0.16.28`, `0.16.28-rc.1`, `v0.16.28` and `0.16` do not. */
|
|
52
|
+
export function exactVersion(value) {
|
|
53
|
+
return typeof value === "string" && STABLE_VERSION.test(value) ? value : null;
|
|
54
|
+
}
|
|
55
|
+
/** MAJOR.MINOR of an exact version. */
|
|
56
|
+
export function trainOf(version) {
|
|
57
|
+
return version.split(".").slice(0, 2).join(".");
|
|
58
|
+
}
|
|
59
|
+
/** Compare two exact versions: negative when `a` is older, 0 when equal, positive when newer. */
|
|
60
|
+
export function compareVersions(a, b) {
|
|
61
|
+
const pa = a.split(".").map(Number);
|
|
62
|
+
const pb = b.split(".").map(Number);
|
|
63
|
+
for (let i = 0; i < 3; i++) {
|
|
64
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
65
|
+
if (d !== 0)
|
|
66
|
+
return d;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
/** The train this CLI ships on; automatic updates never leave it. */
|
|
71
|
+
export function supportedReleaseLine() {
|
|
72
|
+
return trainOf(readManifest().version);
|
|
73
|
+
}
|
|
74
|
+
/** Parse the approved record from an unknown body. Any malformed field refuses the whole record. */
|
|
75
|
+
export function parseApprovedRelease(input) {
|
|
76
|
+
const invalid = { ok: false, reason: "response_invalid" };
|
|
77
|
+
if (!isRecord(input) || input.schemaVersion !== 1)
|
|
78
|
+
return invalid;
|
|
79
|
+
if (typeof input.approvalId !== "string" || !APPROVAL_ID.test(input.approvalId))
|
|
80
|
+
return invalid;
|
|
81
|
+
if (!posInt(input.generation) || !posInt(input.approvedAtMs))
|
|
82
|
+
return invalid;
|
|
83
|
+
if (typeof input.releaseLine !== "string" || !RELEASE_LINE.test(input.releaseLine))
|
|
84
|
+
return invalid;
|
|
85
|
+
const releaseLine = input.releaseLine;
|
|
86
|
+
if (!isRecord(input.members))
|
|
87
|
+
return invalid;
|
|
88
|
+
const members = {};
|
|
89
|
+
for (const member of RELEASE_MEMBERS) {
|
|
90
|
+
const value = input.members[member];
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
const version = exactVersion(value);
|
|
94
|
+
if (version === null || trainOf(version) !== releaseLine)
|
|
95
|
+
return invalid;
|
|
96
|
+
members[member] = version;
|
|
97
|
+
}
|
|
98
|
+
if (members.cli === undefined)
|
|
99
|
+
return invalid;
|
|
100
|
+
const skills = {};
|
|
101
|
+
if (input.skills !== undefined) {
|
|
102
|
+
if (!isRecord(input.skills))
|
|
103
|
+
return invalid;
|
|
104
|
+
for (const pack of SKILL_PACKS) {
|
|
105
|
+
const value = input.skills[pack];
|
|
106
|
+
if (value === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
if (!isRecord(value))
|
|
109
|
+
return invalid;
|
|
110
|
+
const version = exactVersion(value.version);
|
|
111
|
+
if (version === null || trainOf(version) !== releaseLine)
|
|
112
|
+
return invalid;
|
|
113
|
+
if (value.repository !== SKILL_PACK_REPOSITORIES[pack])
|
|
114
|
+
return invalid;
|
|
115
|
+
if (typeof value.commit !== "string" || !COMMIT.test(value.commit))
|
|
116
|
+
return invalid;
|
|
117
|
+
skills[pack] = { version, repository: value.repository, commit: value.commit };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
release: { approvalId: input.approvalId, generation: input.generation, approvedAtMs: input.approvedAtMs, releaseLine, members, skills },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Read the member route with this machine's sign-in. Never throws; every failure is a reason. */
|
|
126
|
+
export async function readMemberApproval(ctx) {
|
|
127
|
+
let cfg;
|
|
128
|
+
try {
|
|
129
|
+
cfg = loadConfig(ctx.home);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { kind: "unavailable", reason: "signed_out" };
|
|
133
|
+
}
|
|
134
|
+
if (!cfg)
|
|
135
|
+
return { kind: "unavailable", reason: "signed_out" };
|
|
136
|
+
try {
|
|
137
|
+
const res = await apiClient(cfg, ctx).getJson(APPROVED_RELEASE_PATH, { accept: [404] });
|
|
138
|
+
if (res.status === 404)
|
|
139
|
+
return { kind: "unavailable", reason: "not_provided" };
|
|
140
|
+
const body = isRecord(res.body) && res.body.ok === true ? res.body.release : res.body;
|
|
141
|
+
const parsed = parseApprovedRelease(body);
|
|
142
|
+
return parsed.ok ? { kind: "approved", release: parsed.release, source: "network" } : { kind: "unavailable", reason: parsed.reason };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err instanceof MeError && err.kind === "network")
|
|
146
|
+
return { kind: "unavailable", reason: "unreachable" };
|
|
147
|
+
if (err instanceof CliError && (err.status === 401 || err.status === 403))
|
|
148
|
+
return { kind: "unavailable", reason: "credential_refused" };
|
|
149
|
+
if (err instanceof MeError && err.kind === "shape")
|
|
150
|
+
return { kind: "unavailable", reason: "response_invalid" };
|
|
151
|
+
return { kind: "unavailable", reason: "unreachable" };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** The component record a host's desired-release answer carries, from the same read the image
|
|
155
|
+
* update used. A legacy answer with images only reads `not_provided`; a record whose generation is
|
|
156
|
+
* not the image generation reads `envelope_mismatch`, so the two halves never mix snapshots. */
|
|
157
|
+
export function hostApproval(read) {
|
|
158
|
+
if (read.kind === "no_credential")
|
|
159
|
+
return { kind: "unavailable", reason: "host_unreadable" };
|
|
160
|
+
if (read.kind === "unreachable")
|
|
161
|
+
return { kind: "unavailable", reason: "unreachable" };
|
|
162
|
+
if (read.status === 401 || read.status === 403)
|
|
163
|
+
return { kind: "unavailable", reason: "credential_refused" };
|
|
164
|
+
// 404 and 503 are the cloud saying it has no approved release to name; anything else failed.
|
|
165
|
+
if (read.status === 404 || read.status === 503)
|
|
166
|
+
return { kind: "unavailable", reason: "not_provided" };
|
|
167
|
+
if (read.status !== 200)
|
|
168
|
+
return { kind: "unavailable", reason: "unreachable" };
|
|
169
|
+
if (!isRecord(read.body) || read.body.ok !== true || !isRecord(read.body.release))
|
|
170
|
+
return { kind: "unavailable", reason: "response_invalid" };
|
|
171
|
+
const release = read.body.release;
|
|
172
|
+
if (release.components === undefined)
|
|
173
|
+
return { kind: "unavailable", reason: "not_provided" };
|
|
174
|
+
const parsed = parseApprovedRelease(release.components);
|
|
175
|
+
if (!parsed.ok)
|
|
176
|
+
return { kind: "unavailable", reason: parsed.reason };
|
|
177
|
+
if (parsed.release.generation !== release.generation)
|
|
178
|
+
return { kind: "unavailable", reason: "envelope_mismatch" };
|
|
179
|
+
return { kind: "approved", release: parsed.release, source: "host" };
|
|
180
|
+
}
|
|
181
|
+
export function approvalCachePathFor(home) {
|
|
182
|
+
return join(configDirFor(home), APPROVED_RELEASE_CACHE_FILE);
|
|
183
|
+
}
|
|
184
|
+
/** The last approved record an applying check read, for an offline diagnostic. Never applied. */
|
|
185
|
+
export function readApprovalCache(home) {
|
|
186
|
+
const path = approvalCachePathFor(home);
|
|
187
|
+
if (!existsSync(path))
|
|
188
|
+
return null;
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
191
|
+
if (!isRecord(parsed) || !posInt(parsed.observedAtMs))
|
|
192
|
+
return null;
|
|
193
|
+
const release = parseApprovedRelease(parsed.release);
|
|
194
|
+
return release.ok ? { observedAtMs: parsed.observedAtMs, release: release.release } : null;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Best effort: a read-only home keeps working without its diagnostic cache. */
|
|
201
|
+
export function writeApprovalCache(home, cache) {
|
|
202
|
+
try {
|
|
203
|
+
const path = approvalCachePathFor(home);
|
|
204
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
205
|
+
writePrivateAtomic(path, `${JSON.stringify({ observedAtMs: cache.observedAtMs, release: { schemaVersion: 1, ...cache.release } }, null, 2)}\n`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// the diagnostic cache is optional
|
|
209
|
+
}
|
|
210
|
+
}
|
package/dist/args.js
CHANGED
|
@@ -29,6 +29,11 @@ export const FLAG_TABLES = {
|
|
|
29
29
|
yes: { value: false, help: "uninstall: confirm without a terminal" },
|
|
30
30
|
"delete-checkouts": { value: false, help: "uninstall: also delete the repository and thoughts checkouts" },
|
|
31
31
|
},
|
|
32
|
+
update: {
|
|
33
|
+
check: { value: false, help: "read the approved release and list every component against it, without changing anything" },
|
|
34
|
+
project: { value: true, help: "also check this project's SDK (an absolute path); with policy sdk, the project the setting is for" },
|
|
35
|
+
dir: { value: true, help: "the runner directory (default: the one setup created)" },
|
|
36
|
+
},
|
|
32
37
|
login: {
|
|
33
38
|
"start-replica": {
|
|
34
39
|
value: false,
|
|
@@ -508,6 +513,19 @@ export const FLAG_TABLES = {
|
|
|
508
513
|
help: "with --remove: answer the question yes (a run with no terminal only reports otherwise)",
|
|
509
514
|
},
|
|
510
515
|
},
|
|
516
|
+
lease: {
|
|
517
|
+
agent: { value: true, help: "the seat's name, 1 to 40 of a-z, 0-9 and - (take: required; renew, release: when this machine holds the ticket for several seats)" },
|
|
518
|
+
ttl: { value: true, help: "take, renew: minutes until the lease lapses without a renewal (the cloud keeps it between 5 and 60; default 30)" },
|
|
519
|
+
commit: { value: true, help: "renew: the full 40-character sha of a commit you pushed" },
|
|
520
|
+
artifact: { value: true, help: "renew: a thoughts path or check-run id you wrote" },
|
|
521
|
+
finding: { value: true, help: "renew: a comment or review id you posted" },
|
|
522
|
+
pr: { value: true, help: "renew: the number of a pull request you opened" },
|
|
523
|
+
note: { value: true, help: "renew: a few words about the progress (not part of what counts as new)" },
|
|
524
|
+
generation: { value: true, help: "renew, release: the generation the take printed (default: the one this machine stored)" },
|
|
525
|
+
},
|
|
526
|
+
work: {
|
|
527
|
+
team: { value: true, help: "available: one team's key (default: every team in the workspace contract)" },
|
|
528
|
+
},
|
|
511
529
|
repo: {
|
|
512
530
|
reason: { value: true, help: "pause: required; resume: optional; 1 to 500 characters after trimming (owner or admin)" },
|
|
513
531
|
write: {
|
|
@@ -562,6 +580,7 @@ export const FLAG_TABLES = {
|
|
|
562
580
|
export const VERB_USAGE = {
|
|
563
581
|
hosts: "hosts [list] | hosts remove <name-or-id> [--yes] | hosts rename <name-or-id> <new-name> [--json]",
|
|
564
582
|
runner: "runner [status] | runner stop | runner start | runner uninstall [--yes] [--delete-checkouts] | runner update [--check] [--dir <runner directory>] [--fleet] | runner auto-update [status|on|off] [--json]",
|
|
583
|
+
update: "update [--check] [--project <absolute path>] [--dir <runner directory>] | update sdk --project <absolute path> | update policy [<component> on|off] | update policy sdk on|off --project <absolute path> [--json] (components: runner, supervisor, watchdog, cli, skills-cloud, skills-dev, replica)",
|
|
565
584
|
setup: "setup [onboard options] (sets up Catalyst step by step; same as catalyst onboard) | setup --engine <file> --engine-sha256 <hex> [-- <install options>] (run by install.sh; install.sh --help lists the options)",
|
|
566
585
|
login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
|
|
567
586
|
onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--remove-local-data] [--runner|--no-runner] [--no-color] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--remove-local-data] [--json]",
|
|
@@ -594,6 +613,8 @@ export const VERB_USAGE = {
|
|
|
594
613
|
messages: "messages read --ticket CTC-N [--since SEQUENCE] [--json]",
|
|
595
614
|
capabilities: "capabilities [--json]",
|
|
596
615
|
project: "project <list [--json] | wip-limit <get|set <n>|set default> [--team K] [--json]>",
|
|
616
|
+
lease: "lease <take <ticket> --agent <seat> [--ttl <minutes>] | renew <ticket> (--commit <sha>|--artifact <path or id>|--finding <id>|--pr <number>) [--note <text>] [--agent <seat>] [--generation <n>] | release <ticket> [--agent <seat>] [--generation <n>] | show <ticket>> [--json] (a seat's ticket lease: take, renew with new progress, release, read; needs login)",
|
|
617
|
+
work: "work available [--team K] [--json] (per team: tickets nobody holds, with the last holder and progress, and open pull requests no live lease covers, drafts included)",
|
|
597
618
|
repo: "repo <status|pause <owner/name> --reason <text>|resume <owner/name> [--reason <text>]|agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> [--json] (status: visible workspace repositories; agents-block/agent-setup: local checkout)",
|
|
598
619
|
legacy: "legacy [--remove [--data] [--yes]] [--json]",
|
|
599
620
|
connections: "connections personal <linear|github> <start|status> [--wait <seconds>] [--json]",
|
package/dist/capabilities.js
CHANGED
|
@@ -8,6 +8,9 @@ export const CAPABILITIES = [
|
|
|
8
8
|
{ verb: "hosts", does: "list this account's machines, current use and live capacity", needs: "member", routes: [], since: "0.16.0" },
|
|
9
9
|
{ verb: "hosts remove|rename", does: "remove or rename a self-hosted machine in this account", needs: "admin", routes: [], since: "0.16.0" },
|
|
10
10
|
{ verb: "runner update|auto-update", does: "move this machine's self-hosted runner to the release Catalyst approved, and check for one every 15 minutes", needs: "member", routes: [], since: "0.16.16" },
|
|
11
|
+
{ verb: "update", does: "list every installed component against the release Catalyst approved, move the runner images to it, and name the command for the rest", needs: "member", routes: [], since: "0.16.31" },
|
|
12
|
+
{ verb: "update sdk", does: "move one project's Catalyst SDK to the approved versions with its own package manager, when the person asks", needs: "member", routes: [], since: "0.16.31" },
|
|
13
|
+
{ verb: "update policy", does: "turn automatic updates off or on for one installed component, or set a project's SDK to notify only", needs: "member", routes: [], since: "0.16.31" },
|
|
11
14
|
{ verb: "runner status|stop|start", does: "stop this machine's self-hosted runner taking work, or start it again with the same registration", needs: "member", routes: [], since: "0.16.27" },
|
|
12
15
|
{ verb: "runner uninstall", does: "remove this machine's self-hosted runner and revoke its registration and runner key", needs: "admin", routes: [], since: "0.16.27" },
|
|
13
16
|
// CTC-4680: plain `catalyst setup` runs the onboard flow, so install.sh may hand off to it. install.sh
|
|
@@ -32,6 +35,8 @@ export const CAPABILITIES = [
|
|
|
32
35
|
{ verb: "project wip-limit set", does: "set a project's new-start WIP limit (0 to 9999, or back to the workspace default)", needs: "admin", routes: [agent("POST", "team-wip-limit")], since: "0.13.1" },
|
|
33
36
|
{ verb: "repo status", does: "list visible repositories with their state, pause details and projects", needs: "member", routes: [agent("GET", "repos")], since: "0.16.27" },
|
|
34
37
|
{ verb: "repo pause|resume", does: "pause or resume a repository as a workspace owner or admin", needs: "admin", routes: [agent("GET", "repos"), agent("POST", "repos/pause"), agent("POST", "repos/resume")], since: "0.16.27" },
|
|
38
|
+
{ verb: "lease take|renew|release|show", does: "hold the ticket a seat works through a ticket lease that lapses without new progress, instead of a label", needs: "member", routes: [agent("POST", "ticket-lease/take"), agent("POST", "ticket-lease/renew"), agent("POST", "ticket-lease/release"), agent("GET", "ticket-lease")], since: "0.16.30" },
|
|
39
|
+
{ verb: "work available", does: "list tickets nobody holds, with the last holder and progress, and open pull requests no lease covers", needs: "member", routes: [], since: "0.16.30" },
|
|
35
40
|
{ verb: "repo agents-block", does: "add or refresh the one Catalyst block in a checkout's AGENTS.md, in the working tree, for the person to commit and open as a pull request", needs: "member", routes: [], since: "0.13.1" },
|
|
36
41
|
{ verb: "repo agent-setup", does: "read a checkout's agent setup (AGENTS.md, CLAUDE.md, .agents and .claude skills and rules) and, on request, make it portable in the working tree", needs: "member", routes: [], since: "0.13.1" },
|
|
37
42
|
{ verb: "env shell", does: "open this repository's environment on this computer: Catalyst's image, the declared toolchain and variables (needs Docker; no login for Catalyst's image)", needs: "member", routes: [], since: "0.16.20" },
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { configDirFor, publishedCachePathFor } from "./config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { newestCliVersion } from "./published.js";
|
|
6
6
|
import { semverOlder } from "./semver.js";
|
|
7
7
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
8
|
const RELEASE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
@@ -62,12 +62,15 @@ export async function maybePrintPublishedUpdateNotice(ctx, options) {
|
|
|
62
62
|
rmSync(temporary, { force: true });
|
|
63
63
|
}
|
|
64
64
|
// Record the attempt before the bounded lookup, including failures, so offline commands do
|
|
65
|
-
// not repeatedly contact npm. Reuse ready's anonymous request and version cache.
|
|
66
|
-
|
|
65
|
+
// not repeatedly contact npm. Reuse ready's anonymous request and version cache. Once this
|
|
66
|
+
// install has read an approved release, that is the version named (CTC-6708).
|
|
67
|
+
const reply = await newestCliVersion(ctx);
|
|
67
68
|
if (reply.latest &&
|
|
68
69
|
RELEASE.test(reply.latest) &&
|
|
69
70
|
semverOlder(options.version, reply.latest)) {
|
|
70
|
-
ctx.stderr(
|
|
71
|
+
ctx.stderr(reply.source === "approved"
|
|
72
|
+
? `Catalyst ${reply.latest} is approved for this workspace. Update with: catalyst update`
|
|
73
|
+
: `Catalyst ${reply.latest} is out. Update with: npm install -g @catalyst-cloud/cli@latest`);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
catch {
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cmdHosts } from "./hosts.js";
|
|
2
2
|
import { cmdRunner } from "./runner-command.js";
|
|
3
|
+
import { cmdUpdate } from "./update-command.js";
|
|
3
4
|
import { cmdSeat } from "./seat.js";
|
|
4
5
|
// cli.ts — the `catalyst` dispatcher (npm: @catalyst-cloud/cli). Verbs live in
|
|
5
6
|
// their own modules; this file keeps every export the 0.1 tests and the bin import.
|
|
@@ -21,6 +22,7 @@ import { openBrowserChecked as defaultOpenBrowser } from "./browser.js";
|
|
|
21
22
|
import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning, } from "./execution.js";
|
|
22
23
|
import { cmdQuery } from "./query.js";
|
|
23
24
|
import { maybePrintPublishedUpdateNotice } from "./cli-update-notice.js";
|
|
25
|
+
import { semverOlder } from "./semver.js";
|
|
24
26
|
import { cmdReady } from "./ready.js";
|
|
25
27
|
import { cmdReplica } from "./replica.js";
|
|
26
28
|
import { cmdRuntime } from "./runtime-verb.js";
|
|
@@ -46,6 +48,7 @@ import { cmdTeam } from "./team.js";
|
|
|
46
48
|
import { cmdCapabilities } from "./capabilities.js";
|
|
47
49
|
import { cmdProject } from "./project.js";
|
|
48
50
|
import { cmdRepo } from "./repo.js";
|
|
51
|
+
import { cmdLease, cmdWork } from "./lease.js";
|
|
49
52
|
import { cmdLegacy } from "./legacy.js";
|
|
50
53
|
import { cmdIdentity } from "./identity.js";
|
|
51
54
|
import { cmdMessages } from "./messages.js";
|
|
@@ -89,6 +92,7 @@ export function usageText() {
|
|
|
89
92
|
" catalyst hosts [list] | hosts remove <name-or-id> [--yes] | hosts rename <name-or-id> <new-name>",
|
|
90
93
|
" catalyst runner [status] | runner stop | runner start | runner uninstall [--yes] [--delete-checkouts] (this machine's self-hosted runner)",
|
|
91
94
|
" catalyst runner update [--check] | runner auto-update [status|on|off] (move this machine's runner to the approved release)",
|
|
95
|
+
" catalyst update [--check] [--project <path>] | update policy [<component> on|off] (every installed component against the approved release)",
|
|
92
96
|
" catalyst mcp add|list|remove (vault references only) | mcp status <name> (a server's state, grants and last 50 tool calls)",
|
|
93
97
|
" catalyst contract [--refresh] [--path <a.b.c>]",
|
|
94
98
|
" catalyst query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
|
|
@@ -113,6 +117,8 @@ export function usageText() {
|
|
|
113
117
|
" catalyst repo pause <owner/name> --reason <text> | repo resume <owner/name> [--reason <text>] (owner or admin; reason: 1 to 500 trimmed characters)",
|
|
114
118
|
" catalyst repo status [--json] (visible workspace repositories and their paused state; needs login)",
|
|
115
119
|
" 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)",
|
|
120
|
+
" catalyst lease <take <ticket> --agent <seat> [--ttl <minutes>]|renew <ticket> --commit|--artifact|--finding|--pr <ref> [--note <text>]|release <ticket>|show <ticket>> [--json] (a seat's ticket lease; needs login)",
|
|
121
|
+
" catalyst work available [--team K] [--json] (tickets nobody holds and open pull requests no lease covers)",
|
|
116
122
|
" catalyst legacy [--remove [--data] [--yes]] (leftovers of the old local Catalyst runtime: list them; remove them only on a yes)",
|
|
117
123
|
" catalyst identity linear <status|options|set> [<linearUserId>] [--json]",
|
|
118
124
|
" catalyst messages read --ticket CTC-N [--since SEQUENCE] [--json] (messages sent to a ticket on your teams)",
|
|
@@ -195,6 +201,10 @@ function maybePrintUpdateNotice(args, ctx) {
|
|
|
195
201
|
const manifest = readManifest();
|
|
196
202
|
if (cfg.lastSkillBundleVersion === manifest.version)
|
|
197
203
|
return;
|
|
204
|
+
// CTC-6741: a newer CLI selected by `catalyst update` records its own bundle here, and the older
|
|
205
|
+
// npm-installed one still runs for `catalyst runtime`. Copied skills only ever move forward.
|
|
206
|
+
if (cfg.lastSkillBundleVersion && semverOlder(manifest.version, cfg.lastSkillBundleVersion))
|
|
207
|
+
return;
|
|
198
208
|
const previous = cfg.lastSkillBundleVersion;
|
|
199
209
|
if (previous) {
|
|
200
210
|
const entry = parseChangelogEntry(readChangelog(), manifest.version);
|
|
@@ -548,6 +558,10 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
548
558
|
return await cmdProject(args, ctx);
|
|
549
559
|
case "repo":
|
|
550
560
|
return await cmdRepo(args, ctx);
|
|
561
|
+
case "lease":
|
|
562
|
+
return await cmdLease(args, ctx);
|
|
563
|
+
case "work":
|
|
564
|
+
return await cmdWork(args, ctx);
|
|
551
565
|
case "legacy":
|
|
552
566
|
return await cmdLegacy(args, ctx, { localSyncSelected: savedLocalSync(ctx), ...deps.legacy });
|
|
553
567
|
case "identity":
|
|
@@ -558,6 +572,8 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
558
572
|
return await cmdHosts(args, ctx, deps.hosts);
|
|
559
573
|
case "runner":
|
|
560
574
|
return await cmdRunner(args, ctx, deps.runner);
|
|
575
|
+
case "update":
|
|
576
|
+
return await cmdUpdate(args, ctx, deps.update);
|
|
561
577
|
case "connections":
|
|
562
578
|
return await cmdConnections(args, ctx, deps.connections ?? {});
|
|
563
579
|
default:
|
|
@@ -624,8 +640,11 @@ const VERB_HELP_KNOWN = Object.fromEntries([
|
|
|
624
640
|
"capabilities",
|
|
625
641
|
"project",
|
|
626
642
|
"repo",
|
|
643
|
+
"lease",
|
|
644
|
+
"work",
|
|
627
645
|
"legacy",
|
|
628
646
|
"onboard",
|
|
647
|
+
"update",
|
|
629
648
|
].map((v) => [v, true]));
|
|
630
649
|
/** CTC-4680: the Pixel Nucleus mark belongs to `catalyst setup` alone. `setup` runs the onboard
|
|
631
650
|
* flow, so this is read from the verb typed, before setup becomes onboard. */
|