ardent-cli 0.0.10 → 0.0.12
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 +449 -57
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { Command as
|
|
4
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import { Command as Command7 } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/branch/index.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
|
+
// src/lib/api.ts
|
|
11
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
12
|
+
|
|
10
13
|
// src/lib/config.ts
|
|
11
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
12
15
|
import { homedir } from "os";
|
|
@@ -104,9 +107,8 @@ async function bootstrapCache() {
|
|
|
104
107
|
"Content-Type": "application/json",
|
|
105
108
|
"Authorization": `Bearer ${token}`
|
|
106
109
|
};
|
|
107
|
-
const [connectorsRes,
|
|
110
|
+
const [connectorsRes, meRes] = await Promise.all([
|
|
108
111
|
fetch(`${apiUrl}/v1/cli/connectors`, { headers }),
|
|
109
|
-
fetch(`${apiUrl}/v1/cli/branches`, { headers }),
|
|
110
112
|
fetch(`${apiUrl}/v1/cli/me`, { headers })
|
|
111
113
|
]);
|
|
112
114
|
let connectorCount = 0;
|
|
@@ -117,28 +119,71 @@ async function bootstrapCache() {
|
|
|
117
119
|
setCacheEntry("connectors", connectors);
|
|
118
120
|
connectorCount = connectors.length;
|
|
119
121
|
}
|
|
120
|
-
if (branchesRes.ok) {
|
|
121
|
-
const data = await branchesRes.json();
|
|
122
|
-
const branches = data.branches || [];
|
|
123
|
-
setCacheEntry("branches", branches);
|
|
124
|
-
branchCount = branches.length;
|
|
125
|
-
}
|
|
126
122
|
if (meRes.ok) {
|
|
127
123
|
const user = await meRes.json();
|
|
128
124
|
setConfig("user", user);
|
|
129
125
|
}
|
|
126
|
+
const currentConnectorId = getConfig("currentConnectorId");
|
|
127
|
+
if (currentConnectorId) {
|
|
128
|
+
const branchesRes = await fetch(
|
|
129
|
+
`${apiUrl}/v1/cli/branches?connector_id=${currentConnectorId}`,
|
|
130
|
+
{ headers }
|
|
131
|
+
);
|
|
132
|
+
if (branchesRes.ok) {
|
|
133
|
+
const data = await branchesRes.json();
|
|
134
|
+
const branches = data.branches || [];
|
|
135
|
+
setCacheEntry("branches", branches);
|
|
136
|
+
branchCount = branches.length;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
130
139
|
return { connectors: connectorCount, branches: branchCount };
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
// src/lib/api.ts
|
|
143
|
+
function getCliVersion() {
|
|
144
|
+
try {
|
|
145
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
146
|
+
const raw = readFileSync2(packageUrl, "utf-8");
|
|
147
|
+
const parsed = JSON.parse(raw);
|
|
148
|
+
return parsed.version ?? "unknown";
|
|
149
|
+
} catch {
|
|
150
|
+
return "unknown";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
var CLI_VERSION = getCliVersion();
|
|
154
|
+
function formatErrorDetail(detail) {
|
|
155
|
+
if (typeof detail === "string") return detail;
|
|
156
|
+
if (Array.isArray(detail)) {
|
|
157
|
+
return detail.map((entry) => {
|
|
158
|
+
const location = Array.isArray(entry.loc) ? entry.loc.join(" \u2192 ") : "";
|
|
159
|
+
const message = entry.msg ?? "unknown error";
|
|
160
|
+
return location ? `${location}: ${message}` : String(message);
|
|
161
|
+
}).join("; ");
|
|
162
|
+
}
|
|
163
|
+
return JSON.stringify(detail);
|
|
164
|
+
}
|
|
134
165
|
function parseApiError(status, text) {
|
|
135
166
|
try {
|
|
136
167
|
const json = JSON.parse(text);
|
|
137
|
-
if (json.detail) return `API error ${status}: ${json.detail}`;
|
|
168
|
+
if (json.detail) return `API error ${status}: ${formatErrorDetail(json.detail)}`;
|
|
138
169
|
} catch {
|
|
139
170
|
}
|
|
140
171
|
return `API error ${status}: ${text}`;
|
|
141
172
|
}
|
|
173
|
+
function handleUpgradeRequired(text) {
|
|
174
|
+
try {
|
|
175
|
+
const json = JSON.parse(text);
|
|
176
|
+
if (json.detail) {
|
|
177
|
+
console.error(`
|
|
178
|
+
\u2717 ${json.detail}
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
console.error("\n\u2717 Your CLI is outdated. Please update:\n");
|
|
183
|
+
}
|
|
184
|
+
console.error(" npm install -g ardent-cli@latest\n");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
142
187
|
var ApiClient = class {
|
|
143
188
|
getHeaders() {
|
|
144
189
|
const token = getToken();
|
|
@@ -150,20 +195,28 @@ var ApiClient = class {
|
|
|
150
195
|
}
|
|
151
196
|
return {
|
|
152
197
|
"Content-Type": "application/json",
|
|
153
|
-
"Authorization": `Bearer ${token}
|
|
198
|
+
"Authorization": `Bearer ${token}`,
|
|
199
|
+
"X-CLI-Version": CLI_VERSION
|
|
154
200
|
};
|
|
155
201
|
}
|
|
202
|
+
async handleResponse(response) {
|
|
203
|
+
if (response.status === 426) {
|
|
204
|
+
const text = await response.text();
|
|
205
|
+
handleUpgradeRequired(text);
|
|
206
|
+
}
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
const text = await response.text();
|
|
209
|
+
throw new Error(parseApiError(response.status, text));
|
|
210
|
+
}
|
|
211
|
+
return response.json();
|
|
212
|
+
}
|
|
156
213
|
async get(path) {
|
|
157
214
|
const url = `${getApiUrl()}${path}`;
|
|
158
215
|
const response = await fetch(url, {
|
|
159
216
|
method: "GET",
|
|
160
217
|
headers: this.getHeaders()
|
|
161
218
|
});
|
|
162
|
-
|
|
163
|
-
const text = await response.text();
|
|
164
|
-
throw new Error(parseApiError(response.status, text));
|
|
165
|
-
}
|
|
166
|
-
return response.json();
|
|
219
|
+
return this.handleResponse(response);
|
|
167
220
|
}
|
|
168
221
|
async post(path, body) {
|
|
169
222
|
const url = `${getApiUrl()}${path}`;
|
|
@@ -172,11 +225,7 @@ var ApiClient = class {
|
|
|
172
225
|
headers: this.getHeaders(),
|
|
173
226
|
body: JSON.stringify(body)
|
|
174
227
|
});
|
|
175
|
-
|
|
176
|
-
const text = await response.text();
|
|
177
|
-
throw new Error(parseApiError(response.status, text));
|
|
178
|
-
}
|
|
179
|
-
return response.json();
|
|
228
|
+
return this.handleResponse(response);
|
|
180
229
|
}
|
|
181
230
|
async delete(path, body) {
|
|
182
231
|
const url = `${getApiUrl()}${path}`;
|
|
@@ -185,11 +234,7 @@ var ApiClient = class {
|
|
|
185
234
|
headers: this.getHeaders(),
|
|
186
235
|
body: body ? JSON.stringify(body) : void 0
|
|
187
236
|
});
|
|
188
|
-
|
|
189
|
-
const text = await response.text();
|
|
190
|
-
throw new Error(parseApiError(response.status, text));
|
|
191
|
-
}
|
|
192
|
-
return response.json();
|
|
237
|
+
return this.handleResponse(response);
|
|
193
238
|
}
|
|
194
239
|
async patch(path, body) {
|
|
195
240
|
const url = `${getApiUrl()}${path}`;
|
|
@@ -198,11 +243,7 @@ var ApiClient = class {
|
|
|
198
243
|
headers: this.getHeaders(),
|
|
199
244
|
body: JSON.stringify(body)
|
|
200
245
|
});
|
|
201
|
-
|
|
202
|
-
const text = await response.text();
|
|
203
|
-
throw new Error(parseApiError(response.status, text));
|
|
204
|
-
}
|
|
205
|
-
return response.json();
|
|
246
|
+
return this.handleResponse(response);
|
|
206
247
|
}
|
|
207
248
|
};
|
|
208
249
|
var api = new ApiClient();
|
|
@@ -277,23 +318,33 @@ function identifyUser(userId, personProperties = {}) {
|
|
|
277
318
|
async function createAction(name, options) {
|
|
278
319
|
try {
|
|
279
320
|
const startTime = performance.now();
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
const connectorId = getConfig("currentConnectorId");
|
|
322
|
+
if (!connectorId) {
|
|
323
|
+
console.error("\u2717 No connector selected. Run 'ardent connector switch' first.");
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
283
326
|
await api.post("/v1/branch/create", {
|
|
284
|
-
|
|
285
|
-
service_type: options.service
|
|
327
|
+
connector_id: connectorId,
|
|
328
|
+
service_type: options.service,
|
|
329
|
+
name
|
|
286
330
|
});
|
|
287
|
-
const
|
|
288
|
-
const
|
|
331
|
+
const response = await api.get(`/v1/cli/branches?connector_id=${connectorId}`);
|
|
332
|
+
const apiBranches = response.branches || [];
|
|
333
|
+
let apiBranch;
|
|
334
|
+
for (const branch2 of apiBranches) {
|
|
335
|
+
if (branch2.name === name) {
|
|
336
|
+
apiBranch = branch2;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
289
340
|
if (!apiBranch) {
|
|
290
341
|
console.error("\u2717 Branch created but could not fetch details");
|
|
291
342
|
process.exit(1);
|
|
292
343
|
}
|
|
293
344
|
const branch = {
|
|
294
345
|
id: apiBranch.id,
|
|
295
|
-
|
|
296
|
-
name,
|
|
346
|
+
connector_id: apiBranch.connector_id,
|
|
347
|
+
name: apiBranch.name,
|
|
297
348
|
service_type: apiBranch.service_type,
|
|
298
349
|
branch_url: apiBranch.branch_url || "",
|
|
299
350
|
status: apiBranch.status,
|
|
@@ -329,7 +380,12 @@ async function listAction() {
|
|
|
329
380
|
let fromCache = false;
|
|
330
381
|
let cacheTime = "";
|
|
331
382
|
try {
|
|
332
|
-
const
|
|
383
|
+
const currentConnectorId = getConfig("currentConnectorId");
|
|
384
|
+
if (!currentConnectorId) {
|
|
385
|
+
console.error("\u2717 No connector selected. Run 'ardent connector switch' first.");
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
const result = await api.get(`/v1/cli/branches?connector_id=${currentConnectorId}`);
|
|
333
389
|
if (!result.branches) {
|
|
334
390
|
throw new Error("API returned invalid response: missing branches array");
|
|
335
391
|
}
|
|
@@ -426,7 +482,7 @@ async function deleteAction(name) {
|
|
|
426
482
|
process.exit(1);
|
|
427
483
|
}
|
|
428
484
|
try {
|
|
429
|
-
await api.delete(`/v1/cli/
|
|
485
|
+
await api.delete(`/v1/cli/branches/${branch.id}`);
|
|
430
486
|
const updatedBranches = cached.data.filter((cachedBranch) => cachedBranch.id !== branch.id);
|
|
431
487
|
setCacheEntry("branches", updatedBranches);
|
|
432
488
|
if (getCurrentBranch() === name) {
|
|
@@ -537,7 +593,7 @@ async function diffAction(options) {
|
|
|
537
593
|
return;
|
|
538
594
|
}
|
|
539
595
|
try {
|
|
540
|
-
const stateRaw = await api.get(`/v1/branches/${branch.
|
|
596
|
+
const stateRaw = await api.get(`/v1/branches/${branch.id}/state`);
|
|
541
597
|
if (!stateRaw || Object.keys(stateRaw).length === 0) {
|
|
542
598
|
console.log("No changes captured");
|
|
543
599
|
return;
|
|
@@ -867,12 +923,20 @@ async function createAction2(type, url, options) {
|
|
|
867
923
|
}
|
|
868
924
|
try {
|
|
869
925
|
const connectorName = options.name || (isByoc ? "my-neon-connection" : "my-postgresql-connection");
|
|
926
|
+
const currentProjectId = getConfig("currentProjectId");
|
|
927
|
+
if (!currentProjectId) {
|
|
928
|
+
console.error("\u2717 No current project set. Switch to a project first:");
|
|
929
|
+
console.error(" ardent project list");
|
|
930
|
+
console.error(" ardent project switch <name>");
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
870
933
|
let createPayload;
|
|
871
934
|
if (isByoc) {
|
|
872
935
|
console.log("Creating connector (BYOC Neon)...");
|
|
873
936
|
createPayload = {
|
|
874
937
|
name: connectorName,
|
|
875
938
|
service_name: "postgresql",
|
|
939
|
+
project_id: currentProjectId,
|
|
876
940
|
byoc: options.byoc,
|
|
877
941
|
neon_api_key: options.apiKey,
|
|
878
942
|
neon_project_id: options.projectId,
|
|
@@ -889,6 +953,7 @@ async function createAction2(type, url, options) {
|
|
|
889
953
|
createPayload = {
|
|
890
954
|
name: connectorName,
|
|
891
955
|
service_name: "postgresql",
|
|
956
|
+
project_id: currentProjectId,
|
|
892
957
|
connection_details: {
|
|
893
958
|
host: parsed.host,
|
|
894
959
|
port: parsed.port,
|
|
@@ -932,6 +997,8 @@ async function createAction2(type, url, options) {
|
|
|
932
997
|
const cachedConnectors = cached?.data || [];
|
|
933
998
|
cachedConnectors.push(newConnector);
|
|
934
999
|
setCacheEntry("connectors", cachedConnectors);
|
|
1000
|
+
setConfig("currentConnectorId", connectorId);
|
|
1001
|
+
setConfig("currentConnectorName", connectorName);
|
|
935
1002
|
trackEvent("CLI: connector create succeeded", { db_type: type, byoc: isByoc });
|
|
936
1003
|
console.log("\u2713 Connector created and ready");
|
|
937
1004
|
console.log(` ID: ${connectorId}`);
|
|
@@ -958,7 +1025,9 @@ async function listAction2() {
|
|
|
958
1025
|
let fromCache = false;
|
|
959
1026
|
let cacheTime = "";
|
|
960
1027
|
try {
|
|
961
|
-
const
|
|
1028
|
+
const currentProjectId = getConfig("currentProjectId");
|
|
1029
|
+
const projectFilter = currentProjectId ? `?project_id=${currentProjectId}` : "";
|
|
1030
|
+
const result = await api.get(`/v1/cli/connectors${projectFilter}`);
|
|
962
1031
|
if (!result.connectors) {
|
|
963
1032
|
throw new Error("API returned invalid response: missing connectors array");
|
|
964
1033
|
}
|
|
@@ -992,11 +1061,21 @@ async function listAction2() {
|
|
|
992
1061
|
console.log(" Create one with: ardent connector create postgresql <url>");
|
|
993
1062
|
return;
|
|
994
1063
|
}
|
|
1064
|
+
const currentConnectorId = getConfig("currentConnectorId");
|
|
1065
|
+
const green3 = "\x1B[32m";
|
|
1066
|
+
const dim3 = "\x1B[2m";
|
|
1067
|
+
const reset3 = "\x1B[0m";
|
|
995
1068
|
console.log("Connectors:\n");
|
|
996
1069
|
for (const connector of connectors) {
|
|
1070
|
+
const isCurrent = connector.id === currentConnectorId;
|
|
997
1071
|
const icon = connector.status === "healthy" ? "\u25CF" : "\u25CB";
|
|
998
|
-
|
|
999
|
-
|
|
1072
|
+
if (isCurrent) {
|
|
1073
|
+
console.log(`${green3}* ${icon} ${connector.name}${reset3}`);
|
|
1074
|
+
console.log(`${green3} ${connector.service_name}${reset3}`);
|
|
1075
|
+
} else {
|
|
1076
|
+
console.log(` ${icon} ${connector.name}`);
|
|
1077
|
+
console.log(`${dim3} ${connector.service_name}${reset3}`);
|
|
1078
|
+
}
|
|
1000
1079
|
console.log();
|
|
1001
1080
|
}
|
|
1002
1081
|
}
|
|
@@ -1056,10 +1135,64 @@ async function deleteAction2(name) {
|
|
|
1056
1135
|
}
|
|
1057
1136
|
}
|
|
1058
1137
|
|
|
1138
|
+
// src/commands/connector/switch.ts
|
|
1139
|
+
async function switchAction2(name) {
|
|
1140
|
+
const cached = getCacheEntry("connectors");
|
|
1141
|
+
let connector = cached?.data.find(
|
|
1142
|
+
(cachedConnector) => cachedConnector.name === name
|
|
1143
|
+
);
|
|
1144
|
+
if (!connector) {
|
|
1145
|
+
try {
|
|
1146
|
+
const result = await api.get("/v1/cli/connectors");
|
|
1147
|
+
if (!result.connectors) {
|
|
1148
|
+
throw new Error("API returned invalid response: missing connectors array");
|
|
1149
|
+
}
|
|
1150
|
+
setCacheEntry("connectors", result.connectors);
|
|
1151
|
+
connector = result.connectors.find(
|
|
1152
|
+
(remoteConnector) => remoteConnector.name === name
|
|
1153
|
+
);
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
if (isNetworkError(err)) {
|
|
1156
|
+
if (!connector) {
|
|
1157
|
+
console.error(`\u2717 Connector "${name}" not found in cache`);
|
|
1158
|
+
console.log(" Run 'ardent connector list' when online to refresh");
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (!connector) {
|
|
1168
|
+
console.error(`\u2717 Connector "${name}" not found`);
|
|
1169
|
+
console.log(" Run: ardent connector list");
|
|
1170
|
+
process.exit(1);
|
|
1171
|
+
}
|
|
1172
|
+
const previousName = getConfig("currentConnectorName");
|
|
1173
|
+
setConfig("currentConnectorId", connector.id);
|
|
1174
|
+
setConfig("currentConnectorName", connector.name);
|
|
1175
|
+
clearCurrentBranch();
|
|
1176
|
+
if (previousName && previousName !== name) {
|
|
1177
|
+
console.log(`Switched from '${previousName}' to '${name}'`);
|
|
1178
|
+
} else {
|
|
1179
|
+
console.log(`Switched to connector '${name}'`);
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
const branchResult = await api.get(
|
|
1183
|
+
`/v1/cli/branches?connector_id=${connector.id}`
|
|
1184
|
+
);
|
|
1185
|
+
setCacheEntry("branches", branchResult.branches || []);
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
trackEvent("CLI: connector switch");
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1059
1191
|
// src/commands/connector/index.ts
|
|
1060
1192
|
var connectorCommand = new Command2("connector").description("Manage database connectors");
|
|
1061
1193
|
connectorCommand.command("create <type> [url]").description("Create a new connector").option("-n, --name <name>", "Connector name").option("--byoc <provider>", "Bring your own Neon project (e.g. neon)").option("--api-key <key>", "Neon API key (required with --byoc)").option("--project-id <id>", "Neon project ID (required with --byoc)").action(createAction2);
|
|
1062
1194
|
connectorCommand.command("list").description("List your connectors").action(listAction2);
|
|
1195
|
+
connectorCommand.command("switch <name>").description("Switch to a different connector").action(switchAction2);
|
|
1063
1196
|
connectorCommand.command("delete <name>").description("Delete a connector by name").action(deleteAction2);
|
|
1064
1197
|
|
|
1065
1198
|
// src/commands/invite/index.ts
|
|
@@ -1257,9 +1390,185 @@ orgCommand.command("members").description("List organization members").action(me
|
|
|
1257
1390
|
orgCommand.command("set-role <email> <role>").description("Update a member's role (owner/admin/member/viewer)").action(setRoleAction);
|
|
1258
1391
|
orgCommand.command("remove <email>").description("Remove a member from the organization").action(removeAction);
|
|
1259
1392
|
|
|
1260
|
-
// src/commands/
|
|
1393
|
+
// src/commands/project/index.ts
|
|
1261
1394
|
import { Command as Command5 } from "commander";
|
|
1262
1395
|
|
|
1396
|
+
// src/commands/project/create.ts
|
|
1397
|
+
async function createAction3(name) {
|
|
1398
|
+
const user = getConfig("user");
|
|
1399
|
+
if (!user?.org_id) {
|
|
1400
|
+
console.error("\u2717 No organization found. Run: ardent login");
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
}
|
|
1403
|
+
try {
|
|
1404
|
+
const project = await api.post("/v1/projects", {
|
|
1405
|
+
name,
|
|
1406
|
+
org_id: user.org_id
|
|
1407
|
+
});
|
|
1408
|
+
const cached = getCacheEntry("projects");
|
|
1409
|
+
const cachedProjects = cached?.data || [];
|
|
1410
|
+
cachedProjects.push(project);
|
|
1411
|
+
setCacheEntry("projects", cachedProjects);
|
|
1412
|
+
setConfig("currentProjectId", project.id);
|
|
1413
|
+
setConfig("currentProjectName", project.name);
|
|
1414
|
+
trackEvent("CLI: project create succeeded");
|
|
1415
|
+
console.log(`\u2713 Project '${name}' created`);
|
|
1416
|
+
console.log(` ID: ${project.id}`);
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
if (isNetworkError(err)) {
|
|
1419
|
+
trackEvent("CLI: project create failed", { reason: "offline" });
|
|
1420
|
+
console.error("\u2717 Cannot create project while offline");
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
if (isPermissionError(err)) {
|
|
1424
|
+
trackEvent("CLI: project create failed", { reason: "permission_denied" });
|
|
1425
|
+
console.error("\u2717 You don't have permission to create projects.");
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
}
|
|
1428
|
+
trackEvent("CLI: project create failed", { reason: "api_error" });
|
|
1429
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
1430
|
+
process.exit(1);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/commands/project/list.ts
|
|
1435
|
+
async function listAction4() {
|
|
1436
|
+
let projects = [];
|
|
1437
|
+
let fromCache = false;
|
|
1438
|
+
let cacheTime = "";
|
|
1439
|
+
try {
|
|
1440
|
+
const user = getConfig("user");
|
|
1441
|
+
if (!user?.org_id) {
|
|
1442
|
+
console.error("\u2717 No organization found. Run: ardent login");
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
const result = await api.get(
|
|
1446
|
+
`/v1/projects?org_id=${user.org_id}`
|
|
1447
|
+
);
|
|
1448
|
+
if (!result.projects) {
|
|
1449
|
+
throw new Error("API returned invalid response: missing projects array");
|
|
1450
|
+
}
|
|
1451
|
+
projects = result.projects;
|
|
1452
|
+
setCacheEntry("projects", projects);
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
if (isNetworkError(err)) {
|
|
1455
|
+
const cached = getCacheEntry("projects");
|
|
1456
|
+
if (cached) {
|
|
1457
|
+
projects = cached.data;
|
|
1458
|
+
fromCache = true;
|
|
1459
|
+
cacheTime = formatCacheTime(cached.updated_at);
|
|
1460
|
+
} else {
|
|
1461
|
+
trackEvent("CLI: project list failed", { reason: "offline_no_cache" });
|
|
1462
|
+
console.error("\u2717 Offline and no cached data available");
|
|
1463
|
+
process.exit(1);
|
|
1464
|
+
}
|
|
1465
|
+
} else {
|
|
1466
|
+
trackEvent("CLI: project list failed", { reason: "api_error" });
|
|
1467
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
1468
|
+
process.exit(1);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
trackEvent("CLI: project list succeeded", {
|
|
1472
|
+
project_count: projects.length,
|
|
1473
|
+
from_cache: fromCache
|
|
1474
|
+
});
|
|
1475
|
+
if (fromCache) {
|
|
1476
|
+
console.log(`\u26A0 Offline - showing cached data from ${cacheTime}
|
|
1477
|
+
`);
|
|
1478
|
+
}
|
|
1479
|
+
if (projects.length === 0) {
|
|
1480
|
+
console.log("No projects found");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const currentProjectId = getConfig("currentProjectId");
|
|
1484
|
+
const green3 = "\x1B[32m";
|
|
1485
|
+
const dim3 = "\x1B[2m";
|
|
1486
|
+
const reset3 = "\x1B[0m";
|
|
1487
|
+
console.log("Projects:\n");
|
|
1488
|
+
for (const project of projects) {
|
|
1489
|
+
const isCurrent = project.id === currentProjectId;
|
|
1490
|
+
if (isCurrent) {
|
|
1491
|
+
console.log(`${green3}* ${project.name}${reset3}`);
|
|
1492
|
+
console.log(`${green3} ${project.id}${reset3}`);
|
|
1493
|
+
} else {
|
|
1494
|
+
console.log(` ${project.name}`);
|
|
1495
|
+
console.log(`${dim3} ${project.id}${reset3}`);
|
|
1496
|
+
}
|
|
1497
|
+
console.log();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/commands/project/switch.ts
|
|
1502
|
+
async function switchAction3(name) {
|
|
1503
|
+
let projects = [];
|
|
1504
|
+
const cached = getCacheEntry("projects");
|
|
1505
|
+
let project = cached?.data.find(
|
|
1506
|
+
(cachedProject) => cachedProject.name === name
|
|
1507
|
+
);
|
|
1508
|
+
if (!project) {
|
|
1509
|
+
try {
|
|
1510
|
+
const user = getConfig("user");
|
|
1511
|
+
if (!user?.org_id) {
|
|
1512
|
+
console.error("\u2717 No organization found. Run: ardent login");
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
const result = await api.get(
|
|
1516
|
+
`/v1/projects?org_id=${user.org_id}`
|
|
1517
|
+
);
|
|
1518
|
+
if (!result.projects) {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
"API returned invalid response: missing projects array"
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
projects = result.projects;
|
|
1524
|
+
setCacheEntry("projects", projects);
|
|
1525
|
+
project = projects.find(
|
|
1526
|
+
(remoteProject) => remoteProject.name === name
|
|
1527
|
+
);
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
if (isNetworkError(err)) {
|
|
1530
|
+
if (!project) {
|
|
1531
|
+
console.error(`\u2717 Project "${name}" not found in cache`);
|
|
1532
|
+
console.log(" Run 'ardent project list' when online to refresh");
|
|
1533
|
+
process.exit(1);
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (!project) {
|
|
1542
|
+
console.error(`\u2717 Project "${name}" not found`);
|
|
1543
|
+
console.log(" Run: ardent project list");
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
const previousId = getConfig("currentProjectId");
|
|
1547
|
+
const previousName = getConfig("currentProjectName");
|
|
1548
|
+
setConfig("currentProjectId", project.id);
|
|
1549
|
+
setConfig("currentProjectName", project.name);
|
|
1550
|
+
setConfig("currentConnectorId", void 0);
|
|
1551
|
+
setConfig("currentConnectorName", void 0);
|
|
1552
|
+
clearCurrentBranch();
|
|
1553
|
+
if (previousName && previousName !== name) {
|
|
1554
|
+
console.log(`Switched from '${previousName}' to '${name}'`);
|
|
1555
|
+
} else {
|
|
1556
|
+
console.log(`Switched to project '${name}'`);
|
|
1557
|
+
}
|
|
1558
|
+
trackEvent("CLI: project switch");
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/commands/project/index.ts
|
|
1562
|
+
var projectCommand = new Command5("project").description(
|
|
1563
|
+
"Manage projects"
|
|
1564
|
+
);
|
|
1565
|
+
projectCommand.command("create <name>").description("Create a new project").action(createAction3);
|
|
1566
|
+
projectCommand.command("list").description("List your projects").action(listAction4);
|
|
1567
|
+
projectCommand.command("switch <name>").description("Switch to a different project").action(switchAction3);
|
|
1568
|
+
|
|
1569
|
+
// src/commands/auth/index.ts
|
|
1570
|
+
import { Command as Command6 } from "commander";
|
|
1571
|
+
|
|
1263
1572
|
// src/commands/auth/login.ts
|
|
1264
1573
|
async function loginAction(options) {
|
|
1265
1574
|
if (options.token) {
|
|
@@ -1389,9 +1698,84 @@ function statusAction() {
|
|
|
1389
1698
|
}
|
|
1390
1699
|
|
|
1391
1700
|
// src/commands/auth/index.ts
|
|
1392
|
-
var loginCommand = new
|
|
1393
|
-
var logoutCommand = new
|
|
1394
|
-
var statusCommand = new
|
|
1701
|
+
var loginCommand = new Command6("login").description("Login to Ardent").option("-t, --token <token>", "API token (skip browser login)").action(loginAction);
|
|
1702
|
+
var logoutCommand = new Command6("logout").description("Logout from Ardent").action(logoutAction);
|
|
1703
|
+
var statusCommand = new Command6("status").description("Show status").action(statusAction);
|
|
1704
|
+
|
|
1705
|
+
// src/lib/update-check.ts
|
|
1706
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1707
|
+
import { join as join2 } from "path";
|
|
1708
|
+
import { homedir as homedir2 } from "os";
|
|
1709
|
+
var UPDATE_CHECK_FILE = join2(homedir2(), ".ardent", "update-check.json");
|
|
1710
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1711
|
+
var PACKAGE_NAME = "ardent-cli";
|
|
1712
|
+
function loadCache() {
|
|
1713
|
+
try {
|
|
1714
|
+
if (existsSync2(UPDATE_CHECK_FILE)) {
|
|
1715
|
+
return JSON.parse(readFileSync3(UPDATE_CHECK_FILE, "utf-8"));
|
|
1716
|
+
}
|
|
1717
|
+
} catch {
|
|
1718
|
+
}
|
|
1719
|
+
return null;
|
|
1720
|
+
}
|
|
1721
|
+
function saveCache(latestVersion) {
|
|
1722
|
+
try {
|
|
1723
|
+
const data = {
|
|
1724
|
+
latest_version: latestVersion,
|
|
1725
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1726
|
+
};
|
|
1727
|
+
writeFileSync2(UPDATE_CHECK_FILE, JSON.stringify(data));
|
|
1728
|
+
} catch {
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function isNewerVersion(current, latest) {
|
|
1732
|
+
const currentParts = current.split(".").map(Number);
|
|
1733
|
+
const latestParts = latest.split(".").map(Number);
|
|
1734
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
1735
|
+
const currentPart = currentParts[i] || 0;
|
|
1736
|
+
const latestPart = latestParts[i] || 0;
|
|
1737
|
+
if (latestPart > currentPart) return true;
|
|
1738
|
+
if (latestPart < currentPart) return false;
|
|
1739
|
+
}
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
async function checkForUpdate(currentVersion) {
|
|
1743
|
+
try {
|
|
1744
|
+
const cache = loadCache();
|
|
1745
|
+
if (cache) {
|
|
1746
|
+
const elapsed = Date.now() - new Date(cache.checked_at).getTime();
|
|
1747
|
+
if (elapsed < CHECK_INTERVAL_MS) {
|
|
1748
|
+
if (isNewerVersion(currentVersion, cache.latest_version)) {
|
|
1749
|
+
printUpdateNotice(currentVersion, cache.latest_version);
|
|
1750
|
+
}
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
const controller = new AbortController();
|
|
1755
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1756
|
+
const response = await fetch(
|
|
1757
|
+
`https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
|
|
1758
|
+
{ signal: controller.signal }
|
|
1759
|
+
);
|
|
1760
|
+
clearTimeout(timeout);
|
|
1761
|
+
if (!response.ok) return;
|
|
1762
|
+
const data = await response.json();
|
|
1763
|
+
const latestVersion = data.version;
|
|
1764
|
+
saveCache(latestVersion);
|
|
1765
|
+
if (isNewerVersion(currentVersion, latestVersion)) {
|
|
1766
|
+
printUpdateNotice(currentVersion, latestVersion);
|
|
1767
|
+
}
|
|
1768
|
+
} catch {
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function printUpdateNotice(current, latest) {
|
|
1772
|
+
const yellow2 = "\x1B[33m";
|
|
1773
|
+
const green3 = "\x1B[32m";
|
|
1774
|
+
const reset3 = "\x1B[0m";
|
|
1775
|
+
console.error(
|
|
1776
|
+
`${yellow2}Update available: ${current} \u2192 ${latest}${reset3} \u2014 run ${green3}npm install -g ardent-cli@latest${reset3}`
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1395
1779
|
|
|
1396
1780
|
// src/index.ts
|
|
1397
1781
|
var HELP_TEXT = `
|
|
@@ -1403,9 +1787,15 @@ AUTHENTICATION
|
|
|
1403
1787
|
logout Clear stored credentials
|
|
1404
1788
|
status Check authentication status
|
|
1405
1789
|
|
|
1790
|
+
PROJECTS
|
|
1791
|
+
project create Create a new project
|
|
1792
|
+
project list List your projects (* = current)
|
|
1793
|
+
project switch Switch to a different project
|
|
1794
|
+
|
|
1406
1795
|
CONNECTORS
|
|
1407
1796
|
connector create Connect a database (postgresql, snowflake, etc.)
|
|
1408
|
-
connector list List your connectors
|
|
1797
|
+
connector list List your connectors (* = current)
|
|
1798
|
+
connector switch Switch to a different connector
|
|
1409
1799
|
connector delete Delete a connector
|
|
1410
1800
|
|
|
1411
1801
|
BRANCHES
|
|
@@ -1435,24 +1825,25 @@ EXAMPLES
|
|
|
1435
1825
|
ardent branch switch my-feature
|
|
1436
1826
|
ardent org members
|
|
1437
1827
|
`;
|
|
1438
|
-
var
|
|
1439
|
-
var program = new
|
|
1440
|
-
function
|
|
1828
|
+
var CLI_VERSION2 = getCliVersion2();
|
|
1829
|
+
var program = new Command7();
|
|
1830
|
+
function getCliVersion2() {
|
|
1441
1831
|
try {
|
|
1442
1832
|
const packageUrl = new URL("../package.json", import.meta.url);
|
|
1443
|
-
const raw =
|
|
1833
|
+
const raw = readFileSync4(packageUrl, "utf-8");
|
|
1444
1834
|
const parsed = JSON.parse(raw);
|
|
1445
1835
|
return parsed.version ?? "unknown";
|
|
1446
1836
|
} catch {
|
|
1447
1837
|
return "unknown";
|
|
1448
1838
|
}
|
|
1449
1839
|
}
|
|
1450
|
-
program.name("ardent").description("CLI for Ardent database branching").version(
|
|
1840
|
+
program.name("ardent").description("CLI for Ardent database branching").version(CLI_VERSION2).configureHelp({
|
|
1451
1841
|
formatHelp: () => BANNER + HELP_TEXT
|
|
1452
1842
|
});
|
|
1453
1843
|
program.addCommand(loginCommand);
|
|
1454
1844
|
program.addCommand(logoutCommand);
|
|
1455
1845
|
program.addCommand(statusCommand);
|
|
1846
|
+
program.addCommand(projectCommand);
|
|
1456
1847
|
program.addCommand(connectorCommand);
|
|
1457
1848
|
program.addCommand(branchCommand);
|
|
1458
1849
|
program.addCommand(inviteCommand);
|
|
@@ -1480,4 +1871,5 @@ if (args.length === 0) {
|
|
|
1480
1871
|
program.help();
|
|
1481
1872
|
}
|
|
1482
1873
|
getAnonymousId();
|
|
1874
|
+
checkForUpdate(CLI_VERSION2);
|
|
1483
1875
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ardent-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Git for Data infrastructure",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsup",
|
|
11
11
|
"dev": "tsup --watch",
|
|
12
|
-
"start": "node dist/index.js"
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "tsx --test src/lib/*.test.ts"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"dist"
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/node": "^20.0.0",
|
|
26
27
|
"tsup": "^8.0.0",
|
|
28
|
+
"tsx": "^4.21.0",
|
|
27
29
|
"typescript": "^5.0.0"
|
|
28
30
|
}
|
|
29
31
|
}
|