@zumino/cli 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -24
- package/package.json +1 -1
- package/skill/SKILL.md +59 -14
- package/src/commands/auth.js +1 -1
- package/src/commands/commands.js +26 -0
- package/src/commands/epic.js +78 -32
- package/src/commands/find.js +41 -50
- package/src/commands/project.js +147 -0
- package/src/commands/queue.js +2 -1
- package/src/commands/request.js +153 -15
- package/src/commands/skill.js +11 -1
- package/src/commands/task.js +280 -75
- package/src/commands/workspace.js +51 -0
- package/src/events.js +104 -0
- package/src/flags.js +257 -0
- package/src/help.js +259 -0
- package/src/main.js +73 -86
- package/src/output.js +17 -29
- package/src/spec.js +1015 -0
- package/src/users.js +38 -0
package/src/main.js
CHANGED
|
@@ -2,7 +2,10 @@ import { parseArgs } from "node:util";
|
|
|
2
2
|
|
|
3
3
|
import { CliError, EXIT_FAILURE } from "./errors.js";
|
|
4
4
|
import { VERSION } from "./client.js";
|
|
5
|
-
import {
|
|
5
|
+
import { note, out } from "./output.js";
|
|
6
|
+
import { command, parseOptions, subcommand } from "./spec.js";
|
|
7
|
+
import { overview, page, suggest } from "./help.js";
|
|
8
|
+
import { validateFlags } from "./flags.js";
|
|
6
9
|
|
|
7
10
|
import * as auth from "./commands/auth.js";
|
|
8
11
|
import * as queue from "./commands/queue.js";
|
|
@@ -11,54 +14,30 @@ import * as find from "./commands/find.js";
|
|
|
11
14
|
import * as task from "./commands/task.js";
|
|
12
15
|
import * as request from "./commands/request.js";
|
|
13
16
|
import * as epic from "./commands/epic.js";
|
|
17
|
+
import * as project from "./commands/project.js";
|
|
18
|
+
import * as workspace from "./commands/workspace.js";
|
|
14
19
|
import * as raw from "./commands/api.js";
|
|
15
20
|
import * as init from "./commands/init.js";
|
|
16
21
|
import * as skill from "./commands/skill.js";
|
|
17
22
|
import * as selfUpdate from "./commands/self-update.js";
|
|
23
|
+
import * as commands from "./commands/commands.js";
|
|
18
24
|
|
|
19
25
|
/*
|
|
20
26
|
* Argument parsing and dispatch.
|
|
21
27
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* **What a command is, and what flags it takes, is declared in `spec.js` and
|
|
29
|
+
* nowhere else.** This file looks the command up there, refuses a flag it does
|
|
30
|
+
* not accept, and hands the rest to the module below. The help pages are
|
|
31
|
+
* rendered from the same declaration, so there is no usage string here to fall
|
|
32
|
+
* out of step with what actually runs.
|
|
33
|
+
*
|
|
34
|
+
* The shape of the surface is `DOMAIN.md`'s rule rather than a style choice:
|
|
35
|
+
* **generic reads span every kind, and every write names its kind.** `queue`,
|
|
36
|
+
* `context` and `find` are top-level because they answer across kinds;
|
|
37
|
+
* everything that touches one kind hangs off that kind's noun, so a fourth kind
|
|
38
|
+
* adds a noun rather than widening a flat verb list every script has learned.
|
|
27
39
|
*/
|
|
28
40
|
|
|
29
|
-
const OPTIONS = {
|
|
30
|
-
// global
|
|
31
|
-
json: { type: "boolean" },
|
|
32
|
-
host: { type: "string" },
|
|
33
|
-
token: { type: "string" },
|
|
34
|
-
project: { type: "string" },
|
|
35
|
-
workspace: { type: "string" },
|
|
36
|
-
account: { type: "string" },
|
|
37
|
-
help: { type: "boolean", short: "h" },
|
|
38
|
-
version: { type: "boolean" },
|
|
39
|
-
limit: { type: "string" },
|
|
40
|
-
// reads
|
|
41
|
-
"needs-input": { type: "boolean" },
|
|
42
|
-
kind: { type: "string" },
|
|
43
|
-
status: { type: "string" },
|
|
44
|
-
state: { type: "string" },
|
|
45
|
-
activity: { type: "boolean" },
|
|
46
|
-
// writes
|
|
47
|
-
title: { type: "string" },
|
|
48
|
-
description: { type: "string" },
|
|
49
|
-
assignee: { type: "string" },
|
|
50
|
-
epic: { type: "string" },
|
|
51
|
-
plan: { type: "string" },
|
|
52
|
-
acceptance: { type: "string" },
|
|
53
|
-
url: { type: "string" },
|
|
54
|
-
body: { type: "string" },
|
|
55
|
-
to: { type: "string" },
|
|
56
|
-
// housekeeping
|
|
57
|
-
dir: { type: "string" },
|
|
58
|
-
check: { type: "boolean" },
|
|
59
|
-
yes: { type: "boolean", short: "y" },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
41
|
const COMMANDS = {
|
|
63
42
|
queue: queue.run,
|
|
64
43
|
context: context.run,
|
|
@@ -66,58 +45,23 @@ const COMMANDS = {
|
|
|
66
45
|
task: task.run,
|
|
67
46
|
request: request.run,
|
|
68
47
|
epic: epic.run,
|
|
48
|
+
project: project.run,
|
|
49
|
+
workspace: workspace.run,
|
|
69
50
|
api: raw.run,
|
|
70
51
|
auth: auth.run,
|
|
71
52
|
init: init.run,
|
|
72
53
|
skill: skill.run,
|
|
73
54
|
"self-update": selfUpdate.run,
|
|
55
|
+
commands: commands.run,
|
|
74
56
|
};
|
|
75
57
|
|
|
76
|
-
const USAGE = `${bold("zumino")} — drive Zumino from a terminal, a script, or an agent.
|
|
77
|
-
|
|
78
|
-
${bold("Reads")} (across every kind of item)
|
|
79
|
-
zumino queue [--needs-input] [--limit N] what to work on next
|
|
80
|
-
zumino context <CODE> [--activity] the whole brief for one task
|
|
81
|
-
zumino find <query> [--kind request|task|epic] [--state open|closed]
|
|
82
|
-
|
|
83
|
-
${bold("Writes")} (each names its kind)
|
|
84
|
-
zumino task create --title T [--description D] [--epic N]
|
|
85
|
-
zumino task status <CODE> <status>
|
|
86
|
-
zumino task assign <CODE> <userId|->
|
|
87
|
-
zumino task spec <CODE> --plan TEXT | --acceptance TEXT
|
|
88
|
-
zumino task comment <CODE> <text>
|
|
89
|
-
zumino task link <CODE> <blocks|blocked-by|related|answers> <CODE>
|
|
90
|
-
zumino task ref <CODE> --url URL [--title T]
|
|
91
|
-
zumino request answer <CODE|#N> <text>
|
|
92
|
-
zumino request promote <CODE|#N> --to <work-project>
|
|
93
|
-
zumino request status <CODE|#N> <status>
|
|
94
|
-
zumino request note <CODE|#N> <text>
|
|
95
|
-
zumino request comment <CODE|#N> <text>
|
|
96
|
-
zumino epic create --title T
|
|
97
|
-
zumino epic status <CODE> <status>
|
|
98
|
-
|
|
99
|
-
${bold("Everything else")}
|
|
100
|
-
zumino api <METHOD> <PATH> [--body JSON] any endpoint, including new ones
|
|
101
|
-
zumino auth login | status | list | logout
|
|
102
|
-
zumino init write .zumino.json for this repo
|
|
103
|
-
zumino skill install [--dir D] [--check] install the agent skill, globally
|
|
104
|
-
zumino self-update
|
|
105
|
-
|
|
106
|
-
${bold("Context")} is resolved in this order, and ${dim("zumino auth status")} says which won:
|
|
107
|
-
--token/--account → ZUMINO_TOKEN → .zumino.json → repo map → sole account
|
|
108
|
-
|
|
109
|
-
${bold("Flags")} --json --project P --workspace W --host URL --account NAME
|
|
110
|
-
${bold("Env")} ZUMINO_TOKEN ZUMINO_URL ZUMINO_PROJECT ZUMINO_WORKSPACE
|
|
111
|
-
ZUMINO_ACCOUNT ZUMINO_NO_UPDATE_CHECK
|
|
112
|
-
${bold("Exit")} 0 ok · 1 failed · 2 nothing resolved · 3 CLI too old`;
|
|
113
|
-
|
|
114
58
|
/** @param {string[]} argv */
|
|
115
59
|
export async function main(argv) {
|
|
116
60
|
let parsed;
|
|
117
61
|
try {
|
|
118
62
|
parsed = parseArgs({
|
|
119
63
|
args: argv,
|
|
120
|
-
options:
|
|
64
|
+
options: parseOptions(),
|
|
121
65
|
allowPositionals: true,
|
|
122
66
|
strict: true,
|
|
123
67
|
});
|
|
@@ -133,21 +77,57 @@ export async function main(argv) {
|
|
|
133
77
|
out(VERSION);
|
|
134
78
|
return 0;
|
|
135
79
|
}
|
|
80
|
+
|
|
136
81
|
const [name, ...rest] = positionals;
|
|
137
|
-
|
|
138
|
-
|
|
82
|
+
|
|
83
|
+
// `zumino`, `zumino --help`, `zumino help`, `zumino help task list`.
|
|
84
|
+
if (!name || name === "help") {
|
|
85
|
+
const words = name === "help" ? rest : [];
|
|
86
|
+
const rendered = page(words);
|
|
87
|
+
if (rendered === null) return notACommand(words.join(" "));
|
|
88
|
+
out(rendered);
|
|
139
89
|
return 0;
|
|
140
90
|
}
|
|
141
91
|
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
92
|
+
const top = command(name);
|
|
93
|
+
if (!top) return notACommand(name);
|
|
94
|
+
|
|
95
|
+
// `zumino task --help` and `zumino task list --help` are the same pages as
|
|
96
|
+
// `zumino help …`, because an agent reaches for whichever it learned first.
|
|
97
|
+
if (flags.help) {
|
|
98
|
+
out(page([name, rest[0]]) ?? page([name]) ?? overview());
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* A group needs a subcommand, and saying which ones exist is more useful than
|
|
104
|
+
* repeating that one is missing. The page goes to stdout because it is the
|
|
105
|
+
* answer to "what can this do"; the refusal goes to stderr and the exit code
|
|
106
|
+
* is a failure, because the command as typed did nothing.
|
|
107
|
+
*/
|
|
108
|
+
let leaf = top;
|
|
109
|
+
let path = top.name;
|
|
110
|
+
if (top.subcommands) {
|
|
111
|
+
const sub = rest[0] ?? top.defaultSub;
|
|
112
|
+
const found = sub ? subcommand(top, sub) : null;
|
|
113
|
+
if (!found) {
|
|
114
|
+
note(
|
|
115
|
+
sub
|
|
116
|
+
? `zumino: ${top.name} has no subcommand "${sub}".`
|
|
117
|
+
: `zumino: ${top.name} needs a subcommand.`,
|
|
118
|
+
);
|
|
119
|
+
out(page([top.name]));
|
|
120
|
+
return EXIT_FAILURE;
|
|
121
|
+
}
|
|
122
|
+
leaf = found;
|
|
123
|
+
path = `${top.name} ${sub}`;
|
|
147
124
|
}
|
|
148
125
|
|
|
149
126
|
try {
|
|
150
|
-
|
|
127
|
+
// Before the command runs, not inside it: a flag this command does not take
|
|
128
|
+
// is a refusal, never a value that reaches the wire and is ignored there.
|
|
129
|
+
validateFlags(leaf, flags, path);
|
|
130
|
+
return (await COMMANDS[name](rest, flags)) ?? 0;
|
|
151
131
|
} catch (err) {
|
|
152
132
|
if (err instanceof CliError) {
|
|
153
133
|
note(`zumino: ${err.message}`);
|
|
@@ -161,3 +141,10 @@ export async function main(argv) {
|
|
|
161
141
|
return EXIT_FAILURE;
|
|
162
142
|
}
|
|
163
143
|
}
|
|
144
|
+
|
|
145
|
+
function notACommand(name) {
|
|
146
|
+
note(`zumino: unknown command "${name}".`);
|
|
147
|
+
const guess = suggest(name);
|
|
148
|
+
note(guess ? ` Did you mean: ${guess}` : " Try: zumino --help");
|
|
149
|
+
return EXIT_FAILURE;
|
|
150
|
+
}
|
package/src/output.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { CliError } from "./errors.js";
|
|
2
1
|
import { styleText } from "node:util";
|
|
3
2
|
|
|
4
3
|
/*
|
|
@@ -158,35 +157,24 @@ export function workspaceSlug(value) {
|
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
/**
|
|
161
|
-
*
|
|
160
|
+
* What the caller is looking at, and how to see the rest of it.
|
|
162
161
|
*
|
|
163
|
-
* `
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (value === undefined || value === null || value === "") return undefined;
|
|
170
|
-
const n = Number(String(value).trim());
|
|
171
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
172
|
-
throw new CliError(`--${flag} takes a positive whole number, not "${value}".`);
|
|
173
|
-
}
|
|
174
|
-
return n;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* An explicitly-given flag value, where empty means "clear it", not "absent".
|
|
179
|
-
*
|
|
180
|
-
* `parseArgs` reports `--description ""` and `--description=` as `""`, which is
|
|
181
|
-
* falsy — so a truthiness gate dropped the flag and the field was left untouched
|
|
182
|
-
* instead of cleared. `config.js` already states the rule for credentials ("an
|
|
183
|
-
* explicit empty value is an error, not an absence"); this is the same
|
|
184
|
-
* distinction for fields where empty is a legitimate instruction rather than a
|
|
185
|
-
* mistake.
|
|
162
|
+
* `total` on a paged read is the unpaginated match count, so a page that is a
|
|
163
|
+
* slice of a larger answer has to say so: a table that stops at fifty rows and
|
|
164
|
+
* says nothing reads as the whole result, which is exactly how an agent told to
|
|
165
|
+
* check for duplicates first decides there are none. The next page is named as
|
|
166
|
+
* the flag that fetches it, because the useful answer to "there is more" is the
|
|
167
|
+
* command.
|
|
186
168
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
169
|
+
* @param {{total?: unknown}} res the response envelope
|
|
170
|
+
* @param {number} shown rows printed
|
|
171
|
+
* @param {number|undefined} offset the offset this page was read at
|
|
189
172
|
*/
|
|
190
|
-
export function
|
|
191
|
-
|
|
173
|
+
export function pageLine(res, shown, offset = 0) {
|
|
174
|
+
const total = typeof res?.total === "number" ? res.total : null;
|
|
175
|
+
const from = offset ?? 0;
|
|
176
|
+
if (total === null) return `${shown} shown.`;
|
|
177
|
+
const last = from + shown;
|
|
178
|
+
if (last >= total) return from === 0 ? `${shown} of ${total}.` : `${from + 1}–${last} of ${total}.`;
|
|
179
|
+
return `${from + 1}–${last} of ${total} — next page: --offset ${last}`;
|
|
192
180
|
}
|