@catalyst-cloud/catalyst-skills 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +19 -12
- package/dist/args.js +34 -4
- package/dist/ask.js +6 -3
- package/dist/cli.js +19 -5
- package/dist/config.js +4 -0
- package/dist/contract.js +6 -0
- package/dist/environment.js +207 -0
- package/dist/events.js +106 -0
- package/dist/execution.js +146 -18
- package/dist/pagination.js +76 -0
- package/dist/published.js +75 -0
- package/dist/query.js +41 -18
- package/dist/ready.js +110 -19
- package/dist/release.js +142 -0
- package/dist/replica.js +296 -30
- package/dist/semver.js +18 -0
- package/dist/skill-shape.js +10 -0
- package/dist/skills.js +52 -1
- package/dist/transport.js +5 -2
- package/dist/ts-deps-loader.js +46 -1
- package/package.json +7 -4
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-github/scripts/lib/cli.mjs +3 -2
- package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-linear/references/reading-a-ticket.md +2 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +4 -3
- package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-onboard/SKILL.md +49 -0
- package/skills/catalyst-onboard/agents/openai.yaml +6 -0
- package/skills/catalyst-onboard/agents/portability.yaml +5 -0
- package/skills/catalyst-onboard/references/the-one-path.md +143 -0
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +50 -0
- package/skills/catalyst-onboard/references/who-fixes-what.md +41 -0
- package/skills/catalyst-onboard/scripts/lib/cli.mjs +117 -0
- package/skills/catalyst-onboard/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +245 -0
- package/skills/catalyst-setup/SKILL.md +3 -3
- package/skills/catalyst-setup/references/what-each-check-means.md +22 -6
- package/skills/catalyst-setup/scripts/lib/cli.mjs +3 -2
- package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +1 -1
- package/skills/connect-me/SKILL.md +1 -1
- package/skills/connect-me/scripts/lib/cli.mjs +3 -2
- package/skills/connect-me/scripts/lib/credential.mjs +29 -0
- package/skills/how-catalyst-works/SKILL.md +1 -1
- package/skills/how-catalyst-works/references/stages-and-mapping.md +2 -2
- package/skills/how-catalyst-works/references/what-runs-next.md +1 -1
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +4 -4
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +4 -3
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
- package/skills/run-this-project/SKILL.md +2 -2
- package/skills/run-this-project/references/making-work-ready.md +1 -1
- package/skills/run-this-project/references/stalls-and-escalation.md +2 -2
- package/skills/run-this-project/scripts/lib/cli.mjs +3 -2
- package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
- package/skills/unstick/SKILL.md +41 -0
- package/skills/unstick/agents/openai.yaml +6 -0
- package/skills/unstick/agents/portability.yaml +5 -0
- package/skills/unstick/references/playbook.md +51 -0
- package/skills/unstick/scripts/lib/cli.mjs +135 -0
- package/skills/unstick/scripts/lib/credential.mjs +29 -0
- package/skills/unstick/scripts/unstick.mjs +57 -0
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-needs-me/references/reading-the-inbox.md +1 -1
- package/skills/what-needs-me/scripts/lib/cli.mjs +3 -2
- package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/SKILL.md +2 -2
- package/skills/whats-happening/references/why-is-it-stuck.md +9 -4
- package/skills/whats-happening/scripts/lib/cli.mjs +3 -2
- package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/scripts/snapshot.mjs +4 -0
package/dist/ready.js
CHANGED
|
@@ -6,23 +6,42 @@ import { join } from "node:path";
|
|
|
6
6
|
import { defaultSkillsDirFor, loadConfig, readManifest } from "./config.js";
|
|
7
7
|
import { contractVersionInRange, readContractCache } from "./contract.js";
|
|
8
8
|
import { CliError } from "./errors.js";
|
|
9
|
-
import {
|
|
9
|
+
import { latestPublishedVersion } from "./published.js";
|
|
10
|
+
import { replicaStatus, writerIsRunning } from "./replica.js";
|
|
10
11
|
import { loadSdk } from "./sdk.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
import { semverOlder } from "./semver.js";
|
|
13
|
+
import { FIRST_STAMPED_VERSION } from "./skill-shape.js";
|
|
14
|
+
import { installedBundleVersion } from "./skills.js";
|
|
15
|
+
export { semverOlder };
|
|
16
|
+
/** `ready`'s own wording for the replica. It never recommends adopting the replica — until the
|
|
17
|
+
* mirror's snapshot path is safe for a large tenant, a customer following that advice is the
|
|
18
|
+
* failure mode. It DOES name the restart command when the writer gave up, because that is an
|
|
19
|
+
* instruction about a thing already running, not a nudge to adopt one (CTC-2499). */
|
|
20
|
+
function readyReplicaLine(s) {
|
|
21
|
+
const w = s.writer;
|
|
22
|
+
if (w?.stopped) {
|
|
23
|
+
return (`replica: the writer stopped ${new Date(w.stopped.at).toISOString()}: ${w.stopped.reason} ` +
|
|
24
|
+
`(last error: ${w.lastError}) — the replica is optional and every read ` +
|
|
25
|
+
`still works through the API; restart it with: ${w.stopped.restartWith}`);
|
|
26
|
+
}
|
|
27
|
+
if (w && w.consecutiveFailures > 0) {
|
|
28
|
+
// Present tense only for a writer that is actually running: the record survives a SIGKILL, and
|
|
29
|
+
// "is backing off" about a dead process promises a retry that will never come (CTC-2499).
|
|
30
|
+
if (!writerIsRunning(s)) {
|
|
31
|
+
return `replica: the writer recorded ${w.consecutiveFailures} failed snapshot pulls and is no longer running (last error: ${w.lastError}) — the replica is optional and every read still works through the API`;
|
|
32
|
+
}
|
|
33
|
+
return `replica: the writer has failed ${w.consecutiveFailures} snapshot pulls in a row and is backing off (last error: ${w.lastError}) — reads fall back to the API`;
|
|
34
|
+
}
|
|
35
|
+
switch (s.verdict) {
|
|
36
|
+
case "not-configured":
|
|
37
|
+
return "replica: not configured — run login first";
|
|
38
|
+
case "absent":
|
|
39
|
+
return `replica: absent at ${s.dbPath} — optional, and off by default for large tenants while the snapshot path is being made safe; every read works through the API`;
|
|
40
|
+
case "fresh":
|
|
41
|
+
return `replica: fresh at ${s.dbPath} (cursor ${s.cursor}, heartbeat ${s.heartbeatAgeMs}ms ago${s.lag !== undefined ? `, ${s.lag} behind head ${s.head}` : ""})`;
|
|
42
|
+
case "stale":
|
|
43
|
+
return `replica: stale at ${s.dbPath} (${s.reasons.join("; ")}${s.cursor !== null ? `; cursor ${s.cursor}` : ""}) — reads fall back to the API`;
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
function whoCanAnswer(doc) {
|
|
28
47
|
const roles = doc.humans.map((h) => `${h.role} ${h.linearUserId}`);
|
|
@@ -79,6 +98,61 @@ export async function readyReport(ctx, deps) {
|
|
|
79
98
|
});
|
|
80
99
|
}
|
|
81
100
|
}
|
|
101
|
+
// CTC-2160 — the two artefacts a customer installs drift apart: the CLI comes from npm, the skill
|
|
102
|
+
// files come from `npx skills add` (GitHub) or the plugin, and only the CLI's own version was ever
|
|
103
|
+
// knowable here. Both are NOTES: being behind a publish is a degrading fact about an install that
|
|
104
|
+
// still works, so `ready` stays READY. This is the one place `ready` touches the network, and it is
|
|
105
|
+
// capped, cached and skippable — see src/published.ts.
|
|
106
|
+
const skillsDir = cfg?.skillsDir ?? defaultSkillsDirFor(ctx.home);
|
|
107
|
+
const offline = deps.offline === true || ctx.env.CATALYST_SKILLS_OFFLINE === "1";
|
|
108
|
+
if (!offline) {
|
|
109
|
+
const pub = await (deps.fetchLatestRelease ?? (() => latestPublishedVersion(ctx)))();
|
|
110
|
+
if (pub.latest === null) {
|
|
111
|
+
checks.push({
|
|
112
|
+
id: "cliRelease",
|
|
113
|
+
ok: false,
|
|
114
|
+
note: true,
|
|
115
|
+
line: `cliRelease: could not check for a newer release (${pub.reason}) — nothing about this install is known to be wrong; re-run when the network is back`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// clause 2 — the CLI. Suppressed when the tenant-minimum note already named this binary (D9).
|
|
120
|
+
const installedCli = readManifest().version;
|
|
121
|
+
const bundleNoteFired = checks.some((c) => c.id === "bundle");
|
|
122
|
+
if (!bundleNoteFired && semverOlder(installedCli, pub.latest)) {
|
|
123
|
+
checks.push({
|
|
124
|
+
id: "cliRelease",
|
|
125
|
+
ok: false,
|
|
126
|
+
note: true,
|
|
127
|
+
line: `cliRelease: ${installedCli} installed is older than the latest published ${pub.latest} — upgrade: npm install -g @catalyst-cloud/catalyst-skills@latest && catalyst-skills login`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// clause 1 — the skill files on disk, read independently of whichever CLI is answering. Both
|
|
131
|
+
// facts can hold at once: a machine that pulled at different hours carries some skills stamped
|
|
132
|
+
// behind AND some old enough to carry no stamp at all — the field report's own mixed install.
|
|
133
|
+
// So they are ADDITIVE, not exclusive: an unstamped file's real version is unknown and may be
|
|
134
|
+
// older than any stamp, which is why the stamped half says "oldest STAMPED" and the unstamped
|
|
135
|
+
// names are always printed rather than hidden behind whichever branch happened to win.
|
|
136
|
+
const found = installedBundleVersion(skillsDir, deps.skillNames);
|
|
137
|
+
const stampBehind = found.version !== null && semverOlder(found.version, pub.latest);
|
|
138
|
+
const unstampedBehind = found.unstamped.length > 0 && !semverOlder(pub.latest, FIRST_STAMPED_VERSION);
|
|
139
|
+
if (stampBehind || unstampedBehind) {
|
|
140
|
+
const facts = [];
|
|
141
|
+
if (stampBehind)
|
|
142
|
+
facts.push(`the oldest stamped skill is ${found.version} (${found.skill})`);
|
|
143
|
+
if (unstampedBehind) {
|
|
144
|
+
const one = found.unstamped.length === 1;
|
|
145
|
+
facts.push(`${found.unstamped.join(", ")} ${one ? "carries" : "carry"} no version and so ${one ? "predates" : "predate"} ${FIRST_STAMPED_VERSION}`);
|
|
146
|
+
}
|
|
147
|
+
checks.push({
|
|
148
|
+
id: "skillsRelease",
|
|
149
|
+
ok: false,
|
|
150
|
+
note: true,
|
|
151
|
+
line: `skillsRelease: in ${skillsDir} ${facts.join(", and ")}, while the published bundle is ${pub.latest} — update: npx skills update -y`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
82
156
|
if (cfg) {
|
|
83
157
|
checks.push(cfg.cliPath && existsSync(cfg.cliPath)
|
|
84
158
|
? { id: "cliPath", ok: true, line: `cliPath: ${cfg.cliPath}` }
|
|
@@ -87,7 +161,6 @@ export async function readyReport(ctx, deps) {
|
|
|
87
161
|
// The skills are installed by the customer's own agent (a plugin, or `npx skills add`), so an
|
|
88
162
|
// empty copy directory is the normal case and must not read as NOT READY. A PARTIAL copy is the
|
|
89
163
|
// one broken state this check can see: half a set this package put there and never finished.
|
|
90
|
-
const skillsDir = cfg?.skillsDir ?? defaultSkillsDirFor(ctx.home);
|
|
91
164
|
const present = deps.skillNames.filter((n) => existsSync(join(skillsDir, n, "SKILL.md")));
|
|
92
165
|
const missing = deps.skillNames.filter((n) => !present.includes(n));
|
|
93
166
|
if (present.length === 0) {
|
|
@@ -107,12 +180,30 @@ export async function readyReport(ctx, deps) {
|
|
|
107
180
|
checks.push({ id: "sdk", ok: false, line: `sdk: ${err instanceof Error ? err.message : String(err)}`, fix: "run under Node 22.15 or newer (or bun); every read still works through the API meanwhile", who: "you" });
|
|
108
181
|
}
|
|
109
182
|
const replica = replicaStatus(ctx, cfg);
|
|
110
|
-
checks.push({ id: "replica", ok: replica.verdict === "fresh", note: true, line:
|
|
183
|
+
checks.push({ id: "replica", ok: replica.verdict === "fresh", note: true, line: readyReplicaLine(replica) });
|
|
111
184
|
if (cache) {
|
|
112
185
|
const doc = cache.doc;
|
|
113
186
|
const who = whoCanAnswer(doc);
|
|
114
187
|
for (const team of doc.teams) {
|
|
115
188
|
const label = team.key ?? team.id;
|
|
189
|
+
// The team's dispatch gate, straight off the cached contract: an older cloud omits it and this
|
|
190
|
+
// emits nothing (the `skillsBundle` precedent above). A shut gate means NOTHING in the team can
|
|
191
|
+
// start, so it is a FAIL carrying the cloud's own remedy as the fix — never an informational
|
|
192
|
+
// note. It is emitted before the `unchecked` branch below so a team whose readiness was never
|
|
193
|
+
// checked still reports its gate, which is exactly the team most likely to be unmapped.
|
|
194
|
+
const dg = team.dispatchGate;
|
|
195
|
+
if (dg && typeof dg.status === "string") {
|
|
196
|
+
const slots = (dg.missingSlots ?? []).join(", ");
|
|
197
|
+
checks.push(dg.status === "open"
|
|
198
|
+
? { id: `team:${label}:dispatchGate`, ok: true, line: `team ${label}: dispatch gate open` }
|
|
199
|
+
: {
|
|
200
|
+
id: `team:${label}:dispatchGate`,
|
|
201
|
+
ok: false,
|
|
202
|
+
line: `team ${label}: dispatch gate ${dg.status}${slots ? ` (${slots})` : ""}, blocking`,
|
|
203
|
+
fix: dg.remedy ?? `open settings for team ${label} and map its stages`,
|
|
204
|
+
who,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
116
207
|
if (team.readiness.status === "unchecked") {
|
|
117
208
|
checks.push({ id: `team:${label}`, ok: true, note: true, line: `team ${label}: readiness not checked yet` });
|
|
118
209
|
continue;
|
|
@@ -140,7 +231,7 @@ export async function readyReport(ctx, deps) {
|
|
|
140
231
|
}
|
|
141
232
|
}
|
|
142
233
|
const ready = checks.every((c) => c.ok || c.note);
|
|
143
|
-
return { ready, checks };
|
|
234
|
+
return { ready, checks, replica };
|
|
144
235
|
}
|
|
145
236
|
export async function cmdReady(args, ctx, deps) {
|
|
146
237
|
const report = await readyReport(ctx, deps);
|
package/dist/release.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// release.ts — `catalyst-skills release`: a person's release of a parked or held ticket, or of every
|
|
2
|
+
// ticket on one team parked under one failure class, through the contract's `ticket-release` and
|
|
3
|
+
// `ticket-release-class` routes. The cloud reads every governor holding the ticket and either releases
|
|
4
|
+
// all of them or releases nothing and names, per governor, the human action that does fix it; this verb
|
|
5
|
+
// prints that answer and exits 1 on a refusal so a script can branch on it.
|
|
6
|
+
//
|
|
7
|
+
// ⛔ NOTHING IS RELEASED WITHOUT A REASON. `--because` (what changed) is required on a real release and
|
|
8
|
+
// is recorded in the tenant's release audit; only `--dry-run` may omit it. `--retry-unchanged` is the
|
|
9
|
+
// explicit statement that something the mirror cannot see changed — the cloud refuses an unchanged
|
|
10
|
+
// cause without it.
|
|
11
|
+
import { flagBool, flagString, positionals } from "./args.js";
|
|
12
|
+
import { requireConfig } from "./config.js";
|
|
13
|
+
import { loadContract } from "./contract.js";
|
|
14
|
+
import { CliError, UsageError } from "./errors.js";
|
|
15
|
+
import { needsNewerCloud } from "./execution.js";
|
|
16
|
+
import { apiClient } from "./transport.js";
|
|
17
|
+
/** One release written as a person reads it: "unparked implement", "cleared the validate hold". */
|
|
18
|
+
export function describeAction(a) {
|
|
19
|
+
switch (a.op) {
|
|
20
|
+
case "unpark":
|
|
21
|
+
return `unparked ${a.phase ?? "a phase"}`;
|
|
22
|
+
case "clear_validate_hold":
|
|
23
|
+
return "cleared the validate hold";
|
|
24
|
+
case "clear_no_change_hold":
|
|
25
|
+
return "cleared the no-change hold";
|
|
26
|
+
case "grant_round_threshold_cycle":
|
|
27
|
+
return "bought one more repair cycle";
|
|
28
|
+
default:
|
|
29
|
+
return `${a.op}${a.phase ? ` at ${a.phase}` : ""}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const refusalLine = (r) => ` ${r.code}: ${r.humanAction}`;
|
|
33
|
+
export function renderTicketRelease(ticket, r) {
|
|
34
|
+
if (r.error === "team_changed") {
|
|
35
|
+
return [`${ticket} moved from team ${r.expectedTeamKey ?? "(none)"} to ${r.teamKey ?? "(none)"} while the release was being checked — run it again`];
|
|
36
|
+
}
|
|
37
|
+
const lines = [];
|
|
38
|
+
const dry = r.dryRun === true;
|
|
39
|
+
if (r.outcome === "nothing-held") {
|
|
40
|
+
lines.push(`${ticket}: nothing holds this ticket — run \`catalyst-skills explain ${ticket}\` for why it is not running`);
|
|
41
|
+
}
|
|
42
|
+
else if (r.outcome === "refused") {
|
|
43
|
+
lines.push(dry ? `${ticket} (dry run): would be refused — nothing would be released:` : `${ticket}: refused — nothing was released:`);
|
|
44
|
+
for (const x of r.refused ?? [])
|
|
45
|
+
lines.push(refusalLine(x));
|
|
46
|
+
const blocked = r.releasable ?? [];
|
|
47
|
+
if (blocked.length > 0)
|
|
48
|
+
lines.push(` (it would otherwise have ${blocked.map(describeAction).join(", ")})`);
|
|
49
|
+
}
|
|
50
|
+
else if (dry) {
|
|
51
|
+
lines.push(`${ticket} (dry run): would release — ${(r.releasable ?? []).map(describeAction).join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
lines.push(`${ticket}: released — ${(r.released ?? []).map(describeAction).join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
for (const w of r.warnings ?? [])
|
|
57
|
+
lines.push(`warning: ${w}`);
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
export function renderClassRelease(r) {
|
|
61
|
+
const released = r.released ?? [];
|
|
62
|
+
const refused = r.refused ?? [];
|
|
63
|
+
const nothing = r.nothingHeld ?? [];
|
|
64
|
+
const head = `${r.team ?? "?"} ${r.class ?? "?"}${r.dryRun ? " (dry run)" : ""}: released ${released.length}, refused ${refused.length}, nothing held ${nothing.length}${r.truncated ? " (more remain — run it again)" : ""}`;
|
|
65
|
+
const lines = [head];
|
|
66
|
+
for (const t of released) {
|
|
67
|
+
const actions = r.dryRun ? (t.releasable ?? []) : (t.released ?? []);
|
|
68
|
+
lines.push(` ${r.dryRun ? "would release" : "released"} ${t.ticket ?? "?"}: ${actions.map(describeAction).join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
for (const t of refused) {
|
|
71
|
+
for (const x of t.refused ?? [])
|
|
72
|
+
lines.push(` refused ${t.ticket ?? "?"} — ${x.code}: ${x.humanAction}`);
|
|
73
|
+
}
|
|
74
|
+
if (nothing.length > 0)
|
|
75
|
+
lines.push(` nothing held: ${nothing.join(", ")}`);
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
/** The contract's route by name, or the "your cloud is older" error when this tenant does not serve it. */
|
|
79
|
+
function releaseRoute(doc, name, cfg) {
|
|
80
|
+
const route = doc.routes.find((r) => r.method === "POST" && r.path.split("/").filter(Boolean).at(-1) === name);
|
|
81
|
+
if (!route)
|
|
82
|
+
throw needsNewerCloud(`release (the tenant contract serves no "${name}" route)`, cfg);
|
|
83
|
+
return route.path;
|
|
84
|
+
}
|
|
85
|
+
export async function cmdRelease(args, ctx) {
|
|
86
|
+
const [ticket, ...extra] = positionals(args);
|
|
87
|
+
const failureClass = flagString(args, "class");
|
|
88
|
+
const team = flagString(args, "team");
|
|
89
|
+
const because = flagString(args, "because");
|
|
90
|
+
const dryRun = flagBool(args, "dry-run");
|
|
91
|
+
const retryUnchanged = flagBool(args, "retry-unchanged");
|
|
92
|
+
if (extra.length > 0)
|
|
93
|
+
throw new UsageError("release takes one ticket");
|
|
94
|
+
if (failureClass !== undefined && ticket !== undefined) {
|
|
95
|
+
throw new UsageError("release takes a ticket or --class with --team, not both");
|
|
96
|
+
}
|
|
97
|
+
if (failureClass === undefined && ticket === undefined) {
|
|
98
|
+
throw new UsageError("release needs a ticket, or --class <failure-class> --team <K>");
|
|
99
|
+
}
|
|
100
|
+
if (failureClass !== undefined && team === undefined)
|
|
101
|
+
throw new UsageError("release --class needs --team <K>");
|
|
102
|
+
if (because === undefined && !dryRun) {
|
|
103
|
+
throw new UsageError("release needs --because <what changed> (recorded in the release audit), or --dry-run to preview");
|
|
104
|
+
}
|
|
105
|
+
const limitRaw = flagString(args, "limit");
|
|
106
|
+
const limit = limitRaw === undefined ? undefined : Number(limitRaw);
|
|
107
|
+
if (limit !== undefined && !Number.isInteger(limit))
|
|
108
|
+
throw new UsageError("--limit must be an integer");
|
|
109
|
+
const cfg = requireConfig(ctx);
|
|
110
|
+
const { doc } = await loadContract(ctx, cfg);
|
|
111
|
+
const api = apiClient(cfg, ctx);
|
|
112
|
+
const common = { ...(because !== undefined ? { because } : {}), retryUnchanged, dryRun };
|
|
113
|
+
if (failureClass !== undefined) {
|
|
114
|
+
const path = releaseRoute(doc, "ticket-release-class", cfg);
|
|
115
|
+
const res = await api.postJson(path, {
|
|
116
|
+
team,
|
|
117
|
+
class: failureClass,
|
|
118
|
+
...common,
|
|
119
|
+
...(limit !== undefined ? { limit } : {}),
|
|
120
|
+
});
|
|
121
|
+
if (args.json)
|
|
122
|
+
ctx.stdout(JSON.stringify(res.body));
|
|
123
|
+
else
|
|
124
|
+
for (const line of renderClassRelease(res.body))
|
|
125
|
+
ctx.stdout(line);
|
|
126
|
+
// A real class release that refused any ticket exits 1, the same contract as a single refusal.
|
|
127
|
+
return res.body.dryRun !== true && (res.body.refused ?? []).length > 0 ? 1 : 0;
|
|
128
|
+
}
|
|
129
|
+
const id = ticket;
|
|
130
|
+
const path = releaseRoute(doc, "ticket-release", cfg);
|
|
131
|
+
const res = await api.postJson(path, { ticket: id, ...common }, { accept: [404, 409] });
|
|
132
|
+
if (res.status === 404) {
|
|
133
|
+
throw new CliError(`ticket ${id} is not in this tenant's mirror — check the identifier`, "ticket-unknown");
|
|
134
|
+
}
|
|
135
|
+
if (args.json)
|
|
136
|
+
ctx.stdout(JSON.stringify(res.body));
|
|
137
|
+
else
|
|
138
|
+
for (const line of renderTicketRelease(id, res.body))
|
|
139
|
+
ctx.stdout(line);
|
|
140
|
+
const refused = res.status === 409 || (res.body.outcome === "refused" && res.body.dryRun !== true);
|
|
141
|
+
return refused ? 1 : 0;
|
|
142
|
+
}
|