@catalyst-cloud/cli 0.14.6 → 0.14.7
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 +5 -0
- package/README.md +5 -3
- package/dist/args.js +51 -13
- package/dist/cli.js +58 -10
- package/dist/config.js +16 -3
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +243 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +203 -0
- package/dist/events-cloud-tail.js +134 -0
- package/dist/events-cloud.js +500 -0
- package/dist/events.js +13 -4
- package/dist/oauth.js +125 -33
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +147 -37
- package/dist/onboard-runtime.js +104 -9
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +99 -11
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard.js +359 -27
- package/dist/owned-fetch.js +182 -0
- package/dist/query.js +77 -27
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +2 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +3 -3
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +5 -4
- package/skills/catalyst-onboard/references/local-sync.md +3 -1
- package/skills/run-this-project/SKILL.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 +2 -2
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.7
|
|
4
|
+
|
|
5
|
+
- Setup uses cloud reads by default and offers local sync as an option. Onboarding preserves verified account and repository choices when it resumes.
|
|
6
|
+
- Settings drafts stay private for review; keeping a draft does not approve it or import values.
|
|
7
|
+
|
|
3
8
|
## 0.14.6
|
|
4
9
|
|
|
5
10
|
- A current Catalyst data folder no longer triggers a legacy-runtime cleanup warning. Legacy checks and removal keep current shared data and report it separately.
|
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ catalyst ready --onboarding --json
|
|
|
126
126
|
|
|
127
127
|
The dry run writes no files. `--only <step>` runs one step and reports its scope separately from full onboarding. Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. The private receipt is `install/last-run.json` under your machine's Catalyst state directory; credentials stay in the login config. A fake HOME reports old services and keeps them intact.
|
|
128
128
|
|
|
129
|
-
Local sync is optional. Include it in the plan with `--local-sync` when you need local SQL or
|
|
129
|
+
Local sync is optional. Include it in the plan with `--local-sync` when you need local SQL or sustained local reads. A resume keeps that choice. Readiness distinguishes failed checks from missing evidence and reports observed project work separately from unfinished setup.
|
|
130
130
|
|
|
131
131
|
## Requirements
|
|
132
132
|
|
|
@@ -156,7 +156,9 @@ Nothing, by default. After `login`, every read, write, ask and explanation goes
|
|
|
156
156
|
| `catalyst replica start` | local SQL, cheap repeated reads, and local event `tail`, `wait-for`, and `query` | one SQLite file plus a bounded event cache under `$XDG_STATE_HOME/catalyst/events/` (fallback `~/.local/state/catalyst/events/`) | one long-running process; foreground by default, `--detach` writes a pidfile beside the database and returns; `login --start-replica` does the same at the end of login |
|
|
157
157
|
| `catalyst watch` | a project owner reacting to its scope | one cursor file (`~/.config/catalyst-cloud/watch-cursor.json`) stamped with the account | lives inside the session that armed it; exits with it |
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
CLI and skill reads use the cloud API by default, including on a machine with a fresh local replica. No background writer or local database is needed. `catalyst query ... --source replica` explicitly chooses the optional local view; the first stderr line names the source. Local sync does not promise offline browsing.
|
|
160
|
+
|
|
161
|
+
For a local view, `catalyst replica status` checks the writer, heartbeat and cursor. It exits `0` for fresh, `1` for present but stale, `2` for not connected, `3` for absent. `replica status --probe` compares its cursor with the cloud head. These diagnostics do not change the default read source, and an absent replica does not make `catalyst ready` fail.
|
|
160
162
|
|
|
161
163
|
`catalyst events status --probe` checks the event cache's own cursor and compares it with the cloud head; `replica status --probe` checks the separate entity-replica cursor. A fresh replica does not prove event freshness. `events tail` follows new cached events, `events wait-for --ticket ... --timeout ...` performs a bounded wait, and `events query` reads retained history. Those three commands only read the cache; `events status --probe` makes the cloud comparison. The replica process is the single writer and reports an event-sync failure without terminating a healthy entity replica. The event cache keeps closed daily segments for at most seven days or 256 MiB per account and reports an explicit gap when a requested sequence has retired.
|
|
162
164
|
|
|
@@ -170,7 +172,7 @@ The replica is a Node process, not a service: the supported path is the plain co
|
|
|
170
172
|
| `whats-happening` | "What's happening? Why is that stuck? How does this work? How does it prioritise?" | The desk for your account: reads the contract, what is running and queued, the eligibility explainer, the coding accounts and the open asks, and answers in one reply with ticket ids. Carries the facts behind the answers: the ladder, the stage map, failures, parks and holds, the queue order and every reason a ticket is excluded. Routes work to a project owner and decisions to `what-needs-me`. | [`SKILL.md`](skills/whats-happening/SKILL.md) |
|
|
171
173
|
| `what-needs-me` | "What needs me? What am I blocking?" | The human's decision inbox, ranked by how much open work each ask holds, and the one way an agent raises a decision on their behalf: files an ask through the cloud's ask route with the account's own template and records the answer so the held work releases. | [`SKILL.md`](skills/what-needs-me/SKILL.md) |
|
|
172
174
|
| `run-this-project` | "Run this project for me. Own it until it closes." | Single-threaded owner of one project: subscribes to the account's event stream for its scope, reacts to each change in the same turn, makes tickets ready and moves them to dispatch, parks what should stop, chases stalls, escalates inward, and keeps one status summary current. Never polls. | [`SKILL.md`](skills/run-this-project/SKILL.md) |
|
|
173
|
-
| `catalyst-linear` | "Show me the ticket, the history, what Catalyst wrote on it." | Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the
|
|
175
|
+
| `catalyst-linear` | "Show me the ticket, the history, what Catalyst wrote on it." | Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the cloud API by default, with explicit local opt-in and the source named; writes comments, card moves, labels and new tickets as the app actor; knows what a ticket accumulates as Catalyst works it. | [`SKILL.md`](skills/catalyst-linear/SKILL.md) |
|
|
174
176
|
| `catalyst-github` | "Show me the PR, the checks, the review, the queue." | A ticket's pull request with its checks, reviews and review threads; whether it is mergeable under the repository's policy; what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). | [`SKILL.md`](skills/catalyst-github/SKILL.md) |
|
|
175
177
|
| `unstick` | "Why is this parked? Unpark it. Get things flowing again." | Reads why a ticket is not running and every park or hold on it, decides whether the recorded cause is fixed, previews the release and releases it the right way from your own login — or one failure class across a team — and raises an ask only for what a person has to do. | [`SKILL.md`](skills/unstick/SKILL.md) |
|
|
176
178
|
| `what-this-repo-needs` | "What env vars does this repo need? What belongs in its environment declaration?" | Scans a repository offline — no login, no network — and lists the environment variable names it needs, grouped build/test, deploy-only and bindings, each with where it was found, what uses it, and where a local value would come from. Never reads or prints a value. Also checks TOML syntax and the environment variable table in `.catalyst/catalyst.toml`. | [`SKILL.md`](skills/what-this-repo-needs/SKILL.md) |
|
package/dist/args.js
CHANGED
|
@@ -41,13 +41,29 @@ export const FLAG_TABLES = {
|
|
|
41
41
|
},
|
|
42
42
|
install: {},
|
|
43
43
|
onboard: {
|
|
44
|
-
team: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
team: {
|
|
45
|
+
value: true,
|
|
46
|
+
help: "explicitly select an existing Linear team by ID or unique key",
|
|
47
|
+
},
|
|
48
|
+
repo: {
|
|
49
|
+
value: true,
|
|
50
|
+
repeat: true,
|
|
51
|
+
help: "explicitly select an accessible repository as owner/name (repeatable)",
|
|
52
|
+
},
|
|
53
|
+
"local-sync": {
|
|
54
|
+
value: false,
|
|
55
|
+
help: "include the optional local sync setup in this plan",
|
|
56
|
+
},
|
|
57
|
+
"resume-from": {
|
|
58
|
+
value: true,
|
|
59
|
+
help: "resume at a verified onboarding step",
|
|
60
|
+
},
|
|
48
61
|
only: { value: true, help: "run only the named onboarding step" },
|
|
49
62
|
yes: { value: false, help: "accept the plan's default choices" },
|
|
50
|
-
"dry-run": {
|
|
63
|
+
"dry-run": {
|
|
64
|
+
value: false,
|
|
65
|
+
help: "print the plan without taking the lock or changing files",
|
|
66
|
+
},
|
|
51
67
|
},
|
|
52
68
|
status: {},
|
|
53
69
|
notice: {},
|
|
@@ -67,7 +83,7 @@ export const FLAG_TABLES = {
|
|
|
67
83
|
since: { value: true, help: "changes: the cursor to read after" },
|
|
68
84
|
source: {
|
|
69
85
|
value: true,
|
|
70
|
-
help: "replica | api (default: replica
|
|
86
|
+
help: "replica | api (default: api; replica is an explicit local opt-in)",
|
|
71
87
|
},
|
|
72
88
|
ticket: { value: true, help: "pulls: only PRs linked to this ticket" },
|
|
73
89
|
all: {
|
|
@@ -235,8 +251,14 @@ export const FLAG_TABLES = {
|
|
|
235
251
|
role: { value: true, help: "accept: the role recording the answer" },
|
|
236
252
|
},
|
|
237
253
|
ready: {
|
|
238
|
-
onboarding: {
|
|
239
|
-
|
|
254
|
+
onboarding: {
|
|
255
|
+
value: false,
|
|
256
|
+
help: "check current onboarding evidence and observed project work",
|
|
257
|
+
},
|
|
258
|
+
"local-sync": {
|
|
259
|
+
value: false,
|
|
260
|
+
help: "include the selected local sync mode",
|
|
261
|
+
},
|
|
240
262
|
offline: {
|
|
241
263
|
value: false,
|
|
242
264
|
help: "skip the published-release check (no network)",
|
|
@@ -244,13 +266,29 @@ export const FLAG_TABLES = {
|
|
|
244
266
|
},
|
|
245
267
|
accounts: {},
|
|
246
268
|
env: {
|
|
247
|
-
root: {
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
root: {
|
|
270
|
+
value: true,
|
|
271
|
+
help: "draft: repository directory (default: current directory)",
|
|
272
|
+
},
|
|
273
|
+
write: {
|
|
274
|
+
value: false,
|
|
275
|
+
help: "draft: write a new .catalyst/catalyst.toml; refuse to overwrite",
|
|
276
|
+
},
|
|
277
|
+
diff: {
|
|
278
|
+
value: false,
|
|
279
|
+
help: "draft: show the generated TOML as an additions-only diff",
|
|
280
|
+
},
|
|
250
281
|
},
|
|
251
282
|
var: {
|
|
252
|
-
repo: {
|
|
253
|
-
|
|
283
|
+
repo: {
|
|
284
|
+
value: true,
|
|
285
|
+
help: "repository scope (owner/name); omit for tenant scope",
|
|
286
|
+
},
|
|
287
|
+
names: {
|
|
288
|
+
value: true,
|
|
289
|
+
repeat: true,
|
|
290
|
+
help: "import: only send these names (repeatable or comma-separated)",
|
|
291
|
+
},
|
|
254
292
|
},
|
|
255
293
|
team: {
|
|
256
294
|
all: { value: false, help: "check every team, one request at a time" },
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { cmdReady } from "./ready.js";
|
|
|
15
15
|
import { cmdReplica } from "./replica.js";
|
|
16
16
|
import { cmdRuntime } from "./runtime-verb.js";
|
|
17
17
|
import { cmdEvents } from "./events.js";
|
|
18
|
+
import { stageOnboardLogin } from "./onboard-login-candidate.js";
|
|
18
19
|
import { stdinIsTty } from "./prompt.js";
|
|
19
20
|
import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion, } from "./skill-shape.js";
|
|
20
21
|
import { installedBundleVersion, installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine, } from "./skills.js";
|
|
@@ -35,9 +36,9 @@ import { cmdLegacy } from "./legacy.js";
|
|
|
35
36
|
import { cmdIdentity } from "./identity.js";
|
|
36
37
|
import { cmdConnections } from "./connections.js";
|
|
37
38
|
import { cmdOnboard, onboardErrorJournal } from "./onboard.js";
|
|
38
|
-
import { createClackOnboardUi, shouldUseOnboardUi } from "./onboard-ui.js";
|
|
39
|
+
import { createClackOnboardUi, shouldUseOnboardUi, } from "./onboard-ui.js";
|
|
39
40
|
import { createOnboardRuntime } from "./onboard-runtime.js";
|
|
40
|
-
import { onboardingReadyReport, observeCloudOnboarding } from "./onboard-ready.js";
|
|
41
|
+
import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
|
|
41
42
|
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
42
43
|
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, };
|
|
43
44
|
export const CUSTOMER_SKILLS = [
|
|
@@ -252,7 +253,10 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
252
253
|
}
|
|
253
254
|
if (input.isTTY) {
|
|
254
255
|
try {
|
|
255
|
-
ui = createClackOnboardUi(await import("@clack/prompts"), {
|
|
256
|
+
ui = createClackOnboardUi(await import("@clack/prompts"), {
|
|
257
|
+
input,
|
|
258
|
+
output: process.stdout,
|
|
259
|
+
});
|
|
256
260
|
}
|
|
257
261
|
catch (error) {
|
|
258
262
|
terminal?.destroy();
|
|
@@ -265,23 +269,64 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
265
269
|
...createOnboardRuntime(args, ctx, {
|
|
266
270
|
ui,
|
|
267
271
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
272
|
+
stageSignin: (signal) => {
|
|
273
|
+
if (args.key || ctx.env.CATALYST_CLOUD_TOKEN)
|
|
274
|
+
throw new CliError("Connect your personal key with catalyst login, then run catalyst onboard. Setup will not replace a supplied key with browser sign-in.", "onboard-person-required", 12);
|
|
275
|
+
return stageOnboardLogin({ ...ctx, stdout: ctx.stderr }, {
|
|
276
|
+
baseUrl: args.baseUrl,
|
|
277
|
+
signal,
|
|
278
|
+
timeoutMs: deps.onboardSigninTimeoutMs,
|
|
279
|
+
device: {
|
|
280
|
+
isTty: deps.isTty ?? (() => Boolean(process.stdout.isTTY)),
|
|
281
|
+
openBrowser: deps.openBrowser ?? defaultOpenBrowser,
|
|
282
|
+
sleep: deps.sleep,
|
|
283
|
+
waitForApproval: ui
|
|
284
|
+
? (run) => ui.wait("Waiting for Catalyst approval", run)
|
|
285
|
+
: undefined,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
},
|
|
268
289
|
login: (stepCtx, signal) => cmdLogin({ ...args, command: "login", flags: {}, json: false }, stepCtx, {
|
|
269
|
-
...deps,
|
|
270
|
-
|
|
290
|
+
...deps,
|
|
291
|
+
signal,
|
|
292
|
+
isTty: () => true,
|
|
293
|
+
waitForApproval: ui
|
|
294
|
+
? (run) => ui.wait("Waiting for Catalyst approval", run)
|
|
295
|
+
: undefined,
|
|
271
296
|
}, true),
|
|
272
297
|
ready: async (stepCtx, journal) => {
|
|
273
298
|
const team = selectedOnboardTeam(journal);
|
|
274
|
-
const report = await onboardingReadyReport(stepCtx, {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
299
|
+
const report = await onboardingReadyReport(stepCtx, {
|
|
300
|
+
teamIds: team ? [team] : undefined,
|
|
301
|
+
localSync: journal?.localSync ?? args.flags["local-sync"] === true,
|
|
302
|
+
observe: observeCloudOnboarding,
|
|
303
|
+
});
|
|
304
|
+
const failed = report.checks.some((check) => check.required && check.state === "fail");
|
|
305
|
+
return {
|
|
306
|
+
state: report.state === "complete"
|
|
307
|
+
? "done"
|
|
308
|
+
: failed
|
|
309
|
+
? "failed"
|
|
310
|
+
: "waiting",
|
|
311
|
+
reason: report.state === "complete"
|
|
312
|
+
? undefined
|
|
313
|
+
: "onboarding_checks_pending",
|
|
314
|
+
evidence: {
|
|
315
|
+
checks: report.checks.length,
|
|
316
|
+
passed: report.checks.filter((check) => check.state === "pass").length,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
278
319
|
},
|
|
279
320
|
legacy: deps.legacy,
|
|
280
321
|
openBrowser: deps.openBrowser,
|
|
281
322
|
sleep: deps.sleep,
|
|
282
323
|
skillNames: CUSTOMER_SKILLS,
|
|
283
324
|
}),
|
|
325
|
+
...(deps.onboardBootstrap
|
|
326
|
+
? { bootstrap: deps.onboardBootstrap }
|
|
327
|
+
: {}),
|
|
284
328
|
...(deps.isTty ? { isTty: deps.isTty } : {}),
|
|
329
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
285
330
|
}, readManifest().version);
|
|
286
331
|
}
|
|
287
332
|
finally {
|
|
@@ -485,7 +530,10 @@ async function cmdLogin(args, ctx, deps, personalOnly = false) {
|
|
|
485
530
|
: ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
|
|
486
531
|
ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
|
|
487
532
|
try {
|
|
488
|
-
const loaded = await loadContract(ctx, config, {
|
|
533
|
+
const loaded = await loadContract(ctx, config, {
|
|
534
|
+
refresh: true,
|
|
535
|
+
signal: deps.signal,
|
|
536
|
+
});
|
|
489
537
|
checkCancelled();
|
|
490
538
|
ctx.stdout(`Tenant contract ${loaded.doc.contractVersion} cached at ${loaded.path}`);
|
|
491
539
|
}
|
package/dist/config.js
CHANGED
|
@@ -144,12 +144,25 @@ function machineFilePresent(file) {
|
|
|
144
144
|
throw error;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
/** The machine paths record, or null on a machine that has not run paths setup. */
|
|
148
|
+
function declaredMachinePaths(home, env) {
|
|
149
|
+
const file = machinePathsFile({ env: { ...env, HOME: home } });
|
|
150
|
+
if (!file || (env.CATALYST_PATHS_FILE === undefined && !machineFilePresent(file)))
|
|
151
|
+
return null;
|
|
152
|
+
return parseMachinePaths(JSON.parse(readFileSync(file, "utf8")));
|
|
153
|
+
}
|
|
154
|
+
/** True only when this machine opted in to the local replica: CATALYST_REPLICA_DB is set, or the
|
|
155
|
+
* machine paths file declares replicaDb. Every other machine reads the cloud (CTC-4508). */
|
|
156
|
+
export function replicaOptedIn(home, env = {}) {
|
|
157
|
+
if (env.CATALYST_REPLICA_DB !== undefined)
|
|
158
|
+
return true;
|
|
159
|
+
return declaredMachinePaths(home, env)?.paths.replicaDb !== undefined;
|
|
160
|
+
}
|
|
147
161
|
export function replicaDbPath(cfg, home, env = {}) {
|
|
148
162
|
if (env.CATALYST_REPLICA_DB !== undefined)
|
|
149
163
|
return resolveCatalystPath("replicaDb", { env });
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
const machine = parseMachinePaths(JSON.parse(readFileSync(file, "utf8")));
|
|
164
|
+
const machine = declaredMachinePaths(home, env);
|
|
165
|
+
if (machine) {
|
|
153
166
|
if (machine.paths.replicaDb === undefined)
|
|
154
167
|
throw new CliError("optional replica is not configured; set CATALYST_REPLICA_DB or declare replicaDb in the machine paths file", "replica-not-configured");
|
|
155
168
|
return resolveCatalystPath("replicaDb", { env, machine });
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { DaemonAuthorityError, readDaemonAuthority, } from "./daemon-authority.js";
|
|
2
|
+
function sleep(ms, signal) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
if (signal.aborted) {
|
|
5
|
+
reject(signal.reason);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const abort = () => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
signal.removeEventListener("abort", abort);
|
|
11
|
+
reject(signal.reason);
|
|
12
|
+
};
|
|
13
|
+
const timer = setTimeout(() => {
|
|
14
|
+
signal.removeEventListener("abort", abort);
|
|
15
|
+
resolve();
|
|
16
|
+
}, ms);
|
|
17
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** One joined task owns fresh /me renewal; no config refresh/write or token publication shortcut.
|
|
21
|
+
* An expired/changed proof aborts peers through onFailure before waiting for held reader cleanup. */
|
|
22
|
+
export async function createDaemonAuthorityLifetime(ctx, parent) {
|
|
23
|
+
const owned = new AbortController(), lifetime = AbortSignal.any([parent, owned.signal]);
|
|
24
|
+
let proof = await readDaemonAuthority(ctx, lifetime);
|
|
25
|
+
const scope = proof.scope;
|
|
26
|
+
let running;
|
|
27
|
+
let fault;
|
|
28
|
+
let report;
|
|
29
|
+
let notice;
|
|
30
|
+
const failures = [];
|
|
31
|
+
const fail = (error) => {
|
|
32
|
+
if (fault)
|
|
33
|
+
return;
|
|
34
|
+
fault = error;
|
|
35
|
+
// Callback starts synchronously so the supervisor aborts both feeds before drain waits.
|
|
36
|
+
if (report) {
|
|
37
|
+
try {
|
|
38
|
+
notice = report(error.kind === "identity"
|
|
39
|
+
? "identity_mismatch"
|
|
40
|
+
: "authentication_required");
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
notice = Promise.reject(error);
|
|
44
|
+
}
|
|
45
|
+
void notice.catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
owned.abort(error);
|
|
48
|
+
};
|
|
49
|
+
const assertCurrent = () => {
|
|
50
|
+
if (fault)
|
|
51
|
+
throw fault;
|
|
52
|
+
try {
|
|
53
|
+
proof.assertCurrent();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof DaemonAuthorityError)
|
|
57
|
+
fail(error);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
scope,
|
|
63
|
+
assertCurrent,
|
|
64
|
+
async getToken() {
|
|
65
|
+
assertCurrent();
|
|
66
|
+
const selected = proof;
|
|
67
|
+
try {
|
|
68
|
+
const token = await selected.getToken();
|
|
69
|
+
// Renewal may replace proof during the await; the returned bearer still needs its own
|
|
70
|
+
// ORIGINAL proof expiry, not the replacement's later authority window.
|
|
71
|
+
selected.assertCurrent();
|
|
72
|
+
assertCurrent();
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof DaemonAuthorityError)
|
|
77
|
+
fail(error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
run(signal, onFailure) {
|
|
82
|
+
if (running)
|
|
83
|
+
throw new Error("daemon_authority_already_running");
|
|
84
|
+
report = onFailure;
|
|
85
|
+
// An earlier synchronous cache guard may already have latched a fault before run starts.
|
|
86
|
+
if (fault && !notice) {
|
|
87
|
+
try {
|
|
88
|
+
notice = report(fault.kind === "identity"
|
|
89
|
+
? "identity_mismatch"
|
|
90
|
+
: "authentication_required");
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
notice = Promise.reject(error);
|
|
94
|
+
}
|
|
95
|
+
void notice.catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
const active = AbortSignal.any([lifetime, signal]);
|
|
98
|
+
running = (async () => {
|
|
99
|
+
let timer;
|
|
100
|
+
const expire = () => {
|
|
101
|
+
if (!active.aborted)
|
|
102
|
+
fail(new DaemonAuthorityError("authentication", "daemon_authority_expired"));
|
|
103
|
+
};
|
|
104
|
+
const arm = () => {
|
|
105
|
+
if (timer !== undefined)
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
const remaining = proof.expiresAt - ctx.now().getTime();
|
|
108
|
+
if (remaining <= 0) {
|
|
109
|
+
expire();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
timer = setTimeout(expire, remaining);
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
assertCurrent();
|
|
116
|
+
arm();
|
|
117
|
+
while (!active.aborted) {
|
|
118
|
+
await sleep(10_000, active);
|
|
119
|
+
if (active.aborted)
|
|
120
|
+
break;
|
|
121
|
+
// Old proof's timer stays armed throughout the read and its cleanup ACK.
|
|
122
|
+
const fresh = await readDaemonAuthority(ctx, active);
|
|
123
|
+
if (active.aborted)
|
|
124
|
+
break;
|
|
125
|
+
if (JSON.stringify(fresh.scope) !== JSON.stringify(scope))
|
|
126
|
+
throw new DaemonAuthorityError("identity", "daemon_authority_changed");
|
|
127
|
+
fresh.assertCurrent();
|
|
128
|
+
proof = fresh;
|
|
129
|
+
arm();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof AggregateError) {
|
|
134
|
+
if (!active.aborted) {
|
|
135
|
+
const primary = error.errors[0];
|
|
136
|
+
fail(primary instanceof DaemonAuthorityError
|
|
137
|
+
? primary
|
|
138
|
+
: new DaemonAuthorityError("authentication", "daemon_authority_cleanup_failed"));
|
|
139
|
+
}
|
|
140
|
+
failures.push(error);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (!active.aborted)
|
|
144
|
+
fail(error instanceof DaemonAuthorityError
|
|
145
|
+
? error
|
|
146
|
+
: new DaemonAuthorityError("authentication", "daemon_authority_unavailable"));
|
|
147
|
+
// Only an actual parent/owned cancellation is an ordinary requested stop. The proof
|
|
148
|
+
// failure callback persists its reason; no automatic restart renews that latch.
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (timer !== undefined)
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
if (notice)
|
|
154
|
+
await notice;
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
void running.catch((error) => {
|
|
158
|
+
if (!failures.includes(error))
|
|
159
|
+
failures.push(error);
|
|
160
|
+
});
|
|
161
|
+
return running;
|
|
162
|
+
},
|
|
163
|
+
async stop() {
|
|
164
|
+
owned.abort(new Error("daemon_authority_stop"));
|
|
165
|
+
if (running)
|
|
166
|
+
await running.catch(() => { });
|
|
167
|
+
if (failures.length)
|
|
168
|
+
throw new AggregateError(failures, "daemon_authority_cleanup_failed");
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|