@tolinax/ayoune-cli 2026.11.4 → 2026.12.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.
@@ -1,10 +1,24 @@
1
1
  import chalk from "chalk";
2
+ import inquirer from "inquirer";
2
3
  import { modelsAndRights } from "../../data/modelsAndRights.js";
3
4
  import { aYOUneModules } from "../../data/modules.js";
4
5
  import { isSuperUser, getTokenPayload } from "../helpers/tokenPayload.js";
5
6
  import { handleResponseFormatOptions } from "../helpers/handleResponseFormatOptions.js";
6
7
  import { EXIT_GENERAL_ERROR } from "../exitCodes.js";
7
8
  import { cliError } from "../helpers/cliError.js";
9
+ // ── Visual helpers ──────────────────────────────────────────────────────────
10
+ const BAR_CHAR = "█";
11
+ const BAR_HALF = "▌";
12
+ const DIM_CHAR = "░";
13
+ function bar(value, max, width) {
14
+ if (max === 0)
15
+ return chalk.dim(DIM_CHAR.repeat(width));
16
+ const filled = (value / max) * width;
17
+ const full = Math.floor(filled);
18
+ const half = filled - full >= 0.5 ? 1 : 0;
19
+ const empty = width - full - half;
20
+ return chalk.cyan(BAR_CHAR.repeat(full)) + (half ? chalk.cyan(BAR_HALF) : "") + chalk.dim(DIM_CHAR.repeat(empty));
21
+ }
8
22
  function getAccessibleModules(su) {
9
23
  var _a;
10
24
  const moduleSummaries = [];
@@ -21,52 +35,209 @@ function getAccessibleModules(su) {
21
35
  }
22
36
  return moduleSummaries;
23
37
  }
