@tolinax/ayoune-cli 2026.3.0 → 2026.4.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.
Files changed (70) hide show
  1. package/data/contextSlots.js +189 -0
  2. package/data/modelsAndRights.js +56 -0
  3. package/data/modules.js +16 -0
  4. package/lib/api/apiCallHandler.js +6 -2
  5. package/lib/api/apiClient.js +9 -1
  6. package/lib/api/auditCallHandler.js +2 -2
  7. package/lib/api/handleAPIError.js +20 -18
  8. package/lib/api/login.js +3 -3
  9. package/lib/api/searchClient.js +119 -0
  10. package/lib/commands/createAccessCommand.js +126 -0
  11. package/lib/commands/createActionsCommand.js +40 -9
  12. package/lib/commands/createAiCommand.js +17 -17
  13. package/lib/commands/createAliasCommand.js +4 -6
  14. package/lib/commands/createAuditCommand.js +5 -9
  15. package/lib/commands/createBatchCommand.js +15 -28
  16. package/lib/commands/createCompletionsCommand.js +6 -3
  17. package/lib/commands/createConfigCommand.js +8 -14
  18. package/lib/commands/createContextCommand.js +163 -0
  19. package/lib/commands/createCopyCommand.js +4 -7
  20. package/lib/commands/createCreateCommand.js +4 -7
  21. package/lib/commands/createDeleteCommand.js +4 -6
  22. package/lib/commands/createDeployCommand.js +31 -55
  23. package/lib/commands/createDescribeCommand.js +12 -10
  24. package/lib/commands/createEditCommand.js +13 -8
  25. package/lib/commands/createEventsCommand.js +4 -4
  26. package/lib/commands/createExecCommand.js +65 -35
  27. package/lib/commands/createExportCommand.js +21 -24
  28. package/lib/commands/createGetCommand.js +13 -14
  29. package/lib/commands/createJobsCommand.js +8 -13
  30. package/lib/commands/createListCommand.js +13 -14
  31. package/lib/commands/createLoginCommand.js +16 -4
  32. package/lib/commands/createLogoutCommand.js +2 -2
  33. package/lib/commands/createModulesCommand.js +16 -19
  34. package/lib/commands/createMonitorCommand.js +9 -16
  35. package/lib/commands/createPermissionsCommand.js +10 -18
  36. package/lib/commands/createProgram.js +47 -21
  37. package/lib/commands/createSearchCommand.js +219 -69
  38. package/lib/commands/createSelfHostUpdateCommand.js +166 -0
  39. package/lib/commands/createServicesCommand.js +5 -8
  40. package/lib/commands/createSetupCommand.js +305 -0
  41. package/lib/commands/createStatusCommand.js +147 -0
  42. package/lib/commands/createStorageCommand.js +2 -3
  43. package/lib/commands/createStreamCommand.js +4 -4
  44. package/lib/commands/createSyncCommand.js +5 -8
  45. package/lib/commands/createTemplateCommand.js +9 -16
  46. package/lib/commands/createUpdateCommand.js +12 -15
  47. package/lib/commands/createUsersCommand.js +21 -31
  48. package/lib/commands/createWebhooksCommand.js +15 -22
  49. package/lib/commands/createWhoAmICommand.js +8 -6
  50. package/lib/helpers/cliError.js +24 -0
  51. package/lib/helpers/config.js +1 -0
  52. package/lib/helpers/configLoader.js +6 -0
  53. package/lib/helpers/contextInjector.js +65 -0
  54. package/lib/helpers/contextResolver.js +70 -0
  55. package/lib/helpers/contextStore.js +46 -0
  56. package/lib/helpers/handleResponseFormatOptions.js +59 -10
  57. package/lib/helpers/logo.js +48 -0
  58. package/lib/helpers/resolveCollectionArgs.js +36 -0
  59. package/lib/helpers/sanitizeFields.js +18 -0
  60. package/lib/helpers/secureStorage.js +72 -0
  61. package/lib/helpers/tokenPayload.js +21 -0
  62. package/lib/helpers/updateNotifier.js +49 -0
  63. package/lib/models/getModuleFromCollection.js +4 -1
  64. package/lib/operations/handleCopySingleOperation.js +10 -2
  65. package/lib/operations/handleCreateSingleOperation.js +3 -0
  66. package/lib/operations/handleDescribeSingleOperation.js +23 -0
  67. package/lib/operations/handleGetOperation.js +9 -3
  68. package/lib/operations/handleListOperation.js +14 -10
  69. package/lib/prompts/promptModule.js +9 -6
  70. package/package.json +163 -158
