@tolinax/ayoune-cli 2026.8.2 → 2026.9.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 (36) hide show
  1. package/lib/api/apiCallHandler.js +1 -1
  2. package/lib/api/apiClient.js +74 -62
  3. package/lib/commands/_registry.js +279 -0
  4. package/lib/commands/aggregate/_shared.js +21 -0
  5. package/lib/commands/aggregate/_stageBuilders.js +295 -0
  6. package/lib/commands/aggregate/exec.js +51 -0
  7. package/lib/commands/aggregate/index.js +38 -0
  8. package/lib/commands/aggregate/list.js +43 -0
  9. package/lib/commands/aggregate/models.js +43 -0
  10. package/lib/commands/aggregate/run.js +53 -0
  11. package/lib/commands/aggregate/save.js +53 -0
  12. package/lib/commands/aggregate/validate.js +47 -0
  13. package/lib/commands/aggregate/wizard.js +174 -0
  14. package/lib/commands/createAggregateCommand.js +5 -658
  15. package/lib/commands/createDeployCommand.js +5 -642
  16. package/lib/commands/createProgram.js +251 -161
  17. package/lib/commands/createServicesCommand.js +4 -5
  18. package/lib/commands/createWhoAmICommand.js +5 -5
  19. package/lib/commands/deploy/_token.js +8 -0
  20. package/lib/commands/deploy/alerts.js +43 -0
  21. package/lib/commands/deploy/clusters.js +62 -0
  22. package/lib/commands/deploy/dashboard.js +31 -0
  23. package/lib/commands/deploy/deployments.js +216 -0
  24. package/lib/commands/deploy/index.js +31 -0
  25. package/lib/commands/deploy/pipelines.js +82 -0
  26. package/lib/commands/deploy/plans.js +147 -0
  27. package/lib/commands/deploy/pods.js +70 -0
  28. package/lib/commands/deploy/repos.js +63 -0
  29. package/lib/helpers/dateFormat.js +119 -0
  30. package/lib/helpers/formatDocument.js +4 -5
  31. package/lib/helpers/logo.js +86 -13
  32. package/lib/helpers/saveFile.js +4 -9
  33. package/lib/models/getModelsInModules.js +6 -8
  34. package/lib/models/getModuleFromCollection.js +2 -2
  35. package/lib/operations/handleCollectionOperation.js +2 -3
  36. package/package.json +2 -12
@@ -1,60 +1,46 @@
1
- //Defines the main program structure for command line interface
1
+ // Defines the main program structure for the `ay` command line interface.
2
+ //
3
+ // This module is on the cold-start path of EVERY `ay` invocation, so it must
4
+ // stay free of heavy imports. Two patterns enforce that:
5
+ //
6
+ // 1. **Lazy command loading.** Instead of statically importing all 41
7
+ // command modules, we look up the user's argv in COMMAND_REGISTRY
8
+ // (`./_registry.ts`) and dynamic-import only the one they're running.
9
+ // Help / version / unknown-command paths fall back to registering
10
+ // lightweight stubs for every command so the help listing still works.
11
+ //
12
+ // 2. **Lazy logo.** `logo.ts` no longer top-level-imports `figlet`. The
13
+ // help-text thunks below only resolve when commander actually shows
14
+ // help, so the cost is paid once per `--help` invocation, not on every
15
+ // `ay <real-command>`.
16
+ //
17
+ // See `_registry.ts` for the registry, and `bench-startup.mjs` for the
18
+ // benchmark used to verify cold-start improvements.
2
19
  import { Command, Option } from "commander";
3
- import chalk from "chalk";
4
20
  import path from "path";
5
21
  import os from "os";
6
22
  import { spinner } from "../../index.js";
7
- import { createModulesCommand } from "./createModulesCommand.js";
8
- import { createListCommand } from "./createListCommand.js";
9
- import { createGetCommand } from "./createGetCommand.js";
10
- import { createEditCommand } from "./createEditCommand.js";
11
- import { createLoginCommand } from "./createLoginCommand.js";
12
- import { createDescribeCommand } from "./createDescribeCommand.js";
13
- import { createCopyCommand } from "./createCopyCommand.js";
14
- import { createCreateCommand } from "./createCreateCommand.js";
15
- import { createStorageCommand } from "./createStorageCommand.js";
16
- import { createAuditCommand } from "./createAuditCommand.js";
17
- import { createStreamCommand } from "./createStreamCommand.js";
18
- import { createEventsCommand } from "./createEventsCommand.js";
19
- import { createWhoAmICommand } from "./createWhoAmICommand.js";
20
- import { createLogoutCommand } from "./createLogoutCommand.js";
21
- import { createCompletionsCommand } from "./createCompletionsCommand.js";
22
- import { createAliasCommand, registerUserAliases } from "./createAliasCommand.js";
23
- import { createConfigCommand } from "./createConfigCommand.js";
24
- import { createActionsCommand } from "./createActionsCommand.js";
25
- import { createExecCommand } from "./createExecCommand.js";
26
- import { createAiCommand } from "./createAiCommand.js";
27
- import { enableDebug } from "../api/apiClient.js";
28
- import { enableDryRun } from "../api/apiCallHandler.js";
23
+ import { findCommandSpec, registerStubsForHelp } from "./_registry.js";
24
+ // Cheap helpers these are pure-TS modules with no heavy transitive imports,
25
+ // so they can stay top-level without affecting cold-start.
29
26
  import { setJsonErrorsEnabled } from "../helpers/cliError.js";
