codeam-cli 2.4.1 → 2.4.3

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 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.1] — 2026-05-03
8
+
9
+ ### Added
10
+
11
+ - **cli:** Interactive `claude login` fallback when no local config (v2.4.1)
12
+
13
+ ## [2.4.0] — 2026-05-03
14
+
15
+ ### Added
16
+
17
+ - **cli:** `codeam deploy` — provision a paired cloud workspace in one command (v2.4.0)
18
+
7
19
  ## [2.2.2] — 2026-05-02
8
20
 
9
21
  ### Fixed
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.1",
182
+ version: "2.4.3",
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,32 +5097,175 @@ 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
  }
5116
+ let isAuthed = false;
5110
5117
  try {
5111
5118
  await execFileP2("gh", ["auth", "status"], { maxBuffer: MAX_BUFFER });
5112
- return;
5119
+ isAuthed = true;
5113
5120
  } catch {
5114
5121
  }
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"
5122
+ if (!isAuthed) {
5123
+ await new Promise((resolve2, reject) => {
5124
+ const proc = (0, import_child_process5.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
5125
+ stdio: "inherit"
5126
+ });
5127
+ proc.on("exit", (code) => {
5128
+ if (code === 0) resolve2();
5129
+ else reject(new Error("gh auth login failed."));
5130
+ });
5131
+ proc.on("error", reject);
5118
5132
  });
