@neuralnomads/codenomad-dev 0.14.0-dev-20260422-e708c565 → 0.14.0-dev-20260427-0ba13713
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/dist/cli-upgrade.js +53 -0
- package/dist/cli-upgrade.test.js +31 -0
- package/dist/filesystem/browser.js +15 -1
- package/dist/index.js +17 -6
- package/dist/opencode-config/package.json +2 -2
- package/dist/opencode-config.js +14 -3
- package/dist/server/http-server.js +42 -112
- package/dist/server/routes/auth.js +6 -7
- package/dist/workspaces/__tests__/git-worktrees.test.js +43 -0
- package/dist/workspaces/git-worktrees.js +2 -2
- package/dist/workspaces/manager.js +5 -60
- package/dist/workspaces/opencode-auth.js +17 -0
- package/dist/workspaces/opencode-auth.test.js +34 -0
- package/package.json +6 -8
- package/public/assets/ChangesTab-lyvm8KrD.js +2 -0
- package/public/assets/DiffToolbar-C_u9j1B7.js +1 -0
- package/public/assets/FilesTab-Cff47qHk.js +2 -0
- package/public/assets/GitChangesTab-DktjnpZJ.js +2 -0
- package/public/assets/{SplitFilePanel-DhUmaW0S.js → SplitFilePanel-BOHH3F--.js} +1 -1
- package/public/assets/StatusTab-CMVYg97L.js +1 -0
- package/public/assets/align-justify-CA09D616.js +1 -0
- package/public/assets/{bundle-full-DEU8L99_.js → bundle-full-xFW13Lny.js} +1 -1
- package/public/assets/{diff-viewer-DEO4RYPx.js → diff-viewer-BkQOVrnt.js} +1 -1
- package/public/assets/index-7DpT014-.js +1 -0
- package/public/assets/index-BDK4V9L0.js +1 -0
- package/public/assets/index-Bx-Bezt2.js +1 -0
- package/public/assets/{index-DbMNXyVd.js → index-CCAZZY0l.js} +1 -1
- package/public/assets/index-CEcZq5WB.js +1 -0
- package/public/assets/index-CQCmSDHL.js +1 -0
- package/public/assets/{index--oqVn_K6.js → index-Cz_fl8aY.js} +1 -1
- package/public/assets/index-Dcgftm1m.css +1 -0
- package/public/assets/index-m4C3yx3J.js +1 -0
- package/public/assets/index-mh_z9p40.js +2 -0
- package/public/assets/{loading-CjZdFQ4L.js → loading-HUhLEgb_.js} +1 -1
- package/public/assets/main-B2dfx3K6.js +48 -0
- package/public/assets/{markdown-B5oFzzk8.js → markdown-DIZIkFnr.js} +22 -22
- package/public/assets/monaco-viewer-BgP6xpZ6.js +26 -0
- package/public/assets/{todo-BCVtJDU0.js → todo-CznQMY4c.js} +1 -1
- package/public/assets/{tool-call-Be4o4PeC.js → tool-call-B_fK5Sov.js} +25 -25
- package/public/assets/{unified-picker-Be5bKL4U.js → unified-picker-DuNuUpLy.js} +1 -1
- package/public/assets/wrap-text-bFf5tsQM.js +1 -0
- package/public/index.html +4 -4
- package/public/loading.html +4 -4
- package/public/sw.js +1 -1
- package/dist/runtime-paths.js +0 -67
- package/public/assets/ChangesTab-CVfOyRjB.js +0 -2
- package/public/assets/DiffToolbar-BqTuTuLi.js +0 -1
- package/public/assets/FilesTab-BCaD4sK4.js +0 -2
- package/public/assets/GitChangesTab-GgUiE4m7.js +0 -2
- package/public/assets/StatusTab-CZk-cmFJ.js +0 -1
- package/public/assets/index-Btyhpe4o.js +0 -1
- package/public/assets/index-C6C534z4.js +0 -1
- package/public/assets/index-CMh7_0rm.js +0 -1
- package/public/assets/index-CWmlJG88.css +0 -1
- package/public/assets/index-D7RMnYOz.js +0 -1
- package/public/assets/index-DE6KDkkL.js +0 -2
- package/public/assets/index-NrX0Q8eA.js +0 -1
- package/public/assets/index-z-uWAw1M.js +0 -1
- package/public/assets/main-BIaFBThA.js +0 -48
- package/public/assets/monaco-viewer-DANembz4.js +0 -26
- package/public/assets/wrap-text-BTyaDmSX.js +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
const CODENOMAD_PACKAGE_NAME = "@neuralnomads/codenomad";
|
|
3
|
+
function detectFromText(value) {
|
|
4
|
+
const lower = (value ?? "").toLowerCase();
|
|
5
|
+
if (!lower)
|
|
6
|
+
return null;
|
|
7
|
+
if (lower.includes("pnpm"))
|
|
8
|
+
return "pnpm";
|
|
9
|
+
if (lower.includes("bun"))
|
|
10
|
+
return "bun";
|
|
11
|
+
if (lower.includes("npm"))
|
|
12
|
+
return "npm";
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export function detectPackageManager(env = process.env) {
|
|
16
|
+
return detectFromText(env.npm_config_user_agent) ?? detectFromText(env.npm_execpath) ?? "npm";
|
|
17
|
+
}
|
|
18
|
+
export function buildUpgradeCommand(version, packageManager = detectPackageManager()) {
|
|
19
|
+
const targetVersion = (version ?? "").trim() || "latest";
|
|
20
|
+
const packageSpec = `${CODENOMAD_PACKAGE_NAME}@${targetVersion}`;
|
|
21
|
+
const args = packageManager === "bun" ? ["add", "-g", packageSpec] : ["install", "-g", packageSpec];
|
|
22
|
+
return {
|
|
23
|
+
command: packageManager,
|
|
24
|
+
args,
|
|
25
|
+
packageSpec,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function formatUpgradeCommand(command) {
|
|
29
|
+
return [command.command, ...command.args].join(" ");
|
|
30
|
+
}
|
|
31
|
+
export function runCliUpgrade(version, env = process.env) {
|
|
32
|
+
const upgrade = buildUpgradeCommand(version, detectPackageManager(env));
|
|
33
|
+
console.log(`Upgrading CodeNomad with: ${formatUpgradeCommand(upgrade)}`);
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const child = spawn(upgrade.command, upgrade.args, {
|
|
36
|
+
env,
|
|
37
|
+
shell: process.platform === "win32",
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
});
|
|
40
|
+
child.on("exit", (code, signal) => {
|
|
41
|
+
if (signal) {
|
|
42
|
+
console.error(`Upgrade command stopped by signal ${signal}`);
|
|
43
|
+
resolve(1);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
resolve(code ?? 0);
|
|
47
|
+
});
|
|
48
|
+
child.on("error", (error) => {
|
|
49
|
+
console.error("Failed to launch upgrade command", error);
|
|
50
|
+
resolve(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { buildUpgradeCommand, detectPackageManager, formatUpgradeCommand } from "./cli-upgrade";
|
|
4
|
+
describe("cli upgrade", () => {
|
|
5
|
+
it("defaults to npm when no package manager can be detected", () => {
|
|
6
|
+
assert.equal(detectPackageManager({}), "npm");
|
|
7
|
+
});
|
|
8
|
+
it("detects package managers from npm user agent", () => {
|
|
9
|
+
assert.equal(detectPackageManager({ npm_config_user_agent: "pnpm/9.0.0 node/v22" }), "pnpm");
|
|
10
|
+
assert.equal(detectPackageManager({ npm_config_user_agent: "bun/1.0.0" }), "bun");
|
|
11
|
+
assert.equal(detectPackageManager({ npm_config_user_agent: "npm/10.0.0 node/v22" }), "npm");
|
|
12
|
+
});
|
|
13
|
+
it("builds latest upgrade command by default", () => {
|
|
14
|
+
const command = buildUpgradeCommand(undefined, "npm");
|
|
15
|
+
assert.equal(command.packageSpec, "@neuralnomads/codenomad@latest");
|
|
16
|
+
assert.deepEqual(command.args, ["install", "-g", "@neuralnomads/codenomad@latest"]);
|
|
17
|
+
assert.equal(formatUpgradeCommand(command), "npm install -g @neuralnomads/codenomad@latest");
|
|
18
|
+
});
|
|
19
|
+
it("builds a versioned upgrade command", () => {
|
|
20
|
+
const command = buildUpgradeCommand("0.10.5", "pnpm");
|
|
21
|
+
assert.equal(command.packageSpec, "@neuralnomads/codenomad@0.10.5");
|
|
22
|
+
assert.deepEqual(command.args, ["install", "-g", "@neuralnomads/codenomad@0.10.5"]);
|
|
23
|
+
assert.equal(formatUpgradeCommand(command), "pnpm install -g @neuralnomads/codenomad@0.10.5");
|
|
24
|
+
});
|
|
25
|
+
it("uses bun add for Bun installs", () => {
|
|
26
|
+
const command = buildUpgradeCommand("0.10.5", "bun");
|
|
27
|
+
assert.equal(command.packageSpec, "@neuralnomads/codenomad@0.10.5");
|
|
28
|
+
assert.deepEqual(command.args, ["add", "-g", "@neuralnomads/codenomad@0.10.5"]);
|
|
29
|
+
assert.equal(formatUpgradeCommand(command), "bun add -g @neuralnomads/codenomad@0.10.5");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -206,6 +206,17 @@ export class FileSystemBrowser {
|
|
|
206
206
|
if (!input || input === "." || input === "./" || input === "/") {
|
|
207
207
|
return ".";
|
|
208
208
|
}
|
|
209
|
+
if (path.isAbsolute(input)) {
|
|
210
|
+
const resolved = path.resolve(input);
|
|
211
|
+
const relativeToRoot = path.relative(this.root, resolved);
|
|
212
|
+
if (relativeToRoot === "") {
|
|
213
|
+
return ".";
|
|
214
|
+
}
|
|
215
|
+
if (this.isOutsideRoot(relativeToRoot)) {
|
|
216
|
+
throw new Error("Access outside of root is not allowed");
|
|
217
|
+
}
|
|
218
|
+
return relativeToRoot.replace(/\\+/g, "/");
|
|
219
|
+
}
|
|
209
220
|
let normalized = input.replace(/\\+/g, "/");
|
|
210
221
|
if (normalized.startsWith("./")) {
|
|
211
222
|
normalized = normalized.replace(/^\.\/+/, "");
|
|
@@ -232,11 +243,14 @@ export class FileSystemBrowser {
|
|
|
232
243
|
const normalized = this.normalizeRelativePath(relativePath);
|
|
233
244
|
const target = path.resolve(this.root, normalized);
|
|
234
245
|
const relativeToRoot = path.relative(this.root, target);
|
|
235
|
-
if (
|
|
246
|
+
if (this.isOutsideRoot(relativeToRoot)) {
|
|
236
247
|
throw new Error("Access outside of root is not allowed");
|
|
237
248
|
}
|
|
238
249
|
return target;
|
|
239
250
|
}
|
|
251
|
+
isOutsideRoot(relativeToRoot) {
|
|
252
|
+
return relativeToRoot === ".." || relativeToRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToRoot);
|
|
253
|
+
}
|
|
240
254
|
resolveUnrestrictedPath(input) {
|
|
241
255
|
if (!input || input === "." || input === "./") {
|
|
242
256
|
return this.homeDir;
|
package/dist/index.js
CHANGED
|
@@ -28,12 +28,12 @@ import { SideCarManager } from "./sidecars/manager";
|
|
|
28
28
|
import { ClientConnectionManager } from "./clients/connection-manager";
|
|
29
29
|
import { PluginChannelManager } from "./plugins/channel";
|
|
30
30
|
import { VoiceModeManager } from "./plugins/voice-mode";
|
|
31
|
-
import {
|
|
31
|
+
import { runCliUpgrade } from "./cli-upgrade";
|
|
32
32
|
const require = createRequire(import.meta.url);
|
|
33
|
-
const packageJson =
|
|
33
|
+
const packageJson = require("../package.json");
|
|
34
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
35
35
|
const __dirname = path.dirname(__filename);
|
|
36
|
-
const DEFAULT_UI_STATIC_DIR =
|
|
36
|
+
const DEFAULT_UI_STATIC_DIR = path.resolve(__dirname, "../public");
|
|
37
37
|
const DEFAULT_HOST = "127.0.0.1";
|
|
38
38
|
const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json";
|
|
39
39
|
const DEFAULT_HTTPS_PORT = 9898;
|
|
@@ -76,9 +76,11 @@ function parseCliOptions(argv) {
|
|
|
76
76
|
.default(false))
|
|
77
77
|
.addOption(new Option("--dangerously-skip-auth", "Disable CodeNomad's internal auth. Use only behind a trusted perimeter (SSO/VPN/etc).")
|
|
78
78
|
.env("CODENOMAD_SKIP_AUTH")
|
|
79
|
-
.default(false))
|
|
79
|
+
.default(false))
|
|
80
|
+
.addOption(new Option("--upgrade [version]", "Upgrade the global CodeNomad CLI server package and exit"));
|
|
80
81
|
program.parse(argv, { from: "user" });
|
|
81
82
|
const parsed = program.opts();
|
|
83
|
+
const upgrade = parsed.upgrade;
|
|
82
84
|
const parseBooleanEnv = (value) => {
|
|
83
85
|
const normalized = (value ?? "").trim().toLowerCase();
|
|
84
86
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "y" || normalized === "on";
|
|
@@ -89,7 +91,7 @@ function parseCliOptions(argv) {
|
|
|
89
91
|
const uiAutoUpdate = autoUpdateString === "1" || autoUpdateString === "true" || autoUpdateString === "yes";
|
|
90
92
|
const httpsEnabled = parseBooleanEnv(parsed.https);
|
|
91
93
|
const httpEnabled = parseBooleanEnv(parsed.http);
|
|
92
|
-
if (!httpsEnabled && !httpEnabled) {
|
|
94
|
+
if (upgrade === undefined && !httpsEnabled && !httpEnabled) {
|
|
93
95
|
throw new InvalidArgumentError("At least one listener must be enabled (--https or --http)");
|
|
94
96
|
}
|
|
95
97
|
return {
|
|
@@ -118,6 +120,7 @@ function parseCliOptions(argv) {
|
|
|
118
120
|
authCookieName: parsed.authCookieName,
|
|
119
121
|
generateToken: Boolean(parsed.generateToken),
|
|
120
122
|
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
|
123
|
+
upgrade,
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
function parsePort(input) {
|
|
@@ -144,6 +147,11 @@ function programHasArg(argv, flag) {
|
|
|
144
147
|
}
|
|
145
148
|
async function main() {
|
|
146
149
|
const options = parseCliOptions(process.argv.slice(2));
|
|
150
|
+
if (options.upgrade !== undefined) {
|
|
151
|
+
const version = typeof options.upgrade === "string" ? options.upgrade : undefined;
|
|
152
|
+
process.exitCode = await runCliUpgrade(version);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
147
155
|
const logger = createLogger({ level: options.logLevel, destination: options.logDestination, component: "app" });
|
|
148
156
|
const workspaceLogger = logger.child({ component: "workspace" });
|
|
149
157
|
const configLogger = logger.child({ component: "config" });
|
|
@@ -211,7 +219,10 @@ async function main() {
|
|
|
211
219
|
getServerBaseUrl: () => serverMeta.localUrl,
|
|
212
220
|
nodeExtraCaCertsPath,
|
|
213
221
|
});
|
|
214
|
-
const fileSystemBrowser = new FileSystemBrowser({
|
|
222
|
+
const fileSystemBrowser = new FileSystemBrowser({
|
|
223
|
+
rootDir: options.rootDir,
|
|
224
|
+
unrestricted: options.unrestrictedRoot,
|
|
225
|
+
});
|
|
215
226
|
const instanceStore = new InstanceStore(configLocation.instancesDir);
|
|
216
227
|
const speechService = new SpeechService(settings, logger.child({ component: "speech" }));
|
|
217
228
|
const sidecarManager = new SideCarManager({
|
package/dist/opencode-config.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
2
4
|
import { createLogger } from "./logger";
|
|
3
|
-
import { resolveOpencodeTemplateDir } from "./runtime-paths";
|
|
4
5
|
const log = createLogger({ component: "opencode-config" });
|
|
5
|
-
const
|
|
6
|
-
const
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const devTemplateDir = path.resolve(__dirname, "../../opencode-config");
|
|
9
|
+
const resourcesPath = process.resourcesPath;
|
|
10
|
+
const prodTemplateDirs = [
|
|
11
|
+
resourcesPath ? path.resolve(resourcesPath, "opencode-config") : undefined,
|
|
12
|
+
path.resolve(__dirname, "opencode-config"),
|
|
13
|
+
].filter((dir) => Boolean(dir));
|
|
14
|
+
const isDevBuild = Boolean(process.env.CODENOMAD_DEV ?? process.env.CLI_UI_DEV_SERVER) || existsSync(devTemplateDir);
|
|
15
|
+
const templateDir = isDevBuild
|
|
16
|
+
? devTemplateDir
|
|
17
|
+
: prodTemplateDirs.find((dir) => existsSync(dir)) ?? prodTemplateDirs[0];
|
|
7
18
|
export function getOpencodeConfigDir() {
|
|
8
19
|
if (!existsSync(templateDir)) {
|
|
9
20
|
throw new Error(`CodeNomad Opencode config template missing at ${templateDir}`);
|
|
@@ -5,8 +5,6 @@ import replyFrom from "@fastify/reply-from";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import { connect as connectTcp } from "net";
|
|
7
7
|
import path from "path";
|
|
8
|
-
import { Readable } from "stream";
|
|
9
|
-
import { pipeline } from "stream/promises";
|
|
10
8
|
import { connect as connectTls } from "tls";
|
|
11
9
|
import { fetch } from "undici";
|
|
12
10
|
import { isValidWorktreeSlug } from "../workspaces/git-worktrees";
|
|
@@ -491,49 +489,47 @@ async function proxyWorkspaceRequest(args) {
|
|
|
491
489
|
if (logger.isLevelEnabled("trace")) {
|
|
492
490
|
logger.trace({ workspaceId, targetUrl, body: request.body }, "Instance proxy payload");
|
|
493
491
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
directory
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
}
|
|
492
|
+
return reply.from(targetUrl, {
|
|
493
|
+
rewriteRequestHeaders: (_originalRequest, headers) => {
|
|
494
|
+
if (instanceAuthHeader) {
|
|
495
|
+
headers.authorization = instanceAuthHeader;
|
|
496
|
+
}
|
|
497
|
+
// OpenCode expects the *full* path; we send it via header to avoid query tampering.
|
|
498
|
+
const isNonASCII = /[^\x00-\x7F]/.test(directory);
|
|
499
|
+
const encodedDirectory = isNonASCII ? encodeURIComponent(directory) : directory;
|
|
500
|
+
headers["x-opencode-directory"] = encodedDirectory;
|
|
501
|
+
if (logger.isLevelEnabled("trace")) {
|
|
502
|
+
const outgoing = {};
|
|
503
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
504
|
+
outgoing[key] = value;
|
|
505
|
+
}
|
|
506
|
+
// Redact sensitive headers.
|
|
507
|
+
for (const key of Object.keys(outgoing)) {
|
|
508
|
+
const lower = key.toLowerCase();
|
|
509
|
+
if (lower === "authorization" || lower === "cookie" || lower === "set-cookie") {
|
|
510
|
+
outgoing[key] = "<redacted>";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
logger.trace({
|
|
514
|
+
workspaceId,
|
|
515
|
+
method: request.method,
|
|
516
|
+
targetUrl,
|
|
517
|
+
worktreeSlug,
|
|
518
|
+
directory,
|
|
519
|
+
contentType: request.headers["content-type"],
|
|
520
|
+
body: bodyToJson(request.body),
|
|
521
|
+
headers: outgoing,
|
|
522
|
+
}, "Proxy -> OpenCode request");
|
|
523
|
+
}
|
|
524
|
+
return headers;
|
|
525
|
+
},
|
|
526
|
+
onError: (proxyReply, { error }) => {
|
|
527
|
+
logger.error({ err: error, workspaceId, targetUrl }, "Failed to proxy workspace request");
|
|
528
|
+
if (!proxyReply.sent) {
|
|
529
|
+
proxyReply.code(502).send({ error: "Workspace instance proxy failed" });
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
});
|
|
537
533
|
}
|
|
538
534
|
function extractOpencodeDirectoryOverride(pathSuffix) {
|
|
539
535
|
if (!pathSuffix) {
|
|
@@ -696,78 +692,12 @@ function isApiRequest(rawUrl) {
|
|
|
696
692
|
function buildProxyHeaders(headers) {
|
|
697
693
|
const result = {};
|
|
698
694
|
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
699
|
-
|
|
700
|
-
if (!value || lower === "host" || isHopByHopHeader(lower))
|
|
695
|
+
if (!value || key.toLowerCase() === "host")
|
|
701
696
|
continue;
|
|
702
697
|
result[key] = Array.isArray(value) ? value.join(",") : value;
|
|
703
698
|
}
|
|
704
699
|
return result;
|
|
705
700
|
}
|
|
706
|
-
function toProxyRequestBody(body) {
|
|
707
|
-
if (body == null) {
|
|
708
|
-
return undefined;
|
|
709
|
-
}
|
|
710
|
-
if (typeof body.pipe === "function") {
|
|
711
|
-
return body;
|
|
712
|
-
}
|
|
713
|
-
if (typeof body[Symbol.asyncIterator] === "function") {
|
|
714
|
-
return body;
|
|
715
|
-
}
|
|
716
|
-
if (Buffer.isBuffer(body) || typeof body === "string" || body instanceof Uint8Array) {
|
|
717
|
-
return body;
|
|
718
|
-
}
|
|
719
|
-
return JSON.stringify(body);
|
|
720
|
-
}
|
|
721
|
-
function buildWorkspaceInstanceProxyHeaders(headers, instanceAuthHeader, directory) {
|
|
722
|
-
const next = buildProxyHeaders(headers);
|
|
723
|
-
if (instanceAuthHeader) {
|
|
724
|
-
next.authorization = instanceAuthHeader;
|
|
725
|
-
}
|
|
726
|
-
const isNonASCII = /[^\x00-\x7F]/.test(directory);
|
|
727
|
-
next["x-opencode-directory"] = isNonASCII ? encodeURIComponent(directory) : directory;
|
|
728
|
-
return next;
|
|
729
|
-
}
|
|
730
|
-
function redactProxyHeadersForLogs(headers) {
|
|
731
|
-
const outgoing = { ...headers };
|
|
732
|
-
for (const key of Object.keys(outgoing)) {
|
|
733
|
-
const lower = key.toLowerCase();
|
|
734
|
-
if (lower === "authorization" || lower === "cookie" || lower === "set-cookie") {
|
|
735
|
-
outgoing[key] = "<redacted>";
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
return outgoing;
|
|
739
|
-
}
|
|
740
|
-
function applyInstanceProxyResponseHeaders(reply, response) {
|
|
741
|
-
response.headers.forEach((value, key) => {
|
|
742
|
-
const lower = key.toLowerCase();
|
|
743
|
-
if (isHopByHopHeader(lower) || lower === "content-length" || lower === "content-encoding") {
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
reply.header(key, value);
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
function toOutgoingHeaders(headers) {
|
|
750
|
-
const next = {};
|
|
751
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
752
|
-
if (value === undefined) {
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
next[key] = Array.isArray(value) ? value.map(String) : String(value);
|
|
756
|
-
}
|
|
757
|
-
return next;
|
|
758
|
-
}
|
|
759
|
-
function isHopByHopHeader(name) {
|
|
760
|
-
return new Set([
|
|
761
|
-
"connection",
|
|
762
|
-
"keep-alive",
|
|
763
|
-
"proxy-authenticate",
|
|
764
|
-
"proxy-authorization",
|
|
765
|
-
"te",
|
|
766
|
-
"trailer",
|
|
767
|
-
"transfer-encoding",
|
|
768
|
-
"upgrade",
|
|
769
|
-
]).has(name);
|
|
770
|
-
}
|
|
771
701
|
async function proxySideCarRequest(args) {
|
|
772
702
|
const sidecarId = args.request.params.id ?? "";
|
|
773
703
|
const sidecar = await args.sidecarManager.get(sidecarId);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { isLoopbackAddress } from "../../auth/http-auth";
|
|
4
|
-
import { resolveAuthTemplatePath } from "../../runtime-paths";
|
|
5
4
|
const LoginSchema = z.object({
|
|
6
5
|
username: z.string().min(1),
|
|
7
6
|
password: z.string().min(1),
|
|
@@ -12,26 +11,26 @@ const TokenSchema = z.object({
|
|
|
12
11
|
const PasswordSchema = z.object({
|
|
13
12
|
password: z.string().min(8),
|
|
14
13
|
});
|
|
15
|
-
const
|
|
16
|
-
const
|
|
14
|
+
const LOGIN_TEMPLATE_URL = new URL("./auth-pages/login.html", import.meta.url);
|
|
15
|
+
const TOKEN_TEMPLATE_URL = new URL("./auth-pages/token.html", import.meta.url);
|
|
17
16
|
let cachedLoginTemplate = null;
|
|
18
17
|
let cachedTokenTemplate = null;
|
|
19
|
-
function readTemplate(
|
|
18
|
+
function readTemplate(url, cache) {
|
|
20
19
|
if (cache)
|
|
21
20
|
return cache;
|
|
22
|
-
const content = fs.readFileSync(
|
|
21
|
+
const content = fs.readFileSync(url, "utf-8");
|
|
23
22
|
return content;
|
|
24
23
|
}
|
|
25
24
|
function getLoginHtml(defaultUsername) {
|
|
26
25
|
if (!cachedLoginTemplate) {
|
|
27
|
-
cachedLoginTemplate = readTemplate(
|
|
26
|
+
cachedLoginTemplate = readTemplate(LOGIN_TEMPLATE_URL, null);
|
|
28
27
|
}
|
|
29
28
|
const escapedUsername = escapeHtml(defaultUsername);
|
|
30
29
|
return cachedLoginTemplate.replace(/\{\{DEFAULT_USERNAME\}\}/g, escapedUsername);
|
|
31
30
|
}
|
|
32
31
|
function getTokenHtml() {
|
|
33
32
|
if (!cachedTokenTemplate) {
|
|
34
|
-
cachedTokenTemplate = readTemplate(
|
|
33
|
+
cachedTokenTemplate = readTemplate(TOKEN_TEMPLATE_URL, null);
|
|
35
34
|
}
|
|
36
35
|
return cachedTokenTemplate;
|
|
37
36
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { listWorktrees } from "../git-worktrees";
|
|
7
|
+
describe("listWorktrees", () => {
|
|
8
|
+
it("uses the selected workspace folder for the root worktree directory", async () => {
|
|
9
|
+
const temp = mkdtempSync(path.join(tmpdir(), "codenomad-git-worktrees-"));
|
|
10
|
+
const binDir = path.join(temp, "bin");
|
|
11
|
+
const repoRoot = path.join(temp, "repo");
|
|
12
|
+
const workspaceFolder = path.join(repoRoot, "proj-1");
|
|
13
|
+
const originalPath = process.env.PATH;
|
|
14
|
+
try {
|
|
15
|
+
mkdirSync(binDir, { recursive: true });
|
|
16
|
+
mkdirSync(workspaceFolder, { recursive: true });
|
|
17
|
+
const gitPath = path.join(binDir, process.platform === "win32" ? "git.cmd" : "git");
|
|
18
|
+
const porcelain = [
|
|
19
|
+
`worktree ${repoRoot}`,
|
|
20
|
+
"HEAD 1111111",
|
|
21
|
+
"branch refs/heads/main",
|
|
22
|
+
"",
|
|
23
|
+
].join("\n");
|
|
24
|
+
if (process.platform === "win32") {
|
|
25
|
+
writeFileSync(gitPath, `@echo off\r\nif "%1"=="worktree" if "%2"=="list" if "%3"=="--porcelain" (\r\necho ${porcelain.replace(/\n/g, "\r\necho ")}\r\nexit /b 0\r\n)\r\nexit /b 1\r\n`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
writeFileSync(gitPath, `#!/bin/sh\nif [ "$1" = "worktree" ] && [ "$2" = "list" ] && [ "$3" = "--porcelain" ]; then\nprintf '%s\n' '${porcelain.replace(/'/g, "'\\''")}'\nexit 0\nfi\nexit 1\n`, { mode: 0o755 });
|
|
29
|
+
}
|
|
30
|
+
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`;
|
|
31
|
+
const worktrees = await listWorktrees({ repoRoot, workspaceFolder });
|
|
32
|
+
assert.equal(worktrees[0]?.slug, "root");
|
|
33
|
+
assert.equal(worktrees[0]?.directory, workspaceFolder);
|
|
34
|
+
assert.equal(worktrees[0]?.kind, "root");
|
|
35
|
+
assert.equal(worktrees[0]?.branch, "main");
|
|
36
|
+
assert.notEqual(worktrees[0]?.directory, repoRoot);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
process.env.PATH = originalPath;
|
|
40
|
+
rmSync(temp, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -87,7 +87,7 @@ export async function listWorktrees(params) {
|
|
|
87
87
|
const { repoRoot, workspaceFolder, logger } = params;
|
|
88
88
|
const result = await runGit(["worktree", "list", "--porcelain"], workspaceFolder);
|
|
89
89
|
if (!result.ok) {
|
|
90
|
-
const rootDescriptor = { slug: "root", directory:
|
|
90
|
+
const rootDescriptor = { slug: "root", directory: workspaceFolder, kind: "root" };
|
|
91
91
|
logger?.debug?.({ repoRoot, err: result.error }, "Failed to list git worktrees; returning root only");
|
|
92
92
|
return [rootDescriptor];
|
|
93
93
|
}
|
|
@@ -95,7 +95,7 @@ export async function listWorktrees(params) {
|
|
|
95
95
|
const rootRecord = records.find((record) => path.resolve(record.worktree) === path.resolve(repoRoot));
|
|
96
96
|
const rootDescriptor = {
|
|
97
97
|
slug: "root",
|
|
98
|
-
directory:
|
|
98
|
+
directory: workspaceFolder,
|
|
99
99
|
kind: "root",
|
|
100
100
|
branch: rootRecord?.branch,
|
|
101
101
|
};
|
|
@@ -6,60 +6,8 @@ import { searchWorkspaceFiles } from "../filesystem/search";
|
|
|
6
6
|
import { clearWorkspaceSearchCache } from "../filesystem/search-cache";
|
|
7
7
|
import { WorkspaceRuntime } from "./runtime";
|
|
8
8
|
import { getOpencodeConfigDir } from "../opencode-config.js";
|
|
9
|
-
import { buildOpencodeBasicAuthHeader,
|
|
9
|
+
import { buildOpencodeBasicAuthHeader, OPENCODE_SERVER_PASSWORD_ENV, OPENCODE_SERVER_USERNAME_ENV, resolveOpencodeServerAuth, } from "./opencode-auth";
|
|
10
10
|
const STARTUP_STABILITY_DELAY_MS = 1500;
|
|
11
|
-
function defaultShellPath() {
|
|
12
|
-
const configured = process.env.SHELL?.trim();
|
|
13
|
-
if (configured) {
|
|
14
|
-
return configured;
|
|
15
|
-
}
|
|
16
|
-
return process.platform === "darwin" ? "/bin/zsh" : "/bin/bash";
|
|
17
|
-
}
|
|
18
|
-
function shellEscape(input) {
|
|
19
|
-
if (!input)
|
|
20
|
-
return "''";
|
|
21
|
-
return `'${input.replace(/'/g, `'\\''`)}'`;
|
|
22
|
-
}
|
|
23
|
-
function wrapCommandForShell(command, shellPath) {
|
|
24
|
-
const shellName = path.basename(shellPath).toLowerCase();
|
|
25
|
-
if (shellName.includes("bash")) {
|
|
26
|
-
return `if [ -f ~/.bashrc ]; then source ~/.bashrc >/dev/null 2>&1; fi; ${command}`;
|
|
27
|
-
}
|
|
28
|
-
if (shellName.includes("zsh")) {
|
|
29
|
-
return `if [ -f ~/.zshrc ]; then source ~/.zshrc >/dev/null 2>&1; fi; ${command}`;
|
|
30
|
-
}
|
|
31
|
-
return command;
|
|
32
|
-
}
|
|
33
|
-
function buildShellArgs(shellPath, command) {
|
|
34
|
-
const shellName = path.basename(shellPath).toLowerCase();
|
|
35
|
-
if (shellName.includes("zsh")) {
|
|
36
|
-
return ["-l", "-i", "-c", command];
|
|
37
|
-
}
|
|
38
|
-
return ["-l", "-c", command];
|
|
39
|
-
}
|
|
40
|
-
function resolveBinaryPathFromUserShell(identifier) {
|
|
41
|
-
if (process.platform === "win32") {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
const shellPath = defaultShellPath();
|
|
45
|
-
const lookupCommand = wrapCommandForShell(`command -v ${shellEscape(identifier)}`, shellPath);
|
|
46
|
-
const result = spawnSync(shellPath, buildShellArgs(shellPath, lookupCommand), {
|
|
47
|
-
encoding: "utf8",
|
|
48
|
-
env: {
|
|
49
|
-
...process.env,
|
|
50
|
-
npm_config_prefix: undefined,
|
|
51
|
-
NPM_CONFIG_PREFIX: undefined,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
if (result.status !== 0) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const resolved = String(result.stdout ?? "")
|
|
58
|
-
.split(/\r?\n/)
|
|
59
|
-
.map((line) => line.trim())
|
|
60
|
-
.find((line) => line.length > 0);
|
|
61
|
-
return resolved ?? null;
|
|
62
|
-
}
|
|
63
11
|
export class WorkspaceManager {
|
|
64
12
|
constructor(options) {
|
|
65
13
|
this.options = options;
|
|
@@ -129,8 +77,10 @@ export class WorkspaceManager {
|
|
|
129
77
|
const serverConfig = this.options.settings.getOwner("config", "server");
|
|
130
78
|
const envVars = serverConfig?.environmentVariables;
|
|
131
79
|
const userEnvironment = envVars && typeof envVars === "object" && !Array.isArray(envVars) ? envVars : {};
|
|
132
|
-
const opencodeUsername =
|
|
133
|
-
|
|
80
|
+
const { username: opencodeUsername, password: opencodePassword } = resolveOpencodeServerAuth({
|
|
81
|
+
userEnvironment,
|
|
82
|
+
processEnv: process.env,
|
|
83
|
+
});
|
|
134
84
|
const authorization = buildOpencodeBasicAuthHeader({ username: opencodeUsername, password: opencodePassword });
|
|
135
85
|
if (!authorization) {
|
|
136
86
|
throw new Error("Failed to build OpenCode auth header");
|
|
@@ -252,11 +202,6 @@ export class WorkspaceManager {
|
|
|
252
202
|
catch (error) {
|
|
253
203
|
this.options.logger.warn({ identifier, err: error }, "Failed to resolve binary path from system PATH");
|
|
254
204
|
}
|
|
255
|
-
const shellResolved = resolveBinaryPathFromUserShell(identifier);
|
|
256
|
-
if (shellResolved) {
|
|
257
|
-
this.options.logger.debug({ identifier, resolved: shellResolved }, "Resolved binary path from user shell");
|
|
258
|
-
return shellResolved;
|
|
259
|
-
}
|
|
260
205
|
return identifier;
|
|
261
206
|
}
|
|
262
207
|
pickBinaryCandidate(candidates) {
|
|
@@ -5,6 +5,23 @@ export const DEFAULT_OPENCODE_USERNAME = "codenomad";
|
|
|
5
5
|
export function generateOpencodeServerPassword() {
|
|
6
6
|
return crypto.randomBytes(32).toString("base64url");
|
|
7
7
|
}
|
|
8
|
+
function readConfiguredValue(key, ...sources) {
|
|
9
|
+
for (const source of sources) {
|
|
10
|
+
const value = source?.[key];
|
|
11
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export function resolveOpencodeServerAuth(options = {}) {
|
|
18
|
+
const generatePassword = options.generatePassword ?? generateOpencodeServerPassword;
|
|
19
|
+
const username = readConfiguredValue(OPENCODE_SERVER_USERNAME_ENV, options.userEnvironment, options.processEnv) ??
|
|
20
|
+
DEFAULT_OPENCODE_USERNAME;
|
|
21
|
+
const password = readConfiguredValue(OPENCODE_SERVER_PASSWORD_ENV, options.userEnvironment, options.processEnv) ??
|
|
22
|
+
generatePassword();
|
|
23
|
+
return { username, password };
|
|
24
|
+
}
|
|
8
25
|
export function buildOpencodeBasicAuthHeader(params) {
|
|
9
26
|
const username = params.username;
|
|
10
27
|
const password = params.password;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { resolveOpencodeServerAuth } from "./opencode-auth";
|
|
4
|
+
describe("resolveOpencodeServerAuth", () => {
|
|
5
|
+
it("uses configured OpenCode auth from workspace environment", () => {
|
|
6
|
+
const auth = resolveOpencodeServerAuth({
|
|
7
|
+
userEnvironment: {
|
|
8
|
+
OPENCODE_SERVER_USERNAME: "alice",
|
|
9
|
+
OPENCODE_SERVER_PASSWORD: "secret",
|
|
10
|
+
},
|
|
11
|
+
processEnv: {},
|
|
12
|
+
generatePassword: () => "generated",
|
|
13
|
+
});
|
|
14
|
+
assert.deepEqual(auth, { username: "alice", password: "secret" });
|
|
15
|
+
});
|
|
16
|
+
it("uses process environment when workspace environment does not provide credentials", () => {
|
|
17
|
+
const auth = resolveOpencodeServerAuth({
|
|
18
|
+
userEnvironment: {},
|
|
19
|
+
processEnv: {
|
|
20
|
+
OPENCODE_SERVER_PASSWORD: "process-secret",
|
|
21
|
+
},
|
|
22
|
+
generatePassword: () => "generated",
|
|
23
|
+
});
|
|
24
|
+
assert.deepEqual(auth, { username: "codenomad", password: "process-secret" });
|
|
25
|
+
});
|
|
26
|
+
it("falls back to generated credentials", () => {
|
|
27
|
+
const auth = resolveOpencodeServerAuth({
|
|
28
|
+
userEnvironment: {},
|
|
29
|
+
processEnv: {},
|
|
30
|
+
generatePassword: () => "generated",
|
|
31
|
+
});
|
|
32
|
+
assert.deepEqual(auth, { username: "codenomad", password: "generated" });
|
|
33
|
+
});
|
|
34
|
+
});
|