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.
Files changed (51) hide show
  1. package/README.md +101 -6
  2. package/bin/codex-snapshot.mjs +1 -6326
  3. package/deploy/aliyun/README.md +311 -0
  4. package/deploy/aliyun/backup-share-data.sh +109 -0
  5. package/deploy/aliyun/check-ecs-status.sh +149 -0
  6. package/deploy/aliyun/codex-snapshot-share.env.example +29 -0
  7. package/deploy/aliyun/codex-snapshot-share.service +26 -0
  8. package/deploy/aliyun/configure-github-pages-api.sh +141 -0
  9. package/deploy/aliyun/configure-local-publisher.sh +197 -0
  10. package/deploy/aliyun/deploy-to-ecs.sh +669 -0
  11. package/deploy/aliyun/deploy.env.example +52 -0
  12. package/deploy/aliyun/doctor.mjs +398 -0
  13. package/deploy/aliyun/install-share-api.sh +252 -0
  14. package/deploy/aliyun/install-system-deps.sh +84 -0
  15. package/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf +34 -0
  16. package/deploy/aliyun/nginx-codex-snapshots.conf +52 -0
  17. package/deploy/aliyun/preflight.mjs +321 -0
  18. package/deploy/aliyun/restore-share-data.sh +141 -0
  19. package/deploy/aliyun/verify-public-share.mjs +404 -0
  20. package/dist/cli/codex-snapshot.mjs +2654 -0
  21. package/dist/core/privacy.js +81 -0
  22. package/dist/core/snapshot.js +1 -0
  23. package/dist/renderers/markdown.mjs +81 -0
  24. package/dist/renderers/transcript.js +195 -0
  25. package/dist/server/http.js +10 -0
  26. package/dist/server/local-security.js +66 -0
  27. package/dist/server/local-viewer-app.mjs +1670 -0
  28. package/dist/server/local-viewer.mjs +210 -0
  29. package/dist/server/share-api.mjs +1149 -0
  30. package/dist/server/share-store.js +136 -0
  31. package/dist/shared/sanitize.js +126 -0
  32. package/dist/shared/transcript.js +1 -0
  33. package/dist/sources/index.mjs +2 -0
  34. package/dist/sources/local-history.mjs +2221 -0
  35. package/package.json +42 -14
  36. package/scripts/build-site.mjs +71 -0
  37. package/scripts/launch-agent.mjs +19 -227
  38. package/scripts/serve-site.mjs +2 -2
  39. package/scripts/test-aliyun-deploy-config.sh +230 -0
  40. package/scripts/test-share-api.mjs +967 -0
  41. package/scripts/test-site-config.mjs +100 -0
  42. package/scripts/test-static-site.mjs +403 -0
  43. package/scripts/write-site-config.mjs +161 -0
  44. package/server/share-api.mjs +1 -771
  45. package/site/assets/config.js +3 -0
  46. package/site/assets/share.js +43 -106
  47. package/site/assets/site.css +3 -605
  48. package/site/assets/site.js +15 -92
  49. package/site/favicon.svg +7 -0
  50. package/site/index.html +3 -83
  51. 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.0",
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": "bin/codex-snapshot.mjs",
9
- "codex-snapshots": "bin/codex-snapshot.mjs",
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
- "dev": "node bin/codex-snapshot.mjs serve --port 4321",
45
- "snapshot": "node bin/codex-snapshot.mjs",
46
- "snapshot:daemon": "node bin/codex-snapshot.mjs serve --port 4321",
47
- "snapshot:install-daemon": "node scripts/launch-agent.mjs install",
48
- "snapshot:uninstall-daemon": "node scripts/launch-agent.mjs uninstall",
49
- "snapshot:daemon:status": "node scripts/launch-agent.mjs status",
50
- "snapshot:daemon:logs": "node scripts/launch-agent.mjs logs",
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
- "test:smoke": "node bin/codex-snapshot.mjs --help && node 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 scripts/launch-agent.mjs && node --check scripts/serve-site.mjs"
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");
@@ -1,241 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execFile } from "node:child_process";
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 launchAgentsDir = path.join(homeDir, "Library", "LaunchAgents");
18
- const logsDir = path.join(homeDir, "Library", "Logs", "codex-snapshots");
19
- const plistPath = path.join(launchAgentsDir, `${label}.plist`);
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
- const command = process.argv[2] || "status";
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
- async function install() {
54
- const pnpmPath = await resolvePnpmPath();
55
- await mkdir(launchAgentsDir, { recursive: true });
56
- await mkdir(logsDir, { recursive: true });
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
- async function status() {
82
- if (!existsSync(plistPath)) {
83
- console.log(`Not installed: ${plistPath}`);
22
+ child.on("exit", (code, signal) => {
23
+ if (signal) {
24
+ process.kill(process.pid, signal);
84
25
  return;
85
26
  }
86
- try {
87
- const { stdout } = await execLaunchctl(["print", `${guiDomain()}/${label}`]);
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, "&amp;")
222
- .replace(/</g, "&lt;")
223
- .replace(/>/g, "&gt;")
224
- .replace(/"/g, "&quot;")
225
- .replace(/'/g, "&apos;");
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
- Environment:
236
- PNPM_EXECUTABLE=/absolute/path/to/pnpm
237
- SNAPSHOT_SHARE_API_URL=${defaultApiUrl}
238
- SNAPSHOT_SHARE_SITE_URL=${defaultSiteUrl}
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
+ });
@@ -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 || 4323);
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 4323]
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"