@catalyst-cloud/cli 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/CHANGELOG.md +75 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bin/catalyst-skills.js +8 -0
- package/bin/catalyst.js +5 -0
- package/bin/launch.js +154 -0
- package/dist/args.js +280 -0
- package/dist/ask.js +161 -0
- package/dist/browser.js +20 -0
- package/dist/cli.js +397 -0
- package/dist/config.js +241 -0
- package/dist/contract-types.js +4 -0
- package/dist/contract.js +184 -0
- package/dist/detach.js +10 -0
- package/dist/environment.js +207 -0
- package/dist/errors.js +27 -0
- package/dist/events.js +106 -0
- package/dist/execution.js +451 -0
- package/dist/oauth.js +300 -0
- package/dist/pagination.js +76 -0
- package/dist/prompt.js +35 -0
- package/dist/published.js +79 -0
- package/dist/query.js +248 -0
- package/dist/ready.js +380 -0
- package/dist/release.js +142 -0
- package/dist/replica.js +614 -0
- package/dist/runtime-store.js +135 -0
- package/dist/runtime-verb.js +66 -0
- package/dist/runtime.js +87 -0
- package/dist/sdk.js +29 -0
- package/dist/secret.js +190 -0
- package/dist/semver.js +18 -0
- package/dist/skill-shape.js +189 -0
- package/dist/skills.js +129 -0
- package/dist/transport.js +205 -0
- package/dist/ts-deps-loader.js +113 -0
- package/dist/watch/consumer.js +141 -0
- package/dist/watch/cursor-file.js +62 -0
- package/dist/watch.js +175 -0
- package/dist/write.js +224 -0
- package/package.json +60 -0
- package/skills/catalyst-github/SKILL.md +35 -0
- package/skills/catalyst-github/agents/openai.yaml +6 -0
- package/skills/catalyst-github/agents/portability.yaml +4 -0
- package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
- package/skills/catalyst-github/scripts/lib/cli.mjs +103 -0
- package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
- package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
- package/skills/catalyst-linear/SKILL.md +43 -0
- package/skills/catalyst-linear/agents/openai.yaml +6 -0
- package/skills/catalyst-linear/agents/portability.yaml +5 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +52 -0
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
- package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
- package/skills/catalyst-linear/scripts/comment.mjs +59 -0
- package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
- package/skills/catalyst-linear/scripts/label.mjs +48 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +164 -0
- package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-linear/scripts/move.mjs +41 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
- package/skills/catalyst-linear/scripts/search.mjs +49 -0
- package/skills/catalyst-onboard/SKILL.md +57 -0
- package/skills/catalyst-onboard/agents/openai.yaml +6 -0
- package/skills/catalyst-onboard/agents/portability.yaml +5 -0
- package/skills/catalyst-onboard/references/declaring-a-repository.md +23 -0
- package/skills/catalyst-onboard/references/skill-sources.md +35 -0
- package/skills/catalyst-onboard/references/the-one-path.md +149 -0
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +46 -0
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +50 -0
- package/skills/catalyst-onboard/references/who-fixes-what.md +44 -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 +345 -0
- package/skills/catalyst-setup/SKILL.md +36 -0
- package/skills/catalyst-setup/agents/openai.yaml +6 -0
- package/skills/catalyst-setup/agents/portability.yaml +4 -0
- package/skills/catalyst-setup/references/what-each-check-means.md +88 -0
- package/skills/catalyst-setup/scripts/check.mjs +75 -0
- package/skills/catalyst-setup/scripts/lib/cli.mjs +103 -0
- package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
- package/skills/connect-me/SKILL.md +63 -0
- package/skills/connect-me/agents/openai.yaml +6 -0
- package/skills/connect-me/agents/portability.yaml +5 -0
- package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
- package/skills/connect-me/scripts/lib/cli.mjs +185 -0
- package/skills/connect-me/scripts/lib/credential.mjs +29 -0
- package/skills/connect-me/scripts/verify-connection.mjs +68 -0
- package/skills/how-catalyst-works/SKILL.md +43 -0
- package/skills/how-catalyst-works/agents/openai.yaml +6 -0
- package/skills/how-catalyst-works/agents/portability.yaml +4 -0
- package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +56 -0
- package/skills/how-catalyst-works/references/the-ladder.md +41 -0
- package/skills/how-catalyst-works/references/what-catalyst-is.md +30 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +164 -0
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
- package/skills/how-catalyst-works/scripts/whats-running.mjs +65 -0
- package/skills/run-this-project/SKILL.md +45 -0
- package/skills/run-this-project/agents/openai.yaml +6 -0
- package/skills/run-this-project/agents/portability.yaml +5 -0
- package/skills/run-this-project/assets/stall-policy.json +15 -0
- package/skills/run-this-project/references/making-work-ready.md +60 -0
- package/skills/run-this-project/references/reacting-to-events.md +76 -0
- package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
- package/skills/run-this-project/scripts/lib/cli.mjs +185 -0
- package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
- package/skills/run-this-project/scripts/make-ready.mjs +64 -0
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +61 -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 +41 -0
- package/skills/what-needs-me/agents/openai.yaml +6 -0
- package/skills/what-needs-me/agents/portability.yaml +5 -0
- package/skills/what-needs-me/references/raising-a-decision.md +41 -0
- package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
- package/skills/what-needs-me/references/settling-an-answer.md +37 -0
- package/skills/what-needs-me/scripts/inbox.mjs +56 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +135 -0
- package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
- package/skills/what-needs-me/scripts/raise.mjs +53 -0
- package/skills/what-needs-me/scripts/settle.mjs +73 -0
- package/skills/whats-happening/SKILL.md +43 -0
- package/skills/whats-happening/agents/openai.yaml +6 -0
- package/skills/whats-happening/agents/portability.yaml +4 -0
- package/skills/whats-happening/assets/status-reply.json +77 -0
- package/skills/whats-happening/references/reading-the-board.md +43 -0
- package/skills/whats-happening/references/reprioritising.md +37 -0
- package/skills/whats-happening/references/routing-work.md +36 -0
- package/skills/whats-happening/references/status-reply.md +34 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +62 -0
- package/skills/whats-happening/scripts/explain.mjs +28 -0
- package/skills/whats-happening/scripts/lib/cli.mjs +135 -0
- package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/scripts/snapshot.mjs +149 -0
- package/vendor/README.md +9 -0
- package/vendor/paths/index.d.ts +85 -0
- package/vendor/paths/index.js +148 -0
- package/vendor/paths/legacy-installer.d.ts +36 -0
- package/vendor/paths/legacy-installer.js +154 -0
- package/vendor/paths/node.d.ts +18 -0
- package/vendor/paths/node.js +102 -0
- package/vendor/paths/provenance.json +17 -0
package/dist/args.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// args.ts — argument parsing for every verb. Positional 0 is the command, positional 1 the
|
|
2
|
+
// subcommand, the rest are positionals; flags are per-verb tables so an unknown flag is a usage
|
|
3
|
+
// error naming the verb, and every verb's --help is generated from its own table.
|
|
4
|
+
import { UsageError } from "./errors.js";
|
|
5
|
+
const GLOBAL_FLAGS = {
|
|
6
|
+
key: { value: true, help: "your personal key (or CATALYST_CLOUD_TOKEN)" },
|
|
7
|
+
"base-url": { value: true, help: "Catalyst Cloud origin (or CATALYST_CLOUD_BASE_URL)" },
|
|
8
|
+
"skills-dir": { value: true, help: "where the skills are copied (default ~/.claude/skills)" },
|
|
9
|
+
force: { value: false, help: "replace a skill directory this package did not install" },
|
|
10
|
+
json: { value: false, help: "machine-readable output" },
|
|
11
|
+
};
|
|
12
|
+
/** Per-verb flag tables. A verb absent here accepts only the global flags. */
|
|
13
|
+
export const FLAG_TABLES = {
|
|
14
|
+
login: {
|
|
15
|
+
"start-replica": { value: false, help: "run `replica start --detach` after connecting" },
|
|
16
|
+
},
|
|
17
|
+
install: {},
|
|
18
|
+
status: {},
|
|
19
|
+
notice: {},
|
|
20
|
+
me: {},
|
|
21
|
+
contract: {
|
|
22
|
+
refresh: { value: false, help: "ignore the cache age and revalidate now" },
|
|
23
|
+
path: { value: true, help: "print a sub-document, e.g. teams.0.stages" },
|
|
24
|
+
},
|
|
25
|
+
query: {
|
|
26
|
+
team: { value: true, help: "team key filter" },
|
|
27
|
+
project: { value: true, help: "project id filter" },
|
|
28
|
+
state: { value: true, help: "state name filter" },
|
|
29
|
+
limit: { value: true, help: "max rows (default 50); one page only unless --all" },
|
|
30
|
+
since: { value: true, help: "changes: the cursor to read after" },
|
|
31
|
+
source: { value: true, help: "replica | api (default: replica when fresh, else api)" },
|
|
32
|
+
ticket: { value: true, help: "pulls: only PRs linked to this ticket" },
|
|
33
|
+
all: { value: false, help: "issues/pulls: follow the cloud's page cursor to the end of the scope" },
|
|
34
|
+
},
|
|
35
|
+
replica: {
|
|
36
|
+
detach: { value: false, help: "start: run the writer in the background and write a pidfile" },
|
|
37
|
+
db: { value: true, help: "replica file path (default from customer.json)" },
|
|
38
|
+
probe: { value: false, help: "status: also fetch the cloud head and print the lag" },
|
|
39
|
+
"stale-ms": { value: true, help: "status: heartbeat age that counts as stale (default 15000)" },
|
|
40
|
+
},
|
|
41
|
+
events: {
|
|
42
|
+
type: { value: true, help: "exact event type" },
|
|
43
|
+
ticket: { value: true, help: "ticket identifier found in the event payload" },
|
|
44
|
+
after: { value: true, help: "event sequence to read after (tail/wait default to local head)" },
|
|
45
|
+
limit: { value: true, help: "query: maximum matching events (default 50)" },
|
|
46
|
+
timeout: { value: true, help: "wait-for: bounded wait in seconds (default 300)" },
|
|
47
|
+
directory: { value: true, help: "event cache directory (default: SDK XDG path)" },
|
|
48
|
+
},
|
|
49
|
+
explain: {
|
|
50
|
+
history: { value: false, help: "per-ticket execution history instead of the eligibility reason" },
|
|
51
|
+
},
|
|
52
|
+
history: {},
|
|
53
|
+
running: {
|
|
54
|
+
ticket: { value: true, help: "with --phase: also the lease attributions for this (ticket, phase)" },
|
|
55
|
+
phase: { value: true, help: "with --ticket: the phase whose lease attributions to read" },
|
|
56
|
+
},
|
|
57
|
+
queue: {
|
|
58
|
+
team: { value: true, help: "team key (default: every team the tenant contract names)" },
|
|
59
|
+
},
|
|
60
|
+
watch: {
|
|
61
|
+
team: { value: true, help: "scope: a team key" },
|
|
62
|
+
ticket: { value: true, repeat: true, help: "scope: a ticket identifier (repeatable)" },
|
|
63
|
+
project: { value: true, help: "scope: a project id" },
|
|
64
|
+
exec: { value: true, help: "run this shell command per frame with the frame on stdin" },
|
|
65
|
+
"cursor-file": { value: true, help: "cursor file path (default ~/.config/catalyst-cloud/watch-cursor.json)" },
|
|
66
|
+
from: { value: true, help: "cursor | head (default cursor)" },
|
|
67
|
+
},
|
|
68
|
+
write: {
|
|
69
|
+
body: { value: true, help: "comment: the body text" },
|
|
70
|
+
stdin: { value: false, help: "comment: read the body from stdin" },
|
|
71
|
+
parent: { value: true, help: "comment: reply under this comment id" },
|
|
72
|
+
bookkeeping: { value: false, help: "comment: prefix the contract's bookkeeping marker" },
|
|
73
|
+
"as-user": { value: false, help: "post with the personal identity instead of the app actor" },
|
|
74
|
+
slot: { value: true, help: "state: the workflow slot to move to" },
|
|
75
|
+
"state-id": { value: true, help: "state: an explicit Linear state id" },
|
|
76
|
+
"state-type": { value: true, help: "state: the first state of this type on the ticket's team (e.g. backlog)" },
|
|
77
|
+
add: { value: true, repeat: true, help: "label: label name or id to add (repeatable)" },
|
|
78
|
+
remove: { value: true, repeat: true, help: "label: label name or id to remove (repeatable)" },
|
|
79
|
+
team: { value: true, help: "create: team key" },
|
|
80
|
+
title: { value: true, help: "create/attachment/session: title" },
|
|
81
|
+
label: { value: true, repeat: true, help: "create: label name or id (repeatable)" },
|
|
82
|
+
priority: { value: true, help: "create: Linear priority 0-4" },
|
|
83
|
+
comment: { value: true, help: "reaction: react to this comment id instead of the ticket" },
|
|
84
|
+
emoji: { value: true, help: "reaction: the emoji" },
|
|
85
|
+
url: { value: true, help: "attachment/session: the URL" },
|
|
86
|
+
"plan-file": { value: true, help: "session: a JSON file holding the plan" },
|
|
87
|
+
activity: { value: true, help: "session: one activity line" },
|
|
88
|
+
},
|
|
89
|
+
ask: {
|
|
90
|
+
team: { value: true, help: "raise: team key" },
|
|
91
|
+
title: { value: true, help: "raise: the question" },
|
|
92
|
+
context: { value: true, help: "raise: context paragraph" },
|
|
93
|
+
option: { value: true, repeat: true, help: "raise: an option (repeatable)" },
|
|
94
|
+
default: { value: true, help: "raise: the default if silent" },
|
|
95
|
+
blocks: { value: true, repeat: true, help: "raise: a ticket this decision blocks (repeatable)" },
|
|
96
|
+
"nothing-to-block": { value: false, help: "raise: declare that nothing is blocked" },
|
|
97
|
+
anyone: { value: false, help: "list: every open ask in the tenant, not only the ones assigned to you" },
|
|
98
|
+
"ask-key": { value: true, help: "raise: idempotency key" },
|
|
99
|
+
answer: { value: true, help: "accept: the answering comment id" },
|
|
100
|
+
role: { value: true, help: "accept: the role recording the answer" },
|
|
101
|
+
},
|
|
102
|
+
ready: {
|
|
103
|
+
offline: { value: false, help: "skip the published-release check (no network)" },
|
|
104
|
+
},
|
|
105
|
+
accounts: {},
|
|
106
|
+
environment: {
|
|
107
|
+
file: { value: true, help: "propose: a JSON file holding the declaration" },
|
|
108
|
+
stdin: { value: false, help: "propose: read the declaration from stdin" },
|
|
109
|
+
"expect-revision": { value: true, help: "propose: refuse unless the stored declaration is still at this revision" },
|
|
110
|
+
approve: { value: false, help: "propose: approve exactly the revision the propose returned" },
|
|
111
|
+
revision: { value: true, help: "approve: the revision to approve (with --hash; default is whatever read returns)" },
|
|
112
|
+
hash: { value: true, help: "approve: the canonical hash to approve (with --revision)" },
|
|
113
|
+
},
|
|
114
|
+
secret: {
|
|
115
|
+
repo: { value: true, help: "the repository, as owner/name (required)" },
|
|
116
|
+
command: { value: true, help: "set: run this command on this machine and store its output (e.g. 'op read op://Vault/item/field'); the command text is audited, so never put a value in it" },
|
|
117
|
+
rotate: { value: true, repeat: true, help: "import: replace this name if it is already set (repeatable)" },
|
|
118
|
+
},
|
|
119
|
+
release: {
|
|
120
|
+
because: { value: true, help: "what changed since the ticket was held (required unless --dry-run)" },
|
|
121
|
+
"retry-unchanged": { value: false, help: "release even though nothing the mirror can see changed (say what did in --because)" },
|
|
122
|
+
"dry-run": { value: false, help: "show what a release would clear and refuse, and change nothing" },
|
|
123
|
+
class: { value: true, help: "release every ticket on --team parked under this failure class" },
|
|
124
|
+
team: { value: true, help: "with --class: the team key" },
|
|
125
|
+
limit: { value: true, help: "with --class: at most this many tickets (the cloud caps it at 25)" },
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
export const VERB_USAGE = {
|
|
129
|
+
login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
|
|
130
|
+
install: "install [--skills-dir <dir>] [--force]",
|
|
131
|
+
status: "status",
|
|
132
|
+
notice: "notice",
|
|
133
|
+
me: "me [--json]",
|
|
134
|
+
contract: "contract [--refresh] [--path <a.b.c>] [--json]",
|
|
135
|
+
query: "query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>> [--team K] [--project P] [--state S] [--limit N] [--all] [--source replica|api] [--json]",
|
|
136
|
+
replica: "replica <start [--detach]|stop|status [--probe] [--json]|sql \"<select>\"|schema [table]> [--db <path>]",
|
|
137
|
+
runtime: "runtime <status [--json]|install|path|uninstall>",
|
|
138
|
+
events: "events <tail|wait-for|query> [--type NAME] [--ticket CTC-N] [--after SEQUENCE] [--limit N] [--timeout SECONDS] [--directory PATH]",
|
|
139
|
+
explain: "explain <ticket> [--history] [--json]",
|
|
140
|
+
history: "history <ticket> [--json]",
|
|
141
|
+
running: "running [--ticket T --phase P] [--json]",
|
|
142
|
+
queue: "queue [--team K] [--json]",
|
|
143
|
+
watch: "watch [--team K] [--ticket T]... [--project P] [--exec CMD] [--cursor-file <path>] [--from cursor|head]",
|
|
144
|
+
write: "write <comment <ticket> --body|--stdin [--parent] [--bookkeeping] [--as-user] | state <ticket> --slot|--state-id|--state-type | label <ticket> --add... --remove... | create --team --title [--label] [--priority] | reaction <ticket>|--comment <id> --emoji <e> | attachment <ticket> --title --url | session <ticket> [--title] [--plan-file] [--activity]>",
|
|
145
|
+
ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] | accept <askTicket> --answer <commentId> --role <role> | list [--anyone] [--json]>",
|
|
146
|
+
ready: "ready [--json] [--offline]",
|
|
147
|
+
accounts: "accounts [--json]",
|
|
148
|
+
environment: "environment [read] [--json] | environment propose --file <path>|--stdin [--expect-revision N] [--approve] [--json] | environment approve [--revision N --hash H] [--json]",
|
|
149
|
+
secret: "secret set <NAME> --repo <owner/name> [--command '<cmd>'] [--json] (value from --command, stdin, or a hidden prompt) | secret import <file> --repo <owner/name> [--rotate NAME]... [--json]",
|
|
150
|
+
release: "release <ticket> --because <what changed> [--retry-unchanged] [--dry-run] [--json] | release --class <failure-class> --team <K> --because <what changed> [--retry-unchanged] [--dry-run] [--limit N] [--json]",
|
|
151
|
+
};
|
|
152
|
+
export function parseArgs(argv) {
|
|
153
|
+
const out = {
|
|
154
|
+
command: null,
|
|
155
|
+
subcommand: null,
|
|
156
|
+
rest: [],
|
|
157
|
+
flags: {},
|
|
158
|
+
force: false,
|
|
159
|
+
help: false,
|
|
160
|
+
version: false,
|
|
161
|
+
json: false,
|
|
162
|
+
};
|
|
163
|
+
const positionals = [];
|
|
164
|
+
// The per-verb table is switched in the moment the command positional is seen, so a verb's own
|
|
165
|
+
// flags are accepted after it and a global flag is accepted anywhere.
|
|
166
|
+
let table = { ...GLOBAL_FLAGS };
|
|
167
|
+
for (let i = 0; i < argv.length; i++) {
|
|
168
|
+
const a = argv[i];
|
|
169
|
+
if (a === "-h" || a === "--help") {
|
|
170
|
+
out.help = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (a === "-V" || a === "--version") {
|
|
174
|
+
out.version = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (a === "--") {
|
|
178
|
+
positionals.push(...argv.slice(i + 1));
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (a.startsWith("--")) {
|
|
182
|
+
let name = a.slice(2);
|
|
183
|
+
let inlineValue;
|
|
184
|
+
const eq = name.indexOf("=");
|
|
185
|
+
if (eq !== -1) {
|
|
186
|
+
inlineValue = name.slice(eq + 1);
|
|
187
|
+
name = name.slice(0, eq);
|
|
188
|
+
}
|
|
189
|
+
const spec = table[name];
|
|
190
|
+
if (!spec) {
|
|
191
|
+
throw new UsageError(out.command ? `unknown option for ${out.command}: --${name}` : `unknown option: --${name}`);
|
|
192
|
+
}
|
|
193
|
+
if (spec.value) {
|
|
194
|
+
const v = inlineValue ?? requireValue(argv, ++i, `--${name}`);
|
|
195
|
+
if (spec.repeat) {
|
|
196
|
+
const prev = out.flags[name];
|
|
197
|
+
out.flags[name] = Array.isArray(prev) ? [...prev, v] : [v];
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
out.flags[name] = v;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (inlineValue !== undefined)
|
|
205
|
+
throw new UsageError(`--${name} takes no value`);
|
|
206
|
+
out.flags[name] = true;
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (a.startsWith("-") && a.length > 1)
|
|
211
|
+
throw new UsageError(`unknown option: ${a}`);
|
|
212
|
+
positionals.push(a);
|
|
213
|
+
if (positionals.length === 1) {
|
|
214
|
+
out.command = a;
|
|
215
|
+
table = { ...GLOBAL_FLAGS, ...(FLAG_TABLES[a] ?? {}) };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
out.command = positionals[0] ?? null;
|
|
219
|
+
out.subcommand = positionals[1] ?? null;
|
|
220
|
+
out.rest = positionals.slice(2);
|
|
221
|
+
if (typeof out.flags.key === "string")
|
|
222
|
+
out.key = out.flags.key;
|
|
223
|
+
if (typeof out.flags["base-url"] === "string")
|
|
224
|
+
out.baseUrl = out.flags["base-url"];
|
|
225
|
+
if (typeof out.flags["skills-dir"] === "string")
|
|
226
|
+
out.skillsDir = out.flags["skills-dir"];
|
|
227
|
+
out.force = out.flags.force === true;
|
|
228
|
+
out.json = out.flags.json === true;
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
function requireValue(argv, i, flag) {
|
|
232
|
+
const v = argv[i];
|
|
233
|
+
if (v === undefined || v === "")
|
|
234
|
+
throw new UsageError(`${flag} requires a value`);
|
|
235
|
+
return v;
|
|
236
|
+
}
|
|
237
|
+
/** The positional arguments after the command: `[subcommand, ...rest]` with nulls dropped. */
|
|
238
|
+
export function positionals(args) {
|
|
239
|
+
return args.subcommand === null ? [] : [args.subcommand, ...args.rest];
|
|
240
|
+
}
|
|
241
|
+
export function flagString(args, name) {
|
|
242
|
+
const v = args.flags[name];
|
|
243
|
+
return typeof v === "string" ? v : undefined;
|
|
244
|
+
}
|
|
245
|
+
export function flagList(args, name) {
|
|
246
|
+
const v = args.flags[name];
|
|
247
|
+
if (Array.isArray(v))
|
|
248
|
+
return v;
|
|
249
|
+
return typeof v === "string" ? [v] : [];
|
|
250
|
+
}
|
|
251
|
+
export function flagBool(args, name) {
|
|
252
|
+
return args.flags[name] === true;
|
|
253
|
+
}
|
|
254
|
+
export function flagInt(args, name, fallback) {
|
|
255
|
+
const v = flagString(args, name);
|
|
256
|
+
if (v === undefined)
|
|
257
|
+
return fallback;
|
|
258
|
+
const n = Number(v);
|
|
259
|
+
if (!Number.isInteger(n))
|
|
260
|
+
throw new UsageError(`--${name} must be an integer`);
|
|
261
|
+
return n;
|
|
262
|
+
}
|
|
263
|
+
/** The help text for one verb: usage line, then every flag in its table and the global ones. */
|
|
264
|
+
export function verbHelp(verb) {
|
|
265
|
+
const lines = [`Usage: catalyst-skills ${VERB_USAGE[verb] ?? verb}`, ""];
|
|
266
|
+
const table = FLAG_TABLES[verb] ?? {};
|
|
267
|
+
const names = Object.keys(table);
|
|
268
|
+
if (names.length > 0) {
|
|
269
|
+
lines.push("Options:");
|
|
270
|
+
for (const name of names)
|
|
271
|
+
lines.push(` --${name}${table[name].value ? " <value>" : ""} ${table[name].help}`);
|
|
272
|
+
lines.push("");
|
|
273
|
+
}
|
|
274
|
+
lines.push("Global options:");
|
|
275
|
+
for (const name of Object.keys(GLOBAL_FLAGS)) {
|
|
276
|
+
lines.push(` --${name}${GLOBAL_FLAGS[name].value ? " <value>" : ""} ${GLOBAL_FLAGS[name].help}`);
|
|
277
|
+
}
|
|
278
|
+
lines.push(" -h, --help this text", " -V, --version the bundle version");
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
package/dist/ask.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// ask.ts — `ask raise|accept|list`: raise a decision through the contract's ask route (the cloud
|
|
2
|
+
// renders the body from its own template, so this passes fields, never headings), record an answer
|
|
3
|
+
// through ask-accept, and list open asks ranked by the work each one holds.
|
|
4
|
+
import { flagBool, flagList, flagString, positionals } from "./args.js";
|
|
5
|
+
import { requireConfig } from "./config.js";
|
|
6
|
+
import { loadContract, teamByKey } from "./contract.js";
|
|
7
|
+
import { UsageError } from "./errors.js";
|
|
8
|
+
import { apiClient } from "./transport.js";
|
|
9
|
+
import { fetchAllPages } from "./pagination.js";
|
|
10
|
+
import { fetchWorkflowStates, postAgent, resolveIssue } from "./write.js";
|
|
11
|
+
export async function cmdAsk(args, ctx) {
|
|
12
|
+
const [sub, ...rest] = positionals(args);
|
|
13
|
+
if (!sub)
|
|
14
|
+
throw new UsageError("ask needs a subcommand: raise | accept | list");
|
|
15
|
+
if (sub === "raise" && !flagBool(args, "nothing-to-block") && flagList(args, "blocks").length === 0) {
|
|
16
|
+
throw new UsageError("ask raise needs --blocks <ticket>... (what this decision holds) or --nothing-to-block");
|
|
17
|
+
}
|
|
18
|
+
const cfg = requireConfig(ctx);
|
|
19
|
+
const { doc } = await loadContract(ctx, cfg);
|
|
20
|
+
const api = apiClient(cfg, ctx);
|
|
21
|
+
switch (sub) {
|
|
22
|
+
case "raise":
|
|
23
|
+
return raise(args, ctx, doc, api);
|
|
24
|
+
case "accept":
|
|
25
|
+
return accept(args, ctx, doc, api, rest);
|
|
26
|
+
case "list":
|
|
27
|
+
return list(args, ctx, doc, api, cfg);
|
|
28
|
+
default:
|
|
29
|
+
throw new UsageError(`unknown ask subcommand: ${sub}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function raise(args, ctx, doc, api) {
|
|
33
|
+
const teamKey = flagString(args, "team");
|
|
34
|
+
const title = flagString(args, "title");
|
|
35
|
+
if (!teamKey || !title)
|
|
36
|
+
throw new UsageError("ask raise needs --team <key> and --title <question>");
|
|
37
|
+
const team = teamByKey(doc, teamKey);
|
|
38
|
+
const options = flagList(args, "option");
|
|
39
|
+
if (options.length > doc.askTemplate.maxLetteredOptions) {
|
|
40
|
+
throw new UsageError(`ask raise takes at most ${doc.askTemplate.maxLetteredOptions} options`);
|
|
41
|
+
}
|
|
42
|
+
const blocks = [];
|
|
43
|
+
for (const t of flagList(args, "blocks"))
|
|
44
|
+
blocks.push((await resolveIssue(api, t)).id);
|
|
45
|
+
const body = { title, teamId: team.id };
|
|
46
|
+
if (flagString(args, "context"))
|
|
47
|
+
body.context = flagString(args, "context");
|
|
48
|
+
if (options.length)
|
|
49
|
+
body.options = options;
|
|
50
|
+
if (flagString(args, "default"))
|
|
51
|
+
body.defaultIfSilent = flagString(args, "default");
|
|
52
|
+
if (blocks.length)
|
|
53
|
+
body.blocks = blocks;
|
|
54
|
+
if (flagBool(args, "nothing-to-block"))
|
|
55
|
+
body.nothingToBlock = true;
|
|
56
|
+
if (flagString(args, "ask-key"))
|
|
57
|
+
body.askKey = flagString(args, "ask-key");
|
|
58
|
+
const result = await postAgent(api, doc, "ask", body);
|
|
59
|
+
ctx.stdout(args.json ? JSON.stringify(result) : `ask raised: ${String(result.identifier ?? result.id ?? "ok")}${blocks.length ? ` (blocks ${blocks.length})` : ""}`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
async function accept(args, ctx, doc, api, rest) {
|
|
63
|
+
const askTicket = rest[0];
|
|
64
|
+
const answer = flagString(args, "answer");
|
|
65
|
+
const role = flagString(args, "role");
|
|
66
|
+
if (!askTicket || !answer || !role)
|
|
67
|
+
throw new UsageError("ask accept needs <askTicket> --answer <commentId> --role <role>");
|
|
68
|
+
const ask = await resolveIssue(api, askTicket);
|
|
69
|
+
const result = await postAgent(api, doc, "ask-accept", {
|
|
70
|
+
askIssueId: ask.id,
|
|
71
|
+
answerCommentId: answer,
|
|
72
|
+
acceptedByRole: role,
|
|
73
|
+
});
|
|
74
|
+
ctx.stdout(args.json ? JSON.stringify(result) : `ask ${ask.identifier}: answer ${answer} recorded by ${role}`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
/** Decide the inbox scope from the connected identity and the `--anyone` flag. A personal key with a
|
|
78
|
+
* matched Linear identity reads "mine" by default; `--anyone` widens; an unmatched identity or a
|
|
79
|
+
* host key cannot mean "me" and says so rather than filtering to nothing. */
|
|
80
|
+
export function inboxScope(cfg, anyone) {
|
|
81
|
+
if (anyone)
|
|
82
|
+
return { kind: "anyone" };
|
|
83
|
+
if (!cfg.user)
|
|
84
|
+
return { kind: "no-person" };
|
|
85
|
+
if (!cfg.user.linearUserId)
|
|
86
|
+
return { kind: "unmatched", label: cfg.user.label };
|
|
87
|
+
return { kind: "mine", linearUserId: cfg.user.linearUserId, label: cfg.user.label };
|
|
88
|
+
}
|
|
89
|
+
/** Rank open asks by the priority-weighted count of open tickets each one blocks. Priority 1 (urgent)
|
|
90
|
+
* weighs 4, 4 (low) weighs 1, 0 (none) weighs 1. */
|
|
91
|
+
export function rankAsks(issues, doc, openState) {
|
|
92
|
+
const askIds = new Set();
|
|
93
|
+
for (const team of doc.teams)
|
|
94
|
+
for (const l of team.labels.ask)
|
|
95
|
+
for (const id of [l.unscopedId, l.teamScopedId, l.preferredId])
|
|
96
|
+
if (id)
|
|
97
|
+
askIds.add(id);
|
|
98
|
+
const isAsk = (issue) => {
|
|
99
|
+
const labels = Array.isArray(issue.labels) ? issue.labels : [];
|
|
100
|
+
return labels.some((l) => (typeof l.id === "string" && askIds.has(l.id)) || (typeof l.name === "string" && (l.name === doc.vocabulary.askMarkerLabel || l.name.startsWith(doc.vocabulary.askLabelPrefix))));
|
|
101
|
+
};
|
|
102
|
+
const byIdentifier = new Map();
|
|
103
|
+
for (const i of issues)
|
|
104
|
+
if (typeof i.identifier === "string")
|
|
105
|
+
byIdentifier.set(i.identifier, i);
|
|
106
|
+
const weight = (issue) => {
|
|
107
|
+
const p = typeof issue?.priority === "number" ? issue.priority : 0;
|
|
108
|
+
return p >= 1 && p <= 4 ? 5 - p : 1;
|
|
109
|
+
};
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const issue of issues) {
|
|
112
|
+
if (!isAsk(issue) || !openState(issue))
|
|
113
|
+
continue;
|
|
114
|
+
const relations = Array.isArray(issue.relations) ? issue.relations : [];
|
|
115
|
+
const blocked = relations
|
|
116
|
+
.filter((r) => r.type === "blocks" && r.issue_identifier === issue.identifier && typeof r.related_identifier === "string")
|
|
117
|
+
.map((r) => r.related_identifier)
|
|
118
|
+
.filter((id) => {
|
|
119
|
+
const t = byIdentifier.get(id);
|
|
120
|
+
return !t || openState(t);
|
|
121
|
+
});
|
|
122
|
+
out.push({
|
|
123
|
+
identifier: String(issue.identifier),
|
|
124
|
+
title: String(issue.title ?? ""),
|
|
125
|
+
state: String(issue.state ?? ""),
|
|
126
|
+
blocks: blocked,
|
|
127
|
+
score: blocked.reduce((sum, id) => sum + weight(byIdentifier.get(id)), 0),
|
|
128
|
+
assigneeId: typeof issue.assignee_id === "string" && issue.assignee_id !== "" ? issue.assignee_id : null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return out.sort((a, b) => b.score - a.score || a.identifier.localeCompare(b.identifier));
|
|
132
|
+
}
|
|
133
|
+
async function list(args, ctx, doc, api, cfg) {
|
|
134
|
+
// ⛔ THE INBOX IS THE WHOLE SCOPE, NOT A PAGE. The mirror caps every page at 500 rows regardless of
|
|
135
|
+
// the requested limit, so a single `limit: 500` read drops every ask past the cap — and, worse,
|
|
136
|
+
// scores the asks it does show against a partial view of what they block (rankAsks below).
|
|
137
|
+
const [inbox, states] = await Promise.all([fetchAllPages(api, "/api/v1/issues", {}), fetchWorkflowStates(api)]);
|
|
138
|
+
const issues = inbox.rows;
|
|
139
|
+
const terminal = new Set(states.filter((s) => ["completed", "canceled", "cancelled"].includes(s.type.toLowerCase())).map((s) => s.name.toLowerCase()));
|
|
140
|
+
const openState = (issue) => !terminal.has(String(issue.state ?? "").toLowerCase());
|
|
141
|
+
const all = rankAsks(issues, doc, openState);
|
|
142
|
+
const scope = inboxScope(cfg, flagBool(args, "anyone"));
|
|
143
|
+
const ranked = scope.kind === "mine" ? all.filter((a) => a.assigneeId === scope.linearUserId) : all;
|
|
144
|
+
if (args.json) {
|
|
145
|
+
ctx.stdout(JSON.stringify({ scope, asks: ranked }));
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
if (scope.kind === "unmatched") {
|
|
149
|
+
ctx.stderr(`[catalyst-skills] your Linear identity is not matched yet (an admin matches it in Settings → Members), so this is every open ask, not only yours`);
|
|
150
|
+
}
|
|
151
|
+
else if (scope.kind === "no-person") {
|
|
152
|
+
ctx.stderr(`[catalyst-skills] connected with the tenant's account key, which names no person — this is every open ask; log in with your personal key to see only yours`);
|
|
153
|
+
}
|
|
154
|
+
if (ranked.length === 0) {
|
|
155
|
+
ctx.stdout(scope.kind === "mine" ? `no open asks assigned to ${scope.label} (${all.length} open in the tenant — add --anyone to see them)` : "no open asks");
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
for (const a of ranked)
|
|
159
|
+
ctx.stdout(`${a.identifier} holds ${a.blocks.length} ticket${a.blocks.length === 1 ? "" : "s"} (weight ${a.score})${a.blocks.length ? `: ${a.blocks.join(", ")}` : ""} ${a.title}`);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// browser.ts — open the device-flow verification page in the person's default browser. Best-effort:
|
|
2
|
+
// a machine with no browser (a headless mini, a container) simply shows the code and URL instead, so
|
|
3
|
+
// this never throws into the login flow — the caller wraps it, and a spawn failure is swallowed here.
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
/** The platform's "open this URL" command. `open` on macOS, `start` via cmd on Windows, else xdg-open. */
|
|
6
|
+
export function openBrowser(url) {
|
|
7
|
+
const [cmd, args] = process.platform === "darwin"
|
|
8
|
+
? ["open", [url]]
|
|
9
|
+
: process.platform === "win32"
|
|
10
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
11
|
+
: ["xdg-open", [url]];
|
|
12
|
+
try {
|
|
13
|
+
const child = spawn(cmd, [...args], { stdio: "ignore", detached: true });
|
|
14
|
+
child.on("error", () => { }); // no opener installed — the printed URL is the fallback
|
|
15
|
+
child.unref();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// spawn itself threw (unusual) — the code and URL on screen are the fallback
|
|
19
|
+
}
|
|
20
|
+
}
|