@uic-coe-connect/cli 0.12.0 → 0.13.1
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/dist/client.js +108 -23
- package/dist/commands/access.js +8 -8
- package/dist/commands/auth.js +14 -5
- package/dist/commands/deploy.js +8 -10
- package/dist/commands/env.js +3 -3
- package/dist/commands/logs.js +1 -1
- package/dist/commands/pipelines.js +5 -5
- package/dist/commands/reports.js +8 -8
- package/dist/commands/resources.js +5 -5
- package/dist/commands/tips.js +4 -4
- package/dist/index.js +16 -5
- package/dist/output.d.ts +17 -0
- package/dist/output.js +86 -6
- package/dist/registry.d.ts +8 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -25,8 +25,44 @@ async function fail(res, server) {
|
|
|
25
25
|
}
|
|
26
26
|
if (res.status === 404)
|
|
27
27
|
throw new CliError(`Not found: ${message}`, 4);
|
|
28
|
+
// A refusal the caller can act on: bad input (400), something that can't be
|
|
29
|
+
// done in the current state (409, e.g. "this COEConnect doesn't manage that
|
|
30
|
+
// VM" or "nothing is deploying"), or input the server understood and
|
|
31
|
+
// rejected (422). These used to fall through to the generic 1, which is not
|
|
32
|
+
// in the documented table at all — so a script told to branch on exit codes
|
|
33
|
+
// got an undocumented one for the most common recoverable failures.
|
|
34
|
+
if ([400, 409, 422].includes(res.status))
|
|
35
|
+
throw new CliError(message, 6);
|
|
28
36
|
throw new CliError(message, 1);
|
|
29
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* How long a plain request may take before the CLI gives up.
|
|
40
|
+
*
|
|
41
|
+
* Generous — some reads go out to a VM agent and back — but finite. Without a
|
|
42
|
+
* limit a server that accepts the connection and then never answers leaves the
|
|
43
|
+
* CLI waiting for ever, printing nothing: no spinner, no error, no exit. That
|
|
44
|
+
* is what `coe auth list` did in production on 2026-09-22, and the silence
|
|
45
|
+
* made it look like the CLI itself was broken rather than one route on the
|
|
46
|
+
* server.
|
|
47
|
+
*/
|
|
48
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
49
|
+
/** Uploads and downloads move real files, so they get longer. */
|
|
50
|
+
const TRANSFER_TIMEOUT_MS = 120_000;
|
|
51
|
+
/**
|
|
52
|
+
* A deploy stream has no overall limit — a pipeline legitimately runs for
|
|
53
|
+
* minutes — so it is policed on *silence* instead. The server heartbeats every
|
|
54
|
+
* ten seconds precisely so a quiet build still looks alive, which makes a long
|
|
55
|
+
* gap a real failure rather than a slow step.
|
|
56
|
+
*/
|
|
57
|
+
const STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
58
|
+
/** Turns a timeout into something that says what to do next. */
|
|
59
|
+
function timeoutError(server, seconds) {
|
|
60
|
+
return new CliError(`${server} did not respond within ${seconds}s.`, 5, "The server accepted the connection but never answered. Try again, and if it persists " +
|
|
61
|
+
"check the server's logs — a route that throws can stop responding without closing.");
|
|
62
|
+
}
|
|
63
|
+
function isAbort(error) {
|
|
64
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
65
|
+
}
|
|
30
66
|
/**
|
|
31
67
|
* Spread into a fetch init. Omits `body` entirely rather than passing
|
|
32
68
|
* `undefined` — fetch rejects a body on GET even when it's undefined.
|
|
@@ -49,9 +85,16 @@ export function createClient(options = {}) {
|
|
|
49
85
|
async request(method, path, body) {
|
|
50
86
|
let res;
|
|
51
87
|
try {
|
|
52
|
-
res = await fetch(`${server}${path}`, {
|
|
88
|
+
res = await fetch(`${server}${path}`, {
|
|
89
|
+
method,
|
|
90
|
+
headers,
|
|
91
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
92
|
+
...bodyInit(body),
|
|
93
|
+
});
|
|
53
94
|
}
|
|
54
95
|
catch (error) {
|
|
96
|
+
if (isAbort(error))
|
|
97
|
+
throw timeoutError(server, REQUEST_TIMEOUT_MS / 1000);
|
|
55
98
|
throw new CliError(`Could not reach ${server}: ${error instanceof Error ? error.message : String(error)}`, 5, "Check the server URL, and that you're on the campus network or VPN.");
|
|
56
99
|
}
|
|
57
100
|
if (!res.ok)
|
|
@@ -72,40 +115,82 @@ export function createClient(options = {}) {
|
|
|
72
115
|
"X-File-Name": encodeURIComponent(filename),
|
|
73
116
|
},
|
|
74
117
|
body: bytes,
|
|
118
|
+
signal: AbortSignal.timeout(TRANSFER_TIMEOUT_MS),
|
|
119
|
+
}).catch((error) => {
|
|
120
|
+
if (isAbort(error))
|
|
121
|
+
throw timeoutError(server, TRANSFER_TIMEOUT_MS / 1000);
|
|
122
|
+
throw error;
|
|
75
123
|
});
|
|
76
124
|
if (!res.ok)
|
|
77
125
|
await fail(res, server);
|
|
78
126
|
return (await res.json());
|
|
79
127
|
},
|
|
80
128
|
async download(path) {
|
|
81
|
-
const res = await fetch(`${server}${path}`, {
|
|
129
|
+
const res = await fetch(`${server}${path}`, {
|
|
130
|
+
headers,
|
|
131
|
+
signal: AbortSignal.timeout(TRANSFER_TIMEOUT_MS),
|
|
132
|
+
}).catch((error) => {
|
|
133
|
+
if (isAbort(error))
|
|
134
|
+
throw timeoutError(server, TRANSFER_TIMEOUT_MS / 1000);
|
|
135
|
+
throw error;
|
|
136
|
+
});
|
|
82
137
|
if (!res.ok)
|
|
83
138
|
await fail(res, server);
|
|
84
139
|
return new Uint8Array(await res.arrayBuffer());
|
|
85
140
|
},
|
|
86
141
|
async stream(method, path, body, onLine) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
142
|
+
// No overall deadline here, only an idle one: the whole point of this
|
|
143
|
+
// call is to sit open while a deploy runs.
|
|
144
|
+
const idle = new AbortController();
|
|
145
|
+
let watchdog;
|
|
146
|
+
const resetWatchdog = () => {
|
|
147
|
+
if (watchdog)
|
|
148
|
+
clearTimeout(watchdog);
|
|
149
|
+
watchdog = setTimeout(() => idle.abort(), STREAM_IDLE_TIMEOUT_MS);
|
|
150
|
+
watchdog.unref?.();
|
|
151
|
+
};
|
|
152
|
+
resetWatchdog();
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(`${server}${path}`, {
|
|
155
|
+
method,
|
|
156
|
+
headers,
|
|
157
|
+
signal: idle.signal,
|
|
158
|
+
...bodyInit(body),
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok)
|
|
161
|
+
await fail(res, server);
|
|
162
|
+
if (!res.body)
|
|
163
|
+
throw new CliError("The server sent no output stream.");
|
|
164
|
+
// The deploy endpoint writes plain newline-delimited text, not SSE, so
|
|
165
|
+
// buffer across chunk boundaries rather than assuming whole lines.
|
|
166
|
+
const reader = res.body.getReader();
|
|
167
|
+
const decoder = new TextDecoder();
|
|
168
|
+
let buffer = "";
|
|
169
|
+
for (;;) {
|
|
170
|
+
const { done, value } = await reader.read();
|
|
171
|
+
if (done)
|
|
172
|
+
break;
|
|
173
|
+
resetWatchdog();
|
|
174
|
+
buffer += decoder.decode(value, { stream: true });
|
|
175
|
+
const lines = buffer.split("\n");
|
|
176
|
+
buffer = lines.pop() ?? "";
|
|
177
|
+
for (const line of lines)
|
|
178
|
+
onLine(line);
|
|
179
|
+
}
|
|
180
|
+
if (buffer)
|
|
181
|
+
onLine(buffer);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (isAbort(error)) {
|
|
185
|
+
throw new CliError(`${server} sent nothing for ${STREAM_IDLE_TIMEOUT_MS / 1000}s.`, 5, "A running deploy heartbeats every 10s, so this means the connection died — " +
|
|
186
|
+
"the deploy itself may still be going. Check: coe builds <app>");
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (watchdog)
|
|
192
|
+
clearTimeout(watchdog);
|
|
106
193
|
}
|
|
107
|
-
if (buffer)
|
|
108
|
-
onLine(buffer);
|
|
109
194
|
},
|
|
110
195
|
};
|
|
111
196
|
}
|
package/dist/commands/access.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "../client.js";
|
|
2
|
-
import { details, emit, info } from "../output.js";
|
|
2
|
+
import { details, emit, info, success } from "../output.js";
|
|
3
3
|
import { register } from "../registry.js";
|
|
4
4
|
import { resolveApp } from "./apps.js";
|
|
5
5
|
function currentAccess(app) {
|
|
@@ -42,7 +42,7 @@ export function registerAccessCommands() {
|
|
|
42
42
|
const access = currentAccess(app);
|
|
43
43
|
const devTeam = [...new Set([...access.devTeam, ...toAdd])];
|
|
44
44
|
await saveAccess(c, app.id, { ...access, devTeam });
|
|
45
|
-
emit({ appId: app.id, devTeam, added: toAdd }, () =>
|
|
45
|
+
emit({ appId: app.id, devTeam, added: toAdd }, () => success(`Dev team for ${app.id}: ${devTeam.join(", ")}`));
|
|
46
46
|
},
|
|
47
47
|
}, {
|
|
48
48
|
name: "access remove-dev",
|
|
@@ -60,7 +60,7 @@ export function registerAccessCommands() {
|
|
|
60
60
|
throw new CliError(`That removes everyone from ${app.id}'s dev team, leaving it webadmin-only.`, 6, "Re-run with --yes if that's intended.");
|
|
61
61
|
}
|
|
62
62
|
await saveAccess(c, app.id, { ...access, devTeam });
|
|
63
|
-
emit({ appId: app.id, devTeam, removed: [...toRemove] }, () =>
|
|
63
|
+
emit({ appId: app.id, devTeam, removed: [...toRemove] }, () => success(`Dev team for ${app.id}: ${devTeam.join(", ") || "(none)"}`));
|
|
64
64
|
},
|
|
65
65
|
}, {
|
|
66
66
|
name: "access set-staff",
|
|
@@ -79,7 +79,7 @@ export function registerAccessCommands() {
|
|
|
79
79
|
staff: { ...access.staff, everyone: mode === "everyone" },
|
|
80
80
|
};
|
|
81
81
|
await saveAccess(c, app.id, next);
|
|
82
|
-
emit({ appId: app.id, staffAccess: mode }, () =>
|
|
82
|
+
emit({ appId: app.id, staffAccess: mode }, () => success(`Staff access for ${app.id}: ${mode}`));
|
|
83
83
|
},
|
|
84
84
|
}, {
|
|
85
85
|
name: "access add-staff",
|
|
@@ -93,7 +93,7 @@ export function registerAccessCommands() {
|
|
|
93
93
|
const members = [...new Set([...access.staff.members, ...toAdd])];
|
|
94
94
|
await saveAccess(c, app.id, { ...access, staff: { ...access.staff, members } });
|
|
95
95
|
emit({ appId: app.id, members, added: toAdd }, () => {
|
|
96
|
-
|
|
96
|
+
success(`Staff members for ${app.id}: ${members.join(", ")}`);
|
|
97
97
|
if (access.staff.everyone) {
|
|
98
98
|
info(" note: staff access is still 'everyone', so this list isn't enforced yet.");
|
|
99
99
|
}
|
|
@@ -110,7 +110,7 @@ export function registerAccessCommands() {
|
|
|
110
110
|
const access = currentAccess(app);
|
|
111
111
|
const members = access.staff.members.filter((n) => !toRemove.has(n));
|
|
112
112
|
await saveAccess(c, app.id, { ...access, staff: { ...access.staff, members } });
|
|
113
|
-
emit({ appId: app.id, members, removed: [...toRemove] }, () =>
|
|
113
|
+
emit({ appId: app.id, members, removed: [...toRemove] }, () => success(`Staff members for ${app.id}: ${members.join(", ") || "(none)"}`));
|
|
114
114
|
},
|
|
115
115
|
}, {
|
|
116
116
|
name: "access block",
|
|
@@ -123,7 +123,7 @@ export function registerAccessCommands() {
|
|
|
123
123
|
const access = currentAccess(app);
|
|
124
124
|
const exceptions = [...new Set([...access.staff.exceptions, ...toAdd])];
|
|
125
125
|
await saveAccess(c, app.id, { ...access, staff: { ...access.staff, exceptions } });
|
|
126
|
-
emit({ appId: app.id, blacklist: exceptions, added: toAdd }, () =>
|
|
126
|
+
emit({ appId: app.id, blacklist: exceptions, added: toAdd }, () => success(`Blacklist for ${app.id}: ${exceptions.join(", ")}`));
|
|
127
127
|
},
|
|
128
128
|
}, {
|
|
129
129
|
name: "access unblock",
|
|
@@ -136,7 +136,7 @@ export function registerAccessCommands() {
|
|
|
136
136
|
const access = currentAccess(app);
|
|
137
137
|
const exceptions = access.staff.exceptions.filter((n) => !toRemove.has(n));
|
|
138
138
|
await saveAccess(c, app.id, { ...access, staff: { ...access.staff, exceptions } });
|
|
139
|
-
emit({ appId: app.id, blacklist: exceptions, removed: [...toRemove] }, () =>
|
|
139
|
+
emit({ appId: app.id, blacklist: exceptions, removed: [...toRemove] }, () => success(`Blacklist for ${app.id}: ${exceptions.join(", ") || "(none)"}`));
|
|
140
140
|
},
|
|
141
141
|
});
|
|
142
142
|
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { CliError, createClient } from "../client.js";
|
|
3
3
|
import { clearCredentials, credentialsPath, readCredentials, resolveServer, saveCredentials, saveServer, sessionLabel, } from "../config.js";
|
|
4
|
-
import { details, emit, info, isJsonMode } from "../output.js";
|
|
4
|
+
import { details, emit, info, isJsonMode, style, success } from "../output.js";
|
|
5
5
|
import { register } from "../registry.js";
|
|
6
6
|
/** Best-effort browser open. Never fatal — the URL is always printed too. */
|
|
7
7
|
function openBrowser(url) {
|
|
@@ -80,7 +80,7 @@ export function registerAuthCommands() {
|
|
|
80
80
|
const all = args.bool("all");
|
|
81
81
|
const result = await client.request("DELETE", `/cli/sessions${all ? "?all=true" : ""}`);
|
|
82
82
|
clearCredentials();
|
|
83
|
-
emit({ loggedOut: true, revoked: result.revoked }, () =>
|
|
83
|
+
emit({ loggedOut: true, revoked: result.revoked }, () => success(`Signed out (${result.revoked} session${result.revoked === 1 ? "" : "s"} revoked)`));
|
|
84
84
|
},
|
|
85
85
|
}, {
|
|
86
86
|
name: "whoami",
|
|
@@ -112,10 +112,19 @@ export function registerAuthCommands() {
|
|
|
112
112
|
}
|
|
113
113
|
for (const s of sessions) {
|
|
114
114
|
details([
|
|
115
|
-
["Label", s.label],
|
|
115
|
+
["Label", s.current ? `${s.label} ${style.ok("(this terminal)")}` : s.label],
|
|
116
116
|
["Created", new Date(s.createdAt).toLocaleString()],
|
|
117
117
|
["Expires", new Date(s.expiresAt).toLocaleString()],
|
|
118
|
-
[
|
|
118
|
+
[
|
|
119
|
+
"Last used",
|
|
120
|
+
// Your own row reads "never" because authenticating stamps the
|
|
121
|
+
// timestamp after this read — say so rather than look wrong.
|
|
122
|
+
s.current
|
|
123
|
+
? "now"
|
|
124
|
+
: s.lastUsedAt
|
|
125
|
+
? new Date(s.lastUsedAt).toLocaleString()
|
|
126
|
+
: "never",
|
|
127
|
+
],
|
|
119
128
|
]);
|
|
120
129
|
info("");
|
|
121
130
|
}
|
|
@@ -131,7 +140,7 @@ export function registerAuthCommands() {
|
|
|
131
140
|
}
|
|
132
141
|
const result = await client().request("DELETE", "/cli/sessions?all=true");
|
|
133
142
|
clearCredentials();
|
|
134
|
-
emit(result, () =>
|
|
143
|
+
emit(result, () => success(`Revoked ${result.revoked} session(s)`));
|
|
135
144
|
},
|
|
136
145
|
}, {
|
|
137
146
|
name: "auth status",
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "../client.js";
|
|
2
|
-
import { emit, info, isJsonMode, table } from "../output.js";
|
|
2
|
+
import { emit, info, isJsonMode, table, style } from "../output.js";
|
|
3
3
|
import { register } from "../registry.js";
|
|
4
4
|
import { resolveApp } from "./apps.js";
|
|
5
5
|
/** Sentinels the deploy stream ends with — the server's own success signal. */
|
|
@@ -11,7 +11,7 @@ const CANCELLED = "__DEPLOY_CANCELLED__";
|
|
|
11
11
|
* They're meaningless here and read as debug noise, so they never reach the
|
|
12
12
|
* console or the JSON log — the stream already carries a human line for each.
|
|
13
13
|
*/
|
|
14
|
-
const MARKER = /^__(NODE|COMMIT|PREV)__:/;
|
|
14
|
+
const MARKER = /^__(NODE|STEPS|COMMIT|PREV)__:/;
|
|
15
15
|
/**
|
|
16
16
|
* The engine's marker for a line worth remembering after the run.
|
|
17
17
|
*
|
|
@@ -173,14 +173,13 @@ export function registerDeployCommands() {
|
|
|
173
173
|
async run({ args, client }) {
|
|
174
174
|
const c = client();
|
|
175
175
|
const app = await resolveApp(c, args.arg(0, "app"));
|
|
176
|
-
const limit =
|
|
177
|
-
const offset =
|
|
178
|
-
const { builds, total } = await c.request("GET", `/registered-apps/${app.id}/builds?limit=${
|
|
179
|
-
`&offset=${Number.isFinite(offset) ? offset : 0}`);
|
|
176
|
+
const limit = args.num("limit", 20);
|
|
177
|
+
const offset = args.num("offset", 0);
|
|
178
|
+
const { builds, total } = await c.request("GET", `/registered-apps/${app.id}/builds?limit=${limit}&offset=${offset}`);
|
|
180
179
|
emit({ appId: app.id, builds, total, offset }, () => {
|
|
181
180
|
table(builds.map((b) => ({
|
|
182
181
|
started: new Date(b.startedAt).toLocaleString(),
|
|
183
|
-
status: b.status,
|
|
182
|
+
status: style.state(b.status),
|
|
184
183
|
version: b.version ?? "—",
|
|
185
184
|
pipeline: b.pipelineName ?? "—",
|
|
186
185
|
source: b.source,
|
|
@@ -240,9 +239,8 @@ export function registerDeployCommands() {
|
|
|
240
239
|
if (!branch) {
|
|
241
240
|
throw new CliError(`${app.id} has no default branch set.`, 4, "Name one with --branch, or set it on the app.");
|
|
242
241
|
}
|
|
243
|
-
const limit =
|
|
244
|
-
const { commits } = await c.request("GET", `/registered-apps/${app.id}/commits?branch=${encodeURIComponent(branch)}`
|
|
245
|
-
`&limit=${Number.isFinite(limit) ? limit : 25}`);
|
|
242
|
+
const limit = args.num("limit", 25);
|
|
243
|
+
const { commits } = await c.request("GET", `/registered-apps/${app.id}/commits?branch=${encodeURIComponent(branch)}&limit=${limit}`);
|
|
246
244
|
emit({ appId: app.id, branch, commits }, () => {
|
|
247
245
|
table(commits.map((x) => ({
|
|
248
246
|
commit: x.sha,
|
package/dist/commands/env.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { CliError } from "../client.js";
|
|
3
|
-
import { emit, info, table } from "../output.js";
|
|
3
|
+
import { emit, info, success, table } from "../output.js";
|
|
4
4
|
import { register } from "../registry.js";
|
|
5
5
|
import { resolveApp } from "./apps.js";
|
|
6
6
|
/**
|
|
@@ -95,7 +95,7 @@ export function registerEnvCommands() {
|
|
|
95
95
|
// clobbers the app's name/url/icon the way a full record PUT would.
|
|
96
96
|
const next = { ...env, ...updates };
|
|
97
97
|
await c.request("PUT", `/registered-apps/${app.id}/env`, { env: next });
|
|
98
|
-
emit({ appId: app.id, set: Object.keys(updates), count: Object.keys(next).length }, () =>
|
|
98
|
+
emit({ appId: app.id, set: Object.keys(updates), count: Object.keys(next).length }, () => success(`Set ${Object.keys(updates).join(", ")} on ${app.id}`));
|
|
99
99
|
},
|
|
100
100
|
}, {
|
|
101
101
|
name: "env unset",
|
|
@@ -116,7 +116,7 @@ export function registerEnvCommands() {
|
|
|
116
116
|
for (const key of keys)
|
|
117
117
|
delete next[key];
|
|
118
118
|
await c.request("PUT", `/registered-apps/${app.id}/env`, { env: next });
|
|
119
|
-
emit({ appId: app.id, removed: keys, count: Object.keys(next).length }, () =>
|
|
119
|
+
emit({ appId: app.id, removed: keys, count: Object.keys(next).length }, () => success(`Removed ${keys.join(", ")} from ${app.id}`));
|
|
120
120
|
},
|
|
121
121
|
});
|
|
122
122
|
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -22,7 +22,7 @@ export function registerLogCommands() {
|
|
|
22
22
|
const c = client();
|
|
23
23
|
const app = await resolveApp(c, args.arg(0, "app"));
|
|
24
24
|
const stream = args.bool("out") ? "out" : "err";
|
|
25
|
-
const tail =
|
|
25
|
+
const tail = args.num("tail", 100);
|
|
26
26
|
if (!Number.isFinite(tail) || tail < 1) {
|
|
27
27
|
throw new CliError(`--tail must be a positive number, got "${args.flag("tail")}".`, 6);
|
|
28
28
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { CliError } from "../client.js";
|
|
3
|
-
import { emit, info, table } from "../output.js";
|
|
3
|
+
import { emit, info, success, table } from "../output.js";
|
|
4
4
|
import { register } from "../registry.js";
|
|
5
5
|
import { resolveApp } from "./apps.js";
|
|
6
6
|
/**
|
|
@@ -202,7 +202,7 @@ export function registerPipelineCommands() {
|
|
|
202
202
|
pipelines.push({ id: "", name, config: { steps } });
|
|
203
203
|
const updated = await savePipelines(c, app.id, pipelines);
|
|
204
204
|
const created = (updated.pipelines ?? []).find((p) => p.name === name);
|
|
205
|
-
emit({ appId: app.id, pipeline: created }, () =>
|
|
205
|
+
emit({ appId: app.id, pipeline: created }, () => success(`Created pipeline "${name}" on ${app.id} (${steps.length} steps)`));
|
|
206
206
|
},
|
|
207
207
|
}, {
|
|
208
208
|
name: "pipelines set-steps",
|
|
@@ -217,7 +217,7 @@ export function registerPipelineCommands() {
|
|
|
217
217
|
throw new CliError("A pipeline needs at least one step.", 6);
|
|
218
218
|
const pipelines = (app.pipelines ?? []).map((p) => p.id === pipeline.id ? { ...p, config: { steps } } : p);
|
|
219
219
|
await savePipelines(c, app.id, pipelines);
|
|
220
|
-
emit({ appId: app.id, pipelineId: pipeline.id, steps }, () =>
|
|
220
|
+
emit({ appId: app.id, pipelineId: pipeline.id, steps }, () => success(`Updated "${pipeline.name}" on ${app.id} (${steps.length} steps)`));
|
|
221
221
|
},
|
|
222
222
|
}, {
|
|
223
223
|
name: "pipelines rename",
|
|
@@ -230,7 +230,7 @@ export function registerPipelineCommands() {
|
|
|
230
230
|
const newName = args.arg(2, "new-name");
|
|
231
231
|
const pipelines = (app.pipelines ?? []).map((p) => p.id === pipeline.id ? { ...p, name: newName } : p);
|
|
232
232
|
await savePipelines(c, app.id, pipelines);
|
|
233
|
-
emit({ appId: app.id, pipelineId: pipeline.id, name: newName }, () =>
|
|
233
|
+
emit({ appId: app.id, pipelineId: pipeline.id, name: newName }, () => success(`Renamed "${pipeline.name}" → "${newName}"`));
|
|
234
234
|
},
|
|
235
235
|
}, {
|
|
236
236
|
name: "pipelines delete",
|
|
@@ -245,7 +245,7 @@ export function registerPipelineCommands() {
|
|
|
245
245
|
}
|
|
246
246
|
const pipelines = (app.pipelines ?? []).filter((p) => p.id !== pipeline.id);
|
|
247
247
|
await savePipelines(c, app.id, pipelines);
|
|
248
|
-
emit({ appId: app.id, deleted: pipeline.id }, () =>
|
|
248
|
+
emit({ appId: app.id, deleted: pipeline.id }, () => success(`Deleted pipeline "${pipeline.name}" from ${app.id}`));
|
|
249
249
|
},
|
|
250
250
|
});
|
|
251
251
|
}
|
package/dist/commands/reports.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { CliError } from "../client.js";
|
|
4
|
-
import { details, emit, info, table } from "../output.js";
|
|
4
|
+
import { details, emit, info, success, table } from "../output.js";
|
|
5
5
|
import { register } from "../registry.js";
|
|
6
6
|
/**
|
|
7
7
|
* Bug reports and feature requests from the terminal.
|
|
@@ -184,7 +184,7 @@ export function registerReportCommands() {
|
|
|
184
184
|
const message = args.arg(1, "message");
|
|
185
185
|
const status = args.flag("status") ? assertStatus(args.flag("status")) : undefined;
|
|
186
186
|
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: message, status });
|
|
187
|
-
emit({ report }, () =>
|
|
187
|
+
emit({ report }, () => success(`Replied on ${report.id}${status ? ` — now ${report.status}` : ""}`));
|
|
188
188
|
},
|
|
189
189
|
}, {
|
|
190
190
|
name: "reports status",
|
|
@@ -198,7 +198,7 @@ export function registerReportCommands() {
|
|
|
198
198
|
const id = args.arg(0, "id");
|
|
199
199
|
const status = assertStatus(args.arg(1, "status"));
|
|
200
200
|
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: args.flag("note") ?? "", status });
|
|
201
|
-
emit({ report }, () =>
|
|
201
|
+
emit({ report }, () => success(`${report.id} is now ${report.status}`));
|
|
202
202
|
},
|
|
203
203
|
}, {
|
|
204
204
|
name: "reports close",
|
|
@@ -213,7 +213,7 @@ export function registerReportCommands() {
|
|
|
213
213
|
const id = args.arg(0, "id");
|
|
214
214
|
const status = args.bool("wont-do") ? "declined" : "done";
|
|
215
215
|
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: args.flag("note") ?? "", status });
|
|
216
|
-
emit({ report }, () =>
|
|
216
|
+
emit({ report }, () => success(`Closed ${report.id} as ${report.status}`));
|
|
217
217
|
},
|
|
218
218
|
}, {
|
|
219
219
|
name: "reports reopen",
|
|
@@ -222,7 +222,7 @@ export function registerReportCommands() {
|
|
|
222
222
|
async run({ args, client }) {
|
|
223
223
|
const id = args.arg(0, "id");
|
|
224
224
|
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: args.flag("note") ?? "", status: "open" });
|
|
225
|
-
emit({ report }, () =>
|
|
225
|
+
emit({ report }, () => success(`Reopened ${report.id}`));
|
|
226
226
|
},
|
|
227
227
|
}, {
|
|
228
228
|
name: "reports attach",
|
|
@@ -244,7 +244,7 @@ export function registerReportCommands() {
|
|
|
244
244
|
throw new CliError(`Can't read ${filePath}: ${error instanceof Error ? error.message : String(error)}`, 4);
|
|
245
245
|
}
|
|
246
246
|
const file = await client().upload(`/reports/${id}/attachments`, basename(filePath), bytes);
|
|
247
|
-
emit({ attachment: file }, () =>
|
|
247
|
+
emit({ attachment: file }, () => success(`Attached ${file.name} (${humanSize(file.size)}) to ${id}`));
|
|
248
248
|
},
|
|
249
249
|
}, {
|
|
250
250
|
name: "reports download",
|
|
@@ -258,7 +258,7 @@ export function registerReportCommands() {
|
|
|
258
258
|
const bytes = await c.download(`/reports/${id}/attachments/${file.id}`);
|
|
259
259
|
const out = args.flag("out") ?? file.name;
|
|
260
260
|
writeFileSync(out, bytes);
|
|
261
|
-
emit({ reportId: id, attachment: file.name, out, bytes: bytes.length }, () =>
|
|
261
|
+
emit({ reportId: id, attachment: file.name, out, bytes: bytes.length }, () => success(`Wrote ${out} (${humanSize(bytes.length)})`));
|
|
262
262
|
},
|
|
263
263
|
}, {
|
|
264
264
|
name: "reports file",
|
|
@@ -290,7 +290,7 @@ export function registerReportCommands() {
|
|
|
290
290
|
attachment = await c.upload(`/reports/${report.id}/attachments`, basename(attach), readFileSync(attach));
|
|
291
291
|
}
|
|
292
292
|
catch (error) {
|
|
293
|
-
|
|
293
|
+
success(`Filed ${report.id} — ${report.title}`);
|
|
294
294
|
throw new CliError(`The report was filed, but ${attach} did not attach: ` +
|
|
295
295
|
(error instanceof Error ? error.message : String(error)), 1, `Try again with: coe reports attach ${report.id} ${attach}`);
|
|
296
296
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CliError } from "../client.js";
|
|
4
|
-
import { details, emit,
|
|
4
|
+
import { details, emit, success, table } from "../output.js";
|
|
5
5
|
import { register } from "../registry.js";
|
|
6
6
|
import { resolveApp } from "./apps.js";
|
|
7
7
|
function humanSize(bytes) {
|
|
@@ -70,7 +70,7 @@ export function registerResourceCommands() {
|
|
|
70
70
|
if (description) {
|
|
71
71
|
await c.request("PATCH", `/registered-apps/${app.id}/files/${file.id}`, { description });
|
|
72
72
|
}
|
|
73
|
-
emit({ appId: app.id, file }, () =>
|
|
73
|
+
emit({ appId: app.id, file }, () => success(`Uploaded ${file.name} (${humanSize(file.size)}) to ${app.id}`));
|
|
74
74
|
},
|
|
75
75
|
}, {
|
|
76
76
|
name: "files download",
|
|
@@ -87,7 +87,7 @@ export function registerResourceCommands() {
|
|
|
87
87
|
const bytes = await c.download(`/registered-apps/${app.id}/files/${file.id}`);
|
|
88
88
|
const out = args.flag("out") ?? file.name;
|
|
89
89
|
writeFileSync(out, bytes);
|
|
90
|
-
emit({ appId: app.id, file: file.name, out, bytes: bytes.length }, () =>
|
|
90
|
+
emit({ appId: app.id, file: file.name, out, bytes: bytes.length }, () => success(`Wrote ${out} (${humanSize(bytes.length)})`));
|
|
91
91
|
},
|
|
92
92
|
}, {
|
|
93
93
|
name: "files delete",
|
|
@@ -102,7 +102,7 @@ export function registerResourceCommands() {
|
|
|
102
102
|
throw new CliError(`This deletes "${file.name}" from ${app.id}.`, 6, "Re-run with --yes to confirm.");
|
|
103
103
|
}
|
|
104
104
|
await c.request("DELETE", `/registered-apps/${app.id}/files/${file.id}`);
|
|
105
|
-
emit({ appId: app.id, deleted: file.name }, () =>
|
|
105
|
+
emit({ appId: app.id, deleted: file.name }, () => success(`Deleted ${file.name} from ${app.id}`));
|
|
106
106
|
},
|
|
107
107
|
}, {
|
|
108
108
|
name: "files link",
|
|
@@ -118,7 +118,7 @@ export function registerResourceCommands() {
|
|
|
118
118
|
name: args.flag("name"),
|
|
119
119
|
description: args.flag("description"),
|
|
120
120
|
});
|
|
121
|
-
emit({ appId: app.id, link }, () =>
|
|
121
|
+
emit({ appId: app.id, link }, () => success(`Linked "${link.name}" → ${link.url}`));
|
|
122
122
|
},
|
|
123
123
|
}, {
|
|
124
124
|
name: "files show",
|
package/dist/commands/tips.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "../client.js";
|
|
2
|
-
import { emit, info, table } from "../output.js";
|
|
2
|
+
import { emit, info, style, success, table } from "../output.js";
|
|
3
3
|
import { register } from "../registry.js";
|
|
4
4
|
import { resolveApp } from "./apps.js";
|
|
5
5
|
export function registerTipCommands() {
|
|
@@ -47,7 +47,7 @@ export function registerTipCommands() {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
if (result.alreadyReviewed === "accepted") {
|
|
50
|
-
|
|
50
|
+
success("This trigger already has accepted advice. Counted, not queued again.");
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
info(result.created
|
|
@@ -76,7 +76,7 @@ export function registerTipCommands() {
|
|
|
76
76
|
}
|
|
77
77
|
table(tips.map((t) => ({
|
|
78
78
|
id: t.id,
|
|
79
|
-
status: t.status,
|
|
79
|
+
status: style.state(t.status),
|
|
80
80
|
kind: t.kind,
|
|
81
81
|
seen: String(t.occurrences),
|
|
82
82
|
apps: t.appIds.join(", "),
|
|
@@ -107,7 +107,7 @@ export function registerTipCommands() {
|
|
|
107
107
|
status,
|
|
108
108
|
note: args.flag("note"),
|
|
109
109
|
});
|
|
110
|
-
emit({ tip }, () =>
|
|
110
|
+
emit({ tip }, () => success(`${tip.id} marked ${tip.status}.`));
|
|
111
111
|
},
|
|
112
112
|
});
|
|
113
113
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { registerReportCommands } from "./commands/reports.js";
|
|
|
14
14
|
import { registerResourceCommands } from "./commands/resources.js";
|
|
15
15
|
import { registerTipCommands } from "./commands/tips.js";
|
|
16
16
|
import { registerVmCommands } from "./commands/vms.js";
|
|
17
|
-
import {
|
|
17
|
+
import { failure, setJsonMode } from "./output.js";
|
|
18
18
|
import { allCommands, findCommand, register } from "./registry.js";
|
|
19
19
|
registerAuthCommands();
|
|
20
20
|
registerAppCommands();
|
|
@@ -71,6 +71,19 @@ function parseArgs(argv) {
|
|
|
71
71
|
bool(name) {
|
|
72
72
|
return flags[name] !== undefined;
|
|
73
73
|
},
|
|
74
|
+
num(name, fallback) {
|
|
75
|
+
const raw = flags[name];
|
|
76
|
+
if (raw === undefined)
|
|
77
|
+
return fallback;
|
|
78
|
+
if (raw === true) {
|
|
79
|
+
throw new CliError(`--${name} needs a number.`, 6, `e.g. --${name} ${fallback}`);
|
|
80
|
+
}
|
|
81
|
+
const value = Number(raw);
|
|
82
|
+
if (!Number.isFinite(value)) {
|
|
83
|
+
throw new CliError(`--${name} must be a number, not "${raw}".`, 6);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
},
|
|
74
87
|
};
|
|
75
88
|
}
|
|
76
89
|
function printHelp() {
|
|
@@ -125,11 +138,9 @@ async function main() {
|
|
|
125
138
|
}
|
|
126
139
|
main().catch((error) => {
|
|
127
140
|
if (error instanceof CliError) {
|
|
128
|
-
|
|
129
|
-
if (error.hint)
|
|
130
|
-
info(` ${error.hint}`);
|
|
141
|
+
failure(error.message, error.hint);
|
|
131
142
|
process.exit(error.exitCode);
|
|
132
143
|
}
|
|
133
|
-
|
|
144
|
+
failure(error instanceof Error ? error.message : String(error));
|
|
134
145
|
process.exit(1);
|
|
135
146
|
});
|
package/dist/output.d.ts
CHANGED
|
@@ -8,12 +8,29 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function setJsonMode(on: boolean): void;
|
|
10
10
|
export declare function isJsonMode(): boolean;
|
|
11
|
+
/** Marks a value for the reader's eye. Safe to pass into `table` or `details`. */
|
|
12
|
+
export declare const style: {
|
|
13
|
+
ok: (t: string) => string;
|
|
14
|
+
bad: (t: string) => string;
|
|
15
|
+
warn: (t: string) => string;
|
|
16
|
+
muted: (t: string) => string;
|
|
17
|
+
strong: (t: string) => string;
|
|
18
|
+
id: (t: string) => string;
|
|
19
|
+
/** A build/process state, coloured by what it means rather than by name. */
|
|
20
|
+
state: (t: string) => string;
|
|
21
|
+
};
|
|
11
22
|
/** The command's result. In JSON mode this is the only thing on stdout. */
|
|
12
23
|
export declare function emit(data: unknown, human: () => void): void;
|
|
13
24
|
/** Progress/status chatter. Always stderr, so it never pollutes piped JSON. */
|
|
14
25
|
export declare function info(message: string): void;
|
|
15
26
|
export declare function warn(message: string): void;
|
|
27
|
+
/** A command that did what it was asked. */
|
|
28
|
+
export declare function success(message: string): void;
|
|
29
|
+
/** How a failure is rendered, including the hint that says what to do next. */
|
|
30
|
+
export declare function failure(message: string, hint?: string): void;
|
|
16
31
|
/** Minimal column alignment — no dependency, and stable enough to eyeball. */
|
|
17
32
|
export declare function table(rows: Array<Record<string, string>>, columns: string[]): void;
|
|
18
33
|
/** Key/value block for `show`-style commands. */
|
|
19
34
|
export declare function details(pairs: Array<[string, string]>): void;
|
|
35
|
+
/** A heading above a block, for commands that print more than one. */
|
|
36
|
+
export declare function heading(text: string): void;
|
package/dist/output.js
CHANGED
|
@@ -13,6 +13,71 @@ export function setJsonMode(on) {
|
|
|
13
13
|
export function isJsonMode() {
|
|
14
14
|
return jsonMode;
|
|
15
15
|
}
|
|
16
|
+
// ---- colour ----------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Colour only when a person is actually looking at that stream.
|
|
19
|
+
*
|
|
20
|
+
* Checked per stream, not once: stdout is frequently redirected while stderr
|
|
21
|
+
* is still a terminal (`coe builds app > builds.txt`), and escape codes in the
|
|
22
|
+
* redirected half would corrupt a file someone is about to parse. `NO_COLOR`
|
|
23
|
+
* is honoured because it is the convention, and `FORCE_COLOR` exists so output
|
|
24
|
+
* can be inspected through a pipe when working on this file.
|
|
25
|
+
*/
|
|
26
|
+
function allowColor(stream) {
|
|
27
|
+
if (process.env.NO_COLOR !== undefined)
|
|
28
|
+
return false;
|
|
29
|
+
if (process.env.FORCE_COLOR)
|
|
30
|
+
return true;
|
|
31
|
+
return stream.isTTY === true && process.env.TERM !== "dumb";
|
|
32
|
+
}
|
|
33
|
+
const CODES = {
|
|
34
|
+
reset: "\u001B[0m",
|
|
35
|
+
bold: "\u001B[1m",
|
|
36
|
+
dim: "\u001B[2m",
|
|
37
|
+
red: "\u001B[31m",
|
|
38
|
+
green: "\u001B[32m",
|
|
39
|
+
yellow: "\u001B[33m",
|
|
40
|
+
blue: "\u001B[34m",
|
|
41
|
+
cyan: "\u001B[36m",
|
|
42
|
+
};
|
|
43
|
+
function paint(text, style, stream) {
|
|
44
|
+
return allowColor(stream) ? `${CODES[style]}${text}${CODES.reset}` : text;
|
|
45
|
+
}
|
|
46
|
+
/** Style for stdout (data). Never colours in JSON mode. */
|
|
47
|
+
const out = (text, style) => jsonMode ? text : paint(text, style, process.stdout);
|
|
48
|
+
/** Style for stderr (chatter). */
|
|
49
|
+
const err = (text, style) => paint(text, style, process.stderr);
|
|
50
|
+
/**
|
|
51
|
+
* Length as a terminal renders it.
|
|
52
|
+
*
|
|
53
|
+
* Column widths are computed from cell contents, and an escape sequence is
|
|
54
|
+
* zero columns wide but several characters long — measuring the raw string
|
|
55
|
+
* makes every coloured cell over-padded and the table ragged.
|
|
56
|
+
*/
|
|
57
|
+
// eslint-disable-next-line no-control-regex
|
|
58
|
+
const ANSI = /\u001B\[[0-9;]*m/g;
|
|
59
|
+
const visibleLength = (text) => text.replace(ANSI, "").length;
|
|
60
|
+
/** Marks a value for the reader's eye. Safe to pass into `table` or `details`. */
|
|
61
|
+
export const style = {
|
|
62
|
+
ok: (t) => out(t, "green"),
|
|
63
|
+
bad: (t) => out(t, "red"),
|
|
64
|
+
warn: (t) => out(t, "yellow"),
|
|
65
|
+
muted: (t) => out(t, "dim"),
|
|
66
|
+
strong: (t) => out(t, "bold"),
|
|
67
|
+
id: (t) => out(t, "cyan"),
|
|
68
|
+
/** A build/process state, coloured by what it means rather than by name. */
|
|
69
|
+
state: (t) => {
|
|
70
|
+
const s = t.toLowerCase();
|
|
71
|
+
if (["ok", "online", "up", "done", "accepted", "active", "yes"].includes(s))
|
|
72
|
+
return style.ok(t);
|
|
73
|
+
if (["failed", "error", "down", "stopped", "rejected", "errored"].includes(s))
|
|
74
|
+
return style.bad(t);
|
|
75
|
+
if (["cancelled", "running", "new", "pending", "queued", "unknown"].includes(s))
|
|
76
|
+
return style.warn(t);
|
|
77
|
+
return t;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
// ---- writers ---------------------------------------------------------------
|
|
16
81
|
/** The command's result. In JSON mode this is the only thing on stdout. */
|
|
17
82
|
export function emit(data, human) {
|
|
18
83
|
if (jsonMode) {
|
|
@@ -26,18 +91,29 @@ export function info(message) {
|
|
|
26
91
|
process.stderr.write(`${message}\n`);
|
|
27
92
|
}
|
|
28
93
|
export function warn(message) {
|
|
29
|
-
process.stderr.write(
|
|
94
|
+
process.stderr.write(`${err("warning:", "yellow")} ${message}\n`);
|
|
95
|
+
}
|
|
96
|
+
/** A command that did what it was asked. */
|
|
97
|
+
export function success(message) {
|
|
98
|
+
process.stderr.write(`${err("✓", "green")} ${message}\n`);
|
|
99
|
+
}
|
|
100
|
+
/** How a failure is rendered, including the hint that says what to do next. */
|
|
101
|
+
export function failure(message, hint) {
|
|
102
|
+
process.stderr.write(`${err("error:", "red")} ${message}\n`);
|
|
103
|
+
if (hint)
|
|
104
|
+
process.stderr.write(`${err(` ${hint}`, "dim")}\n`);
|
|
30
105
|
}
|
|
31
106
|
/** Minimal column alignment — no dependency, and stable enough to eyeball. */
|
|
32
107
|
export function table(rows, columns) {
|
|
33
108
|
if (rows.length === 0) {
|
|
34
|
-
info("(none)");
|
|
109
|
+
info(err("(none)", "dim"));
|
|
35
110
|
return;
|
|
36
111
|
}
|
|
37
|
-
const width = (col) => Math.max(col.length, ...rows.map((r) => (r[col] ?? "")
|
|
112
|
+
const width = (col) => Math.max(col.length, ...rows.map((r) => visibleLength(r[col] ?? "")));
|
|
38
113
|
const widths = columns.map(width);
|
|
39
|
-
const
|
|
40
|
-
|
|
114
|
+
const pad = (cell, i, last) => last ? cell : cell + " ".repeat(Math.max(0, widths[i] - visibleLength(cell)));
|
|
115
|
+
const line = (cells) => cells.map((c, i) => pad(c, i, i === cells.length - 1)).join(" ").trimEnd();
|
|
116
|
+
process.stdout.write(`${out(line(columns.map((c) => c.toUpperCase())), "dim")}\n`);
|
|
41
117
|
for (const row of rows) {
|
|
42
118
|
process.stdout.write(`${line(columns.map((c) => row[c] ?? ""))}\n`);
|
|
43
119
|
}
|
|
@@ -46,6 +122,10 @@ export function table(rows, columns) {
|
|
|
46
122
|
export function details(pairs) {
|
|
47
123
|
const width = Math.max(...pairs.map(([k]) => k.length));
|
|
48
124
|
for (const [k, v] of pairs) {
|
|
49
|
-
process.stdout.write(`${k.padEnd(width)} ${v}\n`);
|
|
125
|
+
process.stdout.write(`${out(k.padEnd(width), "dim")} ${v}\n`);
|
|
50
126
|
}
|
|
51
127
|
}
|
|
128
|
+
/** A heading above a block, for commands that print more than one. */
|
|
129
|
+
export function heading(text) {
|
|
130
|
+
process.stderr.write(`\n${err(text.toUpperCase(), "bold")}\n`);
|
|
131
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ export interface Args {
|
|
|
12
12
|
/** A flag's value, or undefined. `--flag` with no value reads as true. */
|
|
13
13
|
flag(name: string): string | undefined;
|
|
14
14
|
bool(name: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* A numeric flag, or the fallback when it wasn't given.
|
|
17
|
+
*
|
|
18
|
+
* Rejects a value that isn't a number instead of quietly falling back:
|
|
19
|
+
* `--limit abc` used to print the default twenty rows and exit 0, which
|
|
20
|
+
* reads exactly like a limit that worked.
|
|
21
|
+
*/
|
|
22
|
+
num(name: string, fallback: number): number;
|
|
15
23
|
}
|
|
16
24
|
export interface CommandContext {
|
|
17
25
|
args: Args;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uic-coe-connect/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "The coe CLI \u2014 manage COEConnect apps (pipelines, deploys, access, env) from a terminal, with browser-approved auth. Designed to be driven by an AI agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|