codeam-cli 2.4.0 → 2.4.2

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.
Files changed (3) hide show
  1. package/README.md +22 -0
  2. package/dist/index.js +248 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,6 +46,28 @@ That's it. Open the [CodeAgent Mobile app](https://codeagent-mobile.com), enter
46
46
  | `codeam sessions` | List all paired devices |
47
47
  | `codeam status` | Show connection status |
48
48
  | `codeam logout` | Remove all paired sessions |
49
+ | `codeam deploy` | Provision a cloud workspace (GitHub Codespaces) and pair it to your phone |
50
+
51
+ ---
52
+
53
+ ## `codeam deploy` — drive a cloud workspace from your phone
54
+
55
+ Don't want to keep your laptop running while you control Claude from the train? `codeam deploy` spins up a fresh **GitHub Codespace** for any of your repos, installs Claude Code + `codeam-cli` inside it, copies your local Claude credentials so you skip the re-auth (or runs `claude login` interactively if you don't have a local config yet), and finishes by streaming `codeam pair` from inside the codespace — so you get a pairing code on this terminal already wired to the remote workspace.
56
+
57
+ ```bash
58
+ codeam deploy
59
+ ```
60
+
61
+ That's it. You'll be guided through:
62
+
63
+ 1. **Pick a provider** (GitHub Codespaces today; more coming).
64
+ 2. **Pick a repo** from your account.
65
+ 3. **Wait ~1 minute** while the codespace boots and tools install.
66
+ 4. **Scan the QR / enter the code** on the CodeAgent Mobile app — same flow as `codeam pair`, only the agent is now running in the cloud.
67
+
68
+ Requirements: the [GitHub CLI (`gh`)](https://cli.github.com/) installed and authenticated (`gh auth login`). The deploy flow re-uses `gh`'s OAuth — we don't ask for a separate token.
69
+
70
+ Adding more cloud backends (Gitpod, Coder, your own SSH host, …) is a single new file in `apps/cli/src/services/providers/` — the `CloudProvider` interface keeps it pluggable.
49
71
 
50
72
  ---
51
73
 
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.0",
182
+ version: "2.4.2",
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: {
@@ -5080,11 +5080,12 @@ async function logout() {
5080
5080
  var fs8 = __toESM(require("fs"));
5081
5081
  var os6 = __toESM(require("os"));
5082
5082
  var path8 = __toESM(require("path"));
5083
- var import_picocolors7 = __toESM(require("picocolors"));
5083
+ var import_picocolors8 = __toESM(require("picocolors"));
5084
5084
 
5085
5085
  // src/services/providers/github-codespaces.ts
5086
5086
  var import_child_process5 = require("child_process");
5087
5087
  var import_util2 = require("util");
5088
+ var import_picocolors7 = __toESM(require("picocolors"));
5088
5089
  var execFileP2 = (0, import_util2.promisify)(import_child_process5.execFile);
5089
5090
  var MAX_BUFFER = 8 * 1024 * 1024;
5090
5091
  var GitHubCodespacesProvider = class {
@@ -5096,16 +5097,21 @@ var GitHubCodespacesProvider = class {
5096
5097
  try {
5097
5098
  await execFileP2("gh", ["--version"], { maxBuffer: MAX_BUFFER });
5098
5099
  } 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
- );
5100
+ await this.tryInstallGh();
5101
+ try {
5102
+ await execFileP2("gh", ["--version"], { maxBuffer: MAX_BUFFER });
5103
+ } catch {
5104
+ throw new Error(
5105
+ [
5106
+ "GitHub CLI (`gh`) is still not on PATH.",
5107
+ "Install it manually with:",
5108
+ " \u2022 macOS: brew install gh",
5109
+ " \u2022 Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md",
5110
+ " \u2022 Windows: winget install --id GitHub.cli",
5111
+ "Then run `codeam deploy` again."
5112
+ ].join("\n")
5113
+ );
5114
+ }
5109
5115
  }
5110
5116
  try {
5111
5117
  await execFileP2("gh", ["auth", "status"], { maxBuffer: MAX_BUFFER });
@@ -5123,6 +5129,95 @@ var GitHubCodespacesProvider = class {
5123
5129
  proc.on("error", reject);
5124
5130
  });
5125
5131
  }
5132
+ /**
5133
+ * Try to install the `gh` CLI for the user. Opt-in via a confirm
5134
+ * prompt — we never run `brew` / `winget` / `apt` without explicit
5135
+ * consent. Strategy per platform:
5136
+ *
5137
+ * - macOS: `brew install gh` (requires Homebrew)
5138
+ * - Windows: `winget install --id GitHub.cli -e --silent`
5139
+ * - Linux: too many distros / package managers to be safe; we
5140
+ * point the user at the official install doc instead.
5141
+ *
5142
+ * Stdio is inherited so any sudo / authentication prompt the package
5143
+ * manager surfaces (e.g. macOS keychain, Windows UAC) lands in this
5144
+ * terminal. On failure or an unsupported platform we just return —
5145
+ * the caller will re-check `gh --version` and surface the manual-
5146
+ * install error if it's still missing.
5147
+ */
5148
+ async tryInstallGh() {
5149
+ const platform = process.platform;
5150
+ wt(
5151
+ `GitHub CLI (${import_picocolors7.default.cyan("gh")}) is required for Codespaces deploys but isn't on your PATH.`,
5152
+ "Heads up"
5153
+ );
5154
+ if (platform === "linux") {
5155
+ wt(
5156
+ [
5157
+ "On Linux, please install gh from the official guide:",
5158
+ " https://github.com/cli/cli/blob/trunk/docs/install_linux.md",
5159
+ "Re-run `codeam deploy` once it is on your PATH."
5160
+ ].join("\n"),
5161
+ "Install gh on Linux"
5162
+ );
5163
+ return;
5164
+ }
5165
+ let installCmd = null;
5166
+ if (platform === "darwin") {
5167
+ try {
5168
+ await execFileP2("brew", ["--version"], { maxBuffer: MAX_BUFFER });
5169
+ } catch {
5170
+ wt(
5171
+ [
5172
+ "Homebrew (`brew`) is not installed.",
5173
+ "Install it from https://brew.sh and re-run `codeam deploy`,",
5174
+ "or install gh manually: https://cli.github.com/"
5175
+ ].join("\n"),
5176
+ "Cannot auto-install on macOS"
5177
+ );
5178
+ return;
5179
+ }
5180
+ installCmd = {
5181
+ exe: "brew",
5182
+ args: ["install", "gh"],
5183
+ describe: "brew install gh"
5184
+ };
5185
+ } else if (platform === "win32") {
5186
+ try {
5187
+ await execFileP2("winget", ["--version"], { maxBuffer: MAX_BUFFER });
5188
+ } catch {
5189
+ wt(
5190
+ [
5191
+ "winget is not available on this machine.",
5192
+ "Install gh manually: https://github.com/cli/cli/releases/latest"
5193
+ ].join("\n"),
5194
+ "Cannot auto-install on Windows"
5195
+ );
5196
+ return;
5197
+ }
5198
+ installCmd = {
5199
+ exe: "winget",
5200
+ args: ["install", "--id", "GitHub.cli", "-e", "--silent"],
5201
+ describe: "winget install --id GitHub.cli"
5202
+ };
5203
+ } else {
5204
+ return;
5205
+ }
5206
+ const proceed = await ot2({
5207
+ message: `Run ${import_picocolors7.default.cyan(installCmd.describe)} now?`,
5208
+ initialValue: true
5209
+ });
5210
+ if (q(proceed) || !proceed) return;
5211
+ const installStep = fe();
5212
+ installStep.start(`Installing gh via ${installCmd.describe}\u2026`);
5213
+ const ok = await new Promise((resolve2) => {
5214
+ const proc = (0, import_child_process5.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
5215
+ proc.on("exit", (code) => resolve2(code === 0));
5216
+ proc.on("error", () => resolve2(false));
5217
+ });
5218
+ if (ok) installStep.stop("\u2713 gh installed");
5219
+ else installStep.stop("\u2717 gh install failed");
5220
+ }
5126
5221
  async listProjects() {
5127
5222
  const { stdout } = await execFileP2(
5128
5223
  "gh",
@@ -5146,10 +5241,51 @@ var GitHubCodespacesProvider = class {
5146
5241
  private: !!r.isPrivate
5147
5242
  }));
5148
5243
  }
5149
- async createWorkspace(projectId) {
5244
+ /**
5245
+ * Return the machine types available to the user for this repo. The
5246
+ * `gh api /repos/.../codespaces/machines` endpoint reports CPU / RAM /
5247
+ * storage, so we hand all three to the picker for a clean label.
5248
+ *
5249
+ * We filter out anything below 8 GB RAM — Claude Code wants headroom
5250
+ * for `tsc`, build tools, and parallel test runners; the 4 GB tier
5251
+ * (when available) is too tight in practice.
5252
+ */
5253
+ async listMachineTypes(projectId) {
5254
+ try {
5255
+ const { stdout } = await execFileP2(
5256
+ "gh",
5257
+ ["api", `/repos/${projectId}/codespaces/machines`],
5258
+ { maxBuffer: MAX_BUFFER }
5259
+ );
5260
+ const data = JSON.parse(stdout);
5261
+ const machines = data.machines ?? [];
5262
+ const GB = 1024 ** 3;
5263
+ return machines.map((m) => {
5264
+ const memoryGb = m.memory_in_bytes ? Math.round(m.memory_in_bytes / GB) : 0;
5265
+ const storageGb = m.storage_in_bytes ? Math.round(m.storage_in_bytes / GB) : void 0;
5266
+ const parts = [];
5267
+ if (m.cpus) parts.push(`${m.cpus} ${m.cpus === 1 ? "core" : "cores"}`);
5268
+ if (memoryGb) parts.push(`${memoryGb} GB RAM`);
5269
+ if (storageGb) parts.push(`${storageGb} GB storage`);
5270
+ return {
5271
+ id: m.name,
5272
+ label: m.display_name ?? (parts.join(" \xB7 ") || m.name),
5273
+ memoryGb,
5274
+ cpus: m.cpus,
5275
+ storageGb
5276
+ };
5277
+ }).filter((m) => m.memoryGb >= 8).sort((a, b) => a.memoryGb - b.memoryGb || (a.cpus ?? 0) - (b.cpus ?? 0));
5278
+ } catch {
5279
+ return [];
5280
+ }
5281
+ }
5282
+ async createWorkspace(projectId, machineTypeId) {
5283
+ const machine = machineTypeId ?? await this.pickDefaultMachine(projectId);
5284
+ const args2 = ["codespace", "create", "-R", projectId, "--default-permissions"];
5285
+ if (machine) args2.push("-m", machine);
5150
5286
  const { stdout } = await execFileP2(
5151
5287
  "gh",
5152
- ["codespace", "create", "-R", projectId, "--default-permissions"],
5288
+ args2,
5153
5289
  { maxBuffer: MAX_BUFFER, timeout: 12e4 }
5154
5290
  );
5155
5291
  const name = stdout.trim().split("\n").filter(Boolean).pop() ?? "";
@@ -5163,6 +5299,27 @@ var GitHubCodespacesProvider = class {
5163
5299
  webUrl: `https://github.com/codespaces/${name}`
5164
5300
  };
5165
5301
  }
5302
+ /**
5303
+ * Fallback machine picker for when the orchestrator didn't ask the
5304
+ * user — defaults to the cheapest 8 GB tier (`basicLinux32gb`) and
5305
+ * walks up only if the repo restricts that tier. Returns `null` if
5306
+ * the API call fails entirely; the caller will then omit `-m` and
5307
+ * let `gh` use the repo/org default.
5308
+ */
5309
+ async pickDefaultMachine(projectId) {
5310
+ const machines = await this.listMachineTypes(projectId);
5311
+ if (machines.length === 0) return null;
5312
+ const preferenceOrder = [
5313
+ "basicLinux32gb",
5314
+ "standardLinux32gb",
5315
+ "premiumLinux",
5316
+ "largePremiumLinux"
5317
+ ];
5318
+ for (const pref of preferenceOrder) {
5319
+ if (machines.some((m) => m.id === pref)) return pref;
5320
+ }
5321
+ return machines[0].id;
5322
+ }
5166
5323
  async waitUntilAvailable(name) {
5167
5324
  const deadline = Date.now() + 5 * 60 * 1e3;
5168
5325
  while (Date.now() < deadline) {
@@ -5231,7 +5388,7 @@ var PROVIDERS = [
5231
5388
  // src/commands/deploy.ts
5232
5389
  async function deploy() {
5233
5390
  console.log();
5234
- mt(import_picocolors7.default.bgMagenta(import_picocolors7.default.white(" codeam deploy ")));
5391
+ mt(import_picocolors8.default.bgMagenta(import_picocolors8.default.white(" codeam deploy ")));
5235
5392
  const provider = await pickProvider();
5236
5393
  if (!provider) {
5237
5394
  pt("No provider selected.");
@@ -5275,11 +5432,43 @@ async function deploy() {
5275
5432
  process.exit(0);
5276
5433
  }
5277
5434
  const project = projects.find((proj) => proj.id === projectId);
5435
+ let machineTypeId;
5436
+ if (provider.listMachineTypes) {
5437
+ const machineStep = fe();
5438
+ machineStep.start("Loading machine types\u2026");
5439
+ let machines = [];
5440
+ try {
5441
+ machines = await provider.listMachineTypes(project.id);
5442
+ machineStep.stop(
5443
+ machines.length > 0 ? `\u2713 ${machines.length} machine type${machines.length === 1 ? "" : "s"} available` : "\xB7 No machine types reported (using provider default)"
5444
+ );
5445
+ } catch {
5446
+ machineStep.stop("\xB7 Could not list machine types \u2014 using provider default");
5447
+ }
5448
+ if (machines.length > 1) {
5449
+ const picked = await _t({
5450
+ message: "Pick a machine size (starts at 8 GB RAM):",
5451
+ initialValue: machines[0].id,
5452
+ options: machines.map((m) => ({
5453
+ value: m.id,
5454
+ label: m.label,
5455
+ hint: `${m.memoryGb} GB RAM`
5456
+ }))
5457
+ });
5458
+ if (q(picked)) {
5459
+ pt("Cancelled.");
5460
+ process.exit(0);
5461
+ }
5462
+ machineTypeId = picked;
5463
+ } else if (machines.length === 1) {
5464
+ machineTypeId = machines[0].id;
5465
+ }
5466
+ }
5278
5467
  const createStep = fe();
5279
5468
  createStep.start(`Creating workspace for ${project.fullName}\u2026`);
5280
5469
  let workspace;
5281
5470
  try {
5282
- workspace = await provider.createWorkspace(project.id);
5471
+ workspace = await provider.createWorkspace(project.id, machineTypeId);
5283
5472
  createStep.stop(`\u2713 Workspace ready: ${workspace.displayName ?? workspace.id}`);
5284
5473
  } catch (err) {
5285
5474
  createStep.stop(`\u2717 Workspace creation failed`);
@@ -5299,21 +5488,36 @@ async function deploy() {
5299
5488
  }
5300
5489
  claudeStep.stop("\u2713 Claude CLI installed");
5301
5490
  const localClaudeDir = path8.join(os6.homedir(), ".claude");
5302
- if (fs8.existsSync(localClaudeDir) && fs8.statSync(localClaudeDir).isDirectory()) {
5491
+ const haveLocalClaude = fs8.existsSync(localClaudeDir) && fs8.statSync(localClaudeDir).isDirectory();
5492
+ if (haveLocalClaude) {
5303
5493
  const copyStep = fe();
5304
5494
  copyStep.start("Copying local Claude config to workspace\u2026");
5305
5495
  try {
5306
5496
  await provider.uploadDirectory(workspace.id, localClaudeDir, "/home/codespace/.claude");
5307
5497
  copyStep.stop("\u2713 Claude config copied \u2014 no re-auth needed");
5308
5498
  } catch (err) {
5309
- copyStep.stop("\u26A0 Could not copy Claude config \u2014 you may need to login on the workspace");
5499
+ copyStep.stop("\u26A0 Could not copy Claude config \u2014 falling back to remote login");
5310
5500
  void err;
5501
+ await runRemoteClaudeLogin(provider, workspace.id);
5311
5502
  }
5312
5503
  } else {
5313
5504
  wt(
5314
- "No local ~/.claude config found. You can authenticate Claude in the workspace shell when needed.",
5315
- "Heads up"
5505
+ [
5506
+ "No local ~/.claude config found.",
5507
+ "We can run `claude login` inside the workspace right now \u2014 the URL",
5508
+ "will print here, you open it in your browser, paste the code back,",
5509
+ "and the workspace gets authenticated. (Skip if you'd rather do it",
5510
+ "manually later from inside the codespace.)"
5511
+ ].join("\n"),
5512
+ "Claude credentials"
5316
5513
  );
5514
+ const proceed = await ot2({
5515
+ message: "Run `claude login` on the workspace now?",
5516
+ initialValue: true
5517
+ });
5518
+ if (!q(proceed) && proceed) {
5519
+ await runRemoteClaudeLogin(provider, workspace.id);
5520
+ }
5317
5521
  }
5318
5522
  const cliStep = fe();
5319
5523
  cliStep.start("Installing codeam-cli on workspace\u2026");
@@ -5326,8 +5530,8 @@ async function deploy() {
5326
5530
  cliStep.stop("\u2713 codeam-cli installed");
5327
5531
  wt(
5328
5532
  [
5329
- `Workspace: ${import_picocolors7.default.cyan(workspace.displayName ?? workspace.id)}`,
5330
- workspace.webUrl ? `Web: ${import_picocolors7.default.cyan(workspace.webUrl)}` : "",
5533
+ `Workspace: ${import_picocolors8.default.cyan(workspace.displayName ?? workspace.id)}`,
5534
+ workspace.webUrl ? `Web: ${import_picocolors8.default.cyan(workspace.webUrl)}` : "",
5331
5535
  "",
5332
5536
  "Starting `codeam pair` on the workspace.",
5333
5537
  "Scan the QR code below with the CodeAgent Mobile app to finish pairing."
@@ -5336,9 +5540,28 @@ async function deploy() {
5336
5540
  );
5337
5541
  const code = (await provider.streamCommand(workspace.id, "codeam pair")).code;
5338
5542
  if (code === 0) {
5339
- gt(import_picocolors7.default.green(`\u2713 Workspace deployed and paired. Drive from your phone, anywhere.`));
5543
+ gt(import_picocolors8.default.green(`\u2713 Workspace deployed and paired. Drive from your phone, anywhere.`));
5340
5544
  } else {
5341
- gt(import_picocolors7.default.yellow(`Pairing exited with code ${code}. Run "codeam pair" inside the codespace if needed.`));
5545
+ gt(import_picocolors8.default.yellow(`Pairing exited with code ${code}. Run "codeam pair" inside the codespace if needed.`));
5546
+ }
5547
+ }
5548
+ async function runRemoteClaudeLogin(provider, workspaceId) {
5549
+ wt(
5550
+ [
5551
+ "A login URL will print below. Open it in your local browser, sign in,",
5552
+ "and paste any code Claude asks for back into this terminal."
5553
+ ].join("\n"),
5554
+ "Authenticating Claude on workspace"
5555
+ );
5556
+ const result = await provider.streamCommand(
5557
+ workspaceId,
5558
+ 'bash -lc "claude login || claude /login || true"'
5559
+ );
5560
+ if (result.code !== 0) {
5561
+ wt(
5562
+ "claude login exited non-zero. You can re-run it manually inside the codespace later.",
5563
+ "Heads up"
5564
+ );
5342
5565
  }
5343
5566
  }
5344
5567
  async function pickProvider() {
@@ -5348,7 +5571,7 @@ async function pickProvider() {
5348
5571
  message: "Where do you want to deploy?",
5349
5572
  options: PROVIDERS.map((prov) => ({
5350
5573
  value: prov.id,
5351
- label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors7.default.dim("(coming soon)")}`,
5574
+ label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors8.default.dim("(coming soon)")}`,
5352
5575
  hint: prov.tagline
5353
5576
  }))
5354
5577
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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": {