30
27
  import { disableContext } from "../helpers/contextInjector.js";
31
- import { createServicesCommand } from "./createServicesCommand.js";
32
- import { createDeployCommand } from "./createDeployCommand.js";
33
- import { createMonitorCommand } from "./createMonitorCommand.js";
34
- import { createDeleteCommand } from "./createDeleteCommand.js";
35
- import { createUpdateCommand } from "./createUpdateCommand.js";
36
- import { createBatchCommand } from "./createBatchCommand.js";
37
- import { createSearchCommand } from "./createSearchCommand.js";
38
- import { createWebhooksCommand } from "./createWebhooksCommand.js";
39
- import { createJobsCommand } from "./createJobsCommand.js";
40
- import { createExportCommand } from "./createExportCommand.js";
41
- import { createUsersCommand } from "./createUsersCommand.js";
42
- import { createSyncCommand } from "./createSyncCommand.js";
43
- import { createPermissionsCommand } from "./createPermissionsCommand.js";
44
- import { createTemplateCommand } from "./createTemplateCommand.js";
45
- import { createSetupCommand } from "./createSetupCommand.js";
46
- import { createStatusCommand } from "./createStatusCommand.js";
47
- import { createSelfHostUpdateCommand } from "./createSelfHostUpdateCommand.js";
48
- import { createContextCommand } from "./createContextCommand.js";
49
- import { createAccessCommand } from "./createAccessCommand.js";
50
- import { createAggregateCommand } from "./createAggregateCommand.js";
51
- import { createDbCommand } from "./createDbCommand.js";
52
- import { secureStorage } from "../helpers/secureStorage.js";
53
- import { login } from "../api/login.js";
54
28
  import { loadConfig } from "../helpers/configLoader.js";
55
- import { getLogo, getDescription, BRAND_BLUE } from "../helpers/logo.js";
56
- import { checkForUpdates } from "../helpers/updateNotifier.js";
29
+ import { loadAliases } from "./createAliasCommand.js";
30
+ // logo.ts is intentionally lightweight: it does NOT top-level-import chalk
31
+ // or figlet, so static-importing it here is free. The expensive bits live
32
+ // inside getLogoSync()/brandHighlight()/dim(), each of which lazily requires
33
+ // chalk/figlet only on first call.
34
+ import { getLogoSync, brandHighlight, dim } from "../helpers/logo.js";
57
35
  import { createRequire } from "module";
36
+ // HEAVY modules deliberately NOT imported at the top of this file:
37
+ // - ../api/apiClient (pulls in @tolinax/ayoune-core HTTP client + axios)
38
+ // - ../api/apiCallHandler (pulls in apiClient + secureStorage + login)
39
+ // - ../helpers/secureStorage (pulls in node-localstorage + crypto)
40
+ // - ../api/login (pulls in socket.io-client)
41
+ // - ../helpers/updateNotifier (pulls in node-localstorage)
42
+ // They are lazy-imported inside the preAction hook (which only fires for real
43
+ // command runs, never for --version/--help/unknown-command paths).
58
44
  const require = createRequire(import.meta.url);
59
45
  let pkg;
