@catalyst-cloud/catalyst-skills 0.5.0 → 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/README.md +11 -178
- package/bin/catalyst-skills.js +3 -49
- package/bin/catalyst.js +3 -0
- package/package.json +8 -29
- package/CHANGELOG.md +0 -41
- package/LICENSE +0 -21
- package/dist/args.js +0 -261
- package/dist/ask.js +0 -158
- package/dist/browser.js +0 -20
- package/dist/cli.js +0 -356
- package/dist/config.js +0 -148
- package/dist/contract-types.js +0 -4
- package/dist/contract.js +0 -178
- package/dist/detach.js +0 -10
- package/dist/errors.js +0 -27
- package/dist/events.js +0 -106
- package/dist/execution.js +0 -369
- package/dist/oauth.js +0 -300
- package/dist/prompt.js +0 -35
- package/dist/query.js +0 -225
- package/dist/ready.js +0 -163
- package/dist/release.js +0 -142
- package/dist/replica.js +0 -325
- package/dist/sdk.js +0 -26
- package/dist/skill-shape.js +0 -179
- package/dist/skills.js +0 -78
- package/dist/transport.js +0 -205
- package/dist/ts-deps-loader.js +0 -68
- package/dist/watch/consumer.js +0 -141
- package/dist/watch/cursor-file.js +0 -62
- package/dist/watch.js +0 -175
- package/dist/write.js +0 -224
- package/skills/catalyst-github/SKILL.md +0 -35
- package/skills/catalyst-github/agents/openai.yaml +0 -6
- package/skills/catalyst-github/agents/portability.yaml +0 -4
- package/skills/catalyst-github/references/is-it-mergeable.md +0 -57
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +0 -61
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +0 -124
- package/skills/catalyst-github/scripts/lib/cli.mjs +0 -103
- package/skills/catalyst-github/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-github/scripts/lib/pull.mjs +0 -82
- package/skills/catalyst-github/scripts/read-pr.mjs +0 -97
- package/skills/catalyst-linear/SKILL.md +0 -43
- package/skills/catalyst-linear/agents/openai.yaml +0 -6
- package/skills/catalyst-linear/agents/portability.yaml +0 -5
- package/skills/catalyst-linear/references/reading-a-ticket.md +0 -50
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +0 -53
- package/skills/catalyst-linear/references/writing-to-linear.md +0 -43
- package/skills/catalyst-linear/scripts/comment.mjs +0 -59
- package/skills/catalyst-linear/scripts/create-ticket.mjs +0 -44
- package/skills/catalyst-linear/scripts/label.mjs +0 -48
- package/skills/catalyst-linear/scripts/lib/cli.mjs +0 -164
- package/skills/catalyst-linear/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-linear/scripts/move.mjs +0 -41
- package/skills/catalyst-linear/scripts/read-ticket.mjs +0 -93
- package/skills/catalyst-linear/scripts/search.mjs +0 -49
- package/skills/catalyst-setup/SKILL.md +0 -32
- package/skills/catalyst-setup/agents/openai.yaml +0 -6
- package/skills/catalyst-setup/agents/portability.yaml +0 -4
- package/skills/catalyst-setup/references/what-each-check-means.md +0 -66
- package/skills/catalyst-setup/scripts/check.mjs +0 -75
- package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
- package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
- package/skills/connect-me/SKILL.md +0 -63
- package/skills/connect-me/agents/openai.yaml +0 -6
- package/skills/connect-me/agents/portability.yaml +0 -5
- package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
- package/skills/connect-me/scripts/lib/cli.mjs +0 -185
- package/skills/connect-me/scripts/lib/credential.mjs +0 -29
- package/skills/connect-me/scripts/verify-connection.mjs +0 -68
- package/skills/how-catalyst-works/SKILL.md +0 -42
- package/skills/how-catalyst-works/agents/openai.yaml +0 -6
- package/skills/how-catalyst-works/agents/portability.yaml +0 -4
- package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
- package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -54
- package/skills/how-catalyst-works/references/the-ladder.md +0 -41
- package/skills/how-catalyst-works/references/what-runs-next.md +0 -77
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +0 -94
- package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
- package/skills/run-this-project/SKILL.md +0 -45
- package/skills/run-this-project/agents/openai.yaml +0 -6
- package/skills/run-this-project/agents/portability.yaml +0 -5
- package/skills/run-this-project/assets/stall-policy.json +0 -15
- package/skills/run-this-project/references/making-work-ready.md +0 -60
- package/skills/run-this-project/references/reacting-to-events.md +0 -76
- package/skills/run-this-project/references/stalls-and-escalation.md +0 -63
- package/skills/run-this-project/scripts/lib/cli.mjs +0 -185
- package/skills/run-this-project/scripts/lib/credential.mjs +0 -29
- package/skills/run-this-project/scripts/make-ready.mjs +0 -64
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +0 -61
- package/skills/unstick/SKILL.md +0 -41
- package/skills/unstick/agents/openai.yaml +0 -6
- package/skills/unstick/agents/portability.yaml +0 -5
- package/skills/unstick/references/playbook.md +0 -51
- package/skills/unstick/scripts/lib/cli.mjs +0 -135
- package/skills/unstick/scripts/lib/credential.mjs +0 -29
- package/skills/unstick/scripts/unstick.mjs +0 -57
- package/skills/what-needs-me/SKILL.md +0 -41
- package/skills/what-needs-me/agents/openai.yaml +0 -6
- package/skills/what-needs-me/agents/portability.yaml +0 -5
- package/skills/what-needs-me/references/raising-a-decision.md +0 -41
- package/skills/what-needs-me/references/reading-the-inbox.md +0 -38
- package/skills/what-needs-me/references/settling-an-answer.md +0 -37
- package/skills/what-needs-me/scripts/inbox.mjs +0 -56
- package/skills/what-needs-me/scripts/lib/cli.mjs +0 -135
- package/skills/what-needs-me/scripts/lib/credential.mjs +0 -29
- package/skills/what-needs-me/scripts/raise.mjs +0 -53
- package/skills/what-needs-me/scripts/settle.mjs +0 -73
- package/skills/whats-happening/SKILL.md +0 -43
- package/skills/whats-happening/agents/openai.yaml +0 -6
- package/skills/whats-happening/agents/portability.yaml +0 -4
- package/skills/whats-happening/assets/status-reply.json +0 -77
- package/skills/whats-happening/references/reading-the-board.md +0 -43
- package/skills/whats-happening/references/reprioritising.md +0 -37
- package/skills/whats-happening/references/routing-work.md +0 -36
- package/skills/whats-happening/references/status-reply.md +0 -34
- package/skills/whats-happening/references/why-is-it-stuck.md +0 -62
- package/skills/whats-happening/scripts/explain.mjs +0 -28
- package/skills/whats-happening/scripts/lib/cli.mjs +0 -135
- package/skills/whats-happening/scripts/lib/credential.mjs +0 -29
- package/skills/whats-happening/scripts/snapshot.mjs +0 -145
package/dist/contract.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// contract.ts — GET /api/v1/agent/contract, cached with its ETag at ~/.config/catalyst-cloud/contract.json.
|
|
2
|
-
// Every tenant fact a verb needs (stage ids, label ids, route paths, the bookkeeping marker, thresholds)
|
|
3
|
-
// is read from here; no verb carries a literal fallback for any of them.
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname } from "node:path";
|
|
6
|
-
import { contractPathFor, readManifest } from "./config.js";
|
|
7
|
-
import { CliError, MeError } from "./errors.js";
|
|
8
|
-
import { apiClient } from "./transport.js";
|
|
9
|
-
export const CONTRACT_ROUTE = "/api/v1/agent/contract";
|
|
10
|
-
export function readContractCache(home) {
|
|
11
|
-
const path = contractPathFor(home);
|
|
12
|
-
if (!existsSync(path))
|
|
13
|
-
return null;
|
|
14
|
-
try {
|
|
15
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
16
|
-
if (typeof parsed.fetchedAt !== "string" || typeof parsed.contractVersion !== "string" || !parsed.doc)
|
|
17
|
-
return null;
|
|
18
|
-
return parsed;
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export function writeContractCache(home, cache) {
|
|
25
|
-
const path = contractPathFor(home);
|
|
26
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
27
|
-
const tmp = `${path}.tmp`;
|
|
28
|
-
writeFileSync(tmp, JSON.stringify(cache, null, 2) + "\n", { mode: 0o600 });
|
|
29
|
-
renameSync(tmp, path);
|
|
30
|
-
return path;
|
|
31
|
-
}
|
|
32
|
-
/** `1.x` → major 1; `1.2.3` → exact; `unpinned` → anything. Returns null when the range is unparseable. */
|
|
33
|
-
export function contractVersionInRange(version, range) {
|
|
34
|
-
if (range === "unpinned")
|
|
35
|
-
return true;
|
|
36
|
-
const major = /^(\d+)\.x$/.exec(range);
|
|
37
|
-
if (major)
|
|
38
|
-
return version.split(".")[0] === major[1];
|
|
39
|
-
if (/^\d+\.\d+\.\d+$/.test(range))
|
|
40
|
-
return version === range;
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
export function assertContractRange(version, range) {
|
|
44
|
-
const ok = contractVersionInRange(version, range);
|
|
45
|
-
if (ok === null)
|
|
46
|
-
throw new CliError(`tenantContractRange "${range}" in package.json is not a range this CLI understands`, "contract-range");
|
|
47
|
-
if (!ok) {
|
|
48
|
-
throw new CliError(`the tenant serves contract version ${version} but this bundle accepts ${range} — update the bundle (npm install -g @catalyst-cloud/catalyst-skills@latest && catalyst-skills login) or ask your tenant admin which version is live`, "contract-version");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Load the contract per the cloud's own cache policy: a cache younger than `doc.cache.maxAgeSeconds`
|
|
53
|
-
* is used as-is; otherwise a conditional GET revalidates it (304 refreshes `fetchedAt`, 200 replaces
|
|
54
|
-
* the document). A network failure on a cache older than `staleRefusalSeconds` refuses; a 403 (a
|
|
55
|
-
* personal key on an older cloud) refuses naming the cloud version; a major version outside the bundle's range refuses
|
|
56
|
-
* naming both versions.
|
|
57
|
-
*/
|
|
58
|
-
export async function loadContract(ctx, cfg, opts = {}) {
|
|
59
|
-
const path = contractPathFor(ctx.home);
|
|
60
|
-
const cached = readContractCache(ctx.home);
|
|
61
|
-
const nowMs = ctx.now().getTime();
|
|
62
|
-
const ageSeconds = cached ? Math.max(0, Math.floor((nowMs - Date.parse(cached.fetchedAt)) / 1000)) : Infinity;
|
|
63
|
-
const range = readManifest().tenantContractRange;
|
|
64
|
-
if (cached && !opts.refresh && ageSeconds < cached.doc.cache.maxAgeSeconds) {
|
|
65
|
-
assertContractRange(cached.contractVersion, range);
|
|
66
|
-
return { doc: cached.doc, source: "cache", ageSeconds, path };
|
|
67
|
-
}
|
|
68
|
-
if (opts.offline) {
|
|
69
|
-
if (cached) {
|
|
70
|
-
assertContractRange(cached.contractVersion, range);
|
|
71
|
-
return { doc: cached.doc, source: "cache", ageSeconds, path };
|
|
72
|
-
}
|
|
73
|
-
throw new CliError("no cached contract and network reads are disabled", "contract-missing");
|
|
74
|
-
}
|
|
75
|
-
const api = apiClient(cfg, ctx);
|
|
76
|
-
let res;
|
|
77
|
-
try {
|
|
78
|
-
res = await api.getJson(CONTRACT_ROUTE, { etag: cached?.etag ?? null, accept: [304] });
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
if (err instanceof CliError && err.status === 403) {
|
|
82
|
-
throw new CliError(`GET ${CONTRACT_ROUTE} refused (403): this cloud is older than the bundle and does not yet admit a personal key on the contract — update the cloud, or log in with the tenant's account key until it is`, "contract-forbidden", 2, 403);
|
|
83
|
-
}
|
|
84
|
-
if (err instanceof MeError && err.kind === "network" && cached) {
|
|
85
|
-
if (ageSeconds >= cached.doc.cache.staleRefusalSeconds) {
|
|
86
|
-
throw new CliError(`the cached contract is ${ageSeconds}s old (refusal after ${cached.doc.cache.staleRefusalSeconds}s) and the refresh failed: ${err.message}`, "contract-stale");
|
|
87
|
-
}
|
|
88
|
-
assertContractRange(cached.contractVersion, range);
|
|
89
|
-
return { doc: cached.doc, source: "cache", ageSeconds, path };
|
|
90
|
-
}
|
|
91
|
-
throw err;
|
|
92
|
-
}
|
|
93
|
-
if (res.status === 304) {
|
|
94
|
-
if (!cached)
|
|
95
|
-
throw new CliError(`GET ${CONTRACT_ROUTE} answered 304 with no cache to revalidate`, "contract-shape");
|
|
96
|
-
assertContractRange(cached.contractVersion, range);
|
|
97
|
-
writeContractCache(ctx.home, { ...cached, fetchedAt: ctx.now().toISOString() });
|
|
98
|
-
return { doc: cached.doc, source: "revalidated", ageSeconds: 0, path };
|
|
99
|
-
}
|
|
100
|
-
const doc = res.body;
|
|
101
|
-
if (!doc || typeof doc !== "object" || typeof doc.contractVersion !== "string" || !Array.isArray(doc.teams) || !Array.isArray(doc.routes)) {
|
|
102
|
-
throw new CliError(`GET ${CONTRACT_ROUTE} returned an unexpected shape`, "contract-shape");
|
|
103
|
-
}
|
|
104
|
-
const version = res.headers.get("x-catalyst-contract-version") ?? doc.contractVersion;
|
|
105
|
-
assertContractRange(version, range);
|
|
106
|
-
writeContractCache(ctx.home, {
|
|
107
|
-
etag: res.headers.get("etag"),
|
|
108
|
-
fetchedAt: ctx.now().toISOString(),
|
|
109
|
-
contractVersion: version,
|
|
110
|
-
doc,
|
|
111
|
-
});
|
|
112
|
-
return { doc, source: "network", ageSeconds: 0, path };
|
|
113
|
-
}
|
|
114
|
-
// ── helpers every verb uses ──────────────────────────────────────────────────────────────────────
|
|
115
|
-
export function teamByKey(doc, key) {
|
|
116
|
-
const wanted = key.toUpperCase();
|
|
117
|
-
const team = doc.teams.find((t) => (t.key ?? "").toUpperCase() === wanted);
|
|
118
|
-
if (!team) {
|
|
119
|
-
const known = doc.teams.map((t) => t.key ?? t.id).join(", ") || "none";
|
|
120
|
-
throw new CliError(`team "${key}" is not on this tenant's contract (teams: ${known})`, "team-unknown");
|
|
121
|
-
}
|
|
122
|
-
return team;
|
|
123
|
-
}
|
|
124
|
-
export function teamById(doc, id) {
|
|
125
|
-
return doc.teams.find((t) => t.id === id) ?? null;
|
|
126
|
-
}
|
|
127
|
-
/** The team a ticket identifier belongs to, by its `KEY-` prefix. */
|
|
128
|
-
export function teamForTicket(doc, ticket) {
|
|
129
|
-
const dash = ticket.indexOf("-");
|
|
130
|
-
if (dash <= 0)
|
|
131
|
-
throw new CliError(`"${ticket}" is not a ticket identifier (expected KEY-123)`, "ticket-shape");
|
|
132
|
-
return teamByKey(doc, ticket.slice(0, dash));
|
|
133
|
-
}
|
|
134
|
-
export function stageIdForSlot(team, slot) {
|
|
135
|
-
const stage = team.stages[slot];
|
|
136
|
-
if (!stage) {
|
|
137
|
-
const mapped = Object.keys(team.stages).join(", ") || "none";
|
|
138
|
-
throw new CliError(`slot "${slot}" is not mapped for team ${team.key ?? team.id} (mapped: ${mapped})`, "slot-unmapped");
|
|
139
|
-
}
|
|
140
|
-
if (!stage.stateStillExists) {
|
|
141
|
-
throw new CliError(`slot "${slot}" on team ${team.key ?? team.id} maps to state ${stage.stateId}, which no longer exists in Linear — fix the mapping in settings first`, "slot-stale");
|
|
142
|
-
}
|
|
143
|
-
return stage.stateId;
|
|
144
|
-
}
|
|
145
|
-
/** Resolve a label by name through the contract's ask/hold/release lists; an unknown name is returned
|
|
146
|
-
* as-is when it looks like an id (the contract only lists Catalyst's own labels). */
|
|
147
|
-
export function labelId(team, nameOrId) {
|
|
148
|
-
const all = [...team.labels.ask, ...team.labels.hold, ...team.labels.release];
|
|
149
|
-
const hit = all.find((l) => l.name === nameOrId);
|
|
150
|
-
if (hit) {
|
|
151
|
-
if (!hit.preferredId) {
|
|
152
|
-
throw new CliError(`label "${nameOrId}" is absent from this workspace (contract scope: ${hit.scope}) — create it in Linear first`, "label-absent");
|
|
153
|
-
}
|
|
154
|
-
return hit.preferredId;
|
|
155
|
-
}
|
|
156
|
-
return nameOrId;
|
|
157
|
-
}
|
|
158
|
-
/** The route whose last path segment is `name` (e.g. `issue-comment`). The path itself is never assumed. */
|
|
159
|
-
export function routePath(doc, name) {
|
|
160
|
-
const route = doc.routes.find((r) => r.path.split("/").filter(Boolean).at(-1) === name);
|
|
161
|
-
if (!route) {
|
|
162
|
-
throw new CliError(`the tenant contract serves no "${name}" route (routes: ${doc.routes.map((r) => r.path).join(", ")})`, "route-unknown");
|
|
163
|
-
}
|
|
164
|
-
return route.path;
|
|
165
|
-
}
|
|
166
|
-
export function bookkeepingPrefix(doc) {
|
|
167
|
-
return doc.vocabulary.bookkeeping.marker;
|
|
168
|
-
}
|
|
169
|
-
/** Walk a dotted path (`teams.0.stages`) into the document. */
|
|
170
|
-
export function pickPath(doc, path) {
|
|
171
|
-
let cur = doc;
|
|
172
|
-
for (const seg of path.split(".").filter(Boolean)) {
|
|
173
|
-
if (cur === null || typeof cur !== "object")
|
|
174
|
-
return undefined;
|
|
175
|
-
cur = cur[seg];
|
|
176
|
-
}
|
|
177
|
-
return cur;
|
|
178
|
-
}
|
package/dist/detach.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// detach.ts — re-spawn this CLI detached (replica start --detach). Isolated so a test can stub it.
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
/** Spawn `process.execPath <argv>` detached from this process, stdio ignored, and let it go. */
|
|
4
|
-
export function detachSelf(argv, env = process.env) {
|
|
5
|
-
const child = spawn(process.execPath, argv, { detached: true, stdio: "ignore", env });
|
|
6
|
-
child.unref();
|
|
7
|
-
if (typeof child.pid !== "number")
|
|
8
|
-
throw new Error("detached spawn returned no pid");
|
|
9
|
-
return { pid: child.pid };
|
|
10
|
-
}
|
package/dist/errors.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// errors.ts — the three error classes the exit contract is built on: usage → 1, MeError/CliError → 2.
|
|
2
|
-
export class UsageError extends Error {
|
|
3
|
-
}
|
|
4
|
-
export class CliError extends Error {
|
|
5
|
-
code;
|
|
6
|
-
exitCode;
|
|
7
|
-
status;
|
|
8
|
-
constructor(message, code,
|
|
9
|
-
/** Exit code the dispatcher returns for this error. Defaults to 2 (the CliError contract). */
|
|
10
|
-
exitCode = 2,
|
|
11
|
-
/** The HTTP status that produced it, when one did. */
|
|
12
|
-
status) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.code = code;
|
|
15
|
-
this.exitCode = exitCode;
|
|
16
|
-
this.status = status;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export class MeError extends Error {
|
|
20
|
-
kind;
|
|
21
|
-
status;
|
|
22
|
-
constructor(message, kind, status) {
|
|
23
|
-
super(message);
|
|
24
|
-
this.kind = kind;
|
|
25
|
-
this.status = status;
|
|
26
|
-
}
|
|
27
|
-
}
|
package/dist/events.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { flagInt, flagString, positionals } from "./args.js";
|
|
2
|
-
import { apiBase, requireConfig } from "./config.js";
|
|
3
|
-
import { CliError, UsageError } from "./errors.js";
|
|
4
|
-
import { authStrategyFor } from "./oauth.js";
|
|
5
|
-
const eventsModule = "@catalyst-cloud/sdk/events";
|
|
6
|
-
export const loadEventsSdk = () => import(eventsModule);
|
|
7
|
-
export async function createEventSync(ctx, deps = {}) {
|
|
8
|
-
const cfg = requireConfig(ctx);
|
|
9
|
-
const sdk = await (deps.loadSdk ?? loadEventsSdk)();
|
|
10
|
-
const auth = authStrategyFor(ctx, cfg);
|
|
11
|
-
if (auth.kind === "cookie")
|
|
12
|
-
throw new CliError("event sync requires a token or OAuth session", "events-auth");
|
|
13
|
-
return new sdk.CatalystEventSync({
|
|
14
|
-
baseUrl: apiBase(cfg),
|
|
15
|
-
auth,
|
|
16
|
-
tenantId: cfg.account,
|
|
17
|
-
fetch: ctx.fetch,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
export async function cmdEvents(args, ctx, deps = {}) {
|
|
21
|
-
const [sub] = positionals(args);
|
|
22
|
-
if (!sub)
|
|
23
|
-
throw new UsageError("events needs a subcommand: tail | wait-for | query");
|
|
24
|
-
if (!["tail", "wait-for", "query"].includes(sub))
|
|
25
|
-
throw new UsageError(`unknown events subcommand: ${sub}`);
|
|
26
|
-
const cfg = requireConfig(ctx);
|
|
27
|
-
const sdk = await (deps.loadSdk ?? loadEventsSdk)();
|
|
28
|
-
const directory = flagString(args, "directory") ??
|
|
29
|
-
sdk.defaultEventCacheDirectory(cfg.account);
|
|
30
|
-
const after = startingCursor(args, sub === "query");
|
|
31
|
-
const matches = matcher(args);
|
|
32
|
-
if (sub === "query") {
|
|
33
|
-
const events = (await sdk.readCachedEvents({ tenantId: cfg.account, directory, after }))
|
|
34
|
-
.filter(matches)
|
|
35
|
-
.slice(-flagInt(args, "limit", 50));
|
|
36
|
-
for (const event of events)
|
|
37
|
-
ctx.stdout(JSON.stringify(event));
|
|
38
|
-
return 0;
|
|
39
|
-
}
|
|
40
|
-
const controller = new AbortController();
|
|
41
|
-
const abort = () => controller.abort(deps.signal?.reason);
|
|
42
|
-
if (deps.signal?.aborted)
|
|
43
|
-
abort();
|
|
44
|
-
else
|
|
45
|
-
deps.signal?.addEventListener("abort", abort, { once: true });
|
|
46
|
-
const timer = sub === "wait-for"
|
|
47
|
-
? setTimeout(() => controller.abort(new Error("event wait timed out")), flagInt(args, "timeout", 300) * 1_000)
|
|
48
|
-
: undefined;
|
|
49
|
-
try {
|
|
50
|
-
for await (const event of sdk.tailCachedEvents({
|
|
51
|
-
tenantId: cfg.account,
|
|
52
|
-
directory,
|
|
53
|
-
after,
|
|
54
|
-
signal: controller.signal,
|
|
55
|
-
})) {
|
|
56
|
-
if (!matches(event))
|
|
57
|
-
continue;
|
|
58
|
-
ctx.stdout(JSON.stringify(event));
|
|
59
|
-
if (sub === "wait-for")
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
return sub === "wait-for" ? 1 : 0;
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
if (controller.signal.aborted)
|
|
66
|
-
return sub === "wait-for" ? 1 : 0;
|
|
67
|
-
if (error.code === "ENOENT")
|
|
68
|
-
throw new CliError(`event cache is absent at ${directory} — start it with: catalyst-skills replica start --detach`, "events-absent", 3);
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
finally {
|
|
72
|
-
if (timer)
|
|
73
|
-
clearTimeout(timer);
|
|
74
|
-
deps.signal?.removeEventListener("abort", abort);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function startingCursor(args, history) {
|
|
78
|
-
const requested = flagString(args, "after");
|
|
79
|
-
if (requested !== undefined) {
|
|
80
|
-
const parsed = Number(requested);
|
|
81
|
-
if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
82
|
-
throw new UsageError("--after must be a non-negative event sequence");
|
|
83
|
-
return parsed;
|
|
84
|
-
}
|
|
85
|
-
if (history)
|
|
86
|
-
return undefined;
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
function matcher(args) {
|
|
90
|
-
const type = flagString(args, "type");
|
|
91
|
-
const ticket = flagString(args, "ticket")?.toUpperCase();
|
|
92
|
-
return (event) => (!type || event.type === type) &&
|
|
93
|
-
(!ticket || payloadReferences(event.payload, ticket));
|
|
94
|
-
}
|
|
95
|
-
function payloadReferences(value, ticket) {
|
|
96
|
-
if (typeof value === "string")
|
|
97
|
-
return value.toUpperCase() === ticket;
|
|
98
|
-
if (Array.isArray(value))
|
|
99
|
-
return value.some((item) => payloadReferences(item, ticket));
|
|
100
|
-
if (!value || typeof value !== "object")
|
|
101
|
-
return false;
|
|
102
|
-
return Object.entries(value).some(([key, item]) => ["ticket", "identifier", "workItemKey", "issueIdentifier"].includes(key) &&
|
|
103
|
-
typeof item === "string"
|
|
104
|
-
? item.toUpperCase() === ticket
|
|
105
|
-
: payloadReferences(item, ticket));
|
|
106
|
-
}
|