24
- function displayAccess(modules, su) {
38
+ function getModuleCollections(modName) {
39
+ return modelsAndRights
40
+ .filter((m) => m.module === modName)
41
+ .map((m) => ({ collection: m.plural, singular: m.singular, right: m.right }))
42
+ .sort((a, b) => a.collection.localeCompare(b.collection));
43
+ }
44
+ // ── Display: summary ────────────────────────────────────────────────────────
45
+ function displaySummary(modules, su) {
25
46
  const payload = getTokenPayload();
26
47
  const name = (payload === null || payload === void 0 ? void 0 : payload.name) || (payload === null || payload === void 0 ? void 0 : payload.username) || (payload === null || payload === void 0 ? void 0 : payload.email) || "Unknown";
27
48
  const type = (payload === null || payload === void 0 ? void 0 : payload.type) || "user";
49
+ const totalCollections = modules.reduce((sum, m) => sum + m.collections, 0);
50
+ const maxCount = Math.max(...modules.map((m) => m.collections));
51
+ // Sort: largest modules first for visual impact
52
+ const sorted = [...modules].sort((a, b) => b.collections - a.collections);
28
53
  console.log();
29
- console.log(chalk.bold(` Access Summary for ${chalk.cyan(name)}`));
30
- console.log(chalk.dim(` Type: ${su ? chalk.yellow("superuser") : type}`));
31
- console.log(chalk.dim(` ${"─".repeat(50)}`));
54
+ console.log(chalk.bold(` Access Summary`));
55
+ console.log(chalk.dim(` User: `) + chalk.cyan(name) + chalk.dim(` Type: `) + (su ? chalk.yellow.bold("superuser") : chalk.white(type)));
56
+ console.log(chalk.dim(` ${modules.length} modules, ${totalCollections} collections accessible`));
32
57
  console.log();
33
- const totalCollections = modules.reduce((sum, m) => sum + m.collections, 0);
34
- // Column widths
35
- const maxMod = Math.max(...modules.map((m) => m.module.length), 6);
36
- const maxLabel = Math.max(...modules.map((m) => m.label.length), 5);
37
- console.log(` ${chalk.dim("MODULE".padEnd(maxMod + 2))} ${chalk.dim("LABEL".padEnd(maxLabel + 2))} ${chalk.dim("COLLECTIONS")}`);
38
- console.log(chalk.dim(` ${"─".repeat(maxMod + maxLabel + 18)}`));
39
- for (const mod of modules) {
58
+ // Header
59
+ const maxMod = Math.max(...sorted.map((m) => m.module.length), 6);
60
+ const maxLabel = Math.max(Math.min(Math.max(...sorted.map((m) => m.label.length)), 30), 5);
61
+ console.log(chalk.dim(` ${"MODULE".padEnd(maxMod + 2)} ${"LABEL".padEnd(maxLabel + 2)} ${"#".padStart(4)} SHARE`));
62
+ console.log(chalk.dim(` ${"".repeat(maxMod + maxLabel + 32)}`));
63
+ for (const mod of sorted) {
40
64
  const modName = mod.superuserOnly
41
65
  ? chalk.yellow(mod.module.padEnd(maxMod + 2))
42
- : mod.module.padEnd(maxMod + 2);
43
- const label = mod.label.padEnd(maxLabel + 2);
44
- const count = String(mod.collections).padStart(4);
45
- console.log(` ${modName} ${label} ${count}`);
66
+ : chalk.white(mod.module.padEnd(maxMod + 2));
67
+ const labelStr = mod.label.length > 30 ? mod.label.slice(0, 28) + ".." : mod.label;
68
+ const label = chalk.dim(labelStr.padEnd(maxLabel + 2));
69
+ const count = mod.collections > 0 ? chalk.white(String(mod.collections).padStart(4)) : chalk.dim(" 0");
70
+ const viz = bar(mod.collections, maxCount, 16);
71
+ console.log(` ${modName} ${label} ${count} ${viz}`);
46
72
  }
47
73
  console.log();
48
- console.log(chalk.dim(` ${modules.length} modules, ${totalCollections} collections accessible`));
49
74
  if (su) {
50
- console.log(chalk.yellow(` Superuser access: all modules including system collections`));
75
+ console.log(chalk.yellow(` Superuser all modules including system collections`));
76
+ console.log();
51
77
  }
78
+ }
79
+ // ── Display: module detail ──────────────────────────────────────────────────
80
+ function displayModuleDetail(modName, modLabel, collections) {
81
+ const maxCol = Math.max(...collections.map((c) => c.collection.length), 10);
82
+ console.log();
83
+ console.log(chalk.bold(` ${chalk.cyan(modLabel)} `) + chalk.dim(`(${modName})`));
84
+ console.log(chalk.dim(` ${collections.length} collections`));
52
85
  console.log();
86
+ // Group by right prefix if there's variety, else flat list
87
+ const rights = [...new Set(collections.map((c) => c.right.split(".")[0]))];
88
+ const grouped = rights.length > 1;
89
+ if (grouped) {
90
+ for (const prefix of rights) {
91
+ const group = collections.filter((c) => c.right.startsWith(prefix + "."));
92
+ console.log(chalk.dim(` ─ ${prefix} (${group.length})`));
93
+ for (const col of group) {
94
+ const rightSuffix = col.right.split(".").slice(1).join(".");
95
+ console.log(` ${chalk.white(col.collection.padEnd(maxCol + 2))} ${chalk.dim(rightSuffix)}`);
96
+ }
97
+ console.log();
98
+ }
99
+ }
100
+ else {
101
+ console.log(chalk.dim(` ${"COLLECTION".padEnd(maxCol + 2)} RIGHT`));
102
+ console.log(chalk.dim(` ${"─".repeat(maxCol + 30)}`));
103
+ for (const col of collections) {
104
+ console.log(` ${chalk.white(col.collection.padEnd(maxCol + 2))} ${chalk.dim(col.right)}`);
105
+ }
106
+ console.log();
107
+ }
53
108
  }
109
+ // ── Interactive drill-down ──────────────────────────────────────────────────
110
+ async function interactiveDrillDown(modules, su) {
111
+ const sorted = [...modules].sort((a, b) => b.collections - a.collections);
112
+ const choices = sorted.map((m) => ({
113
+ name: `${m.module.padEnd(14)} ${chalk.dim(m.label.padEnd(32))} ${chalk.cyan(String(m.collections).padStart(3))} collections`,
114
+ value: m.module,
115
+ short: m.module,
116
+ }));
117
+ choices.push(new inquirer.Separator());
118
+ choices.push({ name: chalk.dim("Exit"), value: "__exit__", short: "exit" });
119
+ while (true) {
120
+ const { selectedModule } = await inquirer.prompt([
121
+ {
122
+ type: "list",
123
+ name: "selectedModule",
124
+ message: "Select a module to explore:",
125
+ choices,
126
+ pageSize: Math.min(modules.length + 2, 20),
127
+ },
128
+ ]);
129
+ if (selectedModule === "__exit__")
130
+ break;
131
+ const mod = aYOUneModules.find((m) => m.module === selectedModule);
132
+ if (!mod)
133
+ break;
134
+ const collections = getModuleCollections(selectedModule);
135
+ displayModuleDetail(selectedModule, mod.label, collections);
136
+ // Offer to drill further into a collection or go back
137
+ if (collections.length > 0) {
138
+ const collChoices = [
139
+ { name: chalk.dim("← Back to modules"), value: "__back__" },
140
+ new inquirer.Separator(),
141
+ ...collections.map((c) => ({
142
+ name: `${c.collection.padEnd(30)} ${chalk.dim(c.right)}`,
143
+ value: c.collection,
144
+ short: c.collection,
145
+ })),
146
+ ];
147
+ const { selectedCollection } = await inquirer.prompt([
148
+ {
149
+ type: "list",
150
+ name: "selectedCollection",
151
+ message: `Drill into a collection (use with ay list ${selectedModule} <collection>):`,
152
+ choices: collChoices,
153
+ pageSize: Math.min(collections.length + 3, 20),
154
+ },
155
+ ]);
156
+ if (selectedCollection !== "__back__") {
157
+ console.log();
158
+ console.log(chalk.bold(` Quick commands for ${chalk.cyan(selectedCollection)}:`));
159
+ console.log();
160
+ console.log(chalk.dim(` List: `) + `ay list ${selectedModule} ${selectedCollection.toLowerCase()}`);
161
+ console.log(chalk.dim(` Get: `) + `ay get ${selectedModule} ${selectedCollection.toLowerCase()}`);
162
+ console.log(chalk.dim(` Search: `) + `ay search ${selectedCollection.toLowerCase()} "<query>"`);
163
+ console.log(chalk.dim(` Describe: `) + `ay describe ${selectedCollection.toLowerCase()} <id>`);
164
+ console.log(chalk.dim(` Export: `) + `ay export run ${selectedModule} ${selectedCollection.toLowerCase()}`);
165
+ console.log();
166
+ }
167
+ }
168
+ }
169
+ }
170
+ // ── Search ──────────────────────────────────────────────────────────────────
171
+ function displaySearch(query, su) {
172
+ const q = query.toLowerCase();
173
+ const matchedCollections = modelsAndRights.filter((m) => {
174
+ if (m.module === "su" && !su)
175
+ return false;
176
+ return m.plural.toLowerCase().includes(q) || m.singular.toLowerCase().includes(q) || m.right.toLowerCase().includes(q);
177
+ });
178
+ if (matchedCollections.length === 0) {
179
+ console.log();
180
+ console.log(chalk.dim(` No collections matching "${query}"`));
181
+ console.log();
182
+ return;
183
+ }
184
+ console.log();
185
+ console.log(chalk.bold(` Search results for "${chalk.cyan(query)}"`));
186
+ console.log(chalk.dim(` ${matchedCollections.length} matches`));
187
+ console.log();
188
+ // Group by module
189
+ const byModule = new Map();
190
+ for (const m of matchedCollections) {
191
+ if (!byModule.has(m.module))
192
+ byModule.set(m.module, []);
193
+ byModule.get(m.module).push(m);
194
+ }
195
+ const maxCol = Math.max(...matchedCollections.map((m) => m.plural.length), 10);
196
+ for (const [modName, cols] of byModule) {
197
+ const mod = aYOUneModules.find((m) => m.module === modName);
198
+ console.log(chalk.dim(` ─ ${modName}`) + chalk.dim(` (${(mod === null || mod === void 0 ? void 0 : mod.label) || modName})`));
199
+ for (const col of cols) {
200
+ // Highlight the match
201
+ const highlighted = col.plural.replace(new RegExp(`(${query})`, "ig"), (m) => chalk.cyan.underline(m));
202
+ console.log(` ${highlighted.padEnd(maxCol + 12)} ${chalk.dim(col.right)} ${chalk.dim("→")} ay list ${modName} ${col.plural.toLowerCase()}`);
203
+ }
204
+ console.log();
205
+ }
206
+ }
207
+ // ── Command ─────────────────────────────────────────────────────────────────
54
208
  export function createAccessCommand(program) {
55
209
  program
56
210
  .command("access")
57
211
  .description("Show accessible modules and collections for the current user")
58
212
  .addHelpText("after", `
59
213
  Examples:
60
- ay access Show accessible modules and collection counts
61
- ay access -r json Output as JSON
62
- ay access --module crm Show collections in a specific module`)
214
+ ay access Interactive module explorer with drill-down
215
+ ay access -r json Output as JSON (non-interactive)
216
+ ay access --module crm Show collections in a specific module
217
+ ay access --search contacts Find collections by name
218
+ ay access --no-interactive Summary table without drill-down prompt`)
63
219
  .option("--module <name>", "Show collections for a specific module")
220
+ .option("--search <query>", "Search collections by name or right")
221
+ .option("--no-interactive", "Disable interactive drill-down")
64
222
  .action(async (options) => {
65
223
  try {
66
224
  const opts = { ...program.opts(), ...options };
67
225
  const su = isSuperUser();
68
226
  const modules = getAccessibleModules(su);
69
- // Detail mode: list collections in a specific module
227
+ const isTTY = process.stdout.isTTY;
228
+ // Machine mode: explicit `-r` flag passed (e.g., -r table, -r yaml, -r csv),
229
+ // or saving to file, or piped (non-TTY) output.
230
+ // We check Commander's option source to distinguish "user typed -r table" from
231
+ // "user config defaults to yaml". Only an explicit CLI flag triggers machine mode.
232
+ const formatSource = program.getOptionValueSource("responseFormat");
233
+ const explicitFormat = formatSource === "cli";
234
+ const machineMode = explicitFormat || !!opts.save || !isTTY;
235
+ // ── Search mode ─────────────────────────────────────────────
236
+ if (opts.search) {
237
+ displaySearch(opts.search, su);
238
+ return;
239
+ }
240
+ // ── Detail mode: specific module ────────────────────────────
70
241
  if (opts.module) {
71
242
  const modName = opts.module.toLowerCase();
72
243
  const mod = aYOUneModules.find((m) => m.module === modName);
@@ -76,37 +247,17 @@ Examples:
76
247
  if (mod.superuserOnly && !su) {
77
248
  cliError(`Module "${opts.module}" requires superuser access.`, EXIT_GENERAL_ERROR);
78
249
  }
79
- const collections = modelsAndRights
80
- .filter((m) => m.module === modName)
81
- .map((m) => ({
82
- collection: m.plural,
83
- singular: m.singular,
84
- right: m.right,
85
- }));
86
- if (opts.responseFormat !== "json" || opts.save) {
250
+ const collections = getModuleCollections(modName);
251
+ if (machineMode) {
87
252
  handleResponseFormatOptions(opts, collections);
88
253
  }
89
254
  else {
90
- console.log();
91
- console.log(chalk.bold(` Collections in ${chalk.cyan(mod.label)} (${modName})`));
92
- console.log(chalk.dim(` ${"─".repeat(50)}`));
93
- console.log();
94
- const maxCol = Math.max(...collections.map((c) => c.collection.length), 10);
95
- const maxRight = Math.max(...collections.map((c) => c.right.length), 5);
96
- console.log(` ${chalk.dim("COLLECTION".padEnd(maxCol + 2))} ${chalk.dim("RIGHT")}`);
97
- console.log(chalk.dim(` ${"─".repeat(maxCol + maxRight + 6)}`));
98
- for (const col of collections) {
99
- console.log(` ${col.collection.padEnd(maxCol + 2)} ${chalk.dim(col.right)}`);
100
- }
101
- console.log();
102
- console.log(chalk.dim(` ${collections.length} collections`));
103
- console.log();
255
+ displayModuleDetail(modName, mod.label, collections);
104
256
  }
105
257
  return;
106
258
  }
107
- // Summary mode: show all accessible modules
108
- if (opts.responseFormat !== "json" || opts.save) {
109
- // For non-default formats or save, use standard formatting
259
+ // ── Summary mode ────────────────────────────────────────────
260
+ if (machineMode) {
110
261
  const data = modules.map((m) => ({
111
262
  module: m.module,
112
263
  label: m.label,
@@ -114,9 +265,12 @@ Examples:
114
265
  ...(m.superuserOnly ? { superuserOnly: true } : {}),
115
266
  }));
116
267
  handleResponseFormatOptions(opts, data);
268
+ return;
117
269
  }
118
- else {
119
- displayAccess(modules, su);
270
+ // Pretty display + optional interactive drill-down
271
+ displaySummary(modules, su);
272
+ if (opts.interactive !== false) {
273
+ await interactiveDrillDown(modules, su);
120
274
  }
121
275
  }
122
276
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolinax/ayoune-cli",
3
- "version": "2026.11.4",
3
+ "version": "2026.12.0",
4
4
  "description": "CLI for the aYOUne Business-as-a-Service platform",
5
5
  "type": "module",
6
6
  "main": "./index.js",