codex-snapshots 0.1.0 → 0.1.1
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 +101 -6
- package/bin/codex-snapshot.mjs +1 -6326
- package/deploy/aliyun/README.md +311 -0
- package/deploy/aliyun/backup-share-data.sh +109 -0
- package/deploy/aliyun/check-ecs-status.sh +149 -0
- package/deploy/aliyun/codex-snapshot-share.env.example +29 -0
- package/deploy/aliyun/codex-snapshot-share.service +26 -0
- package/deploy/aliyun/configure-github-pages-api.sh +141 -0
- package/deploy/aliyun/configure-local-publisher.sh +197 -0
- package/deploy/aliyun/deploy-to-ecs.sh +669 -0
- package/deploy/aliyun/deploy.env.example +52 -0
- package/deploy/aliyun/doctor.mjs +398 -0
- package/deploy/aliyun/install-share-api.sh +252 -0
- package/deploy/aliyun/install-system-deps.sh +84 -0
- package/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf +34 -0
- package/deploy/aliyun/nginx-codex-snapshots.conf +52 -0
- package/deploy/aliyun/preflight.mjs +321 -0
- package/deploy/aliyun/restore-share-data.sh +141 -0
- package/deploy/aliyun/verify-public-share.mjs +404 -0
- package/dist/cli/codex-snapshot.mjs +2654 -0
- package/dist/core/privacy.js +81 -0
- package/dist/core/snapshot.js +1 -0
- package/dist/renderers/markdown.mjs +81 -0
- package/dist/renderers/transcript.js +195 -0
- package/dist/server/http.js +10 -0
- package/dist/server/local-security.js +66 -0
- package/dist/server/local-viewer-app.mjs +1670 -0
- package/dist/server/local-viewer.mjs +210 -0
- package/dist/server/share-api.mjs +1149 -0
- package/dist/server/share-store.js +136 -0
- package/dist/shared/sanitize.js +126 -0
- package/dist/shared/transcript.js +1 -0
- package/dist/sources/index.mjs +2 -0
- package/dist/sources/local-history.mjs +2221 -0
- package/package.json +42 -14
- package/scripts/build-site.mjs +71 -0
- package/scripts/launch-agent.mjs +19 -227
- package/scripts/serve-site.mjs +2 -2
- package/scripts/test-aliyun-deploy-config.sh +230 -0
- package/scripts/test-share-api.mjs +967 -0
- package/scripts/test-site-config.mjs +100 -0
- package/scripts/test-static-site.mjs +403 -0
- package/scripts/write-site-config.mjs +161 -0
- package/server/share-api.mjs +1 -771
- package/site/assets/config.js +3 -0
- package/site/assets/share.js +43 -106
- package/site/assets/site.css +3 -605
- package/site/assets/site.js +15 -92
- package/site/favicon.svg +7 -0
- package/site/index.html +3 -83
- package/site/share/index.html +3 -8
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-snapshots",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Local-first read-only snapshots for Codex, Claude Code, and Trae sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.20.0",
|
|
7
7
|
"bin": {
|
|
8
|
-
"codex-snapshot": "
|
|
9
|
-
"codex-snapshots": "
|
|
10
|
-
"codex-snapshot-share": "server/share-api.mjs",
|
|
8
|
+
"codex-snapshot": "dist/cli/codex-snapshot.mjs",
|
|
9
|
+
"codex-snapshots": "dist/cli/codex-snapshot.mjs",
|
|
10
|
+
"codex-snapshot-share": "dist/server/share-api.mjs",
|
|
11
11
|
"codex-snapshots-site": "scripts/serve-site.mjs"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"author": "ffffhx",
|
|
31
31
|
"files": [
|
|
32
32
|
"bin",
|
|
33
|
+
"deploy",
|
|
34
|
+
"dist",
|
|
33
35
|
"server",
|
|
34
36
|
"scripts",
|
|
35
37
|
"site",
|
|
@@ -41,16 +43,27 @@
|
|
|
41
43
|
"registry": "https://registry.npmjs.org/"
|
|
42
44
|
},
|
|
43
45
|
"scripts": {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"snapshot:
|
|
49
|
-
"snapshot:daemon:
|
|
50
|
-
"snapshot:daemon:
|
|
46
|
+
"build": "pnpm build:dist && pnpm build:site",
|
|
47
|
+
"build:dist": "tsc -p tsconfig.build.json && chmod +x dist/cli/codex-snapshot.mjs dist/server/share-api.mjs",
|
|
48
|
+
"build:site": "node scripts/build-site.mjs",
|
|
49
|
+
"dev": "pnpm build:dist && node dist/cli/codex-snapshot.mjs serve --port 4321",
|
|
50
|
+
"snapshot": "pnpm build:dist && node dist/cli/codex-snapshot.mjs",
|
|
51
|
+
"snapshot:daemon": "pnpm build:dist && node dist/cli/codex-snapshot.mjs serve --port 4321",
|
|
52
|
+
"snapshot:install-daemon": "pnpm build:dist && node scripts/launch-agent.mjs install",
|
|
53
|
+
"snapshot:uninstall-daemon": "pnpm build:dist && node scripts/launch-agent.mjs uninstall",
|
|
54
|
+
"snapshot:daemon:status": "pnpm build:dist && node scripts/launch-agent.mjs status",
|
|
55
|
+
"snapshot:daemon:logs": "pnpm build:dist && node scripts/launch-agent.mjs logs",
|
|
51
56
|
"site:dev": "node scripts/serve-site.mjs",
|
|
52
|
-
"share:server": "node server/share-api.mjs",
|
|
53
|
-
"
|
|
57
|
+
"share:server": "pnpm build:dist && node dist/server/share-api.mjs",
|
|
58
|
+
"prepack": "pnpm build",
|
|
59
|
+
"test:generated": "pnpm build && git diff --exit-code -- site dist",
|
|
60
|
+
"test:smoke": "pnpm build:dist && node dist/cli/codex-snapshot.mjs --help && node dist/cli/codex-snapshot.mjs daemon --help && node dist/server/share-api.mjs --help && node scripts/serve-site.mjs --help && node --check bin/codex-snapshot.mjs && node --check server/share-api.mjs && node --check dist/cli/codex-snapshot.mjs && node --check dist/server/share-api.mjs && node --check scripts/build-site.mjs && node --check scripts/launch-agent.mjs && node --check scripts/serve-site.mjs && node --check scripts/test-share-api.mjs && node --check scripts/test-static-site.mjs && node --check scripts/write-site-config.mjs && node --check scripts/test-site-config.mjs && node --check deploy/aliyun/doctor.mjs && node --check deploy/aliyun/preflight.mjs && node --check deploy/aliyun/verify-public-share.mjs && bash -n deploy/aliyun/*.sh scripts/test-aliyun-deploy-config.sh",
|
|
61
|
+
"test:share-api": "pnpm build:dist && node scripts/test-share-api.mjs",
|
|
62
|
+
"test:static-site": "pnpm build:site && node scripts/test-static-site.mjs",
|
|
63
|
+
"test:site-config": "node scripts/test-site-config.mjs",
|
|
64
|
+
"test:deploy-config": "bash scripts/test-aliyun-deploy-config.sh",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"test": "pnpm typecheck && pnpm test:smoke && pnpm test:share-api && pnpm test:static-site && pnpm test:site-config && pnpm test:deploy-config"
|
|
54
67
|
},
|
|
55
68
|
"engines": {
|
|
56
69
|
"node": ">=18"
|
|
@@ -58,6 +71,21 @@
|
|
|
58
71
|
"license": "MIT",
|
|
59
72
|
"dependencies": {
|
|
60
73
|
"highlight.js": "^11.11.1",
|
|
61
|
-
"markdown-it": "^14.1.1"
|
|
74
|
+
"markdown-it": "^14.1.1",
|
|
75
|
+
"react": "^19.2.6",
|
|
76
|
+
"react-dom": "^19.2.6",
|
|
77
|
+
"sanitize-html": "^2.17.4"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
81
|
+
"@types/node": "^25.9.1",
|
|
82
|
+
"@types/react": "^19.2.15",
|
|
83
|
+
"@types/react-dom": "^19.2.3",
|
|
84
|
+
"@types/sanitize-html": "^2.16.1",
|
|
85
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
86
|
+
"jsdom": "^29.1.1",
|
|
87
|
+
"tailwindcss": "^4.3.0",
|
|
88
|
+
"typescript": "^6.0.3",
|
|
89
|
+
"vite": "^8.0.14"
|
|
62
90
|
}
|
|
63
91
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
7
|
+
import react from "@vitejs/plugin-react";
|
|
8
|
+
import { build } from "vite";
|
|
9
|
+
|
|
10
|
+
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const OUT_DIR = path.join(ROOT_DIR, "site");
|
|
12
|
+
const ASSETS_DIR = path.join(OUT_DIR, "assets");
|
|
13
|
+
|
|
14
|
+
const entries = [
|
|
15
|
+
{
|
|
16
|
+
entry: path.join(ROOT_DIR, "src/site/main.tsx"),
|
|
17
|
+
fileName: "site",
|
|
18
|
+
globalName: "CodexSnapshotsSite",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
entry: path.join(ROOT_DIR, "src/site/share.tsx"),
|
|
22
|
+
fileName: "share",
|
|
23
|
+
globalName: "CodexSnapshotsShare",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
await Promise.all([
|
|
28
|
+
rm(path.join(ASSETS_DIR, "site.js"), { force: true }),
|
|
29
|
+
rm(path.join(ASSETS_DIR, "share.js"), { force: true }),
|
|
30
|
+
rm(path.join(ASSETS_DIR, "site.css"), { force: true }),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
for (const item of entries) {
|
|
34
|
+
await build({
|
|
35
|
+
root: ROOT_DIR,
|
|
36
|
+
configFile: false,
|
|
37
|
+
define: {
|
|
38
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
39
|
+
},
|
|
40
|
+
publicDir: false,
|
|
41
|
+
logLevel: "warn",
|
|
42
|
+
plugins: [react(), tailwindcss()],
|
|
43
|
+
build: {
|
|
44
|
+
emptyOutDir: false,
|
|
45
|
+
minify: true,
|
|
46
|
+
outDir: OUT_DIR,
|
|
47
|
+
sourcemap: false,
|
|
48
|
+
target: "es2022",
|
|
49
|
+
lib: {
|
|
50
|
+
entry: item.entry,
|
|
51
|
+
formats: ["iife"],
|
|
52
|
+
name: item.globalName,
|
|
53
|
+
fileName: () => `assets/${item.fileName}.js`,
|
|
54
|
+
cssFileName: "assets/site",
|
|
55
|
+
},
|
|
56
|
+
rollupOptions: {
|
|
57
|
+
output: {
|
|
58
|
+
assetFileNames: (assetInfo) => {
|
|
59
|
+
if (assetInfo.name?.endsWith(".css")) {
|
|
60
|
+
return "assets/site.css";
|
|
61
|
+
}
|
|
62
|
+
return "assets/[name][extname]";
|
|
63
|
+
},
|
|
64
|
+
extend: true,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log("Built React/Tailwind static site assets in site/assets");
|
package/scripts/launch-agent.mjs
CHANGED
|
@@ -1,241 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import os from "node:os";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
7
4
|
import path from "node:path";
|
|
8
5
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { promisify } from "node:util";
|
|
10
6
|
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
-
const label = process.env.SNAPSHOT_LAUNCH_AGENT_LABEL || "com.codex-snapshots.viewer";
|
|
13
|
-
const uid = process.getuid?.() ?? Number.parseInt(process.env.UID || "", 10);
|
|
14
|
-
const homeDir = os.homedir();
|
|
15
7
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
8
|
const repoRoot = path.resolve(scriptDir, "..");
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const stdoutPath = path.join(logsDir, "codex-snapshot.out.log");
|
|
21
|
-
const stderrPath = path.join(logsDir, "codex-snapshot.err.log");
|
|
22
|
-
const defaultApiUrl = "http://127.0.0.1:8787";
|
|
23
|
-
const defaultSiteUrl = "http://127.0.0.1:8787";
|
|
9
|
+
const cliPath = path.join(repoRoot, "dist", "cli", "codex-snapshot.mjs");
|
|
10
|
+
const legacyCommand = process.argv[2] || "status";
|
|
11
|
+
const args = ["daemon", legacyCommand, ...process.argv.slice(3)];
|
|
24
12
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
main().catch((error) => {
|
|
28
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
29
|
-
process.exitCode = 1;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
async function main() {
|
|
33
|
-
if (command === "install") {
|
|
34
|
-
await install();
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (command === "uninstall") {
|
|
38
|
-
await uninstall();
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (command === "status") {
|
|
42
|
-
await status();
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (command === "logs") {
|
|
46
|
-
await logs();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
printHelp();
|
|
50
|
-
process.exitCode = 1;
|
|
13
|
+
if (legacyCommand === "help" || legacyCommand === "--help" || legacyCommand === "-h") {
|
|
14
|
+
args.splice(1, 1, "help");
|
|
51
15
|
}
|
|
52
16
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const plist = renderPlist({
|
|
59
|
-
pnpmPath,
|
|
60
|
-
apiUrl: process.env.SNAPSHOT_SHARE_API_URL || defaultApiUrl,
|
|
61
|
-
siteUrl: process.env.SNAPSHOT_SHARE_SITE_URL || defaultSiteUrl,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
await writeFile(plistPath, plist, "utf8");
|
|
65
|
-
await bootoutIfLoaded();
|
|
66
|
-
await execLaunchctl(["bootstrap", guiDomain(), plistPath]);
|
|
67
|
-
await execLaunchctl(["kickstart", "-k", `${guiDomain()}/${label}`]);
|
|
68
|
-
|
|
69
|
-
console.log(`Installed ${label}`);
|
|
70
|
-
console.log(`Plist: ${plistPath}`);
|
|
71
|
-
console.log(`Logs: ${stdoutPath}`);
|
|
72
|
-
console.log(`Preview: http://127.0.0.1:4321/`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function uninstall() {
|
|
76
|
-
await bootoutIfLoaded();
|
|
77
|
-
await rm(plistPath, { force: true });
|
|
78
|
-
console.log(`Uninstalled ${label}`);
|
|
79
|
-
}
|
|
17
|
+
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
18
|
+
cwd: repoRoot,
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
});
|
|
80
21
|
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
22
|
+
child.on("exit", (code, signal) => {
|
|
23
|
+
if (signal) {
|
|
24
|
+
process.kill(process.pid, signal);
|
|
84
25
|
return;
|
|
85
26
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const state = stdout.match(/state = ([^\n]+)/)?.[1]?.trim() || "unknown";
|
|
89
|
-
const pid = stdout.match(/pid = (\d+)/)?.[1] || "";
|
|
90
|
-
console.log(`${label}: ${state}${pid ? `, pid=${pid}` : ""}`);
|
|
91
|
-
console.log(`Plist: ${plistPath}`);
|
|
92
|
-
console.log(`Preview: http://127.0.0.1:4321/`);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.log(`${label}: installed but not loaded`);
|
|
95
|
-
console.log(`Plist: ${plistPath}`);
|
|
96
|
-
if (error instanceof Error && error.message) {
|
|
97
|
-
console.log(error.message);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function logs() {
|
|
103
|
-
console.log(`==> ${stdoutPath}`);
|
|
104
|
-
console.log(await tailFile(stdoutPath));
|
|
105
|
-
console.log(`==> ${stderrPath}`);
|
|
106
|
-
console.log(await tailFile(stderrPath));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function bootoutIfLoaded() {
|
|
110
|
-
try {
|
|
111
|
-
await execLaunchctl(["bootout", guiDomain(), plistPath]);
|
|
112
|
-
} catch {}
|
|
113
|
-
try {
|
|
114
|
-
await execLaunchctl(["bootout", `${guiDomain()}/${label}`]);
|
|
115
|
-
} catch {}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function resolvePnpmPath() {
|
|
119
|
-
if (process.env.PNPM_EXECUTABLE) {
|
|
120
|
-
return process.env.PNPM_EXECUTABLE;
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
const { stdout } = await execFileAsync("/bin/zsh", ["-lc", "command -v pnpm"], {
|
|
124
|
-
cwd: repoRoot,
|
|
125
|
-
maxBuffer: 1024 * 1024,
|
|
126
|
-
});
|
|
127
|
-
const pnpmPath = stdout.trim().split("\n")[0];
|
|
128
|
-
if (pnpmPath) {
|
|
129
|
-
return pnpmPath;
|
|
130
|
-
}
|
|
131
|
-
} catch {}
|
|
132
|
-
throw new Error("Cannot find pnpm. Install pnpm first, or run with PNPM_EXECUTABLE=/absolute/path/to/pnpm.");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function renderPlist({ pnpmPath, apiUrl, siteUrl }) {
|
|
136
|
-
const shellCommand = `cd ${shellQuote(repoRoot)} && exec ${shellQuote(pnpmPath)} snapshot:daemon`;
|
|
137
|
-
const stablePath = [
|
|
138
|
-
path.dirname(pnpmPath),
|
|
139
|
-
"/opt/homebrew/bin",
|
|
140
|
-
"/usr/local/bin",
|
|
141
|
-
"/usr/bin",
|
|
142
|
-
"/bin",
|
|
143
|
-
"/usr/sbin",
|
|
144
|
-
"/sbin",
|
|
145
|
-
].join(":");
|
|
146
|
-
const env = {
|
|
147
|
-
PATH: process.env.SNAPSHOT_DAEMON_PATH || stablePath,
|
|
148
|
-
SNAPSHOT_SHARE_API_URL: apiUrl,
|
|
149
|
-
SNAPSHOT_SHARE_SITE_URL: siteUrl,
|
|
150
|
-
SNAPSHOT_VIEWER_ALLOWED_ORIGINS:
|
|
151
|
-
process.env.SNAPSHOT_VIEWER_ALLOWED_ORIGINS ||
|
|
152
|
-
"http://127.0.0.1:3000,http://localhost:3000",
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
156
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
157
|
-
<plist version="1.0">
|
|
158
|
-
<dict>
|
|
159
|
-
<key>Label</key>
|
|
160
|
-
<string>${xmlEscape(label)}</string>
|
|
161
|
-
<key>ProgramArguments</key>
|
|
162
|
-
<array>
|
|
163
|
-
<string>/bin/zsh</string>
|
|
164
|
-
<string>-lc</string>
|
|
165
|
-
<string>${xmlEscape(shellCommand)}</string>
|
|
166
|
-
</array>
|
|
167
|
-
<key>WorkingDirectory</key>
|
|
168
|
-
<string>${xmlEscape(repoRoot)}</string>
|
|
169
|
-
<key>EnvironmentVariables</key>
|
|
170
|
-
<dict>
|
|
171
|
-
${Object.entries(env)
|
|
172
|
-
.map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
|
|
173
|
-
.join("\n")}
|
|
174
|
-
</dict>
|
|
175
|
-
<key>RunAtLoad</key>
|
|
176
|
-
<true/>
|
|
177
|
-
<key>KeepAlive</key>
|
|
178
|
-
<true/>
|
|
179
|
-
<key>ThrottleInterval</key>
|
|
180
|
-
<integer>10</integer>
|
|
181
|
-
<key>ProcessType</key>
|
|
182
|
-
<string>Background</string>
|
|
183
|
-
<key>StandardOutPath</key>
|
|
184
|
-
<string>${xmlEscape(stdoutPath)}</string>
|
|
185
|
-
<key>StandardErrorPath</key>
|
|
186
|
-
<string>${xmlEscape(stderrPath)}</string>
|
|
187
|
-
</dict>
|
|
188
|
-
</plist>
|
|
189
|
-
`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function guiDomain() {
|
|
193
|
-
if (!Number.isFinite(uid)) {
|
|
194
|
-
throw new Error("Cannot determine current macOS user id.");
|
|
195
|
-
}
|
|
196
|
-
return `gui/${uid}`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function execLaunchctl(args) {
|
|
200
|
-
return execFileAsync("/bin/launchctl", args, {
|
|
201
|
-
cwd: repoRoot,
|
|
202
|
-
maxBuffer: 1024 * 1024,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function tailFile(filePath, lines = 80) {
|
|
207
|
-
try {
|
|
208
|
-
const text = await readFile(filePath, "utf8");
|
|
209
|
-
return text.split(/\r?\n/).slice(-lines).join("\n").trimEnd() || "(empty)";
|
|
210
|
-
} catch {
|
|
211
|
-
return "(missing)";
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function shellQuote(value) {
|
|
216
|
-
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function xmlEscape(value) {
|
|
220
|
-
return String(value)
|
|
221
|
-
.replace(/&/g, "&")
|
|
222
|
-
.replace(/</g, "<")
|
|
223
|
-
.replace(/>/g, ">")
|
|
224
|
-
.replace(/"/g, """)
|
|
225
|
-
.replace(/'/g, "'");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function printHelp() {
|
|
229
|
-
console.log(`Usage:
|
|
230
|
-
pnpm snapshot:install-daemon
|
|
231
|
-
pnpm snapshot:daemon:status
|
|
232
|
-
pnpm snapshot:daemon:logs
|
|
233
|
-
pnpm snapshot:uninstall-daemon
|
|
27
|
+
process.exitCode = code || 0;
|
|
28
|
+
});
|
|
234
29
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
SNAPSHOT_VIEWER_ALLOWED_ORIGINS=http://127.0.0.1:3000,http://localhost:3000
|
|
240
|
-
`);
|
|
241
|
-
}
|
|
30
|
+
child.on("error", (error) => {
|
|
31
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
});
|
package/scripts/serve-site.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "site");
|
|
10
10
|
const parsed = parseArgs(process.argv.slice(2));
|
|
11
11
|
const host = parsed.host || "127.0.0.1";
|
|
12
|
-
const port = Number(parsed.port ||
|
|
12
|
+
const port = Number(parsed.port || 4322);
|
|
13
13
|
|
|
14
14
|
if (parsed.help) {
|
|
15
15
|
printHelp();
|
|
@@ -98,6 +98,6 @@ function printHelp() {
|
|
|
98
98
|
console.log(`codex-snapshots site server
|
|
99
99
|
|
|
100
100
|
Usage:
|
|
101
|
-
node scripts/serve-site.mjs [--host 127.0.0.1] [--port
|
|
101
|
+
node scripts/serve-site.mjs [--host 127.0.0.1] [--port 4322]
|
|
102
102
|
`);
|
|
103
103
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
OUTPUT_FILE="$(mktemp)"
|
|
6
|
+
VALID_CONFIG="$(mktemp)"
|
|
7
|
+
AUTO_TOKEN_CONFIG="$(mktemp)"
|
|
8
|
+
OAUTH_CONFIG="$(mktemp)"
|
|
9
|
+
LOCAL_TOKEN_CONFIG="$(mktemp)"
|
|
10
|
+
TOKEN_FILE="$(mktemp)"
|
|
11
|
+
|
|
12
|
+
cleanup() {
|
|
13
|
+
rm -f "${OUTPUT_FILE}" "${VALID_CONFIG}" "${AUTO_TOKEN_CONFIG}" "${OAUTH_CONFIG}" "${LOCAL_TOKEN_CONFIG}" "${TOKEN_FILE}"
|
|
14
|
+
}
|
|
15
|
+
trap cleanup EXIT
|
|
16
|
+
|
|
17
|
+
if bash "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh" \
|
|
18
|
+
--config "${ROOT_DIR}/deploy/aliyun/deploy.env.example" \
|
|
19
|
+
--dry-run >"${OUTPUT_FILE}" 2>&1; then
|
|
20
|
+
echo "Expected deploy.env.example to fail validation." >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
grep -q "SSH target still uses the placeholder root@1.2.3.4" "${OUTPUT_FILE}"
|
|
25
|
+
grep -q "DOMAIN still uses the placeholder snapshots.example.com" "${OUTPUT_FILE}"
|
|
26
|
+
grep -q "TOKEN still uses the placeholder change-me" "${OUTPUT_FILE}"
|
|
27
|
+
|
|
28
|
+
if node "${ROOT_DIR}/deploy/aliyun/doctor.mjs" \
|
|
29
|
+
--config "${ROOT_DIR}/deploy/aliyun/deploy.env.example" \
|
|
30
|
+
--offline >"${OUTPUT_FILE}" 2>&1; then
|
|
31
|
+
echo "Expected deploy doctor to fail on example placeholders." >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
grep -q "SSH target: set SSH_TARGET to your ECS user and public IP" "${OUTPUT_FILE}"
|
|
36
|
+
grep -q "Deploy dry-run: Deployment config needs real values before continuing" "${OUTPUT_FILE}"
|
|
37
|
+
|
|
38
|
+
cat >"${VALID_CONFIG}" <<'EOF'
|
|
39
|
+
SSH_TARGET=root@203.0.113.42
|
|
40
|
+
SSH_IDENTITY_FILE=
|
|
41
|
+
SSH_PORT=
|
|
42
|
+
DOMAIN=snapshots.mycompany.dev
|
|
43
|
+
API_URL=https://snapshots.mycompany.dev
|
|
44
|
+
SITE_URL=https://ffffhx.github.io/codex-snapshots/
|
|
45
|
+
TOKEN=test-token-0123456789
|
|
46
|
+
CERTBOT_EMAIL=ops@example.org
|
|
47
|
+
INSTALL_DEPS=0
|
|
48
|
+
ISSUE_CERT=1
|
|
49
|
+
RUN_PREFLIGHT=0
|
|
50
|
+
RUN_VERIFY=0
|
|
51
|
+
CONFIGURE_PAGES=0
|
|
52
|
+
WAIT_PAGES=0
|
|
53
|
+
CONFIGURE_LOCAL=0
|
|
54
|
+
REINSTALL_DAEMON=0
|
|
55
|
+
REMOTE_DIR=/tmp/codex-snapshots-deploy
|
|
56
|
+
EOF
|
|
57
|
+
|
|
58
|
+
bash "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh" --config "${VALID_CONFIG}" --dry-run >"${OUTPUT_FILE}" 2>&1
|
|
59
|
+
|
|
60
|
+
grep -q "Resolved deployment plan" "${OUTPUT_FILE}"
|
|
61
|
+
grep -q "Token: set (21 chars)" "${OUTPUT_FILE}"
|
|
62
|
+
grep -q "Issue cert: 1" "${OUTPUT_FILE}"
|
|
63
|
+
|
|
64
|
+
node "${ROOT_DIR}/deploy/aliyun/doctor.mjs" --config "${VALID_CONFIG}" --offline >"${OUTPUT_FILE}" 2>&1
|
|
65
|
+
|
|
66
|
+
grep -q "Ready for local deployment orchestration" "${OUTPUT_FILE}"
|
|
67
|
+
grep -q "Deploy dry-run: deploy-to-ecs.sh accepted the config" "${OUTPUT_FILE}"
|
|
68
|
+
|
|
69
|
+
cat >"${AUTO_TOKEN_CONFIG}" <<'EOF'
|
|
70
|
+
SSH_TARGET=root@203.0.113.42
|
|
71
|
+
DOMAIN=snapshots.mycompany.dev
|
|
72
|
+
API_URL=https://snapshots.mycompany.dev
|
|
73
|
+
SITE_URL=https://ffffhx.github.io/codex-snapshots/
|
|
74
|
+
TOKEN=auto
|
|
75
|
+
INSTALL_DEPS=0
|
|
76
|
+
ISSUE_CERT=0
|
|
77
|
+
RUN_PREFLIGHT=0
|
|
78
|
+
RUN_VERIFY=0
|
|
79
|
+
CONFIGURE_PAGES=0
|
|
80
|
+
WAIT_PAGES=0
|
|
81
|
+
CONFIGURE_LOCAL=1
|
|
82
|
+
REINSTALL_DAEMON=0
|
|
83
|
+
REMOTE_DIR=/tmp/codex-snapshots-deploy
|
|
84
|
+
EOF
|
|
85
|
+
|
|
86
|
+
bash "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh" --config "${AUTO_TOKEN_CONFIG}" --dry-run >"${OUTPUT_FILE}" 2>&1
|
|
87
|
+
|
|
88
|
+
grep -q "Publish token: generated for this deployment" "${OUTPUT_FILE}"
|
|
89
|
+
grep -Eq "Token: generated \\([0-9]+ chars\\)" "${OUTPUT_FILE}"
|
|
90
|
+
grep -q "Configure local publisher: 1" "${OUTPUT_FILE}"
|
|
91
|
+
|
|
92
|
+
node "${ROOT_DIR}/deploy/aliyun/doctor.mjs" --config "${AUTO_TOKEN_CONFIG}" --offline >"${OUTPUT_FILE}" 2>&1
|
|
93
|
+
|
|
94
|
+
grep -q "Publish token: will be generated by deploy-to-ecs.sh" "${OUTPUT_FILE}"
|
|
95
|
+
grep -q "Local publisher config: will be written with generated token" "${OUTPUT_FILE}"
|
|
96
|
+
|
|
97
|
+
cat >"${OAUTH_CONFIG}" <<'EOF'
|
|
98
|
+
SSH_TARGET=root@203.0.113.42
|
|
99
|
+
DOMAIN=snapshots.mycompany.dev
|
|
100
|
+
API_URL=https://snapshots.mycompany.dev
|
|
101
|
+
SITE_URL=https://ffffhx.github.io/codex-snapshots/
|
|
102
|
+
TOKEN=change-me
|
|
103
|
+
SNAPSHOT_GITHUB_CLIENT_ID=client-id-0123456789
|
|
104
|
+
SNAPSHOT_GITHUB_CLIENT_SECRET=client-secret-0123456789
|
|
105
|
+
SNAPSHOT_GITHUB_OWNER_LOGIN=site-owner
|
|
106
|
+
SNAPSHOT_SESSION_SECRET=session-secret-0123456789-0123456789
|
|
107
|
+
INSTALL_DEPS=0
|
|
108
|
+
ISSUE_CERT=0
|
|
109
|
+
RUN_PREFLIGHT=0
|
|
110
|
+
RUN_VERIFY=0
|
|
111
|
+
CONFIGURE_PAGES=0
|
|
112
|
+
WAIT_PAGES=0
|
|
113
|
+
CONFIGURE_LOCAL=1
|
|
114
|
+
REINSTALL_DAEMON=0
|
|
115
|
+
REMOTE_DIR=/tmp/codex-snapshots-deploy
|
|
116
|
+
EOF
|
|
117
|
+
|
|
118
|
+
bash "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh" --config "${OAUTH_CONFIG}" --dry-run >"${OUTPUT_FILE}" 2>&1
|
|
119
|
+
|
|
120
|
+
grep -q "Resolved deployment plan" "${OUTPUT_FILE}"
|
|
121
|
+
grep -q "Token: not configured (GitHub OAuth mode)" "${OUTPUT_FILE}"
|
|
122
|
+
grep -q "GitHub OAuth: configured" "${OUTPUT_FILE}"
|
|
123
|
+
|
|
124
|
+
node "${ROOT_DIR}/deploy/aliyun/doctor.mjs" --config "${OAUTH_CONFIG}" --offline >"${OUTPUT_FILE}" 2>&1
|
|
125
|
+
|
|
126
|
+
grep -q "GitHub OAuth: configured for site-owner" "${OUTPUT_FILE}"
|
|
127
|
+
grep -q "Publish token: not required when GitHub OAuth is configured" "${OUTPUT_FILE}"
|
|
128
|
+
grep -q "Local publisher config: will be written with public API/site URLs for GitHub OAuth" "${OUTPUT_FILE}"
|
|
129
|
+
|
|
130
|
+
cat >"${LOCAL_TOKEN_CONFIG}" <<'EOF'
|
|
131
|
+
SSH_TARGET=root@203.0.113.42
|
|
132
|
+
DOMAIN=snapshots.mycompany.dev
|
|
133
|
+
API_URL=https://snapshots.mycompany.dev
|
|
134
|
+
SITE_URL=https://ffffhx.github.io/codex-snapshots/
|
|
135
|
+
INSTALL_DEPS=0
|
|
136
|
+
ISSUE_CERT=0
|
|
137
|
+
RUN_PREFLIGHT=0
|
|
138
|
+
RUN_VERIFY=0
|
|
139
|
+
CONFIGURE_PAGES=0
|
|
140
|
+
WAIT_PAGES=0
|
|
141
|
+
CONFIGURE_LOCAL=0
|
|
142
|
+
REINSTALL_DAEMON=0
|
|
143
|
+
REMOTE_DIR=/tmp/codex-snapshots-deploy
|
|
144
|
+
EOF
|
|
145
|
+
|
|
146
|
+
cat >"${TOKEN_FILE}" <<'EOF'
|
|
147
|
+
{
|
|
148
|
+
"snapshotShareToken": "local-file-token-0123456789",
|
|
149
|
+
"snapshotShareApiUrl": "https://snapshots.mycompany.dev",
|
|
150
|
+
"snapshotShareSiteUrl": "https://ffffhx.github.io/codex-snapshots"
|
|
151
|
+
}
|
|
152
|
+
EOF
|
|
153
|
+
|
|
154
|
+
SNAPSHOT_SHARE_TOKEN_FILE="${TOKEN_FILE}" \
|
|
155
|
+
bash "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh" --config "${LOCAL_TOKEN_CONFIG}" --dry-run >"${OUTPUT_FILE}" 2>&1
|
|
156
|
+
|
|
157
|
+
grep -q "Resolved deployment plan" "${OUTPUT_FILE}"
|
|
158
|
+
grep -q "Token: set (27 chars)" "${OUTPUT_FILE}"
|
|
159
|
+
|
|
160
|
+
if bash "${ROOT_DIR}/deploy/aliyun/configure-local-publisher.sh" \
|
|
161
|
+
--api-url https://snapshots.example.com \
|
|
162
|
+
--token change-me \
|
|
163
|
+
--token-file "${TOKEN_FILE}" \
|
|
164
|
+
--no-check >"${OUTPUT_FILE}" 2>&1; then
|
|
165
|
+
echo "Expected placeholder local publisher config to fail validation." >&2
|
|
166
|
+
exit 1
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
grep -q "API URL still uses the placeholder https://snapshots.example.com" "${OUTPUT_FILE}"
|
|
170
|
+
grep -q "Token still uses the placeholder change-me" "${OUTPUT_FILE}"
|
|
171
|
+
|
|
172
|
+
bash "${ROOT_DIR}/deploy/aliyun/configure-local-publisher.sh" \
|
|
173
|
+
--api-url https://snapshots.mycompany.dev \
|
|
174
|
+
--site-url https://ffffhx.github.io/codex-snapshots/ \
|
|
175
|
+
--token test-token-0123456789 \
|
|
176
|
+
--token-file "${TOKEN_FILE}" \
|
|
177
|
+
--no-check >"${OUTPUT_FILE}" 2>&1
|
|
178
|
+
|
|
179
|
+
grep -q "Wrote local publisher config" "${OUTPUT_FILE}"
|
|
180
|
+
grep -q "Public share API: https://snapshots.mycompany.dev" "${OUTPUT_FILE}"
|
|
181
|
+
grep -q '"snapshotShareToken": "test-token-0123456789"' "${TOKEN_FILE}"
|
|
182
|
+
grep -q '"snapshotShareApiUrl": "https://snapshots.mycompany.dev"' "${TOKEN_FILE}"
|
|
183
|
+
grep -q '"snapshotShareSiteUrl": "https://ffffhx.github.io/codex-snapshots"' "${TOKEN_FILE}"
|
|
184
|
+
|
|
185
|
+
bash "${ROOT_DIR}/deploy/aliyun/configure-local-publisher.sh" \
|
|
186
|
+
--api-url https://snapshots.mycompany.dev \
|
|
187
|
+
--site-url https://ffffhx.github.io/codex-snapshots/ \
|
|
188
|
+
--token-file "${TOKEN_FILE}" \
|
|
189
|
+
--no-check >"${OUTPUT_FILE}" 2>&1
|
|
190
|
+
|
|
191
|
+
grep -q "Legacy publish token: not configured" "${OUTPUT_FILE}"
|
|
192
|
+
grep -q '"snapshotShareApiUrl": "https://snapshots.mycompany.dev"' "${TOKEN_FILE}"
|
|
193
|
+
grep -q '"snapshotShareSiteUrl": "https://ffffhx.github.io/codex-snapshots"' "${TOKEN_FILE}"
|
|
194
|
+
if grep -q '"snapshotShareToken"' "${TOKEN_FILE}"; then
|
|
195
|
+
echo "OAuth local publisher config should not require a snapshotShareToken." >&2
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
if bash "${ROOT_DIR}/deploy/aliyun/configure-github-pages-api.sh" \
|
|
200
|
+
--api-url https://snapshots.example.com \
|
|
201
|
+
--no-dispatch >"${OUTPUT_FILE}" 2>&1; then
|
|
202
|
+
echo "Expected placeholder GitHub Pages API config to fail validation." >&2
|
|
203
|
+
exit 1
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
grep -q "API URL still uses the placeholder https://snapshots.example.com" "${OUTPUT_FILE}"
|
|
207
|
+
|
|
208
|
+
grep -q -- '--exclude "deploy/aliyun/deploy.env"' "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh"
|
|
209
|
+
grep -q -- '--exclude ".env"' "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh"
|
|
210
|
+
grep -q -- '--exclude "backups"' "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh"
|
|
211
|
+
grep -q 'rm -f .env deploy/aliyun/deploy.env' "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh"
|
|
212
|
+
grep -q -- '--check-local-config' "${ROOT_DIR}/deploy/aliyun/deploy-to-ecs.sh"
|
|
213
|
+
|
|
214
|
+
grep -q -- '--exclude "deploy/aliyun/deploy.env"' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
215
|
+
grep -q -- '--exclude ".env"' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
216
|
+
grep -q -- '--exclude "backups"' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
217
|
+
grep -q 'rm -f "${APP_DIR}/.env" "${APP_DIR}/deploy/aliyun/deploy.env"' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
218
|
+
grep -q 'systemd_env_value' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
219
|
+
grep -q 'SNAPSHOT_SHARE_TOKEN=%s' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
220
|
+
grep -q 'SNAPSHOT_SHARE_PUBLIC_API_URL=%s' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
221
|
+
grep -q 'SNAPSHOT_SHARE_TOKEN must be a single-line value' "${ROOT_DIR}/deploy/aliyun/install-share-api.sh"
|
|
222
|
+
|
|
223
|
+
grep -q 'proxy_pass http://127.0.0.1:8787' "${ROOT_DIR}/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf"
|
|
224
|
+
grep -q 'proxy_set_header X-Forwarded-Host $host;' "${ROOT_DIR}/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf"
|
|
225
|
+
grep -q 'proxy_set_header X-Forwarded-Port $server_port;' "${ROOT_DIR}/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf"
|
|
226
|
+
grep -q 'Strict-Transport-Security "max-age=31536000"' "${ROOT_DIR}/deploy/aliyun/nginx-codex-snapshots.conf"
|
|
227
|
+
grep -q 'X-Frame-Options DENY' "${ROOT_DIR}/deploy/aliyun/nginx-codex-snapshots.conf"
|
|
228
|
+
grep -q 'proxy_set_header X-Forwarded-Proto https;' "${ROOT_DIR}/deploy/aliyun/nginx-codex-snapshots.conf"
|
|
229
|
+
|
|
230
|
+
echo "✓ Aliyun deploy config validation checks passed"
|