5119
- proc.on("exit", (code) => {
5120
- if (code === 0) resolve2();
5121
- else reject(new Error("gh auth login failed."));
5133
+ return;
5134
+ }
5135
+ const hasScope = await this.hasCodespaceScope();
5136
+ if (!hasScope) {
5137
+ wt(
5138
+ [
5139
+ "Your existing GitHub login is missing the `codespace` scope.",
5140
+ "I'll run `gh auth refresh` to add it \u2014 your browser will open",
5141
+ "for a one-tap approval."
5142
+ ].join("\n"),
5143
+ "One more permission needed"
5144
+ );
5145
+ await new Promise((resolve2, reject) => {
5146
+ const proc = (0, import_child_process5.spawn)(
5147
+ "gh",
5148
+ ["auth", "refresh", "-h", "github.com", "-s", "codespace"],
5149
+ { stdio: "inherit" }
5150
+ );
5151
+ proc.on("exit", (code) => {
5152
+ if (code === 0) resolve2();
5153
+ else reject(new Error("gh auth refresh failed \u2014 re-run `gh auth refresh -h github.com -s codespace` manually."));
5154
+ });
5155
+ proc.on("error", reject);
5122
5156
  });
5123
- proc.on("error", reject);
5157
+ }
5158
+ }
5159
+ /**
5160
+ * Check whether the current `gh` token includes the `codespace`
5161
+ * OAuth scope. We hit `/user` with `-i` so GitHub echoes the granted
5162
+ * scopes back in the `X-OAuth-Scopes` response header — the most
5163
+ * authoritative source (more reliable than scraping `gh auth status`,
5164
+ * whose format has shifted across `gh` versions).
5165
+ */
5166
+ async hasCodespaceScope() {
5167
+ try {
5168
+ const { stdout } = await execFileP2(
5169
+ "gh",
5170
+ ["api", "-i", "user"],
5171
+ { maxBuffer: MAX_BUFFER }
5172
+ );
5173
+ const m = stdout.match(/^x-oauth-scopes:\s*(.+)$/im);
5174
+ if (!m) return false;
5175
+ const scopes = m[1].split(",").map((s) => s.trim().toLowerCase());
5176
+ return scopes.includes("codespace");
5177
+ } catch {
5178
+ return false;
5179
+ }
5180
+ }
5181
+ /**
5182
+ * Try to install the `gh` CLI for the user. Opt-in via a confirm
5183
+ * prompt — we never run `brew` / `winget` / `apt` without explicit
5184
+ * consent. Strategy per platform:
5185
+ *
5186
+ * - macOS: `brew install gh` (requires Homebrew)
5187
+ * - Windows: `winget install --id GitHub.cli -e --silent`
5188
+ * - Linux: too many distros / package managers to be safe; we
5189
+ * point the user at the official install doc instead.
5190
+ *
5191
+ * Stdio is inherited so any sudo / authentication prompt the package
5192
+ * manager surfaces (e.g. macOS keychain, Windows UAC) lands in this
5193
+ * terminal. On failure or an unsupported platform we just return —
5194
+ * the caller will re-check `gh --version` and surface the manual-
5195
+ * install error if it's still missing.
5196
+ */
5197
+ async tryInstallGh() {
5198
+ const platform = process.platform;
5199
+ wt(
5200
+ `GitHub CLI (${import_picocolors7.default.cyan("gh")}) is required for Codespaces deploys but isn't on your PATH.`,
5201
+ "Heads up"
5202
+ );
5203
+ if (platform === "linux") {
5204
+ wt(
5205
+ [
5206
+ "On Linux, please install gh from the official guide:",
5207
+ " https://github.com/cli/cli/blob/trunk/docs/install_linux.md",
5208
+ "Re-run `codeam deploy` once it is on your PATH."
5209
+ ].join("\n"),
5210
+ "Install gh on Linux"
5211
+ );
5212
+ return;
5213
+ }
5214
+ let installCmd = null;
5215
+ if (platform === "darwin") {
5216
+ try {
5217
+ await execFileP2("brew", ["--version"], { maxBuffer: MAX_BUFFER });
5218
+ } catch {
5219
+ wt(
5220
+ [
5221
+ "Homebrew (`brew`) is not installed.",
5222
+ "Install it from https://brew.sh and re-run `codeam deploy`,",
5223
+ "or install gh manually: https://cli.github.com/"
5224
+ ].join("\n"),
5225
+ "Cannot auto-install on macOS"
5226
+ );
5227
+ return;
5228
+ }
5229
+ installCmd = {
5230
+ exe: "brew",
5231
+ args: ["install", "gh"],
5232
+ describe: "brew install gh"
5233
+ };
5234
+ } else if (platform === "win32") {
5235
+ try {
5236
+ await execFileP2("winget", ["--version"], { maxBuffer: MAX_BUFFER });
5237
+ } catch {
5238
+ wt(
5239
+ [
5240
+ "winget is not available on this machine.",
5241
+ "Install gh manually: https://github.com/cli/cli/releases/latest"
5242
+ ].join("\n"),
5243
+ "Cannot auto-install on Windows"
5244
+ );
5245
+ return;
5246
+ }
5247
+ installCmd = {
5248
+ exe: "winget",
5249
+ args: ["install", "--id", "GitHub.cli", "-e", "--silent"],
5250
+ describe: "winget install --id GitHub.cli"
5251
+ };
5252
+ } else {
5253
+ return;
5254
+ }
5255
+ const proceed = await ot2({
5256
+ message: `Run ${import_picocolors7.default.cyan(installCmd.describe)} now?`,
5257
+ initialValue: true
5258
+ });
5259
+ if (q(proceed) || !proceed) return;
5260
+ const installStep = fe();
5261
+ installStep.start(`Installing gh via ${installCmd.describe}\u2026`);
5262
+ const ok = await new Promise((resolve2) => {
5263
+ const proc = (0, import_child_process5.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
5264
+ proc.on("exit", (code) => resolve2(code === 0));
5265
+ proc.on("error", () => resolve2(false));
5124
5266
  });
5267
+ if (ok) installStep.stop("\u2713 gh installed");
5268
+ else installStep.stop("\u2717 gh install failed");
5125
5269
  }
5126
5270
  async listProjects() {
5127
5271
  const { stdout } = await execFileP2(
@@ -5146,10 +5290,51 @@ var GitHubCodespacesProvider = class {
5146
5290
  private: !!r.isPrivate
5147
5291
  }));
5148
5292
  }
