ardent-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +879 -0
- package/package.json +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/branch.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/lib/config.ts
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var CONFIG_DIR = join(homedir(), ".ardent");
|
|
14
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
15
|
+
function ensureConfigDir() {
|
|
16
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
17
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
try {
|
|
22
|
+
if (existsSync(CONFIG_FILE)) {
|
|
23
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
function saveConfig(config) {
|
|
30
|
+
ensureConfigDir();
|
|
31
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
32
|
+
}
|
|
33
|
+
function getConfig(key) {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
return config[key];
|
|
36
|
+
}
|
|
37
|
+
function setConfig(key, value) {
|
|
38
|
+
const config = loadConfig();
|
|
39
|
+
config[key] = value;
|
|
40
|
+
saveConfig(config);
|
|
41
|
+
}
|
|
42
|
+
function clearConfig() {
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(CONFIG_FILE)) {
|
|
45
|
+
unlinkSync(CONFIG_FILE);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getApiUrl() {
|
|
51
|
+
return getConfig("apiUrl") || process.env.ARDENT_API_URL || "https://ardentbackendwebappfinal.azurewebsites.net";
|
|
52
|
+
}
|
|
53
|
+
function getToken() {
|
|
54
|
+
return getConfig("token") || process.env.ARDENT_TOKEN;
|
|
55
|
+
}
|
|
56
|
+
function getBranches() {
|
|
57
|
+
const cached = getCacheEntry("branches");
|
|
58
|
+
return cached?.data || [];
|
|
59
|
+
}
|
|
60
|
+
function getBranchByName(name) {
|
|
61
|
+
return getBranches().find((branch) => branch.name === name);
|
|
62
|
+
}
|
|
63
|
+
function setCacheEntry(key, data) {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
if (!config.cache) config.cache = {};
|
|
66
|
+
config.cache[key] = {
|
|
67
|
+
data,
|
|
68
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
69
|
+
};
|
|
70
|
+
saveConfig(config);
|
|
71
|
+
}
|
|
72
|
+
function getCacheEntry(key) {
|
|
73
|
+
const config = loadConfig();
|
|
74
|
+
return config.cache?.[key];
|
|
75
|
+
}
|
|
76
|
+
function formatCacheTime(isoString) {
|
|
77
|
+
const date = new Date(isoString);
|
|
78
|
+
const now = /* @__PURE__ */ new Date();
|
|
79
|
+
const diffMs = now.getTime() - date.getTime();
|
|
80
|
+
const diffMins = Math.floor(diffMs / 6e4);
|
|
81
|
+
if (diffMins < 1) return "just now";
|
|
82
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
83
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
84
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
85
|
+
return date.toLocaleDateString();
|
|
86
|
+
}
|
|
87
|
+
function getCurrentBranch() {
|
|
88
|
+
return getConfig("currentBranch");
|
|
89
|
+
}
|
|
90
|
+
function setCurrentBranch(name) {
|
|
91
|
+
setConfig("currentBranch", name);
|
|
92
|
+
}
|
|
93
|
+
function clearCurrentBranch() {
|
|
94
|
+
const config = loadConfig();
|
|
95
|
+
delete config.currentBranch;
|
|
96
|
+
saveConfig(config);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/lib/api.ts
|
|
100
|
+
var ApiClient = class {
|
|
101
|
+
getHeaders() {
|
|
102
|
+
const token = getToken();
|
|
103
|
+
if (!token) {
|
|
104
|
+
console.error("\u2717 Not authenticated. Run: ardent login");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
"Authorization": `Bearer ${token}`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async get(path) {
|
|
113
|
+
const url = `${getApiUrl()}${path}`;
|
|
114
|
+
const response = await fetch(url, {
|
|
115
|
+
method: "GET",
|
|
116
|
+
headers: this.getHeaders()
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const text = await response.text();
|
|
120
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
121
|
+
}
|
|
122
|
+
return response.json();
|
|
123
|
+
}
|
|
124
|
+
async post(path, body) {
|
|
125
|
+
const url = `${getApiUrl()}${path}`;
|
|
126
|
+
const response = await fetch(url, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: this.getHeaders(),
|
|
129
|
+
body: JSON.stringify(body)
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const text = await response.text();
|
|
133
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
134
|
+
}
|
|
135
|
+
return response.json();
|
|
136
|
+
}
|
|
137
|
+
async delete(path, body) {
|
|
138
|
+
const url = `${getApiUrl()}${path}`;
|
|
139
|
+
const response = await fetch(url, {
|
|
140
|
+
method: "DELETE",
|
|
141
|
+
headers: this.getHeaders(),
|
|
142
|
+
body: body ? JSON.stringify(body) : void 0
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
147
|
+
}
|
|
148
|
+
return response.json();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var api = new ApiClient();
|
|
152
|
+
function isNetworkError(err) {
|
|
153
|
+
if (err instanceof TypeError && err.message.includes("fetch")) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (err instanceof Error) {
|
|
157
|
+
const msg = err.message.toLowerCase();
|
|
158
|
+
return msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("etimedout") || msg.includes("network") || msg.includes("offline") || msg.includes("dns");
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/commands/branch.ts
|
|
164
|
+
var branchCommand = new Command("branch").description("Manage database branches");
|
|
165
|
+
branchCommand.command("create <name>").description("Create a new database branch (e.g., ardent branch create my-feature)").option("-s, --service <type>", "Service type", "postgres").action(async (name, options) => {
|
|
166
|
+
try {
|
|
167
|
+
const job = await api.post("/v1/cli/jobs/create", {
|
|
168
|
+
name
|
|
169
|
+
});
|
|
170
|
+
await api.post("/v1/branch/create", {
|
|
171
|
+
job_id: job.job_id,
|
|
172
|
+
service_type: options.service
|
|
173
|
+
});
|
|
174
|
+
const apiBranches = await api.get(`/v1/branches/${job.job_id}`);
|
|
175
|
+
const apiBranch = apiBranches.find((branch2) => branch2.service_type === options.service);
|
|
176
|
+
if (!apiBranch) {
|
|
177
|
+
console.error("\u2717 Branch created but could not fetch details");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const branch = {
|
|
181
|
+
id: apiBranch.id,
|
|
182
|
+
job_id: job.job_id,
|
|
183
|
+
name,
|
|
184
|
+
service_type: apiBranch.service_type,
|
|
185
|
+
branch_url: apiBranch.branch_url || "",
|
|
186
|
+
status: apiBranch.status,
|
|
187
|
+
created_at: apiBranch.created_at
|
|
188
|
+
};
|
|
189
|
+
const cached = getCacheEntry("branches");
|
|
190
|
+
const cachedBranches = cached?.data || [];
|
|
191
|
+
cachedBranches.push(branch);
|
|
192
|
+
setCacheEntry("branches", cachedBranches);
|
|
193
|
+
setCurrentBranch(name);
|
|
194
|
+
console.log(`\u2713 Branch '${name}' created and checked out`);
|
|
195
|
+
if (branch.branch_url) {
|
|
196
|
+
console.log(`
|
|
197
|
+
${branch.branch_url}`);
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (isNetworkError(err)) {
|
|
201
|
+
console.error("\u2717 Cannot create branch while offline");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
branchCommand.command("list").description("List your branches").action(async () => {
|
|
209
|
+
let branches = [];
|
|
210
|
+
let fromCache = false;
|
|
211
|
+
let cacheTime = "";
|
|
212
|
+
try {
|
|
213
|
+
const result = await api.get("/v1/cli/branches");
|
|
214
|
+
if (!result.branches) {
|
|
215
|
+
throw new Error("API returned invalid response: missing branches array");
|
|
216
|
+
}
|
|
217
|
+
branches = result.branches;
|
|
218
|
+
setCacheEntry("branches", branches);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (isNetworkError(err)) {
|
|
221
|
+
const cached = getCacheEntry("branches");
|
|
222
|
+
if (cached) {
|
|
223
|
+
branches = cached.data;
|
|
224
|
+
fromCache = true;
|
|
225
|
+
cacheTime = formatCacheTime(cached.updated_at);
|
|
226
|
+
} else {
|
|
227
|
+
console.error("\u2717 Offline and no cached data available");
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (fromCache) {
|
|
236
|
+
console.log(`\u26A0 Offline - showing cached data from ${cacheTime}
|
|
237
|
+
`);
|
|
238
|
+
}
|
|
239
|
+
if (branches.length === 0) {
|
|
240
|
+
console.log("No branches found");
|
|
241
|
+
console.log(" Create one with: ardent branch create <name>");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const current = getCurrentBranch();
|
|
245
|
+
const green2 = "\x1B[32m";
|
|
246
|
+
const dim2 = "\x1B[2m";
|
|
247
|
+
const reset2 = "\x1B[0m";
|
|
248
|
+
console.log("Branches:\n");
|
|
249
|
+
for (const branch of branches) {
|
|
250
|
+
const isCurrent = branch.name === current;
|
|
251
|
+
const icon = branch.status === "active" || branch.status === "active_degraded" ? "\u25CF" : "\u25CB";
|
|
252
|
+
if (isCurrent) {
|
|
253
|
+
console.log(`${green2}* ${icon} ${branch.name}${reset2}`);
|
|
254
|
+
if (branch.branch_url) {
|
|
255
|
+
console.log(`${green2} ${branch.branch_url}${reset2}`);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
console.log(` ${icon} ${branch.name}`);
|
|
259
|
+
if (branch.branch_url) {
|
|
260
|
+
console.log(`${dim2} ${branch.branch_url}${reset2}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
console.log();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
branchCommand.command("info [name]").description("Show branch details (defaults to current branch)").action((name) => {
|
|
267
|
+
const branchName = name || getCurrentBranch();
|
|
268
|
+
if (!branchName) {
|
|
269
|
+
console.error("\u2717 No branch specified and no current branch set");
|
|
270
|
+
console.log(" Run: ardent switch <name> or specify a branch name");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const cached = getCacheEntry("branches");
|
|
274
|
+
const branch = cached?.data.find((cachedBranch) => cachedBranch.name === branchName);
|
|
275
|
+
if (!branch) {
|
|
276
|
+
console.error(`\u2717 Branch "${branchName}" not found`);
|
|
277
|
+
console.log(" Run: ardent branch list");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const current = getCurrentBranch();
|
|
281
|
+
const marker = branch.name === current ? " (current)" : "";
|
|
282
|
+
console.log(`Branch: ${branch.name}${marker}
|
|
283
|
+
`);
|
|
284
|
+
console.log(` Type: ${branch.service_type}`);
|
|
285
|
+
console.log(` Status: ${branch.status}`);
|
|
286
|
+
console.log(` Created: ${new Date(branch.created_at).toLocaleString()}`);
|
|
287
|
+
if (branch.branch_url) {
|
|
288
|
+
console.log(`
|
|
289
|
+
URL: ${branch.branch_url}`);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
branchCommand.command("delete <name>").description("Delete a branch (e.g., ardent branch delete my-feature)").action(async (name) => {
|
|
293
|
+
const cached = getCacheEntry("branches");
|
|
294
|
+
const branch = cached?.data.find((cachedBranch) => cachedBranch.name === name);
|
|
295
|
+
if (!branch) {
|
|
296
|
+
console.error(`\u2717 Branch "${name}" not found`);
|
|
297
|
+
console.log(" Run: ardent branch list");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await api.delete(`/v1/cli/jobs/${branch.job_id}`);
|
|
302
|
+
const updatedBranches = cached.data.filter((cachedBranch) => cachedBranch.id !== branch.id);
|
|
303
|
+
setCacheEntry("branches", updatedBranches);
|
|
304
|
+
if (getCurrentBranch() === name) {
|
|
305
|
+
clearCurrentBranch();
|
|
306
|
+
}
|
|
307
|
+
console.log("\u2713 Branch deleted");
|
|
308
|
+
} catch (err) {
|
|
309
|
+
if (isNetworkError(err)) {
|
|
310
|
+
console.error("\u2717 Cannot delete branch while offline");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// src/commands/connector.ts
|
|
319
|
+
import { Command as Command2 } from "commander";
|
|
320
|
+
function parsePostgresUrl(url) {
|
|
321
|
+
const atIndex = url.lastIndexOf("@");
|
|
322
|
+
const credentialsPart = atIndex > 0 ? url.substring(0, atIndex) : url;
|
|
323
|
+
const hostPart = atIndex > 0 ? url.substring(atIndex) : "";
|
|
324
|
+
const cleanedUrl = credentialsPart.replace(/\\!/g, "!") + hostPart;
|
|
325
|
+
const parsed = new URL(cleanedUrl);
|
|
326
|
+
const host = parsed.hostname;
|
|
327
|
+
const port = parsed.port || "5432";
|
|
328
|
+
const username = decodeURIComponent(parsed.username);
|
|
329
|
+
const password = parsed.password ? decodeURIComponent(parsed.password) : "";
|
|
330
|
+
if (!host) throw new Error("Host required in connection URL");
|
|
331
|
+
if (!username) throw new Error("Username required in connection URL");
|
|
332
|
+
return { host, port, username, password };
|
|
333
|
+
}
|
|
334
|
+
var connectorCommand = new Command2("connector").description("Manage database connectors");
|
|
335
|
+
connectorCommand.command("create <type> <url>").description("Create a new connector (e.g., ardent connector create postgresql postgresql://user:pass@host/db)").option("-n, --name <name>", "Connector name").action(async (type, url, options) => {
|
|
336
|
+
const supportedTypes = ["postgresql"];
|
|
337
|
+
if (!supportedTypes.includes(type.toLowerCase())) {
|
|
338
|
+
console.error(`\u2717 Unsupported type: ${type}`);
|
|
339
|
+
console.error(` Supported: ${supportedTypes.join(", ")}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const parsed = parsePostgresUrl(url);
|
|
344
|
+
if (!parsed.password) {
|
|
345
|
+
console.error("\u2717 Password required in connection URL");
|
|
346
|
+
console.error(" Example: postgresql://user:password@host:5432/db");
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const connectionDetails = {
|
|
350
|
+
host: parsed.host,
|
|
351
|
+
port: parsed.port,
|
|
352
|
+
username: parsed.username,
|
|
353
|
+
password: parsed.password
|
|
354
|
+
};
|
|
355
|
+
const connectorName = options.name || "My PostgreSQL Connection";
|
|
356
|
+
console.log("Creating connector...");
|
|
357
|
+
const created = await api.post("/v1/connectors", {
|
|
358
|
+
name: connectorName,
|
|
359
|
+
service_name: "postgresql",
|
|
360
|
+
connection_details: connectionDetails
|
|
361
|
+
});
|
|
362
|
+
const connectorId = created.id;
|
|
363
|
+
console.log("Discovering schema...");
|
|
364
|
+
await api.post(`/v1/connectors/${connectorId}/discover`, {});
|
|
365
|
+
console.log("Setting selection...");
|
|
366
|
+
await api.post(`/v1/connectors/${connectorId}/selection`, {
|
|
367
|
+
selected_paths: ["*"]
|
|
368
|
+
});
|
|
369
|
+
const connector = await api.get(`/v1/connectors/${connectorId}`);
|
|
370
|
+
if (connector.branching_engine_status === "configuration_verified") {
|
|
371
|
+
console.log("Setting up branching engine...");
|
|
372
|
+
await api.post(`/v1/connectors/${connectorId}/engine-setup`, {});
|
|
373
|
+
}
|
|
374
|
+
console.log("\u2713 Connector created and ready");
|
|
375
|
+
console.log(` ID: ${connectorId}`);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
connectorCommand.command("list").description("List your connectors").action(async () => {
|
|
382
|
+
try {
|
|
383
|
+
const result = await api.get("/v1/cli/connectors");
|
|
384
|
+
if (!result.connectors) {
|
|
385
|
+
throw new Error("API returned invalid response: missing connectors array");
|
|
386
|
+
}
|
|
387
|
+
const connectors = result.connectors;
|
|
388
|
+
if (connectors.length === 0) {
|
|
389
|
+
console.log("No connectors found");
|
|
390
|
+
console.log(" Create one with: ardent connector create postgresql <url>");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
console.log("Connectors:\n");
|
|
394
|
+
for (const connector of connectors) {
|
|
395
|
+
const icon = connector.status === "healthy" ? "\u25CF" : "\u25CB";
|
|
396
|
+
console.log(` ${icon} ${connector.name} (${connector.service_name})`);
|
|
397
|
+
console.log(` ID: ${connector.id}`);
|
|
398
|
+
console.log();
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
connectorCommand.command("delete <name>").description("Delete a connector by name (e.g., ardent connector delete 'My PostgreSQL Connection')").action(async (name) => {
|
|
406
|
+
try {
|
|
407
|
+
const result = await api.get("/v1/cli/connectors");
|
|
408
|
+
if (!result.connectors) {
|
|
409
|
+
throw new Error("API returned invalid response: missing connectors array");
|
|
410
|
+
}
|
|
411
|
+
const connector = result.connectors.find((existingConnector) => existingConnector.name === name);
|
|
412
|
+
if (!connector) {
|
|
413
|
+
console.error(`\u2717 Connector "${name}" not found`);
|
|
414
|
+
console.log(" Run: ardent connector list");
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
console.log("Deleting connector...");
|
|
418
|
+
await api.delete(`/v1/connectors/${connector.id}`);
|
|
419
|
+
console.log("\u2713 Connector deleted");
|
|
420
|
+
} catch (err) {
|
|
421
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// src/commands/diff.ts
|
|
427
|
+
import { Command as Command3 } from "commander";
|
|
428
|
+
var green = "\x1B[32m";
|
|
429
|
+
var red = "\x1B[31m";
|
|
430
|
+
var yellow = "\x1B[33m";
|
|
431
|
+
var cyan = "\x1B[36m";
|
|
432
|
+
var dim = "\x1B[2m";
|
|
433
|
+
var bold = "\x1B[1m";
|
|
434
|
+
var reset = "\x1B[0m";
|
|
435
|
+
var MAX_COL_WIDTH = 24;
|
|
436
|
+
var MAX_COLUMNS = 6;
|
|
437
|
+
function truncate(str, maxLen) {
|
|
438
|
+
if (str.length <= maxLen) return str;
|
|
439
|
+
return str.slice(0, maxLen - 2) + "..";
|
|
440
|
+
}
|
|
441
|
+
var diffCommand = new Command3("diff").description("Show CDC changes on current branch").option("-b, --branch <name>", "Branch name (defaults to current)").option("--sql", "Output as replay SQL").action(async (options) => {
|
|
442
|
+
let branch;
|
|
443
|
+
if (options.branch) {
|
|
444
|
+
branch = getBranchByName(options.branch);
|
|
445
|
+
} else {
|
|
446
|
+
const currentName = getCurrentBranch();
|
|
447
|
+
if (currentName) {
|
|
448
|
+
branch = getBranchByName(currentName);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (!branch) {
|
|
452
|
+
const current = getCurrentBranch();
|
|
453
|
+
if (current) {
|
|
454
|
+
console.error(`\u2717 Branch "${current}" not found in cache`);
|
|
455
|
+
console.log(" Run: ardent branch list");
|
|
456
|
+
} else {
|
|
457
|
+
console.error("\u2717 No current branch set");
|
|
458
|
+
console.log(" Run: ardent switch <name> or ardent branch create <name>");
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const state = await api.get(`/v1/branches/${branch.job_id}/state`);
|
|
464
|
+
if (!state || Object.keys(state).length === 0) {
|
|
465
|
+
console.log("No changes captured");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
for (const [_branchId, branchState] of Object.entries(state)) {
|
|
469
|
+
const { diff, ddl, replay_sql } = branchState;
|
|
470
|
+
if (options.sql) {
|
|
471
|
+
if (replay_sql) {
|
|
472
|
+
console.log(replay_sql);
|
|
473
|
+
} else {
|
|
474
|
+
console.log("-- No replay SQL generated");
|
|
475
|
+
}
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (ddl && ddl.length > 0) {
|
|
479
|
+
console.log(`${bold}Schema Changes${reset} ${dim}(${ddl.length} DDL)${reset}
|
|
480
|
+
`);
|
|
481
|
+
for (const change of ddl) {
|
|
482
|
+
const color = change.command_tag.includes("DROP") ? red : change.command_tag.includes("CREATE") ? green : yellow;
|
|
483
|
+
console.log(` ${color}${change.command_tag}${reset} ${change.object_name}`);
|
|
484
|
+
if (change.ddl_command && change.ddl_command.length < 100) {
|
|
485
|
+
console.log(` ${dim}${change.ddl_command}${reset}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
console.log();
|
|
489
|
+
}
|
|
490
|
+
if (diff && diff.length > 0) {
|
|
491
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
492
|
+
for (const row of diff) {
|
|
493
|
+
const tableName = row.table_name || `${row.schema_name}.unknown`;
|
|
494
|
+
if (!byTable.has(tableName)) {
|
|
495
|
+
byTable.set(tableName, []);
|
|
496
|
+
}
|
|
497
|
+
byTable.get(tableName).push(row);
|
|
498
|
+
}
|
|
499
|
+
const inserts = diff.filter((r) => r.last_op === "INSERT").length;
|
|
500
|
+
const updates = diff.filter((r) => r.last_op === "UPDATE").length;
|
|
501
|
+
const deletes = diff.filter((r) => r.last_op === "DELETE").length;
|
|
502
|
+
const summary = [
|
|
503
|
+
inserts > 0 ? `${green}+${inserts} added${reset}` : "",
|
|
504
|
+
updates > 0 ? `${yellow}~${updates} modified${reset}` : "",
|
|
505
|
+
deletes > 0 ? `${red}-${deletes} deleted${reset}` : ""
|
|
506
|
+
].filter(Boolean).join(" ");
|
|
507
|
+
console.log(`${bold}Data Changes${reset} ${dim}(${byTable.size} table${byTable.size > 1 ? "s" : ""})${reset} ${summary}
|
|
508
|
+
`);
|
|
509
|
+
for (const [tableName, rows] of byTable) {
|
|
510
|
+
const tableInserts = rows.filter((r) => r.last_op === "INSERT").length;
|
|
511
|
+
const tableUpdates = rows.filter((r) => r.last_op === "UPDATE").length;
|
|
512
|
+
const tableDeletes = rows.filter((r) => r.last_op === "DELETE").length;
|
|
513
|
+
const tableSummary = [
|
|
514
|
+
tableInserts > 0 ? `${green}+${tableInserts}${reset}` : "",
|
|
515
|
+
tableUpdates > 0 ? `${yellow}~${tableUpdates}${reset}` : "",
|
|
516
|
+
tableDeletes > 0 ? `${red}-${tableDeletes}${reset}` : ""
|
|
517
|
+
].filter(Boolean).join(" ");
|
|
518
|
+
console.log(`${cyan}${tableName}${reset} ${tableSummary}`);
|
|
519
|
+
console.log();
|
|
520
|
+
const allColumns = /* @__PURE__ */ new Set();
|
|
521
|
+
for (const row of rows) {
|
|
522
|
+
const data = row.last_data || row.first_data || {};
|
|
523
|
+
Object.keys(data).forEach((k) => allColumns.add(k));
|
|
524
|
+
if (row.first_data) {
|
|
525
|
+
Object.keys(row.first_data).forEach((k) => allColumns.add(k));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
let columns = Array.from(allColumns);
|
|
529
|
+
let hiddenColumns = 0;
|
|
530
|
+
if (columns.length > MAX_COLUMNS) {
|
|
531
|
+
const changedCols = /* @__PURE__ */ new Set();
|
|
532
|
+
for (const row of rows) {
|
|
533
|
+
if (row.last_op === "UPDATE" && row.first_data && row.last_data) {
|
|
534
|
+
for (const col of columns) {
|
|
535
|
+
if (row.first_data[col] !== row.last_data[col]) {
|
|
536
|
+
changedCols.add(col);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const prioritized = [
|
|
542
|
+
...columns.filter((c) => changedCols.has(c)),
|
|
543
|
+
...columns.filter((c) => !changedCols.has(c))
|
|
544
|
+
];
|
|
545
|
+
hiddenColumns = prioritized.length - MAX_COLUMNS;
|
|
546
|
+
columns = prioritized.slice(0, MAX_COLUMNS);
|
|
547
|
+
}
|
|
548
|
+
const colWidths = { Op: 2 };
|
|
549
|
+
for (const col of columns) {
|
|
550
|
+
colWidths[col] = Math.min(col.length, MAX_COL_WIDTH);
|
|
551
|
+
}
|
|
552
|
+
for (const row of rows) {
|
|
553
|
+
const data = row.last_data || row.first_data || {};
|
|
554
|
+
for (const col of columns) {
|
|
555
|
+
const val = data[col];
|
|
556
|
+
const valStr = val === void 0 ? "" : typeof val === "string" ? val : String(val);
|
|
557
|
+
colWidths[col] = Math.min(MAX_COL_WIDTH, Math.max(colWidths[col], valStr.length));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const vLine = `${dim}\u2502${reset}`;
|
|
561
|
+
const topParts = [
|
|
562
|
+
"\u2500".repeat(colWidths["Op"]),
|
|
563
|
+
...columns.map((col) => "\u2500".repeat(colWidths[col]))
|
|
564
|
+
];
|
|
565
|
+
const hiddenNote = hiddenColumns > 0 ? ` ${dim}+${hiddenColumns} more${reset}` : "";
|
|
566
|
+
console.log(` ${dim}\u250C\u2500${topParts.join("\u2500\u252C\u2500")}\u2500\u2510${reset}${hiddenNote}`);
|
|
567
|
+
const headerParts = [
|
|
568
|
+
`${"Op".padEnd(colWidths["Op"])}`,
|
|
569
|
+
...columns.map((col) => truncate(col, colWidths[col]).padEnd(colWidths[col]))
|
|
570
|
+
];
|
|
571
|
+
console.log(` ${dim}\u2502 ${headerParts.join(" \u2502 ")} \u2502${reset}`);
|
|
572
|
+
const sepParts = [
|
|
573
|
+
"\u2500".repeat(colWidths["Op"]),
|
|
574
|
+
...columns.map((col) => "\u2500".repeat(colWidths[col]))
|
|
575
|
+
];
|
|
576
|
+
console.log(` ${dim}\u251C\u2500${sepParts.join("\u2500\u253C\u2500")}\u2500\u2524${reset}`);
|
|
577
|
+
for (const row of rows) {
|
|
578
|
+
const op = row.last_op;
|
|
579
|
+
const opColor = op === "INSERT" ? green : op === "DELETE" ? red : yellow;
|
|
580
|
+
const opSymbol = op === "INSERT" ? "+" : op === "DELETE" ? "-" : "~";
|
|
581
|
+
const cellParts = [];
|
|
582
|
+
cellParts.push(`${opColor}${opSymbol.padEnd(colWidths["Op"])}${reset}`);
|
|
583
|
+
for (const col of columns) {
|
|
584
|
+
const width = colWidths[col];
|
|
585
|
+
if (op === "UPDATE" && row.first_data && row.last_data) {
|
|
586
|
+
const oldVal = row.first_data[col];
|
|
587
|
+
const newVal = row.last_data[col];
|
|
588
|
+
if (oldVal !== newVal) {
|
|
589
|
+
const halfWidth = Math.floor((width - 1) / 2);
|
|
590
|
+
const oldStr = truncate(oldVal === void 0 ? "" : typeof oldVal === "string" ? oldVal : String(oldVal), halfWidth);
|
|
591
|
+
const newStr = truncate(newVal === void 0 ? "" : typeof newVal === "string" ? newVal : String(newVal), halfWidth);
|
|
592
|
+
cellParts.push(`${red}${oldStr.padEnd(halfWidth)}${reset}\u2192${green}${newStr.padEnd(width - halfWidth - 1)}${reset}`);
|
|
593
|
+
} else {
|
|
594
|
+
const valStr = newVal === void 0 ? "" : typeof newVal === "string" ? newVal : String(newVal);
|
|
595
|
+
cellParts.push(`${dim}${truncate(valStr, width).padEnd(width)}${reset}`);
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
const data = row.last_data || row.first_data || {};
|
|
599
|
+
const val = data[col];
|
|
600
|
+
const valStr = val === void 0 ? "" : typeof val === "string" ? val : String(val);
|
|
601
|
+
cellParts.push(`${opColor}${truncate(valStr, width).padEnd(width)}${reset}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
console.log(` ${vLine} ${cellParts.join(` ${vLine} `)} ${vLine}`);
|
|
605
|
+
}
|
|
606
|
+
const bottomParts = [
|
|
607
|
+
"\u2500".repeat(colWidths["Op"]),
|
|
608
|
+
...columns.map((col) => "\u2500".repeat(colWidths[col]))
|
|
609
|
+
];
|
|
610
|
+
console.log(` ${dim}\u2514\u2500${bottomParts.join("\u2500\u2534\u2500")}\u2500\u2518${reset}`);
|
|
611
|
+
console.log();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if ((!ddl || ddl.length === 0) && (!diff || diff.length === 0)) {
|
|
615
|
+
console.log("No changes");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch (err) {
|
|
619
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// src/commands/invite.ts
|
|
625
|
+
import { Command as Command4 } from "commander";
|
|
626
|
+
async function sendInvite(email, role) {
|
|
627
|
+
const validRoles = ["owner", "admin", "member", "viewer"];
|
|
628
|
+
if (!validRoles.includes(role)) {
|
|
629
|
+
console.error(`\u2717 Invalid role: ${role}`);
|
|
630
|
+
console.error(` Valid roles: ${validRoles.join(", ")}`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
const result = await api.post("/v1/cli/invite", {
|
|
635
|
+
email,
|
|
636
|
+
role
|
|
637
|
+
});
|
|
638
|
+
console.log(`\u2713 Invitation sent to ${result.email}`);
|
|
639
|
+
console.log(` Role: ${result.role}`);
|
|
640
|
+
console.log(` Org: ${result.org_name}`);
|
|
641
|
+
console.log();
|
|
642
|
+
console.log("They'll receive an email with a link to join.");
|
|
643
|
+
} catch (err) {
|
|
644
|
+
if (isNetworkError(err)) {
|
|
645
|
+
console.error("\u2717 Cannot send invite while offline");
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
var inviteCommand = new Command4("invite").description("Invite a user to your organization").argument("[email]", "Email address to invite").argument("[role]", "Role to assign (owner/admin/member/viewer)", "member").action(async (email, role) => {
|
|
653
|
+
if (!email) {
|
|
654
|
+
inviteCommand.help();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
await sendInvite(email, role);
|
|
658
|
+
});
|
|
659
|
+
inviteCommand.command("list").description("List pending invites").action(async () => {
|
|
660
|
+
try {
|
|
661
|
+
const result = await api.get("/v1/cli/invites");
|
|
662
|
+
if (!result.invites) {
|
|
663
|
+
throw new Error("API returned invalid response: missing invites array");
|
|
664
|
+
}
|
|
665
|
+
const invites = result.invites;
|
|
666
|
+
if (invites.length === 0) {
|
|
667
|
+
console.log("No pending invites");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
console.log("Pending invites:\n");
|
|
671
|
+
for (const inv of invites) {
|
|
672
|
+
const created = new Date(inv.created_at).toLocaleDateString();
|
|
673
|
+
console.log(` ${inv.email}`);
|
|
674
|
+
console.log(` Role: ${inv.role} | Sent: ${created}`);
|
|
675
|
+
console.log();
|
|
676
|
+
}
|
|
677
|
+
} catch (err) {
|
|
678
|
+
if (isNetworkError(err)) {
|
|
679
|
+
console.error("\u2717 Cannot list invites while offline");
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
inviteCommand.command("delete <email>").description("Revoke a pending invite").action(async (email) => {
|
|
687
|
+
try {
|
|
688
|
+
await api.delete("/v1/cli/invites", { email });
|
|
689
|
+
console.log(`\u2713 Invite for ${email} deleted`);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
if (isNetworkError(err)) {
|
|
692
|
+
console.error("\u2717 Cannot delete invite while offline");
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// src/commands/switch.ts
|
|
701
|
+
import { Command as Command5 } from "commander";
|
|
702
|
+
var switchCommand = new Command5("switch").description("Switch to a different branch").argument("<name>", "Branch name to switch to").action(async (name) => {
|
|
703
|
+
let cached = getCacheEntry("branches");
|
|
704
|
+
let branch = cached?.data.find((cachedBranch) => cachedBranch.name === name);
|
|
705
|
+
if (!branch) {
|
|
706
|
+
try {
|
|
707
|
+
const result = await api.get("/v1/cli/branches");
|
|
708
|
+
if (!result.branches) {
|
|
709
|
+
throw new Error("API returned invalid response: missing branches array");
|
|
710
|
+
}
|
|
711
|
+
setCacheEntry("branches", result.branches);
|
|
712
|
+
branch = result.branches.find((remoteBranch) => remoteBranch.name === name);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
if (isNetworkError(err)) {
|
|
715
|
+
if (!branch) {
|
|
716
|
+
console.error(`\u2717 Branch "${name}" not found in cache`);
|
|
717
|
+
console.log(" Run 'ardent branch list' when online to refresh");
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (!branch) {
|
|
727
|
+
console.error(`\u2717 Branch "${name}" not found`);
|
|
728
|
+
console.log(" Run: ardent branch list");
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
const previous = getCurrentBranch();
|
|
732
|
+
setCurrentBranch(name);
|
|
733
|
+
if (previous && previous !== name) {
|
|
734
|
+
console.log(`Switched from '${previous}' to '${name}'`);
|
|
735
|
+
} else {
|
|
736
|
+
console.log(`Switched to branch '${name}'`);
|
|
737
|
+
}
|
|
738
|
+
if (branch.branch_url) {
|
|
739
|
+
console.log(`
|
|
740
|
+
${branch.branch_url}`);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// src/commands/auth.ts
|
|
745
|
+
import { Command as Command6 } from "commander";
|
|
746
|
+
var loginCommand = new Command6("login").description("Login to Ardent").option("-t, --token <token>", "API token (skip browser login)").action(async (options) => {
|
|
747
|
+
if (options.token) {
|
|
748
|
+
setConfig("token", options.token);
|
|
749
|
+
console.log("\u2713 Logged in successfully");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
console.log("Opening browser for authentication...");
|
|
753
|
+
try {
|
|
754
|
+
const initResponse = await fetch(`${getApiUrl()}/v1/cli/auth/init`, {
|
|
755
|
+
method: "POST",
|
|
756
|
+
headers: { "Content-Type": "application/json" }
|
|
757
|
+
});
|
|
758
|
+
if (!initResponse.ok) {
|
|
759
|
+
const error = await initResponse.text();
|
|
760
|
+
console.error("\u2717 Failed to initialize auth:", error);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
const { session_id, auth_url } = await initResponse.json();
|
|
764
|
+
const open = (await import("open")).default;
|
|
765
|
+
await open(auth_url);
|
|
766
|
+
console.log("Waiting for authentication...");
|
|
767
|
+
console.log(`(If browser didn't open, visit: ${auth_url})`);
|
|
768
|
+
const pollInterval = 2e3;
|
|
769
|
+
const maxAttempts = 150;
|
|
770
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
771
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
772
|
+
const pollResponse = await fetch(
|
|
773
|
+
`${getApiUrl()}/v1/cli/auth/poll?session=${session_id}`
|
|
774
|
+
);
|
|
775
|
+
if (!pollResponse.ok) {
|
|
776
|
+
console.error("\u2717 Poll failed");
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
const result = await pollResponse.json();
|
|
780
|
+
if (result.status === "completed") {
|
|
781
|
+
setConfig("token", result.token);
|
|
782
|
+
console.log("\n\u2713 Logged in successfully");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (result.status === "expired") {
|
|
786
|
+
console.error("\n\u2717 Session expired. Please try again.");
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
if (result.status === "error") {
|
|
790
|
+
console.error("\n\u2717 Error:", result.message);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
process.stdout.write(".");
|
|
794
|
+
}
|
|
795
|
+
console.error("\n\u2717 Timed out. Please try again.");
|
|
796
|
+
process.exit(1);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
console.error("\u2717 Login failed:", error instanceof Error ? error.message : error);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
var logoutCommand = new Command6("logout").description("Logout from Ardent").action(() => {
|
|
803
|
+
clearConfig();
|
|
804
|
+
console.log("\u2713 Logged out");
|
|
805
|
+
});
|
|
806
|
+
var statusCommand = new Command6("status").description("Show status").action(() => {
|
|
807
|
+
const token = getConfig("token");
|
|
808
|
+
if (token) {
|
|
809
|
+
console.log("\u2713 Authenticated");
|
|
810
|
+
console.log(` Token: ${token.slice(0, 8)}...${token.slice(-4)}`);
|
|
811
|
+
} else {
|
|
812
|
+
console.log("\u2717 Not authenticated");
|
|
813
|
+
console.log(" Run: ardent login");
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// src/index.ts
|
|
818
|
+
var BANNER = `
|
|
819
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
820
|
+
\u2502 \u2502
|
|
821
|
+
\u2502 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2502
|
|
822
|
+
\u2502 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D \u2502
|
|
823
|
+
\u2502 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2502
|
|
824
|
+
\u2502 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2502
|
|
825
|
+
\u2502 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2502
|
|
826
|
+
\u2502 \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u2502
|
|
827
|
+
\u2502 \u2502
|
|
828
|
+
\u2502 Git for your data infrastructure \u2502
|
|
829
|
+
\u2502 \u2502
|
|
830
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
831
|
+
`;
|
|
832
|
+
var HELP_TEXT = `
|
|
833
|
+
USAGE
|
|
834
|
+
ardent <command> [options]
|
|
835
|
+
|
|
836
|
+
COMMANDS
|
|
837
|
+
login Login via browser (GitHub OAuth)
|
|
838
|
+
logout Clear stored credentials
|
|
839
|
+
status Check authentication status
|
|
840
|
+
connector create Connect a database (postgres, snowflake, etc.)
|
|
841
|
+
connector list List your connectors
|
|
842
|
+
connector delete Delete a connector
|
|
843
|
+
branch create Create a new database branch (auto-switches to it)
|
|
844
|
+
branch list List your branches (* = current)
|
|
845
|
+
branch info Show branch details (URL, status, etc.)
|
|
846
|
+
branch delete Delete a branch
|
|
847
|
+
switch <name> Switch to a different branch
|
|
848
|
+
diff Show CDC changes on a branch
|
|
849
|
+
invite <email> Invite a user to your organization
|
|
850
|
+
invite list List pending invites
|
|
851
|
+
invite delete Revoke a pending invite
|
|
852
|
+
|
|
853
|
+
OPTIONS
|
|
854
|
+
--help Show help for any command
|
|
855
|
+
--version Show CLI version
|
|
856
|
+
|
|
857
|
+
EXAMPLES
|
|
858
|
+
ardent login
|
|
859
|
+
ardent connector create postgresql postgresql://user:pass@host:5432/db
|
|
860
|
+
ardent branch create my-feature
|
|
861
|
+
ardent branch list
|
|
862
|
+
ardent invite teammate@company.com
|
|
863
|
+
ardent invite teammate@company.com admin
|
|
864
|
+
|
|
865
|
+
For more help, visit https://docs.tryardent.com/cli
|
|
866
|
+
`;
|
|
867
|
+
var program = new Command7();
|
|
868
|
+
program.name("ardent").description("CLI for Ardent database branching").version("0.0.1").configureHelp({
|
|
869
|
+
formatHelp: () => BANNER + HELP_TEXT
|
|
870
|
+
});
|
|
871
|
+
program.addCommand(loginCommand);
|
|
872
|
+
program.addCommand(logoutCommand);
|
|
873
|
+
program.addCommand(statusCommand);
|
|
874
|
+
program.addCommand(connectorCommand);
|
|
875
|
+
program.addCommand(branchCommand);
|
|
876
|
+
program.addCommand(switchCommand);
|
|
877
|
+
program.addCommand(diffCommand);
|
|
878
|
+
program.addCommand(inviteCommand);
|
|
879
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ardent-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for Ardent database branching",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ardent": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.1.0",
|
|
22
|
+
"open": "^10.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|