@@ -0,0 +1,163 @@
1
+ import chalk from "chalk";
2
+ import { spinner } from "../../index.js";
3
+ import { contextSlots, getSlot, getSlotsByTier } from "../../data/contextSlots.js";
4
+ import { getActiveContext, setContextEntry, unsetContextEntry, clearAllContext, } from "../helpers/contextStore.js";
5
+ import { resolveEntity, validateHierarchy, getDependentSlots } from "../helpers/contextResolver.js";
6
+ import { EXIT_MISUSE } from "../exitCodes.js";
7
+ import { cliError } from "../helpers/cliError.js";
8
+ export function createContextCommand(program) {
9
+ const ctx = program
10
+ .command("context")
11
+ .alias("ctx")
12
+ .description("Manage active entity context for scoped commands")
13
+ .addHelpText("after", `
14
+ Examples:
15
+ ay context Show active context
16
+ ay context set project "Website" Set project context by name
17
+ ay context set sprint "Sprint 14" Set sprint (auto-filtered by project)
18
+ ay context unset project Unset project (cascades to children)
19
+ ay context clear Clear all context
20
+ ay context list List all available context slots`);
21
+ // Default action: show active context
22
+ ctx.action(() => {
23
+ showContext();
24
+ });
25
+ // ay context show
26
+ ctx
27
+ .command("show")
28
+ .description("Show active context")
29
+ .action(() => {
30
+ showContext();
31
+ });
32
+ // ay context set <slot> <name>
33
+ ctx
34
+ .command("set <slot> <nameOrId...>")
35
+ .description("Set active context for a slot")
36
+ .action(async (slot, nameOrIdParts, options) => {
37
+ const opts = { ...program.opts(), ...options };
38
+ const nameOrId = nameOrIdParts.join(" ");
39
+ const slotDef = getSlot(slot);
40
+ if (!slotDef) {
41
+ const available = contextSlots.map((s) => s.slot).join(", ");
42
+ cliError(`Unknown context slot: "${slot}". Available: ${available}`, EXIT_MISUSE);
43
+ return;
44
+ }
45
+ // Hierarchy warning
46
+ const warning = validateHierarchy(slot);
47
+ if (warning) {
48
+ console.error(chalk.yellow(` ${warning}`));
49
+ }
50
+ spinner.start({ text: `Resolving ${slot} "${nameOrId}"...`, color: "magenta" });
51
+ const matches = await resolveEntity(slot, nameOrId);
52
+ if (matches.length === 0) {
53
+ spinner.error({ text: `No ${slot} found matching "${nameOrId}"` });
54
+ spinner.stop();
55
+ return;
56
+ }
57
+ let selected = matches[0];
58
+ // If multiple matches and interactive TTY, let user pick
59
+ if (matches.length > 1 && process.stdin.isTTY) {
60
+ spinner.stop();
61
+ console.error(chalk.dim(` Multiple matches for "${nameOrId}":`));
62
+ matches.forEach((m, i) => {
63
+ console.error(` ${chalk.cyan(`[${i + 1}]`)} ${m.name} ${chalk.dim(`(${m.id})`)}`);
64
+ });
65
+ console.error(chalk.dim(` Using first match. Use an ObjectId for exact selection.\n`));
66
+ selected = matches[0];
67
+ }
68
+ else if (matches.length > 1) {
69
+ spinner.stop();
70
+ }
71
+ setContextEntry(slot, {
72
+ id: selected.id,
73
+ name: selected.name,
74
+ collection: slotDef.collection,
75
+ module: slotDef.module,
76
+ setAt: new Date().toISOString(),
77
+ });
78
+ spinner.success({ text: `Context ${slot} set to "${selected.name}" (${selected.id})` });
79
+ spinner.stop();
80
+ });
81
+ // ay context unset <slot>
82
+ ctx
83
+ .command("unset <slot>")
84
+ .description("Remove context for a slot (cascades to children)")
85
+ .action((slot) => {
86
+ const slotDef = getSlot(slot);
87
+ if (!slotDef) {
88
+ cliError(`Unknown context slot: "${slot}"`, EXIT_MISUSE);
89
+ return;
90
+ }
91
+ // Cascade unset children
92
+ const children = getDependentSlots(slot);
93
+ const unset = [slot];
94
+ for (const child of children) {
95
+ const entry = getActiveContext()[child];
96
+ if (entry) {
97
+ unsetContextEntry(child);
98
+ unset.push(child);
99
+ }
100
+ }
101
+ unsetContextEntry(slot);
102
+ if (unset.length > 1) {
103
+ console.error(chalk.dim(` Cascade: also unset ${unset.slice(1).join(", ")}`));
104
+ }
105
+ spinner.success({ text: `Context "${slot}" unset` });
106
+ spinner.stop();
107
+ });
108
+ // ay context clear
109
+ ctx
110
+ .command("clear")
111
+ .description("Clear all active context")
112
+ .option("--force", "Skip confirmation")
113
+ .action((options) => {
114
+ const opts = { ...program.opts(), ...options };
115
+ const active = getActiveContext();
116
+ const count = Object.keys(active).length;
117
+ if (count === 0) {
118
+ console.error(chalk.dim(" No active context to clear."));
119
+ return;
120
+ }
121
+ clearAllContext();
122
+ spinner.success({ text: `Cleared ${count} context entries` });
123
+ spinner.stop();
124
+ });
125
+ // ay context list
126
+ ctx
127
+ .command("list")
128
+ .description("List all available context slots")
129
+ .action(() => {
130
+ const active = getActiveContext();
131
+ const tierLabels = {
132
+ 1: "Core",
133
+ 2: "Domain",
134
+ 3: "AI / Automation",
135
+ };
136
+ for (const tier of [1, 2, 3]) {
137
+ const slots = getSlotsByTier(tier);
138
+ console.log(chalk.bold(`\n ${tierLabels[tier]}`));
139
+ for (const s of slots) {
140
+ const entry = active[s.slot];
141
+ const status = entry
142
+ ? chalk.green(`= "${entry.name}" (${entry.id})`)
143
+ : chalk.dim("not set");
144
+ const parent = s.parent ? chalk.dim(` [parent: ${s.parent}]`) : "";
145
+ console.log(` ${chalk.cyan(s.slot.padEnd(14))} ${s.collection.padEnd(22)} ${status}${parent}`);
146
+ }
147
+ }
148
+ console.log();
149
+ });
150
+ }
151
+ function showContext() {
152
+ const active = getActiveContext();
153
+ const entries = Object.entries(active);
154
+ if (entries.length === 0) {
155
+ console.log(chalk.dim(" No active context. Use `ay context set <slot> <name>` to set one."));
156
+ return;
157
+ }
158
+ console.log(chalk.bold("\n Active Context"));
159
+ for (const [slot, entry] of entries) {
160
+ console.log(` ${chalk.cyan(slot.padEnd(14))} ${entry.name} ${chalk.dim(`(${entry.collection}, ${entry.id})`)}`);
161
+ }
162
+ console.log();
163
+ }
@@ -2,8 +2,8 @@ import { Argument } from "commander";
2
2
  import { getModuleFromCollection } from "../models/getModuleFromCollection.js";
