@0dai-dev/cli 2.1.1 → 2.2.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.
- package/bin/0dai.js +129 -38
- package/package.json +15 -3
package/bin/0dai.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
|
|
10
|
-
const VERSION = "2.
|
|
10
|
+
const VERSION = "2.2.0";
|
|
11
11
|
const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
|
|
12
12
|
const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
|
|
13
13
|
const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
|
|
@@ -152,10 +152,15 @@ async function cmdInit(target) {
|
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
const isTTY = process.stdout.isTTY;
|
|
156
|
+
let spinner = null;
|
|
157
|
+
if (isTTY) {
|
|
158
|
+
try { spinner = require("@clack/prompts").spinner(); } catch {}
|
|
159
|
+
}
|
|
157
160
|
|
|
158
|
-
|
|
161
|
+
const { projectFiles, fileContents, clis } = collectMetadata(target);
|
|
162
|
+
if (spinner) spinner.start(`Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
163
|
+
else log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
159
164
|
const result = await apiCall("/v1/init", {
|
|
160
165
|
project_files: projectFiles,
|
|
161
166
|
file_contents: fileContents,
|
|
@@ -172,7 +177,8 @@ async function cmdInit(target) {
|
|
|
172
177
|
process.exit(1);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
|
|
180
|
+
if (spinner) spinner.stop(`Detected: ${result.stack || "?"}`);
|
|
181
|
+
else log(`detected: ${result.stack || "?"}`);
|
|
176
182
|
writeFiles(target, result.files || {});
|
|
177
183
|
|
|
178
184
|
// Add to .gitignore
|
|
@@ -310,43 +316,128 @@ async function checkVersion() {
|
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
async function cmdAuthLogin() {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
319
|
+
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
320
|
+
|
|
321
|
+
if (isTTY) {
|
|
322
|
+
// Interactive TUI flow
|
|
323
|
+
const p = require("@clack/prompts");
|
|
324
|
+
p.intro(`${T}0dai${R} authentication`);
|
|
325
|
+
|
|
326
|
+
const method = await p.select({
|
|
327
|
+
message: "How would you like to sign in?",
|
|
328
|
+
options: [
|
|
329
|
+
{ value: "github", label: "GitHub", hint: "recommended" },
|
|
330
|
+
{ value: "google", label: "Google" },
|
|
331
|
+
{ value: "device", label: "Device code", hint: "no browser needed" },
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
if (p.isCancel(method)) { p.cancel("Cancelled"); process.exit(0); }
|
|
335
|
+
|
|
336
|
+
if (method === "github" || method === "google") {
|
|
337
|
+
const url = `${API_URL}/v1/auth/${method}?cli=true`;
|
|
338
|
+
p.log.info(`Opening browser: ${url}`);
|
|
339
|
+
try {
|
|
340
|
+
const { execSync } = require("child_process");
|
|
341
|
+
const cmd = os.platform() === "darwin" ? "open" : os.platform() === "win32" ? "start" : "xdg-open";
|
|
342
|
+
execSync(`${cmd} "${url}"`, { stdio: "ignore" });
|
|
343
|
+
} catch {
|
|
344
|
+
p.log.warn(`Could not open browser. Visit manually:\n ${url}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const s = p.spinner();
|
|
348
|
+
s.start("Waiting for browser confirmation...");
|
|
316
349
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
350
|
+
// Poll auth/status until we get a new token (check every 3s, 5min timeout)
|
|
351
|
+
// For now, ask user to paste token from success page
|
|
352
|
+
s.stop("Browser opened");
|
|
353
|
+
const token = await p.text({
|
|
354
|
+
message: "Paste your token from the success page (or press Enter to skip):",
|
|
355
|
+
placeholder: "0dai_at_...",
|
|
356
|
+
});
|
|
357
|
+
if (token && !p.isCancel(token) && token.startsWith("0dai_at_")) {
|
|
358
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
359
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify({
|
|
360
|
+
access_token: token,
|
|
361
|
+
authenticated_at: new Date().toISOString(),
|
|
362
|
+
}, null, 2) + "\n", { mode: 0o600 });
|
|
363
|
+
// Fetch profile
|
|
364
|
+
const status = await apiCall("/v1/auth/status");
|
|
365
|
+
if (status.email) {
|
|
366
|
+
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
367
|
+
auth.email = status.email;
|
|
368
|
+
auth.plan = status.plan;
|
|
369
|
+
auth.name = status.name;
|
|
370
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2) + "\n", { mode: 0o600 });
|
|
371
|
+
p.outro(`${T}Logged in${R} as ${status.email} (${status.plan} plan)`);
|
|
372
|
+
} else {
|
|
373
|
+
p.outro(`${T}Token saved${R}`);
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
p.log.info("Skipped. You can also use device code flow:");
|
|
341
378
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
379
|
+
|
|
380
|
+
// Device code fallback
|
|
381
|
+
const result = await apiCall("/v1/auth/device", { client_id: "cli" });
|
|
382
|
+
if (result.error) { p.log.error(result.error); process.exit(1); }
|
|
383
|
+
|
|
384
|
+
p.log.step(`Open: ${result.verification_uri}`);
|
|
385
|
+
p.log.step(`Code: ${T}${result.user_code}${R}`);
|
|
386
|
+
|
|
387
|
+
const s = p.spinner();
|
|
388
|
+
s.start("Waiting for confirmation...");
|
|
389
|
+
|
|
390
|
+
const interval = (result.interval || 5) * 1000;
|
|
391
|
+
const deadline = Date.now() + (result.expires_in || 600) * 1000;
|
|
392
|
+
while (Date.now() < deadline) {
|
|
393
|
+
await new Promise(r => setTimeout(r, interval));
|
|
394
|
+
const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
|
|
395
|
+
if (poll.access_token) {
|
|
396
|
+
s.stop("Authorized!");
|
|
397
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
398
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify({
|
|
399
|
+
access_token: poll.access_token, email: poll.email,
|
|
400
|
+
plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
|
|
401
|
+
expires_at: poll.expires_at,
|
|
402
|
+
}, null, 2) + "\n", { mode: 0o600 });
|
|
403
|
+
p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan} plan)`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (poll.error && poll.error !== "authorization_pending") {
|
|
407
|
+
s.stop("Failed");
|
|
408
|
+
p.log.error(poll.error);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
345
411
|
}
|
|
346
|
-
|
|
412
|
+
s.stop("Timed out");
|
|
413
|
+
p.log.error("Try again.");
|
|
414
|
+
process.exit(1);
|
|
415
|
+
|
|
416
|
+
} else {
|
|
417
|
+
// Non-interactive: device code only
|
|
418
|
+
const result = await apiCall("/v1/auth/device", { client_id: "cli" });
|
|
419
|
+
if (result.error) { log(`error: ${result.error}`); process.exit(1); }
|
|
420
|
+
log(`Open: ${result.verification_uri}`);
|
|
421
|
+
log(`Code: ${result.user_code}`);
|
|
422
|
+
log("Waiting...");
|
|
423
|
+
const interval = (result.interval || 5) * 1000;
|
|
424
|
+
const deadline = Date.now() + (result.expires_in || 600) * 1000;
|
|
425
|
+
while (Date.now() < deadline) {
|
|
426
|
+
await new Promise(r => setTimeout(r, interval));
|
|
427
|
+
const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
|
|
428
|
+
if (poll.access_token) {
|
|
429
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
430
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify({
|
|
431
|
+
access_token: poll.access_token, email: poll.email,
|
|
432
|
+
plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
|
|
433
|
+
}, null, 2) + "\n", { mode: 0o600 });
|
|
434
|
+
log(`Logged in as ${poll.email}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
log("Timed out");
|
|
439
|
+
process.exit(1);
|
|
347
440
|
}
|
|
348
|
-
log("Authorization timed out. Try again.");
|
|
349
|
-
process.exit(1);
|
|
350
441
|
}
|
|
351
442
|
|
|
352
443
|
function cmdAuthLogout() {
|
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0dai-dev/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "One config layer for 5 AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider",
|
|
5
5
|
"bin": {
|
|
6
6
|
"0dai": "./bin/0dai.js"
|
|
7
7
|
},
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"ai",
|
|
10
|
+
"agents",
|
|
11
|
+
"claude",
|
|
12
|
+
"codex",
|
|
13
|
+
"gemini",
|
|
14
|
+
"aider",
|
|
15
|
+
"developer-tools",
|
|
16
|
+
"mcp"
|
|
17
|
+
],
|
|
9
18
|
"author": "0dai-dev <dev@0dai.dev>",
|
|
10
19
|
"license": "MIT",
|
|
11
20
|
"repository": {
|
|
@@ -20,5 +29,8 @@
|
|
|
20
29
|
"bin/",
|
|
21
30
|
"lib/",
|
|
22
31
|
"README.md"
|
|
23
|
-
]
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@clack/prompts": "^1.2.0"
|
|
35
|
+
}
|
|
24
36
|
}
|