@0dai-dev/cli 3.10.1 → 4.1.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/README.md +4 -2
- package/bin/0dai.js +153 -2154
- package/lib/commands/audit.js +129 -0
- package/lib/commands/auth.js +241 -0
- package/lib/commands/detect.js +31 -0
- package/lib/commands/doctor.js +194 -0
- package/lib/commands/experience.js +65 -0
- package/lib/commands/feedback.js +92 -0
- package/lib/commands/graph.js +270 -0
- package/lib/commands/init.js +327 -0
- package/lib/commands/metrics.js +145 -0
- package/lib/commands/models.js +90 -0
- package/lib/commands/portfolio.js +96 -0
- package/lib/commands/reflect.js +211 -0
- package/lib/commands/report.js +29 -0
- package/lib/commands/run.js +77 -0
- package/lib/commands/session.js +61 -0
- package/lib/commands/status.js +69 -0
- package/lib/commands/swarm.js +199 -0
- package/lib/commands/update.js +69 -0
- package/lib/commands/validate.js +71 -0
- package/lib/commands/watch.js +118 -0
- package/lib/commands/workspace.js +296 -0
- package/lib/onboarding.js +171 -0
- package/lib/shared.js +360 -0
- package/lib/utils/auth.js +142 -0
- package/lib/utils/constants.js +76 -0
- package/lib/utils/identity.js +147 -0
- package/lib/utils/plan.js +73 -0
- package/lib/wizard.js +311 -0
- package/package.json +13 -4
- package/scripts/postinstall.js +29 -0
package/lib/shared.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared configuration and utilities for 0dai CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2: Thin re-export layer. Logic extracted into lib/utils/*.
|
|
5
|
+
* All command modules require from here — API is unchanged.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const https = require("https");
|
|
10
|
+
const http = require("http");
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
const { spawnSync } = require("child_process");
|
|
15
|
+
|
|
16
|
+
const VERSION = require("../package.json").version;
|
|
17
|
+
|
|
18
|
+
// --- Re-export from extracted modules ---
|
|
19
|
+
const { SUPPORTED_CLIS, MANIFEST_FILES, PROBE_DIRS, SETTINGS_PRESERVE_FIELDS } = require("./utils/constants");
|
|
20
|
+
const {
|
|
21
|
+
deviceFingerprint, registerProject, projectIdFor,
|
|
22
|
+
getGitRemoteOrigin, inferProjectName, detectStackHint,
|
|
23
|
+
collectMetadata, buildProjectIdentity,
|
|
24
|
+
} = require("./utils/identity");
|
|
25
|
+
const { PLAN_LEVELS, _detectPlanLocal, requirePlan, getSwarmQuotaLocal } = require("./utils/plan");
|
|
26
|
+
|
|
27
|
+
// --- Colors & logging ---
|
|
28
|
+
function _validateApiUrl(url) {
|
|
29
|
+
const DEFAULT = "https://api.0dai.dev";
|
|
30
|
+
if (!url) return DEFAULT;
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(url);
|
|
33
|
+
if (u.protocol === "https:") return url;
|
|
34
|
+
if (u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1")) return url;
|
|
35
|
+
return DEFAULT;
|
|
36
|
+
} catch { return DEFAULT; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const API_URL = _validateApiUrl(process.env.ODAI_API_URL);
|
|
40
|
+
const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : "";
|
|
41
|
+
const R = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
42
|
+
const D = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
43
|
+
const E = process.stdout.isTTY ? "\x1b[31m" : "";
|
|
44
|
+
const G = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
45
|
+
const W = process.stdout.isTTY ? "\x1b[33m" : "";
|
|
46
|
+
const log = (msg) => console.log(`${T}[0dai]${R} ${msg}`);
|
|
47
|
+
const CONFIG_DIR = path.join(os.homedir(), ".0dai");
|
|
48
|
+
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
49
|
+
const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
|
|
50
|
+
const PROJECTS_FILE = path.join(CONFIG_DIR, "projects.json");
|
|
51
|
+
const MANAGED_BEGIN = "<!-- 0dai:managed:begin -->";
|
|
52
|
+
const MANAGED_END = "<!-- 0dai:managed:end -->";
|
|
53
|
+
const LEGACY_MANAGED_BEGIN = "<!-- zerodayai:managed:begin -->";
|
|
54
|
+
const LEGACY_MANAGED_END = "<!-- zerodayai:managed:end -->";
|
|
55
|
+
|
|
56
|
+
// --- API ---
|
|
57
|
+
function apiCall(endpoint, data) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const url = new URL(endpoint, API_URL);
|
|
60
|
+
const mod = url.protocol === "https:" ? https : http;
|
|
61
|
+
const body = data ? JSON.stringify(data) : null;
|
|
62
|
+
const headers = {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"X-Device-ID": deviceFingerprint(),
|
|
65
|
+
"X-CLI-Version": VERSION,
|
|
66
|
+
"X-Client-Channel": "npm",
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
70
|
+
const token = auth.api_key || auth.access_token || auth.token;
|
|
71
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
72
|
+
} catch {}
|
|
73
|
+
const opts = {
|
|
74
|
+
hostname: url.hostname,
|
|
75
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
76
|
+
path: url.pathname,
|
|
77
|
+
method: body ? "POST" : "GET",
|
|
78
|
+
headers,
|
|
79
|
+
timeout: 60000,
|
|
80
|
+
};
|
|
81
|
+
if (body) opts.headers["Content-Length"] = Buffer.byteLength(body);
|
|
82
|
+
const req = mod.request(opts, (res) => {
|
|
83
|
+
let chunks = [];
|
|
84
|
+
res.on("data", (c) => chunks.push(c));
|
|
85
|
+
res.on("end", () => {
|
|
86
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
|
|
87
|
+
catch { resolve({ error: `HTTP ${res.statusCode}` }); }
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
req.on("error", (e) => resolve({ error: `${e.message}. Is ${API_URL} reachable?` }));
|
|
91
|
+
req.on("timeout", () => { req.destroy(); resolve({ error: "timeout" }); });
|
|
92
|
+
if (body) req.write(body);
|
|
93
|
+
req.end();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Auth State ---
|
|
98
|
+
function loadAuthState() {
|
|
99
|
+
try { return JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); }
|
|
100
|
+
catch { return null; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function saveAuthState(next) {
|
|
104
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
105
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function updateAuthState(patch) {
|
|
109
|
+
const current = loadAuthState() || {};
|
|
110
|
+
saveAuthState({ ...current, ...patch });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function fetchAuthStatus() {
|
|
114
|
+
const status = await apiCall("/v1/auth/status");
|
|
115
|
+
if (status && !status.error && status.email) {
|
|
116
|
+
updateAuthState({
|
|
117
|
+
email: status.email, plan: status.plan || "free",
|
|
118
|
+
name: status.name || "", license: status.license || {},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return status;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function makeEnsureAuthenticated(cmdAuthLogin) {
|
|
125
|
+
return async function ensureAuthenticated(actionLabel) {
|
|
126
|
+
let auth = loadAuthState();
|
|
127
|
+
if (!auth || !(auth.api_key || auth.access_token || auth.token)) {
|
|
128
|
+
if (cmdAuthLogin && process.stdout.isTTY && process.stdin.isTTY) {
|
|
129
|
+
log(`${actionLabel} requires 0dai account auth`);
|
|
130
|
+
await cmdAuthLogin();
|
|
131
|
+
auth = loadAuthState();
|
|
132
|
+
} else {
|
|
133
|
+
log(`authentication required for ${actionLabel}`);
|
|
134
|
+
console.log(` ${D}Run: 0dai auth login${R}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const status = await fetchAuthStatus();
|
|
139
|
+
if (status.error) {
|
|
140
|
+
log(`${actionLabel} requires a valid 0dai session`);
|
|
141
|
+
console.log(` ${D}Run: 0dai auth login${R}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
return status;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function ensureLicenseActivation() {
|
|
149
|
+
const status = await apiCall("/v1/licenses/status");
|
|
150
|
+
if (!status.error && status.license && status.license.status === "active") {
|
|
151
|
+
updateAuthState({ license: status.license });
|
|
152
|
+
return status.license;
|
|
153
|
+
}
|
|
154
|
+
log("activating free open-source license...");
|
|
155
|
+
const activated = await apiCall("/v1/licenses/activate", {
|
|
156
|
+
device_id: deviceFingerprint(), cli_version: VERSION, channel: "npm",
|
|
157
|
+
});
|
|
158
|
+
if (activated.error || !activated.license) {
|
|
159
|
+
log(`error: ${activated.error || "activation failed"}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
updateAuthState({ license: activated.license });
|
|
163
|
+
return activated.license;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Project Heartbeat ---
|
|
167
|
+
async function sendProjectHeartbeat(identity, result, extra = {}) {
|
|
168
|
+
return apiCall("/v1/projects/heartbeat", {
|
|
169
|
+
project_id: identity.project_id, stack: result.stack || identity.stack || "unknown",
|
|
170
|
+
cli_version: VERSION, activation_status: "active", binding_status: "bound",
|
|
171
|
+
runtime_sessions: 0, swarm_active: 0, swarm_done: 0, channel: "npm", ...extra,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- File Writing ---
|
|
176
|
+
function mergeSettingsJson(existing, incoming) {
|
|
177
|
+
try {
|
|
178
|
+
const base = JSON.parse(incoming);
|
|
179
|
+
const user = JSON.parse(existing);
|
|
180
|
+
for (const field of SETTINGS_PRESERVE_FIELDS) {
|
|
181
|
+
if (field in user && user[field] !== base[field]) base[field] = user[field];
|
|
182
|
+
}
|
|
183
|
+
return JSON.stringify(base, null, 2) + "\n";
|
|
184
|
+
} catch { return incoming; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mergeManagedMarkdown(existing, incoming) {
|
|
188
|
+
let src = incoming;
|
|
189
|
+
if (src.startsWith("# managed: true")) {
|
|
190
|
+
src = src.split("\n").slice(1).join("\n").trimStart();
|
|
191
|
+
}
|
|
192
|
+
const managedBody = `${MANAGED_BEGIN}\n${src.trim()}\n${MANAGED_END}\n`;
|
|
193
|
+
if (existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) {
|
|
194
|
+
const start = existing.indexOf(MANAGED_BEGIN);
|
|
195
|
+
const finish = existing.indexOf(MANAGED_END) + MANAGED_END.length;
|
|
196
|
+
return existing.slice(0, start) + managedBody + existing.slice(finish);
|
|
197
|
+
}
|
|
198
|
+
if (existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END)) {
|
|
199
|
+
const finish = existing.indexOf(LEGACY_MANAGED_END) + LEGACY_MANAGED_END.length;
|
|
200
|
+
const rest = existing.slice(finish).trimStart();
|
|
201
|
+
return rest ? `${managedBody}\n${rest}` : managedBody;
|
|
202
|
+
}
|
|
203
|
+
return `${managedBody}\n${existing}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function contentLooksManaged(existing) {
|
|
207
|
+
return existing.includes("managed: true") || existing.includes('"managed": true') ||
|
|
208
|
+
(existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) ||
|
|
209
|
+
(existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writeFiles(target, files) {
|
|
213
|
+
let created = 0, updated = 0, unchanged = 0, merged = 0, skipped = 0;
|
|
214
|
+
const targetResolved = path.resolve(target);
|
|
215
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
216
|
+
if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
|
|
217
|
+
skipped++; continue;
|
|
218
|
+
}
|
|
219
|
+
const p = path.resolve(targetResolved, rel);
|
|
220
|
+
if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) { skipped++; continue; }
|
|
221
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
222
|
+
let finalContent = content;
|
|
223
|
+
if (fs.existsSync(p)) {
|
|
224
|
+
const existing = fs.readFileSync(p, "utf8");
|
|
225
|
+
if (existing === content) { unchanged++; continue; }
|
|
226
|
+
if (rel.endsWith("settings.json")) { finalContent = mergeSettingsJson(existing, content); merged++; }
|
|
227
|
+
else if (rel === "AGENTS.md") {
|
|
228
|
+
if (existing.includes("managed: false")) { unchanged++; continue; }
|
|
229
|
+
const backupDir = path.join(target, "ai", ".backups");
|
|
230
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
231
|
+
fs.writeFileSync(path.join(backupDir, "AGENTS.md.bak"), existing, "utf8");
|
|
232
|
+
updated++;
|
|
233
|
+
} else { updated++; }
|
|
234
|
+
} else { created++; }
|
|
235
|
+
fs.writeFileSync(p, finalContent, "utf8");
|
|
236
|
+
}
|
|
237
|
+
const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
|
|
238
|
+
if (merged) parts.push(`${merged} merged`);
|
|
239
|
+
if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
|
|
240
|
+
log(parts.join(", "));
|
|
241
|
+
return created + updated;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function writeManagedFiles(target, files) {
|
|
245
|
+
let created = 0, updated = 0, unchanged = 0, merged = 0, staged = 0, skipped = 0;
|
|
246
|
+
const targetResolved = path.resolve(target);
|
|
247
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
248
|
+
if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
|
|
249
|
+
skipped++; continue;
|
|
250
|
+
}
|
|
251
|
+
const p = path.resolve(targetResolved, rel);
|
|
252
|
+
if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) { skipped++; continue; }
|
|
253
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
254
|
+
if (!fs.existsSync(p)) {
|
|
255
|
+
fs.writeFileSync(p, content, "utf8");
|
|
256
|
+
created++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const existing = fs.readFileSync(p, "utf8");
|
|
260
|
+
if (existing === content) {
|
|
261
|
+
unchanged++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (rel.endsWith("settings.json")) {
|
|
266
|
+
fs.writeFileSync(p, mergeSettingsJson(existing, content), "utf8");
|
|
267
|
+
merged++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (rel === "AGENTS.md" || rel.endsWith("/CLAUDE.md")) {
|
|
271
|
+
fs.writeFileSync(p, mergeManagedMarkdown(existing, content), "utf8");
|
|
272
|
+
merged++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!contentLooksManaged(existing)) {
|
|
276
|
+
fs.writeFileSync(`${p}.generated`, content, "utf8");
|
|
277
|
+
staged++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fs.writeFileSync(p, content, "utf8");
|
|
282
|
+
updated++;
|
|
283
|
+
}
|
|
284
|
+
const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
|
|
285
|
+
if (merged) parts.push(`${merged} merged`);
|
|
286
|
+
if (staged) parts.push(`${staged} staged`);
|
|
287
|
+
if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
|
|
288
|
+
log(parts.join(", "));
|
|
289
|
+
return created + updated + merged;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// --- Repo Script Lookup ---
|
|
293
|
+
function findRepoScript(target, scriptName) {
|
|
294
|
+
const candidates = [
|
|
295
|
+
path.join(target, "scripts", scriptName),
|
|
296
|
+
path.join(process.cwd(), "scripts", scriptName),
|
|
297
|
+
path.join(__dirname, "..", "..", "..", "scripts", scriptName),
|
|
298
|
+
];
|
|
299
|
+
for (const c of candidates) { if (fs.existsSync(c)) return c; }
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- Version Check ---
|
|
304
|
+
async function checkVersion() {
|
|
305
|
+
try {
|
|
306
|
+
const intervalSec = parseInt(process.env.ODAI_UPDATE_CHECK_INTERVAL || "3600");
|
|
307
|
+
let lastCheck = 0;
|
|
308
|
+
try { lastCheck = parseFloat(fs.readFileSync(VERSION_CHECK_FILE, "utf8")); } catch {}
|
|
309
|
+
if (Date.now() / 1000 - lastCheck < intervalSec) return;
|
|
310
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
311
|
+
fs.writeFileSync(VERSION_CHECK_FILE, String(Date.now() / 1000));
|
|
312
|
+
const result = await apiCall("/v1/version");
|
|
313
|
+
if (result.version && result.version !== VERSION) {
|
|
314
|
+
const cmp = (a, b) => { const [a1,a2,a3] = a.split(".").map(Number); const [b1,b2,b3] = b.split(".").map(Number); return a1 - b1 || a2 - b2 || a3 - b3; };
|
|
315
|
+
if (cmp(result.version, VERSION) > 0) {
|
|
316
|
+
log(`Update available: ${VERSION} → ${result.version}`);
|
|
317
|
+
console.log(` Run: npm update -g @0dai-dev/cli\n`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Experience ---
|
|
324
|
+
function recordExperienceEvent(target, payload) {
|
|
325
|
+
const script = findRepoScript(target, "experience_pipeline.py");
|
|
326
|
+
if (!script) return;
|
|
327
|
+
try {
|
|
328
|
+
spawnSync("python3", [script, "record-json", "--target", target], {
|
|
329
|
+
input: JSON.stringify(payload), stdio: ["pipe", "ignore", "ignore"],
|
|
330
|
+
});
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- Re-export everything for backward compatibility ---
|
|
335
|
+
module.exports = {
|
|
336
|
+
// Core
|
|
337
|
+
VERSION, API_URL, T, R, D, E, G, W,
|
|
338
|
+
log, CONFIG_DIR, AUTH_FILE, VERSION_CHECK_FILE, PROJECTS_FILE,
|
|
339
|
+
// Constants
|
|
340
|
+
PLAN_LEVELS, MANIFEST_FILES, PROBE_DIRS, SUPPORTED_CLIS, SETTINGS_PRESERVE_FIELDS,
|
|
341
|
+
// API
|
|
342
|
+
apiCall,
|
|
343
|
+
// Auth
|
|
344
|
+
loadAuthState, saveAuthState, updateAuthState,
|
|
345
|
+
fetchAuthStatus, makeEnsureAuthenticated, ensureLicenseActivation,
|
|
346
|
+
// Identity
|
|
347
|
+
deviceFingerprint, registerProject, projectIdFor,
|
|
348
|
+
getGitRemoteOrigin, inferProjectName, detectStackHint,
|
|
349
|
+
collectMetadata, buildProjectIdentity,
|
|
350
|
+
// Plan / Tier
|
|
351
|
+
_detectPlanLocal, requirePlan, getSwarmQuotaLocal,
|
|
352
|
+
// Project
|
|
353
|
+
sendProjectHeartbeat, recordExperienceEvent,
|
|
354
|
+
// Files
|
|
355
|
+
mergeSettingsJson, mergeManagedMarkdown, writeFiles, writeManagedFiles, findRepoScript,
|
|
356
|
+
// Version
|
|
357
|
+
checkVersion,
|
|
358
|
+
// Re-exports for convenience
|
|
359
|
+
spawnSync, fs, path, os, https, http,
|
|
360
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication and license management utilities.
|
|
3
|
+
* Extracted from shared.js to reduce module size.
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), ".0dai");
|
|
12
|
+
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
13
|
+
|
|
14
|
+
function loadAuthState() {
|
|
15
|
+
try { return JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); }
|
|
16
|
+
catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function saveAuthState(next) {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
21
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function updateAuthState(patch) {
|
|
25
|
+
const current = loadAuthState() || {};
|
|
26
|
+
saveAuthState({ ...current, ...patch });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function deviceFingerprint() {
|
|
30
|
+
const crypto = require("crypto");
|
|
31
|
+
const parts = [os.hostname(), os.userInfo().username, os.platform(), os.arch(), os.cpus().length.toString(), os.totalmem().toString()];
|
|
32
|
+
try {
|
|
33
|
+
if (os.platform() === "linux") parts.push(fs.readFileSync("/etc/machine-id", "utf8").trim());
|
|
34
|
+
} catch {}
|
|
35
|
+
return crypto.createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 32);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function apiCall(endpoint, data, API_URL, VERSION) {
|
|
39
|
+
const https = require("https");
|
|
40
|
+
const http = require("http");
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const url = new URL(endpoint, API_URL);
|
|
43
|
+
const mod = url.protocol === "https:" ? https : http;
|
|
44
|
+
const body = data ? JSON.stringify(data) : null;
|
|
45
|
+
const headers = {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
"X-Device-ID": deviceFingerprint(),
|
|
48
|
+
"X-CLI-Version": VERSION,
|
|
49
|
+
"X-Client-Channel": "npm",
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
53
|
+
const token = auth.api_key || auth.access_token || auth.token;
|
|
54
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
55
|
+
} catch {}
|
|
56
|
+
const opts = {
|
|
57
|
+
hostname: url.hostname,
|
|
58
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
59
|
+
path: url.pathname,
|
|
60
|
+
method: body ? "POST" : "GET",
|
|
61
|
+
headers,
|
|
62
|
+
timeout: 60000,
|
|
63
|
+
};
|
|
64
|
+
if (body) opts.headers["Content-Length"] = Buffer.byteLength(body);
|
|
65
|
+
const req = mod.request(opts, (res) => {
|
|
66
|
+
let chunks = [];
|
|
67
|
+
res.on("data", (c) => chunks.push(c));
|
|
68
|
+
res.on("end", () => {
|
|
69
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
|
|
70
|
+
catch { resolve({ error: `HTTP ${res.statusCode}` }); }
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
req.on("error", (e) => resolve({ error: `${e.message}. Is ${API_URL} reachable?` }));
|
|
74
|
+
req.on("timeout", () => { req.destroy(); resolve({ error: "timeout" }); });
|
|
75
|
+
if (body) req.write(body);
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function fetchAuthStatus(apiCallFn, API_URL) {
|
|
81
|
+
const status = await apiCallFn("/v1/auth/status", undefined, API_URL);
|
|
82
|
+
if (status && !status.error && status.email) {
|
|
83
|
+
updateAuthState({
|
|
84
|
+
email: status.email,
|
|
85
|
+
plan: status.plan || "free",
|
|
86
|
+
name: status.name || "",
|
|
87
|
+
license: status.license || {},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return status;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeEnsureAuthenticated(cmdAuthLogin, apiCallFn, API_URL, log, D, R) {
|
|
94
|
+
return async function ensureAuthenticated(actionLabel) {
|
|
95
|
+
let auth = loadAuthState();
|
|
96
|
+
if (!auth || !(auth.api_key || auth.access_token || auth.token)) {
|
|
97
|
+
if (cmdAuthLogin && process.stdout.isTTY && process.stdin.isTTY) {
|
|
98
|
+
log(`${actionLabel} requires 0dai account auth`);
|
|
99
|
+
await cmdAuthLogin();
|
|
100
|
+
auth = loadAuthState();
|
|
101
|
+
} else {
|
|
102
|
+
log(`authentication required for ${actionLabel}`);
|
|
103
|
+
console.log(` ${D}Run: 0dai auth login${R}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const status = await fetchAuthStatus(apiCallFn, API_URL);
|
|
108
|
+
if (status.error) {
|
|
109
|
+
log(`${actionLabel} requires a valid 0dai session`);
|
|
110
|
+
console.log(` ${D}Run: 0dai auth login${R}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
return status;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function ensureLicenseActivation(apiCallFn, API_URL, VERSION, log) {
|
|
118
|
+
const status = await apiCallFn("/v1/licenses/status", undefined, API_URL);
|
|
119
|
+
if (!status.error && status.license && status.license.status === "active") {
|
|
120
|
+
updateAuthState({ license: status.license });
|
|
121
|
+
return status.license;
|
|
122
|
+
}
|
|
123
|
+
log("activating free open-source license...");
|
|
124
|
+
const activated = await apiCallFn("/v1/licenses/activate", {
|
|
125
|
+
device_id: deviceFingerprint(),
|
|
126
|
+
cli_version: VERSION,
|
|
127
|
+
channel: "npm",
|
|
128
|
+
}, API_URL);
|
|
129
|
+
if (activated.error || !activated.license) {
|
|
130
|
+
log(`error: ${activated.error || "activation failed"}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
updateAuthState({ license: activated.license });
|
|
134
|
+
return activated.license;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
CONFIG_DIR, AUTH_FILE,
|
|
139
|
+
loadAuthState, saveAuthState, updateAuthState,
|
|
140
|
+
deviceFingerprint, apiCall, fetchAuthStatus,
|
|
141
|
+
makeEnsureAuthenticated, ensureLicenseActivation,
|
|
142
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants extracted from shared.js.
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_CLIS = [
|
|
7
|
+
{
|
|
8
|
+
name: "claude", bin: "claude",
|
|
9
|
+
pkg: "@anthropic-ai/claude-code", pkgType: "npm",
|
|
10
|
+
install: "npm i -g @anthropic-ai/claude-code",
|
|
11
|
+
altAuth: "Pro/Team subscription",
|
|
12
|
+
agentFiles: [".claude/settings.json", ".claude/CLAUDE.md", ".mcp.json"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "codex", bin: "codex",
|
|
16
|
+
pkg: "@openai/codex", pkgType: "npm",
|
|
17
|
+
install: "npm i -g @openai/codex",
|
|
18
|
+
altAuth: "ChatGPT Pro subscription",
|
|
19
|
+
agentFiles: ["AGENTS.md", ".codex/config.toml"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "opencode", bin: "opencode",
|
|
23
|
+
pkg: null, pkgType: "go",
|
|
24
|
+
install: "go install github.com/nichochar/opencode@latest",
|
|
25
|
+
altAuth: null,
|
|
26
|
+
agentFiles: ["opencode.json"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "gemini", bin: "gemini",
|
|
30
|
+
pkg: "@google/gemini-cli", pkgType: "npm",
|
|
31
|
+
install: "npm i -g @google/gemini-cli",
|
|
32
|
+
altAuth: null,
|
|
33
|
+
agentFiles: null,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "aider", bin: "aider",
|
|
37
|
+
pkg: "aider-chat", pkgType: "pip",
|
|
38
|
+
install: "pip install aider-chat",
|
|
39
|
+
altAuth: null,
|
|
40
|
+
agentFiles: null,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "qoder", bin: "qodercli",
|
|
44
|
+
pkg: "@qoder-ai/qodercli", pkgType: "npm",
|
|
45
|
+
install: "npm i -g @qoder-ai/qodercli",
|
|
46
|
+
altAuth: null,
|
|
47
|
+
agentFiles: [".qoder/settings.json"],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const MANIFEST_FILES = [
|
|
52
|
+
"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb", "tsconfig.json",
|
|
53
|
+
"next.config.js", "next.config.mjs", "next.config.ts",
|
|
54
|
+
"vite.config.js", "vite.config.ts", "vite.config.mjs",
|
|
55
|
+
"vue.config.js", "nuxt.config.js", "nuxt.config.ts",
|
|
56
|
+
"svelte.config.js", "astro.config.mjs", "astro.config.ts",
|
|
57
|
+
"remix.config.js", "angular.json",
|
|
58
|
+
"go.mod", "go.sum", "pyproject.toml", "requirements.txt", "requirements-dev.txt", "setup.py",
|
|
59
|
+
"poetry.lock", "Pipfile", "Pipfile.lock",
|
|
60
|
+
"pubspec.yaml", "Cargo.toml", "Cargo.lock", "pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts",
|
|
61
|
+
"Gemfile", "composer.json",
|
|
62
|
+
"Makefile", "docker-compose.yml", "docker-compose.yaml", "Dockerfile",
|
|
63
|
+
"pnpm-workspace.yaml", "lerna.json", "turbo.json", "nx.json",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const PROBE_DIRS = [
|
|
67
|
+
"src", "lib", "app", "apps", "packages", "services",
|
|
68
|
+
"cmd", "internal", "web", "frontend", "backend",
|
|
69
|
+
"tests", "test", "spec", "__tests__",
|
|
70
|
+
"infra", "deploy", "docker", ".github",
|
|
71
|
+
"android", "ios", "linux", "macos", "windows",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const SETTINGS_PRESERVE_FIELDS = ["model", "permissionMode", "effortLevel"];
|
|
75
|
+
|
|
76
|
+
module.exports = { SUPPORTED_CLIS, MANIFEST_FILES, PROBE_DIRS, SETTINGS_PRESERVE_FIELDS };
|