codeam-cli 2.4.24 → 2.4.26

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/CHANGELOG.md +12 -0
  2. package/dist/index.js +947 -55
  3. 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.25] — 2026-05-03
8
+
9
+ ### Added
10
+
11
+ - **cli:** Three new deploy providers — Gitpod, GitLab Workspaces, Railway (v2.4.25)
12
+
13
+ ## [2.4.24] — 2026-05-03
14
+
15
+ ### Added
16
+
17
+ - **cli:** List repos from user's orgs after expand-scopes (v2.4.24)
18
+
7
19
  ## [2.4.23] — 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.24",
182
+ version: "2.4.26",
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: {
@@ -2793,12 +2793,12 @@ except Exception:sys.exit(0)
2793
2793
  claude.kill();
2794
2794
  } catch {
2795
2795
  }
2796
- const codespaceName = process.env.CODESPACE_NAME;
2797
- if (codespaceName && process.env.CODESPACES === "true") {
2796
+ const codespaceName2 = process.env.CODESPACE_NAME;
2797
+ if (codespaceName2 && process.env.CODESPACES === "true") {
2798
2798
  try {
2799
2799
  const stopProc = (0, import_child_process4.spawn)(
2800
2800
  "bash",
2801
- ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(codespaceName)} >/dev/null 2>&1 || true`],
2801
+ ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(codespaceName2)} >/dev/null 2>&1 || true`],
2802
2802
  { detached: true, stdio: "ignore" }
2803
2803
  );
2804
2804
  stopProc.unref();
@@ -3041,22 +3041,42 @@ except Exception:sys.exit(0)
3041
3041
  fetchQuotaUsage();
3042
3042
  }, 5e3);
3043
3043
  const inCodespace = process.env.CODESPACES === "true";
3044
+ const codespaceName = process.env.CODESPACE_NAME;
3044
3045
  let keepAliveTimer = null;
3046
+ async function setIdleTimeout(minutes) {
3047
+ if (!inCodespace || !codespaceName) return;
3048
+ await new Promise((resolve2) => {
3049
+ const proc = (0, import_child_process4.spawn)(
3050
+ "gh",
3051
+ [
3052
+ "api",
3053
+ "-X",
3054
+ "PATCH",
3055
+ `/user/codespaces/${codespaceName}`,
3056
+ "-F",
3057
+ `idle_timeout_minutes=${minutes}`
3058
+ ],
3059
+ { stdio: "ignore", detached: true }
3060
+ );
3061
+ proc.unref();
3062
+ proc.on("exit", () => resolve2());
3063
+ proc.on("error", () => resolve2());
3064
+ });
3065
+ }
3045
3066
  function setKeepAlive(enabled) {
3046
3067
  if (keepAliveTimer) {
3047
3068
  clearInterval(keepAliveTimer);
3048
3069
  keepAliveTimer = null;
3049
3070
  }
3050
- if (!enabled || !inCodespace) return;
3051
- const ping = () => {
3052
- const proc = (0, import_child_process4.spawn)("bash", ["-lc", "gh codespace list >/dev/null 2>&1 || true"], {
3053
- stdio: "ignore",
3054
- detached: true
3055
- });
3056
- proc.unref();
3057
- };
3058
- ping();
3059
- keepAliveTimer = setInterval(ping, 8 * 60 * 1e3);
3071
+ if (!inCodespace || !codespaceName) return;
3072
+ if (!enabled) {
3073
+ void setIdleTimeout(30);
3074
+ return;
3075
+ }
3076
+ void setIdleTimeout(240);
3077
+ keepAliveTimer = setInterval(() => {
3078
+ void setIdleTimeout(240);
3079
+ }, 30 * 60 * 1e3);
3060
3080
  }
3061
3081
  }
3062
3082
 
@@ -5148,12 +5168,12 @@ async function logout() {
5148
5168
  }
5149
5169
 
5150
5170
  // src/commands/deploy.ts
5151
- var import_child_process6 = require("child_process");
5171
+ var import_child_process9 = require("child_process");
5152
5172
  var fs8 = __toESM(require("fs"));
5153
5173
  var os6 = __toESM(require("os"));
5154
- var path9 = __toESM(require("path"));
5155
- var import_util3 = require("util");
5156
- var import_picocolors8 = __toESM(require("picocolors"));
5174
+ var path12 = __toESM(require("path"));
5175
+ var import_util6 = require("util");
5176
+ var import_picocolors9 = __toESM(require("picocolors"));
5157
5177
 
5158
5178
  // src/services/providers/github-codespaces.ts
5159
5179
  var import_child_process5 = require("child_process");