3
3
  import { handleCopySingleOperation } from "../operations/handleCopySingleOperation.js";
4
4
  import { localStorage } from "../helpers/localStorage.js";
5
- import { spinner } from "../../index.js";
6
5
  import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
6
+ import { cliError } from "../helpers/cliError.js";
7
7
  export function createCopyCommand(program) {
8
8
  program
9
9
  .command("copy")
@@ -18,12 +18,10 @@ Examples:
18
18
  .action(async (collection, id, options) => {
19
19
  try {
20
20
  if (!collection) {
21
- spinner.error({ text: "Missing required argument: collection. Run a list or get command first, or provide it explicitly." });
22
- process.exit(EXIT_MISUSE);
21
+ cliError("Missing required argument: collection. Run a list or get command first, or provide it explicitly.", EXIT_MISUSE);
23
22
  }
24
23
  if (!id) {
25
- spinner.error({ text: "Missing required argument: id. Provide an entry ID explicitly." });
26
- process.exit(EXIT_MISUSE);
24
+ cliError("Missing required argument: id. Provide an entry ID explicitly.", EXIT_MISUSE);
27
25
  }
28
26
  const opts = { ...program.opts(), ...options };
29
27
  const module = getModuleFromCollection(collection);
@@ -32,8 +30,7 @@ Examples:
32
30
  });
33
31
  }
34
32
  catch (e) {
35
- spinner.error({ text: e.message || "An unexpected error occurred" });
36
- process.exit(EXIT_GENERAL_ERROR);
33
+ cliError(e.message || "An unexpected error occurred", EXIT_GENERAL_ERROR);
37
34
  }
38
35
  });
39
36
  }
@@ -4,8 +4,8 @@ import { promptCollectionWithModule } from "../prompts/promptCollectionWithModul
4
4
  import { handleCreateSingleOperation } from "../operations/handleCreateSingleOperation.js";
5
5
  import { promptName } from "../prompts/promptName.js";
6
6
  import { localStorage } from "../helpers/localStorage.js";
7
- import { spinner } from "../../index.js";
8
7
  import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
