@0dai-dev/cli 2.1.0 → 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 +142 -58
- 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
|
|
@@ -185,22 +191,16 @@ async function cmdInit(target) {
|
|
|
185
191
|
log(`initialized (${result.file_count || "?"} files)`);
|
|
186
192
|
console.log(" skills: /build /review /status /feedback /bugfix /delegate");
|
|
187
193
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
_cli_version: VERSION,
|
|
199
|
-
_files_generated: result.file_count || 0,
|
|
200
|
-
};
|
|
201
|
-
apiCall("/v1/feedback", { report: autoReport }).catch(() => {});
|
|
202
|
-
console.log(` ${D}usage data sent to improve 0dai (disable with Essential+ plan)${R}`);
|
|
203
|
-
}
|
|
194
|
+
// Send anonymous usage ping (stack + file count, no project data)
|
|
195
|
+
apiCall("/v1/feedback", { report: {
|
|
196
|
+
stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
|
|
197
|
+
_cli_version: VERSION, _files_generated: result.file_count || 0,
|
|
198
|
+
}}).catch(() => {});
|
|
199
|
+
|
|
200
|
+
// Encourage feedback
|
|
201
|
+
console.log(`\n ${T}Tip:${R} Send feedback to earn +5 init/day for 7 days:`);
|
|
202
|
+
console.log(` ${D}0dai feedback log --type positive --detail "what worked"${R}`);
|
|
203
|
+
console.log(` ${D}0dai feedback push${R}`);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
async function cmdSync(target) {
|
|
@@ -316,43 +316,128 @@ async function checkVersion() {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
async function cmdAuthLogin() {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
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...");
|
|
349
|
+
|
|
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:");
|
|
378
|
+
}
|
|
322
379
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
+
}
|
|
347
411
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
}
|
|
351
437
|
}
|
|
352
|
-
|
|
438
|
+
log("Timed out");
|
|
439
|
+
process.exit(1);
|
|
353
440
|
}
|
|
354
|
-
log("Authorization timed out. Try again.");
|
|
355
|
-
process.exit(1);
|
|
356
441
|
}
|
|
357
442
|
|
|
358
443
|
function cmdAuthLogout() {
|
|
@@ -396,10 +481,9 @@ async function cmdFeedbackPush(target) {
|
|
|
396
481
|
for (const report of reports) {
|
|
397
482
|
log(`pushing: ${report.project || "?"} (${report.verdict || "?"})`);
|
|
398
483
|
const result = await apiCall("/v1/feedback", { report });
|
|
399
|
-
if (result.
|
|
400
|
-
log(`issue
|
|
401
|
-
|
|
402
|
-
log("received by server");
|
|
484
|
+
if (result.received) {
|
|
485
|
+
log(`received${result.issue ? `: ${result.issue}` : ""}`);
|
|
486
|
+
if (result.bonus) log(`${T}bonus:${R} ${result.bonus}`);
|
|
403
487
|
} else {
|
|
404
488
|
log(`error: ${result.error || "unknown"}`);
|
|
405
489
|
}
|
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
|
}
|