codeam-cli 2.2.2 → 2.4.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/CHANGELOG.md +12 -0
- package/dist/index.js +621 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `codeam-cli` are documented here.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.2.2] — 2026-05-02
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **clients:** Recursive suffix search for read_file (v2.2.2)
|
|
12
|
+
|
|
13
|
+
## [2.2.1] — 2026-05-02
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **cli:** Subdir fallback for read_file when CLI cwd is a monorepo parent (v2.2.1)
|
|
18
|
+
|
|
7
19
|
## [2.1.0] — 2026-04-25
|
|
8
20
|
|
|
9
21
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -87,11 +87,11 @@ var require_src = __commonJS({
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
// src/commands/start.ts
|
|
90
|
-
var
|
|
90
|
+
var fs7 = __toESM(require("fs"));
|
|
91
91
|
var os5 = __toESM(require("os"));
|
|
92
|
-
var
|
|
92
|
+
var path7 = __toESM(require("path"));
|
|
93
93
|
var import_crypto = require("crypto");
|
|
94
|
-
var
|
|
94
|
+
var import_child_process4 = require("child_process");
|
|
95
95
|
var import_picocolors2 = __toESM(require("picocolors"));
|
|
96
96
|
|
|
97
97
|
// src/config.ts
|
|
@@ -179,7 +179,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
|
179
179
|
// package.json
|
|
180
180
|
var package_default = {
|
|
181
181
|
name: "codeam-cli",
|
|
182
|
-
version: "2.
|
|
182
|
+
version: "2.4.0",
|
|
183
183
|
description: "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands \u2014 from anywhere.",
|
|
184
184
|
main: "dist/index.js",
|
|
185
185
|
bin: {
|
|
@@ -2163,7 +2163,15 @@ var startCommandSchema = import_zod2.z.object({
|
|
|
2163
2163
|
// `path` is bounded to 4096 chars (a comfortable POSIX path max) so a
|
|
2164
2164
|
// malformed payload can't blow up the disk-side validator.
|
|
2165
2165
|
path: import_zod2.z.string().min(1).max(4096).optional(),
|
|
2166
|
-
content: import_zod2.z.string().optional()
|
|
2166
|
+
content: import_zod2.z.string().optional(),
|
|
2167
|
+
// Mini-IDE / project ops. `paths` (plural, strings) is used for
|
|
2168
|
+
// git_commit's optional file selection — distinct from `files`
|
|
2169
|
+
// (FileEntry[]) used by `start_task` for attachments.
|
|
2170
|
+
query: import_zod2.z.string().max(256).optional(),
|
|
2171
|
+
message: import_zod2.z.string().max(8e3).optional(),
|
|
2172
|
+
paths: import_zod2.z.array(import_zod2.z.string().max(4096)).optional(),
|
|
2173
|
+
side: import_zod2.z.enum(["ours", "theirs"]).optional(),
|
|
2174
|
+
limit: import_zod2.z.number().int().min(1).max(500).optional()
|
|
2167
2175
|
});
|
|
2168
2176
|
function parsePayload(schema, raw) {
|
|
2169
2177
|
const result = schema.safeParse(raw);
|
|
@@ -2212,8 +2220,8 @@ function isUnder(parent, candidate) {
|
|
|
2212
2220
|
}
|
|
2213
2221
|
async function isExistingFile(absPath) {
|
|
2214
2222
|
try {
|
|
2215
|
-
const
|
|
2216
|
-
return
|
|
2223
|
+
const stat3 = await fs5.stat(absPath);
|
|
2224
|
+
return stat3.isFile();
|
|
2217
2225
|
} catch {
|
|
2218
2226
|
return false;
|
|
2219
2227
|
}
|
|
@@ -2286,9 +2294,9 @@ async function readProjectFile(rawPath) {
|
|
|
2286
2294
|
if (!abs) {
|
|
2287
2295
|
return { error: `File not found in the project tree: ${rawPath}` };
|
|
2288
2296
|
}
|
|
2289
|
-
const
|
|
2290
|
-
if (
|
|
2291
|
-
return { error: `File too large (${(
|
|
2297
|
+
const stat3 = await fs5.stat(abs);
|
|
2298
|
+
if (stat3.size > MAX_FILE_BYTES) {
|
|
2299
|
+
return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
|
|
2292
2300
|
}
|
|
2293
2301
|
const buf = await fs5.readFile(abs);
|
|
2294
2302
|
if (looksBinary(buf)) {
|
|
@@ -2318,12 +2326,261 @@ async function writeProjectFile(rawPath, content) {
|
|
|
2318
2326
|
}
|
|
2319
2327
|
}
|
|
2320
2328
|
|
|
2329
|
+
// src/services/project-ops.service.ts
|
|
2330
|
+
var import_child_process3 = require("child_process");
|
|
2331
|
+
var import_util = require("util");
|
|
2332
|
+
var fs6 = __toESM(require("fs/promises"));
|
|
2333
|
+
var path6 = __toESM(require("path"));
|
|
2334
|
+
var execFileP = (0, import_util.promisify)(import_child_process3.execFile);
|
|
2335
|
+
var PROJECT_IGNORE = /* @__PURE__ */ new Set([
|
|
2336
|
+
"node_modules",
|
|
2337
|
+
".git",
|
|
2338
|
+
".next",
|
|
2339
|
+
".expo",
|
|
2340
|
+
"dist",
|
|
2341
|
+
"build",
|
|
2342
|
+
"out",
|
|
2343
|
+
".cache",
|
|
2344
|
+
"coverage",
|
|
2345
|
+
".turbo",
|
|
2346
|
+
".parcel-cache",
|
|
2347
|
+
".idea",
|
|
2348
|
+
".vscode",
|
|
2349
|
+
".vscode-test",
|
|
2350
|
+
"ios",
|
|
2351
|
+
"android",
|
|
2352
|
+
".gradle",
|
|
2353
|
+
".cxx",
|
|
2354
|
+
".intellijPlatform",
|
|
2355
|
+
".kotlin",
|
|
2356
|
+
"tmp",
|
|
2357
|
+
"target",
|
|
2358
|
+
"venv",
|
|
2359
|
+
".venv",
|
|
2360
|
+
".mypy_cache",
|
|
2361
|
+
".pytest_cache",
|
|
2362
|
+
"__pycache__",
|
|
2363
|
+
".DS_Store"
|
|
2364
|
+
]);
|
|
2365
|
+
var MAX_TREE_FILES = 5e3;
|
|
2366
|
+
var MAX_DIFF_BYTES = 512 * 1024;
|
|
2367
|
+
var MAX_GIT_OUTPUT = 256 * 1024;
|
|
2368
|
+
async function listProjectFiles(opts = {}) {
|
|
2369
|
+
const root = opts.cwd ?? process.cwd();
|
|
2370
|
+
const cap = opts.cap ?? MAX_TREE_FILES;
|
|
2371
|
+
const q2 = (opts.query ?? "").trim().toLowerCase();
|
|
2372
|
+
const out = [];
|
|
2373
|
+
let truncated = false;
|
|
2374
|
+
async function walk(dir, depth) {
|
|
2375
|
+
if (out.length >= cap) {
|
|
2376
|
+
truncated = true;
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
let entries = [];
|
|
2380
|
+
try {
|
|
2381
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
2382
|
+
} catch {
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
for (const e of entries) {
|
|
2386
|
+
if (out.length >= cap) {
|
|
2387
|
+
truncated = true;
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (PROJECT_IGNORE.has(e.name)) continue;
|
|
2391
|
+
const full = path6.join(dir, e.name);
|
|
2392
|
+
if (e.isDirectory()) {
|
|
2393
|
+
if (depth >= 12) continue;
|
|
2394
|
+
await walk(full, depth + 1);
|
|
2395
|
+
} else if (e.isFile()) {
|
|
2396
|
+
const rel = path6.relative(root, full);
|
|
2397
|
+
if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
let size = 0;
|
|
2401
|
+
try {
|
|
2402
|
+
const st3 = await fs6.stat(full);
|
|
2403
|
+
size = st3.size;
|
|
2404
|
+
} catch {
|
|
2405
|
+
}
|
|
2406
|
+
out.push({ path: rel, name: e.name, size });
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
await walk(root, 0);
|
|
2411
|
+
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
2412
|
+
return { files: out, truncated, root };
|
|
2413
|
+
}
|
|
2414
|
+
async function git(args2, cwd) {
|
|
2415
|
+
try {
|
|
2416
|
+
const { stdout, stderr } = await execFileP("git", args2, {
|
|
2417
|
+
cwd: cwd ?? process.cwd(),
|
|
2418
|
+
maxBuffer: MAX_GIT_OUTPUT,
|
|
2419
|
+
timeout: 3e4
|
|
2420
|
+
});
|
|
2421
|
+
return { stdout, stderr, code: 0 };
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
const e = err;
|
|
2424
|
+
return {
|
|
2425
|
+
stdout: e.stdout ?? "",
|
|
2426
|
+
stderr: e.stderr ?? e.message ?? "git failed",
|
|
2427
|
+
code: typeof e.code === "number" ? e.code : 1
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
async function gitStatus(cwd) {
|
|
2432
|
+
const root = cwd ?? process.cwd();
|
|
2433
|
+
const r = await git(["status", "--porcelain=v2", "--branch"], root);
|
|
2434
|
+
if (r.code !== 0) {
|
|
2435
|
+
return {
|
|
2436
|
+
branch: null,
|
|
2437
|
+
upstream: null,
|
|
2438
|
+
ahead: 0,
|
|
2439
|
+
behind: 0,
|
|
2440
|
+
entries: [],
|
|
2441
|
+
hasMergeInProgress: false,
|
|
2442
|
+
error: r.stderr.trim()
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
const lines = r.stdout.split("\n").filter(Boolean);
|
|
2446
|
+
let branch = null;
|
|
2447
|
+
let upstream = null;
|
|
2448
|
+
let ahead = 0;
|
|
2449
|
+
let behind = 0;
|
|
2450
|
+
const entries = [];
|
|
2451
|
+
for (const line of lines) {
|
|
2452
|
+
if (line.startsWith("# branch.head ")) branch = line.slice("# branch.head ".length).trim();
|
|
2453
|
+
else if (line.startsWith("# branch.upstream ")) upstream = line.slice("# branch.upstream ".length).trim();
|
|
2454
|
+
else if (line.startsWith("# branch.ab ")) {
|
|
2455
|
+
const m = line.match(/\+(\d+)\s+-(\d+)/);
|
|
2456
|
+
if (m) {
|
|
2457
|
+
ahead = parseInt(m[1], 10);
|
|
2458
|
+
behind = parseInt(m[2], 10);
|
|
2459
|
+
}
|
|
2460
|
+
} else if (line.startsWith("1 ")) {
|
|
2461
|
+
const parts = line.split(" ");
|
|
2462
|
+
const xy = parts[1];
|
|
2463
|
+
const p2 = parts.slice(8).join(" ");
|
|
2464
|
+
entries.push({
|
|
2465
|
+
code: xy,
|
|
2466
|
+
path: p2,
|
|
2467
|
+
staged: xy[0] !== ".",
|
|
2468
|
+
conflict: false
|
|
2469
|
+
});
|
|
2470
|
+
} else if (line.startsWith("2 ")) {
|
|
2471
|
+
const parts = line.split(" ");
|
|
2472
|
+
const xy = parts[1];
|
|
2473
|
+
const tail = parts.slice(9).join(" ");
|
|
2474
|
+
const [newPath, oldPath] = tail.split(" ");
|
|
2475
|
+
entries.push({
|
|
2476
|
+
code: xy,
|
|
2477
|
+
path: newPath ?? "",
|
|
2478
|
+
oldPath: oldPath ?? void 0,
|
|
2479
|
+
staged: xy[0] !== ".",
|
|
2480
|
+
conflict: false
|
|
2481
|
+
});
|
|
2482
|
+
} else if (line.startsWith("? ")) {
|
|
2483
|
+
entries.push({
|
|
2484
|
+
code: "??",
|
|
2485
|
+
path: line.slice(2),
|
|
2486
|
+
staged: false,
|
|
2487
|
+
conflict: false
|
|
2488
|
+
});
|
|
2489
|
+
} else if (line.startsWith("u ")) {
|
|
2490
|
+
const parts = line.split(" ");
|
|
2491
|
+
const xy = parts[1];
|
|
2492
|
+
const p2 = parts.slice(10).join(" ");
|
|
2493
|
+
entries.push({
|
|
2494
|
+
code: xy,
|
|
2495
|
+
path: p2,
|
|
2496
|
+
staged: false,
|
|
2497
|
+
conflict: true
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
let hasMergeInProgress = false;
|
|
2502
|
+
try {
|
|
2503
|
+
const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
|
|
2504
|
+
const mergeHead = path6.isAbsolute(gitDir) ? path6.join(gitDir, "MERGE_HEAD") : path6.join(root, gitDir, "MERGE_HEAD");
|
|
2505
|
+
await fs6.access(mergeHead);
|
|
2506
|
+
hasMergeInProgress = true;
|
|
2507
|
+
} catch {
|
|
2508
|
+
}
|
|
2509
|
+
return { branch, upstream, ahead, behind, entries, hasMergeInProgress };
|
|
2510
|
+
}
|
|
2511
|
+
async function gitDiff(file, cwd) {
|
|
2512
|
+
const args2 = ["diff", "--no-color", "--patch"];
|
|
2513
|
+
if (file) args2.push("--", file);
|
|
2514
|
+
const r = await git(args2, cwd);
|
|
2515
|
+
if (r.code !== 0 && !r.stdout) {
|
|
2516
|
+
return { diff: "", truncated: false, error: r.stderr.trim() };
|
|
2517
|
+
}
|
|
2518
|
+
const truncated = r.stdout.length >= MAX_DIFF_BYTES;
|
|
2519
|
+
return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
|
|
2520
|
+
}
|
|
2521
|
+
async function gitDiffStaged(file, cwd) {
|
|
2522
|
+
const args2 = ["diff", "--cached", "--no-color", "--patch"];
|
|
2523
|
+
if (file) args2.push("--", file);
|
|
2524
|
+
const r = await git(args2, cwd);
|
|
2525
|
+
if (r.code !== 0 && !r.stdout) {
|
|
2526
|
+
return { diff: "", truncated: false, error: r.stderr.trim() };
|
|
2527
|
+
}
|
|
2528
|
+
const truncated = r.stdout.length >= MAX_DIFF_BYTES;
|
|
2529
|
+
return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
|
|
2530
|
+
}
|
|
2531
|
+
async function gitLog(limit = 30, cwd) {
|
|
2532
|
+
const sep2 = "";
|
|
2533
|
+
const fmt = ["%H", "%h", "%an", "%aI", "%s"].join(sep2);
|
|
2534
|
+
const r = await git(["log", `-n${Math.min(limit, 200)}`, `--pretty=format:${fmt}`], cwd);
|
|
2535
|
+
if (r.code !== 0) return { commits: [], error: r.stderr.trim() };
|
|
2536
|
+
const commits = r.stdout.split("\n").filter(Boolean).map((line) => {
|
|
2537
|
+
const [hash, shortHash, author, date, subject] = line.split(sep2);
|
|
2538
|
+
return { hash, shortHash, author, date, subject };
|
|
2539
|
+
});
|
|
2540
|
+
return { commits };
|
|
2541
|
+
}
|
|
2542
|
+
async function gitCommit(message, files, cwd) {
|
|
2543
|
+
if (!message || message.trim().length === 0) {
|
|
2544
|
+
return { error: "Commit message is required." };
|
|
2545
|
+
}
|
|
2546
|
+
if (files && files.length > 0) {
|
|
2547
|
+
const r2 = await git(["add", "--", ...files], cwd);
|
|
2548
|
+
if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
|
|
2549
|
+
} else {
|
|
2550
|
+
const r2 = await git(["add", "-A"], cwd);
|
|
2551
|
+
if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
|
|
2552
|
+
}
|
|
2553
|
+
const r = await git(["commit", "-m", message], cwd);
|
|
2554
|
+
if (r.code !== 0) {
|
|
2555
|
+
return { error: r.stderr.trim() || "git commit failed" };
|
|
2556
|
+
}
|
|
2557
|
+
const head = await git(["rev-parse", "HEAD"], cwd);
|
|
2558
|
+
return { ok: true, commit: head.stdout.trim() };
|
|
2559
|
+
}
|
|
2560
|
+
async function gitPush(cwd) {
|
|
2561
|
+
const r = await git(["push"], cwd);
|
|
2562
|
+
if (r.code !== 0) return { error: r.stderr.trim() || "git push failed" };
|
|
2563
|
+
return { ok: true, output: (r.stdout + r.stderr).trim() };
|
|
2564
|
+
}
|
|
2565
|
+
async function gitPull(cwd) {
|
|
2566
|
+
const r = await git(["pull", "--ff-only"], cwd);
|
|
2567
|
+
if (r.code !== 0) return { error: r.stderr.trim() || "git pull failed" };
|
|
2568
|
+
return { ok: true, output: (r.stdout + r.stderr).trim() };
|
|
2569
|
+
}
|
|
2570
|
+
async function gitResolve(file, side, cwd) {
|
|
2571
|
+
const r = await git(["checkout", `--${side}`, "--", file], cwd);
|
|
2572
|
+
if (r.code !== 0) return { error: r.stderr.trim() || `git checkout --${side} failed` };
|
|
2573
|
+
const add = await git(["add", "--", file], cwd);
|
|
2574
|
+
if (add.code !== 0) return { error: add.stderr.trim() || "git add (resolve) failed" };
|
|
2575
|
+
return { ok: true };
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2321
2578
|
// src/commands/start.ts
|
|
2322
2579
|
function saveFilesTemp(files) {
|
|
2323
2580
|
return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
|
|
2324
2581
|
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
2325
|
-
const tmpPath =
|
|
2326
|
-
|
|
2582
|
+
const tmpPath = path7.join(os5.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
|
|
2583
|
+
fs7.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
|
|
2327
2584
|
return tmpPath;
|
|
2328
2585
|
});
|
|
2329
2586
|
}
|
|
@@ -2396,14 +2653,14 @@ try:
|
|
|
2396
2653
|
sys.exit((st>>8)&0xFF)
|
|
2397
2654
|
except Exception:sys.exit(0)
|
|
2398
2655
|
`;
|
|
2399
|
-
const helperPath =
|
|
2400
|
-
|
|
2656
|
+
const helperPath = path7.join(os5.tmpdir(), "codeam-quota-helper.py");
|
|
2657
|
+
fs7.writeFileSync(helperPath, helperScript, { mode: 420 });
|
|
2401
2658
|
const python = findInPath("python3") ?? findInPath("python");
|
|
2402
2659
|
if (!python) {
|
|
2403
2660
|
quotaFetchInProgress = false;
|
|
2404
2661
|
return;
|
|
2405
2662
|
}
|
|
2406
|
-
const proc = (0,
|
|
2663
|
+
const proc = (0, import_child_process4.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
|
|
2407
2664
|
stdio: ["pipe", "pipe", "ignore"],
|
|
2408
2665
|
cwd: process.cwd(),
|
|
2409
2666
|
env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
|
|
@@ -2429,7 +2686,7 @@ except Exception:sys.exit(0)
|
|
|
2429
2686
|
} catch {
|
|
2430
2687
|
}
|
|
2431
2688
|
try {
|
|
2432
|
-
|
|
2689
|
+
fs7.unlinkSync(helperPath);
|
|
2433
2690
|
} catch {
|
|
2434
2691
|
}
|
|
2435
2692
|
quotaFetchInProgress = false;
|
|
@@ -2479,7 +2736,7 @@ except Exception:sys.exit(0)
|
|
|
2479
2736
|
setTimeout(() => {
|
|
2480
2737
|
for (const p2 of paths) {
|
|
2481
2738
|
try {
|
|
2482
|
-
|
|
2739
|
+
fs7.unlinkSync(p2);
|
|
2483
2740
|
} catch {
|
|
2484
2741
|
}
|
|
2485
2742
|
}
|
|
@@ -2571,6 +2828,62 @@ except Exception:sys.exit(0)
|
|
|
2571
2828
|
await relay.sendResult(cmd.id, "completed", result);
|
|
2572
2829
|
break;
|
|
2573
2830
|
}
|
|
2831
|
+
case "list_files": {
|
|
2832
|
+
const result = await listProjectFiles({ query: parsed.query });
|
|
2833
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2834
|
+
break;
|
|
2835
|
+
}
|
|
2836
|
+
case "git_status": {
|
|
2837
|
+
const result = await gitStatus();
|
|
2838
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2839
|
+
break;
|
|
2840
|
+
}
|
|
2841
|
+
case "git_diff": {
|
|
2842
|
+
const { path: filePath } = parsed;
|
|
2843
|
+
const result = await gitDiff(filePath ?? null);
|
|
2844
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2845
|
+
break;
|
|
2846
|
+
}
|
|
2847
|
+
case "git_diff_staged": {
|
|
2848
|
+
const { path: filePath } = parsed;
|
|
2849
|
+
const result = await gitDiffStaged(filePath ?? null);
|
|
2850
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2851
|
+
break;
|
|
2852
|
+
}
|
|
2853
|
+
case "git_log": {
|
|
2854
|
+
const result = await gitLog(parsed.limit ?? 30);
|
|
2855
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2856
|
+
break;
|
|
2857
|
+
}
|
|
2858
|
+
case "git_commit": {
|
|
2859
|
+
if (!parsed.message) {
|
|
2860
|
+
await relay.sendResult(cmd.id, "failed", { error: "Missing message" });
|
|
2861
|
+
break;
|
|
2862
|
+
}
|
|
2863
|
+
const result = await gitCommit(parsed.message, parsed.paths);
|
|
2864
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2865
|
+
break;
|
|
2866
|
+
}
|
|
2867
|
+
case "git_push": {
|
|
2868
|
+
const result = await gitPush();
|
|
2869
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
case "git_pull": {
|
|
2873
|
+
const result = await gitPull();
|
|
2874
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2875
|
+
break;
|
|
2876
|
+
}
|
|
2877
|
+
case "git_resolve": {
|
|
2878
|
+
const { path: filePath, side } = parsed;
|
|
2879
|
+
if (!filePath || !side) {
|
|
2880
|
+
await relay.sendResult(cmd.id, "failed", { error: "Missing path or side" });
|
|
2881
|
+
break;
|
|
2882
|
+
}
|
|
2883
|
+
const result = await gitResolve(filePath, side);
|
|
2884
|
+
await relay.sendResult(cmd.id, "completed", result);
|
|
2885
|
+
break;
|
|
2886
|
+
}
|
|
2574
2887
|
}
|
|
2575
2888
|
});
|
|
2576
2889
|
ws.addHandler({
|
|
@@ -2598,7 +2911,7 @@ except Exception:sys.exit(0)
|
|
|
2598
2911
|
setTimeout(() => {
|
|
2599
2912
|
for (const p2 of paths) {
|
|
2600
2913
|
try {
|
|
2601
|
-
|
|
2914
|
+
fs7.unlinkSync(p2);
|
|
2602
2915
|
} catch {
|
|
2603
2916
|
}
|
|
2604
2917
|
}
|
|
@@ -4763,6 +5076,294 @@ async function logout() {
|
|
|
4763
5076
|
console.log(import_picocolors6.default.green("\n \u2713 Done. All sessions removed.\n"));
|
|
4764
5077
|
}
|
|
4765
5078
|
|
|
5079
|
+
// src/commands/deploy.ts
|
|
5080
|
+
var fs8 = __toESM(require("fs"));
|
|
5081
|
+
var os6 = __toESM(require("os"));
|
|
5082
|
+
var path8 = __toESM(require("path"));
|
|
5083
|
+
var import_picocolors7 = __toESM(require("picocolors"));
|
|
5084
|
+
|
|
5085
|
+
// src/services/providers/github-codespaces.ts
|
|
5086
|
+
var import_child_process5 = require("child_process");
|
|
5087
|
+
var import_util2 = require("util");
|
|
5088
|
+
var execFileP2 = (0, import_util2.promisify)(import_child_process5.execFile);
|
|
5089
|
+
var MAX_BUFFER = 8 * 1024 * 1024;
|
|
5090
|
+
var GitHubCodespacesProvider = class {
|
|
5091
|
+
id = "github-codespaces";
|
|
5092
|
+
displayName = "GitHub Codespaces";
|
|
5093
|
+
tagline = "Cloud dev environment from any GitHub repo";
|
|
5094
|
+
available = true;
|
|
5095
|
+
async authorize() {
|
|
5096
|
+
try {
|
|
5097
|
+
await execFileP2("gh", ["--version"], { maxBuffer: MAX_BUFFER });
|
|
5098
|
+
} catch {
|
|
5099
|
+
throw new Error(
|
|
5100
|
+
[
|
|
5101
|
+
"GitHub CLI (`gh`) is required for Codespaces deploys.",
|
|
5102
|
+
"Install it with:",
|
|
5103
|
+
" \u2022 macOS: brew install gh",
|
|
5104
|
+
" \u2022 Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md",
|
|
5105
|
+
" \u2022 Windows: winget install --id GitHub.cli",
|
|
5106
|
+
"Then run `gh auth login` and try `codeam deploy` again."
|
|
5107
|
+
].join("\n")
|
|
5108
|
+
);
|
|
5109
|
+
}
|
|
5110
|
+
try {
|
|
5111
|
+
await execFileP2("gh", ["auth", "status"], { maxBuffer: MAX_BUFFER });
|
|
5112
|
+
return;
|
|
5113
|
+
} catch {
|
|
5114
|
+
}
|
|
5115
|
+
await new Promise((resolve2, reject) => {
|
|
5116
|
+
const proc = (0, import_child_process5.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
|
|
5117
|
+
stdio: "inherit"
|
|
5118
|
+
});
|
|
5119
|
+
proc.on("exit", (code) => {
|
|
5120
|
+
if (code === 0) resolve2();
|
|
5121
|
+
else reject(new Error("gh auth login failed."));
|
|
5122
|
+
});
|
|
5123
|
+
proc.on("error", reject);
|
|
5124
|
+
});
|
|
5125
|
+
}
|
|
5126
|
+
async listProjects() {
|
|
5127
|
+
const { stdout } = await execFileP2(
|
|
5128
|
+
"gh",
|
|
5129
|
+
[
|
|
5130
|
+
"repo",
|
|
5131
|
+
"list",
|
|
5132
|
+
"--json",
|
|
5133
|
+
"name,nameWithOwner,description,defaultBranchRef,isPrivate",
|
|
5134
|
+
"--limit",
|
|
5135
|
+
"200"
|
|
5136
|
+
],
|
|
5137
|
+
{ maxBuffer: MAX_BUFFER }
|
|
5138
|
+
);
|
|
5139
|
+
const raw = JSON.parse(stdout);
|
|
5140
|
+
return raw.map((r) => ({
|
|
5141
|
+
id: r.nameWithOwner,
|
|
5142
|
+
name: r.name,
|
|
5143
|
+
fullName: r.nameWithOwner,
|
|
5144
|
+
description: r.description ?? void 0,
|
|
5145
|
+
defaultBranch: r.defaultBranchRef?.name,
|
|
5146
|
+
private: !!r.isPrivate
|
|
5147
|
+
}));
|
|
5148
|
+
}
|
|
5149
|
+
async createWorkspace(projectId) {
|
|
5150
|
+
const { stdout } = await execFileP2(
|
|
5151
|
+
"gh",
|
|
5152
|
+
["codespace", "create", "-R", projectId, "--default-permissions"],
|
|
5153
|
+
{ maxBuffer: MAX_BUFFER, timeout: 12e4 }
|
|
5154
|
+
);
|
|
5155
|
+
const name = stdout.trim().split("\n").filter(Boolean).pop() ?? "";
|
|
5156
|
+
if (!name) {
|
|
5157
|
+
throw new Error("GitHub did not return a codespace name.");
|
|
5158
|
+
}
|
|
5159
|
+
await this.waitUntilAvailable(name);
|
|
5160
|
+
return {
|
|
5161
|
+
id: name,
|
|
5162
|
+
displayName: name,
|
|
5163
|
+
webUrl: `https://github.com/codespaces/${name}`
|
|
5164
|
+
};
|
|
5165
|
+
}
|
|
5166
|
+
async waitUntilAvailable(name) {
|
|
5167
|
+
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
5168
|
+
while (Date.now() < deadline) {
|
|
5169
|
+
const { stdout } = await execFileP2(
|
|
5170
|
+
"gh",
|
|
5171
|
+
["codespace", "list", "--json", "name,state"],
|
|
5172
|
+
{ maxBuffer: MAX_BUFFER }
|
|
5173
|
+
);
|
|
5174
|
+
const list = JSON.parse(stdout);
|
|
5175
|
+
const me2 = list.find((c2) => c2.name === name);
|
|
5176
|
+
if (!me2) throw new Error("Codespace disappeared from the list.");
|
|
5177
|
+
if (me2.state === "Available") return;
|
|
5178
|
+
if (me2.state === "Failed" || me2.state === "Unavailable") {
|
|
5179
|
+
throw new Error(`Codespace state: ${me2.state}.`);
|
|
5180
|
+
}
|
|
5181
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
5182
|
+
}
|
|
5183
|
+
throw new Error("Codespace did not become Available within 5 minutes.");
|
|
5184
|
+
}
|
|
5185
|
+
async exec(workspaceId, command2) {
|
|
5186
|
+
try {
|
|
5187
|
+
const { stdout, stderr } = await execFileP2(
|
|
5188
|
+
"gh",
|
|
5189
|
+
["codespace", "ssh", "-c", workspaceId, "--", command2],
|
|
5190
|
+
{ maxBuffer: MAX_BUFFER, timeout: 6e5 }
|
|
5191
|
+
);
|
|
5192
|
+
return { stdout, stderr, code: 0 };
|
|
5193
|
+
} catch (err) {
|
|
5194
|
+
const e = err;
|
|
5195
|
+
return {
|
|
5196
|
+
stdout: e.stdout ?? "",
|
|
5197
|
+
stderr: e.stderr ?? e.message ?? "gh codespace ssh failed",
|
|
5198
|
+
code: typeof e.code === "number" ? e.code : 1
|
|
5199
|
+
};
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
async streamCommand(workspaceId, command2) {
|
|
5203
|
+
return new Promise((resolve2, reject) => {
|
|
5204
|
+
const proc = (0, import_child_process5.spawn)(
|
|
5205
|
+
"gh",
|
|
5206
|
+
["codespace", "ssh", "-c", workspaceId, "-t", "--", command2],
|
|
5207
|
+
{ stdio: "inherit" }
|
|
5208
|
+
);
|
|
5209
|
+
proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
|
|
5210
|
+
proc.on("error", reject);
|
|
5211
|
+
});
|
|
5212
|
+
}
|
|
5213
|
+
async uploadDirectory(workspaceId, localDir, remoteDir) {
|
|
5214
|
+
await execFileP2(
|
|
5215
|
+
"gh",
|
|
5216
|
+
["codespace", "cp", "-r", "-c", workspaceId, localDir, `remote:${remoteDir}`],
|
|
5217
|
+
{ maxBuffer: MAX_BUFFER, timeout: 3e5 }
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
};
|
|
5221
|
+
|
|
5222
|
+
// src/services/providers/index.ts
|
|
5223
|
+
var PROVIDERS = [
|
|
5224
|
+
new GitHubCodespacesProvider()
|
|
5225
|
+
// Sketches for future providers — uncomment + implement when ready.
|
|
5226
|
+
// new GitpodProvider(),
|
|
5227
|
+
// new CoderProvider(),
|
|
5228
|
+
// new GitLabWebIDEProvider(),
|
|
5229
|
+
];
|
|
5230
|
+
|
|
5231
|
+
// src/commands/deploy.ts
|
|
5232
|
+
async function deploy() {
|
|
5233
|
+
console.log();
|
|
5234
|
+
mt(import_picocolors7.default.bgMagenta(import_picocolors7.default.white(" codeam deploy ")));
|
|
5235
|
+
const provider = await pickProvider();
|
|
5236
|
+
if (!provider) {
|
|
5237
|
+
pt("No provider selected.");
|
|
5238
|
+
process.exit(0);
|
|
5239
|
+
}
|
|
5240
|
+
const authStep = fe();
|
|
5241
|
+
authStep.start(`Authorizing with ${provider.displayName}\u2026`);
|
|
5242
|
+
try {
|
|
5243
|
+
await provider.authorize();
|
|
5244
|
+
authStep.stop(`\u2713 Authorized with ${provider.displayName}`);
|
|
5245
|
+
} catch (err) {
|
|
5246
|
+
authStep.stop(`\u2717 Authorization failed`);
|
|
5247
|
+
pt(err instanceof Error ? err.message : String(err));
|
|
5248
|
+
process.exit(1);
|
|
5249
|
+
}
|
|
5250
|
+
const listStep = fe();
|
|
5251
|
+
listStep.start("Loading your projects\u2026");
|
|
5252
|
+
let projects = [];
|
|
5253
|
+
try {
|
|
5254
|
+
projects = await provider.listProjects();
|
|
5255
|
+
listStep.stop(`\u2713 ${projects.length} project${projects.length === 1 ? "" : "s"} available`);
|
|
5256
|
+
} catch (err) {
|
|
5257
|
+
listStep.stop(`\u2717 Could not list projects`);
|
|
5258
|
+
pt(err instanceof Error ? err.message : String(err));
|
|
5259
|
+
process.exit(1);
|
|
5260
|
+
}
|
|
5261
|
+
if (projects.length === 0) {
|
|
5262
|
+
pt("No projects found on the account.");
|
|
5263
|
+
process.exit(0);
|
|
5264
|
+
}
|
|
5265
|
+
const projectId = await _t({
|
|
5266
|
+
message: "Select a project to deploy:",
|
|
5267
|
+
options: projects.slice(0, 50).map((proj) => ({
|
|
5268
|
+
value: proj.id,
|
|
5269
|
+
label: proj.fullName,
|
|
5270
|
+
hint: proj.description ? proj.description.slice(0, 80) : proj.private ? "private" : "public"
|
|
5271
|
+
}))
|
|
5272
|
+
});
|
|
5273
|
+
if (q(projectId) || typeof projectId !== "string") {
|
|
5274
|
+
pt("Cancelled.");
|
|
5275
|
+
process.exit(0);
|
|
5276
|
+
}
|
|
5277
|
+
const project = projects.find((proj) => proj.id === projectId);
|
|
5278
|
+
const createStep = fe();
|
|
5279
|
+
createStep.start(`Creating workspace for ${project.fullName}\u2026`);
|
|
5280
|
+
let workspace;
|
|
5281
|
+
try {
|
|
5282
|
+
workspace = await provider.createWorkspace(project.id);
|
|
5283
|
+
createStep.stop(`\u2713 Workspace ready: ${workspace.displayName ?? workspace.id}`);
|
|
5284
|
+
} catch (err) {
|
|
5285
|
+
createStep.stop(`\u2717 Workspace creation failed`);
|
|
5286
|
+
pt(err instanceof Error ? err.message : String(err));
|
|
5287
|
+
process.exit(1);
|
|
5288
|
+
}
|
|
5289
|
+
const claudeStep = fe();
|
|
5290
|
+
claudeStep.start("Installing Claude CLI on workspace\u2026");
|
|
5291
|
+
const installResult = await provider.exec(
|
|
5292
|
+
workspace.id,
|
|
5293
|
+
"curl -fsSL https://claude.ai/install.sh | bash"
|
|
5294
|
+
);
|
|
5295
|
+
if (installResult.code !== 0) {
|
|
5296
|
+
claudeStep.stop("\u2717 Claude CLI install failed");
|
|
5297
|
+
pt(installResult.stderr.slice(0, 1e3));
|
|
5298
|
+
process.exit(1);
|
|
5299
|
+
}
|
|
5300
|
+
claudeStep.stop("\u2713 Claude CLI installed");
|
|
5301
|
+
const localClaudeDir = path8.join(os6.homedir(), ".claude");
|
|
5302
|
+
if (fs8.existsSync(localClaudeDir) && fs8.statSync(localClaudeDir).isDirectory()) {
|
|
5303
|
+
const copyStep = fe();
|
|
5304
|
+
copyStep.start("Copying local Claude config to workspace\u2026");
|
|
5305
|
+
try {
|
|
5306
|
+
await provider.uploadDirectory(workspace.id, localClaudeDir, "/home/codespace/.claude");
|
|
5307
|
+
copyStep.stop("\u2713 Claude config copied \u2014 no re-auth needed");
|
|
5308
|
+
} catch (err) {
|
|
5309
|
+
copyStep.stop("\u26A0 Could not copy Claude config \u2014 you may need to login on the workspace");
|
|
5310
|
+
void err;
|
|
5311
|
+
}
|
|
5312
|
+
} else {
|
|
5313
|
+
wt(
|
|
5314
|
+
"No local ~/.claude config found. You can authenticate Claude in the workspace shell when needed.",
|
|
5315
|
+
"Heads up"
|
|
5316
|
+
);
|
|
5317
|
+
}
|
|
5318
|
+
const cliStep = fe();
|
|
5319
|
+
cliStep.start("Installing codeam-cli on workspace\u2026");
|
|
5320
|
+
const cliInstall = await provider.exec(workspace.id, "npm install -g codeam-cli@latest");
|
|
5321
|
+
if (cliInstall.code !== 0) {
|
|
5322
|
+
cliStep.stop("\u2717 codeam-cli install failed");
|
|
5323
|
+
pt(cliInstall.stderr.slice(0, 1e3));
|
|
5324
|
+
process.exit(1);
|
|
5325
|
+
}
|
|
5326
|
+
cliStep.stop("\u2713 codeam-cli installed");
|
|
5327
|
+
wt(
|
|
5328
|
+
[
|
|
5329
|
+
`Workspace: ${import_picocolors7.default.cyan(workspace.displayName ?? workspace.id)}`,
|
|
5330
|
+
workspace.webUrl ? `Web: ${import_picocolors7.default.cyan(workspace.webUrl)}` : "",
|
|
5331
|
+
"",
|
|
5332
|
+
"Starting `codeam pair` on the workspace.",
|
|
5333
|
+
"Scan the QR code below with the CodeAgent Mobile app to finish pairing."
|
|
5334
|
+
].filter(Boolean).join("\n"),
|
|
5335
|
+
"Almost there"
|
|
5336
|
+
);
|
|
5337
|
+
const code = (await provider.streamCommand(workspace.id, "codeam pair")).code;
|
|
5338
|
+
if (code === 0) {
|
|
5339
|
+
gt(import_picocolors7.default.green(`\u2713 Workspace deployed and paired. Drive from your phone, anywhere.`));
|
|
5340
|
+
} else {
|
|
5341
|
+
gt(import_picocolors7.default.yellow(`Pairing exited with code ${code}. Run "codeam pair" inside the codespace if needed.`));
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
async function pickProvider() {
|
|
5345
|
+
const ready = PROVIDERS.filter((prov) => prov.available);
|
|
5346
|
+
if (ready.length === 1) return ready[0];
|
|
5347
|
+
const selection = await _t({
|
|
5348
|
+
message: "Where do you want to deploy?",
|
|
5349
|
+
options: PROVIDERS.map((prov) => ({
|
|
5350
|
+
value: prov.id,
|
|
5351
|
+
label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors7.default.dim("(coming soon)")}`,
|
|
5352
|
+
hint: prov.tagline
|
|
5353
|
+
}))
|
|
5354
|
+
});
|
|
5355
|
+
if (q(selection) || typeof selection !== "string") return null;
|
|
5356
|
+
const found = PROVIDERS.find((prov) => prov.id === selection);
|
|
5357
|
+
if (!found || !found.available) {
|
|
5358
|
+
wt(
|
|
5359
|
+
`${found?.displayName ?? "That provider"} isn\u2019t implemented yet \u2014 we'll ping you on Twitter/X when it ships.`,
|
|
5360
|
+
"Heads up"
|
|
5361
|
+
);
|
|
5362
|
+
return null;
|
|
5363
|
+
}
|
|
5364
|
+
return found;
|
|
5365
|
+
}
|
|
5366
|
+
|
|
4766
5367
|
// src/index.ts
|
|
4767
5368
|
var [, , command, ...args] = process.argv;
|
|
4768
5369
|
async function main() {
|
|
@@ -4775,6 +5376,8 @@ async function main() {
|
|
|
4775
5376
|
return status();
|
|
4776
5377
|
case "logout":
|
|
4777
5378
|
return logout();
|
|
5379
|
+
case "deploy":
|
|
5380
|
+
return deploy();
|
|
4778
5381
|
default:
|
|
4779
5382
|
return start();
|
|
4780
5383
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeam-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands — from anywhere.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|