@brunyee-studio/onus-cli 2.28.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/dist/bin.js ADDED
@@ -0,0 +1,2354 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ cliVersion,
4
+ createApiClient,
5
+ loginWithOAuth
6
+ } from "./chunk-HJQGUY5T.js";
7
+ import {
8
+ clearConfig,
9
+ getConfigDirectory,
10
+ getConfigFilePath,
11
+ loadConfig,
12
+ saveConfig
13
+ } from "./chunk-HE2HTZ3Y.js";
14
+
15
+ // src/bin.ts
16
+ import pc9 from "picocolors";
17
+
18
+ // src/program.ts
19
+ import { Command } from "commander";
20
+ import pc7 from "picocolors";
21
+
22
+ // src/commands/auth.ts
23
+ import pc2 from "picocolors";
24
+
25
+ // src/format.ts
26
+ import pc from "picocolors";
27
+ function sanitizeTerminal(value) {
28
+ let s = value === null || value === void 0 ? "" : String(value);
29
+ s = s.replace(/\u001b\[[0-9;:?]*[ -/]*[@-~]/g, "");
30
+ s = s.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "");
31
+ s = s.replace(/\u001b[PX^_][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "");
32
+ s = s.replace(/\u001b[A-Z\\[\\]\\_]/gi, "");
33
+ s = s.replace(/\u001b/g, "");
34
+ return s.replace(/[\u0000-\u0008\u000b\u000c\u000d\u000e-\u001f\u007f\u0080-\u009f]/g, "");
35
+ }
36
+ function displayValue(cell) {
37
+ return cell === null || cell === void 0 ? pc.dim("\u2014") : sanitizeTerminal(cell);
38
+ }
39
+ function isJsonMode(explicitJson) {
40
+ if (explicitJson !== void 0) return explicitJson;
41
+ if (process.env.ONUS_OUTPUT === "json") return true;
42
+ return !process.stdout.isTTY;
43
+ }
44
+ function formatRecord(fields) {
45
+ if (fields.length === 0) return "";
46
+ const width = Math.max(...fields.map((f) => sanitizeTerminal(f.label).length));
47
+ return fields.map((f) => {
48
+ const label = (sanitizeTerminal(f.label) + ":").padEnd(width + 1);
49
+ return `${pc.bold(pc.cyan(label))} ${displayValue(f.value)}`;
50
+ }).join("\n");
51
+ }
52
+ function formatRecordList(records) {
53
+ return records.map((fields) => formatRecord(fields)).join("\n\n");
54
+ }
55
+ function printJsonOrFormatted(data, explicitJson, ttyRenderer) {
56
+ if (isJsonMode(explicitJson)) {
57
+ console.log(JSON.stringify(data, null, 2));
58
+ } else {
59
+ ttyRenderer();
60
+ }
61
+ }
62
+ function formatSuccess(message) {
63
+ console.log(pc.green("\u2714") + " " + sanitizeTerminal(message));
64
+ }
65
+ function exitCodeForStatus(status) {
66
+ if (status === 401 || status === 403) return 3;
67
+ if (status === 404) return 4;
68
+ if (status === 400 || status === 422) return 2;
69
+ return 1;
70
+ }
71
+ async function handleApiError(error, explicitJson) {
72
+ const isJson = isJsonMode(explicitJson);
73
+ const apiError = typeof error === "object" && error !== null ? error : {};
74
+ const status = exitCodeForStatus(apiError.status ?? apiError.response?.status);
75
+ let body;
76
+ try {
77
+ if (apiError.response && typeof apiError.response.json === "function") {
78
+ body = await apiError.response.json();
79
+ } else if (apiError.json && typeof apiError.json === "function") {
80
+ body = await apiError.json();
81
+ }
82
+ } catch {
83
+ body = void 0;
84
+ }
85
+ const message = body?.message || body?.error || apiError.statusText || apiError.response?.statusText || (error instanceof Error ? error.message : String(error));
86
+ const payload = {
87
+ error: body?.error || "error",
88
+ message
89
+ };
90
+ if (isJson) {
91
+ const bodyHasKeys = body && typeof body === "object" && Object.keys(body).length > 0;
92
+ console.error(JSON.stringify(bodyHasKeys ? body : payload, null, 2));
93
+ } else {
94
+ console.error(pc.red(`Error (${status}):`), sanitizeTerminal(message));
95
+ }
96
+ process.exit(status);
97
+ }
98
+
99
+ // src/help.ts
100
+ function withExamples(command, examples) {
101
+ const block = `
102
+ Examples:
103
+ ${examples.map((e) => ` $ ${e}`).join("\n")}
104
+ `;
105
+ return command.addHelpText("after", block);
106
+ }
107
+
108
+ // src/commands/auth.ts
109
+ function registerAuthCommands(program2) {
110
+ const auth = program2.command("auth").description("Manage CLI authentication and API endpoint configuration");
111
+ auth.command("login").description("Authenticate with Onus via OAuth browser flow or store an access token").option("-t, --token <token>", "Direct access token or API key (headless/agent mode)").option("-u, --api-url <url>", "API base URL").option("-m, --manual", "Use manual copy-paste authorization code without a local browser").option("--timeout <seconds>", "Timeout in seconds for interactive browser callback", "120").option(
112
+ "--loopback-host <host>",
113
+ "Loopback host to bind the callback server. Use 0.0.0.0 under WSL2 mirror networking. Defaults to ONUS_OAUTH_HOST or 127.0.0.1"
114
+ ).action(async function(opts) {
115
+ const globals = this.optsWithGlobals();
116
+ const directToken = opts.token || globals.token;
117
+ const apiUrl = opts.apiUrl || globals.apiUrl;
118
+ if (directToken) {
119
+ const updated = saveConfig({
120
+ token: directToken,
121
+ ...apiUrl ? { apiUrl } : {}
122
+ });
123
+ formatSuccess(`Logged in successfully. Using API: ${updated.apiUrl}`);
124
+ return;
125
+ }
126
+ const activeConfig = loadConfig();
127
+ const targetApiUrl = apiUrl || activeConfig.apiUrl;
128
+ const loopbackHost = opts.loopbackHost || process.env.ONUS_OAUTH_HOST || void 0;
129
+ try {
130
+ const oauthResult = await loginWithOAuth({
131
+ apiUrl: targetApiUrl,
132
+ manual: Boolean(opts.manual),
133
+ timeoutSeconds: opts.timeout ? Number(opts.timeout) : 120,
134
+ ...loopbackHost ? { loopbackHost } : {}
135
+ });
136
+ const updated = saveConfig({
137
+ token: oauthResult.token,
138
+ ...oauthResult.refreshToken ? { refreshToken: oauthResult.refreshToken } : {},
139
+ apiUrl: targetApiUrl
140
+ });
141
+ try {
142
+ const client = createApiClient({
143
+ token: updated.token,
144
+ apiUrl: updated.apiUrl
145
+ });
146
+ const viewer = await client.meGet();
147
+ const name = viewer.full_name || viewer.id;
148
+ formatSuccess(
149
+ `Logged in successfully as ${name}. Stored credentials in ${getConfigFilePath()}`
150
+ );
151
+ } catch {
152
+ formatSuccess(`Logged in successfully. Stored credentials in ${getConfigFilePath()}`);
153
+ }
154
+ } catch (err) {
155
+ await handleApiError(err, globals.json);
156
+ }
157
+ });
158
+ auth.command("logout").description("Clear stored authentication credentials").action(() => {
159
+ clearConfig();
160
+ formatSuccess("Logged out successfully. Stored credentials removed.");
161
+ });
162
+ auth.command("status").description("Show current authentication status and active API endpoint").option("-j, --json", "Output raw JSON").action(function() {
163
+ const globals = this.optsWithGlobals();
164
+ const config = loadConfig();
165
+ const maskedToken = config.token && config.token.length > 10 ? `${config.token.slice(0, 6)}...${config.token.slice(-4)}` : config.token ? "***" : null;
166
+ const hasRefreshToken = Boolean(config.refreshToken);
167
+ const statusObj = {
168
+ authenticated: Boolean(config.token),
169
+ token: maskedToken,
170
+ hasRefreshToken,
171
+ apiUrl: config.apiUrl
172
+ };
173
+ printJsonOrFormatted(statusObj, globals.json, () => {
174
+ console.log(pc2.bold("Onus CLI Auth Status:"));
175
+ console.log(` API URL: ${pc2.cyan(config.apiUrl)}`);
176
+ console.log(
177
+ ` Authenticated: ${config.token ? pc2.green("Yes") + ` (Token: ${maskedToken})` : pc2.yellow('No (Run "onus auth login")')}`
178
+ );
179
+ console.log(
180
+ ` Refresh Token: ${hasRefreshToken ? pc2.green("Configured") : pc2.dim("None")}`
181
+ );
182
+ });
183
+ });
184
+ withExamples(
185
+ auth.commands.find((c) => c.name() === "login"),
186
+ [
187
+ "onus auth login",
188
+ "onus auth login --manual",
189
+ "onus auth login --token <TOKEN> --api-url https://onus.example.com/v1"
190
+ ]
191
+ );
192
+ withExamples(
193
+ auth.commands.find((c) => c.name() === "logout"),
194
+ ["onus auth logout"]
195
+ );
196
+ withExamples(
197
+ auth.commands.find((c) => c.name() === "status"),
198
+ ["onus auth status", "onus auth status --json"]
199
+ );
200
+ }
201
+
202
+ // src/commands/context.ts
203
+ import pc3 from "picocolors";
204
+ function registerContextCommands(program2) {
205
+ program2.command("context").description("Emit environment, auth, and agent-interaction context snapshot").option("-j, --json", "Output raw JSON").action(async function() {
206
+ const globals = this.optsWithGlobals();
207
+ const config = loadConfig();
208
+ const effectiveToken = globals.token || config.token;
209
+ const effectiveUrl = globals.apiUrl || config.apiUrl;
210
+ let viewer = null;
211
+ if (effectiveToken) {
212
+ try {
213
+ const client = createApiClient({ token: effectiveToken, apiUrl: effectiveUrl });
214
+ viewer = await client.meGet();
215
+ } catch {
216
+ }
217
+ }
218
+ const contextData = {
219
+ cliVersion,
220
+ apiUrl: effectiveUrl,
221
+ authenticated: Boolean(effectiveToken),
222
+ viewer,
223
+ resources: ["team", "project", "issue", "comment", "label"],
224
+ capabilities: {
225
+ jsonOutputSupported: true,
226
+ envAuthSupported: true,
227
+ standardExitCodes: {
228
+ success: 0,
229
+ generalError: 1,
230
+ validationError: 2,
231
+ authError: 3,
232
+ notFound: 4
233
+ }
234
+ },
235
+ quickExamples: [
236
+ "onus me --json",
237
+ "onus team list --json",
238
+ "onus project list --json",
239
+ "onus issue list --status in_progress --json",
240
+ 'onus issue create --title "Fix login" --team <team-id> --json',
241
+ "onus comment list <issue-id> --json"
242
+ ]
243
+ };
244
+ printJsonOrFormatted(contextData, globals.json, () => {
245
+ console.log(pc3.bold("Onus CLI Agent Context Snapshot:"));
246
+ console.log(` CLI Version: ${pc3.cyan(contextData.cliVersion)}`);
247
+ console.log(` API Endpoint: ${pc3.cyan(contextData.apiUrl)}`);
248
+ console.log(
249
+ ` Auth State: ${contextData.authenticated ? pc3.green("Authenticated") : pc3.yellow("Unauthenticated")}`
250
+ );
251
+ if (viewer) {
252
+ console.log(` Viewer: ${pc3.bold(viewer.full_name)} (${viewer.id})`);
253
+ }
254
+ console.log("\n" + pc3.bold("Available Resources:"));
255
+ console.log(" " + contextData.resources.join(", "));
256
+ console.log("\n" + pc3.bold("Quick Examples:"));
257
+ for (const ex of contextData.quickExamples) {
258
+ console.log(` $ ${pc3.dim(ex)}`);
259
+ }
260
+ });
261
+ });
262
+ program2.command("schema [resource]").description(
263
+ "Output OpenAPI schemas for resources (issue, project, team, comment, label) or --all"
264
+ ).option("-a, --all", "Output full OpenAPI document").option("-j, --json", "Output raw JSON").action(async function(resource, opts) {
265
+ const globals = this.optsWithGlobals();
266
+ try {
267
+ const config = loadConfig();
268
+ const basePath = (globals.apiUrl || config.apiUrl || "").replace(/\/+$/, "");
269
+ const response = await fetch(`${basePath}/openapi.json`);
270
+ if (!response.ok) {
271
+ throw new Error(
272
+ `Failed to fetch schema from ${basePath}/openapi.json: HTTP ${response.status}`
273
+ );
274
+ }
275
+ const doc = await response.json();
276
+ let result = doc;
277
+ if (resource && !opts.all) {
278
+ const key = resource.toLowerCase();
279
+ const components = doc.components?.schemas || {};
280
+ const matchedKey = Object.keys(components).find((k) => k.toLowerCase().startsWith(key));
281
+ if (matchedKey) {
282
+ result = { [matchedKey]: components[matchedKey] };
283
+ } else {
284
+ console.error(
285
+ pc3.red(
286
+ `Schema for resource "${resource}" not found. Available: ${Object.keys(components).join(", ")}`
287
+ )
288
+ );
289
+ process.exit(4);
290
+ }
291
+ }
292
+ printJsonOrFormatted(result, globals.json ?? true, () => {
293
+ console.log(JSON.stringify(result, null, 2));
294
+ });
295
+ } catch (err) {
296
+ const message = err instanceof Error ? err.message : String(err);
297
+ console.error(pc3.red("Error fetching schema:"), message);
298
+ process.exit(1);
299
+ }
300
+ });
301
+ withExamples(
302
+ program2.commands.find((c) => c.name() === "context"),
303
+ ["onus context", "onus context --json"]
304
+ );
305
+ withExamples(
306
+ program2.commands.find((c) => c.name() === "schema"),
307
+ ["onus schema", "onus schema issue", "onus schema --all"]
308
+ );
309
+ }
310
+
311
+ // src/commands/me.ts
312
+ function registerMeCommand(program2) {
313
+ program2.command("me").description("Get the authenticated viewer profile").option("-j, --json", "Output raw JSON").action(async function() {
314
+ const globals = this.optsWithGlobals();
315
+ try {
316
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
317
+ const viewer = await client.meGet();
318
+ printJsonOrFormatted(viewer, globals.json, () => {
319
+ console.log(
320
+ formatRecord([
321
+ { label: "Name", value: viewer.full_name },
322
+ { label: "Role", value: viewer.role },
323
+ { label: "ID", value: viewer.id }
324
+ ])
325
+ );
326
+ });
327
+ } catch (err) {
328
+ await handleApiError(err, globals.json);
329
+ }
330
+ });
331
+ withExamples(
332
+ program2.commands.find((c) => c.name() === "me"),
333
+ ["onus me", "onus me --json"]
334
+ );
335
+ }
336
+
337
+ // src/teams.ts
338
+ import pc4 from "picocolors";
339
+ var TEAM_UUID_RE = /^[0-9a-f-]{36}$/i;
340
+ async function resolveTeamId(client, team) {
341
+ if (TEAM_UUID_RE.test(team)) return team;
342
+ const teams = await client.teamsGet();
343
+ const match = (teams ?? []).find((t) => t.key?.toUpperCase() === team.toUpperCase());
344
+ if (!match) {
345
+ console.error(pc4.red(`Unknown team key: ${team}`));
346
+ process.exit(2);
347
+ }
348
+ return match.id;
349
+ }
350
+
351
+ // src/commands/team.ts
352
+ function registerTeamCommands(program2) {
353
+ const team = program2.command("team").description("List and inspect Onus teams (read-only)");
354
+ team.command("list").description("List all teams").option("-j, --json", "Output raw JSON").action(async function() {
355
+ const globals = this.optsWithGlobals();
356
+ try {
357
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
358
+ const data = await client.teamsGet();
359
+ const items = Array.isArray(data) ? data : [];
360
+ printJsonOrFormatted(items, globals.json, () => {
361
+ if (items.length === 0) {
362
+ console.log("No teams found.");
363
+ return;
364
+ }
365
+ console.log(
366
+ formatRecordList(
367
+ items.map((t) => [
368
+ { label: "ID", value: t.id },
369
+ { label: "Name", value: t.name },
370
+ { label: "Key", value: t.key },
371
+ { label: "Icon", value: t.icon },
372
+ { label: "Color", value: t.color }
373
+ ])
374
+ )
375
+ );
376
+ });
377
+ } catch (err) {
378
+ await handleApiError(err, globals.json);
379
+ }
380
+ });
381
+ team.command("get <team>").description("Get a single team by UUID or KEY (e.g. ONUS)").option("-j, --json", "Output raw JSON").action(async function(team2) {
382
+ const globals = this.optsWithGlobals();
383
+ try {
384
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
385
+ const id = await resolveTeamId(client, team2);
386
+ const teamRow = await client.teamsIdGet({ id });
387
+ printJsonOrFormatted(teamRow, globals.json, () => {
388
+ console.log(
389
+ formatRecord([
390
+ { label: "ID", value: teamRow.id },
391
+ { label: "Name", value: teamRow.name },
392
+ { label: "Key", value: teamRow.key },
393
+ { label: "Icon", value: teamRow.icon },
394
+ { label: "Color", value: teamRow.color }
395
+ ])
396
+ );
397
+ });
398
+ } catch (err) {
399
+ await handleApiError(err, globals.json);
400
+ }
401
+ });
402
+ withExamples(
403
+ team.commands.find((c) => c.name() === "list"),
404
+ ["onus team list", "onus team list --json"]
405
+ );
406
+ withExamples(
407
+ team.commands.find((c) => c.name() === "get"),
408
+ ["onus team get <teamId>", "onus team get ONUS"]
409
+ );
410
+ }
411
+
412
+ // src/commands/analytics.ts
413
+ function registerAnalyticsCommands(program2) {
414
+ const analytics = program2.command("analytics").description("Query team analytics (read-only)");
415
+ analytics.command("overview").description("Visitors, pageviews, sessions, bounce and duration with previous-period deltas").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--from <from>", "Range start (ISO 8601 with offset)").option("--to <to>", "Range end (ISO 8601 with offset)").option("-j, --json", "Output raw JSON").action(async function(opts) {
416
+ const globals = this.optsWithGlobals();
417
+ try {
418
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
419
+ const teamId = await resolveTeamId(client, opts.team);
420
+ const overview = await client.analyticsOverviewGet({
421
+ teamId,
422
+ ...opts.from ? { from: new Date(opts.from) } : {},
423
+ ...opts.to ? { to: new Date(opts.to) } : {}
424
+ });
425
+ printJsonOrFormatted(overview, globals.json, () => {
426
+ const periods = overview;
427
+ console.log(
428
+ formatRecordList(
429
+ [
430
+ "visitors",
431
+ "pageviews",
432
+ "sessions",
433
+ "bounce_rate",
434
+ "median_session_duration_seconds"
435
+ ].map((metric) => [
436
+ { label: "Metric", value: metric },
437
+ {
438
+ label: "Current",
439
+ value: periods.current?.[metric] != null ? String(periods.current[metric]) : null
440
+ },
441
+ {
442
+ label: "Previous",
443
+ value: periods.previous?.[metric] != null ? String(periods.previous[metric]) : null
444
+ }
445
+ ])
446
+ )
447
+ );
448
+ });
449
+ } catch (err) {
450
+ await handleApiError(err, globals.json);
451
+ }
452
+ });
453
+ analytics.command("events").description("List ingested event names with counts, ordered by volume").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--limit <limit>", "Maximum number of event names (1-500, default 100)").option("-j, --json", "Output raw JSON").action(async function(opts) {
454
+ const globals = this.optsWithGlobals();
455
+ try {
456
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
457
+ const teamId = await resolveTeamId(client, opts.team);
458
+ const limit = opts.limit === void 0 ? void 0 : Number(opts.limit);
459
+ const events = await client.analyticsEventsGet({ teamId, limit });
460
+ const items = Array.isArray(events) ? events : [];
461
+ printJsonOrFormatted(items, globals.json, () => {
462
+ if (items.length === 0) {
463
+ console.log("No events ingested yet.");
464
+ return;
465
+ }
466
+ console.log(
467
+ formatRecordList(
468
+ items.map((e) => [
469
+ { label: "Event", value: e.event },
470
+ { label: "Count", value: String(e.count) },
471
+ { label: "Last Seen", value: e.last_seen == null ? null : String(e.last_seen) }
472
+ ])
473
+ )
474
+ );
475
+ });
476
+ } catch (err) {
477
+ await handleApiError(err, globals.json);
478
+ }
479
+ });
480
+ analytics.command("query").description(
481
+ "Execute a trends or funnel query (JSON body; paths and property math are rejected)"
482
+ ).requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").requiredOption(
483
+ "--query <json>",
484
+ `Query payload, e.g. '{"type":"trends","series":[{"event":"signup"}]}'`
485
+ ).option("--from <from>", "Range start (ISO 8601 with offset)").option("--to <to>", "Range end (ISO 8601 with offset)").option("-j, --json", "Output raw JSON").action(async function(opts) {
486
+ const globals = this.optsWithGlobals();
487
+ try {
488
+ let query;
489
+ try {
490
+ query = JSON.parse(opts.query);
491
+ } catch {
492
+ throw new Error("--query must be valid JSON");
493
+ }
494
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
495
+ const teamId = await resolveTeamId(client, opts.team);
496
+ const result = await client.analyticsQueryPost({
497
+ analyticsQueryPostRequest: {
498
+ team_id: teamId,
499
+ query,
500
+ ...opts.from || opts.to ? {
501
+ from: opts.from ? new Date(opts.from) : null,
502
+ to: opts.to ? new Date(opts.to) : null
503
+ } : {}
504
+ }
505
+ });
506
+ printJsonOrFormatted(result, globals.json, () => {
507
+ const record = result;
508
+ if (Array.isArray(record.steps)) {
509
+ console.log(
510
+ formatRecordList(
511
+ record.steps.map(
512
+ (s, i) => [
513
+ { label: "Step", value: String(i + 1) },
514
+ { label: "Event", value: s.event },
515
+ { label: "Count", value: String(s.count) },
516
+ { label: "Overall", value: String(s.overall) }
517
+ ]
518
+ )
519
+ )
520
+ );
521
+ } else if (Array.isArray(record.series)) {
522
+ console.log(
523
+ formatRecordList(
524
+ record.series.map((s) => [
525
+ { label: "Series", value: s.label },
526
+ { label: "Event", value: s.event },
527
+ { label: "Math", value: s.math },
528
+ { label: "Total", value: s.rows?.total != null ? String(s.rows.total) : null }
529
+ ])
530
+ )
531
+ );
532
+ } else {
533
+ console.log(JSON.stringify(result, null, 2));
534
+ }
535
+ });
536
+ } catch (err) {
537
+ await handleApiError(err, globals.json);
538
+ }
539
+ });
540
+ withExamples(
541
+ analytics.commands.find((c) => c.name() === "overview"),
542
+ [
543
+ "onus analytics overview --team ONUS",
544
+ "onus analytics overview --team ONUS --from 2026-08-01T00:00:00+00:00 --to 2026-08-31T23:59:59+00:00 --json"
545
+ ]
546
+ );
547
+ withExamples(
548
+ analytics.commands.find((c) => c.name() === "events"),
549
+ ["onus analytics events --team ONUS", "onus analytics events --team ONUS --limit 25 --json"]
550
+ );
551
+ withExamples(
552
+ analytics.commands.find((c) => c.name() === "query"),
553
+ [
554
+ `onus analytics query --team ONUS --query '{"type":"trends","series":[{"event":"signup"}]}' --json`,
555
+ `onus analytics query --team ONUS --query '{"type":"funnel","steps":[{"event":"signup"},{"event":"pay"}]}'`
556
+ ]
557
+ );
558
+ }
559
+
560
+ // src/commands/project.ts
561
+ import { readFileSync } from "fs";
562
+ function resolveAgentContext(text, file) {
563
+ if (file !== void 0 && text !== void 0) {
564
+ throw new Error("--agent-context and --agent-context-file cannot be used together");
565
+ }
566
+ if (file !== void 0) return readFileSync(file, "utf8");
567
+ return text;
568
+ }
569
+ function previewAgentContext(value) {
570
+ if (!value) return null;
571
+ const firstLine = value.split("\n")[0].trim();
572
+ const truncated = value.includes("\n") || firstLine.length > 80;
573
+ return truncated ? `${firstLine.slice(0, 79)}\u2026` : firstLine;
574
+ }
575
+ function registerProjectCommands(program2) {
576
+ const project = program2.command("project").description("Manage Onus projects");
577
+ project.command("list").description("List projects").option("-j, --json", "Output raw JSON").action(async function() {
578
+ const globals = this.optsWithGlobals();
579
+ try {
580
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
581
+ const data = await client.projectsGet();
582
+ const items = Array.isArray(data) ? data : [];
583
+ printJsonOrFormatted(items, globals.json, () => {
584
+ if (items.length === 0) {
585
+ console.log("No projects found.");
586
+ return;
587
+ }
588
+ console.log(
589
+ formatRecordList(
590
+ items.map((p) => [
591
+ { label: "ID", value: p.id },
592
+ { label: "Name", value: p.name },
593
+ { label: "Description", value: p.description ?? null },
594
+ { label: "Status", value: p.status },
595
+ { label: "To Date", value: p.target_date },
596
+ { label: "Lead", value: p.lead?.full_name ?? (p.lead_id ? p.lead_id : null) }
597
+ ])
598
+ )
599
+ );
600
+ });
601
+ } catch (err) {
602
+ await handleApiError(err, globals.json);
603
+ }
604
+ });
605
+ project.command("get <id>").description("Get a single project by UUID").option("-j, --json", "Output raw JSON").action(async function(id) {
606
+ const globals = this.optsWithGlobals();
607
+ try {
608
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
609
+ const project2 = await client.projectsIdGet({ id });
610
+ printJsonOrFormatted(project2, globals.json, () => {
611
+ console.log(
612
+ formatRecord([
613
+ { label: "ID", value: project2.id },
614
+ { label: "Name", value: project2.name },
615
+ { label: "Description", value: project2.description ?? null },
616
+ { label: "Agent context", value: previewAgentContext(project2.agent_context) },
617
+ { label: "Status", value: project2.status },
618
+ { label: "To Date", value: project2.target_date }
619
+ ])
620
+ );
621
+ });
622
+ } catch (err) {
623
+ await handleApiError(err, globals.json);
624
+ }
625
+ });
626
+ project.command("create").description("Create a new project").requiredOption("--name <name>", "Project name").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--status <status>", "Project status (backlog|planned|in_progress|completed|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--lead <leadId>", "Lead user UUID").option("--target-date <date>", "Target completion date (ISO)").option("--description <text>", "Project description (rendered under the title)").option(
627
+ "--agent-context <text>",
628
+ "Agent context markdown (agents.md-style guidance; '' clears)"
629
+ ).option("--agent-context-file <path>", "Read agent context markdown from a file").option("-j, --json", "Output raw JSON").action(async function(opts) {
630
+ const globals = this.optsWithGlobals();
631
+ try {
632
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
633
+ const teamId = await resolveTeamId(client, opts.team);
634
+ const agentContext = resolveAgentContext(opts.agentContext, opts.agentContextFile);
635
+ const project2 = await client.projectsPost({
636
+ projectsPostRequest: {
637
+ team_id: teamId,
638
+ name: opts.name,
639
+ status: opts.status,
640
+ priority: opts.priority,
641
+ lead_id: opts.lead,
642
+ target_date: opts.targetDate,
643
+ description: opts.description,
644
+ agent_context: agentContext
645
+ }
646
+ });
647
+ printJsonOrFormatted(project2, globals.json, () => {
648
+ formatSuccess(`Created project "${project2.name}" (${project2.id}).`);
649
+ });
650
+ } catch (err) {
651
+ await handleApiError(err, globals.json);
652
+ }
653
+ });
654
+ project.command("update <id>").description("Update an existing project by UUID").option("--name <name>", "Project name").option("--status <status>", "Project status (backlog|planned|in_progress|completed|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--lead <leadId>", "Lead user UUID").option("--target-date <date>", "Target completion date (ISO)").option("--description <text>", "Project description (rendered under the title)").option(
655
+ "--agent-context <text>",
656
+ "Agent context markdown (agents.md-style guidance; '' clears)"
657
+ ).option("--agent-context-file <path>", "Read agent context markdown from a file").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
658
+ const globals = this.optsWithGlobals();
659
+ try {
660
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
661
+ const agentContext = resolveAgentContext(opts.agentContext, opts.agentContextFile);
662
+ const project2 = await client.projectsIdPatch({
663
+ id,
664
+ projectsIdPatchRequest: {
665
+ name: opts.name,
666
+ status: opts.status,
667
+ priority: opts.priority,
668
+ lead_id: opts.lead,
669
+ target_date: opts.targetDate,
670
+ description: opts.description,
671
+ agent_context: agentContext
672
+ }
673
+ });
674
+ printJsonOrFormatted(project2, globals.json, () => {
675
+ formatSuccess(`Updated project "${project2.name}" (${project2.id}).`);
676
+ });
677
+ } catch (err) {
678
+ await handleApiError(err, globals.json);
679
+ }
680
+ });
681
+ const attachment = project.command("attachment").description("List attachments associated with a project");
682
+ attachment.command("list <id>").description("List attachments on a project").option("-j, --json", "Output raw JSON").action(async function(id) {
683
+ const globals = this.optsWithGlobals();
684
+ try {
685
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
686
+ const items = await client.projectsIdAttachmentsGet({ id });
687
+ printJsonOrFormatted(items, globals.json, () => {
688
+ if (items.length === 0) {
689
+ console.log("No attachments found.");
690
+ return;
691
+ }
692
+ console.log(
693
+ formatRecordList(
694
+ items.map((a) => [
695
+ { label: "ID", value: a.id },
696
+ { label: "Filename", value: a.filename },
697
+ { label: "Type", value: a.content_type },
698
+ { label: "Size", value: a.size_bytes == null ? null : String(a.size_bytes) },
699
+ { label: "Uploader", value: a.uploader?.full_name }
700
+ ])
701
+ )
702
+ );
703
+ });
704
+ } catch (err) {
705
+ await handleApiError(err, globals.json);
706
+ }
707
+ });
708
+ withExamples(
709
+ project.commands.find((c) => c.name() === "list"),
710
+ ["onus project list", "onus project list --json"]
711
+ );
712
+ withExamples(
713
+ project.commands.find((c) => c.name() === "get"),
714
+ ["onus project get <projectId>", "onus project get <projectId> --json"]
715
+ );
716
+ withExamples(
717
+ project.commands.find((c) => c.name() === "create"),
718
+ [
719
+ 'onus project create --name "Website" --team ONUS',
720
+ 'onus project create --name "Mobile" --team ONUS --priority high --target-date <YYYY-MM-DD>',
721
+ 'onus project create --name "Migration" --team ONUS --agent-context-file AGENTS.md'
722
+ ]
723
+ );
724
+ withExamples(
725
+ project.commands.find((c) => c.name() === "update"),
726
+ [
727
+ "onus project update <projectId> --status in_progress",
728
+ "onus project update <projectId> --agent-context 'Add debug logging to all new code'",
729
+ "onus project update <projectId> --agent-context '' # clears agent context"
730
+ ]
731
+ );
732
+ withExamples(
733
+ attachment.commands.find((c) => c.name() === "list"),
734
+ ["onus project attachment list <projectId>", "onus project attachment list <projectId> --json"]
735
+ );
736
+ }
737
+
738
+ // src/commands/issue.ts
739
+ import { readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
740
+ import { basename } from "path";
741
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
742
+ var CLEAR_SENTINELS = /* @__PURE__ */ new Set(["null", "none", ""]);
743
+ function isClearSentinel(value) {
744
+ return CLEAR_SENTINELS.has(value.trim().toLowerCase());
745
+ }
746
+ function collect(value, previous) {
747
+ return [...previous ?? [], value];
748
+ }
749
+ function refToRelationTarget(ref) {
750
+ return UUID_RE.test(ref) ? { related_issue_id: ref } : { related_issue_ref: ref };
751
+ }
752
+ var MIME_BY_EXT = {
753
+ ".jpg": "image/jpeg",
754
+ ".jpeg": "image/jpeg",
755
+ ".png": "image/png",
756
+ ".gif": "image/gif",
757
+ ".webp": "image/webp",
758
+ ".svg": "image/svg+xml",
759
+ ".pdf": "application/pdf",
760
+ ".txt": "text/plain",
761
+ ".md": "text/markdown",
762
+ ".json": "application/json",
763
+ ".csv": "text/csv",
764
+ ".zip": "application/zip",
765
+ ".mp4": "video/mp4",
766
+ ".mp3": "audio/mpeg"
767
+ };
768
+ function contentTypeFor(filename) {
769
+ const dot = filename.lastIndexOf(".");
770
+ if (dot >= 0) {
771
+ const guess = MIME_BY_EXT[filename.slice(dot).toLowerCase()];
772
+ if (guess) return guess;
773
+ }
774
+ return "application/octet-stream";
775
+ }
776
+ function registerIssueCommands(program2) {
777
+ const issue = program2.command("issue").description("Manage Onus issues");
778
+ issue.command("list").description("List and filter issues").option("-j, --json", "Output raw JSON").option("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--project <projectId>", "Filter by project UUID").option("--assignee <assigneeId>", "Filter by assignee UUID").option("--parent <parentId>", "Filter by parent issue UUID").option("--status <status>", "Filter by status (backlog|todo|in_progress|done|canceled)").option("--q <query>", "Free-text search query").option(
779
+ "--created-after <date>",
780
+ "Inclusive lower bound on created_at (yyyy-MM-dd or ISO datetime)"
781
+ ).option(
782
+ "--created-before <date>",
783
+ "Inclusive upper bound on created_at (yyyy-MM-dd or ISO datetime)"
784
+ ).option(
785
+ "--updated-after <date>",
786
+ "Inclusive lower bound on updated_at (yyyy-MM-dd or ISO datetime)"
787
+ ).option(
788
+ "--updated-before <date>",
789
+ "Inclusive upper bound on updated_at (yyyy-MM-dd or ISO datetime)"
790
+ ).option(
791
+ "--completed-after <date>",
792
+ "Inclusive lower bound on completed_at (yyyy-MM-dd or ISO datetime)"
793
+ ).option(
794
+ "--completed-before <date>",
795
+ "Inclusive upper bound on completed_at (yyyy-MM-dd or ISO datetime)"
796
+ ).option("--due-after <date>", "Inclusive lower bound on due_date (yyyy-MM-dd)").option("--due-before <date>", "Inclusive upper bound on due_date (yyyy-MM-dd)").option("--limit <n>", "Limit result count", (v) => Number(v)).option("--cursor <cursor>", "Pagination cursor").action(async function(opts) {
797
+ const globals = this.optsWithGlobals();
798
+ try {
799
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
800
+ const teamId = opts.team ? await resolveTeamId(client, opts.team) : void 0;
801
+ const response = await client.issuesGet({
802
+ status: opts.status,
803
+ teamId,
804
+ assigneeId: opts.assignee,
805
+ projectId: opts.project,
806
+ parentId: opts.parent,
807
+ q: opts.q,
808
+ createdAfter: opts.createdAfter,
809
+ createdBefore: opts.createdBefore,
810
+ updatedAfter: opts.updatedAfter,
811
+ updatedBefore: opts.updatedBefore,
812
+ completedAfter: opts.completedAfter,
813
+ completedBefore: opts.completedBefore,
814
+ dueAfter: opts.dueAfter,
815
+ dueBefore: opts.dueBefore,
816
+ limit: opts.limit,
817
+ cursor: opts.cursor
818
+ });
819
+ const items = response?.data ?? [];
820
+ printJsonOrFormatted(response, globals.json, () => {
821
+ if (items.length === 0) {
822
+ console.log("No issues found.");
823
+ return;
824
+ }
825
+ for (const issue2 of items) {
826
+ const ref = issue2.identifier != null ? `#${issue2.identifier}` : issue2.id;
827
+ const parts = [
828
+ issue2.status,
829
+ issue2.priority,
830
+ issue2.team?.name,
831
+ issue2.assignee?.full_name
832
+ ].filter((p) => p != null && p !== "");
833
+ console.log(`\u2022 ${ref} ${sanitizeTerminal(issue2.title ?? "\u2014")}`);
834
+ if (parts.length > 0) {
835
+ console.log(` ${parts.map((p) => sanitizeTerminal(String(p))).join(" \xB7 ")}`);
836
+ }
837
+ }
838
+ if (response?.next_cursor) {
839
+ console.log(`
840
+ Next cursor: ${sanitizeTerminal(response.next_cursor)}`);
841
+ }
842
+ });
843
+ } catch (err) {
844
+ await handleApiError(err, globals.json);
845
+ }
846
+ });
847
+ issue.command("get <ref>").alias("view").description("Get full issue detail (reference like TEAM-42, or UUID)").option("-j, --json", "Output raw JSON").action(async function(id) {
848
+ const globals = this.optsWithGlobals();
849
+ try {
850
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
851
+ const issue2 = await client.issuesIdGet({ id });
852
+ printJsonOrFormatted(issue2, globals.json, () => {
853
+ console.log(
854
+ formatRecord([
855
+ { label: "ID", value: issue2.id },
856
+ { label: "Title", value: issue2.title },
857
+ { label: "Status", value: issue2.status },
858
+ { label: "Priority", value: issue2.priority },
859
+ {
860
+ label: "Identifier",
861
+ value: issue2.identifier != null ? String(issue2.identifier) : null
862
+ }
863
+ ])
864
+ );
865
+ if (issue2.description) {
866
+ console.log(`
867
+ Description:
868
+ ${sanitizeTerminal(issue2.description)}`);
869
+ }
870
+ if (issue2.comments?.length) {
871
+ console.log(`
872
+ Comments (${issue2.comments.length}):`);
873
+ for (const comment of issue2.comments) {
874
+ console.log(
875
+ ` [${comment.id}] ${sanitizeTerminal(
876
+ comment.author?.full_name ?? "\u2014"
877
+ )}: ${sanitizeTerminal(comment.body)}`
878
+ );
879
+ }
880
+ }
881
+ });
882
+ } catch (err) {
883
+ await handleApiError(err, globals.json);
884
+ }
885
+ });
886
+ issue.command("create").description("Create a new issue").option("--title <title>", "Issue title").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("-d, --description <description>", "Issue description (markdown)").option("--status <status>", "Status (backlog|todo|in_progress|done|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--assignee <assigneeId>", "Assignee UUID").option("--project <projectId>", "Project UUID").option("--parent <parentId>", "Parent issue UUID").option("--label <labelId>", "Label UUID to attach (repeatable)", collect, []).option(
887
+ "--template <nameOrId>",
888
+ "Issue template: UUID or exact name within the team; fills omitted fields"
889
+ ).option("-j, --json", "Output raw JSON").action(async function(opts) {
890
+ const globals = this.optsWithGlobals();
891
+ try {
892
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
893
+ const teamId = await resolveTeamId(client, opts.team);
894
+ let title = opts.title;
895
+ let description = opts.description;
896
+ let priority = opts.priority;
897
+ let labels = (opts.label ?? []).length > 0 ? opts.label : void 0;
898
+ if (opts.template) {
899
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
900
+ opts.template
901
+ );
902
+ let template;
903
+ if (isUuid) {
904
+ try {
905
+ template = await client.templatesIdGet({ id: opts.template });
906
+ } catch {
907
+ template = void 0;
908
+ }
909
+ }
910
+ if (!template) {
911
+ const teamTemplates = await client.templatesGet({ teamId });
912
+ template = (teamTemplates ?? []).find((t) => t.name === opts.template);
913
+ }
914
+ if (!template) {
915
+ throw new Error(`unknown template "${opts.template}"`);
916
+ }
917
+ if (title === void 0 && template.title) title = template.title;
918
+ if (description === void 0 && template.description) description = template.description;
919
+ if (priority === void 0 && template.priority && template.priority !== "none") {
920
+ priority = template.priority;
921
+ }
922
+ if (labels === void 0 && (template.preset_label_ids ?? []).length > 0) {
923
+ labels = template.preset_label_ids;
924
+ }
925
+ }
926
+ if (title === void 0 || String(title).trim().length === 0) {
927
+ throw new Error("title is required (or provide --template that defines one)");
928
+ }
929
+ const issue2 = await client.issuesPost({
930
+ issuesPostRequest: {
931
+ team_id: teamId,
932
+ title,
933
+ description,
934
+ status: opts.status,
935
+ priority,
936
+ assignee_id: opts.assignee,
937
+ project_id: opts.project,
938
+ parent_id: opts.parent,
939
+ labels
940
+ }
941
+ });
942
+ printJsonOrFormatted(issue2, globals.json, () => {
943
+ formatSuccess(`Created issue "${issue2.title}" (${issue2.id}).`);
944
+ });
945
+ } catch (err) {
946
+ await handleApiError(err, globals.json);
947
+ }
948
+ });
949
+ issue.command("update <ref>").alias("edit").description("Update (or edit) an existing issue by reference").option("--title <title>", "Issue title").option("-d, --description <description>", "Issue description").option("--status <status>", "Status (backlog|todo|in_progress|done|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--assignee <assigneeId>", "Assignee UUID").option("--project <projectId>", "Project UUID").option("--parent <parentId>", 'Parent issue UUID (pass "null", "none", or "" to clear)').option("--add-label <labelId>", "Label UUID to add (repeatable)", collect, []).option(
950
+ "--remove-label <labelId>",
951
+ "Label UUID to remove (repeatable)",
952
+ collect,
953
+ []
954
+ ).option("-j, --json", "Output raw JSON").action(async function(id, opts) {
955
+ const globals = this.optsWithGlobals();
956
+ try {
957
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
958
+ const patch = {};
959
+ if (opts.title !== void 0) patch.title = opts.title;
960
+ if (opts.description !== void 0) patch.description = opts.description;
961
+ if (opts.status !== void 0) patch.status = opts.status;
962
+ if (opts.priority !== void 0) patch.priority = opts.priority;
963
+ if (opts.assignee !== void 0) patch.assignee_id = opts.assignee;
964
+ if (opts.project !== void 0) patch.project_id = opts.project;
965
+ if (opts.parent !== void 0)
966
+ patch.parent_id = isClearSentinel(opts.parent) ? null : opts.parent;
967
+ let issue2;
968
+ if (Object.keys(patch).length > 0) {
969
+ issue2 = await client.issuesIdPatch({ id, issuesIdPatchRequest: patch });
970
+ } else {
971
+ issue2 = await client.issuesIdGet({ id });
972
+ }
973
+ for (const labelId of opts.addLabel ?? []) {
974
+ await client.issuesIdLabelsPost({ id, issuesIdLabelsPostRequest: { label_id: labelId } });
975
+ }
976
+ for (const labelId of opts.removeLabel ?? []) {
977
+ await client.issuesIdLabelsLabelIdDelete({ id, labelId });
978
+ }
979
+ printJsonOrFormatted(issue2, globals.json, () => {
980
+ formatSuccess(`Updated issue "${issue2.title}" (${issue2.id}).`);
981
+ });
982
+ } catch (err) {
983
+ await handleApiError(err, globals.json);
984
+ }
985
+ });
986
+ issue.command("delete <ref>").description("Delete an issue by reference").option("-y, --yes", "Skip confirmation").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
987
+ if (!opts.yes) {
988
+ console.error("Destructive operation. Re-run with --yes to confirm deletion.");
989
+ process.exit(2);
990
+ }
991
+ const globals = this.optsWithGlobals();
992
+ try {
993
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
994
+ await client.issuesIdDelete({ id });
995
+ if (globals.json) {
996
+ console.log(JSON.stringify({ ok: true, id }, null, 2));
997
+ } else {
998
+ formatSuccess(`Deleted issue ${id}.`);
999
+ }
1000
+ } catch (err) {
1001
+ await handleApiError(err, globals.json);
1002
+ }
1003
+ });
1004
+ withExamples(
1005
+ issue.commands.find((c) => c.name() === "list"),
1006
+ [
1007
+ "onus issue list",
1008
+ "onus issue list --status in_progress --team ONUS",
1009
+ "onus issue list --json"
1010
+ ]
1011
+ );
1012
+ withExamples(
1013
+ issue.commands.find((c) => c.name() === "get"),
1014
+ ["onus issue get <issueId>"]
1015
+ );
1016
+ withExamples(
1017
+ issue.commands.find((c) => c.name() === "create"),
1018
+ [
1019
+ 'onus issue create --title "Fix login bug" --team ONUS',
1020
+ 'onus issue create --title "Ship v2" --team ONUS --priority high --project <projectId>'
1021
+ ]
1022
+ );
1023
+ withExamples(
1024
+ issue.commands.find((c) => c.name() === "update"),
1025
+ [
1026
+ "onus issue update <issueId> --status done",
1027
+ "onus issue update <issueId> --priority urgent --assignee <assigneeId>"
1028
+ ]
1029
+ );
1030
+ withExamples(
1031
+ issue.commands.find((c) => c.name() === "delete"),
1032
+ ["onus issue delete <issueId> --yes"]
1033
+ );
1034
+ registerIssueCommentCommands(issue);
1035
+ registerIssueRelationCommands(issue);
1036
+ registerIssueAttachmentCommands(issue);
1037
+ registerIssueSubscriptionCommands(issue);
1038
+ registerIssuePrCommands(issue);
1039
+ }
1040
+ function registerIssueCommentCommands(issue) {
1041
+ const comment = issue.command("comment").description("Create, list, edit, and delete issue comments");
1042
+ comment.command("create <ref>").description("Post a comment on an issue").requiredOption("--body <body>", "Comment body text (markdown)").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1043
+ const globals = this.optsWithGlobals();
1044
+ try {
1045
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1046
+ const c = await client.issuesIdCommentsPost({
1047
+ id,
1048
+ commentsCommentIdPatchRequest: { body: opts.body }
1049
+ });
1050
+ printJsonOrFormatted(c, globals.json, () => formatSuccess(`Posted comment ${c.id}.`));
1051
+ } catch (err) {
1052
+ await handleApiError(err, globals.json);
1053
+ }
1054
+ });
1055
+ comment.command("list <ref>").description("List comments for an issue").option("-j, --json", "Output raw JSON").action(async function(id) {
1056
+ const globals = this.optsWithGlobals();
1057
+ try {
1058
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1059
+ const data = await client.issuesIdCommentsGet({ id });
1060
+ const items = Array.isArray(data) ? data : [];
1061
+ printJsonOrFormatted(items, globals.json, () => {
1062
+ if (items.length === 0) {
1063
+ console.log("No comments found.");
1064
+ return;
1065
+ }
1066
+ console.log(
1067
+ formatRecordList(
1068
+ items.map((c) => [
1069
+ { label: "ID", value: c.id },
1070
+ { label: "Author", value: c.author?.full_name },
1071
+ { label: "Created", value: c.created_at },
1072
+ { label: "Body", value: c.body }
1073
+ ])
1074
+ )
1075
+ );
1076
+ });
1077
+ } catch (err) {
1078
+ await handleApiError(err, globals.json);
1079
+ }
1080
+ });
1081
+ comment.command("edit <ref>").description("Edit an existing comment").requiredOption("--comment <commentId>", "Comment UUID").requiredOption("--body <body>", "New comment body text").option("-j, --json", "Output raw JSON").action(async function(_id, opts) {
1082
+ const globals = this.optsWithGlobals();
1083
+ try {
1084
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1085
+ const c = await client.commentsCommentIdPatch({
1086
+ commentId: opts.comment,
1087
+ commentsCommentIdPatchRequest: { body: opts.body }
1088
+ });
1089
+ printJsonOrFormatted(c, globals.json, () => formatSuccess(`Updated comment ${c.id}.`));
1090
+ } catch (err) {
1091
+ await handleApiError(err, globals.json);
1092
+ }
1093
+ });
1094
+ comment.command("delete <ref>").description("Delete a comment").requiredOption("--comment <commentId>", "Comment UUID").option("-j, --json", "Output raw JSON").action(async function(_id, opts) {
1095
+ const globals = this.optsWithGlobals();
1096
+ try {
1097
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1098
+ await client.commentsCommentIdDelete({ commentId: opts.comment });
1099
+ if (globals.json) {
1100
+ console.log(JSON.stringify({ ok: true, commentId: opts.comment }, null, 2));
1101
+ } else {
1102
+ formatSuccess(`Deleted comment ${opts.comment}.`);
1103
+ }
1104
+ } catch (err) {
1105
+ await handleApiError(err, globals.json);
1106
+ }
1107
+ });
1108
+ withExamples(
1109
+ comment.commands.find((c) => c.name() === "create"),
1110
+ ['onus issue comment create <issueId> --body "Thanks for the fix!"']
1111
+ );
1112
+ withExamples(
1113
+ comment.commands.find((c) => c.name() === "list"),
1114
+ ["onus issue comment list <issueId>"]
1115
+ );
1116
+ withExamples(
1117
+ comment.commands.find((c) => c.name() === "edit"),
1118
+ ['onus issue comment edit <issueId> --comment <commentId> --body "Updated text"']
1119
+ );
1120
+ withExamples(
1121
+ comment.commands.find((c) => c.name() === "delete"),
1122
+ ["onus issue comment delete <issueId> --comment <commentId>"]
1123
+ );
1124
+ }
1125
+ function registerIssueRelationCommands(issue) {
1126
+ const relation = issue.command("relation").description("Manage issue relations");
1127
+ relation.command("add <ref>").description("Relate one issue to another").requiredOption("--with <ref>", "Target issue (TEAM-42 or UUID)").requiredOption("--type <type>", "Relation type (blocks|duplicate|related|similar)").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1128
+ const globals = this.optsWithGlobals();
1129
+ try {
1130
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1131
+ const row = await client.issuesIdRelationsPost({
1132
+ id,
1133
+ issuesIdRelationsPostRequest: {
1134
+ ...refToRelationTarget(opts.with),
1135
+ type: opts.type
1136
+ }
1137
+ });
1138
+ printJsonOrFormatted(row, globals.json, () => {
1139
+ formatSuccess(`Added ${opts.type} relation (${row.id}).`);
1140
+ });
1141
+ } catch (err) {
1142
+ await handleApiError(err, globals.json);
1143
+ }
1144
+ });
1145
+ relation.command("remove <ref>").description("Remove a relation from an issue").requiredOption("--relation <relationId>", "Relation UUID").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1146
+ const globals = this.optsWithGlobals();
1147
+ try {
1148
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1149
+ await client.issuesIdRelationsRelationIdDelete({ id, relationId: opts.relation });
1150
+ if (globals.json) {
1151
+ console.log(JSON.stringify({ ok: true, relationId: opts.relation }, null, 2));
1152
+ } else {
1153
+ formatSuccess(`Removed relation ${opts.relation}.`);
1154
+ }
1155
+ } catch (err) {
1156
+ await handleApiError(err, globals.json);
1157
+ }
1158
+ });
1159
+ withExamples(
1160
+ relation.commands.find((c) => c.name() === "add"),
1161
+ ["onus issue relation add <issueId> --with TEAM-42 --type blocks"]
1162
+ );
1163
+ withExamples(
1164
+ relation.commands.find((c) => c.name() === "remove"),
1165
+ ["onus issue relation remove <issueId> --relation <relationId>"]
1166
+ );
1167
+ }
1168
+ function registerIssueAttachmentCommands(issue) {
1169
+ const attachment = issue.command("attachment").description("Upload, download, list, and delete issue attachments");
1170
+ attachment.command("upload <ref>").description("Upload a file attachment to an issue").requiredOption("--file <path>", "Path to the file to upload").option("--name <name>", "Override stored filename").option("--type <type>", "Override content type").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1171
+ const globals = this.optsWithGlobals();
1172
+ try {
1173
+ const filename = opts.name ?? basename(opts.file);
1174
+ const contentType = opts.type ?? contentTypeFor(filename);
1175
+ const stat = statSync(opts.file);
1176
+ const sizeBytes = stat.size;
1177
+ const content = readFileSync2(opts.file);
1178
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1179
+ const upload = await client.issuesIdAttachmentsPost({
1180
+ id,
1181
+ issuesIdAttachmentsPostRequest: {
1182
+ filename,
1183
+ content_type: contentType,
1184
+ size_bytes: sizeBytes
1185
+ }
1186
+ });
1187
+ const res = await fetch(upload.upload_url, {
1188
+ method: "PUT",
1189
+ body: content,
1190
+ headers: { "Content-Type": contentType, "x-upsert": "false" }
1191
+ });
1192
+ if (!res.ok) throw new Error(`upload failed with HTTP ${res.status}`);
1193
+ printJsonOrFormatted(upload, globals.json, () => {
1194
+ formatSuccess(`Uploaded "${filename}" (${upload.id}).`);
1195
+ });
1196
+ } catch (err) {
1197
+ await handleApiError(err, globals.json);
1198
+ }
1199
+ });
1200
+ attachment.command("download <ref>").description("Download an attachment to a local file").requiredOption("--attachment <attachmentId>", "Attachment UUID").option("--output <path>", "Output file path (defaults to the attachment filename)").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1201
+ const globals = this.optsWithGlobals();
1202
+ try {
1203
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1204
+ const { download_url } = await client.attachmentsIdDownloadUrlGet({ id: opts.attachment });
1205
+ const res = await fetch(download_url);
1206
+ if (!res.ok) throw new Error(`download failed with HTTP ${res.status}`);
1207
+ const buffer = Buffer.from(await res.arrayBuffer());
1208
+ let output = opts.output;
1209
+ if (!output) {
1210
+ const issue2 = await client.issuesIdGet({ id });
1211
+ const attach = (issue2.attachments ?? []).find((a) => a.id === opts.attachment);
1212
+ output = attach?.filename ?? opts.attachment;
1213
+ }
1214
+ writeFileSync(output, buffer);
1215
+ printJsonOrFormatted({ ok: true, path: output, bytes: buffer.length }, globals.json, () => {
1216
+ formatSuccess(`Downloaded ${buffer.length} bytes to ${output}.`);
1217
+ });
1218
+ } catch (err) {
1219
+ await handleApiError(err, globals.json);
1220
+ }
1221
+ });
1222
+ attachment.command("list <ref>").description("List attachments on an issue").option("-j, --json", "Output raw JSON").action(async function(id) {
1223
+ const globals = this.optsWithGlobals();
1224
+ try {
1225
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1226
+ const issue2 = await client.issuesIdGet({ id });
1227
+ const items = issue2.attachments ?? [];
1228
+ printJsonOrFormatted(items, globals.json, () => {
1229
+ if (items.length === 0) {
1230
+ console.log("No attachments found.");
1231
+ return;
1232
+ }
1233
+ console.log(
1234
+ formatRecordList(
1235
+ items.map((a) => [
1236
+ { label: "ID", value: a.id },
1237
+ { label: "Filename", value: a.filename },
1238
+ { label: "Type", value: a.content_type },
1239
+ { label: "Size", value: a.size_bytes != null ? String(a.size_bytes) : null },
1240
+ { label: "Uploader", value: a.uploader?.full_name }
1241
+ ])
1242
+ )
1243
+ );
1244
+ });
1245
+ } catch (err) {
1246
+ await handleApiError(err, globals.json);
1247
+ }
1248
+ });
1249
+ attachment.command("delete <ref>").description("Delete an attachment from an issue").requiredOption("--attachment <attachmentId>", "Attachment UUID").option("-j, --json", "Output raw JSON").action(async function(_id, opts) {
1250
+ const globals = this.optsWithGlobals();
1251
+ try {
1252
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1253
+ await client.attachmentsIdDelete({ id: opts.attachment });
1254
+ if (globals.json) {
1255
+ console.log(JSON.stringify({ ok: true, attachmentId: opts.attachment }, null, 2));
1256
+ } else {
1257
+ formatSuccess(`Deleted attachment ${opts.attachment}.`);
1258
+ }
1259
+ } catch (err) {
1260
+ await handleApiError(err, globals.json);
1261
+ }
1262
+ });
1263
+ withExamples(
1264
+ attachment.commands.find((c) => c.name() === "upload"),
1265
+ ["onus issue attachment upload <issueId> --file ./report.pdf"]
1266
+ );
1267
+ withExamples(
1268
+ attachment.commands.find((c) => c.name() === "download"),
1269
+ ["onus issue attachment download <issueId> --attachment <attachmentId>"]
1270
+ );
1271
+ withExamples(
1272
+ attachment.commands.find((c) => c.name() === "list"),
1273
+ ["onus issue attachment list <issueId>"]
1274
+ );
1275
+ withExamples(
1276
+ attachment.commands.find((c) => c.name() === "delete"),
1277
+ ["onus issue attachment delete <issueId> --attachment <attachmentId>"]
1278
+ );
1279
+ }
1280
+ function registerIssueSubscriptionCommands(issue) {
1281
+ issue.command("subscribe <ref>").description("Subscribe the current user to an issue").option("-j, --json", "Output raw JSON").action(async function(id) {
1282
+ const globals = this.optsWithGlobals();
1283
+ try {
1284
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1285
+ const res = await client.issuesIdSubscriptionPost({
1286
+ id,
1287
+ issuesIdSubscriptionPostRequest: { isSubscribed: true }
1288
+ });
1289
+ printJsonOrFormatted(res, globals.json, () => formatSuccess("Subscribed."));
1290
+ } catch (err) {
1291
+ await handleApiError(err, globals.json);
1292
+ }
1293
+ });
1294
+ issue.command("unsubscribe <ref>").description("Unsubscribe the current user from an issue").option("-j, --json", "Output raw JSON").action(async function(id) {
1295
+ const globals = this.optsWithGlobals();
1296
+ try {
1297
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1298
+ const res = await client.issuesIdSubscriptionPost({
1299
+ id,
1300
+ issuesIdSubscriptionPostRequest: { isSubscribed: false }
1301
+ });
1302
+ printJsonOrFormatted(res, globals.json, () => formatSuccess("Unsubscribed."));
1303
+ } catch (err) {
1304
+ await handleApiError(err, globals.json);
1305
+ }
1306
+ });
1307
+ withExamples(
1308
+ issue.commands.find((c) => c.name() === "subscribe"),
1309
+ ["onus issue subscribe <issueId>"]
1310
+ );
1311
+ withExamples(
1312
+ issue.commands.find((c) => c.name() === "unsubscribe"),
1313
+ ["onus issue unsubscribe <issueId>"]
1314
+ );
1315
+ registerIssueBulkCommands(issue);
1316
+ }
1317
+ var BULK_CHUNK = 100;
1318
+ async function runBulkChunks(client, request, idempotencyKey) {
1319
+ const results = [];
1320
+ let succeeded = 0;
1321
+ let failed = 0;
1322
+ const init = idempotencyKey ? { headers: { "Idempotency-Key": idempotencyKey } } : void 0;
1323
+ for (let i = 0; i < request.items.length; i += BULK_CHUNK) {
1324
+ const chunk = request.items.slice(i, i + BULK_CHUNK);
1325
+ const res = await client.issuesBulkPost(
1326
+ { issuesBulkPostRequest: { ...request, items: chunk } },
1327
+ ...init ? [init] : []
1328
+ );
1329
+ results.push(...res.results ?? []);
1330
+ succeeded += res.succeeded ?? 0;
1331
+ failed += res.failed ?? 0;
1332
+ }
1333
+ return { results, succeeded, failed };
1334
+ }
1335
+ function registerIssueBulkCommands(issue) {
1336
+ issue.command("bulk-update [refs...]").description("Batched update of issues with per-item partial success").option("--status <status>", "New status (backlog|todo|in_progress|done|canceled)").option("--priority <priority>", "New priority (none|urgent|high|medium|low)").option("--assignee <assigneeId>", "New assignee UUID").option("--project <projectId>", "New project UUID").option("--label <labelId>", "Label UUID; full replacement set (repeatable)", collect).option("--file <path>", "JSON file with extra update items [{ref, set}]").option("-j, --json", "Output raw JSON").action(async function(refs, opts) {
1337
+ const globals = this.optsWithGlobals();
1338
+ try {
1339
+ const set = {};
1340
+ if (opts.status) set.status = opts.status;
1341
+ if (opts.priority) set.priority = opts.priority;
1342
+ if (opts.assignee) set.assignee_id = opts.assignee;
1343
+ if (opts.project) set.project_id = opts.project;
1344
+ if (opts.label?.length) set.labels = opts.label;
1345
+ let items = refs.map((ref) => ({ ref, set }));
1346
+ if (opts.file) {
1347
+ const extra = JSON.parse(readFileSync2(opts.file, "utf8"));
1348
+ items = items.concat(extra);
1349
+ }
1350
+ if (items.length === 0) throw new Error("no items provided");
1351
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1352
+ const out = await runBulkChunks(client, { operation: "update", items });
1353
+ printJsonOrFormatted(out, globals.json, () => {
1354
+ console.log(`${out.succeeded} succeeded, ${out.failed} failed.`);
1355
+ for (const r of out.results.filter((r2) => !r2.ok)) {
1356
+ console.error(`${r.index}: ${r.error?.code} \u2014 ${r.error?.message}`);
1357
+ }
1358
+ });
1359
+ } catch (err) {
1360
+ await handleApiError(err, globals.json);
1361
+ }
1362
+ });
1363
+ issue.command("bulk-comment [refs...]").description("Batched comments on issues with dedup + idempotency support").requiredOption("--body <text>", "Comment body applied to every ref").option("--file <path>", "JSON file with extra comment items [{ref, body, client_token?}]").option("--idempotency-key <key>", "Retry-safe key: replays the stored response for 24h").option("-j, --json", "Output raw JSON").action(async function(refs, opts) {
1364
+ const globals = this.optsWithGlobals();
1365
+ try {
1366
+ let items = refs.map((ref) => ({ ref, body: opts.body }));
1367
+ if (opts.file) {
1368
+ const extra = JSON.parse(readFileSync2(opts.file, "utf8"));
1369
+ items = items.concat(extra);
1370
+ }
1371
+ if (items.length === 0) throw new Error("no items provided");
1372
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1373
+ const out = await runBulkChunks(
1374
+ client,
1375
+ { operation: "comment", items },
1376
+ opts.idempotencyKey
1377
+ );
1378
+ printJsonOrFormatted(out, globals.json, () => {
1379
+ console.log(`${out.succeeded} succeeded, ${out.failed} failed.`);
1380
+ for (const r of out.results.filter((r2) => !r2.ok)) {
1381
+ console.error(`${r.index}: ${r.error?.code} \u2014 ${r.error?.message}`);
1382
+ }
1383
+ });
1384
+ } catch (err) {
1385
+ await handleApiError(err, globals.json);
1386
+ }
1387
+ });
1388
+ withExamples(
1389
+ issue.commands.find((c) => c.name() === "bulk-update"),
1390
+ [
1391
+ "onus issue bulk-update ENG-42 ENG-43 --status done",
1392
+ "onus issue bulk-update ENG-42 --priority high --label <uuid>",
1393
+ "onus issue bulk-update --file items.json"
1394
+ ]
1395
+ );
1396
+ withExamples(
1397
+ issue.commands.find((c) => c.name() === "bulk-comment"),
1398
+ ['onus issue bulk-comment ENG-42 ENG-43 --body "Deployed" --idempotency-key run-17']
1399
+ );
1400
+ }
1401
+ function registerIssuePrCommands(issue) {
1402
+ issue.command("link-pr <ref>").description("Manually link an issue to a GitHub pull request").requiredOption("--repo <owner/name>", "GitHub repository (owner/name)").requiredOption("--pr <number>", "Pull request number", (v) => Number(v)).option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1403
+ const globals = this.optsWithGlobals();
1404
+ try {
1405
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1406
+ const res = await client.issuesIdPullRequestsPost({
1407
+ id,
1408
+ issuesIdPullRequestsPostRequest: { repo: opts.repo, number: opts.pr }
1409
+ });
1410
+ printJsonOrFormatted(res, globals.json, () => {
1411
+ formatSuccess(`Linked PR ${opts.repo}#${opts.pr} (link ${res.id}).`);
1412
+ });
1413
+ } catch (err) {
1414
+ await handleApiError(err, globals.json);
1415
+ }
1416
+ });
1417
+ issue.command("unlink-pr <ref>").description("Unlink a GitHub pull request from an issue").requiredOption("--pr-link <linkId>", "Pull request link UUID").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1418
+ const globals = this.optsWithGlobals();
1419
+ try {
1420
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1421
+ await client.issuesIdPullRequestsLinkIdDelete({ id, linkId: opts.prLink });
1422
+ if (globals.json) {
1423
+ console.log(JSON.stringify({ ok: true, linkId: opts.prLink }, null, 2));
1424
+ } else {
1425
+ formatSuccess(`Unlinked PR (${opts.prLink}).`);
1426
+ }
1427
+ } catch (err) {
1428
+ await handleApiError(err, globals.json);
1429
+ }
1430
+ });
1431
+ withExamples(
1432
+ issue.commands.find((c) => c.name() === "link-pr"),
1433
+ ["onus issue link-pr <issueId> --repo owner/repo --pr 42"]
1434
+ );
1435
+ withExamples(
1436
+ issue.commands.find((c) => c.name() === "unlink-pr"),
1437
+ ["onus issue unlink-pr <issueId> --pr-link <linkId>"]
1438
+ );
1439
+ }
1440
+
1441
+ // src/commands/comment.ts
1442
+ function registerCommentCommands(program2) {
1443
+ const comment = program2.command("comment").description("List and create issue comments");
1444
+ comment.command("list <issueId>").description("List comments for an issue").option("-j, --json", "Output raw JSON").action(async function(issueId) {
1445
+ const globals = this.optsWithGlobals();
1446
+ try {
1447
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1448
+ const data = await client.issuesIdCommentsGet({ id: issueId });
1449
+ const items = Array.isArray(data) ? data : [];
1450
+ printJsonOrFormatted(items, globals.json, () => {
1451
+ if (items.length === 0) {
1452
+ console.log("No comments found.");
1453
+ return;
1454
+ }
1455
+ console.log(
1456
+ formatRecordList(
1457
+ items.map((c) => [
1458
+ { label: "ID", value: c.id },
1459
+ { label: "Author", value: c.author?.full_name },
1460
+ { label: "Created", value: c.created_at },
1461
+ { label: "Body", value: c.body }
1462
+ ])
1463
+ )
1464
+ );
1465
+ });
1466
+ } catch (err) {
1467
+ await handleApiError(err, globals.json);
1468
+ }
1469
+ });
1470
+ comment.command("create <issueId>").description("Post a comment on an issue").requiredOption("--body <body>", "Comment body text (markdown)").option("-j, --json", "Output raw JSON").action(async function(issueId, opts) {
1471
+ const globals = this.optsWithGlobals();
1472
+ try {
1473
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1474
+ const comment2 = await client.issuesIdCommentsPost({
1475
+ id: issueId,
1476
+ commentsCommentIdPatchRequest: { body: opts.body }
1477
+ });
1478
+ printJsonOrFormatted(comment2, globals.json, () => {
1479
+ formatSuccess(`Posted comment ${comment2.id}.`);
1480
+ });
1481
+ } catch (err) {
1482
+ await handleApiError(err, globals.json);
1483
+ }
1484
+ });
1485
+ withExamples(
1486
+ comment.commands.find((c) => c.name() === "list"),
1487
+ ["onus comment list <issueId>"]
1488
+ );
1489
+ withExamples(
1490
+ comment.commands.find((c) => c.name() === "create"),
1491
+ ['onus comment create <issueId> --body "Looks good to me"']
1492
+ );
1493
+ }
1494
+
1495
+ // src/commands/label.ts
1496
+ function registerLabelCommands(program2) {
1497
+ const label = program2.command("label").description("List and create issue labels");
1498
+ label.command("list").description("List all labels").option("-j, --json", "Output raw JSON").action(async function() {
1499
+ const globals = this.optsWithGlobals();
1500
+ try {
1501
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1502
+ const data = await client.labelsGet();
1503
+ const items = Array.isArray(data) ? data : [];
1504
+ printJsonOrFormatted(items, globals.json, () => {
1505
+ if (items.length === 0) {
1506
+ console.log("No labels found.");
1507
+ return;
1508
+ }
1509
+ console.log(
1510
+ formatRecordList(
1511
+ items.map((l) => [
1512
+ { label: "ID", value: l.id },
1513
+ { label: "Name", value: l.name },
1514
+ { label: "Team", value: l.team_id },
1515
+ { label: "Color", value: l.color }
1516
+ ])
1517
+ )
1518
+ );
1519
+ });
1520
+ } catch (err) {
1521
+ await handleApiError(err, globals.json);
1522
+ }
1523
+ });
1524
+ label.command("create").description("Create a new label").requiredOption("--name <name>", "Label name").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--color <color>", "Label color (hex)").option("-j, --json", "Output raw JSON").action(async function(opts) {
1525
+ const globals = this.optsWithGlobals();
1526
+ try {
1527
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1528
+ const teamId = await resolveTeamId(client, opts.team);
1529
+ const label2 = await client.labelsPost({
1530
+ labelsPostRequest: {
1531
+ team_id: teamId,
1532
+ name: opts.name,
1533
+ color: opts.color
1534
+ }
1535
+ });
1536
+ printJsonOrFormatted(label2, globals.json, () => {
1537
+ formatSuccess(`Created label "${label2.name}".`);
1538
+ });
1539
+ } catch (err) {
1540
+ await handleApiError(err, globals.json);
1541
+ }
1542
+ });
1543
+ withExamples(
1544
+ label.commands.find((c) => c.name() === "list"),
1545
+ ["onus label list", "onus label list --json"]
1546
+ );
1547
+ withExamples(
1548
+ label.commands.find((c) => c.name() === "create"),
1549
+ ['onus label create --name "Bug" --team ONUS --color #e11d48']
1550
+ );
1551
+ }
1552
+
1553
+ // src/commands/dsn.ts
1554
+ function registerDsnCommands(program2) {
1555
+ const dsn = program2.command("dsn").description("Manage team ingest keys (DSNs) \u2014 errors + analytics ingestion credentials");
1556
+ dsn.command("list").description("List a team's ingest keys (masked \u2014 the secret is never re-shown)").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("-j, --json", "Output raw JSON").action(async function(opts) {
1557
+ const globals = this.optsWithGlobals();
1558
+ try {
1559
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1560
+ const teamId = await resolveTeamId(client, opts.team);
1561
+ const items = await client.teamsIdIngestKeysGet({ id: teamId });
1562
+ printJsonOrFormatted(items, globals.json, () => {
1563
+ if (items.length === 0) {
1564
+ console.log("No ingest keys found.");
1565
+ return;
1566
+ }
1567
+ console.log(
1568
+ formatRecordList(
1569
+ items.map((k) => [
1570
+ { label: "ID", value: k.id },
1571
+ { label: "Label", value: k.label },
1572
+ { label: "Hint", value: k.key_hint },
1573
+ { label: "Status", value: k.status },
1574
+ { label: "DSN", value: k.dsn.replace(/^[^@]+/, (h) => `${h.slice(0, 4)}\u2026`) }
1575
+ ])
1576
+ )
1577
+ );
1578
+ });
1579
+ } catch (err) {
1580
+ await handleApiError(err, globals.json);
1581
+ }
1582
+ });
1583
+ dsn.command("create").description("Create an ingest key (the full secret is printed exactly once)").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--label <label>", "Key label", "Default key").option("-j, --json", "Output raw JSON").action(async function(opts) {
1584
+ const globals = this.optsWithGlobals();
1585
+ try {
1586
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1587
+ const teamId = await resolveTeamId(client, opts.team);
1588
+ const key = await client.teamsIdIngestKeysPost({
1589
+ id: teamId,
1590
+ teamsIdIngestKeysPostRequest: {
1591
+ label: opts.label ?? "Default key"
1592
+ }
1593
+ });
1594
+ printJsonOrFormatted(key, globals.json, () => {
1595
+ formatSuccess("Key created \u2014 store this secret now, it is shown only once:");
1596
+ console.log(key.dsn);
1597
+ });
1598
+ } catch (err) {
1599
+ await handleApiError(err, globals.json);
1600
+ }
1601
+ });
1602
+ dsn.command("rotate <keyId>").description("Rotate a key; the old secret stays valid for the grace window").option(
1603
+ "--grace-seconds <seconds>",
1604
+ "Zero-downtime cutover window (default 604800 = 7 days)",
1605
+ "604800"
1606
+ ).option("-j, --json", "Output raw JSON").action(async function(keyId, opts) {
1607
+ const globals = this.optsWithGlobals();
1608
+ try {
1609
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1610
+ const key = await client.ingestKeysIdRotatePost({
1611
+ id: keyId,
1612
+ ingestKeysIdRotatePostRequest: { grace_seconds: Number(opts.graceSeconds ?? 604800) }
1613
+ });
1614
+ printJsonOrFormatted(key, globals.json, () => {
1615
+ formatSuccess("Key rotated \u2014 the new secret is shown only once:");
1616
+ console.log(key.dsn);
1617
+ if (key.grace_until)
1618
+ console.log(`Old secret accepted until ${key.grace_until.toISOString()}`);
1619
+ });
1620
+ } catch (err) {
1621
+ await handleApiError(err, globals.json);
1622
+ }
1623
+ });
1624
+ dsn.command("disable <keyId>").description("Disable an ingest key (reversible)").option("-j, --json", "Output raw JSON").action(async function(keyId) {
1625
+ const globals = this.optsWithGlobals();
1626
+ try {
1627
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1628
+ await client.ingestKeysIdPatch({
1629
+ id: keyId,
1630
+ ingestKeysIdPatchRequest: { status: "disabled" }
1631
+ });
1632
+ printJsonOrFormatted({ ok: true }, globals.json, () => formatSuccess("Key disabled."));
1633
+ } catch (err) {
1634
+ await handleApiError(err, globals.json);
1635
+ }
1636
+ });
1637
+ dsn.command("enable <keyId>").description("Re-enable a disabled ingest key").option("-j, --json", "Output raw JSON").action(async function(keyId) {
1638
+ const globals = this.optsWithGlobals();
1639
+ try {
1640
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1641
+ await client.ingestKeysIdPatch({
1642
+ id: keyId,
1643
+ ingestKeysIdPatchRequest: { status: "active" }
1644
+ });
1645
+ printJsonOrFormatted({ ok: true }, globals.json, () => formatSuccess("Key enabled."));
1646
+ } catch (err) {
1647
+ await handleApiError(err, globals.json);
1648
+ }
1649
+ });
1650
+ dsn.command("revoke <keyId>").description("Revoke an ingest key (terminal)").option("-j, --json", "Output raw JSON").action(async function(keyId) {
1651
+ const globals = this.optsWithGlobals();
1652
+ try {
1653
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1654
+ await client.ingestKeysIdDelete({ id: keyId });
1655
+ printJsonOrFormatted({ ok: true }, globals.json, () => formatSuccess("Key revoked."));
1656
+ } catch (err) {
1657
+ await handleApiError(err, globals.json);
1658
+ }
1659
+ });
1660
+ withExamples(
1661
+ dsn.commands.find((c) => c.name() === "list"),
1662
+ ["onus dsn list --team ONUS", "onus dsn list --team <teamId> --json"]
1663
+ );
1664
+ withExamples(
1665
+ dsn.commands.find((c) => c.name() === "create"),
1666
+ ["onus dsn create --team ONUS --label ci", "onus dsn create --team <teamId> --label ci --json"]
1667
+ );
1668
+ withExamples(
1669
+ dsn.commands.find((c) => c.name() === "rotate"),
1670
+ ["onus dsn rotate <keyId>", "onus dsn rotate <keyId> --grace-seconds 0"]
1671
+ );
1672
+ withExamples(
1673
+ dsn.commands.find((c) => c.name() === "disable"),
1674
+ ["onus dsn disable <keyId>"]
1675
+ );
1676
+ withExamples(
1677
+ dsn.commands.find((c) => c.name() === "enable"),
1678
+ ["onus dsn enable <keyId>"]
1679
+ );
1680
+ withExamples(
1681
+ dsn.commands.find((c) => c.name() === "revoke"),
1682
+ ["onus dsn revoke <keyId>"]
1683
+ );
1684
+ }
1685
+
1686
+ // src/commands/skill.ts
1687
+ import pc5 from "picocolors";
1688
+ import { spawn } from "child_process";
1689
+ import { existsSync } from "fs";
1690
+ import { fileURLToPath } from "url";
1691
+ import path from "path";
1692
+
1693
+ // src/commands/skills-output-filter.ts
1694
+ var FAILED_HEADER_PATTERN = /Failed to install \d+/;
1695
+ var PROMPTSCRIPT_GLOBAL_ERROR_PATTERN = /→\s*PromptScript:\s*PromptScript does not support global skill installation/;
1696
+ var FAILURE_DETAIL_PATTERN = /✗/;
1697
+ function stripAnsi(line) {
1698
+ return line.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
1699
+ }
1700
+ function createPromptScriptGlobalFailureFilter() {
1701
+ let held = null;
1702
+ let rest = "";
1703
+ const isBogusDetail = (plain) => PROMPTSCRIPT_GLOBAL_ERROR_PATTERN.test(plain);
1704
+ const isBlockLine = (plain) => plain.trim() === "" || FAILED_HEADER_PATTERN.test(plain) || FAILURE_DETAIL_PATTERN.test(plain);
1705
+ function resolveHeld() {
1706
+ if (held === null) return "";
1707
+ const lines = held;
1708
+ held = null;
1709
+ const hasGenuineDetail = lines.some((l) => {
1710
+ const plain = stripAnsi(l);
1711
+ return FAILURE_DETAIL_PATTERN.test(plain) && !isBogusDetail(plain);
1712
+ });
1713
+ if (!hasGenuineDetail) return "";
1714
+ return lines.filter((l) => !isBogusDetail(stripAnsi(l))).join("");
1715
+ }
1716
+ return {
1717
+ write(chunk) {
1718
+ let out = "";
1719
+ rest += chunk;
1720
+ for (; ; ) {
1721
+ const newlineIndex = rest.indexOf("\n");
1722
+ if (newlineIndex === -1) break;
1723
+ const line = rest.slice(0, newlineIndex + 1);
1724
+ rest = rest.slice(newlineIndex + 1);
1725
+ const plain = stripAnsi(line);
1726
+ if (held !== null) {
1727
+ if (isBlockLine(plain)) {
1728
+ held.push(line);
1729
+ } else {
1730
+ out += resolveHeld() + line;
1731
+ }
1732
+ } else if (FAILED_HEADER_PATTERN.test(plain)) {
1733
+ held = [line];
1734
+ } else if (!isBogusDetail(plain)) {
1735
+ out += line;
1736
+ }
1737
+ }
1738
+ return out;
1739
+ },
1740
+ end() {
1741
+ const out = resolveHeld() + rest;
1742
+ rest = "";
1743
+ return out;
1744
+ }
1745
+ };
1746
+ }
1747
+
1748
+ // src/commands/skill.ts
1749
+ var SKILL_NAME = "onus-cli";
1750
+ var SKILLS_CLI_VERSION = "1.5.23";
1751
+ function resolveBundledSkillDir(startDir = path.dirname(fileURLToPath(import.meta.url))) {
1752
+ let dir = startDir;
1753
+ for (let i = 0; i < 4; i++) {
1754
+ const candidate = path.join(dir, "skills", SKILL_NAME);
1755
+ if (existsSync(path.join(candidate, "SKILL.md"))) return candidate;
1756
+ const parent = path.dirname(dir);
1757
+ if (parent === dir) break;
1758
+ dir = parent;
1759
+ }
1760
+ throw new Error(
1761
+ `Bundled "${SKILL_NAME}" skill not found (looked upward from ${startDir}). The installation may be corrupt.`
1762
+ );
1763
+ }
1764
+ function buildSkillArgs(opts) {
1765
+ const args = [];
1766
+ if (opts.global) args.push("--global");
1767
+ if (opts.agent?.length) args.push("--agent", ...opts.agent);
1768
+ if (opts.copy) args.push("--copy");
1769
+ return args;
1770
+ }
1771
+ function quoteArgForWindows(arg) {
1772
+ return /\s/.test(arg) ? `"${arg}"` : arg;
1773
+ }
1774
+ async function runSkillsCli(args) {
1775
+ const isWindows = process.platform === "win32";
1776
+ const finalArgs = isWindows ? [`--yes`, `skills@${SKILLS_CLI_VERSION}`, ...args].map(quoteArgForWindows) : [`--yes`, `skills@${SKILLS_CLI_VERSION}`, ...args];
1777
+ const filterGlobalFailures = args.includes("--global") && !args.includes("--agent");
1778
+ const child = spawn("npx", finalArgs, {
1779
+ stdio: filterGlobalFailures ? ["inherit", "pipe", "pipe"] : "inherit",
1780
+ ...isWindows ? { shell: true } : {}
1781
+ });
1782
+ if (filterGlobalFailures) {
1783
+ const pipeThroughFilter = (readable, target) => {
1784
+ if (!readable) return;
1785
+ const filter = createPromptScriptGlobalFailureFilter();
1786
+ readable.setEncoding("utf8");
1787
+ readable.on("data", (chunk) => {
1788
+ const text = filter.write(chunk);
1789
+ if (text) target.write(text);
1790
+ });
1791
+ readable.on("end", () => {
1792
+ const rest = filter.end();
1793
+ if (rest) target.write(rest);
1794
+ });
1795
+ };
1796
+ pipeThroughFilter(child.stdout, process.stdout);
1797
+ pipeThroughFilter(child.stderr, process.stderr);
1798
+ }
1799
+ let status;
1800
+ try {
1801
+ status = await new Promise((resolve, reject) => {
1802
+ child.once("error", reject);
1803
+ child.once("exit", (code) => resolve(code));
1804
+ });
1805
+ } catch (err) {
1806
+ console.error(
1807
+ pc5.red("Failed to run the skills CLI (npx):"),
1808
+ err instanceof Error ? err.message : String(err)
1809
+ );
1810
+ process.exit(1);
1811
+ }
1812
+ if (status !== 0) {
1813
+ console.error(pc5.red(`Skills CLI exited with status ${status ?? "unknown"}.`));
1814
+ process.exit(status ?? 1);
1815
+ }
1816
+ }
1817
+ function addSharedOptions(cmd) {
1818
+ return cmd.option("-g, --global", "Operate on the user-wide skill directory instead of the project").option("-a, --agent <agents...>", "Target specific agents (e.g. claude-code, codex)").option("-c, --copy", "Copy files instead of symlinking into agent directories");
1819
+ }
1820
+ function registerSkillCommands(program2) {
1821
+ const skill = program2.command("skill").description(
1822
+ `Manage the ${SKILL_NAME} agent skill for AI coding assistants (via the skills npm package)`
1823
+ );
1824
+ addSharedOptions(
1825
+ skill.command("install").description(`Install the bundled ${SKILL_NAME} skill into detected AI agents`)
1826
+ ).action(async function(opts) {
1827
+ try {
1828
+ const skillDir = resolveBundledSkillDir();
1829
+ await runSkillsCli(["add", skillDir, "--yes", ...buildSkillArgs(opts)]);
1830
+ console.log(
1831
+ pc5.green(`
1832
+ ${SKILL_NAME} skill installed. Agents can now use it to drive the Onus CLI.`)
1833
+ );
1834
+ } catch (err) {
1835
+ console.error(
1836
+ pc5.red("Error installing skill:"),
1837
+ err instanceof Error ? err.message : String(err)
1838
+ );
1839
+ process.exit(1);
1840
+ }
1841
+ });
1842
+ addSharedOptions(
1843
+ skill.command("remove").alias("rm").description("Remove the installed skill from AI agents")
1844
+ ).action(async function(opts) {
1845
+ await runSkillsCli(["remove", SKILL_NAME, "--yes", ...buildSkillArgs(opts)]);
1846
+ console.log(pc5.green(`
1847
+ ${SKILL_NAME} skill removed.`));
1848
+ });
1849
+ addSharedOptions(
1850
+ skill.command("update").description("Update installed copies of the skill to the bundled version")
1851
+ ).action(async function(opts) {
1852
+ try {
1853
+ const skillDir = resolveBundledSkillDir();
1854
+ await runSkillsCli(["add", skillDir, "--yes", ...buildSkillArgs(opts)]);
1855
+ console.log(pc5.green(`
1856
+ ${SKILL_NAME} skill updated.`));
1857
+ } catch (err) {
1858
+ console.error(
1859
+ pc5.red("Error updating skill:"),
1860
+ err instanceof Error ? err.message : String(err)
1861
+ );
1862
+ process.exit(1);
1863
+ }
1864
+ });
1865
+ withExamples(
1866
+ skill.commands.find((c) => c.name() === "install"),
1867
+ [
1868
+ "onus skill install",
1869
+ "onus skill install --global",
1870
+ "onus skill install --agent claude-code codex",
1871
+ "onus skill install --copy"
1872
+ ]
1873
+ );
1874
+ withExamples(
1875
+ skill.commands.find((c) => c.name() === "remove"),
1876
+ ["onus skill remove", "onus skill remove --agent claude-code"]
1877
+ );
1878
+ withExamples(
1879
+ skill.commands.find((c) => c.name() === "update"),
1880
+ ["onus skill update", "onus skill update --global"]
1881
+ );
1882
+ }
1883
+
1884
+ // src/commands/template.ts
1885
+ var PRIORITIES = ["urgent", "high", "medium", "low", "none"];
1886
+ function asPriority(value) {
1887
+ return PRIORITIES.includes(value) ? value : void 0;
1888
+ }
1889
+ function collect2(value, previous) {
1890
+ return [...previous, value];
1891
+ }
1892
+ function registerTemplateCommands(program2) {
1893
+ const template = program2.command("template").description("Manage reusable per-team issue templates");
1894
+ template.command("list").description("List issue templates (optionally for one team)").option("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("-j, --json", "Output raw JSON").action(async function(opts) {
1895
+ const globals = this.optsWithGlobals();
1896
+ try {
1897
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1898
+ const teamId = opts.team ? await resolveTeamId(client, opts.team) : void 0;
1899
+ const items = await client.templatesGet(teamId ? { teamId } : {});
1900
+ printJsonOrFormatted(items, globals.json, () => {
1901
+ if (!Array.isArray(items) || items.length === 0) {
1902
+ console.log("No templates found.");
1903
+ return;
1904
+ }
1905
+ console.log(
1906
+ formatRecordList(
1907
+ items.map((t) => [
1908
+ { label: "ID", value: t.id },
1909
+ { label: "Name", value: t.name },
1910
+ { label: "Team", value: t.team_id },
1911
+ { label: "Default", value: t.is_default ? "yes" : "no" }
1912
+ ])
1913
+ )
1914
+ );
1915
+ });
1916
+ } catch (err) {
1917
+ await handleApiError(err, globals.json);
1918
+ }
1919
+ });
1920
+ template.command("create").description("Create an issue template").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").requiredOption("--name <name>", "Template name").option("--title <title>", "Title prefix applied on use").option("-d, --description <description>", "Description body (markdown)").option("--priority <priority>", `Priority (${PRIORITIES.join("|")})`).option("--label <labelId>", "Preset label UUID (repeatable)", collect2, []).option("--default", "Make this the team default template").option("-j, --json", "Output raw JSON").action(async function(opts) {
1921
+ const globals = this.optsWithGlobals();
1922
+ try {
1923
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1924
+ const teamId = await resolveTeamId(client, opts.team);
1925
+ const created = await client.templatesPost({
1926
+ templatesPostRequest: {
1927
+ team_id: teamId,
1928
+ name: opts.name,
1929
+ title: opts.title,
1930
+ description: opts.description,
1931
+ priority: opts.priority ? asPriority(opts.priority) : void 0,
1932
+ preset_label_ids: (opts.label ?? []).length > 0 ? opts.label : void 0,
1933
+ is_default: opts.default
1934
+ }
1935
+ });
1936
+ printJsonOrFormatted(created, globals.json, () => {
1937
+ formatSuccess(`Created template "${created.name}".`);
1938
+ });
1939
+ } catch (err) {
1940
+ await handleApiError(err, globals.json);
1941
+ }
1942
+ });
1943
+ template.command("get <id>").description("Show a single issue template").option("-j, --json", "Output raw JSON").action(async function(id, _opts) {
1944
+ const globals = this.optsWithGlobals();
1945
+ try {
1946
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1947
+ const row = await client.templatesIdGet({ id });
1948
+ printJsonOrFormatted(row, globals.json, () => {
1949
+ console.log(
1950
+ formatRecord([
1951
+ { label: "ID", value: row.id },
1952
+ { label: "Name", value: row.name },
1953
+ { label: "Team", value: row.team_id },
1954
+ { label: "Title", value: row.title },
1955
+ { label: "Priority", value: row.priority },
1956
+ { label: "Labels", value: (row.preset_label_ids ?? []).join(", ") || null },
1957
+ { label: "Default", value: row.is_default ? "yes" : "no" }
1958
+ ])
1959
+ );
1960
+ });
1961
+ } catch (err) {
1962
+ await handleApiError(err, globals.json);
1963
+ }
1964
+ });
1965
+ template.command("update <id>").alias("edit").description("Update an issue template").option("--name <name>").option("--title <title>", 'Title prefix ("--title \\"\\" to clear)').option("-d, --description <description>", "Description body (markdown)").option("--priority <priority>", `Priority (${PRIORITIES.join("|")})`).option(
1966
+ "--add-label <labelId>",
1967
+ "Add a preset label UUID (repeatable)",
1968
+ collect2,
1969
+ []
1970
+ ).option("--default", "Make this the team default template").option("--no-default", "Remove the default flag from this template").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1971
+ const globals = this.optsWithGlobals();
1972
+ try {
1973
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1974
+ const defaultExplicit = opts.default === false ? false : opts.default === void 0 ? void 0 : true;
1975
+ const added = opts.addLabel ?? [];
1976
+ let mergedLabels;
1977
+ if (added.length > 0) {
1978
+ const current = await client.templatesIdGet({ id });
1979
+ mergedLabels = Array.from(/* @__PURE__ */ new Set([...current.preset_label_ids ?? [], ...added]));
1980
+ }
1981
+ const updated = await client.templatesIdPatch({
1982
+ id,
1983
+ templatesIdPatchRequest: {
1984
+ ...opts.name ? { name: opts.name } : {},
1985
+ ...opts.title !== void 0 ? { title: opts.title } : {},
1986
+ ...opts.description !== void 0 ? { description: opts.description } : {},
1987
+ ...opts.priority ? { priority: asPriority(opts.priority) } : {},
1988
+ ...mergedLabels ? { preset_label_ids: mergedLabels } : {},
1989
+ ...defaultExplicit !== void 0 ? { is_default: defaultExplicit } : {}
1990
+ }
1991
+ });
1992
+ printJsonOrFormatted(updated, globals.json, () => {
1993
+ formatSuccess(`Updated template "${updated.name}".`);
1994
+ });
1995
+ } catch (err) {
1996
+ await handleApiError(err, globals.json);
1997
+ }
1998
+ });
1999
+ template.command("delete <id>").description("Delete an issue template").option("-y, --yes", "Skip confirmation").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
2000
+ const globals = this.optsWithGlobals();
2001
+ try {
2002
+ if (!opts.yes && !globals.json) {
2003
+ const { default: pc10 } = await import("picocolors");
2004
+ const answer = await new Promise((resolve) => {
2005
+ process.stdout.write(pc10.yellow(`Delete template ${id}? (y/N) `));
2006
+ process.stdin.once("data", (d) => resolve(String(d).trim().toLowerCase()));
2007
+ });
2008
+ if (answer !== "y" && answer !== "yes") {
2009
+ console.log("Aborted.");
2010
+ return;
2011
+ }
2012
+ }
2013
+ const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
2014
+ await client.templatesIdDelete({ id });
2015
+ printJsonOrFormatted({ deleted: id }, globals.json, () => {
2016
+ formatSuccess(`Deleted template ${id}.`);
2017
+ });
2018
+ } catch (err) {
2019
+ await handleApiError(err, globals.json);
2020
+ }
2021
+ });
2022
+ withExamples(
2023
+ template.commands.find((c) => c.name() === "list"),
2024
+ ["onus template list", "onus template list --team ONUS"]
2025
+ );
2026
+ withExamples(
2027
+ template.commands.find((c) => c.name() === "create"),
2028
+ ['onus template create --team ONUS --name "Bug report" --priority high --default']
2029
+ );
2030
+ withExamples(
2031
+ template.commands.find((c) => c.name() === "get"),
2032
+ ["onus template get <id>"]
2033
+ );
2034
+ withExamples(
2035
+ template.commands.find((c) => c.name() === "update"),
2036
+ ["onus template update <id> --priority urgent", "onus template update <id> --default"]
2037
+ );
2038
+ withExamples(
2039
+ template.commands.find((c) => c.name() === "delete"),
2040
+ ["onus template delete <id> --yes"]
2041
+ );
2042
+ }
2043
+
2044
+ // src/commands/watch.ts
2045
+ import pc6 from "picocolors";
2046
+ var BACKOFF_BASE_MS = 1e3;
2047
+ var BACKOFF_MAX_MS = 3e4;
2048
+ function computeBackoff(attempt) {
2049
+ return Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attempt), BACKOFF_MAX_MS);
2050
+ }
2051
+ function parseSseFrames(raw) {
2052
+ const frames = [];
2053
+ let start = 0;
2054
+ for (; ; ) {
2055
+ const end = raw.indexOf("\n\n", start);
2056
+ if (end === -1) break;
2057
+ const block = raw.slice(start, end);
2058
+ start = end + 2;
2059
+ const lines = block.split("\n");
2060
+ if (lines.every((l) => l.startsWith(":") || l.trim() === "")) continue;
2061
+ const msg = { data: "" };
2062
+ const dataLines = [];
2063
+ for (const line of lines) {
2064
+ if (line.startsWith("id:")) msg.id = line.slice(3).trim();
2065
+ else if (line.startsWith("event:")) msg.event = line.slice(6).trim();
2066
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
2067
+ }
2068
+ msg.data = dataLines.join("\n");
2069
+ frames.push(msg);
2070
+ }
2071
+ return { frames, rest: raw.slice(start) };
2072
+ }
2073
+ var WatchStop = class extends Error {
2074
+ constructor() {
2075
+ super("watch-stop");
2076
+ }
2077
+ };
2078
+ async function runWatch(opts, deps = {}) {
2079
+ const fetchImpl = deps.fetchImpl ?? fetch;
2080
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2081
+ const out = deps.out ?? ((line) => console.log(line));
2082
+ const decoder = new TextDecoder();
2083
+ let cursor = null;
2084
+ let attempt = 0;
2085
+ try {
2086
+ for (; ; ) {
2087
+ const url = new URL(`${opts.apiUrl.replace(/\/+$/, "")}/v1/events/stream`);
2088
+ if (opts.team) url.searchParams.set("team_id", opts.team);
2089
+ if (opts.type) url.searchParams.set("type", opts.type);
2090
+ const headers = {
2091
+ Authorization: `Bearer ${opts.token}`,
2092
+ Accept: "text/event-stream"
2093
+ };
2094
+ if (cursor != null) {
2095
+ headers["Last-Event-ID"] = String(cursor);
2096
+ url.searchParams.set("cursor", String(cursor));
2097
+ }
2098
+ const res = await fetchImpl(url, { headers });
2099
+ if (res.status === 401 || res.status === 403) return 1;
2100
+ if (!res.ok || !res.body) {
2101
+ await sleep(computeBackoff(attempt++));
2102
+ continue;
2103
+ }
2104
+ const reader = res.body.getReader();
2105
+ let buffer = "";
2106
+ let gotData = false;
2107
+ let gracefulClose = false;
2108
+ for (; ; ) {
2109
+ const { done, value } = await reader.read();
2110
+ if (done) break;
2111
+ buffer += decoder.decode(value, { stream: true });
2112
+ const { frames, rest } = parseSseFrames(buffer);
2113
+ buffer = rest;
2114
+ for (const frame of frames) {
2115
+ if (frame.event === "stream.close") {
2116
+ try {
2117
+ const closeData = JSON.parse(frame.data || "{}");
2118
+ if (typeof closeData.last_seq === "number") cursor = closeData.last_seq;
2119
+ } catch {
2120
+ }
2121
+ gracefulClose = true;
2122
+ break;
2123
+ }
2124
+ let payload = {};
2125
+ try {
2126
+ payload = JSON.parse(frame.data || "{}");
2127
+ } catch {
2128
+ }
2129
+ if (typeof payload.seq === "number") cursor = payload.seq;
2130
+ gotData = true;
2131
+ if (opts.json) out(frame.data);
2132
+ else if (payload.replay === true)
2133
+ out(`[replay] ${pc6.dim(`${frame.event} ${frame.data}`)}`);
2134
+ else out(`${frame.event} ${frame.data}`);
2135
+ }
2136
+ if (gracefulClose) break;
2137
+ }
2138
+ if (gotData) attempt = 0;
2139
+ if (!gracefulClose) await sleep(computeBackoff(attempt++));
2140
+ }
2141
+ } catch (err) {
2142
+ if (err instanceof WatchStop) return 0;
2143
+ throw err;
2144
+ }
2145
+ }
2146
+ function registerWatchCommand(program2) {
2147
+ program2.command("watch").description("Stream live issue/comment/error events (SSE) with automatic resume").option("--team <keyOrId>", "Limit to one team (key like HOT or a team UUID)").option("--type <types>", "Comma-separated event types to allowlist").option("-j, --json", "Output NDJSON data lines instead of formatted output");
2148
+ withExamples(
2149
+ program2.commands.find((c) => c.name() === "watch"),
2150
+ [
2151
+ "onus watch",
2152
+ "onus watch --team HOT --type issue.status_changed,error_group.regressed",
2153
+ "onus watch --json"
2154
+ ]
2155
+ );
2156
+ program2.commands.find((c) => c.name() === "watch").action(async function() {
2157
+ const globals = this.optsWithGlobals();
2158
+ const { loadConfig: loadConfig2 } = await import("./config-FWANRWOT.js");
2159
+ const config = loadConfig2();
2160
+ const token = globals.token ?? config.token ?? "";
2161
+ const apiUrl = globals.apiUrl ?? config.apiUrl;
2162
+ const opts = this.opts();
2163
+ let teamId = opts.team;
2164
+ if (teamId) {
2165
+ const { createApiClient: createApiClient2 } = await import("./client-QNEDGCP7.js");
2166
+ const client = createApiClient2({ token, apiUrl });
2167
+ teamId = await resolveTeamId(client, teamId);
2168
+ }
2169
+ const code = await runWatch(
2170
+ { token, apiUrl, team: teamId, type: opts.type, json: opts.json },
2171
+ {}
2172
+ );
2173
+ if (code !== 0) process.exitCode = code;
2174
+ });
2175
+ }
2176
+
2177
+ // src/program.ts
2178
+ function buildProgram() {
2179
+ const program2 = new Command();
2180
+ program2.name("onus").description("Official CLI and Agent Toolkit for Onus issue tracker").version(cliVersion).option("-j, --json", "Output pure JSON for machine parsing / agent interaction").option("-t, --token <token>", "Override authentication token").option("-u, --api-url <url>", "Override API base URL");
2181
+ registerAuthCommands(program2);
2182
+ registerContextCommands(program2);
2183
+ registerMeCommand(program2);
2184
+ registerTeamCommands(program2);
2185
+ registerAnalyticsCommands(program2);
2186
+ registerProjectCommands(program2);
2187
+ registerIssueCommands(program2);
2188
+ registerCommentCommands(program2);
2189
+ registerLabelCommands(program2);
2190
+ registerDsnCommands(program2);
2191
+ registerSkillCommands(program2);
2192
+ registerTemplateCommands(program2);
2193
+ registerWatchCommand(program2);
2194
+ program2.on("command:*", () => {
2195
+ console.error(pc7.red(`Invalid command: ${program2.args.join(" ")}`));
2196
+ console.error(`See ${pc7.bold("onus --help")} for a list of available commands.`);
2197
+ process.exit(2);
2198
+ });
2199
+ return program2;
2200
+ }
2201
+
2202
+ // src/update-check.ts
2203
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
2204
+ import path2 from "path";
2205
+ import pc8 from "picocolors";
2206
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@brunyee-studio/onus-cli/latest";
2207
+ var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
2208
+ var FETCH_TIMEOUT_MS = 2e3;
2209
+ function getUpdateCheckFilePath() {
2210
+ return path2.join(getConfigDirectory(), "update-check.json");
2211
+ }
2212
+ function readUpdateCheckCache() {
2213
+ const filePath = getUpdateCheckFilePath();
2214
+ try {
2215
+ if (!existsSync2(filePath)) return null;
2216
+ const raw = JSON.parse(readFileSync3(filePath, "utf8"));
2217
+ if (typeof raw?.lastChecked !== "number" || typeof raw?.latestVersion !== "string" || raw.latestVersion === "") {
2218
+ return null;
2219
+ }
2220
+ return { lastChecked: raw.lastChecked, latestVersion: raw.latestVersion };
2221
+ } catch {
2222
+ return null;
2223
+ }
2224
+ }
2225
+ function writeUpdateCheckCache(state) {
2226
+ const filePath = getUpdateCheckFilePath();
2227
+ try {
2228
+ const dir = path2.dirname(filePath);
2229
+ if (!existsSync2(dir)) {
2230
+ mkdirSync(dir, { recursive: true, mode: 448 });
2231
+ }
2232
+ writeFileSync2(filePath, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
2233
+ } catch {
2234
+ }
2235
+ }
2236
+ function isNewerVersion(installed, latest) {
2237
+ const parse = (v) => {
2238
+ const [mainPart, ...preParts] = v.split("-");
2239
+ const main = (mainPart ?? "").split(".").map((s) => Number.parseInt(s, 10));
2240
+ if (main.length === 0 || main.some((n) => Number.isNaN(n))) return null;
2241
+ return { main, pre: preParts.join("-") ? preParts.join("-").split(".") : [] };
2242
+ };
2243
+ const installedParsed = parse(installed);
2244
+ const latestParsed = parse(latest);
2245
+ if (!installedParsed || !latestParsed) return false;
2246
+ const len = Math.max(installedParsed.main.length, latestParsed.main.length);
2247
+ for (let i = 0; i < len; i++) {
2248
+ const a = installedParsed.main[i] ?? 0;
2249
+ const b = latestParsed.main[i] ?? 0;
2250
+ if (a !== b) return b > a;
2251
+ }
2252
+ if (installedParsed.pre.length > 0 && latestParsed.pre.length === 0) return true;
2253
+ if (installedParsed.pre.length === 0 && latestParsed.pre.length > 0) return false;
2254
+ return comparePrerelease(installedParsed.pre, latestParsed.pre);
2255
+ }
2256
+ function comparePrerelease(a, b) {
2257
+ const len = Math.max(a.length, b.length);
2258
+ for (let i = 0; i < len; i++) {
2259
+ const aPart = a[i];
2260
+ const bPart = b[i];
2261
+ if (aPart === void 0) return false;
2262
+ if (bPart === void 0) return true;
2263
+ const aNum = Number.parseInt(aPart, 10);
2264
+ const bNum = Number.parseInt(bPart, 10);
2265
+ const bothNumeric = !Number.isNaN(aNum) && !Number.isNaN(bNum);
2266
+ if (bothNumeric) {
2267
+ if (aNum !== bNum) return bNum > aNum;
2268
+ } else if (aPart !== bPart) {
2269
+ return false;
2270
+ }
2271
+ }
2272
+ return false;
2273
+ }
2274
+ async function fetchLatestVersion(options = {}) {
2275
+ const url = options.registryUrl ?? REGISTRY_LATEST_URL;
2276
+ try {
2277
+ const doFetch = options.fetchFn ?? fetch;
2278
+ const response = await doFetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
2279
+ if (!response.ok) return null;
2280
+ const body = await response.json();
2281
+ const version = body?.version;
2282
+ return typeof version === "string" && version.length > 0 ? version : null;
2283
+ } catch {
2284
+ return null;
2285
+ }
2286
+ }
2287
+ function shouldRunUpdateCheck(options) {
2288
+ const { stderrIsTTY, argv, env } = options;
2289
+ if (argv.includes("--json") || argv.includes("-j")) return false;
2290
+ if (env.ONUS_OUTPUT === "json") return false;
2291
+ if (argv.some((a) => ["--help", "-h", "--version", "-V", "-v"].includes(a))) return false;
2292
+ if (!stderrIsTTY) return false;
2293
+ if (env.CI && env.CI !== "0" && env.CI !== "false") return false;
2294
+ if (env.ONUS_NO_UPDATE_CHECK && !["0", "false", ""].includes(env.ONUS_NO_UPDATE_CHECK)) {
2295
+ return false;
2296
+ }
2297
+ return true;
2298
+ }
2299
+ async function checkForUpdate(options = {}) {
2300
+ const argv = options.argv ?? process.argv.slice(2);
2301
+ const env = options.env ?? process.env;
2302
+ const stderrIsTTY = options.stderrIsTTY ?? (options.stderrStream ? options.stderrStream.isTTY === true : process.stderr.isTTY === true);
2303
+ if (!options.skipGuards && !shouldRunUpdateCheck({ stderrIsTTY, argv, env })) {
2304
+ return null;
2305
+ }
2306
+ const installed = options.installedVersion ?? cliVersion;
2307
+ const now = options.now ?? Date.now();
2308
+ const cached = readUpdateCheckCache();
2309
+ let latest = null;
2310
+ if (cached && now - cached.lastChecked < UPDATE_CHECK_TTL_MS) {
2311
+ latest = cached.latestVersion;
2312
+ } else {
2313
+ latest = await fetchLatestVersion(options);
2314
+ if (latest) {
2315
+ writeUpdateCheckCache({ lastChecked: now, latestVersion: latest });
2316
+ } else if (cached) {
2317
+ latest = cached.latestVersion;
2318
+ }
2319
+ }
2320
+ if (!latest || !isNewerVersion(installed, latest)) return null;
2321
+ return { installed, latest };
2322
+ }
2323
+ function renderUpdateNotice(installed, latest) {
2324
+ const header = pc8.yellow(`Update available: ${installed} \u2192 ${latest}`);
2325
+ const line1 = pc8.dim(" Run: ") + pc8.bold("npm i -g @brunyee-studio/onus-cli@latest");
2326
+ const line2 = pc8.dim(" ") + pc8.bold("onus skill update -g");
2327
+ return `
2328
+ ${pc8.dim("\u2500".repeat(52))}
2329
+ \u26A0 ${header}
2330
+ ${line1}
2331
+ ${line2}
2332
+ ${pc8.dim("\u2500".repeat(52))}
2333
+ `;
2334
+ }
2335
+ async function notifyIfUpdateAvailable(options = {}) {
2336
+ try {
2337
+ const result = await checkForUpdate(options);
2338
+ if (!result) return;
2339
+ const stream = options.stderrStream ?? process.stderr;
2340
+ stream.write(renderUpdateNotice(result.installed, result.latest));
2341
+ } catch {
2342
+ }
2343
+ }
2344
+
2345
+ // src/bin.ts
2346
+ var program = buildProgram();
2347
+ process.on("unhandledRejection", (reason) => {
2348
+ console.error(pc9.red("Unhandled error:"), reason);
2349
+ process.exit(1);
2350
+ });
2351
+ program.parseAsync(process.argv).then(() => notifyIfUpdateAvailable()).catch((err) => {
2352
+ console.error(pc9.red("Execution error:"), err instanceof Error ? err.message : String(err));
2353
+ process.exit(1);
2354
+ });