codexpanel 0.0.1 → 0.1.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/README.md +61 -0
- package/bin/codexpanel.cjs +259 -0
- package/package.json +41 -6
- package/cli.js +0 -2
- package/codexh5/cli.js +0 -2
- package/codexh5/codexfish/cli.js +0 -2
- package/codexh5/codexfish/codexyes/cli.js +0 -2
- package/codexh5/codexfish/codexyes/package.json +0 -7
- package/codexh5/codexfish/package.json +0 -7
- package/codexh5/package.json +0 -7
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# codexpanel
|
|
2
|
+
|
|
3
|
+
codexpanel is a mobile control plane for managing local Codex work from a phone. It provides a Next.js web console, relay service, desktop agent, PostgreSQL-backed durable storage, shared protocol package, and reusable UI package in one monorepo.
|
|
4
|
+
|
|
5
|
+
## Workspace
|
|
6
|
+
|
|
7
|
+
- `apps/web`: Next.js phone console plus static admin, device setup, and project docs
|
|
8
|
+
- `apps/relay`: HTTP/SSE/WSS relay service, PostgreSQL/SQLite storage, and device onboarding scripts
|
|
9
|
+
- `apps/agent`: desktop-side agent entrypoint
|
|
10
|
+
- `packages/protocol`: shared schemas and transport helpers
|
|
11
|
+
- `packages/codex-app-server`: Codex app-server connector bindings
|
|
12
|
+
- `packages/ui`: shared React UI primitives
|
|
13
|
+
|
|
14
|
+
Human reference files, local databases, deployment bundles, server handoff notes, and old demo artifacts live outside the production tree in `files_for_human/` and are ignored by Git.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
npm run check
|
|
20
|
+
npm run build
|
|
21
|
+
npm run smoke:storage
|
|
22
|
+
npm run test:e2e
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Desktop agent install
|
|
26
|
+
|
|
27
|
+
The public npm entrypoint is the primary one-line installer for Windows desktop agents:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
npx -y codexpanel
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
By default it downloads the Windows bootstrap script from `https://jd.6a.gs/agent/bootstrap.ps1`, installs or updates the desktop agent under `%LOCALAPPDATA%\CodexPanelAgent`, and starts the agent against the production relay.
|
|
34
|
+
|
|
35
|
+
If the relay is protected, pass `--access-code <code>` or set `CODEXPANEL_ACCESS_CODE` before running the installer.
|
|
36
|
+
|
|
37
|
+
Use `--relay` for local or private relay verification:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
npx -y codexpanel --relay http://127.0.0.1:4871
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Useful options include `--user-id`, `--device-id`, `--device-name`, `--workspace`, `--access-code`, `--no-autostart`, `--no-tunnel`, `--dry-run`, and `--print-command`.
|
|
44
|
+
|
|
45
|
+
## Storage and deployment
|
|
46
|
+
|
|
47
|
+
The relay uses `CODEXPANEL_DATABASE_URL` or `DATABASE_URL` for PostgreSQL. If neither is set, it falls back to the local SQLite file at `CODEXPANEL_DB_FILE` for development only. Production startup rejects accidental SQLite fallback unless `CODEXPANEL_ALLOW_SQLITE_IN_PRODUCTION=true` is explicitly set for an emergency.
|
|
48
|
+
|
|
49
|
+
`npm run smoke:storage` always verifies the SQLite development fallback and the production guard. To verify the PostgreSQL path, set `CODEXPANEL_STORAGE_SMOKE_DATABASE_URL` to a disposable PostgreSQL database URL before running it; the smoke test rewrites its storage tables.
|
|
50
|
+
|
|
51
|
+
`docker-compose.yml` starts PostgreSQL and the relay together. The auto-deploy script pulls the configured branch, rebuilds Docker Compose, and waits for `/api/health` before declaring the update complete.
|
|
52
|
+
|
|
53
|
+
## Architecture guardrails
|
|
54
|
+
|
|
55
|
+
Read [docs/CTO_ARCHITECTURE_DIRECTIVE.md](docs/CTO_ARCHITECTURE_DIRECTIVE.md) before changing Next.js, relay, storage, deployment, auth, protocol, or agent behavior.
|
|
56
|
+
|
|
57
|
+
- API compatibility is tracked in [docs/api-compatibility-matrix.md](docs/api-compatibility-matrix.md).
|
|
58
|
+
- `npm run check` runs `npm run scan:encoding` before workspace checks.
|
|
59
|
+
- Storage code lives under `apps/relay/src/storage/`; UI code must not import backend or database modules.
|
|
60
|
+
|
|
61
|
+
See [docs/git-workflow.md](docs/git-workflow.md) for the day-to-day Git and GitHub workflow.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const http = require("http");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const VERSION = "0.1.0";
|
|
12
|
+
const DEFAULT_RELAY_URL = "https://jd.6a.gs";
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
return `
|
|
16
|
+
CodexPanel desktop agent installer
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
npx -y codexpanel [install] [options]
|
|
20
|
+
npx -y codexpanel --relay http://127.0.0.1:4871
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--relay, --url <url> Relay URL. Defaults to CODEXPANEL_RELAY_URL or ${DEFAULT_RELAY_URL}
|
|
24
|
+
--user, --user-id <id> CodexPanel user id. Defaults to CODEXPANEL_USER_ID or demo_user_a
|
|
25
|
+
--device, --device-id <id> Stable device id. Defaults to the relay bootstrap script's generated id
|
|
26
|
+
--name, --device-name <name> Device display name
|
|
27
|
+
--workspace, --cwd <path> Default workspace for Codex Desktop/app-server
|
|
28
|
+
--access-code <code> Access code for protected relay deployments
|
|
29
|
+
--mode <mode> Agent mode. Defaults to desktop-agent
|
|
30
|
+
--app-server-listen <url> Local Codex app-server WebSocket URL
|
|
31
|
+
--no-autostart Do not enable Windows login autostart
|
|
32
|
+
--no-tunnel Disable Cloudflare WSS direct tunnel
|
|
33
|
+
--print-command Print the equivalent PowerShell bootstrap command
|
|
34
|
+
--dry-run Print resolved installer details without running PowerShell
|
|
35
|
+
-h, --help Show help
|
|
36
|
+
-v, --version Show version
|
|
37
|
+
`.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const args = [...argv];
|
|
42
|
+
const options = {
|
|
43
|
+
command: "install",
|
|
44
|
+
relayUrl: process.env.CODEXPANEL_RELAY_URL || process.env.BRIDGECODEX_RELAY_URL || DEFAULT_RELAY_URL,
|
|
45
|
+
userId: process.env.CODEXPANEL_USER_ID || process.env.BRIDGECODEX_USER_ID || "demo_user_a",
|
|
46
|
+
deviceId: process.env.CODEXPANEL_DEVICE_ID || process.env.BRIDGECODEX_DEVICE_ID || "",
|
|
47
|
+
deviceName: process.env.CODEXPANEL_DEVICE_NAME || process.env.BRIDGECODEX_DEVICE_NAME || "",
|
|
48
|
+
workspace: process.env.CODEXPANEL_WORKSPACE || process.env.BRIDGECODEX_WORKSPACE || "",
|
|
49
|
+
accessCode: process.env.CODEXPANEL_ACCESS_CODE || process.env.BRIDGECODEX_ACCESS_CODE || "",
|
|
50
|
+
mode: process.env.CODEXPANEL_AGENT_MODE || process.env.BRIDGECODEX_AGENT_MODE || "desktop-agent",
|
|
51
|
+
appServerListen: process.env.CODEX_APP_SERVER_LISTEN || "",
|
|
52
|
+
autoStart: true,
|
|
53
|
+
tunnelEnabled: true,
|
|
54
|
+
dryRun: false,
|
|
55
|
+
printCommand: false,
|
|
56
|
+
keepScript: false,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const readValue = (flag) => {
|
|
60
|
+
const value = args.shift();
|
|
61
|
+
if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
while (args.length) {
|
|
66
|
+
const arg = args.shift();
|
|
67
|
+
if (!arg) continue;
|
|
68
|
+
if (arg === "install" || arg === "setup") {
|
|
69
|
+
options.command = "install";
|
|
70
|
+
} else if (arg === "help") {
|
|
71
|
+
options.help = true;
|
|
72
|
+
} else if (arg === "version") {
|
|
73
|
+
options.version = true;
|
|
74
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
75
|
+
options.help = true;
|
|
76
|
+
} else if (arg === "-v" || arg === "--version") {
|
|
77
|
+
options.version = true;
|
|
78
|
+
} else if (arg === "--relay" || arg === "--url") {
|
|
79
|
+
options.relayUrl = readValue(arg);
|
|
80
|
+
} else if (arg === "--user" || arg === "--user-id") {
|
|
81
|
+
options.userId = readValue(arg);
|
|
82
|
+
} else if (arg === "--device" || arg === "--device-id") {
|
|
83
|
+
options.deviceId = readValue(arg);
|
|
84
|
+
} else if (arg === "--name" || arg === "--device-name") {
|
|
85
|
+
options.deviceName = readValue(arg);
|
|
86
|
+
} else if (arg === "--workspace" || arg === "--cwd") {
|
|
87
|
+
options.workspace = readValue(arg);
|
|
88
|
+
} else if (arg === "--access-code") {
|
|
89
|
+
options.accessCode = readValue(arg);
|
|
90
|
+
} else if (arg === "--mode") {
|
|
91
|
+
options.mode = readValue(arg);
|
|
92
|
+
} else if (arg === "--app-server-listen") {
|
|
93
|
+
options.appServerListen = readValue(arg);
|
|
94
|
+
} else if (arg === "--no-autostart") {
|
|
95
|
+
options.autoStart = false;
|
|
96
|
+
} else if (arg === "--no-tunnel") {
|
|
97
|
+
options.tunnelEnabled = false;
|
|
98
|
+
} else if (arg === "--print-command") {
|
|
99
|
+
options.printCommand = true;
|
|
100
|
+
} else if (arg === "--dry-run") {
|
|
101
|
+
options.dryRun = true;
|
|
102
|
+
} else if (arg === "--keep-script") {
|
|
103
|
+
options.keepScript = true;
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return options;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeRelayUrl(value) {
|
|
113
|
+
const raw = String(value || "").trim().replace(/\/+$/, "");
|
|
114
|
+
if (!raw) throw new Error("Relay URL is empty");
|
|
115
|
+
const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
116
|
+
const url = new URL(withProtocol);
|
|
117
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
118
|
+
throw new Error(`Relay URL must use http or https: ${withProtocol}`);
|
|
119
|
+
}
|
|
120
|
+
return url.toString().replace(/\/+$/, "");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bootstrapUrl(options) {
|
|
124
|
+
const url = new URL("/agent/bootstrap.ps1", normalizeRelayUrl(options.relayUrl));
|
|
125
|
+
url.searchParams.set("userId", options.userId);
|
|
126
|
+
if (options.deviceId) url.searchParams.set("deviceId", options.deviceId);
|
|
127
|
+
if (options.deviceName) url.searchParams.set("deviceName", options.deviceName);
|
|
128
|
+
if (options.workspace) url.searchParams.set("workspace", options.workspace);
|
|
129
|
+
if (options.accessCode) url.searchParams.set("accessCode", options.accessCode);
|
|
130
|
+
if (options.mode) url.searchParams.set("mode", options.mode);
|
|
131
|
+
if (options.appServerListen) url.searchParams.set("appServerListen", options.appServerListen);
|
|
132
|
+
url.searchParams.set("autoStart", options.autoStart ? "1" : "0");
|
|
133
|
+
url.searchParams.set("tunnelEnabled", options.tunnelEnabled ? "1" : "0");
|
|
134
|
+
return url;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function downloadText(url, redirects = 0) {
|
|
138
|
+
if (redirects > 5) return Promise.reject(new Error(`Too many redirects for ${url}`));
|
|
139
|
+
const client = url.protocol === "https:" ? https : http;
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const req = client.get(url, {
|
|
142
|
+
headers: {
|
|
143
|
+
"user-agent": `codexpanel-npm/${VERSION}`,
|
|
144
|
+
"accept": "text/plain, */*",
|
|
145
|
+
},
|
|
146
|
+
timeout: 30000,
|
|
147
|
+
}, (res) => {
|
|
148
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode || 0) && res.headers.location) {
|
|
149
|
+
res.resume();
|
|
150
|
+
const next = new URL(res.headers.location, url);
|
|
151
|
+
downloadText(next, redirects + 1).then(resolve, reject);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let body = "";
|
|
155
|
+
res.setEncoding("utf8");
|
|
156
|
+
res.on("data", (chunk) => body += chunk);
|
|
157
|
+
res.on("end", () => {
|
|
158
|
+
if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) {
|
|
159
|
+
const authHint = res.statusCode === 401 || res.statusCode === 403
|
|
160
|
+
? " The relay requires an access code; re-run with --access-code <code> or set CODEXPANEL_ACCESS_CODE."
|
|
161
|
+
: "";
|
|
162
|
+
reject(new Error(`HTTP ${res.statusCode} while downloading ${url}.${authHint}`));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
resolve(body.replace(/^\uFEFF/, ""));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
req.on("timeout", () => req.destroy(new Error(`Timeout downloading ${url}`)));
|
|
169
|
+
req.on("error", reject);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function powershellPath() {
|
|
174
|
+
if (process.platform !== "win32") return "pwsh";
|
|
175
|
+
const candidates = ["pwsh.exe", "powershell.exe"];
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
const result = spawnSync("where.exe", [candidate], { encoding: "utf8", windowsHide: true });
|
|
178
|
+
const first = (result.stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0];
|
|
179
|
+
if (first) return first;
|
|
180
|
+
}
|
|
181
|
+
return "powershell.exe";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function powershellCommand(url) {
|
|
185
|
+
const escaped = String(url).replace(/'/g, "''");
|
|
186
|
+
return `powershell -NoProfile -ExecutionPolicy Bypass -Command "irm '${escaped}' | iex"`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function install(options) {
|
|
190
|
+
const url = bootstrapUrl(options);
|
|
191
|
+
if (options.printCommand) console.log(powershellCommand(url));
|
|
192
|
+
if (options.dryRun) {
|
|
193
|
+
console.log(JSON.stringify({
|
|
194
|
+
ok: true,
|
|
195
|
+
platform: process.platform,
|
|
196
|
+
relayUrl: normalizeRelayUrl(options.relayUrl),
|
|
197
|
+
bootstrapUrl: String(url),
|
|
198
|
+
userId: options.userId,
|
|
199
|
+
mode: options.mode,
|
|
200
|
+
autoStart: options.autoStart,
|
|
201
|
+
tunnelEnabled: options.tunnelEnabled,
|
|
202
|
+
}, null, 2));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (process.platform !== "win32") {
|
|
207
|
+
throw new Error("CodexPanel desktop agent installation currently supports Windows. Use --print-command on Windows, or open the relay setup page for this platform.");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(`CodexPanel: downloading installer from ${normalizeRelayUrl(options.relayUrl)}`);
|
|
211
|
+
const script = await downloadText(url);
|
|
212
|
+
if (!/CodexPanel/i.test(script) || !/desktop-agent/i.test(script)) {
|
|
213
|
+
throw new Error("Downloaded bootstrap script did not look like the CodexPanel Windows installer.");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tmp = path.join(os.tmpdir(), `codexpanel-bootstrap-${Date.now()}-${Math.random().toString(16).slice(2)}.ps1`);
|
|
217
|
+
fs.writeFileSync(tmp, Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), Buffer.from(script, "utf8")]));
|
|
218
|
+
|
|
219
|
+
console.log("CodexPanel: starting Windows installer");
|
|
220
|
+
const result = spawnSync(powershellPath(), [
|
|
221
|
+
"-NoLogo",
|
|
222
|
+
"-NoProfile",
|
|
223
|
+
"-ExecutionPolicy",
|
|
224
|
+
"Bypass",
|
|
225
|
+
"-File",
|
|
226
|
+
tmp,
|
|
227
|
+
], {
|
|
228
|
+
stdio: "inherit",
|
|
229
|
+
windowsHide: false,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!options.keepScript) {
|
|
233
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
234
|
+
} else {
|
|
235
|
+
console.log(`CodexPanel: kept bootstrap script at ${tmp}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (result.error) throw result.error;
|
|
239
|
+
if (result.status !== 0) throw new Error(`PowerShell installer exited with code ${result.status}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function main() {
|
|
243
|
+
const options = parseArgs(process.argv.slice(2));
|
|
244
|
+
if (options.version) {
|
|
245
|
+
console.log(VERSION);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (options.help) {
|
|
249
|
+
console.log(usage());
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (options.command !== "install") throw new Error(`Unknown command: ${options.command}`);
|
|
253
|
+
await install(options);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
main().catch((error) => {
|
|
257
|
+
console.error(`codexpanel: ${error.message}`);
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
2
|
"name": "codexpanel",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CodexPanel mobile control plane monorepo.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"codexpanel": "bin/codexpanel.cjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18.0.0"
|
|
19
|
+
},
|
|
20
|
+
"workspaces": [
|
|
21
|
+
"apps/*",
|
|
22
|
+
"packages/*"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev:web": "npm run dev -w @codexpanel/web",
|
|
26
|
+
"build": "npm run build --workspaces --if-present",
|
|
27
|
+
"check": "npm run scan:encoding && npm run verify:api-contracts && npm run verify:storage && npm run check --workspaces --if-present",
|
|
28
|
+
"test:e2e": "playwright test",
|
|
29
|
+
"scan:encoding": "node scripts/scan-encoding.cjs",
|
|
30
|
+
"verify:api-contracts": "node scripts/verify-api-contracts.mjs",
|
|
31
|
+
"generate:codex": "npm run generate -w @codexpanel/codex-app-server",
|
|
32
|
+
"verify:storage": "node scripts/verify-storage-boundary.mjs",
|
|
33
|
+
"smoke:storage": "node scripts/storage-smoke.cjs",
|
|
34
|
+
"purge:legacy-plaintext": "node scripts/purge-legacy-plaintext-cache.mjs"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@playwright/test": "1.52.0",
|
|
38
|
+
"@types/node": "^24.0.0",
|
|
39
|
+
"tsx": "^4.19.4",
|
|
40
|
+
"typescript": "^5.8.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/cli.js
DELETED
package/codexh5/cli.js
DELETED
package/codexh5/codexfish/cli.js
DELETED