8
+ import { cliError } from "../helpers/cliError.js";
9
9
  export function createCreateCommand(program) {
10
10
  program
11
11
  .command("create")
@@ -22,8 +22,7 @@ Examples:
22
22
  const opts = { ...program.opts(), ...options };
23
23
  if (!collection) {
24
24
  if (!process.stdin.isTTY) {
25
- spinner.error({ text: "Missing required argument: collection" });
26
- process.exit(EXIT_MISUSE);
25
+ cliError("Missing required argument: collection", EXIT_MISUSE);
27
26
  }
28
27
  collection = await promptCollectionWithModule();
29
28
  }
@@ -31,8 +30,7 @@ Examples:
31
30
  let entryName = name;
32
31
  if (!entryName) {
33
32
  if (!process.stdin.isTTY) {
34
- spinner.error({ text: "Missing required argument: name" });
35
- process.exit(EXIT_MISUSE);
33
+ cliError("Missing required argument: name", EXIT_MISUSE);
36
34
  }
37
35
  entryName = await promptName();
38
36
  }
@@ -43,8 +41,7 @@ Examples:
43
41
  localStorage.setItem("lastCollection", collection);
44
42
  }
45
43
  catch (e) {
46
- spinner.error({ text: e.message || "An unexpected error occurred" });
47
- process.exit(EXIT_GENERAL_ERROR);
44
+ cliError(e.message || "An unexpected error occurred", EXIT_GENERAL_ERROR);
48
45
  }
49
46
  });
50
47
  }
@@ -5,6 +5,7 @@ import { handleDeleteSingleOperation } from "../operations/handleDeleteSingleOpe
5
5
  import { localStorage } from "../helpers/localStorage.js";
6
6
  import { spinner } from "../../index.js";
7
7
  import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
8
+ import { cliError } from "../helpers/cliError.js";
8
9
  export function createDeleteCommand(program) {
9
10
  program
10
11
  .command("delete")
@@ -22,8 +23,7 @@ Examples:
22
23
  .action(async (collection, ids, options) => {
23
24
  try {
24
25
  if (!collection) {
25
- spinner.error({ text: "Missing required argument: collection. Run a list or get command first, or provide it explicitly." });
26
- process.exit(EXIT_MISUSE);
26
+ cliError("Missing required argument: collection. Run a list or get command first, or provide it explicitly.", EXIT_MISUSE);
27
27
  }
28
28
  const opts = { ...program.opts(), ...options };
29
29
  const module = getModuleFromCollection(collection);
@@ -47,8 +47,7 @@ Examples:
47
47
  }
48
48
  }
49
49
  if (idList.length === 0) {
50
- spinner.error({ text: "No IDs provided. Pass IDs as argument or via --ids-stdin." });
51
- process.exit(EXIT_MISUSE);
50
+ cliError("No IDs provided. Pass IDs as argument or via --ids-stdin.", EXIT_MISUSE);
52
51
  }
53
52
  // Confirmation prompt (unless --force)
54
53
  if (!opts.force && process.stdin.isTTY) {
@@ -91,8 +90,7 @@ Examples:
91
90
  }
92
91
  }
93
92
  catch (e) {
94
- spinner.error({ text: e.message || "An unexpected error occurred" });
95
- process.exit(EXIT_GENERAL_ERROR);
93
+ cliError(e.message || "An unexpected error occurred", EXIT_GENERAL_ERROR);
96
94
  }
97
95
  });
98
96
  }
@@ -3,11 +3,12 @@ import { getModuleBaseUrl } from "../api/apiClient.js";
3
3
  import { apiCallHandler } from "../api/apiCallHandler.js";
4
4
  import { handleResponseFormatOptions } from "../helpers/handleResponseFormatOptions.js";
5
5
  import { saveFile } from "../helpers/saveFile.js";
6
- import { localStorage } from "../helpers/localStorage.js";
6
+ import { secureStorage } from "../helpers/secureStorage.js";
7
7
  import { spinner } from "../../index.js";
8
8
  import { EXIT_GENERAL_ERROR } from "../exitCodes.js";
9
+ import { cliError } from "../helpers/cliError.js";
9
10
  function getToken() {
10
- return localStorage.getItem("token") || process.env.AYOUNE_TOKEN || "";
11
+ return secureStorage.getItem("token") || process.env.AYOUNE_TOKEN || "";
11
12
  }
