codex-webstrapper 0.1.0 → 0.1.5

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/README.md CHANGED
@@ -75,6 +75,8 @@ This project provides near-parity by emulation/bridging, not by removing those d
75
75
 
76
76
  ```bash
77
77
  codex-webstrap [--port <n>] [--bind <ip>] [--open] [--token-file <path>] [--codex-app <path>]
78
+ codex-webstrapper [--port <n>] [--bind <ip>] [--open] [--token-file <path>] [--codex-app <path>]
79
+ codex-webstrapper open [--port <n>] [--bind <ip>] [--token-file <path>] [--copy]
78
80
  ```
79
81
 
80
82
  ### Environment Overrides
@@ -112,7 +114,12 @@ codex-webstrap [--port <n>] [--bind <ip>] [--open] [--token-file <path>] [--code
112
114
  - macOS
113
115
  - Node.js 20+
114
116
  - Installed Codex app bundle at `/Applications/Codex.app` (or pass `--codex-app`)
115
- - `codex` CLI available in `PATH` (or set `CODEX_CLI_PATH`)
117
+
118
+ By default, webstrapper runs the app-server via the bundled desktop CLI at:
119
+ - `/Applications/Codex.app/Contents/Resources/codex`
120
+
121
+ Optional override:
122
+ - `CODEX_CLI_PATH=/custom/codex`
116
123
 
117
124
  ### Install
118
125
 
@@ -131,7 +138,7 @@ npm install -g codex-webstrapper
131
138
  With global install:
132
139
 
133
140
  ```bash
134
- codex-webstrap --port 8080 --bind 127.0.0.1
141
+ codex-webstrapper --port 8080 --bind 127.0.0.1
135
142
  ```
136
143
 
137
144
  From local checkout:
@@ -143,7 +150,19 @@ From local checkout:
143
150
  Optional auto-open:
144
151
 
145
152
  ```bash
146
- codex-webstrap --open
153
+ codex-webstrapper --open
154
+ ```
155
+
156
+ Generate/open the full auth URL from your persisted token:
157
+
158
+ ```bash
159
+ codex-webstrapper open
160
+ ```
161
+
162
+ Copy the full auth URL (including token) to macOS clipboard:
163
+
164
+ ```bash
165
+ codex-webstrapper open --copy
147
166
  ```
148
167
 
149
168
  ## Authentication Model
@@ -155,6 +174,12 @@ codex-webstrap --open
155
174
  open "http://127.0.0.1:8080/__webstrapper/auth?token=$(cat ~/.codex-webstrap/token)"
156
175
  ```
157
176
 
177
+ Or use the helper command:
178
+
179
+ ```bash
180
+ codex-webstrapper open
181
+ ```
182
+
158
183
  3. Server sets `cw_session` cookie (`HttpOnly`, `SameSite=Lax`, scoped to `/`).
159
184
  4. UI and bridge endpoints require a valid session cookie.
160
185
 
@@ -232,7 +257,7 @@ Codex setup-script compatible command:
232
257
  - Codex app not found
233
258
  - Pass `--codex-app /path/to/Codex.app`.
234
259
  - `codex` CLI spawn failures
235
- - Ensure `codex` is on `PATH` or set `CODEX_CLI_PATH`.
260
+ - Ensure the bundled CLI exists in your Codex app install, or set `CODEX_CLI_PATH`.
236
261
 
237
262
  ## License
238
263
 
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ SOURCE_PATH="${BASH_SOURCE[0]}"
5
+ while [[ -L "$SOURCE_PATH" ]]; do
6
+ SOURCE_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
7
+ SOURCE_PATH="$(readlink "$SOURCE_PATH")"
8
+ [[ "$SOURCE_PATH" != /* ]] && SOURCE_PATH="$SOURCE_DIR/$SOURCE_PATH"
9
+ done
10
+
11
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
5
12
  ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
13
 
7
14
  PORT="${CODEX_WEBSTRAP_PORT:-8080}"
@@ -10,40 +17,89 @@ OPEN_FLAG="0"
10
17
  TOKEN_FILE="${CODEX_WEBSTRAP_TOKEN_FILE:-}"
11
18
  CODEX_APP="${CODEX_WEBSTRAP_CODEX_APP:-}"
12
19
  INTERNAL_WS_PORT="${CODEX_WEBSTRAP_INTERNAL_WS_PORT:-38080}"
20
+ COPY_FLAG="0"
21
+ COMMAND="serve"
22
+
23
+ DEFAULT_TOKEN_FILE="${HOME}/.codex-webstrap/token"
24
+ if [[ -z "$TOKEN_FILE" ]]; then
25
+ TOKEN_FILE="$DEFAULT_TOKEN_FILE"
26
+ fi
27
+
28
+ print_usage() {
29
+ cat <<USAGE
30
+ Usage:
31
+ $(basename "$0") [--port <n>] [--bind <ip>] [--open] [--token-file <path>] [--codex-app <path>]
32
+ $(basename "$0") open [--port <n>] [--bind <ip>] [--token-file <path>] [--copy]
33
+
34
+ Commands:
35
+ open Build the full auth URL and open it in the browser.
36
+
37
+ Options for open:
38
+ --copy Copy full auth URL to clipboard with pbcopy instead of launching browser.
39
+
40
+ Env overrides:
41
+ CODEX_WEBSTRAP_PORT
42
+ CODEX_WEBSTRAP_BIND
43
+ CODEX_WEBSTRAP_TOKEN_FILE
44
+ CODEX_WEBSTRAP_CODEX_APP
45
+ CODEX_WEBSTRAP_INTERNAL_WS_PORT
46
+ USAGE
47
+ }
48
+
49
+ require_value() {
50
+ local flag="$1"
51
+ local value="${2:-}"
52
+ if [[ -z "$value" ]]; then
53
+ echo "Missing value for ${flag}" >&2
54
+ exit 1
55
+ fi
56
+ }
57
+
58
+ if [[ $# -gt 0 ]]; then
59
+ case "$1" in
60
+ open)
61
+ COMMAND="open"
62
+ shift
63
+ ;;
64
+ --help|-h|help)
65
+ print_usage
66
+ exit 0
67
+ ;;
68
+ esac
69
+ fi
13
70
 
14
71
  while [[ $# -gt 0 ]]; do
15
72
  case "$1" in
16
73
  --port)
74
+ require_value "$1" "${2:-}"
17
75
  PORT="$2"
18
76
  shift 2
19
77
  ;;
20
78
  --bind)
79
+ require_value "$1" "${2:-}"
21
80
  BIND="$2"
22
81
  shift 2
23
82
  ;;
24
- --open)
25
- OPEN_FLAG="1"
26
- shift
27
- ;;
28
83
  --token-file)
84
+ require_value "$1" "${2:-}"
29
85
  TOKEN_FILE="$2"
30
86
  shift 2
31
87
  ;;
88
+ --copy)
89
+ COPY_FLAG="1"
90
+ shift
91
+ ;;
92
+ --open)
93
+ OPEN_FLAG="1"
94
+ shift
95
+ ;;
32
96
  --codex-app)
97
+ require_value "$1" "${2:-}"
33
98
  CODEX_APP="$2"
34
99
  shift 2
35
100
  ;;
36
- --help|-h)
37
- cat <<USAGE
38
- Usage: $(basename "$0") [--port <n>] [--bind <ip>] [--open] [--token-file <path>] [--codex-app <path>]
39
-
40
- Env overrides:
41
- CODEX_WEBSTRAP_PORT
42
- CODEX_WEBSTRAP_BIND
43
- CODEX_WEBSTRAP_TOKEN_FILE
44
- CODEX_WEBSTRAP_CODEX_APP
45
- CODEX_WEBSTRAP_INTERNAL_WS_PORT
46
- USAGE
101
+ --help|-h|help)
102
+ print_usage
47
103
  exit 0
48
104
  ;;
49
105
  *)
@@ -53,6 +109,42 @@ USAGE
53
109
  esac
54
110
  done
55
111
 
112
+ if [[ "$COMMAND" != "open" && "$COPY_FLAG" == "1" ]]; then
113
+ echo "--copy can only be used with the 'open' command" >&2
114
+ exit 1
115
+ fi
116
+
117
+ if [[ "$COMMAND" == "open" ]]; then
118
+ if [[ ! -f "$TOKEN_FILE" ]]; then
119
+ echo "Token file not found: $TOKEN_FILE" >&2
120
+ exit 1
121
+ fi
122
+
123
+ TOKEN="$(tr -d '\r\n' < "$TOKEN_FILE")"
124
+ if [[ -z "$TOKEN" ]]; then
125
+ echo "Token file is empty: $TOKEN_FILE" >&2
126
+ exit 1
127
+ fi
128
+
129
+ ENCODED_TOKEN="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$TOKEN")"
130
+ AUTH_URL="http://${BIND}:${PORT}/__webstrapper/auth?token=${ENCODED_TOKEN}"
131
+
132
+ if [[ "$COPY_FLAG" == "1" ]]; then
133
+ if ! command -v pbcopy >/dev/null 2>&1; then
134
+ echo "pbcopy not found in PATH" >&2
135
+ exit 1
136
+ fi
137
+ printf '%s' "$AUTH_URL" | pbcopy
138
+ printf 'Copied auth URL to clipboard.\n'
139
+ printf '%s\n' "$AUTH_URL"
140
+ else
141
+ open "$AUTH_URL"
142
+ printf 'Opened auth URL in browser.\n'
143
+ printf '%s\n' "$AUTH_URL"
144
+ fi
145
+ exit 0
146
+ fi
147
+
56
148
  export CODEX_WEBSTRAP_PORT="$PORT"
57
149
  export CODEX_WEBSTRAP_BIND="$BIND"
58
150
  export CODEX_WEBSTRAP_TOKEN_FILE="$TOKEN_FILE"
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "codex-webstrapper",
3
- "version": "0.1.0",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Web wrapper for Codex desktop assets with bridge + token auth",
6
6
  "license": "MIT",
7
7
  "bin": {
8
- "codex-webstrap": "./bin/codex-webstrap.sh"
8
+ "codex-webstrap": "./bin/codex-webstrap.sh",
9
+ "codex-webstrapper": "./bin/codex-webstrap.sh"
9
10
  },
10
11
  "files": [
11
12
  "bin/",
package/src/assets.mjs CHANGED
@@ -37,7 +37,8 @@ export function resolveCodexAppPaths(explicitCodexAppPath) {
37
37
  const resourcesPath = path.join(appPath, "Contents", "Resources");
38
38
  const asarPath = path.join(resourcesPath, "app.asar");
39
39
  const infoPlistPath = path.join(appPath, "Contents", "Info.plist");
40
- return { appPath, resourcesPath, asarPath, infoPlistPath };
40
+ const codexCliPath = path.join(resourcesPath, "codex");
41
+ return { appPath, resourcesPath, asarPath, infoPlistPath, codexCliPath };
41
42
  }
42
43
 
43
44
  export async function ensureCodexAppExists(paths) {
@@ -880,6 +880,7 @@ export class MessageRouter {
880
880
  }
881
881
 
882
882
  let payload = {};
883
+ let status = 200;
883
884
  switch (endpoint) {
884
885
  case "get-global-state": {
885
886
  const key = params?.key;
@@ -1018,7 +1019,7 @@ export class MessageRouter {
1018
1019
  };
1019
1020
  break;
1020
1021
  case "local-environments":
1021
- payload = [];
1022
+ payload = await this._resolveLocalEnvironments(params);
1022
1023
  break;
1023
1024
  case "has-custom-cli-executable":
1024
1025
  payload = { hasCustomCliExecutable: false };
@@ -1050,6 +1051,11 @@ export class MessageRouter {
1050
1051
  };
1051
1052
  break;
1052
1053
  }
1054
+ case "git-push": {
1055
+ payload = await this._handleGitPush(params);
1056
+ status = payload.ok ? 200 : 500;
1057
+ break;
1058
+ }
1053
1059
  case "git-merge-base": {
1054
1060
  const gitRoot = typeof params?.gitRoot === "string" && params.gitRoot.length > 0
1055
1061
  ? params.gitRoot
@@ -1115,7 +1121,7 @@ export class MessageRouter {
1115
1121
  this._sendFetchJson(ws, {
1116
1122
  requestId,
1117
1123
  url: message.url,
1118
- status: 200,
1124
+ status,
1119
1125
  payload
1120
1126
  });
1121
1127
  return true;
@@ -1333,6 +1339,55 @@ export class MessageRouter {
1333
1339
  };
1334
1340
  }
1335
1341
 
1342
+ async _handleGitPush(params) {
1343
+ const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
1344
+ ? params.cwd
1345
+ : process.cwd();
1346
+ const remote = typeof params?.remote === "string" && params.remote.trim().length > 0
1347
+ ? params.remote.trim()
1348
+ : null;
1349
+ const branch = typeof params?.branch === "string" && params.branch.trim().length > 0
1350
+ ? params.branch.trim()
1351
+ : null;
1352
+
1353
+ const args = ["-C", cwd, "push"];
1354
+ if (params?.force === true || params?.forceWithLease === true) {
1355
+ args.push("--force-with-lease");
1356
+ }
1357
+ if (params?.setUpstream === true) {
1358
+ args.push("--set-upstream");
1359
+ }
1360
+ if (remote) {
1361
+ args.push(remote);
1362
+ }
1363
+ if (branch) {
1364
+ args.push(branch);
1365
+ }
1366
+
1367
+ const result = await this._runCommand("git", args, {
1368
+ cwd,
1369
+ timeoutMs: 120_000,
1370
+ allowNonZero: true
1371
+ });
1372
+
1373
+ if (result.ok) {
1374
+ return {
1375
+ ok: true,
1376
+ code: result.code,
1377
+ stdout: result.stdout || "",
1378
+ stderr: result.stderr || ""
1379
+ };
1380
+ }
1381
+
1382
+ return {
1383
+ ok: false,
1384
+ code: result.code,
1385
+ error: result.error || result.stderr || "git push failed",
1386
+ stdout: result.stdout || "",
1387
+ stderr: result.stderr || ""
1388
+ };
1389
+ }
1390
+
1336
1391
  async _runCommand(command, args, { timeoutMs = 5_000, allowNonZero = false, cwd = process.cwd() } = {}) {
1337
1392
  return new Promise((resolve) => {
1338
1393
  const child = spawn(command, args, {
@@ -1471,6 +1526,128 @@ export class MessageRouter {
1471
1526
  return trimmed.replace(/\/+$/, "");
1472
1527
  }
1473
1528
 
1529
+ async _resolveLocalEnvironments(params) {
1530
+ const workspaceRoot = this._resolveLocalEnvironmentWorkspaceRoot(params?.workspaceRoot);
1531
+ if (!workspaceRoot) {
1532
+ return { environments: [] };
1533
+ }
1534
+
1535
+ const configDir = path.join(workspaceRoot, ".codex", "environments");
1536
+ let entries;
1537
+ try {
1538
+ entries = await fs.readdir(configDir, { withFileTypes: true });
1539
+ } catch {
1540
+ return { environments: [] };
1541
+ }
1542
+
1543
+ const configFiles = entries
1544
+ .filter((entry) => entry.isFile() && /^environment(?:-\d+)?\.toml$/i.test(entry.name))
1545
+ .map((entry) => entry.name)
1546
+ .sort((left, right) => {
1547
+ const leftIsDefault = left.toLowerCase() === "environment.toml";
1548
+ const rightIsDefault = right.toLowerCase() === "environment.toml";
1549
+ if (leftIsDefault && !rightIsDefault) {
1550
+ return -1;
1551
+ }
1552
+ if (!leftIsDefault && rightIsDefault) {
1553
+ return 1;
1554
+ }
1555
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
1556
+ });
1557
+
1558
+ if (configFiles.length === 0) {
1559
+ return { environments: [] };
1560
+ }
1561
+
1562
+ const environments = await Promise.all(configFiles.map(async (fileName) => {
1563
+ const configPath = path.join(configDir, fileName);
1564
+ try {
1565
+ const raw = await fs.readFile(configPath, "utf8");
1566
+ const environment = this._parseLocalEnvironmentConfig(raw, configPath);
1567
+ return {
1568
+ type: "success",
1569
+ configPath,
1570
+ environment
1571
+ };
1572
+ } catch (error) {
1573
+ return {
1574
+ type: "error",
1575
+ configPath,
1576
+ error: {
1577
+ message: toErrorMessage(error)
1578
+ }
1579
+ };
1580
+ }
1581
+ }));
1582
+
1583
+ return { environments };
1584
+ }
1585
+
1586
+ _resolveLocalEnvironmentWorkspaceRoot(root) {
1587
+ const normalized = this._normalizeWorkspaceRoot(root);
1588
+ if (normalized) {
1589
+ return path.resolve(normalized);
1590
+ }
1591
+
1592
+ const activeRoot = this._normalizeWorkspaceRoot(this.activeWorkspaceRoots?.[0]);
1593
+ if (activeRoot) {
1594
+ return path.resolve(activeRoot);
1595
+ }
1596
+
1597
+ return this.defaultWorkspaceRoot ? path.resolve(this.defaultWorkspaceRoot) : null;
1598
+ }
1599
+
1600
+ _parseLocalEnvironmentConfig(raw, configPath) {
1601
+ const name = this._parseTomlString(raw, "name") || path.basename(configPath, ".toml");
1602
+ const versionRaw = this._parseTomlNumber(raw, "version");
1603
+ const setupScript = this._parseTomlStringInSection(raw, "setup", "script") || "";
1604
+
1605
+ return {
1606
+ version: Number.isInteger(versionRaw) ? versionRaw : 1,
1607
+ name,
1608
+ setup: {
1609
+ script: setupScript
1610
+ },
1611
+ actions: []
1612
+ };
1613
+ }
1614
+
1615
+ _parseTomlString(raw, key) {
1616
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1617
+ const pattern = new RegExp(`^\\s*${escapedKey}\\s*=\\s*(?:"([^"]*)"|'([^']*)')\\s*$`, "m");
1618
+ const match = raw.match(pattern);
1619
+ if (!match) {
1620
+ return null;
1621
+ }
1622
+ return match[1] ?? match[2] ?? null;
1623
+ }
1624
+
1625
+ _parseTomlNumber(raw, key) {
1626
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1627
+ const pattern = new RegExp(`^\\s*${escapedKey}\\s*=\\s*(-?\\d+)\\s*$`, "m");
1628
+ const match = raw.match(pattern);
1629
+ if (!match) {
1630
+ return null;
1631
+ }
1632
+ const value = Number.parseInt(match[1], 10);
1633
+ return Number.isNaN(value) ? null : value;
1634
+ }
1635
+
1636
+ _parseTomlStringInSection(raw, sectionName, key) {
1637
+ const escapedSection = sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1638
+ const sectionPattern = new RegExp(`^\\s*\\[${escapedSection}\\]\\s*$`, "m");
1639
+ const sectionMatch = sectionPattern.exec(raw);
1640
+ if (!sectionMatch) {
1641
+ return null;
1642
+ }
1643
+
1644
+ const sectionStart = sectionMatch.index + sectionMatch[0].length;
1645
+ const rest = raw.slice(sectionStart);
1646
+ const nextSectionMatch = rest.match(/^\s*\[[^\]]+\]\s*$/m);
1647
+ const sectionBody = nextSectionMatch ? rest.slice(0, nextSectionMatch.index) : rest;
1648
+ return this._parseTomlString(sectionBody, key);
1649
+ }
1650
+
1474
1651
  _loadPersistedGlobalState() {
1475
1652
  if (!this.globalStatePath) {
1476
1653
  return;
package/src/server.mjs CHANGED
@@ -148,8 +148,20 @@ async function main() {
148
148
  });
149
149
  const patchedIndexHtml = await buildPatchedIndexHtml(assetBundle.indexPath);
150
150
 
151
+ let codexCliPath = process.env.CODEX_CLI_PATH || codexPaths.codexCliPath;
152
+ if (!process.env.CODEX_CLI_PATH) {
153
+ const bundledCliExists = await fs.access(codexPaths.codexCliPath).then(() => true).catch(() => false);
154
+ if (!bundledCliExists) {
155
+ codexCliPath = "codex";
156
+ logger.warn("Bundled Codex CLI not found, falling back to PATH", {
157
+ bundledCliPath: codexPaths.codexCliPath
158
+ });
159
+ }
160
+ }
161
+
151
162
  const appServer = new AppServerManager({
152
163
  internalPort: config.internalWsPort,
164
+ codexCliPath,
153
165
  logger: createLogger("app-server")
154
166
  });
155
167