codeam-cli 2.3.0 → 2.4.1

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 (2) hide show
  1. package/dist/index.js +325 -1
  2. package/package.json +1 -1
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.3.0",
182
+ version: "2.4.1",
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: {
@@ -5076,6 +5076,328 @@ async function logout() {
5076
5076
  console.log(import_picocolors6.default.green("\n \u2713 Done. All sessions removed.\n"));
5077
5077
  }
5078
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
+ const haveLocalClaude = fs8.existsSync(localClaudeDir) && fs8.statSync(localClaudeDir).isDirectory();
5303
+ if (haveLocalClaude) {
5304
+ const copyStep = fe();
5305
+ copyStep.start("Copying local Claude config to workspace\u2026");
5306
+ try {
5307
+ await provider.uploadDirectory(workspace.id, localClaudeDir, "/home/codespace/.claude");
5308
+ copyStep.stop("\u2713 Claude config copied \u2014 no re-auth needed");
5309
+ } catch (err) {
5310
+ copyStep.stop("\u26A0 Could not copy Claude config \u2014 falling back to remote login");
5311
+ void err;
5312
+ await runRemoteClaudeLogin(provider, workspace.id);
5313
+ }
5314
+ } else {
5315
+ wt(
5316
+ [
5317
+ "No local ~/.claude config found.",
5318
+ "We can run `claude login` inside the workspace right now \u2014 the URL",
5319
+ "will print here, you open it in your browser, paste the code back,",
5320
+ "and the workspace gets authenticated. (Skip if you'd rather do it",
5321
+ "manually later from inside the codespace.)"
5322
+ ].join("\n"),
5323
+ "Claude credentials"
5324
+ );
5325
+ const proceed = await ot2({
5326
+ message: "Run `claude login` on the workspace now?",
5327
+ initialValue: true
5328
+ });
5329
+ if (!q(proceed) && proceed) {
5330
+ await runRemoteClaudeLogin(provider, workspace.id);
5331
+ }
5332
+ }
5333
+ const cliStep = fe();
5334
+ cliStep.start("Installing codeam-cli on workspace\u2026");
5335
+ const cliInstall = await provider.exec(workspace.id, "npm install -g codeam-cli@latest");
5336
+ if (cliInstall.code !== 0) {
5337
+ cliStep.stop("\u2717 codeam-cli install failed");
5338
+ pt(cliInstall.stderr.slice(0, 1e3));
5339
+ process.exit(1);
5340
+ }
5341
+ cliStep.stop("\u2713 codeam-cli installed");
5342
+ wt(
5343
+ [
5344
+ `Workspace: ${import_picocolors7.default.cyan(workspace.displayName ?? workspace.id)}`,
5345
+ workspace.webUrl ? `Web: ${import_picocolors7.default.cyan(workspace.webUrl)}` : "",
5346
+ "",
5347
+ "Starting `codeam pair` on the workspace.",
5348
+ "Scan the QR code below with the CodeAgent Mobile app to finish pairing."
5349
+ ].filter(Boolean).join("\n"),
5350
+ "Almost there"
5351
+ );
5352
+ const code = (await provider.streamCommand(workspace.id, "codeam pair")).code;
5353
+ if (code === 0) {
5354
+ gt(import_picocolors7.default.green(`\u2713 Workspace deployed and paired. Drive from your phone, anywhere.`));
5355
+ } else {
5356
+ gt(import_picocolors7.default.yellow(`Pairing exited with code ${code}. Run "codeam pair" inside the codespace if needed.`));
5357
+ }
5358
+ }
5359
+ async function runRemoteClaudeLogin(provider, workspaceId) {
5360
+ wt(
5361
+ [
5362
+ "A login URL will print below. Open it in your local browser, sign in,",
5363
+ "and paste any code Claude asks for back into this terminal."
5364
+ ].join("\n"),
5365
+ "Authenticating Claude on workspace"
5366
+ );
5367
+ const result = await provider.streamCommand(
5368
+ workspaceId,
5369
+ 'bash -lc "claude login || claude /login || true"'
5370
+ );
5371
+ if (result.code !== 0) {
5372
+ wt(
5373
+ "claude login exited non-zero. You can re-run it manually inside the codespace later.",
5374
+ "Heads up"
5375
+ );
5376
+ }
5377
+ }
5378
+ async function pickProvider() {
5379
+ const ready = PROVIDERS.filter((prov) => prov.available);
5380
+ if (ready.length === 1) return ready[0];
5381
+ const selection = await _t({
5382
+ message: "Where do you want to deploy?",
5383
+ options: PROVIDERS.map((prov) => ({
5384
+ value: prov.id,
5385
+ label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors7.default.dim("(coming soon)")}`,
5386
+ hint: prov.tagline
5387
+ }))
5388
+ });
5389
+ if (q(selection) || typeof selection !== "string") return null;
5390
+ const found = PROVIDERS.find((prov) => prov.id === selection);
5391
+ if (!found || !found.available) {
5392
+ wt(
5393
+ `${found?.displayName ?? "That provider"} isn\u2019t implemented yet \u2014 we'll ping you on Twitter/X when it ships.`,
5394
+ "Heads up"
5395
+ );
5396
+ return null;
5397
+ }
5398
+ return found;
5399
+ }
5400
+
5079
5401
  // src/index.ts
5080
5402
  var [, , command, ...args] = process.argv;
5081
5403
  async function main() {
@@ -5088,6 +5410,8 @@ async function main() {
5088
5410
  return status();
5089
5411
  case "logout":
5090
5412
  return logout();
5413
+ case "deploy":
5414
+ return deploy();
5091
5415
  default:
5092
5416
  return start();
5093
5417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
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": {