@caliber-ai/cli 0.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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/bin.js +1395 -0
- package/dist/bin.js.map +1 -0
- package/package.json +37 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/constants.ts
|
|
24
|
+
var constants_exports = {};
|
|
25
|
+
__export(constants_exports, {
|
|
26
|
+
API_URL: () => API_URL,
|
|
27
|
+
AUTH_DIR: () => AUTH_DIR,
|
|
28
|
+
AUTH_FILE: () => AUTH_FILE,
|
|
29
|
+
BACKUPS_DIR: () => BACKUPS_DIR,
|
|
30
|
+
CALIBER_DIR: () => CALIBER_DIR,
|
|
31
|
+
CLI_CALLBACK_PORT: () => CLI_CALLBACK_PORT,
|
|
32
|
+
FRONTEND_URL: () => FRONTEND_URL,
|
|
33
|
+
MANIFEST_FILE: () => MANIFEST_FILE
|
|
34
|
+
});
|
|
35
|
+
import path from "path";
|
|
36
|
+
import os from "os";
|
|
37
|
+
var API_URL, FRONTEND_URL, CLI_CALLBACK_PORT, AUTH_DIR, AUTH_FILE, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR;
|
|
38
|
+
var init_constants = __esm({
|
|
39
|
+
"src/constants.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
API_URL = process.env.CALIBER_API_URL || "https://caliber-backend.up.railway.app";
|
|
42
|
+
FRONTEND_URL = process.env.CALIBER_FRONTEND_URL || "https://caliber-app.up.railway.app";
|
|
43
|
+
CLI_CALLBACK_PORT = 19284;
|
|
44
|
+
AUTH_DIR = path.join(os.homedir(), ".caliber");
|
|
45
|
+
AUTH_FILE = path.join(AUTH_DIR, "auth.json");
|
|
46
|
+
CALIBER_DIR = ".caliber";
|
|
47
|
+
MANIFEST_FILE = path.join(CALIBER_DIR, "manifest.json");
|
|
48
|
+
BACKUPS_DIR = path.join(CALIBER_DIR, "backups");
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// src/cli.ts
|
|
53
|
+
import { Command } from "commander";
|
|
54
|
+
|
|
55
|
+
// src/commands/init.ts
|
|
56
|
+
import chalk2 from "chalk";
|
|
57
|
+
import ora2 from "ora";
|
|
58
|
+
import readline from "readline";
|
|
59
|
+
import path10 from "path";
|
|
60
|
+
|
|
61
|
+
// src/auth/token-store.ts
|
|
62
|
+
init_constants();
|
|
63
|
+
import fs from "fs";
|
|
64
|
+
function getStoredAuth() {
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(AUTH_FILE)) return null;
|
|
67
|
+
const data = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
68
|
+
return data;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function storeAuth(auth2) {
|
|
74
|
+
if (!fs.existsSync(AUTH_DIR)) {
|
|
75
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
const data = {
|
|
78
|
+
accessToken: auth2.accessToken,
|
|
79
|
+
refreshToken: auth2.refreshToken,
|
|
80
|
+
expiresAt: Date.now() + auth2.expiresIn * 1e3,
|
|
81
|
+
userId: auth2.user.id,
|
|
82
|
+
email: auth2.user.email
|
|
83
|
+
};
|
|
84
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
85
|
+
}
|
|
86
|
+
function clearAuth() {
|
|
87
|
+
try {
|
|
88
|
+
if (fs.existsSync(AUTH_FILE)) fs.unlinkSync(AUTH_FILE);
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isTokenExpired() {
|
|
93
|
+
const auth2 = getStoredAuth();
|
|
94
|
+
if (!auth2) return true;
|
|
95
|
+
return Date.now() >= auth2.expiresAt - 6e4;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/commands/login.ts
|
|
99
|
+
import open from "open";
|
|
100
|
+
import ora from "ora";
|
|
101
|
+
import chalk from "chalk";
|
|
102
|
+
|
|
103
|
+
// src/auth/pkce.ts
|
|
104
|
+
import crypto from "crypto";
|
|
105
|
+
function generatePKCE() {
|
|
106
|
+
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
107
|
+
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
108
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
109
|
+
return { verifier, challenge, state };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/auth/callback-server.ts
|
|
113
|
+
init_constants();
|
|
114
|
+
import http from "http";
|
|
115
|
+
import { URL as URL2 } from "url";
|
|
116
|
+
function startCallbackServer(expectedState) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const server = http.createServer((req, res) => {
|
|
119
|
+
if (!req.url?.startsWith("/callback")) {
|
|
120
|
+
res.writeHead(404);
|
|
121
|
+
res.end();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const url = new URL2(req.url, `http://127.0.0.1:${CLI_CALLBACK_PORT}`);
|
|
125
|
+
const error = url.searchParams.get("error");
|
|
126
|
+
if (error) {
|
|
127
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
128
|
+
res.end("<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>");
|
|
129
|
+
server.close();
|
|
130
|
+
reject(new Error(`Auth failed: ${error}`));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const state = url.searchParams.get("state");
|
|
134
|
+
const accessToken = url.searchParams.get("access_token");
|
|
135
|
+
const refreshToken = url.searchParams.get("refresh_token");
|
|
136
|
+
const expiresIn = url.searchParams.get("expires_in");
|
|
137
|
+
const userId = url.searchParams.get("user_id");
|
|
138
|
+
const email = url.searchParams.get("email");
|
|
139
|
+
if (!accessToken || !refreshToken || !userId || state !== expectedState) {
|
|
140
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
141
|
+
res.end("<html><body><h2>Invalid callback</h2></body></html>");
|
|
142
|
+
server.close();
|
|
143
|
+
reject(new Error("Invalid callback: missing token data or state mismatch"));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
147
|
+
res.end(`<html><body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #09090b; color: #fafafa;">
|
|
148
|
+
<div style="text-align: center;">
|
|
149
|
+
<h2 style="color: #6366f1;">Authentication successful!</h2>
|
|
150
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
151
|
+
</div>
|
|
152
|
+
</body></html>`);
|
|
153
|
+
server.close();
|
|
154
|
+
resolve({
|
|
155
|
+
accessToken,
|
|
156
|
+
refreshToken,
|
|
157
|
+
expiresIn: parseInt(expiresIn || "3600", 10),
|
|
158
|
+
user: { id: userId, email: email || "" }
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
server.listen(CLI_CALLBACK_PORT, "127.0.0.1", () => {
|
|
162
|
+
});
|
|
163
|
+
server.on("error", (err) => {
|
|
164
|
+
reject(new Error(`Failed to start callback server: ${err.message}`));
|
|
165
|
+
});
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
server.close();
|
|
168
|
+
reject(new Error("Authentication timed out"));
|
|
169
|
+
}, 5 * 60 * 1e3);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/commands/login.ts
|
|
174
|
+
init_constants();
|
|
175
|
+
|
|
176
|
+
// src/telemetry.ts
|
|
177
|
+
init_constants();
|
|
178
|
+
import { PostHog } from "posthog-node";
|
|
179
|
+
import fs2 from "fs";
|
|
180
|
+
import path2 from "path";
|
|
181
|
+
import os2 from "os";
|
|
182
|
+
import { randomUUID } from "crypto";
|
|
183
|
+
var POSTHOG_API_KEY = process.env.CALIBER_POSTHOG_KEY || "phc_XXrV0pSX4s2QVxVoOaeuyXDvtlRwPAjovt1ttMGVMPp";
|
|
184
|
+
var DEVICE_ID_FILE = path2.join(AUTH_DIR, "device-id");
|
|
185
|
+
var client = null;
|
|
186
|
+
function getClient() {
|
|
187
|
+
if (!client) {
|
|
188
|
+
client = new PostHog(POSTHOG_API_KEY, { flushAt: 1, flushInterval: 0 });
|
|
189
|
+
}
|
|
190
|
+
return client;
|
|
191
|
+
}
|
|
192
|
+
function getDeviceId() {
|
|
193
|
+
try {
|
|
194
|
+
if (fs2.existsSync(DEVICE_ID_FILE)) {
|
|
195
|
+
return fs2.readFileSync(DEVICE_ID_FILE, "utf-8").trim();
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
const id = randomUUID();
|
|
200
|
+
try {
|
|
201
|
+
if (!fs2.existsSync(AUTH_DIR)) {
|
|
202
|
+
fs2.mkdirSync(AUTH_DIR, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
fs2.writeFileSync(DEVICE_ID_FILE, id, { mode: 384 });
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
return id;
|
|
208
|
+
}
|
|
209
|
+
function isFirstRun() {
|
|
210
|
+
return !fs2.existsSync(DEVICE_ID_FILE);
|
|
211
|
+
}
|
|
212
|
+
function getDistinctId() {
|
|
213
|
+
const auth2 = getStoredAuth();
|
|
214
|
+
return auth2?.userId || getDeviceId();
|
|
215
|
+
}
|
|
216
|
+
var pkg = JSON.parse(
|
|
217
|
+
fs2.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
218
|
+
);
|
|
219
|
+
function baseProperties() {
|
|
220
|
+
return {
|
|
221
|
+
source: "cli",
|
|
222
|
+
cli_version: pkg.version,
|
|
223
|
+
os: os2.platform()
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function trackEvent(event, properties = {}) {
|
|
227
|
+
try {
|
|
228
|
+
getClient().capture({
|
|
229
|
+
distinctId: getDistinctId(),
|
|
230
|
+
event,
|
|
231
|
+
properties: { ...baseProperties(), ...properties }
|
|
232
|
+
});
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function identifyUser(userId, email) {
|
|
237
|
+
try {
|
|
238
|
+
const ph = getClient();
|
|
239
|
+
const deviceId = getDeviceId();
|
|
240
|
+
ph.identify({ distinctId: userId, properties: { email, source: "cli" } });
|
|
241
|
+
if (deviceId !== userId) {
|
|
242
|
+
ph.alias({ distinctId: userId, alias: deviceId });
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function countSuggestedFiles(setup) {
|
|
248
|
+
let count = 0;
|
|
249
|
+
const claude = setup.claude;
|
|
250
|
+
const cursor = setup.cursor;
|
|
251
|
+
if (claude) {
|
|
252
|
+
if (claude.claudeMd) count++;
|
|
253
|
+
if (claude.settings) count++;
|
|
254
|
+
if (claude.settingsLocal) count++;
|
|
255
|
+
if (Array.isArray(claude.skills)) count += claude.skills.length;
|
|
256
|
+
if (claude.mcpServers && Object.keys(claude.mcpServers).length > 0) count++;
|
|
257
|
+
}
|
|
258
|
+
if (cursor) {
|
|
259
|
+
if (cursor.cursorrules) count++;
|
|
260
|
+
if (Array.isArray(cursor.rules)) count += cursor.rules.length;
|
|
261
|
+
if (cursor.mcpServers && Object.keys(cursor.mcpServers).length > 0) count++;
|
|
262
|
+
}
|
|
263
|
+
return count;
|
|
264
|
+
}
|
|
265
|
+
async function shutdownTelemetry() {
|
|
266
|
+
try {
|
|
267
|
+
await Promise.race([
|
|
268
|
+
client?.shutdown(),
|
|
269
|
+
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
270
|
+
]);
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/commands/login.ts
|
|
276
|
+
async function loginCommand() {
|
|
277
|
+
const existing = getStoredAuth();
|
|
278
|
+
if (existing) {
|
|
279
|
+
console.log(chalk.dim(`Already logged in as ${existing.email}`));
|
|
280
|
+
console.log(chalk.dim("Run `caliber logout` first to switch accounts."));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const { state } = generatePKCE();
|
|
284
|
+
const redirectUri = `http://127.0.0.1:${CLI_CALLBACK_PORT}/callback`;
|
|
285
|
+
const authUrl = `${FRONTEND_URL}/auth/cli?redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`;
|
|
286
|
+
console.log(chalk.bold("\nOpening browser for authentication...\n"));
|
|
287
|
+
console.log(chalk.dim(`If the browser doesn't open, visit:
|
|
288
|
+
${authUrl}
|
|
289
|
+
`));
|
|
290
|
+
const serverPromise = startCallbackServer(state);
|
|
291
|
+
await open(authUrl);
|
|
292
|
+
const spinner = ora("Waiting for authentication...").start();
|
|
293
|
+
try {
|
|
294
|
+
const result = await serverPromise;
|
|
295
|
+
storeAuth({
|
|
296
|
+
accessToken: result.accessToken,
|
|
297
|
+
refreshToken: result.refreshToken,
|
|
298
|
+
expiresIn: result.expiresIn,
|
|
299
|
+
user: result.user
|
|
300
|
+
});
|
|
301
|
+
identifyUser(result.user.id, result.user.email);
|
|
302
|
+
trackEvent("signin", { method: "cli_pkce" });
|
|
303
|
+
spinner.succeed(chalk.green(`Logged in as ${result.user.email}`));
|
|
304
|
+
} catch (err) {
|
|
305
|
+
spinner.fail(chalk.red(`Authentication failed: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/fingerprint/index.ts
|
|
311
|
+
import crypto2 from "crypto";
|
|
312
|
+
|
|
313
|
+
// src/fingerprint/git.ts
|
|
314
|
+
import { execSync } from "child_process";
|
|
315
|
+
function getGitRemoteUrl() {
|
|
316
|
+
try {
|
|
317
|
+
return execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
318
|
+
} catch {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/fingerprint/package-json.ts
|
|
324
|
+
import fs3 from "fs";
|
|
325
|
+
import path3 from "path";
|
|
326
|
+
import { globSync } from "glob";
|
|
327
|
+
var NODE_FRAMEWORK_DEPS = {
|
|
328
|
+
react: "React",
|
|
329
|
+
next: "Next.js",
|
|
330
|
+
vue: "Vue",
|
|
331
|
+
nuxt: "Nuxt",
|
|
332
|
+
svelte: "Svelte",
|
|
333
|
+
"@sveltejs/kit": "SvelteKit",
|
|
334
|
+
angular: "Angular",
|
|
335
|
+
"@angular/core": "Angular",
|
|
336
|
+
express: "Express",
|
|
337
|
+
fastify: "Fastify",
|
|
338
|
+
hono: "Hono",
|
|
339
|
+
nestjs: "NestJS",
|
|
340
|
+
"@nestjs/core": "NestJS",
|
|
341
|
+
tailwindcss: "Tailwind CSS",
|
|
342
|
+
prisma: "Prisma",
|
|
343
|
+
drizzle: "Drizzle",
|
|
344
|
+
"drizzle-orm": "Drizzle",
|
|
345
|
+
"@supabase/supabase-js": "Supabase",
|
|
346
|
+
mongoose: "MongoDB",
|
|
347
|
+
typeorm: "TypeORM",
|
|
348
|
+
sequelize: "Sequelize",
|
|
349
|
+
"better-auth": "Better Auth"
|
|
350
|
+
};
|
|
351
|
+
var PYTHON_FRAMEWORK_DEPS = {
|
|
352
|
+
fastapi: "FastAPI",
|
|
353
|
+
django: "Django",
|
|
354
|
+
flask: "Flask",
|
|
355
|
+
sqlalchemy: "SQLAlchemy",
|
|
356
|
+
pydantic: "Pydantic",
|
|
357
|
+
celery: "Celery",
|
|
358
|
+
pytest: "pytest",
|
|
359
|
+
uvicorn: "Uvicorn",
|
|
360
|
+
starlette: "Starlette",
|
|
361
|
+
httpx: "HTTPX",
|
|
362
|
+
alembic: "Alembic",
|
|
363
|
+
tortoise: "Tortoise ORM",
|
|
364
|
+
"google-cloud-pubsub": "Google Pub/Sub",
|
|
365
|
+
stripe: "Stripe",
|
|
366
|
+
redis: "Redis"
|
|
367
|
+
};
|
|
368
|
+
var WORKSPACE_GLOBS = [
|
|
369
|
+
"apps/*/package.json",
|
|
370
|
+
"packages/*/package.json",
|
|
371
|
+
"services/*/package.json",
|
|
372
|
+
"libs/*/package.json"
|
|
373
|
+
];
|
|
374
|
+
function detectNodeFrameworks(pkgPath) {
|
|
375
|
+
try {
|
|
376
|
+
const pkg2 = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
377
|
+
const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
|
|
378
|
+
const frameworks = [];
|
|
379
|
+
for (const [dep, framework] of Object.entries(NODE_FRAMEWORK_DEPS)) {
|
|
380
|
+
if (allDeps[dep]) frameworks.push(framework);
|
|
381
|
+
}
|
|
382
|
+
return frameworks;
|
|
383
|
+
} catch {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function detectPythonFrameworks(dir) {
|
|
388
|
+
const frameworks = [];
|
|
389
|
+
const candidates = [
|
|
390
|
+
path3.join(dir, "pyproject.toml"),
|
|
391
|
+
path3.join(dir, "requirements.txt"),
|
|
392
|
+
...globSync("apps/*/pyproject.toml", { cwd: dir, absolute: true }),
|
|
393
|
+
...globSync("apps/*/requirements.txt", { cwd: dir, absolute: true }),
|
|
394
|
+
...globSync("services/*/pyproject.toml", { cwd: dir, absolute: true })
|
|
395
|
+
];
|
|
396
|
+
for (const filePath of candidates) {
|
|
397
|
+
if (!fs3.existsSync(filePath)) continue;
|
|
398
|
+
try {
|
|
399
|
+
const content = fs3.readFileSync(filePath, "utf-8").toLowerCase();
|
|
400
|
+
for (const [dep, framework] of Object.entries(PYTHON_FRAMEWORK_DEPS)) {
|
|
401
|
+
if (content.includes(dep)) frameworks.push(framework);
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return frameworks;
|
|
407
|
+
}
|
|
408
|
+
function analyzePackageJson(dir) {
|
|
409
|
+
const rootPkgPath = path3.join(dir, "package.json");
|
|
410
|
+
let name;
|
|
411
|
+
const allFrameworks = [];
|
|
412
|
+
const languages = [];
|
|
413
|
+
if (fs3.existsSync(rootPkgPath)) {
|
|
414
|
+
try {
|
|
415
|
+
const pkg2 = JSON.parse(fs3.readFileSync(rootPkgPath, "utf-8"));
|
|
416
|
+
name = pkg2.name;
|
|
417
|
+
const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
|
|
418
|
+
allFrameworks.push(...detectNodeFrameworks(rootPkgPath));
|
|
419
|
+
if (allDeps.typescript || allDeps["@types/node"]) {
|
|
420
|
+
languages.push("TypeScript");
|
|
421
|
+
}
|
|
422
|
+
languages.push("JavaScript");
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const glob of WORKSPACE_GLOBS) {
|
|
427
|
+
const matches = globSync(glob, { cwd: dir, absolute: true });
|
|
428
|
+
for (const pkgPath of matches) {
|
|
429
|
+
allFrameworks.push(...detectNodeFrameworks(pkgPath));
|
|
430
|
+
try {
|
|
431
|
+
const pkg2 = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
432
|
+
const deps = { ...pkg2.dependencies, ...pkg2.devDependencies };
|
|
433
|
+
if (deps.typescript || deps["@types/node"]) {
|
|
434
|
+
languages.push("TypeScript");
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
allFrameworks.push(...detectPythonFrameworks(dir));
|
|
441
|
+
return {
|
|
442
|
+
name,
|
|
443
|
+
frameworks: [...new Set(allFrameworks)],
|
|
444
|
+
languages: [...new Set(languages)]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/fingerprint/file-tree.ts
|
|
449
|
+
import fs4 from "fs";
|
|
450
|
+
import path4 from "path";
|
|
451
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
452
|
+
"node_modules",
|
|
453
|
+
".git",
|
|
454
|
+
".next",
|
|
455
|
+
"dist",
|
|
456
|
+
"build",
|
|
457
|
+
".cache",
|
|
458
|
+
".turbo",
|
|
459
|
+
"coverage",
|
|
460
|
+
".caliber",
|
|
461
|
+
"__pycache__",
|
|
462
|
+
".venv",
|
|
463
|
+
"vendor",
|
|
464
|
+
"target"
|
|
465
|
+
]);
|
|
466
|
+
function getFileTree(dir, maxDepth = 3) {
|
|
467
|
+
const files = [];
|
|
468
|
+
scan(dir, "", 0, maxDepth, files);
|
|
469
|
+
return files;
|
|
470
|
+
}
|
|
471
|
+
function scan(base, rel, depth, maxDepth, result) {
|
|
472
|
+
if (depth > maxDepth) return;
|
|
473
|
+
const fullPath = path4.join(base, rel);
|
|
474
|
+
let entries;
|
|
475
|
+
try {
|
|
476
|
+
entries = fs4.readdirSync(fullPath, { withFileTypes: true });
|
|
477
|
+
} catch {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
if (entry.name.startsWith(".") && depth === 0 && entry.isDirectory()) continue;
|
|
482
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
483
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
484
|
+
if (entry.isDirectory()) {
|
|
485
|
+
result.push(`${relPath}/`);
|
|
486
|
+
scan(base, relPath, depth + 1, maxDepth, result);
|
|
487
|
+
} else {
|
|
488
|
+
result.push(relPath);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/fingerprint/languages.ts
|
|
494
|
+
import path5 from "path";
|
|
495
|
+
var EXT_TO_LANG = {
|
|
496
|
+
".ts": "TypeScript",
|
|
497
|
+
".tsx": "TypeScript",
|
|
498
|
+
".js": "JavaScript",
|
|
499
|
+
".jsx": "JavaScript",
|
|
500
|
+
".py": "Python",
|
|
501
|
+
".go": "Go",
|
|
502
|
+
".rs": "Rust",
|
|
503
|
+
".rb": "Ruby",
|
|
504
|
+
".java": "Java",
|
|
505
|
+
".kt": "Kotlin",
|
|
506
|
+
".swift": "Swift",
|
|
507
|
+
".cs": "C#",
|
|
508
|
+
".cpp": "C++",
|
|
509
|
+
".c": "C",
|
|
510
|
+
".php": "PHP",
|
|
511
|
+
".dart": "Dart",
|
|
512
|
+
".ex": "Elixir",
|
|
513
|
+
".exs": "Elixir",
|
|
514
|
+
".scala": "Scala",
|
|
515
|
+
".zig": "Zig"
|
|
516
|
+
};
|
|
517
|
+
function detectLanguages(fileTree) {
|
|
518
|
+
const langs = /* @__PURE__ */ new Set();
|
|
519
|
+
for (const file of fileTree) {
|
|
520
|
+
const ext = path5.extname(file).toLowerCase();
|
|
521
|
+
if (EXT_TO_LANG[ext]) {
|
|
522
|
+
langs.add(EXT_TO_LANG[ext]);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return [...langs];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/fingerprint/existing-config.ts
|
|
529
|
+
import fs5 from "fs";
|
|
530
|
+
import path6 from "path";
|
|
531
|
+
function readExistingConfigs(dir) {
|
|
532
|
+
const configs = {};
|
|
533
|
+
const claudeMdPath = path6.join(dir, "CLAUDE.md");
|
|
534
|
+
if (fs5.existsSync(claudeMdPath)) {
|
|
535
|
+
configs.claudeMd = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
536
|
+
}
|
|
537
|
+
const claudeSettingsPath = path6.join(dir, ".claude", "settings.json");
|
|
538
|
+
if (fs5.existsSync(claudeSettingsPath)) {
|
|
539
|
+
try {
|
|
540
|
+
configs.claudeSettings = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const skillsDir = path6.join(dir, ".claude", "skills");
|
|
545
|
+
if (fs5.existsSync(skillsDir)) {
|
|
546
|
+
try {
|
|
547
|
+
const files = fs5.readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
|
|
548
|
+
configs.claudeSkills = files.map((f) => ({
|
|
549
|
+
filename: f,
|
|
550
|
+
content: fs5.readFileSync(path6.join(skillsDir, f), "utf-8")
|
|
551
|
+
}));
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const cursorrulesPath = path6.join(dir, ".cursorrules");
|
|
556
|
+
if (fs5.existsSync(cursorrulesPath)) {
|
|
557
|
+
configs.cursorrules = fs5.readFileSync(cursorrulesPath, "utf-8");
|
|
558
|
+
}
|
|
559
|
+
const cursorRulesDir = path6.join(dir, ".cursor", "rules");
|
|
560
|
+
if (fs5.existsSync(cursorRulesDir)) {
|
|
561
|
+
try {
|
|
562
|
+
const files = fs5.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"));
|
|
563
|
+
configs.cursorRules = files.map((f) => ({
|
|
564
|
+
filename: f,
|
|
565
|
+
content: fs5.readFileSync(path6.join(cursorRulesDir, f), "utf-8")
|
|
566
|
+
}));
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return configs;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/fingerprint/index.ts
|
|
574
|
+
function collectFingerprint(dir) {
|
|
575
|
+
const gitRemoteUrl = getGitRemoteUrl();
|
|
576
|
+
const pkgInfo = analyzePackageJson(dir);
|
|
577
|
+
const fileTree = getFileTree(dir);
|
|
578
|
+
const fileLangs = detectLanguages(fileTree);
|
|
579
|
+
const existingConfigs = readExistingConfigs(dir);
|
|
580
|
+
const languages = [.../* @__PURE__ */ new Set([...pkgInfo.languages, ...fileLangs])];
|
|
581
|
+
return {
|
|
582
|
+
gitRemoteUrl,
|
|
583
|
+
packageName: pkgInfo.name,
|
|
584
|
+
languages,
|
|
585
|
+
frameworks: pkgInfo.frameworks,
|
|
586
|
+
fileTree,
|
|
587
|
+
existingConfigs
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function computeFingerprintHash(fingerprint) {
|
|
591
|
+
const key = [
|
|
592
|
+
fingerprint.gitRemoteUrl || "",
|
|
593
|
+
fingerprint.packageName || ""
|
|
594
|
+
].join("::");
|
|
595
|
+
return crypto2.createHash("sha256").update(key).digest("hex");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/api/client.ts
|
|
599
|
+
init_constants();
|
|
600
|
+
async function refreshTokenIfNeeded() {
|
|
601
|
+
const auth2 = getStoredAuth();
|
|
602
|
+
if (!auth2) return null;
|
|
603
|
+
if (!isTokenExpired()) return auth2.accessToken;
|
|
604
|
+
try {
|
|
605
|
+
const resp = await fetch(`${API_URL}/api/auth/refresh`, {
|
|
606
|
+
method: "POST",
|
|
607
|
+
headers: { "Content-Type": "application/json" },
|
|
608
|
+
body: JSON.stringify({ refreshToken: auth2.refreshToken })
|
|
609
|
+
});
|
|
610
|
+
if (!resp.ok) return null;
|
|
611
|
+
const { data } = await resp.json();
|
|
612
|
+
storeAuth({
|
|
613
|
+
accessToken: data.accessToken,
|
|
614
|
+
refreshToken: data.refreshToken,
|
|
615
|
+
expiresIn: data.expiresIn,
|
|
616
|
+
user: { id: auth2.userId, email: auth2.email }
|
|
617
|
+
});
|
|
618
|
+
return data.accessToken;
|
|
619
|
+
} catch {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function apiRequest(path11, options = {}) {
|
|
624
|
+
const token = await refreshTokenIfNeeded();
|
|
625
|
+
if (!token) {
|
|
626
|
+
throw new Error("Not authenticated. Run `caliber login` first.");
|
|
627
|
+
}
|
|
628
|
+
const resp = await fetch(`${API_URL}${path11}`, {
|
|
629
|
+
method: options.method || "GET",
|
|
630
|
+
headers: {
|
|
631
|
+
"Content-Type": "application/json",
|
|
632
|
+
Authorization: `Bearer ${token}`
|
|
633
|
+
},
|
|
634
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
635
|
+
});
|
|
636
|
+
if (!resp.ok) {
|
|
637
|
+
const error = await resp.json().catch(() => ({ error: resp.statusText }));
|
|
638
|
+
throw new Error(error.error || `API error: ${resp.status}`);
|
|
639
|
+
}
|
|
640
|
+
const json = await resp.json();
|
|
641
|
+
return json.data;
|
|
642
|
+
}
|
|
643
|
+
async function apiStream(path11, body, onChunk, onComplete, onError, onStatus) {
|
|
644
|
+
const token = await refreshTokenIfNeeded();
|
|
645
|
+
if (!token) {
|
|
646
|
+
throw new Error("Not authenticated. Run `caliber login` first.");
|
|
647
|
+
}
|
|
648
|
+
const resp = await fetch(`${API_URL}${path11}`, {
|
|
649
|
+
method: "POST",
|
|
650
|
+
headers: {
|
|
651
|
+
"Content-Type": "application/json",
|
|
652
|
+
Authorization: `Bearer ${token}`
|
|
653
|
+
},
|
|
654
|
+
body: JSON.stringify(body)
|
|
655
|
+
});
|
|
656
|
+
if (!resp.ok || !resp.body) {
|
|
657
|
+
throw new Error(`API error: ${resp.status}`);
|
|
658
|
+
}
|
|
659
|
+
const reader = resp.body.getReader();
|
|
660
|
+
const decoder = new TextDecoder();
|
|
661
|
+
let buffer = "";
|
|
662
|
+
while (true) {
|
|
663
|
+
const { done, value } = await reader.read();
|
|
664
|
+
if (done) break;
|
|
665
|
+
buffer += decoder.decode(value, { stream: true });
|
|
666
|
+
const lines = buffer.split("\n");
|
|
667
|
+
buffer = lines.pop() || "";
|
|
668
|
+
for (const line of lines) {
|
|
669
|
+
if (!line.startsWith("data: ")) continue;
|
|
670
|
+
const data = line.slice(6);
|
|
671
|
+
if (data === "[DONE]") return;
|
|
672
|
+
try {
|
|
673
|
+
const parsed = JSON.parse(data);
|
|
674
|
+
if (parsed.type === "chunk") onChunk(parsed.content);
|
|
675
|
+
else if (parsed.type === "status" && onStatus) onStatus(parsed.message);
|
|
676
|
+
else if (parsed.type === "complete") onComplete({ setup: parsed.setup, explanation: parsed.explanation });
|
|
677
|
+
else if (parsed.type === "error") onError(parsed.message);
|
|
678
|
+
} catch {
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/writers/index.ts
|
|
685
|
+
import fs10 from "fs";
|
|
686
|
+
|
|
687
|
+
// src/writers/claude/index.ts
|
|
688
|
+
import fs6 from "fs";
|
|
689
|
+
import path7 from "path";
|
|
690
|
+
function writeClaudeConfig(config) {
|
|
691
|
+
const written = [];
|
|
692
|
+
fs6.writeFileSync("CLAUDE.md", config.claudeMd);
|
|
693
|
+
written.push("CLAUDE.md");
|
|
694
|
+
const claudeDir = ".claude";
|
|
695
|
+
if (!fs6.existsSync(claudeDir)) fs6.mkdirSync(claudeDir, { recursive: true });
|
|
696
|
+
fs6.writeFileSync(
|
|
697
|
+
path7.join(claudeDir, "settings.json"),
|
|
698
|
+
JSON.stringify(config.settings, null, 2)
|
|
699
|
+
);
|
|
700
|
+
written.push(path7.join(claudeDir, "settings.json"));
|
|
701
|
+
fs6.writeFileSync(
|
|
702
|
+
path7.join(claudeDir, "settings.local.json"),
|
|
703
|
+
JSON.stringify(config.settingsLocal, null, 2)
|
|
704
|
+
);
|
|
705
|
+
written.push(path7.join(claudeDir, "settings.local.json"));
|
|
706
|
+
if (config.skills?.length) {
|
|
707
|
+
const skillsDir = path7.join(claudeDir, "skills");
|
|
708
|
+
if (!fs6.existsSync(skillsDir)) fs6.mkdirSync(skillsDir, { recursive: true });
|
|
709
|
+
for (const skill of config.skills) {
|
|
710
|
+
const filename = `${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
|
|
711
|
+
const skillPath = path7.join(skillsDir, filename);
|
|
712
|
+
fs6.writeFileSync(skillPath, skill.content);
|
|
713
|
+
written.push(skillPath);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
717
|
+
const mcpConfig = { mcpServers: config.mcpServers };
|
|
718
|
+
fs6.writeFileSync(".mcp.json", JSON.stringify(mcpConfig, null, 2));
|
|
719
|
+
written.push(".mcp.json");
|
|
720
|
+
}
|
|
721
|
+
return written;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/writers/cursor/index.ts
|
|
725
|
+
import fs7 from "fs";
|
|
726
|
+
import path8 from "path";
|
|
727
|
+
function writeCursorConfig(config) {
|
|
728
|
+
const written = [];
|
|
729
|
+
if (config.cursorrules) {
|
|
730
|
+
fs7.writeFileSync(".cursorrules", config.cursorrules);
|
|
731
|
+
written.push(".cursorrules");
|
|
732
|
+
}
|
|
733
|
+
if (config.rules?.length) {
|
|
734
|
+
const rulesDir = path8.join(".cursor", "rules");
|
|
735
|
+
if (!fs7.existsSync(rulesDir)) fs7.mkdirSync(rulesDir, { recursive: true });
|
|
736
|
+
for (const rule of config.rules) {
|
|
737
|
+
const rulePath = path8.join(rulesDir, rule.filename);
|
|
738
|
+
fs7.writeFileSync(rulePath, rule.content);
|
|
739
|
+
written.push(rulePath);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
743
|
+
const cursorDir = ".cursor";
|
|
744
|
+
if (!fs7.existsSync(cursorDir)) fs7.mkdirSync(cursorDir, { recursive: true });
|
|
745
|
+
const mcpConfig = { mcpServers: config.mcpServers };
|
|
746
|
+
fs7.writeFileSync(
|
|
747
|
+
path8.join(cursorDir, "mcp.json"),
|
|
748
|
+
JSON.stringify(mcpConfig, null, 2)
|
|
749
|
+
);
|
|
750
|
+
written.push(path8.join(cursorDir, "mcp.json"));
|
|
751
|
+
}
|
|
752
|
+
return written;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/writers/backup.ts
|
|
756
|
+
init_constants();
|
|
757
|
+
import fs8 from "fs";
|
|
758
|
+
import path9 from "path";
|
|
759
|
+
function createBackup(files) {
|
|
760
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
761
|
+
const backupDir = path9.join(BACKUPS_DIR, timestamp);
|
|
762
|
+
for (const file of files) {
|
|
763
|
+
if (!fs8.existsSync(file)) continue;
|
|
764
|
+
const dest = path9.join(backupDir, file);
|
|
765
|
+
const destDir = path9.dirname(dest);
|
|
766
|
+
if (!fs8.existsSync(destDir)) {
|
|
767
|
+
fs8.mkdirSync(destDir, { recursive: true });
|
|
768
|
+
}
|
|
769
|
+
fs8.copyFileSync(file, dest);
|
|
770
|
+
}
|
|
771
|
+
return backupDir;
|
|
772
|
+
}
|
|
773
|
+
function restoreBackup(backupDir, file) {
|
|
774
|
+
const backupFile = path9.join(backupDir, file);
|
|
775
|
+
if (!fs8.existsSync(backupFile)) return false;
|
|
776
|
+
const destDir = path9.dirname(file);
|
|
777
|
+
if (!fs8.existsSync(destDir)) {
|
|
778
|
+
fs8.mkdirSync(destDir, { recursive: true });
|
|
779
|
+
}
|
|
780
|
+
fs8.copyFileSync(backupFile, file);
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/writers/manifest.ts
|
|
785
|
+
init_constants();
|
|
786
|
+
import fs9 from "fs";
|
|
787
|
+
import crypto3 from "crypto";
|
|
788
|
+
function readManifest() {
|
|
789
|
+
try {
|
|
790
|
+
if (!fs9.existsSync(MANIFEST_FILE)) return null;
|
|
791
|
+
return JSON.parse(fs9.readFileSync(MANIFEST_FILE, "utf-8"));
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function writeManifest(manifest) {
|
|
797
|
+
if (!fs9.existsSync(CALIBER_DIR)) {
|
|
798
|
+
fs9.mkdirSync(CALIBER_DIR, { recursive: true });
|
|
799
|
+
}
|
|
800
|
+
fs9.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
|
|
801
|
+
}
|
|
802
|
+
function fileChecksum(filePath) {
|
|
803
|
+
const content = fs9.readFileSync(filePath);
|
|
804
|
+
return crypto3.createHash("sha256").update(content).digest("hex");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/writers/index.ts
|
|
808
|
+
function writeSetup(setup) {
|
|
809
|
+
const filesToWrite = getFilesToWrite(setup);
|
|
810
|
+
const existingFiles = filesToWrite.filter((f) => fs10.existsSync(f));
|
|
811
|
+
const backupDir = existingFiles.length > 0 ? createBackup(existingFiles) : void 0;
|
|
812
|
+
const written = [];
|
|
813
|
+
if ((setup.targetAgent === "claude" || setup.targetAgent === "both") && setup.claude) {
|
|
814
|
+
written.push(...writeClaudeConfig(setup.claude));
|
|
815
|
+
}
|
|
816
|
+
if ((setup.targetAgent === "cursor" || setup.targetAgent === "both") && setup.cursor) {
|
|
817
|
+
written.push(...writeCursorConfig(setup.cursor));
|
|
818
|
+
}
|
|
819
|
+
ensureGitignore();
|
|
820
|
+
const entries = written.map((file) => ({
|
|
821
|
+
path: file,
|
|
822
|
+
action: existingFiles.includes(file) ? "modified" : "created",
|
|
823
|
+
checksum: fileChecksum(file),
|
|
824
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
825
|
+
}));
|
|
826
|
+
writeManifest({ version: 1, backupDir, entries });
|
|
827
|
+
return { written, backupDir };
|
|
828
|
+
}
|
|
829
|
+
function undoSetup() {
|
|
830
|
+
const manifest = readManifest();
|
|
831
|
+
if (!manifest) {
|
|
832
|
+
throw new Error("No manifest found. Nothing to undo.");
|
|
833
|
+
}
|
|
834
|
+
const restored = [];
|
|
835
|
+
const removed = [];
|
|
836
|
+
for (const entry of manifest.entries) {
|
|
837
|
+
if (entry.action === "created") {
|
|
838
|
+
if (fs10.existsSync(entry.path)) {
|
|
839
|
+
fs10.unlinkSync(entry.path);
|
|
840
|
+
removed.push(entry.path);
|
|
841
|
+
}
|
|
842
|
+
} else if (entry.action === "modified" && manifest.backupDir) {
|
|
843
|
+
if (restoreBackup(manifest.backupDir, entry.path)) {
|
|
844
|
+
restored.push(entry.path);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const { MANIFEST_FILE: MANIFEST_FILE2 } = (init_constants(), __toCommonJS(constants_exports));
|
|
849
|
+
if (fs10.existsSync(MANIFEST_FILE2)) {
|
|
850
|
+
fs10.unlinkSync(MANIFEST_FILE2);
|
|
851
|
+
}
|
|
852
|
+
return { restored, removed };
|
|
853
|
+
}
|
|
854
|
+
function getFilesToWrite(setup) {
|
|
855
|
+
const files = [];
|
|
856
|
+
if ((setup.targetAgent === "claude" || setup.targetAgent === "both") && setup.claude) {
|
|
857
|
+
files.push("CLAUDE.md", ".claude/settings.json", ".claude/settings.local.json");
|
|
858
|
+
if (setup.claude.mcpServers) files.push(".mcp.json");
|
|
859
|
+
if (setup.claude.skills) {
|
|
860
|
+
for (const s of setup.claude.skills) {
|
|
861
|
+
files.push(`.claude/skills/${s.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if ((setup.targetAgent === "cursor" || setup.targetAgent === "both") && setup.cursor) {
|
|
866
|
+
if (setup.cursor.cursorrules) files.push(".cursorrules");
|
|
867
|
+
if (setup.cursor.rules) {
|
|
868
|
+
for (const r of setup.cursor.rules) files.push(`.cursor/rules/${r.filename}`);
|
|
869
|
+
}
|
|
870
|
+
if (setup.cursor.mcpServers) files.push(".cursor/mcp.json");
|
|
871
|
+
}
|
|
872
|
+
return files;
|
|
873
|
+
}
|
|
874
|
+
function ensureGitignore() {
|
|
875
|
+
const gitignorePath = ".gitignore";
|
|
876
|
+
if (fs10.existsSync(gitignorePath)) {
|
|
877
|
+
const content = fs10.readFileSync(gitignorePath, "utf-8");
|
|
878
|
+
if (!content.includes(".caliber/")) {
|
|
879
|
+
fs10.appendFileSync(gitignorePath, "\n# Caliber local state\n.caliber/\n");
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
fs10.writeFileSync(gitignorePath, "# Caliber local state\n.caliber/\n");
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/commands/init.ts
|
|
887
|
+
async function initCommand(options) {
|
|
888
|
+
console.log(chalk2.bold.hex("#6366f1")(`
|
|
889
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
890
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
891
|
+
\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
|
|
892
|
+
\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
893
|
+
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
894
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
|
|
895
|
+
`));
|
|
896
|
+
console.log(chalk2.dim(" Configure your coding agent environment\n"));
|
|
897
|
+
let auth2 = getStoredAuth();
|
|
898
|
+
if (!auth2) {
|
|
899
|
+
console.log(chalk2.yellow("Not logged in. Starting authentication...\n"));
|
|
900
|
+
await loginCommand();
|
|
901
|
+
auth2 = getStoredAuth();
|
|
902
|
+
if (!auth2) {
|
|
903
|
+
console.log(chalk2.red("Authentication required. Exiting."));
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
console.log(chalk2.dim(`Authenticated as ${auth2.email}
|
|
908
|
+
`));
|
|
909
|
+
const spinner = ora2("Analyzing project...").start();
|
|
910
|
+
const fingerprint = collectFingerprint(process.cwd());
|
|
911
|
+
const hash = computeFingerprintHash(fingerprint);
|
|
912
|
+
spinner.succeed("Project analyzed");
|
|
913
|
+
trackEvent("scan_completed", {
|
|
914
|
+
languages: fingerprint.languages,
|
|
915
|
+
frameworks: fingerprint.frameworks,
|
|
916
|
+
has_existing_config: !!(fingerprint.existingConfigs.claudeMd || fingerprint.existingConfigs.cursorrules),
|
|
917
|
+
has_claude_md: !!fingerprint.existingConfigs.claudeMd,
|
|
918
|
+
has_claude_settings: !!fingerprint.existingConfigs.claudeSettings,
|
|
919
|
+
has_cursorrules: !!fingerprint.existingConfigs.cursorrules,
|
|
920
|
+
cursor_rules_count: fingerprint.existingConfigs.cursorRules?.length ?? 0,
|
|
921
|
+
skills_count: fingerprint.existingConfigs.claudeSkills?.length ?? 0,
|
|
922
|
+
file_count: fingerprint.fileTree.length
|
|
923
|
+
});
|
|
924
|
+
console.log(chalk2.dim(` Languages: ${fingerprint.languages.join(", ") || "none detected"}`));
|
|
925
|
+
console.log(chalk2.dim(` Frameworks: ${fingerprint.frameworks.join(", ") || "none detected"}`));
|
|
926
|
+
console.log(chalk2.dim(` Files: ${fingerprint.fileTree.length} found
|
|
927
|
+
`));
|
|
928
|
+
const matchSpinner = ora2("Checking for existing setup...").start();
|
|
929
|
+
let existingSetup = null;
|
|
930
|
+
try {
|
|
931
|
+
const match = await apiRequest("/api/projects/match", {
|
|
932
|
+
method: "POST",
|
|
933
|
+
body: { fingerprintHash: hash }
|
|
934
|
+
});
|
|
935
|
+
if (match.setup) {
|
|
936
|
+
existingSetup = match.setup;
|
|
937
|
+
trackEvent("existing_config_detected");
|
|
938
|
+
matchSpinner.succeed("Found existing setup");
|
|
939
|
+
} else {
|
|
940
|
+
matchSpinner.info("No existing setup found");
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
matchSpinner.info("No existing setup found");
|
|
944
|
+
}
|
|
945
|
+
const targetAgent = options.agent || await promptAgent();
|
|
946
|
+
trackEvent("target_agent_selected", { target_agent: targetAgent });
|
|
947
|
+
const isEmpty = fingerprint.fileTree.length < 3;
|
|
948
|
+
if (isEmpty) {
|
|
949
|
+
fingerprint.description = await promptInput("What will you build in this project?");
|
|
950
|
+
}
|
|
951
|
+
let generatedSetup = null;
|
|
952
|
+
let setupExplanation;
|
|
953
|
+
trackEvent("generation_started", { target_agent: targetAgent });
|
|
954
|
+
const generationStart = Date.now();
|
|
955
|
+
const genSpinner = ora2("Generating setup...").start();
|
|
956
|
+
await apiStream(
|
|
957
|
+
"/api/setups/generate",
|
|
958
|
+
{
|
|
959
|
+
fingerprint,
|
|
960
|
+
targetAgent,
|
|
961
|
+
prompt: fingerprint.description
|
|
962
|
+
},
|
|
963
|
+
() => {
|
|
964
|
+
},
|
|
965
|
+
(payload) => {
|
|
966
|
+
generatedSetup = payload.setup;
|
|
967
|
+
setupExplanation = payload.explanation;
|
|
968
|
+
},
|
|
969
|
+
(error) => {
|
|
970
|
+
trackEvent("error_occurred", { error_type: "generation_failed", error_message: error });
|
|
971
|
+
genSpinner.fail(`Generation error: ${error}`);
|
|
972
|
+
},
|
|
973
|
+
(status) => {
|
|
974
|
+
genSpinner.text = status;
|
|
975
|
+
}
|
|
976
|
+
);
|
|
977
|
+
if (!generatedSetup) {
|
|
978
|
+
genSpinner.fail("Failed to generate setup.");
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
trackEvent("generation_completed", {
|
|
982
|
+
target_agent: targetAgent,
|
|
983
|
+
duration_ms: Date.now() - generationStart,
|
|
984
|
+
files_suggested: countSuggestedFiles(generatedSetup)
|
|
985
|
+
});
|
|
986
|
+
genSpinner.succeed("Setup generated");
|
|
987
|
+
printSetupSummary(generatedSetup);
|
|
988
|
+
let action = await promptAction();
|
|
989
|
+
while (action === "explain") {
|
|
990
|
+
if (setupExplanation) {
|
|
991
|
+
console.log(chalk2.bold("\nWhy this setup?\n"));
|
|
992
|
+
console.log(chalk2.dim(setupExplanation));
|
|
993
|
+
console.log("");
|
|
994
|
+
} else {
|
|
995
|
+
console.log(chalk2.dim("\nNo explanation available for this setup.\n"));
|
|
996
|
+
}
|
|
997
|
+
action = await promptAction();
|
|
998
|
+
}
|
|
999
|
+
if (action === "decline") {
|
|
1000
|
+
trackEvent("setup_declined");
|
|
1001
|
+
console.log(chalk2.dim("Setup declined. No files were modified."));
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (action === "refine") {
|
|
1005
|
+
generatedSetup = await refineLoop(generatedSetup, targetAgent);
|
|
1006
|
+
if (!generatedSetup) {
|
|
1007
|
+
trackEvent("generation_cancelled");
|
|
1008
|
+
console.log(chalk2.dim("Refinement cancelled. No files were modified."));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (options.dryRun) {
|
|
1013
|
+
console.log(chalk2.yellow("\n[Dry run] Would write the following files:"));
|
|
1014
|
+
console.log(JSON.stringify(generatedSetup, null, 2));
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const writeSpinner = ora2("Writing config files...").start();
|
|
1018
|
+
try {
|
|
1019
|
+
const result = writeSetup(generatedSetup);
|
|
1020
|
+
writeSpinner.succeed("Config files written");
|
|
1021
|
+
trackEvent("setup_applied", { files_written: result.written.length, target_agent: targetAgent });
|
|
1022
|
+
for (const file of result.written) {
|
|
1023
|
+
trackEvent("config_file_written", { file_type: path10.extname(file) || path10.basename(file) });
|
|
1024
|
+
}
|
|
1025
|
+
console.log(chalk2.bold("\nFiles created/updated:"));
|
|
1026
|
+
for (const file of result.written) {
|
|
1027
|
+
console.log(` ${chalk2.green("\u2713")} ${file}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (result.backupDir) {
|
|
1030
|
+
console.log(chalk2.dim(`
|
|
1031
|
+
Backups saved to ${result.backupDir}`));
|
|
1032
|
+
}
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
writeSpinner.fail("Failed to write files");
|
|
1035
|
+
console.error(chalk2.red(err instanceof Error ? err.message : "Unknown error"));
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const project = await apiRequest("/api/projects", {
|
|
1040
|
+
method: "POST",
|
|
1041
|
+
body: {
|
|
1042
|
+
fingerprintHash: hash,
|
|
1043
|
+
name: fingerprint.packageName || "untitled",
|
|
1044
|
+
gitRemoteUrl: fingerprint.gitRemoteUrl,
|
|
1045
|
+
description: fingerprint.description
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
trackEvent("project_created", { project_name: fingerprint.packageName || "untitled" });
|
|
1049
|
+
await apiRequest(`/api/setups/project/${project.id}`, {
|
|
1050
|
+
method: "POST",
|
|
1051
|
+
body: {
|
|
1052
|
+
targetAgent,
|
|
1053
|
+
config: generatedSetup,
|
|
1054
|
+
isMaster: true
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
console.log(chalk2.yellow(`
|
|
1059
|
+
Warning: Could not save project to server.`));
|
|
1060
|
+
console.log(chalk2.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
1061
|
+
console.log(chalk2.dim(` Your local setup is unaffected.
|
|
1062
|
+
`));
|
|
1063
|
+
}
|
|
1064
|
+
console.log(chalk2.bold.green("\nSetup complete! Your coding agent is now configured."));
|
|
1065
|
+
console.log(chalk2.dim("Run `caliber undo` to revert changes.\n"));
|
|
1066
|
+
}
|
|
1067
|
+
async function refineLoop(currentSetup, _targetAgent) {
|
|
1068
|
+
const history = [];
|
|
1069
|
+
let refinementRound = 0;
|
|
1070
|
+
while (true) {
|
|
1071
|
+
const message = await promptInput("\nWhat would you like to change?");
|
|
1072
|
+
if (!message || message.toLowerCase() === "done" || message.toLowerCase() === "accept") {
|
|
1073
|
+
return currentSetup;
|
|
1074
|
+
}
|
|
1075
|
+
if (message.toLowerCase() === "cancel") {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
refinementRound++;
|
|
1079
|
+
trackEvent("refinement_message_sent", { refinement_round: refinementRound });
|
|
1080
|
+
let refined = null;
|
|
1081
|
+
const refineSpinner = ora2("Refining setup...").start();
|
|
1082
|
+
await apiStream(
|
|
1083
|
+
"/api/setups/refine",
|
|
1084
|
+
{ currentSetup, message, conversationHistory: history },
|
|
1085
|
+
() => {
|
|
1086
|
+
},
|
|
1087
|
+
(payload) => {
|
|
1088
|
+
refined = payload.setup;
|
|
1089
|
+
},
|
|
1090
|
+
(error) => {
|
|
1091
|
+
refineSpinner.fail(`Refinement error: ${error}`);
|
|
1092
|
+
},
|
|
1093
|
+
(status) => {
|
|
1094
|
+
refineSpinner.text = status;
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
if (refined) {
|
|
1098
|
+
currentSetup = refined;
|
|
1099
|
+
history.push({ role: "user", content: message });
|
|
1100
|
+
history.push({ role: "assistant", content: JSON.stringify(refined) });
|
|
1101
|
+
refineSpinner.succeed("Setup updated");
|
|
1102
|
+
printSetupSummary(refined);
|
|
1103
|
+
console.log(chalk2.dim('Type "done" to accept, or describe more changes.'));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function promptInput(question) {
|
|
1108
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1109
|
+
return new Promise((resolve) => {
|
|
1110
|
+
rl.question(chalk2.cyan(`${question} `), (answer) => {
|
|
1111
|
+
rl.close();
|
|
1112
|
+
resolve(answer.trim());
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
function promptAgent() {
|
|
1117
|
+
return new Promise((resolve) => {
|
|
1118
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1119
|
+
console.log(chalk2.bold("Which coding agent are you using?"));
|
|
1120
|
+
console.log(" 1. Claude Code");
|
|
1121
|
+
console.log(" 2. Cursor");
|
|
1122
|
+
console.log(" 3. Both");
|
|
1123
|
+
rl.question(chalk2.cyan("\nChoose (1-3): "), (answer) => {
|
|
1124
|
+
rl.close();
|
|
1125
|
+
const map = { "1": "claude", "2": "cursor", "3": "both" };
|
|
1126
|
+
resolve(map[answer.trim()] || "claude");
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function promptAction() {
|
|
1131
|
+
return new Promise((resolve) => {
|
|
1132
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1133
|
+
console.log(chalk2.bold("What would you like to do?"));
|
|
1134
|
+
console.log(" 1. Accept and apply");
|
|
1135
|
+
console.log(" 2. Refine via chat");
|
|
1136
|
+
console.log(" 3. Explain recommendations");
|
|
1137
|
+
console.log(" 4. Decline");
|
|
1138
|
+
rl.question(chalk2.cyan("\nChoose (1-4): "), (answer) => {
|
|
1139
|
+
rl.close();
|
|
1140
|
+
const map = {
|
|
1141
|
+
"1": "accept",
|
|
1142
|
+
"2": "refine",
|
|
1143
|
+
"3": "explain",
|
|
1144
|
+
"4": "decline"
|
|
1145
|
+
};
|
|
1146
|
+
resolve(map[answer.trim()] || "accept");
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
function printSetupSummary(setup) {
|
|
1151
|
+
const claude = setup.claude;
|
|
1152
|
+
const cursor = setup.cursor;
|
|
1153
|
+
console.log("");
|
|
1154
|
+
if (claude) {
|
|
1155
|
+
if (claude.claudeMd) {
|
|
1156
|
+
console.log(` ${chalk2.green("+")} CLAUDE.md ${chalk2.dim("\u2014 project guidelines, commands, conventions")}`);
|
|
1157
|
+
}
|
|
1158
|
+
if (claude.settings) {
|
|
1159
|
+
console.log(` ${chalk2.green("+")} .claude/settings.json ${chalk2.dim("\u2014 tool permissions & hooks")}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (claude.settingsLocal) {
|
|
1162
|
+
console.log(` ${chalk2.green("+")} .claude/settings.local.json ${chalk2.dim("\u2014 local dev permissions")}`);
|
|
1163
|
+
}
|
|
1164
|
+
const skills = claude.skills;
|
|
1165
|
+
if (Array.isArray(skills) && skills.length > 0) {
|
|
1166
|
+
console.log(` ${chalk2.green("+")} ${skills.length} skill${skills.length > 1 ? "s" : ""}:`);
|
|
1167
|
+
for (const skill of skills) {
|
|
1168
|
+
console.log(` ${chalk2.dim("\u2022")} ${skill.name}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const mcpServers = claude.mcpServers;
|
|
1172
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
1173
|
+
const names = Object.keys(mcpServers);
|
|
1174
|
+
console.log(` ${chalk2.green("+")} ${names.length} MCP server${names.length > 1 ? "s" : ""}:`);
|
|
1175
|
+
for (const name of names) {
|
|
1176
|
+
console.log(` ${chalk2.dim("\u2022")} ${name}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (cursor) {
|
|
1181
|
+
if (cursor.cursorrules) {
|
|
1182
|
+
console.log(` ${chalk2.green("+")} .cursorrules ${chalk2.dim("\u2014 coding rules for Cursor")}`);
|
|
1183
|
+
}
|
|
1184
|
+
const rules = cursor.rules;
|
|
1185
|
+
if (Array.isArray(rules) && rules.length > 0) {
|
|
1186
|
+
console.log(` ${chalk2.green("+")} ${rules.length} Cursor rule${rules.length > 1 ? "s" : ""}:`);
|
|
1187
|
+
for (const rule of rules) {
|
|
1188
|
+
console.log(` ${chalk2.dim("\u2022")} ${rule.filename}`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const mcpServers = cursor.mcpServers;
|
|
1192
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
1193
|
+
const names = Object.keys(mcpServers);
|
|
1194
|
+
console.log(` ${chalk2.green("+")} ${names.length} MCP server${names.length > 1 ? "s" : ""}:`);
|
|
1195
|
+
for (const name of names) {
|
|
1196
|
+
console.log(` ${chalk2.dim("\u2022")} ${name}`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
console.log("");
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/commands/undo.ts
|
|
1204
|
+
import chalk3 from "chalk";
|
|
1205
|
+
import ora3 from "ora";
|
|
1206
|
+
function undoCommand() {
|
|
1207
|
+
const spinner = ora3("Reverting setup...").start();
|
|
1208
|
+
try {
|
|
1209
|
+
const { restored, removed } = undoSetup();
|
|
1210
|
+
if (restored.length === 0 && removed.length === 0) {
|
|
1211
|
+
spinner.info("Nothing to undo.");
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
spinner.succeed("Setup reverted successfully.\n");
|
|
1215
|
+
trackEvent("setup_undone", { restored: restored.length, removed: removed.length });
|
|
1216
|
+
if (restored.length > 0) {
|
|
1217
|
+
console.log(chalk3.cyan(" Restored from backup:"));
|
|
1218
|
+
for (const file of restored) {
|
|
1219
|
+
console.log(` ${chalk3.green("\u21A9")} ${file}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (removed.length > 0) {
|
|
1223
|
+
console.log(chalk3.cyan(" Removed:"));
|
|
1224
|
+
for (const file of removed) {
|
|
1225
|
+
console.log(` ${chalk3.red("\u2717")} ${file}`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
console.log("");
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
spinner.fail(chalk3.red(err instanceof Error ? err.message : "Undo failed"));
|
|
1231
|
+
process.exit(1);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/commands/status.ts
|
|
1236
|
+
import chalk4 from "chalk";
|
|
1237
|
+
import fs11 from "fs";
|
|
1238
|
+
function statusCommand(options) {
|
|
1239
|
+
const auth2 = getStoredAuth();
|
|
1240
|
+
const manifest = readManifest();
|
|
1241
|
+
if (options.json) {
|
|
1242
|
+
console.log(JSON.stringify({
|
|
1243
|
+
authenticated: !!auth2,
|
|
1244
|
+
email: auth2?.email,
|
|
1245
|
+
manifest
|
|
1246
|
+
}, null, 2));
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
console.log(chalk4.bold("\nCaliber Status\n"));
|
|
1250
|
+
if (auth2) {
|
|
1251
|
+
console.log(` Auth: ${chalk4.green("Logged in")} as ${auth2.email}`);
|
|
1252
|
+
} else {
|
|
1253
|
+
console.log(` Auth: ${chalk4.yellow("Not logged in")}`);
|
|
1254
|
+
}
|
|
1255
|
+
if (!manifest) {
|
|
1256
|
+
console.log(` Setup: ${chalk4.dim("No setup applied")}`);
|
|
1257
|
+
console.log(chalk4.dim("\n Run `caliber init` to get started.\n"));
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
console.log(` Files managed: ${chalk4.cyan(manifest.entries.length.toString())}`);
|
|
1261
|
+
for (const entry of manifest.entries) {
|
|
1262
|
+
const exists = fs11.existsSync(entry.path);
|
|
1263
|
+
const icon = exists ? chalk4.green("\u2713") : chalk4.red("\u2717");
|
|
1264
|
+
console.log(` ${icon} ${entry.path} (${entry.action})`);
|
|
1265
|
+
}
|
|
1266
|
+
console.log("");
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/commands/update.ts
|
|
1270
|
+
import chalk5 from "chalk";
|
|
1271
|
+
import ora4 from "ora";
|
|
1272
|
+
import readline2 from "readline";
|
|
1273
|
+
async function updateCommand(options) {
|
|
1274
|
+
const auth2 = getStoredAuth();
|
|
1275
|
+
if (!auth2) {
|
|
1276
|
+
console.log(chalk5.red("Not logged in. Run `caliber login` first."));
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
const manifest = readManifest();
|
|
1280
|
+
if (!manifest) {
|
|
1281
|
+
console.log(chalk5.yellow("No existing setup found. Run `caliber init` first."));
|
|
1282
|
+
process.exit(1);
|
|
1283
|
+
}
|
|
1284
|
+
const spinner = ora4("Re-analyzing project...").start();
|
|
1285
|
+
const fingerprint = collectFingerprint(process.cwd());
|
|
1286
|
+
spinner.succeed("Project re-analyzed");
|
|
1287
|
+
trackEvent("scan_completed", {
|
|
1288
|
+
languages: fingerprint.languages,
|
|
1289
|
+
frameworks: fingerprint.frameworks,
|
|
1290
|
+
has_existing_config: !!(fingerprint.existingConfigs.claudeMd || fingerprint.existingConfigs.cursorrules),
|
|
1291
|
+
has_claude_md: !!fingerprint.existingConfigs.claudeMd,
|
|
1292
|
+
has_claude_settings: !!fingerprint.existingConfigs.claudeSettings,
|
|
1293
|
+
has_cursorrules: !!fingerprint.existingConfigs.cursorrules,
|
|
1294
|
+
cursor_rules_count: fingerprint.existingConfigs.cursorRules?.length ?? 0,
|
|
1295
|
+
skills_count: fingerprint.existingConfigs.claudeSkills?.length ?? 0,
|
|
1296
|
+
file_count: fingerprint.fileTree.length
|
|
1297
|
+
});
|
|
1298
|
+
let generatedSetup = null;
|
|
1299
|
+
trackEvent("generation_started", { target_agent: "both" });
|
|
1300
|
+
const generationStart = Date.now();
|
|
1301
|
+
const genSpinner = ora4("Regenerating setup...").start();
|
|
1302
|
+
await apiStream(
|
|
1303
|
+
"/api/setups/generate",
|
|
1304
|
+
{
|
|
1305
|
+
fingerprint,
|
|
1306
|
+
targetAgent: "both"
|
|
1307
|
+
},
|
|
1308
|
+
() => {
|
|
1309
|
+
},
|
|
1310
|
+
(payload) => {
|
|
1311
|
+
generatedSetup = payload.setup;
|
|
1312
|
+
},
|
|
1313
|
+
(error) => {
|
|
1314
|
+
trackEvent("error_occurred", { error_type: "generation_failed", error_message: error });
|
|
1315
|
+
genSpinner.fail(`Generation error: ${error}`);
|
|
1316
|
+
},
|
|
1317
|
+
(status) => {
|
|
1318
|
+
genSpinner.text = status;
|
|
1319
|
+
}
|
|
1320
|
+
);
|
|
1321
|
+
if (!generatedSetup) {
|
|
1322
|
+
genSpinner.fail("Failed to regenerate setup.");
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1325
|
+
genSpinner.succeed("Setup regenerated");
|
|
1326
|
+
trackEvent("generation_completed", {
|
|
1327
|
+
target_agent: "both",
|
|
1328
|
+
duration_ms: Date.now() - generationStart,
|
|
1329
|
+
files_suggested: countSuggestedFiles(generatedSetup)
|
|
1330
|
+
});
|
|
1331
|
+
if (options.dryRun) {
|
|
1332
|
+
console.log(chalk5.yellow("\n[Dry run] Would write:"));
|
|
1333
|
+
console.log(JSON.stringify(generatedSetup, null, 2));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1337
|
+
const answer = await new Promise((resolve) => {
|
|
1338
|
+
rl.question(chalk5.cyan("\n\nApply updated setup? (y/n): "), resolve);
|
|
1339
|
+
});
|
|
1340
|
+
rl.close();
|
|
1341
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
1342
|
+
trackEvent("generation_cancelled");
|
|
1343
|
+
console.log(chalk5.dim("Update cancelled."));
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const writeSpinner = ora4("Updating config files...").start();
|
|
1347
|
+
const result = writeSetup(generatedSetup);
|
|
1348
|
+
writeSpinner.succeed("Config files updated");
|
|
1349
|
+
trackEvent("setup_updated", { files_written: result.written.length });
|
|
1350
|
+
for (const file of result.written) {
|
|
1351
|
+
console.log(` ${chalk5.green("\u2713")} ${file}`);
|
|
1352
|
+
}
|
|
1353
|
+
console.log("");
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/commands/logout.ts
|
|
1357
|
+
import chalk6 from "chalk";
|
|
1358
|
+
function logoutCommand() {
|
|
1359
|
+
const auth2 = getStoredAuth();
|
|
1360
|
+
if (!auth2) {
|
|
1361
|
+
console.log(chalk6.dim("Not currently logged in."));
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
clearAuth();
|
|
1365
|
+
trackEvent("logout");
|
|
1366
|
+
console.log(chalk6.green("Logged out successfully."));
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/cli.ts
|
|
1370
|
+
var program = new Command();
|
|
1371
|
+
program.name("caliber").description("Configure your coding agent environment").version("0.1.0");
|
|
1372
|
+
program.command("init").description("Initialize coding agent setup for this project").option("--agent <type>", "Target agent: claude, cursor, or both").option("--dry-run", "Preview changes without writing files").option("--force", "Overwrite existing setup without prompting").action(initCommand);
|
|
1373
|
+
program.command("undo").description("Revert all config changes made by Caliber").action(undoCommand);
|
|
1374
|
+
program.command("status").description("Show current Caliber setup status").option("--json", "Output as JSON").action(statusCommand);
|
|
1375
|
+
program.command("update").description("Re-analyze project and update setup").option("--dry-run", "Preview changes without writing files").action(updateCommand);
|
|
1376
|
+
program.command("login").description("Authenticate with Caliber").action(loginCommand);
|
|
1377
|
+
program.command("logout").description("Clear stored credentials").action(logoutCommand);
|
|
1378
|
+
|
|
1379
|
+
// src/bin.ts
|
|
1380
|
+
var firstRun = isFirstRun();
|
|
1381
|
+
getDeviceId();
|
|
1382
|
+
if (firstRun) {
|
|
1383
|
+
trackEvent("cli_installed");
|
|
1384
|
+
}
|
|
1385
|
+
var auth = getStoredAuth();
|
|
1386
|
+
if (auth) {
|
|
1387
|
+
identifyUser(auth.userId, auth.email);
|
|
1388
|
+
}
|
|
1389
|
+
process.on("SIGINT", () => process.exit(130));
|
|
1390
|
+
process.on("SIGTERM", () => process.exit(143));
|
|
1391
|
+
program.parseAsync().finally(async () => {
|
|
1392
|
+
await shutdownTelemetry();
|
|
1393
|
+
process.exit(0);
|
|
1394
|
+
});
|
|
1395
|
+
//# sourceMappingURL=bin.js.map
|