codeam-cli 2.4.1 → 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 +210 -21
  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.1",
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`);
@@ -5341,8 +5530,8 @@ async function deploy() {
5341
5530
  cliStep.stop("\u2713 codeam-cli installed");
5342
5531
  wt(
5343
5532
  [
5344
- `Workspace: ${import_picocolors7.default.cyan(workspace.displayName ?? workspace.id)}`,
5345
- 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)}` : "",
5346
5535
  "",
5347
5536
  "Starting `codeam pair` on the workspace.",
5348
5537
  "Scan the QR code below with the CodeAgent Mobile app to finish pairing."
@@ -5351,9 +5540,9 @@ async function deploy() {
5351
5540
  );
5352
5541
  const code = (await provider.streamCommand(workspace.id, "codeam pair")).code;
5353
5542
  if (code === 0) {
5354
- 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.`));
5355
5544
  } else {
5356
- 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.`));
5357
5546
  }
5358
5547
  }
5359
5548
  async function runRemoteClaudeLogin(provider, workspaceId) {
@@ -5382,7 +5571,7 @@ async function pickProvider() {
5382
5571
  message: "Where do you want to deploy?",
5383
5572
  options: PROVIDERS.map((prov) => ({
5384
5573
  value: prov.id,
5385
- 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)")}`,
5386
5575
  hint: prov.tagline
5387
5576
  }))
5388
5577
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.4.1",
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": {