codeam-cli 2.4.23 → 2.4.25
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 +18 -0
- package/dist/index.js +947 -52
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ 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.24] — 2026-05-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **cli:** List repos from user's orgs after expand-scopes (v2.4.24)
|
|
12
|
+
|
|
13
|
+
## [2.4.23] — 2026-05-03
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **cli:** Shutdown_session also runs gh codespace stop (v2.4.23)
|
|
18
|
+
|
|
19
|
+
## [2.4.22] — 2026-05-03
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **cli:** "+ Don't see your project?" expands gh OAuth scopes (v2.4.22)
|
|
24
|
+
|
|
7
25
|
## [2.4.21] — 2026-05-03
|
|
8
26
|
|
|
9
27
|
### 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.
|
|
182
|
+
version: "2.4.25",
|
|
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: {
|
|
@@ -5148,12 +5148,12 @@ async function logout() {
|
|
|
5148
5148
|
}
|
|
5149
5149
|
|
|
5150
5150
|
// src/commands/deploy.ts
|
|
5151
|
-
var
|
|
5151
|
+
var import_child_process9 = require("child_process");
|
|
5152
5152
|
var fs8 = __toESM(require("fs"));
|
|
5153
5153
|
var os6 = __toESM(require("os"));
|
|
5154
|
-
var
|
|
5155
|
-
var
|
|
5156
|
-
var
|
|
5154
|
+
var path12 = __toESM(require("path"));
|
|
5155
|
+
var import_util6 = require("util");
|
|
5156
|
+
var import_picocolors9 = __toESM(require("picocolors"));
|
|
5157
5157
|
|
|
5158
5158
|
// src/services/providers/github-codespaces.ts
|
|
5159
5159
|
var import_child_process5 = require("child_process");
|
|
@@ -5430,20 +5430,43 @@ var GitHubCodespacesProvider = class {
|
|
|
5430
5430
|
});
|
|
5431
5431
|
}
|
|
5432
5432
|
async listProjects() {
|
|
5433
|
-
const
|
|
5434
|
-
"
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
"list",
|
|
5433
|
+
const fetchRepos = async (owner) => {
|
|
5434
|
+
const args2 = ["repo", "list"];
|
|
5435
|
+
if (owner) args2.push(owner);
|
|
5436
|
+
args2.push(
|
|
5438
5437
|
"--json",
|
|
5439
5438
|
"name,nameWithOwner,description,defaultBranchRef,isPrivate",
|
|
5440
5439
|
"--limit",
|
|
5441
5440
|
"200"
|
|
5442
|
-
|
|
5443
|
-
{
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5441
|
+
);
|
|
5442
|
+
try {
|
|
5443
|
+
const { stdout } = await execFileP2("gh", args2, { maxBuffer: MAX_BUFFER });
|
|
5444
|
+
return JSON.parse(stdout);
|
|
5445
|
+
} catch {
|
|
5446
|
+
return [];
|
|
5447
|
+
}
|
|
5448
|
+
};
|
|
5449
|
+
const own = await fetchRepos();
|
|
5450
|
+
let orgRepos = [];
|
|
5451
|
+
try {
|
|
5452
|
+
const { stdout } = await execFileP2(
|
|
5453
|
+
"gh",
|
|
5454
|
+
["api", "--paginate", "user/orgs", "--jq", ".[].login"],
|
|
5455
|
+
{ maxBuffer: MAX_BUFFER }
|
|
5456
|
+
);
|
|
5457
|
+
const orgLogins = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
5458
|
+
const perOrg = await Promise.all(orgLogins.map((org) => fetchRepos(org)));
|
|
5459
|
+
orgRepos = perOrg.flat();
|
|
5460
|
+
} catch {
|
|
5461
|
+
}
|
|
5462
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5463
|
+
const merged = [];
|
|
5464
|
+
for (const r of [...own, ...orgRepos]) {
|
|
5465
|
+
if (seen.has(r.nameWithOwner)) continue;
|
|
5466
|
+
seen.add(r.nameWithOwner);
|
|
5467
|
+
merged.push(r);
|
|
5468
|
+
}
|
|
5469
|
+
return merged.map((r) => ({
|
|
5447
5470
|
id: r.nameWithOwner,
|
|
5448
5471
|
name: r.name,
|
|
5449
5472
|
fullName: r.nameWithOwner,
|
|
@@ -5692,20 +5715,892 @@ function shellQuote(s) {
|
|
|
5692
5715
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
5693
5716
|
}
|
|
5694
5717
|
|
|
5718
|
+
// src/services/providers/gitpod.ts
|
|
5719
|
+
var import_child_process6 = require("child_process");
|
|
5720
|
+
var import_util3 = require("util");
|
|
5721
|
+
var path9 = __toESM(require("path"));
|
|
5722
|
+
var import_picocolors8 = __toESM(require("picocolors"));
|
|
5723
|
+
var execFileP3 = (0, import_util3.promisify)(import_child_process6.execFile);
|
|
5724
|
+
var MAX_BUFFER2 = 8 * 1024 * 1024;
|
|
5725
|
+
function resetStdinForChild2() {
|
|
5726
|
+
if (process.stdin.isTTY) {
|
|
5727
|
+
try {
|
|
5728
|
+
process.stdin.setRawMode(false);
|
|
5729
|
+
} catch {
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
var GitpodProvider = class {
|
|
5734
|
+
id = "gitpod";
|
|
5735
|
+
displayName = "Gitpod";
|
|
5736
|
+
tagline = "Cloud dev environments from any Git repo";
|
|
5737
|
+
available = true;
|
|
5738
|
+
async authorize() {
|
|
5739
|
+
try {
|
|
5740
|
+
await execFileP3("gitpod", ["--version"], { maxBuffer: MAX_BUFFER2 });
|
|
5741
|
+
} catch {
|
|
5742
|
+
throw new Error(
|
|
5743
|
+
[
|
|
5744
|
+
"Gitpod CLI (`gitpod`) is required for Gitpod deploys.",
|
|
5745
|
+
"Install it with one of:",
|
|
5746
|
+
" \u2022 macOS: brew install gitpod-io/tap/gitpod",
|
|
5747
|
+
" \u2022 Other: https://github.com/gitpod-io/gitpod-cli#installation",
|
|
5748
|
+
"Then run `gitpod login` and try `codeam deploy` again."
|
|
5749
|
+
].join("\n")
|
|
5750
|
+
);
|
|
5751
|
+
}
|
|
5752
|
+
try {
|
|
5753
|
+
await execFileP3("gitpod", ["whoami"], { maxBuffer: MAX_BUFFER2 });
|
|
5754
|
+
return;
|
|
5755
|
+
} catch {
|
|
5756
|
+
}
|
|
5757
|
+
wt(
|
|
5758
|
+
"A login URL will print below. Open it in your browser and approve.",
|
|
5759
|
+
"Authenticating Gitpod"
|
|
5760
|
+
);
|
|
5761
|
+
resetStdinForChild2();
|
|
5762
|
+
await new Promise((resolve2, reject) => {
|
|
5763
|
+
const proc = (0, import_child_process6.spawn)("gitpod", ["login"], { stdio: "inherit" });
|
|
5764
|
+
proc.on("exit", (code) => {
|
|
5765
|
+
if (code === 0) resolve2();
|
|
5766
|
+
else reject(new Error("gitpod login failed."));
|
|
5767
|
+
});
|
|
5768
|
+
proc.on("error", reject);
|
|
5769
|
+
});
|
|
5770
|
+
}
|
|
5771
|
+
async listProjects() {
|
|
5772
|
+
try {
|
|
5773
|
+
const { stdout } = await execFileP3(
|
|
5774
|
+
"gitpod",
|
|
5775
|
+
["workspace", "list", "--output", "json", "--limit", "200"],
|
|
5776
|
+
{ maxBuffer: MAX_BUFFER2 }
|
|
5777
|
+
);
|
|
5778
|
+
const list = JSON.parse(stdout);
|
|
5779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5780
|
+
const projects = [];
|
|
5781
|
+
for (const w3 of list) {
|
|
5782
|
+
const url = w3.contextUrl ?? "";
|
|
5783
|
+
if (!url || seen.has(url)) continue;
|
|
5784
|
+
seen.add(url);
|
|
5785
|
+
const m = url.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/.+)?$/);
|
|
5786
|
+
if (!m) continue;
|
|
5787
|
+
const fullName = `${m[2]}/${m[3]}`;
|
|
5788
|
+
projects.push({
|
|
5789
|
+
id: url,
|
|
5790
|
+
// Gitpod indexes workspaces by URL
|
|
5791
|
+
name: m[3],
|
|
5792
|
+
fullName,
|
|
5793
|
+
description: w3.description,
|
|
5794
|
+
private: false
|
|
5795
|
+
// Gitpod doesn't expose visibility here
|
|
5796
|
+
});
|
|
5797
|
+
}
|
|
5798
|
+
return projects;
|
|
5799
|
+
} catch {
|
|
5800
|
+
return [];
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
async createWorkspace(projectId, machineTypeId) {
|
|
5804
|
+
const args2 = ["workspace", "create", projectId, "--start", "--output", "json"];
|
|
5805
|
+
if (machineTypeId) args2.push("--class", machineTypeId);
|
|
5806
|
+
const { stdout } = await execFileP3("gitpod", args2, {
|
|
5807
|
+
maxBuffer: MAX_BUFFER2,
|
|
5808
|
+
timeout: 3e5
|
|
5809
|
+
});
|
|
5810
|
+
let parsed;
|
|
5811
|
+
try {
|
|
5812
|
+
parsed = JSON.parse(stdout);
|
|
5813
|
+
} catch {
|
|
5814
|
+
parsed = { id: stdout.trim() };
|
|
5815
|
+
}
|
|
5816
|
+
if (!parsed.id) {
|
|
5817
|
+
throw new Error("Gitpod did not return a workspace id.");
|
|
5818
|
+
}
|
|
5819
|
+
await this.waitUntilRunning(parsed.id);
|
|
5820
|
+
return {
|
|
5821
|
+
id: parsed.id,
|
|
5822
|
+
displayName: parsed.id,
|
|
5823
|
+
webUrl: parsed.url ?? `https://gitpod.io/start/#${parsed.id}`
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
async waitUntilRunning(workspaceId) {
|
|
5827
|
+
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
5828
|
+
while (Date.now() < deadline) {
|
|
5829
|
+
try {
|
|
5830
|
+
const { stdout } = await execFileP3(
|
|
5831
|
+
"gitpod",
|
|
5832
|
+
["workspace", "get", workspaceId, "--output", "json"],
|
|
5833
|
+
{ maxBuffer: MAX_BUFFER2 }
|
|
5834
|
+
);
|
|
5835
|
+
const status2 = JSON.parse(stdout).status?.toLowerCase() ?? "";
|
|
5836
|
+
if (status2 === "running" || status2 === "available") return;
|
|
5837
|
+
if (status2 === "failed" || status2 === "stopped") {
|
|
5838
|
+
throw new Error(`Gitpod workspace state: ${status2}.`);
|
|
5839
|
+
}
|
|
5840
|
+
} catch {
|
|
5841
|
+
}
|
|
5842
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
5843
|
+
}
|
|
5844
|
+
throw new Error("Gitpod workspace did not become Running within 5 minutes.");
|
|
5845
|
+
}
|
|
5846
|
+
async listMachineTypes(_projectId) {
|
|
5847
|
+
try {
|
|
5848
|
+
const { stdout } = await execFileP3(
|
|
5849
|
+
"gitpod",
|
|
5850
|
+
["organization", "list-classes", "--output", "json"],
|
|
5851
|
+
{ maxBuffer: MAX_BUFFER2 }
|
|
5852
|
+
);
|
|
5853
|
+
const list = JSON.parse(stdout);
|
|
5854
|
+
return list.map((c2) => ({
|
|
5855
|
+
id: c2.id,
|
|
5856
|
+
label: c2.displayName ?? c2.id,
|
|
5857
|
+
memoryGb: 8
|
|
5858
|
+
}));
|
|
5859
|
+
} catch {
|
|
5860
|
+
return [];
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
async listExistingWorkspaces(projectId) {
|
|
5864
|
+
try {
|
|
5865
|
+
const args2 = ["workspace", "list", "--output", "json", "--limit", "200"];
|
|
5866
|
+
const { stdout } = await execFileP3("gitpod", args2, { maxBuffer: MAX_BUFFER2 });
|
|
5867
|
+
const list = JSON.parse(stdout);
|
|
5868
|
+
return list.filter((w3) => !projectId || w3.contextUrl === projectId).map((w3) => ({
|
|
5869
|
+
id: w3.id,
|
|
5870
|
+
displayName: w3.id,
|
|
5871
|
+
webUrl: `https://gitpod.io/start/#${w3.id}`,
|
|
5872
|
+
state: w3.status ?? "Unknown",
|
|
5873
|
+
lastUsedAt: w3.lastActivity
|
|
5874
|
+
}));
|
|
5875
|
+
} catch {
|
|
5876
|
+
return [];
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
async startWorkspace(workspaceId) {
|
|
5880
|
+
try {
|
|
5881
|
+
await execFileP3(
|
|
5882
|
+
"gitpod",
|
|
5883
|
+
["workspace", "start", workspaceId],
|
|
5884
|
+
{ maxBuffer: MAX_BUFFER2, timeout: 6e4 }
|
|
5885
|
+
);
|
|
5886
|
+
} catch {
|
|
5887
|
+
}
|
|
5888
|
+
await this.waitUntilRunning(workspaceId);
|
|
5889
|
+
return {
|
|
5890
|
+
id: workspaceId,
|
|
5891
|
+
displayName: workspaceId,
|
|
5892
|
+
webUrl: `https://gitpod.io/start/#${workspaceId}`
|
|
5893
|
+
};
|
|
5894
|
+
}
|
|
5895
|
+
async exec(workspaceId, command2) {
|
|
5896
|
+
try {
|
|
5897
|
+
const { stdout, stderr } = await execFileP3(
|
|
5898
|
+
"gitpod",
|
|
5899
|
+
["workspace", "ssh", workspaceId, "--", command2],
|
|
5900
|
+
{ maxBuffer: MAX_BUFFER2, timeout: 6e5 }
|
|
5901
|
+
);
|
|
5902
|
+
return { stdout, stderr, code: 0 };
|
|
5903
|
+
} catch (err) {
|
|
5904
|
+
const e = err;
|
|
5905
|
+
return {
|
|
5906
|
+
stdout: e.stdout ?? "",
|
|
5907
|
+
stderr: e.stderr ?? e.message ?? "gitpod workspace ssh failed",
|
|
5908
|
+
code: typeof e.code === "number" ? e.code : 1
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
async streamCommand(workspaceId, command2) {
|
|
5913
|
+
resetStdinForChild2();
|
|
5914
|
+
return new Promise((resolve2, reject) => {
|
|
5915
|
+
const proc = (0, import_child_process6.spawn)(
|
|
5916
|
+
"gitpod",
|
|
5917
|
+
["workspace", "ssh", workspaceId, "--", "-tt", command2],
|
|
5918
|
+
{ stdio: "inherit" }
|
|
5919
|
+
);
|
|
5920
|
+
proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
|
|
5921
|
+
proc.on("error", reject);
|
|
5922
|
+
});
|
|
5923
|
+
}
|
|
5924
|
+
async uploadDirectory(workspaceId, localDir, remoteDir, options = {}) {
|
|
5925
|
+
const tarArgs = ["-czf", "-", "-C", localDir];
|
|
5926
|
+
for (const pattern of options.exclude ?? []) {
|
|
5927
|
+
tarArgs.push(`--exclude=${pattern}`);
|
|
5928
|
+
const stripped = pattern.replace(/^\.\/+/, "");
|
|
5929
|
+
if (stripped !== pattern) tarArgs.push(`--exclude=${stripped}`);
|
|
5930
|
+
}
|
|
5931
|
+
tarArgs.push(".");
|
|
5932
|
+
const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
|
|
5933
|
+
const remoteCmd = `mkdir -p ${shellQuote2(remoteDir)} && tar -xzf - -C ${shellQuote2(remoteDir)}`;
|
|
5934
|
+
await new Promise((resolve2, reject) => {
|
|
5935
|
+
const tar = (0, import_child_process6.spawn)("tar", tarArgs, {
|
|
5936
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5937
|
+
env: tarEnv
|
|
5938
|
+
});
|
|
5939
|
+
const ssh = (0, import_child_process6.spawn)(
|
|
5940
|
+
"gitpod",
|
|
5941
|
+
["workspace", "ssh", workspaceId, "--", remoteCmd],
|
|
5942
|
+
{ stdio: [tar.stdout, "pipe", "pipe"] }
|
|
5943
|
+
);
|
|
5944
|
+
let tarErr = "";
|
|
5945
|
+
let sshErr = "";
|
|
5946
|
+
tar.stderr?.on("data", (d3) => {
|
|
5947
|
+
tarErr += d3.toString();
|
|
5948
|
+
});
|
|
5949
|
+
ssh.stderr?.on("data", (d3) => {
|
|
5950
|
+
sshErr += d3.toString();
|
|
5951
|
+
});
|
|
5952
|
+
tar.on("error", reject);
|
|
5953
|
+
ssh.on("error", reject);
|
|
5954
|
+
ssh.on("exit", (code) => {
|
|
5955
|
+
if (code === 0) resolve2();
|
|
5956
|
+
else reject(new Error(`Remote tar failed: ${(sshErr || tarErr || `exit ${code}`).trim().slice(0, 500)}`));
|
|
5957
|
+
});
|
|
5958
|
+
});
|
|
5959
|
+
}
|
|
5960
|
+
async uploadFile(workspaceId, remotePath, contents, options = {}) {
|
|
5961
|
+
const remoteDir = path9.posix.dirname(remotePath);
|
|
5962
|
+
const parts = [
|
|
5963
|
+
`mkdir -p ${shellQuote2(remoteDir)}`,
|
|
5964
|
+
`cat > ${shellQuote2(remotePath)}`
|
|
5965
|
+
];
|
|
5966
|
+
if (options.mode != null) {
|
|
5967
|
+
parts.push(`chmod ${options.mode.toString(8)} ${shellQuote2(remotePath)}`);
|
|
5968
|
+
}
|
|
5969
|
+
const cmd = parts.join(" && ");
|
|
5970
|
+
await new Promise((resolve2, reject) => {
|
|
5971
|
+
const proc = (0, import_child_process6.spawn)(
|
|
5972
|
+
"gitpod",
|
|
5973
|
+
["workspace", "ssh", workspaceId, "--", cmd],
|
|
5974
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
5975
|
+
);
|
|
5976
|
+
let stderr = "";
|
|
5977
|
+
proc.stderr?.on("data", (d3) => {
|
|
5978
|
+
stderr += d3.toString();
|
|
5979
|
+
});
|
|
5980
|
+
proc.on("error", reject);
|
|
5981
|
+
proc.on("exit", (code) => {
|
|
5982
|
+
if (code === 0) resolve2();
|
|
5983
|
+
else reject(new Error(`Remote write failed: ${(stderr || `exit ${code}`).trim().slice(0, 500)}`));
|
|
5984
|
+
});
|
|
5985
|
+
proc.stdin?.write(contents);
|
|
5986
|
+
proc.stdin?.end();
|
|
5987
|
+
});
|
|
5988
|
+
}
|
|
5989
|
+
};
|
|
5990
|
+
function shellQuote2(s) {
|
|
5991
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5994
|
+
// src/services/providers/gitlab-workspaces.ts
|
|
5995
|
+
var import_child_process7 = require("child_process");
|
|
5996
|
+
var import_util4 = require("util");
|
|
5997
|
+
var path10 = __toESM(require("path"));
|
|
5998
|
+
var execFileP4 = (0, import_util4.promisify)(import_child_process7.execFile);
|
|
5999
|
+
var MAX_BUFFER3 = 8 * 1024 * 1024;
|
|
6000
|
+
var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
|
|
6001
|
+
function resetStdinForChild3() {
|
|
6002
|
+
if (process.stdin.isTTY) {
|
|
6003
|
+
try {
|
|
6004
|
+
process.stdin.setRawMode(false);
|
|
6005
|
+
} catch {
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
var GitLabWorkspacesProvider = class {
|
|
6010
|
+
id = "gitlab-workspaces";
|
|
6011
|
+
displayName = "GitLab Workspaces";
|
|
6012
|
+
tagline = "Self-hosted dev environments on your K8s cluster";
|
|
6013
|
+
available = true;
|
|
6014
|
+
async authorize() {
|
|
6015
|
+
try {
|
|
6016
|
+
await execFileP4("glab", ["--version"], { maxBuffer: MAX_BUFFER3 });
|
|
6017
|
+
} catch {
|
|
6018
|
+
throw new Error(
|
|
6019
|
+
[
|
|
6020
|
+
"GitLab CLI (`glab`) is required for GitLab Workspaces deploys.",
|
|
6021
|
+
"Install it with one of:",
|
|
6022
|
+
" \u2022 macOS: brew install glab",
|
|
6023
|
+
" \u2022 Linux: https://gitlab.com/gitlab-org/cli#installation",
|
|
6024
|
+
" \u2022 Windows: winget install GitLab.glab",
|
|
6025
|
+
"Then run `glab auth login` and try `codeam deploy` again."
|
|
6026
|
+
].join("\n")
|
|
6027
|
+
);
|
|
6028
|
+
}
|
|
6029
|
+
try {
|
|
6030
|
+
await execFileP4("glab", ["auth", "status"], { maxBuffer: MAX_BUFFER3 });
|
|
6031
|
+
return;
|
|
6032
|
+
} catch {
|
|
6033
|
+
}
|
|
6034
|
+
wt(
|
|
6035
|
+
"A token / OAuth flow will open below. After approval the deploy resumes.",
|
|
6036
|
+
"Authenticating GitLab"
|
|
6037
|
+
);
|
|
6038
|
+
resetStdinForChild3();
|
|
6039
|
+
await new Promise((resolve2, reject) => {
|
|
6040
|
+
const proc = (0, import_child_process7.spawn)(
|
|
6041
|
+
"glab",
|
|
6042
|
+
["auth", "login", "--scopes", "api,read_user,read_repository"],
|
|
6043
|
+
{ stdio: "inherit" }
|
|
6044
|
+
);
|
|
6045
|
+
proc.on("exit", (code) => {
|
|
6046
|
+
if (code === 0) resolve2();
|
|
6047
|
+
else reject(new Error("glab auth login failed."));
|
|
6048
|
+
});
|
|
6049
|
+
proc.on("error", reject);
|
|
6050
|
+
});
|
|
6051
|
+
}
|
|
6052
|
+
async listProjects() {
|
|
6053
|
+
try {
|
|
6054
|
+
const { stdout } = await execFileP4(
|
|
6055
|
+
"glab",
|
|
6056
|
+
["repo", "list", "--per-page", "200", "--output", "json"],
|
|
6057
|
+
{ maxBuffer: MAX_BUFFER3 }
|
|
6058
|
+
);
|
|
6059
|
+
const list = JSON.parse(stdout);
|
|
6060
|
+
return list.filter((r) => r.path_with_namespace).map((r) => ({
|
|
6061
|
+
id: r.path_with_namespace,
|
|
6062
|
+
name: r.name ?? r.path_with_namespace,
|
|
6063
|
+
fullName: r.path_with_namespace,
|
|
6064
|
+
description: r.description ?? void 0,
|
|
6065
|
+
defaultBranch: r.default_branch,
|
|
6066
|
+
private: r.visibility !== "public"
|
|
6067
|
+
}));
|
|
6068
|
+
} catch {
|
|
6069
|
+
return [];
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
/**
|
|
6073
|
+
* GitLab.com's machine sizes are tied to whatever the agent's
|
|
6074
|
+
* K8s cluster offers — this is project / agent specific and the
|
|
6075
|
+
* GraphQL API doesn't enumerate "available classes" today. We
|
|
6076
|
+
* return an empty list, which makes the orchestrator skip the
|
|
6077
|
+
* picker. Users with multiple sizes wired to their agent should
|
|
6078
|
+
* change the default in their devfile.yaml.
|
|
6079
|
+
*/
|
|
6080
|
+
async listMachineTypes(_projectId) {
|
|
6081
|
+
return [];
|
|
6082
|
+
}
|
|
6083
|
+
async createWorkspace(projectId, _machineTypeId) {
|
|
6084
|
+
const token = await this.getGlabToken();
|
|
6085
|
+
if (!token) {
|
|
6086
|
+
throw new Error(
|
|
6087
|
+
"Could not extract GitLab token from `glab`. Run `glab auth login` and retry."
|
|
6088
|
+
);
|
|
6089
|
+
}
|
|
6090
|
+
const projectFullPath = projectId;
|
|
6091
|
+
const mutation = `mutation Create($p: WorkspaceCreateInput!) {
|
|
6092
|
+
workspaceCreate(input: $p) {
|
|
6093
|
+
workspace { id name desiredState actualState url }
|
|
6094
|
+
errors
|
|
6095
|
+
}
|
|
6096
|
+
}`;
|
|
6097
|
+
const body = JSON.stringify({
|
|
6098
|
+
query: mutation,
|
|
6099
|
+
variables: {
|
|
6100
|
+
p: {
|
|
6101
|
+
projectId: `gid://gitlab/Project/${projectFullPath}`,
|
|
6102
|
+
editor: "webide",
|
|
6103
|
+
desiredState: "RUNNING",
|
|
6104
|
+
// The devfile MUST exist in the repo at .devfile.yaml.
|
|
6105
|
+
devfilePath: ".devfile.yaml"
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
});
|
|
6109
|
+
const data = await this.gql(token, body);
|
|
6110
|
+
const ws = data.data?.workspaceCreate?.workspace;
|
|
6111
|
+
const errs = data.data?.workspaceCreate?.errors ?? data.errors?.map((e) => e.message ?? "") ?? [];
|
|
6112
|
+
if (!ws?.id || errs.length > 0) {
|
|
6113
|
+
throw new Error(
|
|
6114
|
+
`GitLab Workspaces createWorkspace failed: ${errs.join("; ") || "no workspace returned"}.
|
|
6115
|
+
Common causes: project has no .devfile.yaml; no agent registered; user lacks permission.
|
|
6116
|
+
Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
|
|
6117
|
+
);
|
|
6118
|
+
}
|
|
6119
|
+
await this.waitUntilRunning(token, ws.id);
|
|
6120
|
+
return {
|
|
6121
|
+
id: ws.id,
|
|
6122
|
+
displayName: ws.name ?? ws.id,
|
|
6123
|
+
webUrl: ws.url
|
|
6124
|
+
};
|
|
6125
|
+
}
|
|
6126
|
+
async waitUntilRunning(token, workspaceId) {
|
|
6127
|
+
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
6128
|
+
const query = `query Get($id: WorkspaceID!) {
|
|
6129
|
+
workspace(id: $id) { actualState }
|
|
6130
|
+
}`;
|
|
6131
|
+
while (Date.now() < deadline) {
|
|
6132
|
+
try {
|
|
6133
|
+
const data = await this.gql(
|
|
6134
|
+
token,
|
|
6135
|
+
JSON.stringify({ query, variables: { id: workspaceId } })
|
|
6136
|
+
);
|
|
6137
|
+
const state = data.data?.workspace?.actualState?.toUpperCase() ?? "";
|
|
6138
|
+
if (state === "RUNNING") return;
|
|
6139
|
+
if (state === "FAILED" || state === "STOPPED") {
|
|
6140
|
+
throw new Error(`Workspace state: ${state}.`);
|
|
6141
|
+
}
|
|
6142
|
+
} catch {
|
|
6143
|
+
}
|
|
6144
|
+
await new Promise((r) => setTimeout(r, 4e3));
|
|
6145
|
+
}
|
|
6146
|
+
throw new Error("GitLab workspace did not become Running within 5 minutes.");
|
|
6147
|
+
}
|
|
6148
|
+
async listExistingWorkspaces(_projectId) {
|
|
6149
|
+
const token = await this.getGlabToken();
|
|
6150
|
+
if (!token) return [];
|
|
6151
|
+
const query = `query { currentUser { workspaces { nodes {
|
|
6152
|
+
id name actualState url updatedAt
|
|
6153
|
+
} } } }`;
|
|
6154
|
+
try {
|
|
6155
|
+
const data = await this.gql(token, JSON.stringify({ query }));
|
|
6156
|
+
const nodes = data.data?.currentUser?.workspaces?.nodes ?? [];
|
|
6157
|
+
return nodes.map((n) => ({
|
|
6158
|
+
id: n.id,
|
|
6159
|
+
displayName: n.name ?? n.id,
|
|
6160
|
+
webUrl: n.url,
|
|
6161
|
+
state: n.actualState,
|
|
6162
|
+
lastUsedAt: n.updatedAt
|
|
6163
|
+
}));
|
|
6164
|
+
} catch {
|
|
6165
|
+
return [];
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
async startWorkspace(workspaceId) {
|
|
6169
|
+
const token = await this.getGlabToken();
|
|
6170
|
+
if (!token) throw new Error("Not authenticated with GitLab.");
|
|
6171
|
+
const mutation = `mutation Start($id: WorkspaceID!) {
|
|
6172
|
+
workspaceUpdate(input: { id: $id, desiredState: RUNNING }) {
|
|
6173
|
+
workspace { id name url } errors
|
|
6174
|
+
}
|
|
6175
|
+
}`;
|
|
6176
|
+
await this.gql(
|
|
6177
|
+
token,
|
|
6178
|
+
JSON.stringify({ query: mutation, variables: { id: workspaceId } })
|
|
6179
|
+
);
|
|
6180
|
+
await this.waitUntilRunning(token, workspaceId);
|
|
6181
|
+
return { id: workspaceId, displayName: workspaceId };
|
|
6182
|
+
}
|
|
6183
|
+
async exec(workspaceId, command2) {
|
|
6184
|
+
const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
|
|
6185
|
+
try {
|
|
6186
|
+
const { stdout, stderr } = await execFileP4(
|
|
6187
|
+
"ssh",
|
|
6188
|
+
[
|
|
6189
|
+
"-o",
|
|
6190
|
+
"StrictHostKeyChecking=accept-new",
|
|
6191
|
+
"-o",
|
|
6192
|
+
"BatchMode=yes",
|
|
6193
|
+
`${workspaceId}@${sshHost}`,
|
|
6194
|
+
command2
|
|
6195
|
+
],
|
|
6196
|
+
{ maxBuffer: MAX_BUFFER3, timeout: 6e5 }
|
|
6197
|
+
);
|
|
6198
|
+
return { stdout, stderr, code: 0 };
|
|
6199
|
+
} catch (err) {
|
|
6200
|
+
const e = err;
|
|
6201
|
+
return {
|
|
6202
|
+
stdout: e.stdout ?? "",
|
|
6203
|
+
stderr: e.stderr ?? e.message ?? "ssh to GitLab workspace failed",
|
|
6204
|
+
code: typeof e.code === "number" ? e.code : 1
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
async streamCommand(workspaceId, command2) {
|
|
6209
|
+
const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
|
|
6210
|
+
resetStdinForChild3();
|
|
6211
|
+
return new Promise((resolve2, reject) => {
|
|
6212
|
+
const proc = (0, import_child_process7.spawn)(
|
|
6213
|
+
"ssh",
|
|
6214
|
+
["-tt", "-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, command2],
|
|
6215
|
+
{ stdio: "inherit" }
|
|
6216
|
+
);
|
|
6217
|
+
proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
|
|
6218
|
+
proc.on("error", reject);
|
|
6219
|
+
});
|
|
6220
|
+
}
|
|
6221
|
+
async uploadDirectory(workspaceId, localDir, remoteDir, options = {}) {
|
|
6222
|
+
const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
|
|
6223
|
+
const tarArgs = ["-czf", "-", "-C", localDir];
|
|
6224
|
+
for (const pattern of options.exclude ?? []) {
|
|
6225
|
+
tarArgs.push(`--exclude=${pattern}`);
|
|
6226
|
+
const stripped = pattern.replace(/^\.\/+/, "");
|
|
6227
|
+
if (stripped !== pattern) tarArgs.push(`--exclude=${stripped}`);
|
|
6228
|
+
}
|
|
6229
|
+
tarArgs.push(".");
|
|
6230
|
+
const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
|
|
6231
|
+
const remoteCmd = `mkdir -p ${shellQuote3(remoteDir)} && tar -xzf - -C ${shellQuote3(remoteDir)}`;
|
|
6232
|
+
await new Promise((resolve2, reject) => {
|
|
6233
|
+
const tar = (0, import_child_process7.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
|
|
6234
|
+
const ssh = (0, import_child_process7.spawn)(
|
|
6235
|
+
"ssh",
|
|
6236
|
+
["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, remoteCmd],
|
|
6237
|
+
{ stdio: [tar.stdout, "pipe", "pipe"] }
|
|
6238
|
+
);
|
|
6239
|
+
let tarErr = "";
|
|
6240
|
+
let sshErr = "";
|
|
6241
|
+
tar.stderr?.on("data", (d3) => {
|
|
6242
|
+
tarErr += d3.toString();
|
|
6243
|
+
});
|
|
6244
|
+
ssh.stderr?.on("data", (d3) => {
|
|
6245
|
+
sshErr += d3.toString();
|
|
6246
|
+
});
|
|
6247
|
+
tar.on("error", reject);
|
|
6248
|
+
ssh.on("error", reject);
|
|
6249
|
+
ssh.on("exit", (code) => {
|
|
6250
|
+
if (code === 0) resolve2();
|
|
6251
|
+
else reject(new Error(`Remote tar failed: ${(sshErr || tarErr || `exit ${code}`).trim().slice(0, 500)}`));
|
|
6252
|
+
});
|
|
6253
|
+
});
|
|
6254
|
+
}
|
|
6255
|
+
async uploadFile(workspaceId, remotePath, contents, options = {}) {
|
|
6256
|
+
const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
|
|
6257
|
+
const remoteDir = path10.posix.dirname(remotePath);
|
|
6258
|
+
const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
|
|
6259
|
+
if (options.mode != null) {
|
|
6260
|
+
parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
|
|
6261
|
+
}
|
|
6262
|
+
const cmd = parts.join(" && ");
|
|
6263
|
+
await new Promise((resolve2, reject) => {
|
|
6264
|
+
const proc = (0, import_child_process7.spawn)(
|
|
6265
|
+
"ssh",
|
|
6266
|
+
["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, cmd],
|
|
6267
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
6268
|
+
);
|
|
6269
|
+
let stderr = "";
|
|
6270
|
+
proc.stderr?.on("data", (d3) => {
|
|
6271
|
+
stderr += d3.toString();
|
|
6272
|
+
});
|
|
6273
|
+
proc.on("error", reject);
|
|
6274
|
+
proc.on("exit", (code) => {
|
|
6275
|
+
if (code === 0) resolve2();
|
|
6276
|
+
else reject(new Error(`Remote write failed: ${(stderr || `exit ${code}`).trim().slice(0, 500)}`));
|
|
6277
|
+
});
|
|
6278
|
+
proc.stdin?.write(contents);
|
|
6279
|
+
proc.stdin?.end();
|
|
6280
|
+
});
|
|
6281
|
+
}
|
|
6282
|
+
/**
|
|
6283
|
+
* Pull the user's `glab` token via `glab auth status -t`, since
|
|
6284
|
+
* `glab` stores it in its own config and we need it to call the
|
|
6285
|
+
* GraphQL API directly.
|
|
6286
|
+
*/
|
|
6287
|
+
async getGlabToken() {
|
|
6288
|
+
try {
|
|
6289
|
+
const { stdout, stderr } = await execFileP4(
|
|
6290
|
+
"glab",
|
|
6291
|
+
["auth", "status", "--show-token"],
|
|
6292
|
+
{ maxBuffer: MAX_BUFFER3 }
|
|
6293
|
+
);
|
|
6294
|
+
const haystack = stdout + "\n" + stderr;
|
|
6295
|
+
const m = haystack.match(/Token:\s+(\S+)/);
|
|
6296
|
+
return m?.[1] ?? null;
|
|
6297
|
+
} catch {
|
|
6298
|
+
return null;
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
async gql(token, body) {
|
|
6302
|
+
const url = `${GITLAB_API_BASE.replace(/\/$/, "").replace(/\/v4$/, "")}/api/graphql`;
|
|
6303
|
+
const res = await fetch(url, {
|
|
6304
|
+
method: "POST",
|
|
6305
|
+
headers: {
|
|
6306
|
+
"Content-Type": "application/json",
|
|
6307
|
+
Authorization: `Bearer ${token}`
|
|
6308
|
+
},
|
|
6309
|
+
body
|
|
6310
|
+
});
|
|
6311
|
+
if (!res.ok) {
|
|
6312
|
+
const text = await res.text();
|
|
6313
|
+
throw new Error(`GitLab GraphQL ${res.status}: ${text.slice(0, 400)}`);
|
|
6314
|
+
}
|
|
6315
|
+
return await res.json();
|
|
6316
|
+
}
|
|
6317
|
+
};
|
|
6318
|
+
function shellQuote3(s) {
|
|
6319
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6322
|
+
// src/services/providers/railway.ts
|
|
6323
|
+
var import_child_process8 = require("child_process");
|
|
6324
|
+
var import_util5 = require("util");
|
|
6325
|
+
var path11 = __toESM(require("path"));
|
|
6326
|
+
var execFileP5 = (0, import_util5.promisify)(import_child_process8.execFile);
|
|
6327
|
+
var MAX_BUFFER4 = 8 * 1024 * 1024;
|
|
6328
|
+
function resetStdinForChild4() {
|
|
6329
|
+
if (process.stdin.isTTY) {
|
|
6330
|
+
try {
|
|
6331
|
+
process.stdin.setRawMode(false);
|
|
6332
|
+
} catch {
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
var RailwayProvider = class {
|
|
6337
|
+
id = "railway";
|
|
6338
|
+
displayName = "Railway";
|
|
6339
|
+
tagline = "Always-on container \u2014 no IDE, just an agent terminal";
|
|
6340
|
+
available = true;
|
|
6341
|
+
async authorize() {
|
|
6342
|
+
try {
|
|
6343
|
+
await execFileP5("railway", ["--version"], { maxBuffer: MAX_BUFFER4 });
|
|
6344
|
+
} catch {
|
|
6345
|
+
throw new Error(
|
|
6346
|
+
[
|
|
6347
|
+
"Railway CLI (`railway`) is required for Railway deploys.",
|
|
6348
|
+
"Install it with one of:",
|
|
6349
|
+
" \u2022 npm: npm install -g @railway/cli",
|
|
6350
|
+
" \u2022 macOS: brew install railway",
|
|
6351
|
+
" \u2022 Linux: https://docs.railway.app/develop/cli#install",
|
|
6352
|
+
"Then run `railway login` and try `codeam deploy` again."
|
|
6353
|
+
].join("\n")
|
|
6354
|
+
);
|
|
6355
|
+
}
|
|
6356
|
+
try {
|
|
6357
|
+
await execFileP5("railway", ["whoami"], { maxBuffer: MAX_BUFFER4 });
|
|
6358
|
+
return;
|
|
6359
|
+
} catch {
|
|
6360
|
+
}
|
|
6361
|
+
wt(
|
|
6362
|
+
"A login URL prints below. Open it in your browser and approve.",
|
|
6363
|
+
"Authenticating Railway"
|
|
6364
|
+
);
|
|
6365
|
+
resetStdinForChild4();
|
|
6366
|
+
await new Promise((resolve2, reject) => {
|
|
6367
|
+
const proc = (0, import_child_process8.spawn)("railway", ["login"], { stdio: "inherit" });
|
|
6368
|
+
proc.on("exit", (code) => {
|
|
6369
|
+
if (code === 0) resolve2();
|
|
6370
|
+
else reject(new Error("railway login failed."));
|
|
6371
|
+
});
|
|
6372
|
+
proc.on("error", reject);
|
|
6373
|
+
});
|
|
6374
|
+
}
|
|
6375
|
+
async listProjects() {
|
|
6376
|
+
try {
|
|
6377
|
+
const { stdout } = await execFileP5(
|
|
6378
|
+
"railway",
|
|
6379
|
+
["list", "--json"],
|
|
6380
|
+
{ maxBuffer: MAX_BUFFER4 }
|
|
6381
|
+
);
|
|
6382
|
+
const list = JSON.parse(stdout);
|
|
6383
|
+
return list.filter((r) => r.id && r.name).map((r) => ({
|
|
6384
|
+
id: r.id,
|
|
6385
|
+
name: r.name,
|
|
6386
|
+
fullName: r.name,
|
|
6387
|
+
description: r.description ?? void 0,
|
|
6388
|
+
private: true
|
|
6389
|
+
// Railway projects are private by default
|
|
6390
|
+
}));
|
|
6391
|
+
} catch {
|
|
6392
|
+
try {
|
|
6393
|
+
const { stdout } = await execFileP5("railway", ["list"], { maxBuffer: MAX_BUFFER4 });
|
|
6394
|
+
const projects = [];
|
|
6395
|
+
for (const line of stdout.split("\n")) {
|
|
6396
|
+
const trimmed = line.trim();
|
|
6397
|
+
if (!trimmed || trimmed.startsWith("Project")) continue;
|
|
6398
|
+
const idMatch = trimmed.match(/id:\s*(\S+)/);
|
|
6399
|
+
const nameMatch = trimmed.match(/name:\s*([^|]+)/);
|
|
6400
|
+
if (idMatch && nameMatch) {
|
|
6401
|
+
const name = nameMatch[1].trim();
|
|
6402
|
+
projects.push({
|
|
6403
|
+
id: idMatch[1],
|
|
6404
|
+
name,
|
|
6405
|
+
fullName: name,
|
|
6406
|
+
private: true
|
|
6407
|
+
});
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
return projects;
|
|
6411
|
+
} catch {
|
|
6412
|
+
return [];
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
}
|
|
6416
|
+
/**
|
|
6417
|
+
* Railway exposes resource sizes (CPU / RAM) at the SERVICE level,
|
|
6418
|
+
* not the project level — and only on paid plans. The CLI has no
|
|
6419
|
+
* `list-classes` equivalent today. Returning empty makes the
|
|
6420
|
+
* orchestrator skip the picker; users on paid plans can resize
|
|
6421
|
+
* a service from the Railway dashboard after deploy.
|
|
6422
|
+
*/
|
|
6423
|
+
async listMachineTypes(_projectId) {
|
|
6424
|
+
return [];
|
|
6425
|
+
}
|
|
6426
|
+
async createWorkspace(_projectId, _machineTypeId) {
|
|
6427
|
+
wt(
|
|
6428
|
+
[
|
|
6429
|
+
"Railway service auto-creation from `codeam deploy` isn't implemented yet.",
|
|
6430
|
+
"Workaround for now:",
|
|
6431
|
+
" 1. From your repo: railway link (pick this project)",
|
|
6432
|
+
" 2. Run: railway up --detach",
|
|
6433
|
+
" 3. Re-run codeam deploy and pick the existing service."
|
|
6434
|
+
].join("\n"),
|
|
6435
|
+
"Heads up \u2014 manual step needed"
|
|
6436
|
+
);
|
|
6437
|
+
throw new Error(
|
|
6438
|
+
"Railway provider needs an existing service to attach to. See the note above."
|
|
6439
|
+
);
|
|
6440
|
+
}
|
|
6441
|
+
async listExistingWorkspaces(projectId) {
|
|
6442
|
+
if (!projectId) return [];
|
|
6443
|
+
try {
|
|
6444
|
+
const { stdout } = await execFileP5(
|
|
6445
|
+
"railway",
|
|
6446
|
+
["service", "list", "--project", projectId, "--json"],
|
|
6447
|
+
{ maxBuffer: MAX_BUFFER4 }
|
|
6448
|
+
);
|
|
6449
|
+
const list = JSON.parse(stdout);
|
|
6450
|
+
return list.filter((s) => s.id && s.name).map((s) => {
|
|
6451
|
+
const latest = s.deployments?.[0];
|
|
6452
|
+
return {
|
|
6453
|
+
id: `${projectId}/${s.id}`,
|
|
6454
|
+
displayName: s.name,
|
|
6455
|
+
state: latest?.status ?? "Unknown",
|
|
6456
|
+
lastUsedAt: latest?.updatedAt
|
|
6457
|
+
};
|
|
6458
|
+
});
|
|
6459
|
+
} catch {
|
|
6460
|
+
return [];
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
async startWorkspace(workspaceId) {
|
|
6464
|
+
const [projectId, serviceId] = workspaceId.split("/");
|
|
6465
|
+
if (!projectId || !serviceId) {
|
|
6466
|
+
throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
|
|
6467
|
+
}
|
|
6468
|
+
try {
|
|
6469
|
+
await execFileP5(
|
|
6470
|
+
"railway",
|
|
6471
|
+
["service", "restart", "--service", serviceId, "--project", projectId],
|
|
6472
|
+
{ maxBuffer: MAX_BUFFER4, timeout: 6e4 }
|
|
6473
|
+
);
|
|
6474
|
+
} catch {
|
|
6475
|
+
}
|
|
6476
|
+
return { id: workspaceId, displayName: serviceId };
|
|
6477
|
+
}
|
|
6478
|
+
async exec(workspaceId, command2) {
|
|
6479
|
+
const [projectId, serviceId] = workspaceId.split("/");
|
|
6480
|
+
if (!projectId || !serviceId) {
|
|
6481
|
+
return {
|
|
6482
|
+
stdout: "",
|
|
6483
|
+
stderr: "Invalid Railway workspace id (expected projectId/serviceId).",
|
|
6484
|
+
code: 1
|
|
6485
|
+
};
|
|
6486
|
+
}
|
|
6487
|
+
try {
|
|
6488
|
+
const { stdout, stderr } = await execFileP5(
|
|
6489
|
+
"railway",
|
|
6490
|
+
["run", "--project", projectId, "--service", serviceId, "--", "bash", "-lc", command2],
|
|
6491
|
+
{ maxBuffer: MAX_BUFFER4, timeout: 6e5 }
|
|
6492
|
+
);
|
|
6493
|
+
return { stdout, stderr, code: 0 };
|
|
6494
|
+
} catch (err) {
|
|
6495
|
+
const e = err;
|
|
6496
|
+
return {
|
|
6497
|
+
stdout: e.stdout ?? "",
|
|
6498
|
+
stderr: e.stderr ?? e.message ?? "railway run failed",
|
|
6499
|
+
code: typeof e.code === "number" ? e.code : 1
|
|
6500
|
+
};
|
|
6501
|
+
}
|
|
6502
|
+
}
|
|
6503
|
+
async streamCommand(workspaceId, command2) {
|
|
6504
|
+
const [projectId, serviceId] = workspaceId.split("/");
|
|
6505
|
+
if (!projectId || !serviceId) {
|
|
6506
|
+
throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
|
|
6507
|
+
}
|
|
6508
|
+
resetStdinForChild4();
|
|
6509
|
+
return new Promise((resolve2, reject) => {
|
|
6510
|
+
const proc = (0, import_child_process8.spawn)(
|
|
6511
|
+
"railway",
|
|
6512
|
+
["shell", "--project", projectId, "--service", serviceId, "--command", command2],
|
|
6513
|
+
{ stdio: "inherit" }
|
|
6514
|
+
);
|
|
6515
|
+
proc.on("exit", (code) => resolve2({ code: code ?? 0 }));
|
|
6516
|
+
proc.on("error", reject);
|
|
6517
|
+
});
|
|
6518
|
+
}
|
|
6519
|
+
async uploadDirectory(workspaceId, localDir, remoteDir, options = {}) {
|
|
6520
|
+
const [projectId, serviceId] = workspaceId.split("/");
|
|
6521
|
+
if (!projectId || !serviceId) {
|
|
6522
|
+
throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
|
|
6523
|
+
}
|
|
6524
|
+
const tarArgs = ["-czf", "-", "-C", localDir];
|
|
6525
|
+
for (const pattern of options.exclude ?? []) {
|
|
6526
|
+
tarArgs.push(`--exclude=${pattern}`);
|
|
6527
|
+
const stripped = pattern.replace(/^\.\/+/, "");
|
|
6528
|
+
if (stripped !== pattern) tarArgs.push(`--exclude=${stripped}`);
|
|
6529
|
+
}
|
|
6530
|
+
tarArgs.push(".");
|
|
6531
|
+
const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
|
|
6532
|
+
const remoteCmd = `mkdir -p ${shellQuote4(remoteDir)} && tar -xzf - -C ${shellQuote4(remoteDir)}`;
|
|
6533
|
+
await new Promise((resolve2, reject) => {
|
|
6534
|
+
const tar = (0, import_child_process8.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
|
|
6535
|
+
const sh = (0, import_child_process8.spawn)(
|
|
6536
|
+
"railway",
|
|
6537
|
+
["shell", "--project", projectId, "--service", serviceId, "--command", remoteCmd],
|
|
6538
|
+
{ stdio: [tar.stdout, "pipe", "pipe"] }
|
|
6539
|
+
);
|
|
6540
|
+
let tarErr = "";
|
|
6541
|
+
let shErr = "";
|
|
6542
|
+
tar.stderr?.on("data", (d3) => {
|
|
6543
|
+
tarErr += d3.toString();
|
|
6544
|
+
});
|
|
6545
|
+
sh.stderr?.on("data", (d3) => {
|
|
6546
|
+
shErr += d3.toString();
|
|
6547
|
+
});
|
|
6548
|
+
tar.on("error", reject);
|
|
6549
|
+
sh.on("error", reject);
|
|
6550
|
+
sh.on("exit", (code) => {
|
|
6551
|
+
if (code === 0) resolve2();
|
|
6552
|
+
else reject(new Error(`Remote tar failed: ${(shErr || tarErr || `exit ${code}`).trim().slice(0, 500)}`));
|
|
6553
|
+
});
|
|
6554
|
+
});
|
|
6555
|
+
}
|
|
6556
|
+
async uploadFile(workspaceId, remotePath, contents, options = {}) {
|
|
6557
|
+
const [projectId, serviceId] = workspaceId.split("/");
|
|
6558
|
+
if (!projectId || !serviceId) {
|
|
6559
|
+
throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
|
|
6560
|
+
}
|
|
6561
|
+
const remoteDir = path11.posix.dirname(remotePath);
|
|
6562
|
+
const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
|
|
6563
|
+
if (options.mode != null) {
|
|
6564
|
+
parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
|
|
6565
|
+
}
|
|
6566
|
+
const cmd = parts.join(" && ");
|
|
6567
|
+
await new Promise((resolve2, reject) => {
|
|
6568
|
+
const proc = (0, import_child_process8.spawn)(
|
|
6569
|
+
"railway",
|
|
6570
|
+
["shell", "--project", projectId, "--service", serviceId, "--command", cmd],
|
|
6571
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
6572
|
+
);
|
|
6573
|
+
let stderr = "";
|
|
6574
|
+
proc.stderr?.on("data", (d3) => {
|
|
6575
|
+
stderr += d3.toString();
|
|
6576
|
+
});
|
|
6577
|
+
proc.on("error", reject);
|
|
6578
|
+
proc.on("exit", (code) => {
|
|
6579
|
+
if (code === 0) resolve2();
|
|
6580
|
+
else reject(new Error(`Remote write failed: ${(stderr || `exit ${code}`).trim().slice(0, 500)}`));
|
|
6581
|
+
});
|
|
6582
|
+
proc.stdin?.write(contents);
|
|
6583
|
+
proc.stdin?.end();
|
|
6584
|
+
});
|
|
6585
|
+
}
|
|
6586
|
+
};
|
|
6587
|
+
function shellQuote4(s) {
|
|
6588
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
6589
|
+
}
|
|
6590
|
+
|
|
5695
6591
|
// src/services/providers/index.ts
|
|
5696
6592
|
var PROVIDERS = [
|
|
5697
|
-
new GitHubCodespacesProvider()
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
// new GitLabWebIDEProvider(),
|
|
6593
|
+
new GitHubCodespacesProvider(),
|
|
6594
|
+
new GitpodProvider(),
|
|
6595
|
+
new GitLabWorkspacesProvider(),
|
|
6596
|
+
new RailwayProvider()
|
|
5702
6597
|
];
|
|
5703
6598
|
|
|
5704
6599
|
// src/commands/deploy.ts
|
|
5705
|
-
var
|
|
6600
|
+
var execFileP6 = (0, import_util6.promisify)(import_child_process9.execFile);
|
|
5706
6601
|
async function deploy() {
|
|
5707
6602
|
console.log();
|
|
5708
|
-
mt(
|
|
6603
|
+
mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ")));
|
|
5709
6604
|
const provider = await pickProvider();
|
|
5710
6605
|
if (!provider) {
|
|
5711
6606
|
pt("No provider selected.");
|
|
@@ -5742,7 +6637,7 @@ async function deploy() {
|
|
|
5742
6637
|
if (provider.expandListScopes) {
|
|
5743
6638
|
options.push({
|
|
5744
6639
|
value: EXPAND_SCOPES,
|
|
5745
|
-
label:
|
|
6640
|
+
label: import_picocolors9.default.cyan("+ Don't see your project? Expand scopes\u2026"),
|
|
5746
6641
|
hint: "Re-authorize with broader scopes (org / team repos)"
|
|
5747
6642
|
});
|
|
5748
6643
|
}
|
|
@@ -5790,7 +6685,7 @@ async function deploy() {
|
|
|
5790
6685
|
label: w3.displayName ?? w3.id,
|
|
5791
6686
|
hint: [w3.state, formatLastUsed(w3.lastUsedAt)].filter(Boolean).join(" \xB7 ")
|
|
5792
6687
|
})),
|
|
5793
|
-
{ value: "__new__", label:
|
|
6688
|
+
{ value: "__new__", label: import_picocolors9.default.green("+ Create a new workspace"), hint: "fresh codespace" }
|
|
5794
6689
|
]
|
|
5795
6690
|
});
|
|
5796
6691
|
if (q(choice)) {
|
|
@@ -5857,7 +6752,7 @@ async function deploy() {
|
|
|
5857
6752
|
process.exit(1);
|
|
5858
6753
|
}
|
|
5859
6754
|
}
|
|
5860
|
-
const localClaudeDir =
|
|
6755
|
+
const localClaudeDir = path12.join(os6.homedir(), ".claude");
|
|
5861
6756
|
const localCredsKind = await detectLocalClaudeCredentials(localClaudeDir);
|
|
5862
6757
|
let bridged = "none";
|
|
5863
6758
|
if (localCredsKind !== "none") {
|
|
@@ -5955,7 +6850,7 @@ async function deploy() {
|
|
|
5955
6850
|
}
|
|
5956
6851
|
}
|
|
5957
6852
|
if (bridged !== "none") {
|
|
5958
|
-
const localClaudeJson =
|
|
6853
|
+
const localClaudeJson = path12.join(os6.homedir(), ".claude.json");
|
|
5959
6854
|
if (fs8.existsSync(localClaudeJson)) {
|
|
5960
6855
|
try {
|
|
5961
6856
|
const contents = fs8.readFileSync(localClaudeJson);
|
|
@@ -5997,12 +6892,12 @@ async function deploy() {
|
|
|
5997
6892
|
cliStep.stop("\u2713 codeam-cli installed");
|
|
5998
6893
|
wt(
|
|
5999
6894
|
[
|
|
6000
|
-
`Workspace: ${
|
|
6001
|
-
workspace.webUrl ? `Web: ${
|
|
6895
|
+
`Workspace: ${import_picocolors9.default.cyan(workspace.displayName ?? workspace.id)}`,
|
|
6896
|
+
workspace.webUrl ? `Web: ${import_picocolors9.default.cyan(workspace.webUrl)}` : "",
|
|
6002
6897
|
"",
|
|
6003
6898
|
"Starting `codeam pair` on the workspace.",
|
|
6004
6899
|
"Scan the QR code below with the CodeAgent Mobile app to finish pairing.",
|
|
6005
|
-
|
|
6900
|
+
import_picocolors9.default.dim("(Once paired, this terminal disconnects automatically; the session stays alive on the codespace.)")
|
|
6006
6901
|
].filter(Boolean).join("\n"),
|
|
6007
6902
|
"Almost there"
|
|
6008
6903
|
);
|
|
@@ -6118,11 +7013,11 @@ async function deploy() {
|
|
|
6118
7013
|
].join("\n");
|
|
6119
7014
|
const code = (await provider.streamCommand(workspace.id, `bash -lc ${shellQuoteSingle(wrapper)}`)).code;
|
|
6120
7015
|
if (code === 0) {
|
|
6121
|
-
gt(
|
|
7016
|
+
gt(import_picocolors9.default.green("\u2713 Workspace deployed and paired. Drive from your phone, anywhere."));
|
|
6122
7017
|
} else if (code === 130) {
|
|
6123
|
-
gt(
|
|
7018
|
+
gt(import_picocolors9.default.yellow("Disconnected from local terminal. Mobile session keeps running on the codespace."));
|
|
6124
7019
|
} else {
|
|
6125
|
-
gt(
|
|
7020
|
+
gt(import_picocolors9.default.yellow('Pairing did not complete. Run "codeam pair" inside the codespace if needed.'));
|
|
6126
7021
|
}
|
|
6127
7022
|
}
|
|
6128
7023
|
function shellQuoteSingle(s) {
|
|
@@ -6148,12 +7043,12 @@ async function runRemoteClaudeLogin(provider, workspaceId) {
|
|
|
6148
7043
|
}
|
|
6149
7044
|
}
|
|
6150
7045
|
async function detectLocalClaudeCredentials(localClaudeDir) {
|
|
6151
|
-
if (fs8.existsSync(
|
|
7046
|
+
if (fs8.existsSync(path12.join(localClaudeDir, ".credentials.json"))) {
|
|
6152
7047
|
return "flat-file";
|
|
6153
7048
|
}
|
|
6154
7049
|
if (process.platform === "darwin") {
|
|
6155
7050
|
try {
|
|
6156
|
-
await
|
|
7051
|
+
await execFileP6(
|
|
6157
7052
|
"security",
|
|
6158
7053
|
["find-generic-password", "-s", "Claude Code-credentials"],
|
|
6159
7054
|
{ maxBuffer: 1024 * 1024 }
|
|
@@ -6181,11 +7076,11 @@ async function verifyClaudeAuth(provider, workspaceId) {
|
|
|
6181
7076
|
}
|
|
6182
7077
|
}
|
|
6183
7078
|
async function bridgeClaudeCredentials(provider, workspaceId, localClaudeDir) {
|
|
6184
|
-
const fileBased =
|
|
7079
|
+
const fileBased = path12.join(localClaudeDir, ".credentials.json");
|
|
6185
7080
|
if (fs8.existsSync(fileBased)) return "flat-file";
|
|
6186
7081
|
if (process.platform === "darwin") {
|
|
6187
7082
|
try {
|
|
6188
|
-
const { stdout } = await
|
|
7083
|
+
const { stdout } = await execFileP6(
|
|
6189
7084
|
"security",
|
|
6190
7085
|
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
6191
7086
|
{ maxBuffer: 1024 * 1024 }
|
|
@@ -6233,7 +7128,7 @@ async function pickProvider() {
|
|
|
6233
7128
|
message: "Where do you want to deploy?",
|
|
6234
7129
|
options: PROVIDERS.map((prov) => ({
|
|
6235
7130
|
value: prov.id,
|
|
6236
|
-
label: prov.available ? prov.displayName : `${prov.displayName} ${
|
|
7131
|
+
label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors9.default.dim("(coming soon)")}`,
|
|
6237
7132
|
hint: prov.tagline
|
|
6238
7133
|
}))
|
|
6239
7134
|
});
|
|
@@ -6250,27 +7145,27 @@ async function pickProvider() {
|
|
|
6250
7145
|
}
|
|
6251
7146
|
|
|
6252
7147
|
// src/commands/deploy-manage.ts
|
|
6253
|
-
var
|
|
7148
|
+
var import_picocolors10 = __toESM(require("picocolors"));
|
|
6254
7149
|
async function deployList() {
|
|
6255
7150
|
console.log();
|
|
6256
|
-
mt(
|
|
7151
|
+
mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy ls ")));
|
|
6257
7152
|
const workspaces = await collectWorkspacesWithStatus();
|
|
6258
7153
|
if (workspaces.length === 0) {
|
|
6259
|
-
gt(
|
|
7154
|
+
gt(import_picocolors10.default.dim("No deployed workspaces found."));
|
|
6260
7155
|
return;
|
|
6261
7156
|
}
|
|
6262
7157
|
for (const w3 of workspaces) {
|
|
6263
|
-
const tag = w3.codeamRunning ?
|
|
6264
|
-
console.log(` ${tag} ${
|
|
7158
|
+
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"}`);
|
|
7159
|
+
console.log(` ${tag} ${import_picocolors10.default.cyan(w3.displayName ?? w3.id)} ${import_picocolors10.default.dim("(" + w3.providerName + ")")}`);
|
|
6265
7160
|
}
|
|
6266
|
-
gt(
|
|
7161
|
+
gt(import_picocolors10.default.dim("Use `codeam deploy stop` to terminate a session."));
|
|
6267
7162
|
}
|
|
6268
7163
|
async function deployStop() {
|
|
6269
7164
|
console.log();
|
|
6270
|
-
mt(
|
|
7165
|
+
mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy stop ")));
|
|
6271
7166
|
const workspaces = await collectWorkspacesWithStatus();
|
|
6272
7167
|
if (workspaces.length === 0) {
|
|
6273
|
-
gt(
|
|
7168
|
+
gt(import_picocolors10.default.dim("No deployed workspaces found."));
|
|
6274
7169
|
return;
|
|
6275
7170
|
}
|
|
6276
7171
|
const choice = await _t({
|
|
@@ -6280,7 +7175,7 @@ async function deployStop() {
|
|
|
6280
7175
|
label: w3.displayName ?? w3.id,
|
|
6281
7176
|
hint: [
|
|
6282
7177
|
w3.providerName,
|
|
6283
|
-
w3.codeamRunning ?
|
|
7178
|
+
w3.codeamRunning ? import_picocolors10.default.green("\u25CF codeam-pair running") : import_picocolors10.default.dim("\u25CB no codeam-pair"),
|
|
6284
7179
|
w3.state ?? ""
|
|
6285
7180
|
].filter(Boolean).join(" \xB7 ")
|
|
6286
7181
|
}))
|
|
@@ -6308,7 +7203,7 @@ async function deployStop() {
|
|
|
6308
7203
|
O2.info("No codeam-pair process to stop on this workspace.");
|
|
6309
7204
|
}
|
|
6310
7205
|
const alsoStop = await ot2({
|
|
6311
|
-
message: `Also stop the workspace ${
|
|
7206
|
+
message: `Also stop the workspace ${import_picocolors10.default.cyan(target.displayName ?? target.id)} to save compute hours?`,
|
|
6312
7207
|
initialValue: true
|
|
6313
7208
|
});
|
|
6314
7209
|
if (!q(alsoStop) && alsoStop) {
|
|
@@ -6330,7 +7225,7 @@ async function deployStop() {
|
|
|
6330
7225
|
O2.warn(err instanceof Error ? err.message : String(err));
|
|
6331
7226
|
}
|
|
6332
7227
|
}
|
|
6333
|
-
gt(
|
|
7228
|
+
gt(import_picocolors10.default.green("\u2713 Done."));
|
|
6334
7229
|
}
|
|
6335
7230
|
async function collectWorkspacesWithStatus() {
|
|
6336
7231
|
const out = [];
|
|
@@ -6379,10 +7274,10 @@ async function probeCodeamPair(provider, workspace) {
|
|
|
6379
7274
|
}
|
|
6380
7275
|
async function stopWorkspaceFromLocal(target) {
|
|
6381
7276
|
if (target.provider.id === "github-codespaces") {
|
|
6382
|
-
const { execFile:
|
|
6383
|
-
const { promisify:
|
|
6384
|
-
const
|
|
6385
|
-
await
|
|
7277
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
7278
|
+
const { promisify: promisify7 } = await import("util");
|
|
7279
|
+
const execFileP7 = promisify7(execFile7);
|
|
7280
|
+
await execFileP7("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
|
|
6386
7281
|
return;
|
|
6387
7282
|
}
|
|
6388
7283
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeam-cli",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.25",
|
|
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": {
|