12
13
  export function createDeployCommand(program) {
13
14
  const deploy = program
@@ -27,7 +28,7 @@ export function createDeployCommand(program) {
27
28
  .option("-l, --limit <number>", "Limit", parseInt, 50)
28
29
  .option("-p, --page <number>", "Page", parseInt, 1)
29
30
  .action(async (options) => {
30
- var _a, _b, _c;
31
+ var _a, _b;
31
32
  try {
32
33
  const opts = { ...program.opts(), ...options };
33
34
  spinner.start({ text: "Fetching deployments...", color: "magenta" });
@@ -47,16 +48,15 @@ export function createDeployCommand(program) {
47
48
  if (opts.search)
48
49
  params.q = opts.search;
49
50
  const res = await apiCallHandler("devops", "deployments", "get", null, params);
50
- handleResponseFormatOptions(opts, res);
51
- const total = (_c = (_b = (_a = res.meta) === null || _a === void 0 ? void 0 : _a.pageInfo) === null || _b === void 0 ? void 0 : _b.totalEntries) !== null && _c !== void 0 ? _c : 0;
51
+ const { result: fmtResult, meta: fmtMeta } = handleResponseFormatOptions(opts, res);
52
+ const total = (_b = (_a = fmtMeta === null || fmtMeta === void 0 ? void 0 : fmtMeta.pageInfo) === null || _a === void 0 ? void 0 : _a.totalEntries) !== null && _b !== void 0 ? _b : (Array.isArray(fmtResult) ? fmtResult.length : 0);
52
53
  spinner.success({ text: `Found ${total} deployments` });
53
54
  spinner.stop();
54
55
  if (opts.save)
55
56
  await saveFile("deploy-list", opts, res);
56
57
  }
57
58
  catch (e) {
58
- spinner.error({ text: e.message || "Failed to list deployments" });
59
- process.exit(EXIT_GENERAL_ERROR);
59
+ cliError(e.message || "Failed to list deployments", EXIT_GENERAL_ERROR);
60
60
  }
61
61
  });
62
62
  // ay deploy get <id>
@@ -76,8 +76,7 @@ export function createDeployCommand(program) {
76
76
  spinner.stop();
77
77
  }
78
78
  catch (e) {
79
- spinner.error({ text: e.message || "Failed to get deployment" });
80
- process.exit(EXIT_GENERAL_ERROR);
79
+ cliError(e.message || "Failed to get deployment", EXIT_GENERAL_ERROR);
81
80
  }
82
81
  });
83
82
  // ay deploy logs <id>
@@ -113,8 +112,7 @@ Examples:
113
112
  },
114
113
  }, (res) => {
115
114
  if (res.statusCode !== 200) {
116
- console.error(chalk.red(` Error: HTTP ${res.statusCode}`));
117
- process.exit(EXIT_GENERAL_ERROR);
115
+ cliError(`Error: HTTP ${res.statusCode}`, EXIT_GENERAL_ERROR);
118
116
  }
119
117
  res.setEncoding("utf-8");
120
118
  res.on("data", (chunk) => {
@@ -146,8 +144,7 @@ Examples:
146
144
  });
147
145
  });
148
146
  req.on("error", (e) => {
149
- console.error(chalk.red(` Stream error: ${e.message}`));
150
- process.exit(EXIT_GENERAL_ERROR);
147
+ cliError(`Stream error: ${e.message}`, EXIT_GENERAL_ERROR);
151
148
  });
152
149
  // Keep alive until Ctrl+C
153
150
  process.on("SIGINT", () => {
@@ -173,8 +170,7 @@ Examples:
173
170
  }
174
171
  }
175
172
  catch (e) {
176
- spinner.error({ text: e.message || "Failed to fetch logs" });
177
- process.exit(EXIT_GENERAL_ERROR);
173
+ cliError(e.message || "Failed to fetch logs", EXIT_GENERAL_ERROR);
178
174
  }
179
175
  });
180
176
  // ay deploy scale <deployment> <replicas>
@@ -194,8 +190,7 @@ Examples:
194
190
  spinner.stop();
195
191
  }
196
192
  catch (e) {
197
- spinner.error({ text: e.message || "Failed to scale deployment" });
198
- process.exit(EXIT_GENERAL_ERROR);
193
+ cliError(e.message || "Failed to scale deployment", EXIT_GENERAL_ERROR);
199
194
  }
200
195
  });
201
196
  // ay deploy restart <deployment>
@@ -214,8 +209,7 @@ Examples:
214
209
  spinner.stop();
215
210
  }
216
211
  catch (e) {
217
- spinner.error({ text: e.message || "Failed to restart deployment" });
218
- process.exit(EXIT_GENERAL_ERROR);
212
+ cliError(e.message || "Failed to restart deployment", EXIT_GENERAL_ERROR);
219
213
  }
220
214
  });
221
215
  // ─── PODS ──────────────────────────────────────────────
@@ -255,8 +249,7 @@ Examples:
255
249
  await saveFile("deploy-pods", opts, res);
256
250
  }
