@catalyst-cloud/catalyst-skills 0.3.1 → 0.5.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 +16 -0
- package/README.md +28 -15
- package/dist/args.js +19 -1
- package/dist/ask.js +1 -1
- package/dist/browser.js +20 -0
- package/dist/cli.js +56 -22
- package/dist/config.js +48 -6
- package/dist/contract.js +1 -1
- package/dist/events.js +106 -0
- package/dist/execution.js +55 -9
- package/dist/oauth.js +300 -0
- package/dist/query.js +1 -1
- package/dist/ready.js +1 -1
- package/dist/release.js +142 -0
- package/dist/replica.js +10 -3
- package/dist/{http.js → transport.js} +21 -10
- package/dist/watch/consumer.js +2 -2
- package/dist/watch.js +4 -2
- package/dist/write.js +1 -1
- package/package.json +5 -4
- package/skills/catalyst-github/scripts/lib/cli.mjs +3 -2
- package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +4 -3
- package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/SKILL.md +1 -1
- package/skills/catalyst-setup/references/what-each-check-means.md +5 -1
- 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 +16 -8
- 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/references/stages-and-mapping.md +1 -1
- 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 +1 -1
- 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/scripts/lib/cli.mjs +3 -2
- package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/SKILL.md +1 -1
- 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
|
@@ -10,6 +10,7 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
13
14
|
|
|
14
15
|
export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
|
|
15
16
|
export const NOT_CONFIGURED_EXIT = 2;
|
|
@@ -33,7 +34,7 @@ export function loadCustomerConfig() {
|
|
|
33
34
|
if (!existsSync(path)) return null;
|
|
34
35
|
try {
|
|
35
36
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
-
if (!parsed || typeof parsed
|
|
37
|
+
if (!hasCredential(parsed) || typeof parsed.account !== "string") return null;
|
|
37
38
|
return parsed;
|
|
38
39
|
} catch {
|
|
39
40
|
return null;
|
|
@@ -44,7 +45,7 @@ export function loadCustomerConfig() {
|
|
|
44
45
|
export function requireConfigured() {
|
|
45
46
|
const cfg = loadCustomerConfig();
|
|
46
47
|
if (cfg) return cfg;
|
|
47
|
-
console.error(`not connected: ${configPath()} is missing or unreadable — run:
|
|
48
|
+
console.error(`not connected: ${configPath()} is missing or unreadable — run: ${CONNECT_COMMAND}`);
|
|
48
49
|
process.exit(NOT_CONFIGURED_EXIT);
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
|
|
3
|
+
// byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
|
|
4
|
+
// (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
|
|
5
|
+
// each carries its own copy. This file is a library — run a sibling script with --help for usage.
|
|
6
|
+
//
|
|
7
|
+
// customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
|
|
8
|
+
// session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
|
|
9
|
+
// catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
|
|
10
|
+
// itself. A new credential kind lands here, once.
|
|
11
|
+
|
|
12
|
+
/** The command that connects this machine, as every not-connected line names it. */
|
|
13
|
+
export const CONNECT_COMMAND =
|
|
14
|
+
"npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
|
|
15
|
+
|
|
16
|
+
/** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
|
|
17
|
+
export function hasCredential(cfg) {
|
|
18
|
+
if (cfg === null || typeof cfg !== "object") return false;
|
|
19
|
+
const key = cfg["key"];
|
|
20
|
+
if (typeof key === "string" && key !== "") return true;
|
|
21
|
+
const login = cfg["auth"];
|
|
22
|
+
return (
|
|
23
|
+
login !== null &&
|
|
24
|
+
typeof login === "object" &&
|
|
25
|
+
login["kind"] === "oauth" &&
|
|
26
|
+
typeof login["refreshToken"] === "string" &&
|
|
27
|
+
login["refreshToken"] !== ""
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -28,7 +28,7 @@ The contract's `teams[].workflowMode` reports which one a team is in:
|
|
|
28
28
|
- **mapped**: the team kept its existing stages and a human chose which stage fills each slot.
|
|
29
29
|
- **mixed**: some slots adopted, some hand-chosen.
|
|
30
30
|
|
|
31
|
-
`teams[].gitAutomation` is a
|
|
31
|
+
`teams[].gitAutomation` is a stored consent for a feature that is not built yet (Catalyst managing a team's Linear git automations); nothing reads it, so `off` never stops work and turning it on would start none. What decides whether a team's tickets start is the mapping above: until dispatch, pr, done and canceled each point at a live stage, Catalyst starts nothing in that team, and `explain` says so by name.
|
|
32
32
|
|
|
33
33
|
## The state id is the authority; names are display
|
|
34
34
|
|
|
@@ -66,7 +66,7 @@ No survivor is `no_eligible_account_slot` when any candidate was skipped on capa
|
|
|
66
66
|
|
|
67
67
|
## The unknowns (the evaluator fails closed)
|
|
68
68
|
|
|
69
|
-
When the cloud cannot answer, it says so rather than guessing: `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown`, `branch_snapshot_unknown`. An unknown is "I could not look", never "it is not there"; report it as inconclusive.
|
|
69
|
+
When the cloud cannot answer, it says so rather than guessing: `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown`, `branch_snapshot_unknown`. An unknown is "I could not look", never "it is not there"; report it as inconclusive. The exception is a team with no saved stage mapping: `workflow_mapping_unknown` never clears on its own there, `explain` names the missing stages, and the fix is a tenant owner or admin mapping the team.
|
|
70
70
|
|
|
71
71
|
One advisory gates nothing: `human_addressed_unlabeled_ask_suspect` (assigned to a human with no delegate; possibly an unlabelled ask).
|
|
72
72
|
|
|
@@ -33,12 +33,12 @@ Which parks release themselves:
|
|
|
33
33
|
|
|
34
34
|
| Park | Releases by |
|
|
35
35
|
| -- | -- |
|
|
36
|
-
| repeated failure |
|
|
37
|
-
| remediate round cap reached |
|
|
36
|
+
| repeated failure | a release from the person's own login once its cause is fixed (`catalyst-skills release`), not a clock |
|
|
37
|
+
| remediate round cap reached | the same release, which buys one more round, not a clock |
|
|
38
38
|
| missing branch | its own budgeted release loop |
|
|
39
39
|
| rebase conflict | its own budgeted release loop |
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
A person's own key releases a park, and every other hold that a retry can fix, in one step: `catalyst-skills release <ticket> --because <what changed>`. The cloud releases every governor holding the ticket or releases nothing and names what a person must do instead (a person's own pull request, a review that will not converge, the lifetime repair budget). It refuses a cause it cannot see change unless the caller says what changed. The `unstick` skill runs that loop. When a ticket is parked, name the failure class the explainer shows and whether its cause is fixed.
|
|
42
42
|
|
|
43
43
|
## Two holds that are not failures
|
|
44
44
|
|
|
@@ -53,5 +53,5 @@ Each attempt posts a phase-outcome comment (complete or FAILED, with phase, atte
|
|
|
53
53
|
|
|
54
54
|
1. Run `node scripts/explain-ticket.mjs <ticket>`; the reason names the layer.
|
|
55
55
|
2. `retry_backoff` or `routing_unavailable`: wait; it retries itself. Say when.
|
|
56
|
-
3. `cooling_down`, `remediate_parked`, `no_change_hold`, `validate_class_spent`: name what releases it (
|
|
56
|
+
3. `cooling_down`, `remediate_parked`, `no_change_hold`, `validate_class_spent`: name what releases it (a release once the cause is fixed, a comment, a push) and who can do that.
|
|
57
57
|
4. A system-level cause (provider down, runner image breaker, repo paused) is ONE alert, never a per-ticket escalation.
|
|
@@ -6,9 +6,10 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
9
10
|
|
|
10
11
|
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
11
|
-
export const CONNECT_HINT =
|
|
12
|
+
export const CONNECT_HINT = CONNECT_COMMAND;
|
|
12
13
|
|
|
13
14
|
/** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME (used by tests) over HOME. */
|
|
14
15
|
export function configPath() {
|
|
@@ -22,8 +23,8 @@ export function loadConfig() {
|
|
|
22
23
|
if (!existsSync(path)) return { ok: false, reason: `no config at ${path}` };
|
|
23
24
|
try {
|
|
24
25
|
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
-
if (!cfg || typeof cfg.
|
|
26
|
-
return { ok: false, reason: `${path} is missing
|
|
26
|
+
if (!hasCredential(cfg) || typeof cfg.account !== "string") {
|
|
27
|
+
return { ok: false, reason: `${path} is missing a key or login, or the account` };
|
|
27
28
|
}
|
|
28
29
|
return { ok: true, cfg, path };
|
|
29
30
|
} catch (err) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
|
|
3
|
+
// byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
|
|
4
|
+
// (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
|
|
5
|
+
// each carries its own copy. This file is a library — run a sibling script with --help for usage.
|
|
6
|
+
//
|
|
7
|
+
// customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
|
|
8
|
+
// session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
|
|
9
|
+
// catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
|
|
10
|
+
// itself. A new credential kind lands here, once.
|
|
11
|
+
|
|
12
|
+
/** The command that connects this machine, as every not-connected line names it. */
|
|
13
|
+
export const CONNECT_COMMAND =
|
|
14
|
+
"npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
|
|
15
|
+
|
|
16
|
+
/** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
|
|
17
|
+
export function hasCredential(cfg) {
|
|
18
|
+
if (cfg === null || typeof cfg !== "object") return false;
|
|
19
|
+
const key = cfg["key"];
|
|
20
|
+
if (typeof key === "string" && key !== "") return true;
|
|
21
|
+
const login = cfg["auth"];
|
|
22
|
+
return (
|
|
23
|
+
login !== null &&
|
|
24
|
+
typeof login === "object" &&
|
|
25
|
+
login["kind"] === "oauth" &&
|
|
26
|
+
typeof login["refreshToken"] === "string" &&
|
|
27
|
+
login["refreshToken"] !== ""
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -42,4 +42,4 @@ Every script exits 2 when this machine is not connected (run `catalyst-skills lo
|
|
|
42
42
|
- **Escalate inward.** Instrument, then you, then the desk, then the human as an ask filed through `what-needs-me` with what it blocks. Decide the technical calls yourself; take the sane default and record it; a fleet or provider condition is one note, never one ask per ticket.
|
|
43
43
|
- **Reply where the message arrived,** in-thread, tagged, as the app actor. Never post as the human, never answer someone else's ask. Bookkeeping records take the contract's marker (`catalyst-skills write comment --bookkeeping`).
|
|
44
44
|
- **One status summary,** kept current after every reaction: in flight, blocked and on whom, closed, next, each line with a ticket id. Where a human decision is pending, the line carries the ask's id.
|
|
45
|
-
- **Say what a key cannot see.**
|
|
45
|
+
- **Say what a key cannot see.** PR labels and reactions are not mirrored; the CLI says so by name and points at settings. A park or hold is released from the person's own login once its cause is fixed, through the `unstick` skill (`catalyst-skills release`). Execution history (`explain --history`) and coding-account status (`accounts`) ARE readable — read them rather than reconstructing them from comments.
|
|
@@ -48,7 +48,7 @@ The card's stage is the fourth, weakest signal: a completed phase moves the card
|
|
|
48
48
|
|
|
49
49
|
## Parking, and what it does not do
|
|
50
50
|
|
|
51
|
-
Parking is the lever that stops the cloud offering more rounds on a ticket: moved out of the dispatch column and the ladder's stages, the ticket is excluded at the next offer. It does not kill a phase that is already running under a lease; that container finishes its phase, posts its outcome, and the next offer finds the card parked. If you park a ticket the cloud has itself parked (three consecutive failures, or the remediate round cap), record why in a bookkeeping note
|
|
51
|
+
Parking is the lever that stops the cloud offering more rounds on a ticket: moved out of the dispatch column and the ladder's stages, the ticket is excluded at the next offer. It does not kill a phase that is already running under a lease; that container finishes its phase, posts its outcome, and the next offer finds the card parked. If you park a ticket the cloud has itself parked (three consecutive failures, or the remediate round cap), record why in a bookkeeping note. Releasing a cloud park is not a card move: once its cause is fixed, the `unstick` skill releases it with `catalyst-skills release <ticket>`, and only a refusal that names a person's action becomes an ask.
|
|
52
52
|
|
|
53
53
|
Un-parking is the same dispatch move again. The counted attempts and rounds do not reset when a card comes back; the contract's thresholds say how many remain.
|
|
54
54
|
|
|
@@ -26,7 +26,7 @@ Run `catalyst-skills explain <ticket>` first, and let its reason pick the row:
|
|
|
26
26
|
| blocked | a live blocks relation | chase the blocker: is it dispatchable, is it an ask nobody answered, is it done but still open |
|
|
27
27
|
| ask ticket, ask shape suspected | the ticket is a question | route it to `what-needs-me`; if it is really work, a human applies the release label |
|
|
28
28
|
| not at dispatch stage | somebody moved the card, or it never entered | read the card's history; re-dispatch with `make-ready.mjs` if it should run |
|
|
29
|
-
| cooling down, remediate parked | the cloud parked it after repeated failure or the round cap | read the last outcome card for the class;
|
|
29
|
+
| phase parked, cooling down, remediate parked | the cloud parked it after repeated failure or the round cap | read the last outcome card for the class; once its cause is fixed, the `unstick` skill releases it (`catalyst-skills release <ticket> --because <what changed>`); if the fix is a decision, file an ask |
|
|
30
30
|
| no change hold | a remediate round changed nothing | a human comment on the ticket, or a new push to the branch, releases it; say what should change |
|
|
31
31
|
| validate class spent, stale failure episode | the repair budget for this failure is used, or the ladder moved on | read the outcome cards; usually a decision about the approach, so an ask |
|
|
32
32
|
| waiting on | a merge-gate failure with no automatic repair | read the merge-wait comment and the PR's three legs through `catalyst-github` |
|
|
@@ -52,7 +52,7 @@ Before anything reaches the human, answer three questions:
|
|
|
52
52
|
2. **Does this need to block at all?** If a sane default exists, take it, record it, and file the ask anyway so the human can overrule; the work does not wait.
|
|
53
53
|
3. **Who else can move this?** Another steward, the desk, a repository owner. Pulling in a peer is preferred over pulling in the human.
|
|
54
54
|
|
|
55
|
-
Only a genuine product, priority or approval decision, or an action only a human can physically take (
|
|
55
|
+
Only a genuine product, priority or approval decision, or an action only a human can physically take (close a person's own pull request, read a review that will not converge, resolve a repository setting, apply the release label, answer a merge policy question), survives to become an ask.
|
|
56
56
|
|
|
57
57
|
## When a stall becomes an ask
|
|
58
58
|
|
|
@@ -10,6 +10,7 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
13
14
|
|
|
14
15
|
export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
|
|
15
16
|
export const NOT_CONFIGURED_EXIT = 2;
|
|
@@ -33,7 +34,7 @@ export function loadCustomerConfig() {
|
|
|
33
34
|
if (!existsSync(path)) return null;
|
|
34
35
|
try {
|
|
35
36
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
-
if (!parsed || typeof parsed
|
|
37
|
+
if (!hasCredential(parsed) || typeof parsed.account !== "string") return null;
|
|
37
38
|
return parsed;
|
|
38
39
|
} catch {
|
|
39
40
|
return null;
|
|
@@ -44,7 +45,7 @@ export function loadCustomerConfig() {
|
|
|
44
45
|
export function requireConfigured() {
|
|
45
46
|
const cfg = loadCustomerConfig();
|
|
46
47
|
if (cfg) return cfg;
|
|
47
|
-
console.error(`not connected: ${configPath()} is missing or unreadable — run:
|
|
48
|
+
console.error(`not connected: ${configPath()} is missing or unreadable — run: ${CONNECT_COMMAND}`);
|
|
48
49
|
process.exit(NOT_CONFIGURED_EXIT);
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
|
|
3
|
+
// byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
|
|
4
|
+
// (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
|
|
5
|
+
// each carries its own copy. This file is a library — run a sibling script with --help for usage.
|
|
6
|
+
//
|
|
7
|
+
// customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
|
|
8
|
+
// session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
|
|
9
|
+
// catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
|
|
10
|
+
// itself. A new credential kind lands here, once.
|
|
11
|
+
|
|
12
|
+
/** The command that connects this machine, as every not-connected line names it. */
|
|
13
|
+
export const CONNECT_COMMAND =
|
|
14
|
+
"npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
|
|
15
|
+
|
|
16
|
+
/** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
|
|
17
|
+
export function hasCredential(cfg) {
|
|
18
|
+
if (cfg === null || typeof cfg !== "object") return false;
|
|
19
|
+
const key = cfg["key"];
|
|
20
|
+
if (typeof key === "string" && key !== "") return true;
|
|
21
|
+
const login = cfg["auth"];
|
|
22
|
+
return (
|
|
23
|
+
login !== null &&
|
|
24
|
+
typeof login === "object" &&
|
|
25
|
+
login["kind"] === "oauth" &&
|
|
26
|
+
typeof login["refreshToken"] === "string" &&
|
|
27
|
+
login["refreshToken"] !== ""
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unstick
|
|
3
|
+
description: >-
|
|
4
|
+
Get a stuck Catalyst Cloud ticket moving again. Use when the person asks "why is this parked and can you release it?", "unpark this", "get things flowing again", "the outage is over, retry what failed", or hands you a ticket that is not moving. Reads why nothing runs and what holds the ticket through the catalyst-skills CLI, decides whether the recorded cause is fixed, previews the release, releases every governor holding the ticket the right way (or one failure class across a team), and raises an ask only for what a person has to do. Never releases a cause it cannot show changed without saying so.
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
|
+
---
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
|
|
9
|
+
|
|
10
|
+
# Unstick
|
|
11
|
+
|
|
12
|
+
You get one stuck ticket, or one set of tickets stuck for the same reason, moving again. The cloud parks or holds a ticket when retrying would only repeat a failure: after repeated failures, when the repair-round cap is spent, when a repair changed nothing, when the same validate failure recurred. Each of those is a governor. Once its cause is fixed, the person's own key releases it; nothing here needs an operator.
|
|
13
|
+
|
|
14
|
+
## Run first
|
|
15
|
+
|
|
16
|
+
Scripts are run, never read. Each prints `--help`; exit 2 means this machine is not connected (run `catalyst-skills login`).
|
|
17
|
+
|
|
18
|
+
- `node scripts/unstick.mjs <ticket>` — one JSON document: the eligibility explanation, the execution history (every governor holding the ticket with what releases it, and past releases), and a dry-run release showing what a release would clear and refuse. Changes nothing.
|
|
19
|
+
- `node scripts/unstick.mjs <ticket> --because "<what changed>"` — the same, then the real release. Add `--retry-unchanged` only when you can say what changed outside what the cloud can see.
|
|
20
|
+
- `node scripts/unstick.mjs --class <failure-class> --team <key> [--because "<what changed>"]` — the same for every ticket on one team parked under one failure class (at most 25 per call).
|
|
21
|
+
|
|
22
|
+
The verbs underneath are `catalyst-skills explain <ticket>`, `catalyst-skills explain <ticket> --history` and `catalyst-skills release <ticket> --because <text> [--retry-unchanged] [--dry-run]`.
|
|
23
|
+
|
|
24
|
+
## Load on demand
|
|
25
|
+
|
|
26
|
+
| when | read |
|
|
27
|
+
| -- | -- |
|
|
28
|
+
| deciding whether to release, what a refusal means, when to ask a person | `references/playbook.md` |
|
|
29
|
+
| what a reason in the explanation means | the `whats-happening` skill's why-is-it-stuck reference, or `how-catalyst-works` |
|
|
30
|
+
| a refusal needs a decision or a person's action | the `what-needs-me` skill |
|
|
31
|
+
| a refusal names a person's pull request or its review | the `catalyst-github` skill |
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
- **Read before you release.** Explain, then history, then a dry run. A release is never the first call.
|
|
36
|
+
- **The cause decides, not the wish to move.** Release when you can name what changed since the ticket was held: a push, a comment that says what to change, an outage that ended, a secret that was rotated, an account that was re-enrolled. Put that sentence in `--because`; it is recorded against your name.
|
|
37
|
+
- **Unchanged means say so.** The cloud refuses a release whose cause it cannot see change. Pass `--retry-unchanged` only with a `--because` that names the change it cannot see. If nothing changed, do not release; say so.
|
|
38
|
+
- **One release per cause.** If the history shows a release for the same failure and nothing has changed since, a second release buys the same failure. Raise an ask instead.
|
|
39
|
+
- **A refusal names its fix.** Do the fix when it is yours to do; everything else is one ask through `what-needs-me`, attached to an existing ask for the same decision. Never close or merge a person's pull request, and never answer an ask as the person.
|
|
40
|
+
- **A shared cause is one note.** Several tickets parked by one outage or one bad image are one class release and one line in your reply, never one ask per ticket.
|
|
41
|
+
- **Their tenant, as them.** Every read and write goes through the CLI and the person's own login. You never name another tenant.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Unstick"
|
|
3
|
+
short_description: "Find out why a Catalyst ticket is parked or held and release it once its cause is fixed; asks only for what needs a person"
|
|
4
|
+
default_prompt: "Use $unstick to find out why <ticket> is stuck and get it moving again."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: false
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# The unstick playbook
|
|
2
|
+
|
|
3
|
+
This reference restates how a person's release of a stuck ticket works and the order you follow. The cloud does the deciding: it reads every governor holding the ticket and either releases all of them or releases nothing and names, for each one it refuses, the action that does fix it. Your job is to read, judge whether the cause is fixed, and say so honestly.
|
|
4
|
+
|
|
5
|
+
## The order
|
|
6
|
+
|
|
7
|
+
1. **Why is nothing running?** `catalyst-skills explain <ticket>`. If the reason is not a park or a hold (for example `blocked`, `not_at_dispatch_stage`, an ask, a missing environment check, `scope_overlap`), there is nothing to release: follow the `whats-happening` skill's why-is-it-stuck reference instead and stop here.
|
|
8
|
+
2. **What holds it, and has it been released before?** `catalyst-skills explain <ticket> --history`. Read three things: the "Held by" lines (each governor and what releases it), the last failure (its class and summary), and the "Releases" lines (who released it before, why, and what happened).
|
|
9
|
+
3. **Is the cause fixed?** Decide from evidence, in this order:
|
|
10
|
+
- a push to the ticket's branch, a comment on the ticket that says what to change, or main moving on since the park: the cloud can see these, and a release goes through;
|
|
11
|
+
- a change the cloud cannot see (a provider outage that ended, a rotated secret, a re-enrolled coding account, a fixed tenant setting): release with `--retry-unchanged`, and the reason you give names that change;
|
|
12
|
+
- nothing changed: do not release. Tell the person what failed and what would have to change.
|
|
13
|
+
If a previous release in the history named the same failure and nothing has changed since, the next release will fail the same way. Do not release again; raise an ask.
|
|
14
|
+
4. **Preview.** `catalyst-skills release <ticket> --dry-run`. It prints what a release would clear and what it would refuse, and changes nothing.
|
|
15
|
+
5. **Release.** `catalyst-skills release <ticket> --because "<the change>"`. Report what it released, verbatim, with the ticket id. A warning line (for example, that a released repair round runs again at the escalated tier and a second cap parks it again) goes in your reply too.
|
|
16
|
+
6. **Act on each refusal.** A refusal is terminal for that attempt: nothing was released. Each names its fix; the table below says who does it.
|
|
17
|
+
|
|
18
|
+
## What a refusal means and who fixes it
|
|
19
|
+
|
|
20
|
+
| refusal | what it means | who acts |
|
|
21
|
+
| -- | -- | -- |
|
|
22
|
+
| `cause_unchanged` | nothing the cloud can see changed since it was held | you, if you can name a change it cannot see (`--retry-unchanged` with that `--because`); otherwise nobody should release it yet |
|
|
23
|
+
| `lease_held` | a phase is running on the ticket right now | nobody; wait for the phase's outcome comment, then look again |
|
|
24
|
+
| `branch_still_missing` | the ticket's branch was never pushed | it releases itself when the branch appears; check the earlier phases through `catalyst-linear` |
|
|
25
|
+
| `human_owned_pr` | a person opened the pull request this ticket would work on | that person closes or merges it, or hands it to Catalyst; never close a person's pull request yourself |
|
|
26
|
+
| `review_not_converging` | review and repair kept finding new problems | a person reads the findings (`catalyst-github`) and comments on the ticket to resume; raise an ask for that read |
|
|
27
|
+
| `round_threshold` | the ticket spent its lifetime repair budget | a person answers the ask the cloud already raised, or pushes a fix; point at that ask, do not raise a second one |
|
|
28
|
+
| `base_revision_lost` | the ticket's base commit is gone | whoever administers the tenant re-pins it; raise one ask |
|
|
29
|
+
| `remediate_cap` | this tenant keeps the repair-round cap for an administrator | whoever administers the tenant; raise one ask |
|
|
30
|
+
| `unknown_park` | a park this bundle does not know | raise one ask with the ticket id and the park name, verbatim |
|
|
31
|
+
| `held_beyond_class` | a class release reached a ticket held by more than that class | release that ticket on its own, from step 1 |
|
|
32
|
+
| `team_changed` (the ticket moved teams during the check) | the release was checked for one team and the ticket is now on another | run the release again |
|
|
33
|
+
|
|
34
|
+
Raise an ask only through `what-needs-me`, with the stuck ticket in what it blocks, and attach to an existing ask for the same decision rather than filing a second one.
|
|
35
|
+
|
|
36
|
+
## Several tickets, one cause
|
|
37
|
+
|
|
38
|
+
When several tickets on one team are parked for the same failure class (the explanation and the history show the same class, typically an outage or a runner problem that has since ended):
|
|
39
|
+
|
|
40
|
+
1. `catalyst-skills release --class <failure-class> --team <key> --dry-run` to see which tickets it reaches.
|
|
41
|
+
2. `catalyst-skills release --class <failure-class> --team <key> --because "<the change>" --retry-unchanged` when the change is one the cloud cannot see.
|
|
42
|
+
3. Report one line: how many released, which were refused and why, and whether more remain (run it again for the next batch).
|
|
43
|
+
|
|
44
|
+
A ticket the class release refuses is listed and left alone; take it through the single-ticket order above.
|
|
45
|
+
|
|
46
|
+
## What a release does not do
|
|
47
|
+
|
|
48
|
+
- It does not skip a phase or accept work as done; a released phase runs again.
|
|
49
|
+
- It does not reset a lifetime budget. A released repair round still counts toward the ticket's lifetime threshold, and the cloud stops a loop that claims the same unit too often in an hour.
|
|
50
|
+
- It does not move the card. The ticket stays where it is and runs when the cloud next offers it.
|
|
51
|
+
- It is recorded: who released, with which login, why, what it cleared and what it refused. `explain --history` shows it to everyone who reads the ticket.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: spawn the catalyst-skills CLI.
|
|
3
|
+
// The CLI holds the SDK and the key; this file holds neither. It reads customer.json only
|
|
4
|
+
// to learn where the CLI lives, and it exits 2 with one line when the machine is not connected.
|
|
5
|
+
// Run any script beside this one with --help; this file is a library and is never run directly.
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
10
|
+
|
|
11
|
+
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
12
|
+
export const NOT_CONFIGURED_EXIT = 2;
|
|
13
|
+
|
|
14
|
+
export function configPath() {
|
|
15
|
+
const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/";
|
|
16
|
+
return join(home, ".config", "catalyst-cloud", "customer.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The connected-machine config, or exit 2 with the one line that says how to connect. */
|
|
20
|
+
export function loadConfig() {
|
|
21
|
+
const path = configPath();
|
|
22
|
+
if (!existsSync(path)) notConfigured(`no config at ${path}`);
|
|
23
|
+
try {
|
|
24
|
+
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
+
if (!hasCredential(cfg)) notConfigured(`config at ${path} has no key or login`);
|
|
26
|
+
return cfg;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err && err.exitCode === NOT_CONFIGURED_EXIT) throw err;
|
|
29
|
+
notConfigured(`config at ${path} is unreadable`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function notConfigured(why) {
|
|
34
|
+
process.stderr.write(`not connected (${why}) — run: ${CONNECT_COMMAND}\n`);
|
|
35
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run one catalyst-skills verb and return {code, stdout, stderr}. Uses the CLI path the login
|
|
40
|
+
* recorded when it still exists, else `npx <package>`. A CLI exit of 2 with a "not joined" or
|
|
41
|
+
* "not connected" line is turned into this script's own exit 2, so every caller sees one contract.
|
|
42
|
+
*/
|
|
43
|
+
export function runCli(args, opts = {}) {
|
|
44
|
+
const cfg = loadConfig();
|
|
45
|
+
const useRecorded = typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
|
|
46
|
+
const cmd = useRecorded ? process.execPath : "npx";
|
|
47
|
+
const argv = useRecorded ? [cfg.cliPath, ...args] : [PACKAGE, ...args];
|
|
48
|
+
const res = spawnSync(cmd, argv, {
|
|
49
|
+
input: opts.stdin,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
52
|
+
shell: !useRecorded && process.platform === "win32",
|
|
53
|
+
env: process.env,
|
|
54
|
+
});
|
|
55
|
+
if (res.error) {
|
|
56
|
+
process.stderr.write(`could not run ${cmd}: ${res.error.message}\n`);
|
|
57
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
58
|
+
}
|
|
59
|
+
const stdout = res.stdout ?? "";
|
|
60
|
+
const stderr = res.stderr ?? "";
|
|
61
|
+
if (res.status === NOT_CONFIGURED_EXIT && /not (joined|connected|configured)/i.test(stderr)) {
|
|
62
|
+
process.stderr.write(stderr);
|
|
63
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
64
|
+
}
|
|
65
|
+
return { code: res.status ?? 1, stdout, stderr };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Run a verb and require success; on failure print the CLI's own stderr and exit with its code (or 1). */
|
|
69
|
+
export function mustRun(args, opts = {}) {
|
|
70
|
+
const r = runCli(args, opts);
|
|
71
|
+
if (r.code !== 0) {
|
|
72
|
+
if (r.stderr) process.stderr.write(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`);
|
|
73
|
+
if (r.stdout && !opts.quiet) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`);
|
|
74
|
+
process.exit(r.code === 0 ? 1 : r.code);
|
|
75
|
+
}
|
|
76
|
+
return r;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Parse the CLI's --json stdout; a non-JSON answer is a failed check, exit 1. */
|
|
80
|
+
export function parseJson(stdout, what = "output") {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(stdout.trim());
|
|
83
|
+
} catch {
|
|
84
|
+
process.stderr.write(`could not parse ${what} as JSON: ${stdout.trim().slice(0, 200)}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Minimal flag parsing for the scripts: --name value, --name=value, --flag, repeated flags collect. */
|
|
90
|
+
export function parseFlags(argv, spec) {
|
|
91
|
+
const flags = {};
|
|
92
|
+
const positionals = [];
|
|
93
|
+
for (let i = 0; i < argv.length; i++) {
|
|
94
|
+
const a = argv[i];
|
|
95
|
+
if (a === "--help" || a === "-h") return { help: true, flags, positionals };
|
|
96
|
+
if (a.startsWith("--")) {
|
|
97
|
+
let name = a.slice(2);
|
|
98
|
+
let value;
|
|
99
|
+
const eq = name.indexOf("=");
|
|
100
|
+
if (eq !== -1) {
|
|
101
|
+
value = name.slice(eq + 1);
|
|
102
|
+
name = name.slice(0, eq);
|
|
103
|
+
}
|
|
104
|
+
const s = spec[name];
|
|
105
|
+
if (!s) {
|
|
106
|
+
process.stderr.write(`unknown option --${name} (try --help)\n`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
if (s.value) {
|
|
110
|
+
if (value === undefined) value = argv[++i];
|
|
111
|
+
if (value === undefined || value === "") {
|
|
112
|
+
process.stderr.write(`--${name} needs a value\n`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
if (s.repeat) (flags[name] ??= []).push(value);
|
|
116
|
+
else flags[name] = value;
|
|
117
|
+
} else flags[name] = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
positionals.push(a);
|
|
121
|
+
}
|
|
122
|
+
return { help: false, flags, positionals };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function printHelp(usage, spec, notes = []) {
|
|
126
|
+
const lines = [`Usage: ${usage}`, ""];
|
|
127
|
+
const names = Object.keys(spec);
|
|
128
|
+
if (names.length) {
|
|
129
|
+
lines.push("Options:");
|
|
130
|
+
for (const n of names) lines.push(` --${n}${spec[n].value ? " <value>" : ""} ${spec[n].help}`);
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|
|
133
|
+
lines.push(...notes, "", "Exit codes: 0 ok · 1 the check failed or the arguments were wrong · 2 this machine is not connected to a tenant");
|
|
134
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
135
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
|
|
3
|
+
// byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
|
|
4
|
+
// (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
|
|
5
|
+
// each carries its own copy. This file is a library — run a sibling script with --help for usage.
|
|
6
|
+
//
|
|
7
|
+
// customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
|
|
8
|
+
// session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
|
|
9
|
+
// catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
|
|
10
|
+
// itself. A new credential kind lands here, once.
|
|
11
|
+
|
|
12
|
+
/** The command that connects this machine, as every not-connected line names it. */
|
|
13
|
+
export const CONNECT_COMMAND =
|
|
14
|
+
"npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
|
|
15
|
+
|
|
16
|
+
/** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
|
|
17
|
+
export function hasCredential(cfg) {
|
|
18
|
+
if (cfg === null || typeof cfg !== "object") return false;
|
|
19
|
+
const key = cfg["key"];
|
|
20
|
+
if (typeof key === "string" && key !== "") return true;
|
|
21
|
+
const login = cfg["auth"];
|
|
22
|
+
return (
|
|
23
|
+
login !== null &&
|
|
24
|
+
typeof login === "object" &&
|
|
25
|
+
login["kind"] === "oauth" &&
|
|
26
|
+
typeof login["refreshToken"] === "string" &&
|
|
27
|
+
login["refreshToken"] !== ""
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// unstick.mjs — "why is this stuck, and can it be released?" in one JSON document. Runs
|
|
3
|
+
// `catalyst-skills explain`, `explain --history` and a dry-run `release` for one ticket (or a dry-run
|
|
4
|
+
// class release for one team), and only with --because runs the real release. Reaches the cloud only
|
|
5
|
+
// by spawning the CLI.
|
|
6
|
+
import { mustRun, parseFlags, parseJson, printHelp, runCli } from "./lib/cli.mjs";
|
|
7
|
+
|
|
8
|
+
const SPEC = {
|
|
9
|
+
because: { value: true, help: "what changed since the ticket was held; runs the real release after the preview" },
|
|
10
|
+
"retry-unchanged": { value: false, help: "release even though nothing the cloud can see changed (say what did in --because)" },
|
|
11
|
+
class: { value: true, help: "release every ticket on --team parked under this failure class" },
|
|
12
|
+
team: { value: true, help: "with --class: the team key" },
|
|
13
|
+
limit: { value: true, help: "with --class: at most this many tickets (the cloud caps it at 25)" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
|
|
17
|
+
const ticket = positionals[0];
|
|
18
|
+
const byClass = typeof flags.class === "string";
|
|
19
|
+
if (help || (!ticket && !byClass) || (byClass && (ticket || typeof flags.team !== "string"))) {
|
|
20
|
+
printHelp("node scripts/unstick.mjs <ticket> [--because <text>] [--retry-unchanged] [--help] | --class <c> --team <K> [--because <text>] [--retry-unchanged] [--limit N] [--help]", SPEC, [
|
|
21
|
+
"Without --because it changes nothing: it prints the explanation, the history (every governor holding the ticket",
|
|
22
|
+
"and past releases) and a dry-run release. With --because it then runs the real release. Read references/playbook.md",
|
|
23
|
+
"before passing --because. A refused release exits 1 with the refusals in the document.",
|
|
24
|
+
]);
|
|
25
|
+
process.exit(help ? 0 : 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const releaseArgs = byClass ? ["release", "--class", flags.class, "--team", flags.team] : ["release", ticket];
|
|
29
|
+
if (flags["retry-unchanged"]) releaseArgs.push("--retry-unchanged");
|
|
30
|
+
if (byClass && typeof flags.limit === "string") releaseArgs.push("--limit", flags.limit);
|
|
31
|
+
|
|
32
|
+
const out = {};
|
|
33
|
+
if (!byClass) {
|
|
34
|
+
out.explain = parseJson(mustRun(["explain", ticket, "--json"]).stdout, "explain");
|
|
35
|
+
out.history = parseJson(mustRun(["explain", ticket, "--history", "--json"]).stdout, "history");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const preview = runCli([...releaseArgs, "--dry-run", "--json"]);
|
|
39
|
+
if (preview.code !== 0 && preview.code !== 1) {
|
|
40
|
+
process.stderr.write(preview.stderr);
|
|
41
|
+
process.exit(preview.code);
|
|
42
|
+
}
|
|
43
|
+
out.preview = parseJson(preview.stdout, "the dry-run release");
|
|
44
|
+
|
|
45
|
+
let exit = 0;
|
|
46
|
+
if (typeof flags.because === "string") {
|
|
47
|
+
const real = runCli([...releaseArgs, "--because", flags.because, "--json"]);
|
|
48
|
+
if (real.code !== 0 && real.code !== 1) {
|
|
49
|
+
process.stderr.write(real.stderr);
|
|
50
|
+
process.exit(real.code);
|
|
51
|
+
}
|
|
52
|
+
out.release = parseJson(real.stdout, "the release");
|
|
53
|
+
exit = real.code;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
57
|
+
process.exit(exit);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
9
10
|
|
|
10
11
|
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
11
12
|
export const NOT_CONFIGURED_EXIT = 2;
|
|
@@ -21,7 +22,7 @@ export function loadConfig() {
|
|
|
21
22
|
if (!existsSync(path)) notConfigured(`no config at ${path}`);
|
|
22
23
|
try {
|
|
23
24
|
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
-
if (
|
|
25
|
+
if (!hasCredential(cfg)) notConfigured(`config at ${path} has no key or login`);
|
|
25
26
|
return cfg;
|
|
26
27
|
} catch (err) {
|
|
27
28
|
if (err && err.exitCode === NOT_CONFIGURED_EXIT) throw err;
|
|
@@ -30,7 +31,7 @@ export function loadConfig() {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function notConfigured(why) {
|
|
33
|
-
process.stderr.write(`not connected (${why}) — run:
|
|
34
|
+
process.stderr.write(`not connected (${why}) — run: ${CONNECT_COMMAND}\n`);
|
|
34
35
|
process.exit(NOT_CONFIGURED_EXIT);
|
|
35
36
|
}
|
|
36
37
|
|