@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.
Files changed (2) hide show
  1. package/bin/0dai.js +129 -38
  2. 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.1.1";
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
- log("collecting project metadata...");
156
- const { projectFiles, fileContents, clis } = collectMetadata(target);
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
- log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
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
- log(`detected: ${result.stack || "?"}`);
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
- // Step 1: request device code
314
- const result = await apiCall("/v1/auth/device", { client_id: "cli" });
315
- if (result.error) { log(`error: ${result.error}`); process.exit(1); }
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
- log(`Open this URL in your browser:\n`);
318
- console.log(` ${result.verification_uri}\n`);
319
- log(`Enter code: ${result.user_code}\n`);
320
- log("Waiting for authorization...");
321
-
322
- // Step 2: poll for token
323
- const interval = (result.interval || 5) * 1000;
324
- const deadline = Date.now() + (result.expires_in || 600) * 1000;
325
-
326
- while (Date.now() < deadline) {
327
- await new Promise(r => setTimeout(r, interval));
328
- const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
329
- if (poll.access_token) {
330
- // Save token
331
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
332
- fs.writeFileSync(AUTH_FILE, JSON.stringify({
333
- access_token: poll.access_token,
334
- email: poll.email,
335
- plan: poll.plan || "free",
336
- authenticated_at: new Date().toISOString(),
337
- expires_at: poll.expires_at,
338
- }, null, 2) + "\n", { mode: 0o600 });
339
- log(`Logged in as ${poll.email} (${poll.plan} plan)`);
340
- return;
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
- if (poll.error && poll.error !== "authorization_pending") {
343
- log(`${poll.error}`);
344
- process.exit(1);
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
- process.stdout.write(".");
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.1.1",
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": ["ai", "agents", "claude", "codex", "gemini", "aider", "developer-tools", "mcp"],
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
  }