257
251
  catch (e) {
258
- spinner.error({ text: e.message || "Failed to list pods" });
259
- process.exit(EXIT_GENERAL_ERROR);
252
+ cliError(e.message || "Failed to list pods", EXIT_GENERAL_ERROR);
260
253
  }
261
254
  });
262
255
  // ay deploy pod-delete <id>
@@ -275,8 +268,7 @@ Examples:
275
268
  spinner.stop();
276
269
  }
277
270
  catch (e) {
278
- spinner.error({ text: e.message || "Failed to delete pod" });
279
- process.exit(EXIT_GENERAL_ERROR);
271
+ cliError(e.message || "Failed to delete pod", EXIT_GENERAL_ERROR);
280
272
  }
281
273
  });
282
274
  // ─── CLUSTERS ──────────────────────────────────────────
@@ -308,8 +300,7 @@ Examples:
308
300
  await saveFile("deploy-clusters", opts, res);
309
301
  }
310
302
  catch (e) {
311
- spinner.error({ text: e.message || "Failed to list clusters" });
312
- process.exit(EXIT_GENERAL_ERROR);
303
+ cliError(e.message || "Failed to list clusters", EXIT_GENERAL_ERROR);
313
304
  }
314
305
  });
315
306
  // ay deploy cluster-sync <id>
@@ -328,8 +319,7 @@ Examples:
328
319
  spinner.stop();
329
320
  }
330
321
  catch (e) {
331
- spinner.error({ text: e.message || "Failed to sync cluster" });
332
- process.exit(EXIT_GENERAL_ERROR);
322
+ cliError(e.message || "Failed to sync cluster", EXIT_GENERAL_ERROR);
333
323
  }
334
324
  });
335
325
  // ─── ALERTS ────────────────────────────────────────────
@@ -363,8 +353,7 @@ Examples:
363
353
  await saveFile("deploy-alerts", opts, res);
364
354
  }
365
355
  catch (e) {
366
- spinner.error({ text: e.message || "Failed to fetch alerts" });
367
- process.exit(EXIT_GENERAL_ERROR);
356
+ cliError(e.message || "Failed to fetch alerts", EXIT_GENERAL_ERROR);
368
357
  }
369
358
  });
370
359
  // ─── PIPELINES ─────────────────────────────────────────
@@ -399,8 +388,7 @@ Examples:
399
388
  await saveFile("deploy-pipelines", opts, res);
400
389
  }
401
390
  catch (e) {
402
- spinner.error({ text: e.message || "Failed to fetch pipelines" });
403
- process.exit(EXIT_GENERAL_ERROR);
391
+ cliError(e.message || "Failed to fetch pipelines", EXIT_GENERAL_ERROR);
404
392
  }
405
393
  });
406
394
  // ay deploy trigger <repo>
@@ -432,8 +420,7 @@ Examples:
432
420
  spinner.stop();
433
421
  }
434
422
  catch (e) {
435
- spinner.error({ text: e.message || "Failed to trigger pipeline" });
436
- process.exit(EXIT_GENERAL_ERROR);
423
+ cliError(e.message || "Failed to trigger pipeline", EXIT_GENERAL_ERROR);
437
424
  }
438
425
  });
439
426
  // ─── DEPLOYMENT PLANS ──────────────────────────────────
@@ -466,8 +453,7 @@ Examples:
466
453
  await saveFile("deploy-plans", opts, res);
467
454
  }
468
455
  catch (e) {
469
- spinner.error({ text: e.message || "Failed to list deployment plans" });
470
- process.exit(EXIT_GENERAL_ERROR);
456
+ cliError(e.message || "Failed to list deployment plans", EXIT_GENERAL_ERROR);
471
457
  }
472
458
  });
473
459
  // ay deploy plans get <id>
@@ -487,8 +473,7 @@ Examples:
487
473
  spinner.stop();
488
474
  }
489
475
  catch (e) {
490
- spinner.error({ text: e.message || "Failed to get plan" });
491
- process.exit(EXIT_GENERAL_ERROR);
476
+ cliError(e.message || "Failed to get plan", EXIT_GENERAL_ERROR);
492
477
  }
493
478
  });
494
479
  // ay deploy plans create --body '{...}'
@@ -507,8 +492,7 @@ Examples:
507
492
  body = JSON.parse(opts.body);
508
493
  }
509
494
  catch (_b) {
510
- spinner.error({ text: "Invalid JSON in --body" });
511
- process.exit(EXIT_GENERAL_ERROR);
495
+ cliError("Invalid JSON in --body", EXIT_GENERAL_ERROR);
512
496
  }
513
497
  }
514
498
  else if (opts.bodyFile) {
@@ -518,13 +502,11 @@ Examples:
518
502
  body = JSON.parse(content);
519
503
  }