@@ -5715,20 +5735,892 @@ function shellQuote(s) {
5715
5735
  return `'${s.replace(/'/g, `'\\''`)}'`;
5716
5736
  }
5717
5737
 
5738
+ // src/services/providers/gitpod.ts
5739
+ var import_child_process6 = require("child_process");
5740
+ var import_util3 = require("util");
5741
+ var path9 = __toESM(require("path"));
5742
+ var import_picocolors8 = __toESM(require("picocolors"));
5743
+ var execFileP3 = (0, import_util3.promisify)(import_child_process6.execFile);
5744
+ var MAX_BUFFER2 = 8 * 1024 * 1024;
5745
+ function resetStdinForChild2() {
5746
+ if (process.stdin.isTTY) {
5747
+ try {
5748
+ process.stdin.setRawMode(false);
5749
+ } catch {
5750
+ }
5751
+ }
5752
+ }
5753
+ var GitpodProvider = class {
5754
+ id = "gitpod";
5755
+ displayName = "Gitpod";
5756
+ tagline = "Cloud dev environments from any Git repo";
5757
+ available = true;
5758
+ async authorize() {
5759
+ try {
5760
+ await execFileP3("gitpod", ["--version"], { maxBuffer: MAX_BUFFER2 });
5761
+ } catch {
5762
+ throw new Error(
5763
+ [
5764
+ "Gitpod CLI (`gitpod`) is required for Gitpod deploys.",
5765
+ "Install it with one of:",
5766
+ " \u2022 macOS: brew install gitpod-io/tap/gitpod",
5767
+ " \u2022 Other: https://github.com/gitpod-io/gitpod-cli#installation",
5768
+ "Then run `gitpod login` and try `codeam deploy` again."
5769
+ ].join("\n")
5770
+ );
5771
+ }
5772
+ try {
5773
+ await execFileP3("gitpod", ["whoami"], { maxBuffer: MAX_BUFFER2 });
5774
+ return;
5775
+ } catch {
5776
+ }
5777
+ wt(
5778
+ "A login URL will print below. Open it in your browser and approve.",
5779
+ "Authenticating Gitpod"
5780
+ );
5781
+ resetStdinForChild2();
5782
+ await new Promise((resolve2, reject) => {
5783
+ const proc = (0, import_child_process6.spawn)("gitpod", ["login"], { stdio: "inherit" });
5784
+ proc.on("exit", (code) => {
5785
+ if (code === 0) resolve2();
5786
+ else reject(new Error("gitpod login failed."));
5787
+ });
5788
+ proc.on("error", reject);
5789
+ });
5790
+ }
5791
+ async listProjects() {
5792
+ try {
5793
+ const { stdout } = await execFileP3(
5794
+ "gitpod",
5795
+ ["workspace", "list", "--output", "json", "--limit", "200"],
5796
+ { maxBuffer: MAX_BUFFER2 }
5797
+ );
5798
+ const list = JSON.parse(stdout);
5799
+ const seen = /* @__PURE__ */ new Set();
5800
+ const projects = [];
5801
+ for (const w3 of list) {
5802
+ const url = w3.contextUrl ?? "";
5803
+ if (!url || seen.has(url)) continue;
5804
+ seen.add(url);
5805
+ const m = url.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/.+)?$/);
5806
+ if (!m) continue;
5807
+ const fullName = `${m[2]}/${m[3]}`;
5808
+ projects.push({
5809
+ id: url,
5810
+ // Gitpod indexes workspaces by URL
5811
+ name: m[3],
5812
+ fullName,
5813
+ description: w3.description,
5814
+ private: false
5815
+ // Gitpod doesn't expose visibility here
5816
+ });
5817
+ }
5818
+ return projects;
5819
+ } catch {
5820
+ return [];
5821
+ }
5822
+ }
5823
+ async createWorkspace(projectId, machineTypeId) {
5824
+ const args2 = ["workspace", "create", projectId, "--start", "--output", "json"];
5825
+ if (machineTypeId) args2.push("--class", machineTypeId);
5826
+ const { stdout } = await execFileP3("gitpod", args2, {
5827
+ maxBuffer: MAX_BUFFER2,
5828
+ timeout: 3e5
5829
+ });
5830
+ let parsed;
5831
+ try {
5832
+ parsed = JSON.parse(stdout);
5833
+ } catch {
5834
+ parsed = { id: stdout.trim() };
5835
+ }
5836
+ if (!parsed.id) {
5837
+ throw new Error("Gitpod did not return a workspace id.");
5838
+ }
5839
+ await this.waitUntilRunning(parsed.id);
5840
+ return {
5841
+ id: parsed.id,
5842
+ displayName: parsed.id,
5843
+ webUrl: parsed.url ?? `https://gitpod.io/start/#${parsed.id}`
5844
+ };
5845
+ }
5846
+ async waitUntilRunning(workspaceId) {
5847
+ const deadline = Date.now() + 5 * 60 * 1e3;
5848
+ while (Date.now() < deadline) {
5849
+ try {
5850
+ const { stdout } = await execFileP3(
5851
+ "gitpod",
5852
+ ["workspace", "get", workspaceId, "--output", "json"],
5853
+ { maxBuffer: MAX_BUFFER2 }
5854
+ );
5855
+ const status2 = JSON.parse(stdout).status?.toLowerCase() ?? "";
5856
+ if (status2 === "running" || status2 === "available") return;
5857
+ if (status2 === "failed" || status2 === "stopped") {
5858
+ throw new Error(`Gitpod workspace state: ${status2}.`);
5859
+ }
5860
+ } catch {
5861
+ }
5862
+ await new Promise((r) => setTimeout(r, 3e3));
5863
+ }
5864
+ throw new Error("Gitpod workspace did not become Running within 5 minutes.");
5865
+ }
5866
+ async listMachineTypes(_projectId) {
5867
+ try {
5868
+ const { stdout } = await execFileP3(
5869
+ "gitpod",
5870
+ ["organization", "list-classes", "--output", "json"],
5871
+ { maxBuffer: MAX_BUFFER2 }
5872
+ );
5873
+ const list = JSON.parse(stdout);
5874
+ return list.map((c2) => ({
5875
+ id: c2.id,
5876
+ label: c2.displayName ?? c2.id,
5877
+ memoryGb: 8
5878
+ }));
5879
+ } catch {
5880
+ return [];
5881
+ }
5882
+ }
5883
+ async listExistingWorkspaces(projectId) {
5884
+ try {
5885
+ const args2 = ["workspace", "list", "--output", "json", "--limit", "200"];
5886
+ const { stdout } = await execFileP3("gitpod", args2, { maxBuffer: MAX_BUFFER2 });
5887
+ const list = JSON.parse(stdout);
5888
+ return list.filter((w3) => !projectId || w3.contextUrl === projectId).map((w3) => ({
5889
+ id: w3.id,
5890
+ displayName: w3.id,
5891
+ webUrl: `https://gitpod.io/start/#${w3.id}`,
5892
+ state: w3.status ?? "Unknown",
5893
+ lastUsedAt: w3.lastActivity
5894
+ }));
5895
+ } catch {
5896
+ return [];
5897
+ }
5898
+ }
5899
+ async startWorkspace(workspaceId) {
5900
+ try {
5901
+ await execFileP3(
5902
+ "gitpod",
5903
+ ["workspace", "start", workspaceId],
5904
+ { maxBuffer: MAX_BUFFER2, timeout: 6e4 }
5905
+ );
5906
+ } catch {
5907
+ }
5908
+ await this.waitUntilRunning(workspaceId);
5909
+ return {
5910
+ id: workspaceId,
5911
+ displayName: workspaceId,
5912
+ webUrl: `https://gitpod.io/start/#${workspaceId}`
5913
+ };
5914
+ }
5915
+ async exec(workspaceId, command2) {
5916
+ try {
5917
+ const { stdout, stderr } = await execFileP3(
5918
+ "gitpod",
5919
+ ["workspace", "ssh", workspaceId, "--", command2],
5920
+ { maxBuffer: MAX_BUFFER2, timeout: 6e5 }
5921
+ );
5922
+ return { stdout, stderr, code: 0 };
5923
+ } catch (err) {
5924
+ const e = err;
5925
+ return {
5926
+ stdout: e.stdout ?? "",
5927
+ stderr: e.stderr ?? e.message ?? "gitpod workspace ssh failed",
5928
+ code: typeof e.code === "number" ? e.code : 1
5929
+ };
5930
+ }
5931
+ }
5932
+ async streamCommand(workspaceId, command2) {
5933
+ resetStdinForChild2();
5934
+ return new Promise((resolve2, reject) => {
5935
+ const proc = (0, import_child_process6.spawn)(
5936
+ "gitpod",
5937
+ ["workspace", "ssh", workspaceId, "--", "-tt", command2],
5938
+ { stdio: "inherit" }
5939
+ );
5940
+ proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
5941
+ proc.on("error", reject);
5942
+ });
5943
+ }
5944
+ async uploadDirectory(workspaceId, localDir, remoteDir, options = {}) {
5945
+ const tarArgs = ["-czf", "-", "-C", localDir];
5946
+ for (const pattern of options.exclude ?? []) {
5947
+ tarArgs.push(`--exclude=${pattern}`);
5948
+ const stripped = pattern.replace(/^\.\/+/, "");
5949
+ if (stripped !== pattern) tarArgs.push(`--exclude=${stripped}`);
5950
+ }
5951
+ tarArgs.push(".");
5952
+ const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
5953
+ const remoteCmd = `mkdir -p ${shellQuote2(remoteDir)} && tar -xzf - -C ${shellQuote2(remoteDir)}`;
5954
+ await new Promise((resolve2, reject) => {
5955
+ const tar = (0, import_child_process6.spawn)("tar", tarArgs, {
5956
+ stdio: ["ignore", "pipe", "pipe"],
5957
+ env: tarEnv
5958
+ });
5959
+ const ssh = (0, import_child_process6.spawn)(
5960
+ "gitpod",
5961
+ ["workspace", "ssh", workspaceId, "--", remoteCmd],
5962
+ { stdio: [tar.stdout, "pipe", "pipe"] }
5963
+ );
5964
+ let tarErr = "";
5965
+ let sshErr = "";
5966
+ tar.stderr?.on("data", (d3) => {
5967
+ tarErr += d3.toString();
5968
+ });
5969
+ ssh.stderr?.on("data", (d3) => {
5970
+ sshErr += d3.toString();
5971
+ });
5972
+ tar.on("error", reject);
5973
+ ssh.on("error", reject);
5974
+ ssh.on("exit", (code) => {
5975
+ if (code === 0) resolve2();
5976
+ else reject(new Error(`Remote tar failed: ${(sshErr || tarErr || `exit ${code}`).trim().slice(0, 500)}`));
5977
+ });
5978
+ });
5979
+ }
5980
+ async uploadFile(workspaceId, remotePath, contents, options = {}) {
5981
+ const remoteDir = path9.posix.dirname(remotePath);
5982
+ const parts = [
5983
+ `mkdir -p ${shellQuote2(remoteDir)}`,
5984
+ `cat > ${shellQuote2(remotePath)}`
5985
+ ];
5986
+ if (options.mode != null) {
5987
+ parts.push(`chmod ${options.mode.toString(8)} ${shellQuote2(remotePath)}`);
5988
+ }
5989
+ const cmd = parts.join(" && ");
5990
+ await new Promise((resolve2, reject) => {
5991
+ const proc = (0, import_child_process6.spawn)(
5992
+ "gitpod",
5993
+ ["workspace", "ssh", workspaceId, "--", cmd],
5994
+ { stdio: ["pipe", "pipe", "pipe"] }
5995
+ );
5996
+ let stderr = "";
5997
+ proc.stderr?.on("data", (d3) => {
5998
+ stderr += d3.toString();
5999
+ });
6000
+ proc.on("error", reject);
6001
+ proc.on("exit", (code) => {
6002
+ if (code === 0) resolve2();
6003
+ else reject(new Error(`Remote write failed: ${(stderr || `exit ${code}`).trim().slice(0, 500)}`));
6004
+ });
6005
+ proc.stdin?.write(contents);
6006
+ proc.stdin?.end();
6007
+ });
6008
+ }
6009
+ };
6010
+ function shellQuote2(s) {
6011
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6012
+ }
6013
+
6014
+ // src/services/providers/gitlab-workspaces.ts
6015
+ var import_child_process7 = require("child_process");
6016
+ var import_util4 = require("util");
6017
+ var path10 = __toESM(require("path"));
6018
+ var execFileP4 = (0, import_util4.promisify)(import_child_process7.execFile);
6019
+ var MAX_BUFFER3 = 8 * 1024 * 1024;
6020
+ var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
6021
+ function resetStdinForChild3() {
6022
+ if (process.stdin.isTTY) {
6023
+ try {
6024
+ process.stdin.setRawMode(false);
6025
+ } catch {
6026
+ }
6027
+ }
6028
+ }
6029
+ var GitLabWorkspacesProvider = class {
6030
+ id = "gitlab-workspaces";
6031
+ displayName = "GitLab Workspaces";
6032
+ tagline = "Self-hosted dev environments on your K8s cluster";
6033
+ available = true;
6034
+ async authorize() {
6035
+ try {
6036
+ await execFileP4("glab", ["--version"], { maxBuffer: MAX_BUFFER3 });
6037
+ } catch {
6038
+ throw new Error(
6039
+ [
6040
+ "GitLab CLI (`glab`) is required for GitLab Workspaces deploys.",
6041
+ "Install it with one of:",
6042
+ " \u2022 macOS: brew install glab",
6043
+ " \u2022 Linux: https://gitlab.com/gitlab-org/cli#installation",
6044
+ " \u2022 Windows: winget install GitLab.glab",
6045
+ "Then run `glab auth login` and try `codeam deploy` again."
6046
+ ].join("\n")
6047
+ );
6048
+ }
6049
+ try {
6050
+ await execFileP4("glab", ["auth", "status"], { maxBuffer: MAX_BUFFER3 });
6051
+ return;
6052
+ } catch {
6053
+ }
6054
+ wt(
6055
+ "A token / OAuth flow will open below. After approval the deploy resumes.",
6056
+ "Authenticating GitLab"
6057
+ );
6058
+ resetStdinForChild3();
6059
+ await new Promise((resolve2, reject) => {
6060
+ const proc = (0, import_child_process7.spawn)(
6061
+ "glab",
6062
+ ["auth", "login", "--scopes", "api,read_user,read_repository"],
6063
+ { stdio: "inherit" }
6064
+ );
6065
+ proc.on("exit", (code) => {
6066
+ if (code === 0) resolve2();
6067
+ else reject(new Error("glab auth login failed."));
6068
+ });
6069
+ proc.on("error", reject);
6070
+ });
6071
+ }
6072
+ async listProjects() {
6073
+ try {
6074
+ const { stdout } = await execFileP4(
6075
+ "glab",
6076
+ ["repo", "list", "--per-page", "200", "--output", "json"],
6077
+ { maxBuffer: MAX_BUFFER3 }
6078
+ );
6079
+ const list = JSON.parse(stdout);
6080
+ return list.filter((r) => r.path_with_namespace).map((r) => ({
6081
+ id: r.path_with_namespace,
6082
+ name: r.name ?? r.path_with_namespace,
6083
+ fullName: r.path_with_namespace,
6084
+ description: r.description ?? void 0,
6085
+ defaultBranch: r.default_branch,
6086
+ private: r.visibility !== "public"
6087
+ }));
6088
+ } catch {
6089
+ return [];
6090
+ }
6091
+ }
6092
+ /**
6093
+ * GitLab.com's machine sizes are tied to whatever the agent's
6094
+ * K8s cluster offers — this is project / agent specific and the
6095
+ * GraphQL API doesn't enumerate "available classes" today. We
6096
+ * return an empty list, which makes the orchestrator skip the
6097
+ * picker. Users with multiple sizes wired to their agent should
6098
+ * change the default in their devfile.yaml.
6099
+ */
6100
+ async listMachineTypes(_projectId) {
6101
+ return [];
6102
+ }
6103
+ async createWorkspace(projectId, _machineTypeId) {
6104
+ const token = await this.getGlabToken();
6105
+ if (!token) {
6106
+ throw new Error(
6107
+ "Could not extract GitLab token from `glab`. Run `glab auth login` and retry."
6108
+ );
6109
+ }
6110
+ const projectFullPath = projectId;
6111
+ const mutation = `mutation Create($p: WorkspaceCreateInput!) {
6112
+ workspaceCreate(input: $p) {
6113
+ workspace { id name desiredState actualState url }
6114
+ errors
6115
+ }
6116
+ }`;
6117
+ const body = JSON.stringify({
6118
+ query: mutation,
6119
+ variables: {
6120
+ p: {
6121
+ projectId: `gid://gitlab/Project/${projectFullPath}`,
6122
+ editor: "webide",
6123
+ desiredState: "RUNNING",
6124
+ // The devfile MUST exist in the repo at .devfile.yaml.
6125
+ devfilePath: ".devfile.yaml"
6126
+ }
6127
+ }
6128
+ });
6129
+ const data = await this.gql(token, body);
6130
+ const ws = data.data?.workspaceCreate?.workspace;
6131
+ const errs = data.data?.workspaceCreate?.errors ?? data.errors?.map((e) => e.message ?? "") ?? [];
6132
+ if (!ws?.id || errs.length > 0) {
6133
+ throw new Error(
6134
+ `GitLab Workspaces createWorkspace failed: ${errs.join("; ") || "no workspace returned"}.
6135
+ Common causes: project has no .devfile.yaml; no agent registered; user lacks permission.
6136
+ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
6137
+ );
6138
+ }
6139
+ await this.waitUntilRunning(token, ws.id);
6140
+ return {
6141
+ id: ws.id,
6142
+ displayName: ws.name ?? ws.id,
6143
+ webUrl: ws.url
6144
+ };
6145
+ }
6146
+ async waitUntilRunning(token, workspaceId) {
6147
+ const deadline = Date.now() + 5 * 60 * 1e3;
6148
+ const query = `query Get($id: WorkspaceID!) {
6149
+ workspace(id: $id) { actualState }
6150
+ }`;
6151
+ while (Date.now() < deadline) {
6152
+ try {
6153
+ const data = await this.gql(
6154
+ token,
6155
+ JSON.stringify({ query, variables: { id: workspaceId } })
6156
+ );
6157
+ const state = data.data?.workspace?.actualState?.toUpperCase() ?? "";
6158
+ if (state === "RUNNING") return;
6159
+ if (state === "FAILED" || state === "STOPPED") {
6160
+ throw new Error(`Workspace state: ${state}.`);
6161
+ }
6162
+ } catch {
6163
+ }
6164
+ await new Promise((r) => setTimeout(r, 4e3));
6165
+ }
6166
+ throw new Error("GitLab workspace did not become Running within 5 minutes.");
6167
+ }
6168
+ async listExistingWorkspaces(_projectId) {
6169
+ const token = await this.getGlabToken();
6170
+ if (!token) return [];
6171
+ const query = `query { currentUser { workspaces { nodes {
6172
+ id name actualState url updatedAt
6173
+ } } } }`;
6174
+ try {
6175
+ const data = await this.gql(token, JSON.stringify({ query }));
6176
+ const nodes = data.data?.currentUser?.workspaces?.nodes ?? [];
6177
+ return nodes.map((n) => ({
6178
+ id: n.id,
6179
+ displayName: n.name ?? n.id,
6180
+ webUrl: n.url,
6181
+ state: n.actualState,
6182
+ lastUsedAt: n.updatedAt
6183
+ }));
6184
+ } catch {
6185
+ return [];
6186
+ }
6187
+ }
6188
+ async startWorkspace(workspaceId) {
6189
+ const token = await this.getGlabToken();
6190
+ if (!token) throw new Error("Not authenticated with GitLab.");
6191
+ const mutation = `mutation Start($id: WorkspaceID!) {
6192
+ workspaceUpdate(input: { id: $id, desiredState: RUNNING }) {
6193
+ workspace { id name url } errors
6194
+ }
6195
+ }`;
6196
+ await this.gql(
6197
+ token,
6198
+ JSON.stringify({ query: mutation, variables: { id: workspaceId } })
6199
+ );
6200
+ await this.waitUntilRunning(token, workspaceId);
6201
+ return { id: workspaceId, displayName: workspaceId };
6202
+ }
6203
+ async exec(workspaceId, command2) {
6204
+ const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
6205
+ try {
6206
+ const { stdout, stderr } = await execFileP4(
6207
+ "ssh",
6208
+ [
6209
+ "-o",
6210
+ "StrictHostKeyChecking=accept-new",
6211
+ "-o",
6212
+ "BatchMode=yes",
6213
+ `${workspaceId}@${sshHost}`,
6214
+ command2
6215
+ ],
6216
+ { maxBuffer: MAX_BUFFER3, timeout: 6e5 }
6217
+ );
6218
+ return { stdout, stderr, code: 0 };
6219
+ } catch (err) {
6220
+ const e = err;
6221
+ return {
6222
+ stdout: e.stdout ?? "",
6223
+ stderr: e.stderr ?? e.message ?? "ssh to GitLab workspace failed",
6224
+ code: typeof e.code === "number" ? e.code : 1
6225
+ };
6226
+ }
6227
+ }
6228
+ async streamCommand(workspaceId, command2) {
6229
+ const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
6230
+ resetStdinForChild3();
6231
+ return new Promise((resolve2, reject) => {
6232
+ const proc = (0, import_child_process7.spawn)(
6233
+ "ssh",
6234
+ ["-tt", "-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, command2],
6235
+ { stdio: "inherit" }
6236
+ );
6237
+ proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
6238
+ proc.on("error", reject);
6239
+ });
6240
+ }
6241
+ async uploadDirectory(workspaceId, localDir, remoteDir, options = {}) {
6242
+ const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
6243
+ const tarArgs = ["-czf", "-", "-C", localDir];
6244
+ for (const pattern of options.exclude ?? []) {
6245
+ tarArgs.push(`--exclude=${pattern}`);
6246
+ const stripped = pattern.replace(/^\.\/+/, "");
6247
+ if (stripped !== pattern) tarArgs.push(`--exclude=${stripped}`);
6248
+ }
6249
+ tarArgs.push(".");
6250
+ const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
6251
+ const remoteCmd = `mkdir -p ${shellQuote3(remoteDir)} && tar -xzf - -C ${shellQuote3(remoteDir)}`;
6252
+ await new Promise((resolve2, reject) => {
6253
+ const tar = (0, import_child_process7.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
6254
+ const ssh = (0, import_child_process7.spawn)(
6255
+ "ssh",
6256
+ ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, remoteCmd],
6257
+ { stdio: [tar.stdout, "pipe", "pipe"] }
6258
+ );
6259
+ let tarErr = "";
6260
+ let sshErr = "";
6261
+ tar.stderr?.on("data", (d3) => {
6262
+ tarErr += d3.toString();
6263
+ });
6264
+ ssh.stderr?.on("data", (d3) => {
6265
+ sshErr += d3.toString();
6266
+ });
6267
+ tar.on("error", reject);
6268
+ ssh.on("error", reject);
6269
+ ssh.on("exit", (code) => {
6270
+ if (code === 0) resolve2();
6271
+ else reject(new Error(`Remote tar failed: ${(sshErr || tarErr || `exit ${code}`).trim().slice(0, 500)}`));
6272
+ });
6273
+ });
6274
+ }
6275
+ async uploadFile(workspaceId, remotePath, contents, options = {}) {
6276
+ const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
6277
+ const remoteDir = path10.posix.dirname(remotePath);
6278
+ const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
6279
+ if (options.mode != null) {
6280
+ parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
6281
+ }
6282
+ const cmd = parts.join(" && ");
6283
+ await new Promise((resolve2, reject) => {
6284
+ const proc = (0, import_child_process7.spawn)(
6285
+ "ssh",
6286
+ ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, cmd],
6287
+ { stdio: ["pipe", "pipe", "pipe"] }
6288
+ );
6289
+ let stderr = "";
6290
+ proc.stderr?.on("data", (d3) => {
6291
+ stderr += d3.toString();
6292
+ });
6293
+ proc.on("error", reject);
6294
+ proc.on("exit", (code) => {
6295
+ if (code === 0) resolve2();
6296
+ else reject(new Error(`Remote write failed: ${(stderr || `exit ${code}`).trim().slice(0, 500)}`));
6297
+ });
6298
+ proc.stdin?.write(contents);
6299
+ proc.stdin?.end();
6300
+ });
6301
+ }
6302
+ /**
6303
+ * Pull the user's `glab` token via `glab auth status -t`, since
6304
+ * `glab` stores it in its own config and we need it to call the
6305
+ * GraphQL API directly.
6306
+ */
6307
+ async getGlabToken() {
6308
+ try {
6309
+ const { stdout, stderr } = await execFileP4(
6310
+ "glab",
6311
+ ["auth", "status", "--show-token"],
6312
+ { maxBuffer: MAX_BUFFER3 }
6313
+ );
6314
+ const haystack = stdout + "\n" + stderr;
6315
+ const m = haystack.match(/Token:\s+(\S+)/);
6316
+ return m?.[1] ?? null;
6317
+ } catch {
6318
+ return null;
6319
+ }
6320
+ }
6321
+ async gql(token, body) {
6322
+ const url = `${GITLAB_API_BASE.replace(/\/$/, "").replace(/\/v4$/, "")}/api/graphql`;
6323
+ const res = await fetch(url, {
6324
+ method: "POST",
6325
+ headers: {
6326
+ "Content-Type": "application/json",
6327
+ Authorization: `Bearer ${token}`
6328
+ },
6329
+ body
6330
+ });
6331
+ if (!res.ok) {
6332
+ const text = await res.text();
6333
+ throw new Error(`GitLab GraphQL ${res.status}: ${text.slice(0, 400)}`);
6334
+ }
6335
+ return await res.json();
6336
+ }
6337
+ };
6338
+ function shellQuote3(s) {
6339
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6340
+ }
6341
+
6342
+ // src/services/providers/railway.ts
6343
+ var import_child_process8 = require("child_process");
6344
+ var import_util5 = require("util");
6345
+ var path11 = __toESM(require("path"));
6346
+ var execFileP5 = (0, import_util5.promisify)(import_child_process8.execFile);
6347
+ var MAX_BUFFER4 = 8 * 1024 * 1024;
6348
+ function resetStdinForChild4() {
6349
+ if (process.stdin.isTTY) {
6350
+ try {
6351
+ process.stdin.setRawMode(false);
6352
+ } catch {
6353
+ }
6354
+ }
6355
+ }
6356
+ var RailwayProvider = class {
6357
+ id = "railway";
6358
+ displayName = "Railway";
6359
+ tagline = "Always-on container \u2014 no IDE, just an agent terminal";
6360
+ available = true;
6361
+ async authorize() {
6362
+ try {
6363
+ await execFileP5("railway", ["--version"], { maxBuffer: MAX_BUFFER4 });
6364
+ } catch {
6365
+ throw new Error(
6366
+ [
6367
+ "Railway CLI (`railway`) is required for Railway deploys.",
6368
+ "Install it with one of:",
6369
+ " \u2022 npm: npm install -g @railway/cli",
6370
+ " \u2022 macOS: brew install railway",
6371
+ " \u2022 Linux: https://docs.railway.app/develop/cli#install",
6372
+ "Then run `railway login` and try `codeam deploy` again."
6373
+ ].join("\n")
6374
+ );
6375
+ }
6376
+ try {
6377
+ await execFileP5("railway", ["whoami"], { maxBuffer: MAX_BUFFER4 });
6378
+ return;
6379
+ } catch {
6380
+ }
6381
+ wt(
6382
+ "A login URL prints below. Open it in your browser and approve.",
6383
+ "Authenticating Railway"
6384
+ );
6385
+ resetStdinForChild4();
6386
+ await new Promise((resolve2, reject) => {
6387
+ const proc = (0, import_child_process8.spawn)("railway", ["login"], { stdio: "inherit" });
6388
+ proc.on("exit", (code) => {
6389
+ if (code === 0) resolve2();
6390
+ else reject(new Error("railway login failed."));
6391
+ });
6392
+ proc.on("error", reject);
6393
+ });
6394
+ }
6395
+ async listProjects() {
6396
+ try {
6397
+ const { stdout } = await execFileP5(
6398
+ "railway",
6399
+ ["list", "--json"],
6400
+ { maxBuffer: MAX_BUFFER4 }
6401
+ );
6402
+ const list = JSON.parse(stdout);
6403
+ return list.filter((r) => r.id && r.name).map((r) => ({
6404
+ id: r.id,
6405
+ name: r.name,
6406
+ fullName: r.name,
6407
+ description: r.description ?? void 0,
6408
+ private: true
6409
+ // Railway projects are private by default
6410
+ }));
6411
+ } catch {
6412
+ try {
6413
+ const { stdout } = await execFileP5("railway", ["list"], { maxBuffer: MAX_BUFFER4 });
6414
+ const projects = [];
6415
+ for (const line of stdout.split("\n")) {
6416
+ const trimmed = line.trim();
6417
+ if (!trimmed || trimmed.startsWith("Project")) continue;
6418
+ const idMatch = trimmed.match(/id:\s*(\S+)/);
6419
+ const nameMatch = trimmed.match(/name:\s*([^|]+)/);
6420
+ if (idMatch && nameMatch) {
6421
+ const name = nameMatch[1].trim();
6422
+ projects.push({
6423
+ id: idMatch[1],
6424
+ name,
6425
+ fullName: name,
6426
+ private: true
6427
+ });
6428
+ }
6429
+ }
6430
+ return projects;
6431
+ } catch {
6432
+ return [];
6433
+ }
6434
+ }
6435
+ }
6436
+ /**
6437
+ * Railway exposes resource sizes (CPU / RAM) at the SERVICE level,
6438
+ * not the project level — and only on paid plans. The CLI has no
6439
+ * `list-classes` equivalent today. Returning empty makes the
6440
+ * orchestrator skip the picker; users on paid plans can resize
6441
+ * a service from the Railway dashboard after deploy.
6442
+ */
6443
+ async listMachineTypes(_projectId) {
6444
+ return [];
6445
+ }
6446
+ async createWorkspace(_projectId, _machineTypeId) {
6447
+ wt(
6448
+ [
6449
+ "Railway service auto-creation from `codeam deploy` isn't implemented yet.",
6450
+ "Workaround for now:",
6451
+ " 1. From your repo: railway link (pick this project)",
6452
+ " 2. Run: railway up --detach",
6453
+ " 3. Re-run codeam deploy and pick the existing service."
6454
+ ].join("\n"),
6455
+ "Heads up \u2014 manual step needed"
6456
+ );
6457
+ throw new Error(
6458
+ "Railway provider needs an existing service to attach to. See the note above."
6459
+ );
6460
+ }
6461
+ async listExistingWorkspaces(projectId) {
6462
+ if (!projectId) return [];
6463
+ try {
6464
+ const { stdout } = await execFileP5(
6465
+ "railway",
6466
+ ["service", "list", "--project", projectId, "--json"],
6467
+ { maxBuffer: MAX_BUFFER4 }
6468
+ );
6469
+ const list = JSON.parse(stdout);
6470
+ return list.filter((s) => s.id && s.name).map((s) => {
6471
+ const latest = s.deployments?.[0];
6472
+ return {
6473
+ id: `${projectId}/${s.id}`,
6474
+ displayName: s.name,
6475
+ state: latest?.status ?? "Unknown",
6476
+ lastUsedAt: latest?.updatedAt
6477
+ };
6478
+ });
6479
+ } catch {
6480
+ return [];
6481
+ }
6482
+ }
6483
+ async startWorkspace(workspaceId) {
6484
+ const [projectId, serviceId] = workspaceId.split("/");
6485
+ if (!projectId || !serviceId) {
6486
+ throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
6487
+ }
6488
+ try {
6489
+ await execFileP5(
6490
+ "railway",
6491
+ ["service", "restart", "--service", serviceId, "--project", projectId],
6492
+ { maxBuffer: MAX_BUFFER4, timeout: 6e4 }
6493
+ );
6494
+ } catch {
6495
+ }
6496
+ return { id: workspaceId, displayName: serviceId };
6497
+ }
6498
+ async exec(workspaceId, command2) {
6499
+ const [projectId, serviceId] = workspaceId.split("/");
6500
+ if (!projectId || !serviceId) {
6501
+ return {
6502
+ stdout: "",
6503
+ stderr: "Invalid Railway workspace id (expected projectId/serviceId).",
6504
+ code: 1
6505
+ };
6506
+ }
6507
+ try {
6508
+ const { stdout, stderr } = await execFileP5(
6509
+ "railway",
6510
+ ["run", "--project", projectId, "--service", serviceId, "--", "bash", "-lc", command2],
6511
+ { maxBuffer: MAX_BUFFER4, timeout: 6e5 }
6512
+ );
6513
+ return { stdout, stderr, code: 0 };
6514
+ } catch (err) {
6515
+ const e = err;
6516
+ return {
6517
+ stdout: e.stdout ?? "",
6518
+ stderr: e.stderr ?? e.message ?? "railway run failed",
6519
+ code: typeof e.code === "number" ? e.code : 1
6520
+ };
6521
+ }
6522
+ }
6523
+ async streamCommand(workspaceId, command2) {
6524
+ const [projectId, serviceId] = workspaceId.split("/");
6525
+ if (!projectId || !serviceId) {
6526
+ throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
6527
+ }
6528
+ resetStdinForChild4();
6529
+ return new Promise((resolve2, reject) => {
6530
+ const proc = (0, import_child_process8.spawn)(
6531
+ "railway",
6532
+ ["shell", "--project", projectId, "--service", serviceId, "--command", command2],
6533
+ { stdio: "inherit" }
6534
+ );
6535
+ proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
6536
+ proc.on("error", reject);
6537
+ });
6538
+ }
6539
+ async uploadDirectory(workspaceId, localDir, remoteDir, options = {}) {
6540
+ const [projectId, serviceId] = workspaceId.split("/");
6541
+ if (!projectId || !serviceId) {
6542
+ throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
6543
+ }
6544
+ const tarArgs = ["-czf", "-", "-C", localDir];
6545
+ for (const pattern of options.exclude ?? []) {
6546
+ tarArgs.push(`--exclude=${pattern}`);
6547
+ const stripped = pattern.replace(/^\.\/+/, "");
6548
+ if (stripped !== pattern) tarArgs.push(`--exclude=${stripped}`);
6549
+ }
6550
+ tarArgs.push(".");
6551
+ const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
6552
+ const remoteCmd = `mkdir -p ${shellQuote4(remoteDir)} && tar -xzf - -C ${shellQuote4(remoteDir)}`;
6553
+ await new Promise((resolve2, reject) => {
6554
+ const tar = (0, import_child_process8.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
6555
+ const sh = (0, import_child_process8.spawn)(
6556
+ "railway",
6557
+ ["shell", "--project", projectId, "--service", serviceId, "--command", remoteCmd],
6558
+ { stdio: [tar.stdout, "pipe", "pipe"] }
6559
+ );
6560
+ let tarErr = "";
6561
+ let shErr = "";
6562
+ tar.stderr?.on("data", (d3) => {
6563
+ tarErr += d3.toString();
6564
+ });
6565
+ sh.stderr?.on("data", (d3) => {
6566
+ shErr += d3.toString();
6567
+ });
6568
+ tar.on("error", reject);
6569
+ sh.on("error", reject);
6570
+ sh.on("exit", (code) => {
6571
+ if (code === 0) resolve2();
6572
+ else reject(new Error(`Remote tar failed: ${(shErr || tarErr || `exit ${code}`).trim().slice(0, 500)}`));
6573
+ });
6574
+ });
6575
+ }
6576
+ async uploadFile(workspaceId, remotePath, contents, options = {}) {
6577
+ const [projectId, serviceId] = workspaceId.split("/");
6578
+ if (!projectId || !serviceId) {
6579
+ throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
6580
+ }
6581
+ const remoteDir = path11.posix.dirname(remotePath);
6582
+ const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
6583
+ if (options.mode != null) {
6584
+ parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
6585
+ }
6586
+ const cmd = parts.join(" && ");
6587
+ await new Promise((resolve2, reject) => {
6588
+ const proc = (0, import_child_process8.spawn)(
6589
+ "railway",
6590
+ ["shell", "--project", projectId, "--service", serviceId, "--command", cmd],
6591
+ { stdio: ["pipe", "pipe", "pipe"] }
6592
+ );
6593
+ let stderr = "";
6594
+ proc.stderr?.on("data", (d3) => {
6595
+ stderr += d3.toString();
6596
+ });
6597
+ proc.on("error", reject);
6598
+ proc.on("exit", (code) => {
6599
+ if (code === 0) resolve2();
6600
+ else reject(new Error(`Remote write failed: ${(stderr || `exit ${code}`).trim().slice(0, 500)}`));
6601
+ });
6602
+ proc.stdin?.write(contents);
6603
+ proc.stdin?.end();
6604
+ });
6605
+ }
6606
+ };
6607
+ function shellQuote4(s) {
6608
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6609
+ }
6610
+
5718
6611
  // src/services/providers/index.ts
5719
6612
  var PROVIDERS = [
5720
- new GitHubCodespacesProvider()
5721
- // Sketches for future providers — uncomment + implement when ready.
5722
- // new GitpodProvider(),
5723
- // new CoderProvider(),
5724
- // new GitLabWebIDEProvider(),
6613
+ new GitHubCodespacesProvider(),
6614
+ new GitpodProvider(),
6615
+ new GitLabWorkspacesProvider(),
6616
+ new RailwayProvider()
5725
6617
  ];
5726
6618
 
5727
6619
  // src/commands/deploy.ts
5728
- var execFileP3 = (0, import_util3.promisify)(import_child_process6.execFile);
6620
+ var execFileP6 = (0, import_util6.promisify)(import_child_process9.execFile);
5729
6621
  async function deploy() {
5730
6622
  console.log();
5731
- mt(import_picocolors8.default.bgMagenta(import_picocolors8.default.white(" codeam deploy ")));
6623
+ mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ")));
5732
6624
  const provider = await pickProvider();
5733
6625
  if (!provider) {
5734
6626
  pt("No provider selected.");
@@ -5765,7 +6657,7 @@ async function deploy() {
5765
6657
  if (provider.expandListScopes) {
5766
6658
  options.push({
5767
6659
  value: EXPAND_SCOPES,
5768
- label: import_picocolors8.default.cyan("+ Don't see your project? Expand scopes\u2026"),
6660
+ label: import_picocolors9.default.cyan("+ Don't see your project? Expand scopes\u2026"),
5769
6661
  hint: "Re-authorize with broader scopes (org / team repos)"
5770
6662
  });
5771
6663
  }
@@ -5813,7 +6705,7 @@ async function deploy() {
5813
6705
  label: w3.displayName ?? w3.id,
5814
6706
  hint: [w3.state, formatLastUsed(w3.lastUsedAt)].filter(Boolean).join(" \xB7 ")
5815
6707
  })),
5816
- { value: "__new__", label: import_picocolors8.default.green("+ Create a new workspace"), hint: "fresh codespace" }
6708
+ { value: "__new__", label: import_picocolors9.default.green("+ Create a new workspace"), hint: "fresh codespace" }
5817
6709
  ]
5818
6710
  });
5819
6711
  if (q(choice)) {
@@ -5880,7 +6772,7 @@ async function deploy() {
5880
6772
  process.exit(1);
5881
6773
  }
5882
6774
  }
5883
- const localClaudeDir = path9.join(os6.homedir(), ".claude");
6775
+ const localClaudeDir = path12.join(os6.homedir(), ".claude");
5884
6776
  const localCredsKind = await detectLocalClaudeCredentials(localClaudeDir);
5885
6777
  let bridged = "none";
5886
6778
  if (localCredsKind !== "none") {
@@ -5978,7 +6870,7 @@ async function deploy() {
5978
6870
  }
5979
6871
  }
5980
6872
  if (bridged !== "none") {
5981
- const localClaudeJson = path9.join(os6.homedir(), ".claude.json");
6873
+ const localClaudeJson = path12.join(os6.homedir(), ".claude.json");
5982
6874
  if (fs8.existsSync(localClaudeJson)) {
5983
6875
  try {
5984
6876
  const contents = fs8.readFileSync(localClaudeJson);
@@ -6020,12 +6912,12 @@ async function deploy() {
6020
6912
  cliStep.stop("\u2713 codeam-cli installed");
6021
6913
  wt(
6022
6914
  [
6023
- `Workspace: ${import_picocolors8.default.cyan(workspace.displayName ?? workspace.id)}`,
6024
- workspace.webUrl ? `Web: ${import_picocolors8.default.cyan(workspace.webUrl)}` : "",
6915
+ `Workspace: ${import_picocolors9.default.cyan(workspace.displayName ?? workspace.id)}`,
6916
+ workspace.webUrl ? `Web: ${import_picocolors9.default.cyan(workspace.webUrl)}` : "",
6025
6917
  "",
6026
6918
  "Starting `codeam pair` on the workspace.",
6027
6919
  "Scan the QR code below with the CodeAgent Mobile app to finish pairing.",
6028
- import_picocolors8.default.dim("(Once paired, this terminal disconnects automatically; the session stays alive on the codespace.)")
6920
+ import_picocolors9.default.dim("(Once paired, this terminal disconnects automatically; the session stays alive on the codespace.)")
6029
6921
  ].filter(Boolean).join("\n"),
6030
6922
  "Almost there"
6031
6923
  );
@@ -6141,11 +7033,11 @@ async function deploy() {
6141
7033
  ].join("\n");
6142
7034
  const code = (await provider.streamCommand(workspace.id, `bash -lc ${shellQuoteSingle(wrapper)}`)).code;
6143
7035
  if (code === 0) {
6144
- gt(import_picocolors8.default.green("\u2713 Workspace deployed and paired. Drive from your phone, anywhere."));
7036
+ gt(import_picocolors9.default.green("\u2713 Workspace deployed and paired. Drive from your phone, anywhere."));
6145
7037
  } else if (code === 130) {
6146
- gt(import_picocolors8.default.yellow("Disconnected from local terminal. Mobile session keeps running on the codespace."));
7038
+ gt(import_picocolors9.default.yellow("Disconnected from local terminal. Mobile session keeps running on the codespace."));
6147
7039
  } else {
6148
- gt(import_picocolors8.default.yellow('Pairing did not complete. Run "codeam pair" inside the codespace if needed.'));
7040
+ gt(import_picocolors9.default.yellow('Pairing did not complete. Run "codeam pair" inside the codespace if needed.'));
6149
7041
  }
6150
7042
  }
6151
7043
  function shellQuoteSingle(s) {
@@ -6171,12 +7063,12 @@ async function runRemoteClaudeLogin(provider, workspaceId) {
6171
7063
  }
6172
7064
  }
6173
7065
  async function detectLocalClaudeCredentials(localClaudeDir) {
6174
- if (fs8.existsSync(path9.join(localClaudeDir, ".credentials.json"))) {
7066
+ if (fs8.existsSync(path12.join(localClaudeDir, ".credentials.json"))) {
6175
7067
  return "flat-file";
6176
7068
  }
6177
7069
  if (process.platform === "darwin") {
6178
7070
  try {
6179
- await execFileP3(
7071
+ await execFileP6(
6180
7072
  "security",
6181
7073
  ["find-generic-password", "-s", "Claude Code-credentials"],
6182
7074
  { maxBuffer: 1024 * 1024 }
@@ -6204,11 +7096,11 @@ async function verifyClaudeAuth(provider, workspaceId) {
6204
7096
  }
6205
7097
  }
6206
7098
  async function bridgeClaudeCredentials(provider, workspaceId, localClaudeDir) {
6207
- const fileBased = path9.join(localClaudeDir, ".credentials.json");
7099
+ const fileBased = path12.join(localClaudeDir, ".credentials.json");
6208
7100
  if (fs8.existsSync(fileBased)) return "flat-file";
6209
7101
  if (process.platform === "darwin") {
6210
7102
  try {
6211
- const { stdout } = await execFileP3(
7103
+ const { stdout } = await execFileP6(
6212
7104
  "security",
6213
7105
  ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
6214
7106
  { maxBuffer: 1024 * 1024 }
@@ -6256,7 +7148,7 @@ async function pickProvider() {
6256
7148
  message: "Where do you want to deploy?",
6257
7149
  options: PROVIDERS.map((prov) => ({
6258
7150
  value: prov.id,
6259
- label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors8.default.dim("(coming soon)")}`,
7151
+ label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors9.default.dim("(coming soon)")}`,
6260
7152
  hint: prov.tagline
6261
7153
  }))
6262
7154
  });
@@ -6273,27 +7165,27 @@ async function pickProvider() {
6273
7165
  }
6274
7166
 
6275
7167
  // src/commands/deploy-manage.ts
6276
- var import_picocolors9 = __toESM(require("picocolors"));
7168
+ var import_picocolors10 = __toESM(require("picocolors"));
6277
7169
  async function deployList() {
6278
7170
  console.log();
6279
- mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ls ")));
7171
+ mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy ls ")));
6280
7172
  const workspaces = await collectWorkspacesWithStatus();
6281
7173
  if (workspaces.length === 0) {
6282
- gt(import_picocolors9.default.dim("No deployed workspaces found."));
7174
+ gt(import_picocolors10.default.dim("No deployed workspaces found."));
6283
7175
  return;
6284
7176
  }
6285
7177
  for (const w3 of workspaces) {
6286
- const tag = w3.codeamRunning ? import_picocolors9.default.green("\u25CF running") : w3.state === "Available" ? import_picocolors9.default.dim("\u25CB idle") : import_picocolors9.default.dim(`\u25CB ${w3.state ?? "stopped"}`);
6287
- console.log(` ${tag} ${import_picocolors9.default.cyan(w3.displayName ?? w3.id)} ${import_picocolors9.default.dim("(" + w3.providerName + ")")}`);
7178
+ const tag = w3.codeamRunning ? import_picocolors10.default.green("\u25CF running") : w3.state === "Available" ? import_picocolors10.default.dim("\u25CB idle") : import_picocolors10.default.dim(`\u25CB ${w3.state ?? "stopped"}`);
7179
+ console.log(` ${tag} ${import_picocolors10.default.cyan(w3.displayName ?? w3.id)} ${import_picocolors10.default.dim("(" + w3.providerName + ")")}`);
6288
7180
  }
6289
- gt(import_picocolors9.default.dim("Use `codeam deploy stop` to terminate a session."));
7181
+ gt(import_picocolors10.default.dim("Use `codeam deploy stop` to terminate a session."));
6290
7182
  }
6291
7183
  async function deployStop() {
6292
7184
  console.log();
6293
- mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy stop ")));
7185
+ mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy stop ")));
6294
7186
  const workspaces = await collectWorkspacesWithStatus();
6295
7187
  if (workspaces.length === 0) {
6296
- gt(import_picocolors9.default.dim("No deployed workspaces found."));
7188
+ gt(import_picocolors10.default.dim("No deployed workspaces found."));
6297
7189
  return;
6298
7190
  }
6299
7191
  const choice = await _t({
@@ -6303,7 +7195,7 @@ async function deployStop() {
6303
7195
  label: w3.displayName ?? w3.id,
6304
7196
  hint: [
6305
7197
  w3.providerName,
6306
- w3.codeamRunning ? import_picocolors9.default.green("\u25CF codeam-pair running") : import_picocolors9.default.dim("\u25CB no codeam-pair"),
7198
+ w3.codeamRunning ? import_picocolors10.default.green("\u25CF codeam-pair running") : import_picocolors10.default.dim("\u25CB no codeam-pair"),
6307
7199
  w3.state ?? ""
6308
7200
  ].filter(Boolean).join(" \xB7 ")
6309
7201
  }))
@@ -6331,7 +7223,7 @@ async function deployStop() {
6331
7223
  O2.info("No codeam-pair process to stop on this workspace.");
6332
7224
  }
6333
7225
  const alsoStop = await ot2({
6334
- message: `Also stop the workspace ${import_picocolors9.default.cyan(target.displayName ?? target.id)} to save compute hours?`,
7226
+ message: `Also stop the workspace ${import_picocolors10.default.cyan(target.displayName ?? target.id)} to save compute hours?`,
6335
7227
  initialValue: true
6336
7228
  });
6337
7229
  if (!q(alsoStop) && alsoStop) {
@@ -6353,7 +7245,7 @@ async function deployStop() {
6353
7245
  O2.warn(err instanceof Error ? err.message : String(err));
6354
7246
  }
6355
7247
  }
6356
- gt(import_picocolors9.default.green("\u2713 Done."));
7248
+ gt(import_picocolors10.default.green("\u2713 Done."));
6357
7249
  }
6358
7250
  async function collectWorkspacesWithStatus() {
6359
7251
  const out = [];
@@ -6402,10 +7294,10 @@ async function probeCodeamPair(provider, workspace) {
6402
7294
  }
6403
7295
  async function stopWorkspaceFromLocal(target) {
6404
7296
  if (target.provider.id === "github-codespaces") {
6405
- const { execFile: execFile4 } = await import("child_process");
6406
- const { promisify: promisify4 } = await import("util");
6407
- const execFileP4 = promisify4(execFile4);
6408
- await execFileP4("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
7297
+ const { execFile: execFile7 } = await import("child_process");
7298
+ const { promisify: promisify7 } = await import("util");
7299
+ const execFileP7 = promisify7(execFile7);
7300
+ await execFileP7("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
6409
7301
  return;
6410
7302
  }
6411
7303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.4.24",
3
+ "version": "2.4.26",
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": {