codeam-cli 2.4.4 → 2.4.6
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 +217 -31
- 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.4.4] — 2026-05-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **cli:** Unblock interactive gh prompts inside codeam deploy (v2.4.4)
|
|
12
|
+
|
|
13
|
+
## [2.4.3] — 2026-05-03
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **cli:** Refresh missing `codespace` scope on existing gh logins (v2.4.3)
|
|
18
|
+
|
|
7
19
|
## [2.4.2] — 2026-05-03
|
|
8
20
|
|
|
9
21
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -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.4.
|
|
182
|
+
version: "2.4.6",
|
|
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: {
|
|
@@ -5143,27 +5143,71 @@ var GitHubCodespacesProvider = class {
|
|
|
5143
5143
|
}
|
|
5144
5144
|
const hasScope = await this.hasCodespaceScope();
|
|
5145
5145
|
if (!hasScope) {
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5146
|
+
const expectedUser = await this.getActiveGhUser();
|
|
5147
|
+
const noteLines = [
|
|
5148
|
+
"Your existing GitHub login is missing the `codespace` scope.",
|
|
5149
|
+
"I'll run `gh auth refresh` to add it \u2014 your browser will open",
|
|
5150
|
+
"for a one-tap approval."
|
|
5151
|
+
];
|
|
5152
|
+
if (expectedUser) {
|
|
5153
|
+
noteLines.push("");
|
|
5154
|
+
noteLines.push(
|
|
5155
|
+
`${import_picocolors7.default.yellow("\u26A0")} Sign in as ${import_picocolors7.default.cyan(expectedUser)} in the browser.`
|
|
5156
|
+
);
|
|
5157
|
+
noteLines.push(
|
|
5158
|
+
" If a different GitHub account is already signed in, sign out"
|
|
5159
|
+
);
|
|
5160
|
+
noteLines.push(
|
|
5161
|
+
" of it first \u2014 or open the URL in an incognito/private window."
|
|
5162
|
+
);
|
|
5163
|
+
}
|
|
5164
|
+
wt(noteLines.join("\n"), "One more permission needed");
|
|
5154
5165
|
resetStdinForChild();
|
|
5155
|
-
await new Promise((resolve2, reject) => {
|
|
5166
|
+
const refreshCode = await new Promise((resolve2, reject) => {
|
|
5156
5167
|
const proc = (0, import_child_process5.spawn)(
|
|
5157
5168
|
"gh",
|
|
5158
5169
|
["auth", "refresh", "-h", "github.com", "-s", "codespace"],
|
|
5159
5170
|
{ stdio: "inherit" }
|
|
5160
5171
|
);
|
|
5161
|
-
proc.on("exit", (code) =>
|
|
5162
|
-
if (code === 0) resolve2();
|
|
5163
|
-
else reject(new Error("gh auth refresh failed \u2014 re-run `gh auth refresh -h github.com -s codespace` manually."));
|
|
5164
|
-
});
|
|
5172
|
+
proc.on("exit", (code) => resolve2(code ?? 1));
|
|
5165
5173
|
proc.on("error", reject);
|
|
5166
5174
|
});
|
|
5175
|
+
if (refreshCode !== 0) {
|
|
5176
|
+
const lines = [
|
|
5177
|
+
"The browser approval came back for a different GitHub account",
|
|
5178
|
+
`than the one gh is configured for${expectedUser ? ` (${import_picocolors7.default.cyan(expectedUser)})` : ""}.`,
|
|
5179
|
+
"",
|
|
5180
|
+
"To recover:",
|
|
5181
|
+
" 1. Open https://github.com and sign out of any non-target",
|
|
5182
|
+
` account${expectedUser ? ` (or open the URL in an incognito window)` : ""}.`,
|
|
5183
|
+
" 2. Re-run codeam deploy.",
|
|
5184
|
+
"",
|
|
5185
|
+
"You can also grant the scope manually first and skip this step",
|
|
5186
|
+
"on the next run:",
|
|
5187
|
+
` ${import_picocolors7.default.cyan("gh auth refresh -h github.com -s codespace")}`
|
|
5188
|
+
];
|
|
5189
|
+
throw new Error(lines.join("\n"));
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
/**
|
|
5194
|
+
* Return the GitHub login that the current `gh` token belongs to,
|
|
5195
|
+
* or `null` if the call fails. Used to tell the user which account
|
|
5196
|
+
* they need to authenticate as in the browser when refreshing
|
|
5197
|
+
* scopes — multi-account browser sessions are the #1 cause of
|
|
5198
|
+
* `gh auth refresh` failures.
|
|
5199
|
+
*/
|
|
5200
|
+
async getActiveGhUser() {
|
|
5201
|
+
try {
|
|
5202
|
+
const { stdout } = await execFileP2(
|
|
5203
|
+
"gh",
|
|
5204
|
+
["api", "user", "--jq", ".login"],
|
|
5205
|
+
{ maxBuffer: MAX_BUFFER }
|
|
5206
|
+
);
|
|
5207
|
+
const login = stdout.trim();
|
|
5208
|
+
return login.length > 0 ? login : null;
|
|
5209
|
+
} catch {
|
|
5210
|
+
return null;
|
|
5167
5211
|
}
|
|
5168
5212
|
}
|
|
5169
5213
|
/**
|
|
@@ -5420,7 +5464,7 @@ var GitHubCodespacesProvider = class {
|
|
|
5420
5464
|
return new Promise((resolve2, reject) => {
|
|
5421
5465
|
const proc = (0, import_child_process5.spawn)(
|
|
5422
5466
|
"gh",
|
|
5423
|
-
["codespace", "ssh", "-c", workspaceId, "
|
|
5467
|
+
["codespace", "ssh", "-c", workspaceId, "--", "-tt", command2],
|
|
5424
5468
|
{ stdio: "inherit" }
|
|
5425
5469
|
);
|
|
5426
5470
|
proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
|
|
@@ -5428,13 +5472,88 @@ var GitHubCodespacesProvider = class {
|
|
|
5428
5472
|
});
|
|
5429
5473
|
}
|
|
5430
5474
|
async uploadDirectory(workspaceId, localDir, remoteDir) {
|
|
5431
|
-
|
|
5432
|
-
"
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5475
|
+
const sshArgs = [
|
|
5476
|
+
"codespace",
|
|
5477
|
+
"ssh",
|
|
5478
|
+
"-c",
|
|
5479
|
+
workspaceId,
|
|
5480
|
+
"--",
|
|
5481
|
+
`mkdir -p ${shellQuote(remoteDir)} && tar -xzf - -C ${shellQuote(remoteDir)}`
|
|
5482
|
+
];
|
|
5483
|
+
await new Promise((resolve2, reject) => {
|
|
5484
|
+
const tar = (0, import_child_process5.spawn)("tar", ["-czf", "-", "-C", localDir, "."], {
|
|
5485
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5486
|
+
});
|
|
5487
|
+
const ssh = (0, import_child_process5.spawn)("gh", sshArgs, {
|
|
5488
|
+
stdio: [tar.stdout, "pipe", "pipe"]
|
|
5489
|
+
});
|
|
5490
|
+
let tarErr = "";
|
|
5491
|
+
let sshErr = "";
|
|
5492
|
+
tar.stderr?.on("data", (d3) => {
|
|
5493
|
+
tarErr += d3.toString();
|
|
5494
|
+
});
|
|
5495
|
+
ssh.stderr?.on("data", (d3) => {
|
|
5496
|
+
sshErr += d3.toString();
|
|
5497
|
+
});
|
|
5498
|
+
tar.on("error", reject);
|
|
5499
|
+
ssh.on("error", reject);
|
|
5500
|
+
ssh.on("exit", (code) => {
|
|
5501
|
+
if (code === 0) {
|
|
5502
|
+
resolve2();
|
|
5503
|
+
} else {
|
|
5504
|
+
const reason = (sshErr || tarErr || `exit ${code}`).trim().slice(0, 500);
|
|
5505
|
+
reject(new Error(`Remote tar failed: ${reason}`));
|
|
5506
|
+
}
|
|
5507
|
+
});
|
|
5508
|
+
});
|
|
5509
|
+
}
|
|
5510
|
+
async listExistingWorkspaces(projectId) {
|
|
5511
|
+
try {
|
|
5512
|
+
const { stdout } = await execFileP2(
|
|
5513
|
+
"gh",
|
|
5514
|
+
[
|
|
5515
|
+
"codespace",
|
|
5516
|
+
"list",
|
|
5517
|
+
"--repo",
|
|
5518
|
+
projectId,
|
|
5519
|
+
"--json",
|
|
5520
|
+
"name,displayName,state,lastUsedAt"
|
|
5521
|
+
],
|
|
5522
|
+
{ maxBuffer: MAX_BUFFER }
|
|
5523
|
+
);
|
|
5524
|
+
const list = JSON.parse(stdout);
|
|
5525
|
+
return list.map((c2) => ({
|
|
5526
|
+
id: c2.name,
|
|
5527
|
+
displayName: c2.displayName || c2.name,
|
|
5528
|
+
webUrl: `https://github.com/codespaces/${c2.name}`,
|
|
5529
|
+
state: c2.state,
|
|
5530
|
+
lastUsedAt: c2.lastUsedAt
|
|
5531
|
+
}));
|
|
5532
|
+
} catch {
|
|
5533
|
+
return [];
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
async startWorkspace(workspaceId) {
|
|
5537
|
+
try {
|
|
5538
|
+
await execFileP2(
|
|
5539
|
+
"gh",
|
|
5540
|
+
["api", "-X", "POST", `/user/codespaces/${workspaceId}/start`],
|
|
5541
|
+
{ maxBuffer: MAX_BUFFER, timeout: 6e4 }
|
|
5542
|
+
);
|
|
5543
|
+
} catch (err) {
|
|
5544
|
+
void err;
|
|
5545
|
+
}
|
|
5546
|
+
await this.waitUntilAvailable(workspaceId);
|
|
5547
|
+
return {
|
|
5548
|
+
id: workspaceId,
|
|
5549
|
+
displayName: workspaceId,
|
|
5550
|
+
webUrl: `https://github.com/codespaces/${workspaceId}`
|
|
5551
|
+
};
|
|
5436
5552
|
}
|
|
5437
5553
|
};
|
|
5554
|
+
function shellQuote(s) {
|
|
5555
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
5556
|
+
}
|
|
5438
5557
|
|
|
5439
5558
|
// src/services/providers/index.ts
|
|
5440
5559
|
var PROVIDERS = [
|
|
@@ -5491,8 +5610,53 @@ async function deploy() {
|
|
|
5491
5610
|
process.exit(0);
|
|
5492
5611
|
}
|
|
5493
5612
|
const project = projects.find((proj) => proj.id === projectId);
|
|
5613
|
+
let workspace = null;
|
|
5614
|
+
if (provider.listExistingWorkspaces && provider.startWorkspace) {
|
|
5615
|
+
const existingStep = fe();
|
|
5616
|
+
existingStep.start("Checking for existing workspaces\u2026");
|
|
5617
|
+
let existing = [];
|
|
5618
|
+
try {
|
|
5619
|
+
existing = await provider.listExistingWorkspaces(project.id);
|
|
5620
|
+
existingStep.stop(
|
|
5621
|
+
existing.length === 0 ? "\xB7 No existing workspaces \u2014 will create a fresh one" : `\u2713 ${existing.length} existing workspace${existing.length === 1 ? "" : "s"} found`
|
|
5622
|
+
);
|
|
5623
|
+
} catch {
|
|
5624
|
+
existingStep.stop("\xB7 Could not list existing workspaces \u2014 will create a fresh one");
|
|
5625
|
+
}
|
|
5626
|
+
if (existing.length > 0) {
|
|
5627
|
+
const choice = await _t({
|
|
5628
|
+
message: "Reuse an existing workspace or create a new one?",
|
|
5629
|
+
options: [
|
|
5630
|
+
...existing.map((w3) => ({
|
|
5631
|
+
value: w3.id,
|
|
5632
|
+
label: w3.displayName ?? w3.id,
|
|
5633
|
+
hint: [w3.state, formatLastUsed(w3.lastUsedAt)].filter(Boolean).join(" \xB7 ")
|
|
5634
|
+
})),
|
|
5635
|
+
{ value: "__new__", label: import_picocolors8.default.green("+ Create a new workspace"), hint: "fresh codespace" }
|
|
5636
|
+
]
|
|
5637
|
+
});
|
|
5638
|
+
if (q(choice)) {
|
|
5639
|
+
pt("Cancelled.");
|
|
5640
|
+
process.exit(0);
|
|
5641
|
+
}
|
|
5642
|
+
if (choice !== "__new__") {
|
|
5643
|
+
const reuseStep = fe();
|
|
5644
|
+
const picked = existing.find((w3) => w3.id === choice);
|
|
5645
|
+
const needsStart = picked.state && picked.state !== "Available";
|
|
5646
|
+
reuseStep.start(needsStart ? `Starting ${picked.displayName ?? picked.id}\u2026` : `Connecting to ${picked.displayName ?? picked.id}\u2026`);
|
|
5647
|
+
try {
|
|
5648
|
+
workspace = await provider.startWorkspace(picked.id);
|
|
5649
|
+
reuseStep.stop(`\u2713 Reusing ${workspace.displayName ?? workspace.id}`);
|
|
5650
|
+
} catch (err) {
|
|
5651
|
+
reuseStep.stop("\u2717 Could not start the existing workspace");
|
|
5652
|
+
pt(err instanceof Error ? err.message : String(err));
|
|
5653
|
+
process.exit(1);
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5494
5658
|
let machineTypeId;
|
|
5495
|
-
if (provider.listMachineTypes) {
|
|
5659
|
+
if (!workspace && provider.listMachineTypes) {
|
|
5496
5660
|
const machineStep = fe();
|
|
5497
5661
|
machineStep.start("Loading machine types\u2026");
|
|
5498
5662
|
let machines = [];
|
|
@@ -5523,16 +5687,17 @@ async function deploy() {
|
|
|
5523
5687
|
machineTypeId = machines[0].id;
|
|
5524
5688
|
}
|
|
5525
5689
|
}
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5690
|
+
if (!workspace) {
|
|
5691
|
+
const createStep = fe();
|
|
5692
|
+
createStep.start(`Creating workspace for ${project.fullName}\u2026`);
|
|
5693
|
+
try {
|
|
5694
|
+
workspace = await provider.createWorkspace(project.id, machineTypeId);
|
|
5695
|
+
createStep.stop(`\u2713 Workspace ready: ${workspace.displayName ?? workspace.id}`);
|
|
5696
|
+
} catch (err) {
|
|
5697
|
+
createStep.stop(`\u2717 Workspace creation failed`);
|
|
5698
|
+
pt(err instanceof Error ? err.message : String(err));
|
|
5699
|
+
process.exit(1);
|
|
5700
|
+
}
|
|
5536
5701
|
}
|
|
5537
5702
|
const claudeStep = fe();
|
|
5538
5703
|
claudeStep.start("Installing Claude CLI on workspace\u2026");
|
|
@@ -5623,6 +5788,27 @@ async function runRemoteClaudeLogin(provider, workspaceId) {
|
|
|
5623
5788
|
);
|
|
5624
5789
|
}
|
|
5625
5790
|
}
|
|
5791
|
+
function formatLastUsed(iso) {
|
|
5792
|
+
if (!iso) return "";
|
|
5793
|
+
const t2 = Date.parse(iso);
|
|
5794
|
+
if (Number.isNaN(t2)) return "";
|
|
5795
|
+
const diffMs = Date.now() - t2;
|
|
5796
|
+
if (diffMs < 0) return "in the future";
|
|
5797
|
+
const minute = 6e4;
|
|
5798
|
+
const hour = 60 * minute;
|
|
5799
|
+
const day = 24 * hour;
|
|
5800
|
+
if (diffMs < minute) return "just now";
|
|
5801
|
+
if (diffMs < hour) {
|
|
5802
|
+
const m = Math.round(diffMs / minute);
|
|
5803
|
+
return `${m} min${m === 1 ? "" : "s"} ago`;
|
|
5804
|
+
}
|
|
5805
|
+
if (diffMs < day) {
|
|
5806
|
+
const h = Math.round(diffMs / hour);
|
|
5807
|
+
return `${h} hour${h === 1 ? "" : "s"} ago`;
|
|
5808
|
+
}
|
|
5809
|
+
const d3 = Math.round(diffMs / day);
|
|
5810
|
+
return `${d3} day${d3 === 1 ? "" : "s"} ago`;
|
|
5811
|
+
}
|
|
5626
5812
|
async function pickProvider() {
|
|
5627
5813
|
const ready = PROVIDERS.filter((prov) => prov.available);
|
|
5628
5814
|
if (ready.length === 1) return ready[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeam-cli",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
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": {
|