@0dai-dev/cli 3.10.0 → 4.0.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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Plan detection and quota management.
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 PLAN_LEVELS = { trial: 0, free: 0, essential: 1, pro: 2, team: 3, enterprise: 4 };
12
+
13
+ function _detectPlanLocal(target) {
14
+ const projYaml = path.join(target, "ai", "manifest", "project.yaml");
15
+ if (fs.existsSync(projYaml)) {
16
+ try {
17
+ const text = fs.readFileSync(projYaml, "utf8");
18
+ for (const line of text.split("\n")) {
19
+ if (line.startsWith("plan:")) {
20
+ const plan = line.split(":")[1].trim().toLowerCase();
21
+ if (PLAN_LEVELS[plan] !== undefined) return plan;
22
+ }
23
+ }
24
+ } catch {}
25
+ }
26
+ const projectsFile = path.join(os.homedir(), ".0dai", "projects.json");
27
+ if (fs.existsSync(projectsFile)) {
28
+ try {
29
+ const projects = JSON.parse(fs.readFileSync(projectsFile, "utf8"));
30
+ const targetResolved = path.resolve(target);
31
+ for (const p of (projects.projects || [])) {
32
+ if (path.resolve(p.path || "") === targetResolved) {
33
+ const plan = (p.plan || "").toLowerCase();
34
+ if (PLAN_LEVELS[plan] !== undefined) return plan;
35
+ }
36
+ }
37
+ } catch {}
38
+ }
39
+ return "free";
40
+ }
41
+
42
+ function requirePlan(requiredPlan, featureName, target) {
43
+ const plan = _detectPlanLocal(target || process.cwd());
44
+ if ((PLAN_LEVELS[plan] || 0) >= (PLAN_LEVELS[requiredPlan] || 0)) return null;
45
+ return {
46
+ error: `${featureName} requires ${requiredPlan.charAt(0).toUpperCase() + requiredPlan.slice(1)} plan ($15/mo).`,
47
+ hint: "Run: 0dai upgrade",
48
+ current_plan: plan,
49
+ };
50
+ }
51
+
52
+ function getSwarmQuotaLocal(target) {
53
+ const plan = _detectPlanLocal(target);
54
+ const limits = { free: 0, pro: 50, team: 200, enterprise: 999999 };
55
+ const dailyLimit = limits[plan] || 0;
56
+ const budgetPath = path.join(target, "ai", "swarm", "budget.json");
57
+ let usedToday = 0;
58
+ if (fs.existsSync(budgetPath)) {
59
+ try {
60
+ const budget = JSON.parse(fs.readFileSync(budgetPath, "utf8"));
61
+ const today = new Date().toISOString().slice(0, 10);
62
+ usedToday = (budget.daily_tasks || {})[today] || 0;
63
+ } catch {}
64
+ }
65
+ return { plan, daily_limit: dailyLimit, used_today: usedToday, remaining: Math.max(0, dailyLimit - usedToday) };
66
+ }
67
+
68
+ module.exports = {
69
+ PLAN_LEVELS,
70
+ _detectPlanLocal,
71
+ requirePlan,
72
+ getSwarmQuotaLocal,
73
+ };
package/lib/wizard.js ADDED
@@ -0,0 +1,311 @@
1
+ /**
2
+ * First-run interactive wizard for 0dai.
3
+ *
4
+ * Guides new users through: agent selection → auth → stack detection →
5
+ * config generation → what's next. Safe to Ctrl+C at any step.
6
+ */
7
+ "use strict";
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const readline = require("readline");
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Helpers
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const AGENTS = [
18
+ { id: "claude", name: "Claude Code" },
19
+ { id: "codex", name: "Codex" },
20
+ { id: "gemini", name: "Gemini CLI" },
21
+ { id: "aider", name: "Aider" },
22
+ { id: "opencode", name: "OpenCode" },
23
+ ];
24
+
25
+ function isInteractive() {
26
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
27
+ }
28
+
29
+ function needsWizard(target) {
30
+ return !fs.existsSync(path.join(target, "ai", "VERSION"));
31
+ }
32
+
33
+ function ask(rl, question, defaultVal) {
34
+ return new Promise((resolve) => {
35
+ rl.question(question, (answer) => {
36
+ resolve(answer.trim() || defaultVal || "");
37
+ });
38
+ });
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Wizard steps
43
+ // ---------------------------------------------------------------------------
44
+
45
+ async function stepAgent(rl) {
46
+ console.log("");
47
+ console.log(" Welcome to 0dai! Let's set up your project.");
48
+ console.log("");
49
+ console.log(" Which AI coding agent do you use most?");
50
+ console.log("");
51
+ AGENTS.forEach((a, i) => {
52
+ console.log(` ${i + 1}) ${a.name}`);
53
+ });
54
+ console.log(` 6) Multiple / all of them`);
55
+ console.log("");
56
+
57
+ const choice = await ask(rl, " Choose [1-6, default 6]: ", "6");
58
+ const idx = parseInt(choice, 10);
59
+ if (idx >= 1 && idx <= 5) {
60
+ console.log(` → Primary agent: ${AGENTS[idx - 1].name}`);
61
+ return AGENTS[idx - 1].id;
62
+ }
63
+ console.log(" → All agents will be configured.");
64
+ return "all";
65
+ }
66
+
67
+ async function stepAuth(rl) {
68
+ console.log("");
69
+ console.log(" 0dai works in two modes:");
70
+ console.log("");
71
+ console.log(" Local mode — generate configs offline, no account needed");
72
+ console.log(" Cloud mode — full graph, swarm, roaming, 55 MCP tools");
73
+ console.log("");
74
+
75
+ const answer = await ask(rl, " Sign in now? [Y/n]: ", "y");
76
+ if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
77
+ console.log(" → Local mode. Sign in later: 0dai auth login");
78
+ return "local";
79
+ }
80
+ console.log(" → Cloud mode. Run: 0dai auth login");
81
+ console.log(" (Authentication happens after wizard completes.)");
82
+ return "cloud";
83
+ }
84
+
85
+ async function stepDetect(rl, target) {
86
+ console.log("");
87
+ console.log(" Detecting project stack...");
88
+ console.log("");
89
+
90
+ const detected = [];
91
+ const checks = [
92
+ { file: "package.json", label: "Node.js" },
93
+ { file: "next.config.js", label: "Next.js" },
94
+ { file: "next.config.ts", label: "Next.js" },
95
+ { file: "next.config.mjs", label: "Next.js" },
96
+ { file: "tsconfig.json", label: "TypeScript" },
97
+ { file: "pyproject.toml", label: "Python" },
98
+ { file: "go.mod", label: "Go" },
99
+ { file: "Cargo.toml", label: "Rust" },
100
+ { file: "pubspec.yaml", label: "Flutter/Dart" },
101
+ { file: "Gemfile", label: "Ruby" },
102
+ { file: "prisma/schema.prisma", label: "Prisma" },
103
+ { file: "docker-compose.yml", label: "Docker" },
104
+ { file: "docker-compose.yaml", label: "Docker" },
105
+ { file: ".github/workflows", label: "GitHub Actions" },
106
+ ];
107
+
108
+ const seen = new Set();
109
+ for (const c of checks) {
110
+ try {
111
+ fs.statSync(path.join(target, c.file));
112
+ if (!seen.has(c.label)) {
113
+ detected.push(c.label);
114
+ seen.add(c.label);
115
+ console.log(` ✓ ${c.label}`);
116
+ }
117
+ } catch {}
118
+ }
119
+
120
+ // Check package.json for framework hints
121
+ try {
122
+ const pkg = JSON.parse(fs.readFileSync(path.join(target, "package.json"), "utf8"));
123
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
124
+ if (deps["react"] && !seen.has("React")) { detected.push("React"); seen.add("React"); console.log(" ✓ React"); }
125
+ if (deps["vue"] && !seen.has("Vue")) { detected.push("Vue"); seen.add("Vue"); console.log(" ✓ Vue"); }
126
+ if (deps["jest"] || deps["vitest"]) { const t = deps["vitest"] ? "Vitest" : "Jest"; if (!seen.has(t)) { detected.push(t); seen.add(t); console.log(` ✓ ${t}`); } }
127
+ } catch {}
128
+
129
+ if (detected.length === 0) {
130
+ console.log(" Could not auto-detect stack.");
131
+ const manual = await ask(rl, " What's your stack? (comma separated): ", "");
132
+ if (manual) {
133
+ detected.push(...manual.split(",").map(s => s.trim()).filter(Boolean));
134
+ }
135
+ } else {
136
+ console.log("");
137
+ const ok = await ask(rl, " Looks right? [Y/n]: ", "y");
138
+ if (ok.toLowerCase() === "n" || ok.toLowerCase() === "no") {
139
+ const manual = await ask(rl, " What's your stack? (comma separated): ", "");
140
+ if (manual) {
141
+ detected.length = 0;
142
+ detected.push(...manual.split(",").map(s => s.trim()).filter(Boolean));
143
+ }
144
+ }
145
+ }
146
+
147
+ return detected;
148
+ }
149
+
150
+ function stepGenerate(target, agent, stack) {
151
+ console.log("");
152
+ console.log(" Generating AI agent configs...");
153
+ console.log("");
154
+
155
+ // Create minimal ai/ directory with generated configs
156
+ const aiDir = path.join(target, "ai");
157
+ const manifestDir = path.join(aiDir, "manifest");
158
+ fs.mkdirSync(manifestDir, { recursive: true });
159
+
160
+ // VERSION
161
+ fs.writeFileSync(path.join(aiDir, "VERSION"), "3.10.1\n");
162
+
163
+ // Discovery
164
+ const discovery = {
165
+ project_name: path.basename(target),
166
+ stack: stack[0] || "unknown",
167
+ detected_stack: stack,
168
+ selected_agents: agent === "all" ? AGENTS.map(a => a.id) : [agent],
169
+ detected_at: new Date().toISOString(),
170
+ wizard: true,
171
+ };
172
+ fs.writeFileSync(
173
+ path.join(manifestDir, "discovery.json"),
174
+ JSON.stringify(discovery, null, 2) + "\n",
175
+ );
176
+
177
+ // Project YAML
178
+ fs.writeFileSync(
179
+ path.join(manifestDir, "project.yaml"),
180
+ `plan: free\nname: ${discovery.project_name}\nstack: ${discovery.stack}\n`,
181
+ );
182
+
183
+ // CLAUDE.md
184
+ const claudeMd = `# ${discovery.project_name}\n\nStack: ${stack.join(", ") || "unknown"}\nGenerated by 0dai wizard.\n\n## Commands\n\nSee ai/manifest/ for full configuration.\n`;
185
+ fs.writeFileSync(path.join(target, "CLAUDE.md"), claudeMd);
186
+ console.log(" ✓ CLAUDE.md");
187
+
188
+ // AGENTS.md
189
+ fs.writeFileSync(
190
+ path.join(target, "AGENTS.md"),
191
+ `# Agent Configuration\n\nProject: ${discovery.project_name}\nStack: ${stack.join(", ") || "unknown"}\n\nSee ai/manifest/ for configuration details.\n`,
192
+ );
193
+ console.log(" ✓ AGENTS.md");
194
+
195
+ // ai/manifest files
196
+ console.log(" ✓ ai/manifest/discovery.json");
197
+ console.log(" ✓ ai/manifest/project.yaml");
198
+ console.log(" ✓ ai/VERSION");
199
+
200
+ const count = 5; // CLAUDE.md, AGENTS.md, discovery.json, project.yaml, VERSION
201
+ console.log("");
202
+ console.log(` ${count} configs generated in ai/ directory.`);
203
+ return count;
204
+ }
205
+
206
+ function stepNext(mode) {
207
+ console.log("");
208
+ console.log(" ✅ 0dai is ready!");
209
+ console.log("");
210
+ console.log(" Try these commands:");
211
+ console.log(" 0dai status — see your project config");
212
+ console.log(" 0dai doctor — check config health");
213
+ if (mode === "cloud") {
214
+ console.log(" 0dai auth login — sign in for cloud features");
215
+ console.log(" 0dai sync — refresh configs from server");
216
+ }
217
+ console.log("");
218
+ console.log(" Pro features ($15/mo):");
219
+ console.log(" 0dai swarm run — delegate tasks to AI agents");
220
+ console.log(" 0dai graph push — sync project intelligence");
221
+ console.log(" 0dai upgrade — unlock all features");
222
+ console.log("");
223
+ console.log(" Docs: https://0dai.dev/docs");
224
+ console.log("");
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Main wizard
229
+ // ---------------------------------------------------------------------------
230
+
231
+ async function runWizard(target) {
232
+ if (!isInteractive()) {
233
+ // Non-interactive: use defaults silently
234
+ stepGenerate(target, "all", []);
235
+ return { completed: true, interactive: false };
236
+ }
237
+
238
+ const rl = readline.createInterface({
239
+ input: process.stdin,
240
+ output: process.stdout,
241
+ });
242
+
243
+ // Clean exit on Ctrl+C — no partial state
244
+ let aborted = false;
245
+ rl.on("close", () => {
246
+ if (!aborted) {
247
+ aborted = true;
248
+ }
249
+ });
250
+
251
+ try {
252
+ const agent = await stepAgent(rl);
253
+ if (aborted) return { completed: false };
254
+
255
+ const mode = await stepAuth(rl);
256
+ if (aborted) return { completed: false };
257
+
258
+ const stack = await stepDetect(rl, target);
259
+ if (aborted) return { completed: false };
260
+
261
+ stepGenerate(target, agent, stack);
262
+ stepNext(mode);
263
+
264
+ return { completed: true, interactive: true, agent, mode, stack };
265
+ } catch (err) {
266
+ // Ctrl+C or other interrupt
267
+ if (err && err.code !== "ERR_USE_AFTER_CLOSE") {
268
+ console.error(`\n Wizard error: ${err.message || err}`);
269
+ }
270
+ return { completed: false };
271
+ } finally {
272
+ rl.close();
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Prompt user to run wizard if ai/ doesn't exist.
278
+ * Returns true if wizard ran (or was declined), false to continue normal flow.
279
+ */
280
+ async function maybeWizard(target, cmd) {
281
+ if (!needsWizard(target)) return false;
282
+
283
+ // init command goes straight to wizard
284
+ if (cmd === "init") return false; // let cmdInit handle it with --no-wizard check
285
+
286
+ if (!isInteractive()) return false;
287
+
288
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
289
+ try {
290
+ const answer = await ask(rl, "\n No 0dai config found. Run the setup wizard? [Y/n]: ", "y");
291
+ rl.close();
292
+ if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
293
+ console.log(" Run '0dai init' when ready.\n");
294
+ return true; // handled — don't continue to the command
295
+ }
296
+ await runWizard(target);
297
+ return true;
298
+ } catch {
299
+ rl.close();
300
+ return false;
301
+ }
302
+ }
303
+
304
+ module.exports = {
305
+ runWizard,
306
+ maybeWizard,
307
+ needsWizard,
308
+ isInteractive,
309
+ stepGenerate,
310
+ AGENTS,
311
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "3.10.0",
3
+ "version": "4.0.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"
@@ -25,9 +25,13 @@
25
25
  "engines": {
26
26
  "node": ">=16"
27
27
  },
28
+ "scripts": {
29
+ "postinstall": "node scripts/postinstall.js || true"
30
+ },
28
31
  "files": [
29
32
  "bin/",
30
33
  "lib/",
34
+ "scripts/",
31
35
  "README.md"
32
36
  ],
33
37
  "dependencies": {
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install welcome message for 0dai CLI.
4
+ * Always exits 0 — must never break npm install.
5
+ */
6
+ "use strict";
7
+ try {
8
+ // Suppress in CI environments
9
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.BUILD_NUMBER) {
10
+ process.exit(0);
11
+ }
12
+
13
+ const pkg = require("../package.json");
14
+ const v = pkg.version || "?";
15
+
16
+ console.log("");
17
+ console.log(" \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
18
+ console.log(" \u2502 0dai installed successfully! v" + v.padEnd(10) + "\u2502");
19
+ console.log(" \u2502 \u2502");
20
+ console.log(" \u2502 Get started: \u2502");
21
+ console.log(" \u2502 cd your-project \u2502");
22
+ console.log(" \u2502 0dai init \u2502");
23
+ console.log(" \u2502 \u2502");
24
+ console.log(" \u2502 Docs: https://0dai.dev/docs \u2502");
25
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
26
+ console.log("");
27
+ } catch (_) {
28
+ // Never fail
29
+ }