520
504
  catch (_c) {
521
- spinner.error({ text: `Invalid JSON in file: ${opts.bodyFile}` });
522
- process.exit(EXIT_GENERAL_ERROR);
505
+ cliError(`Invalid JSON in file: ${opts.bodyFile}`, EXIT_GENERAL_ERROR);
523
506
  }
524
507
  }
525
508
  if (!body) {
526
- spinner.error({ text: "Provide plan definition via --body or --body-file" });
527
- process.exit(EXIT_GENERAL_ERROR);
509
+ cliError("Provide plan definition via --body or --body-file", EXIT_GENERAL_ERROR);
528
510
  }
529
511
  spinner.start({ text: "Creating deployment plan...", color: "magenta" });
530
512
  const res = await apiCallHandler("devops", "deployment-plans", "post", body, {
@@ -536,8 +518,7 @@ Examples:
536
518
  spinner.stop();
537
519
  }
538
520
  catch (e) {
539
- spinner.error({ text: e.message || "Failed to create plan" });
540
- process.exit(EXIT_GENERAL_ERROR);
521
+ cliError(e.message || "Failed to create plan", EXIT_GENERAL_ERROR);
541
522
  }
542
523
  });
543
524
  // ay deploy plans execute <id>
@@ -557,8 +538,7 @@ Examples:
557
538
  spinner.stop();
558
539
  }
559
540
  catch (e) {
560
- spinner.error({ text: e.message || "Failed to execute plan" });
561
- process.exit(EXIT_GENERAL_ERROR);
541
+ cliError(e.message || "Failed to execute plan", EXIT_GENERAL_ERROR);
562
542
  }
563
543
  });
564
544
  // ay deploy plans delete <id>
@@ -578,8 +558,7 @@ Examples:
578
558
  spinner.stop();
579
559
  }
580
560
  catch (e) {
581
- spinner.error({ text: e.message || "Failed to delete plan" });
582
- process.exit(EXIT_GENERAL_ERROR);
561
+ cliError(e.message || "Failed to delete plan", EXIT_GENERAL_ERROR);
583
562
  }
584
563
  });
585
564
  // ─── REPOSITORIES ──────────────────────────────────────
@@ -616,8 +595,7 @@ Examples:
616
595
  await saveFile("deploy-repos", opts, res);
617
596
  }
618
597
  catch (e) {
619
- spinner.error({ text: e.message || "Failed to list repositories" });
620
- process.exit(EXIT_GENERAL_ERROR);
598
+ cliError(e.message || "Failed to list repositories", EXIT_GENERAL_ERROR);
621
599
  }
622
600
  });
623
601
  // ay deploy repos sync <id>
@@ -636,8 +614,7 @@ Examples:
636
614
  spinner.stop();
637
615
  }
638
616
  catch (e) {
639
- spinner.error({ text: e.message || "Failed to sync repository" });
640
- process.exit(EXIT_GENERAL_ERROR);
617
+ cliError(e.message || "Failed to sync repository", EXIT_GENERAL_ERROR);
641
618
  }
642
619
  });
643
620
  // ─── DASHBOARD ─────────────────────────────────────────
@@ -659,8 +636,7 @@ Examples:
659
636
  spinner.stop();
660
637
  }
661
638
  catch (e) {
662
- spinner.error({ text: e.message || "Failed to load dashboard" });
663
- process.exit(EXIT_GENERAL_ERROR);
639
+ cliError(e.message || "Failed to load dashboard", EXIT_GENERAL_ERROR);
664
640
  }
665
641
  });
666
642
  }
@@ -2,8 +2,8 @@ import { Argument } from "commander";
2
2
  import { getModuleFromCollection } from "../models/getModuleFromCollection.js";
3
3
  import { handleDescribeSingleOperation } from "../operations/handleDescribeSingleOperation.js";
4
4
  import { localStorage } from "../helpers/localStorage.js";
5
- import { spinner } from "../../index.js";
6
5
  import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
