codehost 0.3.1 → 0.4.0
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 +12 -0
- package/README.md +5 -0
- package/package.json +2 -1
- package/public/_redirects +5 -0
- package/public/install.ps1 +31 -15
- package/public/install.sh +40 -15
- package/src/cli/commands/dev.ts +4 -1
- package/src/cli/commands/serve.ts +4 -2
- package/src/cli/daemonize.ts +6 -0
- package/src/cli/self-update.ts +89 -0
- package/src/shared/repo.test.ts +37 -0
- package/src/shared/repo.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [0.4.0](https://github.com/snomiao/codehost/compare/v0.3.1...v0.4.0) (2026-06-08)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* normalize Windows serve path to POSIX-drive form for VS Code web ([8add2d6](https://github.com/snomiao/codehost/commit/8add2d66a6b5ae765f91386f738ad38abcb90057))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* setup.sh/ps1 installer aliases + self-update to latest on serve/setup ([93a5d64](https://github.com/snomiao/codehost/commit/93a5d64170c5851ca0f488348975dae46f467b5e))
|
|
12
|
+
|
|
1
13
|
## [0.3.1](https://github.com/snomiao/codehost/compare/v0.3.0...v0.3.1) (2026-06-07)
|
|
2
14
|
|
|
3
15
|
|
package/README.md
CHANGED
|
@@ -32,6 +32,11 @@ curl -fsSL https://codehost.dev/install.sh | sh
|
|
|
32
32
|
powershell -c "irm codehost.dev/install.ps1 | iex"
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
`/setup.sh` and `/setup.ps1` are aliases of the same script. Re-running it any
|
|
36
|
+
time **upgrades you to the latest codehost** (`bun add -g codehost@latest`), so
|
|
37
|
+
it doubles as the updater — and it bootstraps Bun by absolute path, so it won't
|
|
38
|
+
bail just because your shell hasn't picked Bun up on `PATH` yet.
|
|
39
|
+
|
|
35
40
|
Already have Bun? `bun add -g codehost && codehost setup` does the same. Or, for
|
|
36
41
|
a specific directory/token in one shot:
|
|
37
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"deploy:signal": "cd worker && wrangler deploy",
|
|
21
21
|
"deploy:pages": "vite build && wrangler pages deploy dist/public --project-name codehost",
|
|
22
22
|
"start": "bun src/server.ts",
|
|
23
|
+
"test": "bun test",
|
|
23
24
|
"typecheck": "tsc --noEmit && tsc --noEmit -p src/web/tsconfig.sw.json && tsc --noEmit -p worker/tsconfig.json"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
package/public/_redirects
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# Friendly aliases for the installer: /setup.sh and /setup.ps1 serve the same
|
|
2
|
+
# scripts as /install.*. Must precede the SPA catch-all below (first match wins).
|
|
3
|
+
/setup.sh /install.sh 200
|
|
4
|
+
/setup.ps1 /install.ps1 200
|
|
5
|
+
|
|
1
6
|
# SPA fallback: deep links like /gh/<owner>/<repo>/tree/<branch> and /dev/<path>
|
|
2
7
|
# have no static file, so serve the app and let it route client-side. Cloudflare
|
|
3
8
|
# Pages serves existing files first (/assets/*, /sw.js, /install.*), so only
|
package/public/install.ps1
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
# codehost installer — https://codehost.dev
|
|
1
|
+
# codehost installer / updater — https://codehost.dev
|
|
2
2
|
#
|
|
3
3
|
# powershell -c "irm codehost.dev/install.ps1 | iex"
|
|
4
|
+
# powershell -c "irm codehost.dev/setup.ps1 | iex" # same script, friendlier name
|
|
4
5
|
#
|
|
5
|
-
# Ensures Bun is installed, installs the `codehost` CLI globally
|
|
6
|
-
# the native WebRTC binary via Bun's lifecycle
|
|
7
|
-
# setup` to pick a token, install VS Code, and
|
|
6
|
+
# Ensures Bun is installed, installs/UPGRADES the `codehost` CLI globally to the
|
|
7
|
+
# latest release (which fetches the native WebRTC binary via Bun's lifecycle
|
|
8
|
+
# scripts), then runs `codehost setup` to pick a token, install VS Code, and
|
|
9
|
+
# start a server daemon. Safe to re-run any time — it always lands you on the
|
|
10
|
+
# newest codehost.
|
|
8
11
|
#
|
|
9
|
-
#
|
|
12
|
+
# Bun is the runtime and can't be skipped — we bootstrap it and then invoke
|
|
13
|
+
# everything by absolute path, so a not-yet-reloaded shell PATH never makes the
|
|
14
|
+
# install "fail" after Bun is actually present.
|
|
15
|
+
#
|
|
16
|
+
# Env override: $env:CODEHOST_NO_SETUP = "1" -> install/update only, skip setup.
|
|
10
17
|
|
|
11
18
|
$ErrorActionPreference = "Stop"
|
|
12
19
|
|
|
@@ -15,29 +22,38 @@ function Fail($m) { Write-Host "[codehost] $m" -ForegroundColor Red; exit 1 }
|
|
|
15
22
|
|
|
16
23
|
# Bun installs its global bin under %USERPROFILE%\.bun\bin by default.
|
|
17
24
|
$bunBin = Join-Path $env:USERPROFILE ".bun\bin"
|
|
25
|
+
$bunExe = Join-Path $bunBin "bun.exe"
|
|
18
26
|
if (Test-Path $bunBin) { $env:Path = "$bunBin;$env:Path" }
|
|
19
27
|
|
|
20
|
-
if (-not (
|
|
28
|
+
if (-not (Test-Path $bunExe)) {
|
|
21
29
|
Info "Bun not found - installing from bun.sh..."
|
|
22
30
|
Invoke-RestMethod https://bun.sh/install.ps1 | Invoke-Expression
|
|
23
31
|
if (Test-Path $bunBin) { $env:Path = "$bunBin;$env:Path" }
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
# Prefer the absolute path; fall back to a PATH lookup in case Bun honored a
|
|
35
|
+
# custom install dir. Either way we never bail just because PATH wasn't reloaded.
|
|
36
|
+
if (-not (Test-Path $bunExe)) {
|
|
37
|
+
$cmd = Get-Command bun -ErrorAction SilentlyContinue
|
|
38
|
+
if ($cmd) { $bunExe = $cmd.Source }
|
|
39
|
+
else { Fail "Bun was installed but bun.exe wasn't found at $bunBin. Open a new terminal and re-run." }
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
# `@latest` makes every re-run an upgrade, so users always end up on the newest
|
|
43
|
+
# codehost instead of a stale globally-pinned copy.
|
|
44
|
+
Info "installing the latest codehost CLI (bun add -g codehost@latest)..."
|
|
45
|
+
& $bunExe add -g codehost@latest
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
47
|
+
# Resolve the codehost shim Bun just wrote (extension varies on Windows).
|
|
48
|
+
$codehostCmd = Get-Command codehost -ErrorAction SilentlyContinue
|
|
49
|
+
if ($codehostCmd) { $codehostExe = $codehostCmd.Source }
|
|
50
|
+
elseif (Test-Path (Join-Path $bunBin "codehost.exe")) { $codehostExe = Join-Path $bunBin "codehost.exe" }
|
|
51
|
+
else { Fail "codehost installed but not found in $bunBin. Add it to PATH and run: codehost setup" }
|
|
36
52
|
|
|
37
53
|
if ($env:CODEHOST_NO_SETUP -eq "1") {
|
|
38
|
-
Info "installed. Run ``codehost setup`` in the directory you want to serve."
|
|
54
|
+
Info "installed/updated. Run ``codehost setup`` in the directory you want to serve."
|
|
39
55
|
exit 0
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
Info "running ``codehost setup``..."
|
|
43
|
-
|
|
59
|
+
& $codehostExe setup
|
package/public/install.sh
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# codehost installer — https://codehost.dev
|
|
2
|
+
# codehost installer / updater — https://codehost.dev
|
|
3
3
|
#
|
|
4
4
|
# curl -fsSL https://codehost.dev/install.sh | sh
|
|
5
|
+
# curl -fsSL https://codehost.dev/setup.sh | sh # same script, friendlier name
|
|
5
6
|
#
|
|
6
|
-
# Ensures Bun is installed, installs the `codehost` CLI globally
|
|
7
|
-
# the native WebRTC binary via Bun's lifecycle
|
|
8
|
-
# setup` to pick a token, install VS Code, and
|
|
7
|
+
# Ensures Bun is installed, installs/UPGRADES the `codehost` CLI globally to the
|
|
8
|
+
# latest release (which fetches the native WebRTC binary via Bun's lifecycle
|
|
9
|
+
# scripts), then runs `codehost setup` to pick a token, install VS Code, and
|
|
10
|
+
# start a server daemon. Safe to re-run any time — it always lands you on the
|
|
11
|
+
# newest codehost.
|
|
12
|
+
#
|
|
13
|
+
# Bun is the runtime (the CLI is TypeScript run by Bun + a native addon), so it
|
|
14
|
+
# can't be skipped — but we bootstrap it and then invoke everything by absolute
|
|
15
|
+
# path, so a not-yet-reloaded shell PATH never makes the install "fail" after
|
|
16
|
+
# Bun is actually present.
|
|
9
17
|
#
|
|
10
18
|
# Env overrides:
|
|
11
|
-
# CODEHOST_NO_SETUP=1 install only; don't run `codehost setup`
|
|
19
|
+
# CODEHOST_NO_SETUP=1 install/update only; don't run `codehost setup`
|
|
12
20
|
set -eu
|
|
13
21
|
|
|
14
22
|
info() { printf '\033[1;36m[codehost]\033[0m %s\n' "$1"; }
|
|
@@ -20,7 +28,18 @@ BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
|
|
|
20
28
|
export BUN_INSTALL
|
|
21
29
|
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
# Resolve a usable bun binary: one already on PATH, or the managed install.
|
|
32
|
+
# Echoes the absolute path (empty if none) so callers can invoke it directly.
|
|
33
|
+
bun_bin() {
|
|
34
|
+
if command -v bun >/dev/null 2>&1; then
|
|
35
|
+
command -v bun
|
|
36
|
+
elif [ -x "$BUN_INSTALL/bin/bun" ]; then
|
|
37
|
+
printf '%s\n' "$BUN_INSTALL/bin/bun"
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BUN="$(bun_bin)"
|
|
42
|
+
if [ -z "$BUN" ]; then
|
|
24
43
|
info "Bun not found — installing from bun.sh…"
|
|
25
44
|
if command -v curl >/dev/null 2>&1; then
|
|
26
45
|
curl -fsSL https://bun.sh/install | bash
|
|
@@ -30,26 +49,32 @@ if ! command -v bun >/dev/null 2>&1; then
|
|
|
30
49
|
err "need curl or wget to install Bun. Install one and re-run."
|
|
31
50
|
exit 1
|
|
32
51
|
fi
|
|
33
|
-
|
|
52
|
+
BUN="$(bun_bin)"
|
|
34
53
|
fi
|
|
35
54
|
|
|
36
|
-
if
|
|
37
|
-
err "Bun
|
|
55
|
+
if [ -z "$BUN" ]; then
|
|
56
|
+
err "Bun was installed but its binary isn't at $BUN_INSTALL/bin/bun. Set BUN_INSTALL to its location and re-run."
|
|
38
57
|
exit 1
|
|
39
58
|
fi
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
# `@latest` makes every re-run an upgrade, so users always end up on the newest
|
|
61
|
+
# codehost instead of a stale globally-pinned copy.
|
|
62
|
+
info "installing the latest codehost CLI ($BUN add -g codehost@latest)…"
|
|
63
|
+
"$BUN" add -g codehost@latest
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
CODEHOST="$BUN_INSTALL/bin/codehost"
|
|
66
|
+
if [ ! -x "$CODEHOST" ]; then
|
|
67
|
+
CODEHOST="$(command -v codehost || true)"
|
|
68
|
+
fi
|
|
69
|
+
if [ -z "$CODEHOST" ] || [ ! -x "$CODEHOST" ]; then
|
|
70
|
+
err "codehost installed but not found under $BUN_INSTALL/bin. Add it to PATH and run: codehost setup"
|
|
46
71
|
exit 1
|
|
47
72
|
fi
|
|
48
73
|
|
|
49
74
|
if [ "${CODEHOST_NO_SETUP:-}" = "1" ]; then
|
|
50
|
-
info "installed. Run \`codehost setup\` in the directory you want to serve."
|
|
75
|
+
info "installed/updated. Run \`codehost setup\` in the directory you want to serve."
|
|
51
76
|
exit 0
|
|
52
77
|
fi
|
|
53
78
|
|
|
54
79
|
info "running \`codehost setup\`…"
|
|
55
|
-
exec
|
|
80
|
+
exec "$CODEHOST" setup
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { announceConnect } from "../open-url";
|
|
|
8
8
|
import { runServer } from "../run-server";
|
|
9
9
|
import { launchVscode } from "../vscode";
|
|
10
10
|
import { repoIdentity } from "../git";
|
|
11
|
+
import { toPosixPath } from "../../shared/repo";
|
|
11
12
|
import { DEFAULT_SIGNAL_URL } from "./serve";
|
|
12
13
|
|
|
13
14
|
interface DevArgs {
|
|
@@ -85,7 +86,9 @@ export const devCommand: CommandModule<{}, DevArgs> = {
|
|
|
85
86
|
const id = repoIdentity(dir);
|
|
86
87
|
const meta: PeerMeta = {
|
|
87
88
|
name: argv.name ?? host,
|
|
88
|
-
|
|
89
|
+
// POSIX-drive form for the browser (C:\ws -> /c/ws); `dir` stays the real
|
|
90
|
+
// OS path for the local VS Code working dir.
|
|
91
|
+
cwd: toPosixPath(dir),
|
|
89
92
|
host,
|
|
90
93
|
kind: "repo",
|
|
91
94
|
repo: id.repo,
|
|
@@ -2,7 +2,7 @@ import { hostname } from "node:os";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import type { CommandModule } from "yargs";
|
|
4
4
|
import type { PeerMeta } from "../../shared/signaling";
|
|
5
|
-
import { DEFAULT_LAYOUT } from "../../shared/repo";
|
|
5
|
+
import { DEFAULT_LAYOUT, toPosixPath } from "../../shared/repo";
|
|
6
6
|
import { TOKEN_REQUIREMENTS, validateToken } from "../../shared/token";
|
|
7
7
|
import { launchServeDaemon } from "../daemonize";
|
|
8
8
|
import { announceConnect } from "../open-url";
|
|
@@ -87,7 +87,9 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
|
|
|
87
87
|
// onto subfolders via VS Code's ?folder= using this layout.
|
|
88
88
|
const meta: PeerMeta = {
|
|
89
89
|
name: argv.name ?? host,
|
|
90
|
-
|
|
90
|
+
// POSIX-drive form for the browser ?folder= URI (C:\ws -> /c/ws); the real
|
|
91
|
+
// OS path `dir` is still what we spawn VS Code in.
|
|
92
|
+
cwd: toPosixPath(dir),
|
|
91
93
|
host,
|
|
92
94
|
kind: "root",
|
|
93
95
|
layout: DEFAULT_LAYOUT,
|
package/src/cli/daemonize.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { daemonName, startDaemon } from "./oxmgr";
|
|
2
|
+
import { selfUpdate } from "./self-update";
|
|
2
3
|
|
|
3
4
|
export interface ServeDaemonOptions {
|
|
4
5
|
/** Subcommand to re-launch under oxmgr. */
|
|
@@ -31,6 +32,11 @@ export interface ServeDaemonResult {
|
|
|
31
32
|
* the shell and restarts on failure. Shared by `serve -d` and `setup`.
|
|
32
33
|
*/
|
|
33
34
|
export async function launchServeDaemon(opts: ServeDaemonOptions): Promise<ServeDaemonResult> {
|
|
35
|
+
// Upgrade the global install (if that's how we're running) before spawning, so
|
|
36
|
+
// the fresh daemon runs the latest code. startDaemon does delete+start, so a
|
|
37
|
+
// re-launch replaces any live daemon with the updated one. Non-fatal.
|
|
38
|
+
await selfUpdate();
|
|
39
|
+
|
|
34
40
|
const label = opts.name ?? opts.dir.split("/").pop() ?? opts.host;
|
|
35
41
|
const name = daemonName(label);
|
|
36
42
|
const command = buildForegroundCommand(opts);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Best-effort self-update, run right before a daemon is (re)spawned (see the top
|
|
6
|
+
// of launchServeDaemon). Re-launching `setup` or `serve -d` replaces the managed
|
|
7
|
+
// daemon (oxmgr delete + start), so upgrading the global package here means the
|
|
8
|
+
// fresh daemon process runs the new code — there's no in-place restart, so this
|
|
9
|
+
// never trips oxmgr's `on-failure` policy and never drops a live session
|
|
10
|
+
// mid-flight. A days-old daemon only updates on the next launcher run.
|
|
11
|
+
|
|
12
|
+
const REGISTRY_LATEST = "https://registry.npmjs.org/codehost/latest";
|
|
13
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
14
|
+
const INSTALL_TIMEOUT_MS = 120_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upgrade the global `codehost` to the latest published version if we're running
|
|
18
|
+
* from a real global install (`bun add -g` / `npm i -g`). A dev checkout or a
|
|
19
|
+
* `bunx` run is left untouched so we never clobber the user's global copy.
|
|
20
|
+
*
|
|
21
|
+
* Fully non-fatal: an offline registry, a slow network, or a failed `bun add`
|
|
22
|
+
* must never stop the server from launching — every failure path just logs and
|
|
23
|
+
* returns. Disable entirely with CODEHOST_NO_SELF_UPDATE=1.
|
|
24
|
+
*/
|
|
25
|
+
export async function selfUpdate(): Promise<void> {
|
|
26
|
+
if (process.env.CODEHOST_NO_SELF_UPDATE === "1") return;
|
|
27
|
+
try {
|
|
28
|
+
if (!isGlobalInstall()) return; // dev checkout or bunx: don't touch the global
|
|
29
|
+
|
|
30
|
+
const installed = currentVersion();
|
|
31
|
+
if (!installed) return; // can't locate our own package.json — don't risk it
|
|
32
|
+
|
|
33
|
+
const latest = await fetchLatest();
|
|
34
|
+
if (!latest || latest === installed) return;
|
|
35
|
+
|
|
36
|
+
console.log(`[codehost] updating codehost ${installed} → ${latest}…`);
|
|
37
|
+
const r = spawnSync(process.execPath, ["add", "-g", `codehost@${latest}`], {
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
40
|
+
});
|
|
41
|
+
if (r.status === 0) {
|
|
42
|
+
console.log(`[codehost] updated to ${latest}; the new daemon will run it.`);
|
|
43
|
+
} else {
|
|
44
|
+
console.warn(`[codehost] self-update to ${latest} failed; continuing on ${installed}.`);
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.warn(`[codehost] self-update skipped: ${(err as Error).message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Version from our own package.json (src/cli → package root is two up). */
|
|
52
|
+
function currentVersion(): string | null {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(
|
|
55
|
+
readFileSync(join(import.meta.dir, "..", "..", "package.json"), "utf8"),
|
|
56
|
+
) as { version?: unknown };
|
|
57
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True only when this file lives in a well-known *global* package directory we
|
|
65
|
+
* own. Conservative on purpose: anything unrecognized (dev repo, bunx cache,
|
|
66
|
+
* unusual layout) returns false so we skip rather than risk overwriting the
|
|
67
|
+
* wrong tree.
|
|
68
|
+
*/
|
|
69
|
+
function isGlobalInstall(): boolean {
|
|
70
|
+
const dir = import.meta.dir.replace(/\\/g, "/");
|
|
71
|
+
return (
|
|
72
|
+
dir.includes("/.bun/install/global/node_modules/codehost/") || // bun add -g
|
|
73
|
+
dir.includes("/lib/node_modules/codehost/") // npm i -g (unix default prefix)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Latest published version per the npm registry, or null on any failure. */
|
|
78
|
+
async function fetchLatest(): Promise<string | null> {
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(REGISTRY_LATEST, {
|
|
81
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) return null;
|
|
84
|
+
const data = (await res.json()) as { version?: unknown };
|
|
85
|
+
return typeof data.version === "string" ? data.version : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { toPosixPath } from "./repo";
|
|
3
|
+
|
|
4
|
+
describe("toPosixPath", () => {
|
|
5
|
+
test("Windows drive path -> POSIX drive form", () => {
|
|
6
|
+
expect(toPosixPath("C:\\ws")).toBe("/c/ws");
|
|
7
|
+
expect(toPosixPath("C:\\Users\\x")).toBe("/c/Users/x");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("lowercases the drive letter", () => {
|
|
11
|
+
expect(toPosixPath("D:\\foo")).toBe("/d/foo");
|
|
12
|
+
expect(toPosixPath("c:\\ws")).toBe("/c/ws");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("drive root collapses to /<letter> (no trailing slash)", () => {
|
|
16
|
+
expect(toPosixPath("C:\\")).toBe("/c");
|
|
17
|
+
expect(toPosixPath("C:")).toBe("/c");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("forward-slash Windows paths normalize too", () => {
|
|
21
|
+
expect(toPosixPath("C:/ws")).toBe("/c/ws");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("POSIX absolute paths are unchanged (mac/linux not broken)", () => {
|
|
25
|
+
expect(toPosixPath("/Users/sno/ws")).toBe("/Users/sno/ws");
|
|
26
|
+
expect(toPosixPath("/home/x/proj")).toBe("/home/x/proj");
|
|
27
|
+
expect(toPosixPath("/")).toBe("/");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("already-normalized POSIX-drive path is a no-op", () => {
|
|
31
|
+
expect(toPosixPath("/c/ws")).toBe("/c/ws");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("trims trailing backslashes/slashes on a drive path", () => {
|
|
35
|
+
expect(toPosixPath("C:\\ws\\")).toBe("/c/ws");
|
|
36
|
+
});
|
|
37
|
+
});
|
package/src/shared/repo.ts
CHANGED
|
@@ -52,6 +52,25 @@ export function repoKey(t: Pick<RepoTarget, "owner" | "name">): string {
|
|
|
52
52
|
return `gh/${t.owner}/${t.name}`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Normalize a served workspace path to the POSIX-drive form the browser side
|
|
57
|
+
* and VS Code web expect. A Windows drive path becomes a `/c/...` style path
|
|
58
|
+
* (lowercased drive, backslashes -> slashes): `C:\ws` -> `/c/ws`,
|
|
59
|
+
* `C:\Users\x` -> `/c/Users/x`, `D:\` -> `/d`. POSIX absolute paths (mac/linux)
|
|
60
|
+
* are returned unchanged. Used for `PeerMeta.cwd`, which feeds the `?folder=`
|
|
61
|
+
* URI — the real OS path is still used for the local VS Code working dir.
|
|
62
|
+
*/
|
|
63
|
+
export function toPosixPath(p: string): string {
|
|
64
|
+
const drive = /^([A-Za-z]):(?:[\\/](.*))?$/.exec(p);
|
|
65
|
+
if (drive) {
|
|
66
|
+
const letter = drive[1].toLowerCase();
|
|
67
|
+
const rest = (drive[2] ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
68
|
+
return rest ? `/${letter}/${rest}` : `/${letter}`;
|
|
69
|
+
}
|
|
70
|
+
// Already POSIX (or a relative path): just unify any stray backslashes.
|
|
71
|
+
return p.replace(/\\/g, "/");
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
/** Fill a layout template from a repo target (default branch -> "main"). */
|
|
56
75
|
export function fillLayout(layout: string, t: RepoTarget): string {
|
|
57
76
|
return layout
|