@tolinax/ayoune-cli 2026.7.1 → 2026.7.2

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.
@@ -5,14 +5,6 @@ import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../exitCodes.js";
5
5
  import { cliError } from "../helpers/cliError.js";
6
6
  import { handleResponseFormatOptions } from "../helpers/handleResponseFormatOptions.js";
7
7
  import { apiCallHandler } from "../api/apiCallHandler.js";
8
- import { writeToTarget, getDbStats } from "../db/copyEngine.js";
9
- function formatBytes(bytes) {
10
- if (bytes === 0)
11
- return "0 B";
12
- const units = ["B", "KB", "MB", "GB", "TB"];
13
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
14
- return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
15
- }
16
8
  function maskUri(uri) {
17
9
  return uri.replace(/:\/\/([^:]+):([^@]+)@/, "://***:***@");
18
10
  }
@@ -21,21 +13,24 @@ export function createDbCommand(program) {
21
13
  .command("db")
22
14
  .description("Copy query results to external databases, manage targets")
23
15
  .addHelpText("after", `
16
+ All database operations go through the aggregation service — the CLI never
17
+ connects to any database directly.
18
+
24
19
  Examples:
25
20
  ay db copy Interactive: pick target, pick query
26
21
  ay db copy my-local-db Pick query for target "my-local-db"
27
- ay db copy my-local-db active-users Fully autonomous: run query, write to target
22
+ ay db copy my-local-db active-users Fully autonomous
28
23
  ay db targets list List configured targets
29
24
  ay db targets add Add a new target
30
- ay db stats Show aYOUne database stats`);
25
+ ay db targets test <slug> Test connectivity to a target`);
31
26
  // ─── ay db copy [target] [query] ───────────────────────────────
32
27
  db.command("copy [target] [query]")
33
28
  .description("Run a saved query and write results to a target database")
34
29
  .option("--collection <name>", "Override target collection name")
35
30
  .option("--write-mode <mode>", "Override write mode (insert, upsert, replace)")
36
31
  .addHelpText("after", `
37
- Runs a saved aggregation query and writes the results to a pre-configured target.
38
- Both target and query are resolved by slug. If omitted, you'll be prompted to select.
32
+ Executes a saved aggregation query via the aggregation service and writes
33
+ the results to a pre-configured DataTarget. Both are resolved by slug.
39
34
 
40
35
  Examples:
41
36
  ay db copy Interactive mode
@@ -43,7 +38,6 @@ Examples:
43
38
  ay db copy my-local-db active-users Fully autonomous
44
39
  ay db copy my-local-db active-users --collection results --write-mode upsert`)
45
40
  .action(async (target, query, options) => {
46
- var _a;
47
41
  try {
48
42
  const opts = { ...program.opts(), ...options };
49
43
  // Step 1: Resolve target
@@ -66,57 +60,29 @@ Examples:
66
60
  cliError("Provide query slug or use interactive mode", EXIT_MISUSE);
67
61
  queryData = await promptQuery();
68
62
  }
69
- // Step 3: Execute query via aggregation API
70
- const queryId = queryData._id;
71
- spinner.start({ text: `Running query "${queryData.name}"...`, color: "magenta" });
72
- const aggResult = await apiCallHandler("aggregation", `queries/execute/${queryId}`, "post");
73
- spinner.stop();
74
- const docs = Array.isArray(aggResult) ? aggResult : ((aggResult === null || aggResult === void 0 ? void 0 : aggResult.payload) || []);
75
- if (!docs.length) {
76
- spinner.success({ text: "Query returned 0 results — nothing to write" });
77
- return;
78
- }
79
- console.log(chalk.dim(` Query returned ${docs.length} documents\n`));
80
- // Step 4: Write to target
81
- const collection = opts.collection || targetData.targetCollection || ((_a = queryData.name) === null || _a === void 0 ? void 0 : _a.toLowerCase().replace(/\s+/g, "_")) || "query_results";
82
- const writeMode = opts.writeMode || targetData.writeMode || "insert";
83
- const batchSize = targetData.batchSize || 1000;
84
- spinner.start({ text: `Writing ${docs.length} docs to ${maskUri(targetData.connectionUri)}/${collection}...`, color: "cyan" });
85
- const result = await writeToTarget(targetData.connectionUri, targetData.database, collection, docs, writeMode, batchSize);
63
+ // Step 3: Call the aggregation service export endpoint
64
+ spinner.start({ text: `Running "${queryData.name}" → ${targetData.name}...`, color: "magenta" });
65
+ const body = {
66
+ queryId: queryData._id,
67
+ targetId: targetData._id,
68
+ };
69
+ if (opts.collection)
70
+ body.targetCollection = opts.collection;
71
+ if (opts.writeMode)
72
+ body.writeMode = opts.writeMode;
73
+ const res = await apiCallHandler("aggregation", "export", "post", body);
86
74
  spinner.stop();
87
- // Step 5: Update target status via config-api
88
- try {
89
- await apiCallHandler("config", `datatargets/${targetData._id}`, "put", {
90
- lastUsed: new Date().toISOString(),
91
- lastStatus: result.errors > 0 ? "failed" : "success",
92
- lastResultCount: result.written,
93
- lastError: result.errors > 0 ? `${result.errors} write errors` : undefined,
94
- });
95
- }
96
- catch (_b) {
97
- // Non-critical — don't fail the copy if status update fails
98
- }
99
75
  // Output
100
- const wrapped = {
101
- payload: {
102
- target: targetData.name,
103
- query: queryData.name,
104
- collection,
105
- writeMode,
106
- written: result.written,
107
- errors: result.errors,
108
- duration: result.duration,
109
- },
110
- meta: { written: result.written, errors: result.errors },
111
- };
112
- if (opts.responseFormat === "json" || opts.responseFormat === "yaml") {
113
- handleResponseFormatOptions(opts, wrapped);
76
+ handleResponseFormatOptions(opts, res);
77
+ const p = res.payload || {};
78
+ if (p.written === 0 && p.errors === 0) {
79
+ spinner.success({ text: "Query returned 0 results — nothing written" });
114
80
  }
115
81
  else {
116
- const icon = result.errors === 0 ? chalk.green("●") : chalk.yellow("●");
117
- console.log(`\n ${icon} ${result.written} docs written to ${chalk.cyan(collection)}${result.errors ? chalk.red(` (${result.errors} errors)`) : ""} ${chalk.dim(`(${result.duration}ms)`)}\n`);
82
+ const icon = (p.errors || 0) === 0 ? chalk.green("●") : chalk.yellow("●");
83
+ console.log(`\n ${icon} ${p.written} docs written to ${chalk.cyan(p.collection || "?")}${p.errors ? chalk.red(` (${p.errors} errors)`) : ""} ${chalk.dim(`(query: ${p.queryTime}ms, write: ${p.writeTime}ms)`)}\n`);
118
84
  }
119
- if (result.errors > 0)
85
+ if (p.errors > 0)
120
86
  process.exit(EXIT_GENERAL_ERROR);
121
87
  }
122
88
  catch (e) {
@@ -154,7 +120,8 @@ Examples:
154
120
  .command("add")
155
121
  .description("Add a new database target")
156
122
  .option("--name <name>", "Target name")
157
- .option("--uri <uri>", "MongoDB connection URI")
123
+ .option("--type <type>", "Database type (mongodb, postgresql, rest)", "mongodb")
124
+ .option("--uri <uri>", "Connection URI")
158
125
  .option("--database <db>", "Database name")
159
126
  .option("--collection <col>", "Default target collection name")
160
127
  .option("--write-mode <mode>", "Write mode: insert, upsert, replace", "insert")
@@ -162,18 +129,19 @@ Examples:
162
129
  var _a;
163
130
  try {
164
131
  const opts = { ...program.opts(), ...options };
165
- let { name, uri, database, collection, writeMode } = opts;
166
- // Interactive prompts for missing fields
132
+ let { name, type, uri, database, collection, writeMode } = opts;
167
133
  if (!name || !uri) {
168
134
  if (!process.stdin.isTTY)
169
135
  cliError("Provide --name and --uri in non-interactive mode", EXIT_MISUSE);
170
136
  const answers = await inquirer.prompt([
171
137
  ...(!name ? [{ type: "input", name: "name", message: "Target name:", validate: (v) => v.length > 0 || "Required" }] : []),
172
- ...(!uri ? [{ type: "input", name: "uri", message: "MongoDB connection URI:", validate: (v) => v.startsWith("mongodb") || "Must be a MongoDB URI" }] : []),
138
+ ...(!opts.type || opts.type === "mongodb" ? [{ type: "list", name: "dbType", message: "Database type:", choices: ["mongodb", "postgresql", "rest"], default: "mongodb" }] : []),
139
+ ...(!uri ? [{ type: "input", name: "uri", message: "Connection URI:", validate: (v) => v.length > 0 || "Required" }] : []),
173
140
  ...(!database ? [{ type: "input", name: "database", message: "Database name (leave empty for URI default):" }] : []),
174
- ...(!collection ? [{ type: "input", name: "collection", message: "Default collection name (leave empty to derive from query):" }] : []),
141
+ ...(!collection ? [{ type: "input", name: "collection", message: "Default collection/table name (leave empty to derive from query):" }] : []),
175
142
  ]);
176
143
  name = name || answers.name;
144
+ type = answers.dbType || type;
177
145
  uri = uri || answers.uri;
178
146
  database = database || answers.database || undefined;
179
147
  collection = collection || answers.collection || undefined;
@@ -181,7 +149,7 @@ Examples:
181
149
  spinner.start({ text: "Creating target...", color: "cyan" });
182
150
  const body = {
183
151
  name,
184
- type: "mongodb",
152
+ type,
185
153
  connectionUri: uri,
186
154
  database: database || undefined,
187
155
  targetCollection: collection || undefined,
@@ -214,47 +182,28 @@ Examples:
214
182
  cliError(e.message || "Failed to remove target", EXIT_GENERAL_ERROR);
215
183
  }
216
184
  });
217
- // ─── ay db stats [uri] ─────────────────────────────────────────
218
- db.command("stats [uri]")
219
- .description("Show database statistics (defaults to aYOUne database)")
220
- .addHelpText("after", `
221
- Defaults to MONGO_CONNECTSTRING when no URI is given.
222
-
223
- Examples:
224
- ay db stats
225
- ay db stats "mongodb://other/db" -r table`)
226
- .action(async (uri, options) => {
185
+ // ay db targets test <slug>
186
+ targets
187
+ .command("test <slug>")
188
+ .description("Test connectivity to a target")
189
+ .action(async (slug, options) => {
227
190
  try {
228
191
  const opts = { ...program.opts(), ...options };
229
- const dbUri = uri || process.env.MONGO_CONNECTSTRING || process.env.MONGODB_URI;
230
- if (!dbUri) {
231
- cliError("No database URI. Set MONGO_CONNECTSTRING env var or pass a URI.", EXIT_MISUSE);
232
- }
233
- if (!dbUri.startsWith("mongodb")) {
234
- cliError("URI must start with mongodb:// or mongodb+srv://", EXIT_MISUSE);
235
- }
236
- spinner.start({ text: `Connecting to ${maskUri(dbUri)}...`, color: "cyan" });
237
- const stats = await getDbStats(dbUri);
192
+ const target = await resolveTarget(slug);
193
+ spinner.start({ text: `Testing connection to "${slug}"...`, color: "cyan" });
194
+ const res = await apiCallHandler("aggregation", "export/test", "post", { targetId: target._id });
238
195
  spinner.stop();
239
- const wrapped = {
240
- payload: stats,
241
- meta: { database: stats.database, collectionCount: stats.collections.length, totalSize: formatBytes(stats.totalSize) },
242
- };
243
- if (opts.responseFormat === "json" || opts.responseFormat === "yaml") {
244
- handleResponseFormatOptions(opts, wrapped);
196
+ const p = res.payload || {};
197
+ if (p.connected) {
198
+ console.log(chalk.green(`\n ● Connected to ${p.database} (${p.collections} collections)\n`));
245
199
  }
246
200
  else {
247
- console.log(chalk.cyan.bold(`\n Database: ${stats.database}\n`));
248
- console.log(` ${chalk.dim("Total size:")} ${formatBytes(stats.totalSize)}`);
249
- console.log(` ${chalk.dim("Collections:")} ${stats.collections.length}\n`);
250
- for (const col of stats.collections) {
251
- console.log(` ${chalk.white(col.name)}: ${col.documents.toLocaleString()} docs ${chalk.dim(`(${formatBytes(col.size)})`)}`);
252
- }
253
- console.log();
201
+ console.log(chalk.red(`\n ● Connection failed: ${p.error}\n`));
202
+ process.exit(EXIT_GENERAL_ERROR);
254
203
  }
255
204
  }
256
205
  catch (e) {
257
- cliError(e.message || "Failed to get stats", EXIT_GENERAL_ERROR);
206
+ cliError(e.message || "Connection test failed", EXIT_GENERAL_ERROR);
258
207
  }
259
208
  });
260
209
  }
@@ -267,7 +216,6 @@ async function resolveTarget(slugOrId) {
267
216
  cliError(`Target not found: ${slugOrId}`, EXIT_GENERAL_ERROR);
268
217
  return res.payload;
269
218
  }
270
- // Resolve by slug
271
219
  const res = await apiCallHandler("config", "datatargets", "get", null, { slug: slugOrId, limit: 1 });
272
220
  const entries = res === null || res === void 0 ? void 0 : res.payload;
273
221
  if (!entries || (Array.isArray(entries) && entries.length === 0)) {
@@ -299,7 +247,7 @@ async function promptTarget() {
299
247
  cliError("No targets configured. Add one with: ay db targets add", EXIT_GENERAL_ERROR);
300
248
  }
301
249
  const choices = targets.map((t) => ({
302
- name: `${t.name} ${chalk.dim(`(${maskUri(t.connectionUri)})`)}`,
250
+ name: `${t.name} ${chalk.dim(`(${t.type}: ${maskUri(t.connectionUri || "")})`)}`,
303
251
  value: t,
304
252
  }));
305
253
  const { selected } = await inquirer.prompt([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolinax/ayoune-cli",
3
- "version": "2026.7.1",
3
+ "version": "2026.7.2",
4
4
  "description": "CLI for the aYOUne Business-as-a-Service platform",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -134,7 +134,6 @@
134
134
  "lodash": "^4.17.21",
135
135
  "mkdirp": "^3.0.1",
136
136
  "moment": "^2.30.1",
137
- "mongodb": "^6.21.0",
138
137
  "nanospinner": "^1.1.0",
139
138
  "node-localstorage": "^3.0.5",
140
139
  "os": "^0.1.2",