60
46
  try {
@@ -63,120 +49,103 @@ try {
63
49
  catch (_a) {
64
50
  pkg = require("../../../package.json"); // source tree (tests)
65
51
  }
66
- export function createProgram(program) {
67
- program
68
- .version(pkg.version || "0.0.0")
69
- .addOption(new Option("-r, --responseFormat <format>", "Set the output format")
70
- .choices(["json", "csv", "yaml", "table"])
71
- .default("json"))
72
- .addOption(new Option("-v, --verbosity <level>", "Set the verbosity level of the returned meta information")
73
- .choices(["default", "extended", "minimal"])
74
- .default("default")
75
- .conflicts("hideMeta"))
76
- .addOption(new Option("-m, --hideMeta", "Returns only the payload without meta information. ")
77
- .default(false)
78
- .conflicts("verbosity"))
79
- .addOption(new Option("-s, --save", "Saves the response as file"))
80
- .addOption(new Option("-d, --debug", "Show detailed request/response information"))
81
- .addOption(new Option("-o, --outPath [filePath]", "Lets you set a path").default(path.join(os.homedir(), "aYOUne")))
82
- .addOption(new Option("-n, --name [fileName]", "Lets you set a filename"))
83
- .addOption(new Option("-q, --quiet", "Suppress all output except errors and results"))
84
- .addOption(new Option("--force", "Skip confirmation prompts for destructive actions"))
85
- .addOption(new Option("--dry-run", "Preview what a command would do without executing"))
86
- .addOption(new Option("--jq <expression>", "Filter JSON output using JMESPath expression"))
87
- .addOption(new Option("--columns <fields>", "Comma-separated list of columns to display"))
88
- .addOption(new Option("--no-color", "Disable colored output"))
89
- .addOption(new Option("--json-errors", "Output errors as JSON to stderr (for AI agents/scripts)"))
90
- .addOption(new Option("--token <jwt>", "Authenticate with a JWT token for this session"))
91
- .addOption(new Option("--no-context", "Disable context injection for this command"))
92
- .description(getDescription());
93
- program.showHelpAfterError();
94
- program.showSuggestionAfterError(true);
95
- createModulesCommand(program);
96
- createListCommand(program);
97
- createGetCommand(program);
98
- createEditCommand(program);
99
- createCopyCommand(program);
100
- createDescribeCommand(program);
101
- createCreateCommand(program);
102
- createStorageCommand(program);
103
- createAuditCommand(program);
104
- createStreamCommand(program);
105
- createEventsCommand(program);
106
- createWhoAmICommand(program);
107
- createLogoutCommand(program);
108
- createActionsCommand(program);
109
- createExecCommand(program);
110
- createAiCommand(program);
111
- createServicesCommand(program);
112
- createDeployCommand(program);
113
- createMonitorCommand(program);
114
- createDeleteCommand(program);
115
- createUpdateCommand(program);
116
- createBatchCommand(program);
117
- createSearchCommand(program);
118
- createWebhooksCommand(program);
119
- createJobsCommand(program);
120
- createExportCommand(program);
121
- createUsersCommand(program);
122
- createSyncCommand(program);
123
- createPermissionsCommand(program);
124
- createTemplateCommand(program);
125
- createAccessCommand(program);
126
- createAggregateCommand(program);
127
- createDbCommand(program);
128
- createSetupCommand(program);
129
- createStatusCommand(program);
130
- createSelfHostUpdateCommand(program);
131
- createContextCommand(program);
132
- createCompletionsCommand(program);
133
- createAliasCommand(program);
134
- createConfigCommand(program);
135
- registerUserAliases(program);
136
- createLoginCommand(program);
137
- program.addHelpText("beforeAll", getLogo());
138
- program.addHelpText("afterAll", chalk.dim("\n Run ay <command> --help for more info\n"));
139
- program.configureHelp({
140
- sortOptions: true,
141
- sortSubcommands: true,
142
- showGlobalOptions: true,
143
- formatHelp: (cmd, helper) => {
144
- const defaultHelp = Command.prototype.createHelp().formatHelp(cmd, helper);
145
- return defaultHelp.replace(/^(Usage:|Commands:|Options:|Global Options:)/gm, (match) => chalk.hex(BRAND_BLUE).bold(match));
146
- },
147
- });
148
- // Respect NO_COLOR env var (no-color.org standard)
149
- if (process.env.NO_COLOR !== undefined) {
150
- chalk.level = 0;
52
+ /**
53
+ * Find the first non-flag argument in argv (skipping `node` + script path).
54
+ * This is the command name candidate that we'll feed to the registry.
55
+ *
56
+ * Returns `undefined` if argv has no command (e.g. plain `ay`).
57
+ */
58
+ function findFirstCommandArg(argv) {
59
+ for (let i = 2; i < argv.length; i++) {
60
+ const arg = argv[i];
61
+ if (!arg.startsWith("-"))
62
+ return arg;
151
63
  }
64
+ return undefined;
65
+ }
66
+ /**
67
+ * Expand a user alias in argv. If `argv[2]` is a key in `~/.config/ayoune/aliases.json`,
68
+ * replace it with the alias value (split on whitespace) and return the new argv.
69
+ * Otherwise return argv unchanged.
70
+ *
71
+ * Example: alias `ll` → `list leads`, invocation `ay ll --limit 5` becomes
72
+ * `ay list leads --limit 5`.
73
+ */
74
+ function expandUserAlias(argv) {
75
+ const cmdArg = findFirstCommandArg(argv);
76
+ if (!cmdArg)
77
+ return argv;
78
+ // Cheap if no alias file exists; loadAliases() returns {} on miss.
79
+ const aliases = loadAliases();
80
+ const expansion = aliases[cmdArg];
81
+ if (!expansion)
82
+ return argv;
83
+ // Find where the alias sits in argv so we can splice in its expansion
84
+ // without disturbing leading global flags.
85
+ const idx = argv.indexOf(cmdArg, 2);
86
+ if (idx < 0)
87
+ return argv;
88
+ const expanded = expansion.split(/\s+/).filter(Boolean);
89
+ return [...argv.slice(0, idx), ...expanded, ...argv.slice(idx + 1)];
90
+ }
91
+ function classifyInvocation(argv) {
92
+ const args = argv.slice(2);
93
+ // --version trumps everything; commander handles it before any subcommand.
94
+ if (args.includes("--version") || args.includes("-V"))
95
+ return "version";
96
+ // Bare `ay` → root help
97
+ if (args.length === 0)
98
+ return "rootHelp";
99
+ // `ay help [subcmd?]`
100
+ if (args[0] === "help") {
101
+ return args.length >= 2 && !args[1].startsWith("-") ? "cmdHelp" : "rootHelp";
102
+ }
103
+ // `ay --help` / `ay -h` with no command before the flag → root help
104
+ const helpIdx = args.findIndex((a) => a === "--help" || a === "-h");
105
+ if (helpIdx === 0)
106
+ return "rootHelp";
107
+ // `ay <cmd> --help` / `ay <cmd> -h` → cmd-specific help
108
+ if (helpIdx > 0)
109
+ return "cmdHelp";
110
+ return "command";
111
+ }
112
+ /**
113
+ * Apply global option side-effects (debug, dry-run, quiet, etc.) and
114
+ * config-file defaults. Runs in commander's preAction hook so it sees the
115
+ * fully-parsed global flags before any subcommand action fires.
116
+ */
117
+ function installPreActionHook(program) {
152
118
  program.hook("preAction", async (thisCommand) => {
153
119
  var _a;
154
120
  // Apply saved defaults — only when the user didn't pass the flag explicitly
155
121
  const config = loadConfig();
156
122
  const defaults = (_a = config.defaults) !== null && _a !== void 0 ? _a : {};
157
123
  const rawArgs = process.argv.slice(2);
158
- if (defaults.responseFormat && !rawArgs.some(a => ["-r", "--responseFormat"].includes(a)))
124
+ if (defaults.responseFormat && !rawArgs.some((a) => ["-r", "--responseFormat"].includes(a)))
159
125
  program.setOptionValue("responseFormat", defaults.responseFormat);
160
- if (defaults.verbosity && !rawArgs.some(a => ["-v", "--verbosity"].includes(a)))
126
+ if (defaults.verbosity && !rawArgs.some((a) => ["-v", "--verbosity"].includes(a)))
161
127
  program.setOptionValue("verbosity", defaults.verbosity);
162
- if (defaults.outPath && !rawArgs.some(a => ["-o", "--outPath"].includes(a)))
128
+ if (defaults.outPath && !rawArgs.some((a) => ["-o", "--outPath"].includes(a)))
163
129
  program.setOptionValue("outPath", defaults.outPath);
164
- if (defaults.hideMeta && !rawArgs.some(a => ["-m", "--hideMeta"].includes(a)))
130
+ if (defaults.hideMeta && !rawArgs.some((a) => ["-m", "--hideMeta"].includes(a)))
165
131
  program.setOptionValue("hideMeta", defaults.hideMeta);
166
- if (defaults.quiet && !rawArgs.some(a => ["-q", "--quiet"].includes(a)))
132
+ if (defaults.quiet && !rawArgs.some((a) => ["-q", "--quiet"].includes(a)))
167
133
  program.setOptionValue("quiet", defaults.quiet);
168
- if (defaults.force && !rawArgs.some(a => ["--force"].includes(a)))
134
+ if (defaults.force && !rawArgs.some((a) => ["--force"].includes(a)))
169
135
  program.setOptionValue("force", defaults.force);
170
- if (defaults.dryRun && !rawArgs.some(a => ["--dry-run"].includes(a)))
136
+ if (defaults.dryRun && !rawArgs.some((a) => ["--dry-run"].includes(a)))
171
137
  program.setOptionValue("dryRun", defaults.dryRun);
172
138
  const opts = program.opts();
173
139
  if (opts.color === false) {
140
+ const { default: chalk } = await import("chalk");
174
141
  chalk.level = 0;
175
142
  }
176
143
  if (opts.debug) {
144
+ const { enableDebug } = await import("../api/apiClient.js");
177
145
  enableDebug();
178
146
  }
179
147
  if (opts.dryRun) {
148
+ const { enableDryRun } = await import("../api/apiCallHandler.js");
180
149
  enableDryRun();
181
150
  }
182
151
  if (opts.jsonErrors) {
@@ -192,30 +161,151 @@ export function createProgram(program) {
192
161
  spinner.stop = () => spinner;
193
162
  // spinner.error left intact
194
163
  }
195
- // Token from --token flag (highest priority), then env var, then stored token
196
- if (opts.token) {
197
- secureStorage.setItem("token", opts.token);
198
- }
199
- else {
200
- const envToken = process.env.AYOUNE_TOKEN;
201
- if (envToken && !secureStorage.getItem("token")) {
202
- secureStorage.setItem("token", envToken);
203
- }
204
- }
205
- // First-run onboarding: auto-login if no token stored
164
+ // secureStorage is heavy (node-localstorage + crypto), so only load it
165
+ // when the user actually has a token to set or we need to check for one.
166
+ const hasTokenFlag = !!opts.token;
167
+ const hasTokenEnv = !!process.env.AYOUNE_TOKEN;
206
168
  const cmdName = thisCommand.name();
207
- const skipAuth = ["login", "logout", "whoami", "completions", "alias", "config", "help", "setup", "status", "self-host-update", "context", "db"];
208
- if (!skipAuth.includes(cmdName) && process.stdin.isTTY) {
209
- const token = secureStorage.getItem("token");
210
- if (!token) {
211
- console.error(chalk.hex('#2B8DC6').bold("\n Welcome to aYOUne CLI!\n"));
212
- console.error(chalk.dim(" You need to authenticate before using this command.\n"));
213
- await login();
169
+ const skipAuth = [
170
+ "login",
171
+ "logout",
172
+ "whoami",
173
+ "completions",
174
+ "alias",
175
+ "config",
176
+ "help",
177
+ "setup",
178
+ "status",
179
+ "self-host-update",
180
+ "context",
181
+ "db",
182
+ ];
183
+ const needsAuthCheck = !skipAuth.includes(cmdName) && process.stdin.isTTY;
184
+ if (hasTokenFlag || hasTokenEnv || needsAuthCheck) {
185
+ const { secureStorage } = await import("../helpers/secureStorage.js");
186
+ // Token from --token flag (highest priority), then env var, then stored token
187
+ if (hasTokenFlag) {
188
+ secureStorage.setItem("token", opts.token);
189
+ }
190
+ else if (hasTokenEnv && !secureStorage.getItem("token")) {
191
+ secureStorage.setItem("token", process.env.AYOUNE_TOKEN);
192
+ }
193
+ // First-run onboarding: auto-login if no token stored
194
+ if (needsAuthCheck) {
195
+ const token = secureStorage.getItem("token");
196
+ if (!token) {
197
+ const { default: chalk } = await import("chalk");
198
+ const { login } = await import("../api/login.js");
199
+ console.error(chalk.hex("#2B8DC6").bold("\n Welcome to aYOUne CLI!\n"));
200
+ console.error(chalk.dim(" You need to authenticate before using this command.\n"));
201
+ await login();
202
+ }
214
203
  }
215
204
  }
216
205
  // Non-blocking update check (throttled, silent on errors)
206
+ const { checkForUpdates } = await import("../helpers/updateNotifier.js");
217
207
  checkForUpdates(pkg.version || "0.0.0");
218
208
  });
219
- //Parse command line arguments
220
- program.parse(process.argv);
209
+ }
210
+ /**
211
+ * Wire global options that exist on every command (responseFormat, verbosity,
212
+ * etc.). These must be added to the root program BEFORE subcommand
213
+ * registration so they're inherited.
214
+ */
215
+ function installGlobalOptions(program) {
216
+ program
217
+ .version(pkg.version || "0.0.0")
218
+ .addOption(new Option("-r, --responseFormat <format>", "Set the output format")
219
+ .choices(["json", "csv", "yaml", "table"])
220
+ .default("json"))
221
+ .addOption(new Option("-v, --verbosity <level>", "Set the verbosity level of the returned meta information")
222
+ .choices(["default", "extended", "minimal"])
223
+ .default("default")
224
+ .conflicts("hideMeta"))
225
+ .addOption(new Option("-m, --hideMeta", "Returns only the payload without meta information. ")
226
+ .default(false)
227
+ .conflicts("verbosity"))
228
+ .addOption(new Option("-s, --save", "Saves the response as file"))
229
+ .addOption(new Option("-d, --debug", "Show detailed request/response information"))
230
+ .addOption(new Option("-o, --outPath [filePath]", "Lets you set a path").default(path.join(os.homedir(), "aYOUne")))
231
+ .addOption(new Option("-n, --name [fileName]", "Lets you set a filename"))
232
+ .addOption(new Option("-q, --quiet", "Suppress all output except errors and results"))
233
+ .addOption(new Option("--force", "Skip confirmation prompts for destructive actions"))
234
+ .addOption(new Option("--dry-run", "Preview what a command would do without executing"))
235
+ .addOption(new Option("--jq <expression>", "Filter JSON output using JMESPath expression"))
236
+ .addOption(new Option("--columns <fields>", "Comma-separated list of columns to display"))
237
+ .addOption(new Option("--no-color", "Disable colored output"))
238
+ .addOption(new Option("--json-errors", "Output errors as JSON to stderr (for AI agents/scripts)"))
239
+ .addOption(new Option("--token <jwt>", "Authenticate with a JWT token for this session"))
240
+ .addOption(new Option("--no-context", "Disable context injection for this command"));
241
+ }
242
+ /**
243
+ * Configure help output: the brand logo, custom formatter that highlights
244
+ * section headings, and respect for NO_COLOR. The thunks (`() => getLogo()`)
245
+ * are critical — they defer the figlet/chalk import until commander actually
246
+ * needs to render help, keeping the cold-start path tiny.
247
+ */
248
+ function installHelp(program) {
249
+ program.showHelpAfterError();
250
+ program.showSuggestionAfterError(true);
251
+ // Help-text thunks: commander only invokes these when --help is rendered,
252
+ // so the figlet/chalk loads inside getLogoSync()/dim() are paid once per
253
+ // help invocation, never on the hot path.
254
+ program.addHelpText("beforeAll", () => getLogoSync());
255
+ program.addHelpText("afterAll", () => dim("\n Run ay <command> --help for more info\n"));
256
+ program.configureHelp({
257
+ sortOptions: true,
258
+ sortSubcommands: true,
259
+ showGlobalOptions: true,
260
+ formatHelp: (cmd, helper) => {
261
+ const defaultHelp = Command.prototype.createHelp().formatHelp(cmd, helper);
262
+ return defaultHelp.replace(/^(Usage:|Commands:|Options:|Global Options:)/gm, (match) => brandHighlight(match));
263
+ },
264
+ });
265
+ // Respect NO_COLOR env var (no-color.org standard) — applied here so it
266
+ // affects help output even before preAction fires.
267
+ if (process.env.NO_COLOR !== undefined) {
268
+ // chalk is needed for the color level toggle; cheap to import.
269
+ // We do it lazily so it doesn't pollute non-color paths.
270
+ import("chalk").then(({ default: chalk }) => {
271
+ chalk.level = 0;
272
+ });
273
+ }
274
+ program.description("aYOUne — Business as a Service CLI");
275
+ }
276
+ export async function createProgram(program) {
277
+ installGlobalOptions(program);
278
+ installHelp(program);
279
+ installPreActionHook(program);
280
+ // Step 1: expand any user-defined alias in argv (e.g. `ay ll` → `ay list leads`).
281
+ // The expansion happens BEFORE registry lookup so the resolved name is what
282
+ // gets matched.
283
+ process.argv = expandUserAlias(process.argv);
284
+ // Step 2: classify the invocation and load only what's needed.
285
+ const kind = classifyInvocation(process.argv);
286
+ const firstCmd = findFirstCommandArg(process.argv);
287
+ const spec = firstCmd ? findCommandSpec(firstCmd) : undefined;
288
+ if (kind === "version") {
289
+ // Nothing to register — commander handles -V before any subcommand action.
290
+ }
291
+ else if (kind === "rootHelp") {
292
+ // Lightweight stubs for every command so the help listing is complete and
293
+ // commander can suggest similar names for typos.
294
+ registerStubsForHelp(program);
295
+ }
296
+ else if (kind === "cmdHelp" || kind === "command") {
297
+ if (spec) {
298
+ // Hot path: load and register the ONE full command the user is running
299
+ // (or asking help for). Its options/examples will be available to
300
+ // commander's help formatter.
301
+ const register = await spec.loader();
302
+ register(program);
303
+ }
304
+ else {
305
+ // Unknown command — register all stubs so commander's "did you mean"
306
+ // can suggest similar names. parseAsync will exit non-zero.
307
+ registerStubsForHelp(program);
308
+ }
309
+ }
310
+ await program.parseAsync(process.argv);
221
311
  }
@@ -127,16 +127,15 @@ Examples:
127
127
  // Deduplicate by host
128
128
  const uniqueTargets = [...new Map(targets.map((t) => [t.host, t])).values()];
129
129
  spinner.start({ text: `Checking ${uniqueTargets.length} service(s)...`, color: "magenta" });
130
- const { default: axios } = await import("axios");
131
- const https = await import("https");
132
- const agent = new https.Agent({ rejectUnauthorized: false });
130
+ const { http } = await import("@tolinax/ayoune-core/lib/http");
133
131
  const results = await Promise.allSettled(uniqueTargets.map(async (t) => {
134
132
  const start = Date.now();
135
133
  try {
136
- const resp = await axios.get(`https://${t.host}/`, {
134
+ const resp = await http.get(`https://${t.host}/`, {
137
135
  timeout: opts.timeout,
138
- httpsAgent: agent,
139
136
  validateStatus: () => true,
137
+ logging: false,
138
+ metrics: false,
140
139
  });
141
140
  return {
142
141
  host: t.host,
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import moment from "moment";
2
+ import { formatIsoLocal, fromNow, fromUnix, isBefore } from "../helpers/dateFormat.js";
3
3
  import { secureStorage } from "../helpers/secureStorage.js";
4
4
  import { decodeToken } from "../api/decodeToken.js";
5
5
  import { login } from "../api/login.js";
@@ -22,10 +22,10 @@ const LABEL_MAP = {
22
22
  const SKIP_FIELDS = new Set(["password", "hash", "salt", "__v"]);
23
23
  function formatValue(key, value) {
24
24
  if ((key === "iat" || key === "exp") && typeof value === "number") {
25
- const m = moment.unix(value);
26
- const relative = m.fromNow();
27
- const formatted = m.format("YYYY-MM-DD HH:mm:ss");
28
- if (key === "exp" && m.isBefore(moment())) {
25
+ const date = fromUnix(value);
26
+ const relative = fromNow(date);
27
+ const formatted = formatIsoLocal(date);
28
+ if (key === "exp" && isBefore(date, new Date())) {
29
29
  return chalk.red(`${formatted} (expired ${relative})`);
30
30
  }
31
31
  return `${formatted} ${chalk.dim(`(${relative})`)}`;
@@ -0,0 +1,8 @@
1
+ // Shared helper used by deploy subcommands that bypass apiCallHandler and
2
+ // hit the DevOps API directly (e.g. the SSE log stream in
3
+ // `deployments/logs --follow`). secureStorage is the canonical token source;
4
+ // AYOUNE_TOKEN env var is the script-friendly fallback.
5
+ import { secureStorage } from "../../helpers/secureStorage.js";
6
+ export function getDeployToken() {
7
+ return secureStorage.getItem("token") || process.env.AYOUNE_TOKEN || "";
8
+ }
@@ -0,0 +1,43 @@
1
+ // `ay deploy alerts` — surface active deployment alerts (pod crashes,
2
+ // OOM kills, image pull errors, etc.). Read-only filter view; ack/resolve
3
+ // lives under `ay monitor` so we don't duplicate state mutation here.
4
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
5
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
6
+ import { saveFile } from "../../helpers/saveFile.js";
7
+ import { spinner } from "../../../index.js";
8
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
9
+ import { cliError } from "../../helpers/cliError.js";
10
+ export function addAlertsSubcommands(deploy, rootProgram) {
11
+ deploy
12
+ .command("alerts")
13
+ .description("List active deployment alerts")
14
+ .option("--severity <level>", "Filter: critical, warning, info")
15
+ .option("--type <type>", "Filter: pod_crash, oom_killed, image_pull_error, deployment_failed, pipeline_failed, cluster_unreachable, high_restart_count, custom")
16
+ .option("-l, --limit <number>", "Limit", parseInt, 50)
17
+ .action(async (options) => {
18
+ var _a, _b, _c;
19
+ try {
20
+ const opts = { ...rootProgram.opts(), ...options };
21
+ spinner.start({ text: "Fetching alerts...", color: "magenta" });
22
+ const params = {
23
+ limit: opts.limit,
24
+ responseFormat: opts.responseFormat,
25
+ verbosity: opts.verbosity,
26
+ };
27
+ if (opts.severity)
28
+ params.severity = opts.severity;
29
+ if (opts.type)
30
+ params.type = opts.type;
31
+ const res = await apiCallHandler("devops", "alerts", "get", null, params);
32
+ handleResponseFormatOptions(opts, res);
33
+ 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;
34
+ spinner.success({ text: `Found ${total} alerts` });
35
+ spinner.stop();
36
+ if (opts.save)
37
+ await saveFile("deploy-alerts", opts, res);
38
+ }
39
+ catch (e) {
40
+ cliError(e.message || "Failed to fetch alerts", EXIT_GENERAL_ERROR);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,62 @@
1
+ // `ay deploy {clusters, cluster-sync}` — Kubernetes cluster discovery and
2
+ // manual reconcile trigger. The DevOps API maintains a registry of clusters
3
+ // the customer has connected; `clusters` lists them and `cluster-sync` kicks
4
+ // off a re-scan of resources for one cluster.
5
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
6
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
7
+ import { saveFile } from "../../helpers/saveFile.js";
8
+ import { spinner } from "../../../index.js";
9
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
10
+ import { cliError } from "../../helpers/cliError.js";
11
+ export function addClustersSubcommands(deploy, rootProgram) {
12
+ // ay deploy clusters
13
+ deploy
14
+ .command("clusters")
15
+ .alias("cl")
16
+ .description("List Kubernetes clusters and their health status")
17
+ .addHelpText("after", `
18
+ Examples:
19
+ ay deploy clusters List all clusters
20
+ ay deploy clusters -r table Show clusters in table format`)
21
+ .option("-l, --limit <number>", "Limit", parseInt, 50)
22
+ .action(async (options) => {
23
+ var _a, _b, _c;
24
+ try {
25
+ const opts = { ...rootProgram.opts(), ...options };
26
+ spinner.start({ text: "Fetching clusters...", color: "magenta" });
27
+ const res = await apiCallHandler("devops", "clusters", "get", null, {
28
+ limit: opts.limit,
29
+ responseFormat: opts.responseFormat,
30
+ verbosity: opts.verbosity,
31
+ });
32
+ handleResponseFormatOptions(opts, res);
33
+ 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;
34
+ spinner.success({ text: `Found ${total} clusters` });
35
+ spinner.stop();
36
+ if (opts.save)
37
+ await saveFile("deploy-clusters", opts, res);
38
+ }
39
+ catch (e) {
40
+ cliError(e.message || "Failed to list clusters", EXIT_GENERAL_ERROR);
41
+ }
42
+ });
43
+ // ay deploy cluster-sync <id>
44
+ deploy
45
+ .command("cluster-sync <id>")
46
+ .description("Trigger sync for a Kubernetes cluster")
47
+ .action(async (id, options) => {
48
+ try {
49
+ const opts = { ...rootProgram.opts(), ...options };
50
+ spinner.start({ text: `Syncing cluster ${id}...`, color: "magenta" });
51
+ const res = await apiCallHandler("devops", `clusters/${id}/sync`, "post", null, {
52
+ responseFormat: opts.responseFormat,
53
+ });
54
+ handleResponseFormatOptions(opts, res);
55
+ spinner.success({ text: `Cluster ${id} sync initiated` });
56
+ spinner.stop();
57
+ }
58
+ catch (e) {
59
+ cliError(e.message || "Failed to sync cluster", EXIT_GENERAL_ERROR);
60
+ }
61
+ });
62
+ }
@@ -0,0 +1,31 @@
1
+ // `ay deploy dashboard` — single-call overview that returns the same
2
+ // summary the web devops dashboard renders (cluster health, deployment
3
+ // counts, active alerts, recent pipelines). Useful as a one-shot health
4
+ // check during incident response.
5
+ import { apiCallHandler } from "../../api/apiCallHandler.js";
6
+ import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
7
+ import { spinner } from "../../../index.js";
8
+ import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
9
+ import { cliError } from "../../helpers/cliError.js";
10
+ export function addDashboardSubcommands(deploy, rootProgram) {
11
+ deploy
12
+ .command("dashboard")
13
+ .alias("dash")
14
+ .description("Show deployment overview dashboard (clusters, deployments, alerts, pipelines)")
15
+ .action(async (options) => {
16
+ try {
17
+ const opts = { ...rootProgram.opts(), ...options };
18
+ spinner.start({ text: "Fetching dashboard...", color: "magenta" });
19
+ const res = await apiCallHandler("devops", "dashboard", "get", null, {
20
+ responseFormat: opts.responseFormat,
21
+ verbosity: opts.verbosity,
22
+ });
23
+ handleResponseFormatOptions(opts, res);
24
+ spinner.success({ text: "Dashboard loaded" });
25
+ spinner.stop();
26
+ }
27
+ catch (e) {
28
+ cliError(e.message || "Failed to load dashboard", EXIT_GENERAL_ERROR);
29
+ }
30
+ });
31
+ }