5149
- async createWorkspace(projectId) {
5293
+ /**
5294
+ * Return the machine types available to the user for this repo. The
5295
+ * `gh api /repos/.../codespaces/machines` endpoint reports CPU / RAM /
5296
+ * storage, so we hand all three to the picker for a clean label.
5297
+ *
5298
+ * We filter out anything below 8 GB RAM — Claude Code wants headroom
5299
+ * for `tsc`, build tools, and parallel test runners; the 4 GB tier
5300
+ * (when available) is too tight in practice.
5301
+ */
5302
+ async listMachineTypes(projectId) {
5303
+ try {
5304
+ const { stdout } = await execFileP2(
5305
+ "gh",
5306
+ ["api", `/repos/${projectId}/codespaces/machines`],
5307
+ { maxBuffer: MAX_BUFFER }
5308
+ );
5309
+ const data = JSON.parse(stdout);
5310
+ const machines = data.machines ?? [];
5311
+ const GB = 1024 ** 3;
5312
+ return machines.map((m) => {
5313
+ const memoryGb = m.memory_in_bytes ? Math.round(m.memory_in_bytes / GB) : 0;
5314
+ const storageGb = m.storage_in_bytes ? Math.round(m.storage_in_bytes / GB) : void 0;
5315
+ const parts = [];
5316
+ if (m.cpus) parts.push(`${m.cpus} ${m.cpus === 1 ? "core" : "cores"}`);
5317
+ if (memoryGb) parts.push(`${memoryGb} GB RAM`);
5318
+ if (storageGb) parts.push(`${storageGb} GB storage`);
5319
+ return {
5320
+ id: m.name,
5321
+ label: m.display_name ?? (parts.join(" \xB7 ") || m.name),
5322
+ memoryGb,
5323
+ cpus: m.cpus,
5324
+ storageGb
5325
+ };
5326
+ }).filter((m) => m.memoryGb >= 8).sort((a, b) => a.memoryGb - b.memoryGb || (a.cpus ?? 0) - (b.cpus ?? 0));
5327
+ } catch {
5328
+ return [];
5329
+ }
5330
+ }
5331
+ async createWorkspace(projectId, machineTypeId) {
5332
+ const machine = machineTypeId ?? await this.pickDefaultMachine(projectId);
5333
+ const args2 = ["codespace", "create", "-R", projectId, "--default-permissions"];
5334
+ if (machine) args2.push("-m", machine);
5150
5335
  const { stdout } = await execFileP2(
5151
5336
  "gh",
5152
- ["codespace", "create", "-R", projectId, "--default-permissions"],
5337
+ args2,
5153
5338
  { maxBuffer: MAX_BUFFER, timeout: 12e4 }
5154
5339
  );
5155
5340
  const name = stdout.trim().split("\n").filter(Boolean).pop() ?? "";
@@ -5163,6 +5348,27 @@ var GitHubCodespacesProvider = class {
5163
5348
  webUrl: `https://github.com/codespaces/${name}`
5164
5349
  };
5165
5350
  }
5351
+ /**
5352
+ * Fallback machine picker for when the orchestrator didn't ask the
5353
+ * user — defaults to the cheapest 8 GB tier (`basicLinux32gb`) and
5354
+ * walks up only if the repo restricts that tier. Returns `null` if
5355
+ * the API call fails entirely; the caller will then omit `-m` and
5356
+ * let `gh` use the repo/org default.
5357
+ */
5358
+ async pickDefaultMachine(projectId) {
5359
+ const machines = await this.listMachineTypes(projectId);
5360
+ if (machines.length === 0) return null;
5361
+ const preferenceOrder = [
5362
+ "basicLinux32gb",
5363
+ "standardLinux32gb",
5364
+ "premiumLinux",
5365
+ "largePremiumLinux"
5366
+ ];
5367
+ for (const pref of preferenceOrder) {
5368
+ if (machines.some((m) => m.id === pref)) return pref;
5369
+ }
5370
+ return machines[0].id;
5371
+ }
5166
5372
  async waitUntilAvailable(name) {
5167
5373
  const deadline = Date.now() + 5 * 60 * 1e3;
5168
5374
  while (Date.now() < deadline) {
@@ -5231,7 +5437,7 @@ var PROVIDERS = [
5231
5437
  // src/commands/deploy.ts
5232
5438
  async function deploy() {
5233
5439
  console.log();
5234
- mt(import_picocolors7.default.bgMagenta(import_picocolors7.default.white(" codeam deploy ")));
5440
+ mt(import_picocolors8.default.bgMagenta(import_picocolors8.default.white(" codeam deploy ")));
5235
5441
  const provider = await pickProvider();
5236
5442
  if (!provider) {
5237
5443
  pt("No provider selected.");
@@ -5275,11 +5481,43 @@ async function deploy() {
5275
5481
  process.exit(0);
5276
5482
  }
5277
5483
  const project = projects.find((proj) => proj.id === projectId);
5484
+ let machineTypeId;
5485
+ if (provider.listMachineTypes) {
5486
+ const machineStep = fe();
5487
+ machineStep.start("Loading machine types\u2026");
5488
+ let machines = [];
5489
+ try {
5490
+ machines = await provider.listMachineTypes(project.id);
5491
+ machineStep.stop(
5492
+ machines.length > 0 ? `\u2713 ${machines.length} machine type${machines.length === 1 ? "" : "s"} available` : "\xB7 No machine types reported (using provider default)"
5493
+ );
5494
+ } catch {
5495
+ machineStep.stop("\xB7 Could not list machine types \u2014 using provider default");
5496
+ }
5497
+ if (machines.length > 1) {
5498
+ const picked = await _t({
5499
+ message: "Pick a machine size (starts at 8 GB RAM):",
5500
+ initialValue: machines[0].id,
5501
+ options: machines.map((m) => ({
5502
+ value: m.id,
5503
+ label: m.label,
5504
+ hint: `${m.memoryGb} GB RAM`
5505
+ }))
5506
+ });
5507
+ if (q(picked)) {
5508
+ pt("Cancelled.");
5509
+ process.exit(0);
5510
+ }
5511
+ machineTypeId = picked;
5512
+ } else if (machines.length === 1) {
5513
+ machineTypeId = machines[0].id;
5514
+ }
5515
+ }
5278
5516
  const createStep = fe();
5279
5517
  createStep.start(`Creating workspace for ${project.fullName}\u2026`);
5280
5518
  let workspace;
5281
5519
  try {
5282
- workspace = await provider.createWorkspace(project.id);
5520
+ workspace = await provider.createWorkspace(project.id, machineTypeId);
5283
5521
  createStep.stop(`\u2713 Workspace ready: ${workspace.displayName ?? workspace.id}`);
5284
5522
  } catch (err) {
5285
5523
  createStep.stop(`\u2717 Workspace creation failed`);
@@ -5341,8 +5579,8 @@ async function deploy() {
5341
5579
  cliStep.stop("\u2713 codeam-cli installed");
5342
5580
  wt(
5343
5581
  [
5344
- `Workspace: ${import_picocolors7.default.cyan(workspace.displayName ?? workspace.id)}`,
5345
- workspace.webUrl ? `Web: ${import_picocolors7.default.cyan(workspace.webUrl)}` : "",
5582
+ `Workspace: ${import_picocolors8.default.cyan(workspace.displayName ?? workspace.id)}`,
5583
+ workspace.webUrl ? `Web: ${import_picocolors8.default.cyan(workspace.webUrl)}` : "",
5346
5584
  "",
5347
5585
  "Starting `codeam pair` on the workspace.",
5348
5586
  "Scan the QR code below with the CodeAgent Mobile app to finish pairing."
@@ -5351,9 +5589,9 @@ async function deploy() {
5351
5589
  );
5352
5590
  const code = (await provider.streamCommand(workspace.id, "codeam pair")).code;
5353
5591
  if (code === 0) {
5354
- gt(import_picocolors7.default.green(`\u2713 Workspace deployed and paired. Drive from your phone, anywhere.`));
5592
+ gt(import_picocolors8.default.green(`\u2713 Workspace deployed and paired. Drive from your phone, anywhere.`));
5355
5593
  } else {
5356
- gt(import_picocolors7.default.yellow(`Pairing exited with code ${code}. Run "codeam pair" inside the codespace if needed.`));
5594
+ gt(import_picocolors8.default.yellow(`Pairing exited with code ${code}. Run "codeam pair" inside the codespace if needed.`));
5357
5595
  }
5358
5596
  }
5359
5597
  async function runRemoteClaudeLogin(provider, workspaceId) {
@@ -5382,7 +5620,7 @@ async function pickProvider() {
5382
5620
  message: "Where do you want to deploy?",
5383
5621
  options: PROVIDERS.map((prov) => ({
5384
5622
  value: prov.id,
5385
- label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors7.default.dim("(coming soon)")}`,
5623
+ label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors8.default.dim("(coming soon)")}`,
5386
5624
  hint: prov.tagline
5387
5625
  }))
5388
5626
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
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": {