6
+ import { cliError } from "../helpers/cliError.js";
7
7
  export function createDescribeCommand(program) {
8
8
  program
9
9
  .command("describe")
@@ -11,19 +11,21 @@ export function createDescribeCommand(program) {
11
11
  .description("Show detailed YAML description of an entry")
12
12
  .addHelpText("after", `
13
13
  Examples:
14
- ay describe contacts 64a1b2c3d4e5 Describe a contact entry
15
- ay d Describe last used entry`)
14
+ ay describe contacts 64a1b2c3d4e5 Describe a contact entry
15
+ ay describe tasks 64a1b2c3d4e5 comments Extract sub-resource array
16
+ ay describe tasks 64a1b2c3d4e5 comments -r json Sub-resource as JSON
17
+ ay describe tasks 64a1b2c3d4e5 --jq "subject" JMESPath on describe
18
+ ay d Describe last used entry`)
16
19
  .addArgument(new Argument("[collection]", "The collection to use").default(localStorage.getItem("lastCollection"), `The last used collection (${localStorage.getItem("lastCollection")})`))
17
20
  .addArgument(new Argument("[id]", "The ID of the entry to describe").default(localStorage.getItem("lastId"), `The last used id (${localStorage.getItem("lastId")})`))
18
- .action(async (collection, id, options) => {
21
+ .addArgument(new Argument("[subResource]", "Extract a sub-resource array (e.g., comments, attachments, worklogs)"))
22
+ .action(async (collection, id, subResource, options) => {
19
23
  try {
20
24
  if (!collection) {
21
- spinner.error({ text: "Missing required argument: collection. Run a list or get command first, or provide it explicitly." });
22
- process.exit(EXIT_MISUSE);
25
+ cliError("Missing required argument: collection. Run a list or get command first, or provide it explicitly.", EXIT_MISUSE);
23
26
  }
24
27
  if (!id) {
25
- spinner.error({ text: "Missing required argument: id. Provide an entry ID explicitly." });
26
- process.exit(EXIT_MISUSE);
28
+ cliError("Missing required argument: id. Provide an entry ID explicitly.", EXIT_MISUSE);
27
29
  }
28
30
  const opts = { ...program.opts(), ...options };
29
31
  const module = getModuleFromCollection(collection);
@@ -32,11 +34,11 @@ Examples:
32
34
  localStorage.setItem("lastId", id);
33
35
  await handleDescribeSingleOperation(module.module, collection, id, {
34
36
  ...opts,
37
+ subResource,
35
38
  });
36
39
  }
37
40
  catch (e) {
38
- spinner.error({ text: e.message || "An unexpected error occurred" });
39
- process.exit(EXIT_GENERAL_ERROR);
41
+ cliError(e.message || "An unexpected error occurred", EXIT_GENERAL_ERROR);
40
42
  }
41
43
  });
42
44
  }
@@ -2,9 +2,10 @@ import { Argument } from "commander";
2
2
  import { getModuleFromCollection } from "../models/getModuleFromCollection.js";
3
3
  import { handleGetSingleOperation } from "../operations/handleGetSingleOperation.js";
4
4
  import { handleEditOperation } from "../operations/handleEditOperation.js";
5
+ import { handleEditRawOperation } from "../operations/handleEditRawOperation.js";
5
6
  import { localStorage } from "../helpers/localStorage.js";
6
- import { spinner } from "../../index.js";
7
7
  import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
8
+ import { cliError } from "../helpers/cliError.js";
8
9
  export function createEditCommand(program) {
9
10
  program
10
11
  .command("edit")
@@ -19,12 +20,10 @@ Examples:
19
20
  .action(async (collection, id, options) => {
20
21
  try {
21
22
  if (!collection) {
22
- spinner.error({ text: "Missing required argument: collection. Run a list or get command first, or provide it explicitly." });
23
- process.exit(EXIT_MISUSE);
23
+ cliError("Missing required argument: collection. Run a list or get command first, or provide it explicitly.", EXIT_MISUSE);
24
24
  }
25
25
  if (!id) {
26
- spinner.error({ text: "Missing required argument: id. Provide an entry ID explicitly." });
27
- process.exit(EXIT_MISUSE);
26
+ cliError("Missing required argument: id. Provide an entry ID explicitly.", EXIT_MISUSE);
28
27
  }
29
28
  const opts = { ...program.opts(), ...options };
30
29
  const module = getModuleFromCollection(collection);
@@ -33,11 +32,17 @@ Examples:
33
32
  localStorage.setItem("lastId", id);
34
33
  let result = {};
35
34
  result = await handleGetSingleOperation(module.module, collection, id, opts);
36
- await handleEditOperation(module.module, collection, result.content);
35
+ // handleEditOperation expects {columns, rows} table structure;
36
+ // fall back to raw JSON editor if content doesn't have columns
37
+ if (result.content && result.content.columns) {
38
+ await handleEditOperation(module.module, collection, result.content);
39
+ }
40
+ else {
41
+ await handleEditRawOperation(module.module, collection, result.result || result.data);
42
+ }
37
43
  }
38
44
  catch (e) {
39
- spinner.error({ text: e.message || "An unexpected error occurred" });
40
- process.exit(EXIT_GENERAL_ERROR);
45
+ cliError(e.message || "An unexpected error occurred", EXIT_GENERAL_ERROR);
41
46
  }
42
47
  });
43
48
  }