@codextheme/cli 0.2.0 → 0.2.2
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 +5 -5
- package/package.json +1 -1
- package/src/handoff-runner.mjs +146 -0
- package/src/handoff-worker.mjs +25 -0
- package/src/handoff.mjs +223 -0
- package/src/lifecycle.mjs +15 -3
- package/src/main.mjs +24 -7
- package/src/runtime.mjs +81 -48
- package/themes/cathedral-nocturne.codextheme-theme +1 -1
- package/themes/crimson-procession.codextheme-theme +1 -1
- package/themes/silver-reliquary.codextheme-theme +1 -1
package/README.md
CHANGED
|
@@ -5,22 +5,22 @@ Fixed-version, one-command curated and private custom skins for Codex Desktop on
|
|
|
5
5
|
Apply a catalog skin:
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx --yes @codextheme/cli@0.2.
|
|
8
|
+
npx --yes @codextheme/cli@0.2.2 apply cathedral-nocturne
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
The custom skin studio at [codextheme.tech](https://codextheme.tech) creates an expiring private command:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npx --yes @codextheme/cli@0.2.
|
|
14
|
+
npx --yes @codextheme/cli@0.2.2 apply-private <private-id>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Private packages are downloaded only from the fixed `https://codextheme.tech` origin, bounded, integrity-checked, schema-validated, safety-linted, and cached locally with owner-only permissions. The temporary server link expires after 24 hours; `reapply` uses the validated local cache and works after that link expires.
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
npx --yes @codextheme/cli@0.2.
|
|
21
|
-
npx --yes @codextheme/cli@0.2.
|
|
20
|
+
npx --yes @codextheme/cli@0.2.2 reapply
|
|
21
|
+
npx --yes @codextheme/cli@0.2.2 restore
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The CLI has no install scripts, does not use `sudo`, and does not modify the Codex application bundle. Requirements: macOS, Node.js 22.4+, and Codex Desktop. A running Codex process is never closed or reopened without an explicit `y` confirmation.
|
|
24
|
+
The CLI has no install scripts, does not use `sudo`, and does not modify the Codex application bundle. Requirements: macOS, Node.js 22.4+, and Codex Desktop. A running Codex process is never closed or reopened without an explicit `y` confirmation. After confirmation, version 0.2.2 uses an owner-only detached one-shot worker so an apply started inside Codex survives that restart; it does not install a persistent service.
|
|
25
25
|
|
|
26
26
|
CodexTheme is an independent project and is not affiliated with or endorsed by OpenAI.
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { applyPrivateTheme, applyTheme } from "./lifecycle.mjs";
|
|
3
|
+
|
|
4
|
+
const PUBLIC_FAILURES = new Map([
|
|
5
|
+
["E_CODEX_NOT_FOUND", "没有找到已安装的 Codex Desktop。"],
|
|
6
|
+
["E_DOM_INCOMPATIBLE", "当前 Codex 界面结构与该主题暂不兼容。"],
|
|
7
|
+
["E_CORE_VERIFY", "主题应用后的完整性验证失败。"],
|
|
8
|
+
["E_PRIVATE_CACHE", "无法安全读取本地私有主题缓存。"],
|
|
9
|
+
["E_RESTORE_FAILED", "主题应用失败,且未能完整恢复 Codex 官方外观。"],
|
|
10
|
+
["E_HANDOFF_BUSY", "已有一个 Codex 重启任务正在处理。"],
|
|
11
|
+
["E_HANDOFF_FAILED", "CodexTheme 未能完成后台主题应用。"],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function delay(milliseconds) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function processIsAlive(pid) {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return error?.code === "EPERM";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function waitForProcessExit(
|
|
28
|
+
pid,
|
|
29
|
+
{ isAlive = processIsAlive, wait = delay, timeoutMs = 10000, pollMs = 100 } = {},
|
|
30
|
+
) {
|
|
31
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
32
|
+
throw Object.assign(new Error("Invalid handoff parent process."), { code: "E_HANDOFF_FAILED" });
|
|
33
|
+
}
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
if (!isAlive(pid)) return;
|
|
37
|
+
await wait(pollMs);
|
|
38
|
+
}
|
|
39
|
+
throw Object.assign(new Error("Foreground CLI did not exit before handoff."), { code: "E_HANDOFF_FAILED" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function notifyHandoff(status, { execFileApi = execFile } = {}) {
|
|
43
|
+
const message = status === "success"
|
|
44
|
+
? "Theme applied. Codex has reopened."
|
|
45
|
+
: "Theme apply failed. Open CodexTheme Help for recovery steps.";
|
|
46
|
+
const script = `display notification ${JSON.stringify(message)} with title "CodexTheme"`;
|
|
47
|
+
await new Promise((resolve) => {
|
|
48
|
+
execFileApi("/usr/bin/osascript", ["-e", script], () => resolve());
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function publicFailure(error) {
|
|
53
|
+
const code = PUBLIC_FAILURES.has(error?.code) ? error.code : "E_HANDOFF_FAILED";
|
|
54
|
+
return { code, message: PUBLIC_FAILURES.get(code) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function validateWorkerInvocation(argv, pendingPath) {
|
|
58
|
+
if (!Array.isArray(argv) || argv.length !== 2 || argv[0] !== pendingPath || !/^[1-9][0-9]*$/.test(argv[1])) {
|
|
59
|
+
throw Object.assign(new Error("Invalid handoff worker invocation."), { code: "E_HANDOFF_FAILED" });
|
|
60
|
+
}
|
|
61
|
+
const parentPid = Number(argv[1]);
|
|
62
|
+
if (!Number.isSafeInteger(parentPid)) {
|
|
63
|
+
throw Object.assign(new Error("Invalid handoff worker invocation."), { code: "E_HANDOFF_FAILED" });
|
|
64
|
+
}
|
|
65
|
+
return parentPid;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function applyJob({ job, lifecycle, cache, runtime, stateStore, now, onApplyStart }) {
|
|
69
|
+
const shared = {
|
|
70
|
+
runtime,
|
|
71
|
+
stateStore,
|
|
72
|
+
promptRestart: async () => true,
|
|
73
|
+
now,
|
|
74
|
+
};
|
|
75
|
+
if (job.action.source === "catalog") {
|
|
76
|
+
onApplyStart();
|
|
77
|
+
return lifecycle.applyTheme({ ...shared, slug: job.action.themeSlug });
|
|
78
|
+
}
|
|
79
|
+
const bundle = JSON.parse(await cache.read(job.action.cacheKey));
|
|
80
|
+
onApplyStart();
|
|
81
|
+
return lifecycle.applyPrivateTheme({
|
|
82
|
+
...shared,
|
|
83
|
+
bundle,
|
|
84
|
+
cacheKey: job.action.cacheKey,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function runHandoffJob({
|
|
89
|
+
store,
|
|
90
|
+
runtime,
|
|
91
|
+
stateStore,
|
|
92
|
+
cache,
|
|
93
|
+
lifecycle = { applyTheme, applyPrivateTheme },
|
|
94
|
+
waitForParentExit = waitForProcessExit,
|
|
95
|
+
settleAfterParentExit = () => delay(1500),
|
|
96
|
+
notify = notifyHandoff,
|
|
97
|
+
now = () => new Date(),
|
|
98
|
+
}) {
|
|
99
|
+
let exitCode = 1;
|
|
100
|
+
let applyStarted = false;
|
|
101
|
+
try {
|
|
102
|
+
const job = await store.readPending();
|
|
103
|
+
await waitForParentExit(job.parentPid);
|
|
104
|
+
await settleAfterParentExit();
|
|
105
|
+
await applyJob({
|
|
106
|
+
job,
|
|
107
|
+
lifecycle,
|
|
108
|
+
cache,
|
|
109
|
+
runtime,
|
|
110
|
+
stateStore,
|
|
111
|
+
now,
|
|
112
|
+
onApplyStart: () => { applyStarted = true; },
|
|
113
|
+
});
|
|
114
|
+
await store.writeResult({
|
|
115
|
+
schemaVersion: 1,
|
|
116
|
+
status: "success",
|
|
117
|
+
completedAt: now().toISOString(),
|
|
118
|
+
message: "主题已应用并通过验证。",
|
|
119
|
+
});
|
|
120
|
+
await notify("success").catch(() => {});
|
|
121
|
+
exitCode = 0;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
let recovered = !applyStarted;
|
|
124
|
+
if (applyStarted) {
|
|
125
|
+
try {
|
|
126
|
+
const recovery = await runtime.recover();
|
|
127
|
+
recovered = recovery?.recovered === true;
|
|
128
|
+
} catch { /* The sanitized result below records failed recovery. */ }
|
|
129
|
+
}
|
|
130
|
+
const failure = publicFailure(error);
|
|
131
|
+
try {
|
|
132
|
+
await store.writeResult({
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
status: "failure",
|
|
135
|
+
completedAt: now().toISOString(),
|
|
136
|
+
code: failure.code,
|
|
137
|
+
message: failure.message,
|
|
138
|
+
recovered,
|
|
139
|
+
});
|
|
140
|
+
} catch { /* Notification still gives the user a recovery signal. */ }
|
|
141
|
+
await notify("failure").catch(() => {});
|
|
142
|
+
} finally {
|
|
143
|
+
await store.removePending().catch(() => {});
|
|
144
|
+
}
|
|
145
|
+
return exitCode;
|
|
146
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createPrivateCache } from "./cache.mjs";
|
|
4
|
+
import { createHandoffStore } from "./handoff.mjs";
|
|
5
|
+
import { runHandoffJob, validateWorkerInvocation } from "./handoff-runner.mjs";
|
|
6
|
+
import { runtime } from "./runtime.mjs";
|
|
7
|
+
import { createStateStore } from "./state.mjs";
|
|
8
|
+
|
|
9
|
+
const store = createHandoffStore();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const parentPid = validateWorkerInvocation(process.argv.slice(2), store.pendingPath);
|
|
13
|
+
const pending = await store.readPending();
|
|
14
|
+
if (pending.parentPid !== parentPid) {
|
|
15
|
+
throw Object.assign(new Error("Handoff parent mismatch."), { code: "E_HANDOFF_FAILED" });
|
|
16
|
+
}
|
|
17
|
+
process.exitCode = await runHandoffJob({
|
|
18
|
+
store,
|
|
19
|
+
runtime,
|
|
20
|
+
stateStore: createStateStore(),
|
|
21
|
+
cache: createPrivateCache(),
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
package/src/handoff.mjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const SAFE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8
|
+
const SAFE_CACHE_KEY = /^[a-f0-9]{64}$/;
|
|
9
|
+
const MAX_PENDING_AGE_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
function handoffError(code, message) {
|
|
12
|
+
return Object.assign(new Error(message), { code });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function exactKeys(value, keys) {
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
17
|
+
const actual = Object.keys(value).sort();
|
|
18
|
+
const expected = [...keys].sort();
|
|
19
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanAction(value) {
|
|
23
|
+
if (
|
|
24
|
+
exactKeys(value, ["schemaVersion", "source", "themeSlug"])
|
|
25
|
+
&& value.schemaVersion === 2
|
|
26
|
+
&& value.source === "catalog"
|
|
27
|
+
&& typeof value.themeSlug === "string"
|
|
28
|
+
&& SAFE_SLUG.test(value.themeSlug)
|
|
29
|
+
) {
|
|
30
|
+
return { source: "catalog", themeSlug: value.themeSlug };
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
exactKeys(value, ["schemaVersion", "source", "cacheKey"])
|
|
34
|
+
&& value.schemaVersion === 2
|
|
35
|
+
&& value.source === "private"
|
|
36
|
+
&& typeof value.cacheKey === "string"
|
|
37
|
+
&& SAFE_CACHE_KEY.test(value.cacheKey)
|
|
38
|
+
) {
|
|
39
|
+
return { source: "private", cacheKey: value.cacheKey };
|
|
40
|
+
}
|
|
41
|
+
throw handoffError("E_HANDOFF_FAILED", "无法创建安全的 Codex 重启任务。");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cleanStoredAction(value) {
|
|
45
|
+
if (value?.source === "catalog" && exactKeys(value, ["source", "themeSlug"])) {
|
|
46
|
+
return cleanAction({ schemaVersion: 2, ...value });
|
|
47
|
+
}
|
|
48
|
+
if (value?.source === "private" && exactKeys(value, ["source", "cacheKey"])) {
|
|
49
|
+
return cleanAction({ schemaVersion: 2, ...value });
|
|
50
|
+
}
|
|
51
|
+
throw handoffError("E_HANDOFF_FAILED", "本地 Codex 重启任务无效。");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cleanJob(value) {
|
|
55
|
+
if (
|
|
56
|
+
!exactKeys(value, ["schemaVersion", "createdAt", "parentPid", "action"])
|
|
57
|
+
|| value.schemaVersion !== 1
|
|
58
|
+
|| typeof value.createdAt !== "string"
|
|
59
|
+
|| !Number.isFinite(Date.parse(value.createdAt))
|
|
60
|
+
|| !Number.isSafeInteger(value.parentPid)
|
|
61
|
+
|| value.parentPid <= 0
|
|
62
|
+
) {
|
|
63
|
+
throw handoffError("E_HANDOFF_FAILED", "本地 Codex 重启任务无效。");
|
|
64
|
+
}
|
|
65
|
+
const action = cleanStoredAction(value.action);
|
|
66
|
+
return {
|
|
67
|
+
schemaVersion: 1,
|
|
68
|
+
createdAt: value.createdAt,
|
|
69
|
+
parentPid: value.parentPid,
|
|
70
|
+
action,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cleanResult(value) {
|
|
75
|
+
if (
|
|
76
|
+
value?.schemaVersion !== 1
|
|
77
|
+
|| !["success", "failure"].includes(value.status)
|
|
78
|
+
|| typeof value.completedAt !== "string"
|
|
79
|
+
|| !Number.isFinite(Date.parse(value.completedAt))
|
|
80
|
+
|| typeof value.message !== "string"
|
|
81
|
+
|| !value.message
|
|
82
|
+
) {
|
|
83
|
+
throw handoffError("E_HANDOFF_FAILED", "Codex 重启任务结果无效。");
|
|
84
|
+
}
|
|
85
|
+
if (value.status === "success" && exactKeys(value, ["schemaVersion", "status", "completedAt", "message"])) {
|
|
86
|
+
return { schemaVersion: 1, status: "success", completedAt: value.completedAt, message: value.message };
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
value.status === "failure"
|
|
90
|
+
&& exactKeys(value, ["schemaVersion", "status", "completedAt", "code", "message", "recovered"])
|
|
91
|
+
&& typeof value.code === "string"
|
|
92
|
+
&& /^E_[A-Z0-9_]+$/.test(value.code)
|
|
93
|
+
&& typeof value.recovered === "boolean"
|
|
94
|
+
) {
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
status: "failure",
|
|
98
|
+
completedAt: value.completedAt,
|
|
99
|
+
code: value.code,
|
|
100
|
+
message: value.message,
|
|
101
|
+
recovered: value.recovered,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
throw handoffError("E_HANDOFF_FAILED", "Codex 重启任务结果无效。");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function ensureDirectory(directory, fsApi) {
|
|
108
|
+
await fsApi.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
109
|
+
await fsApi.chmod(directory, 0o700);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function createExclusive(filename, serialized, fsApi) {
|
|
113
|
+
const temporary = `${filename}.${process.pid}.${Date.now()}.tmp`;
|
|
114
|
+
try {
|
|
115
|
+
await fsApi.writeFile(temporary, serialized, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
116
|
+
await fsApi.link(temporary, filename);
|
|
117
|
+
await fsApi.chmod(filename, 0o600);
|
|
118
|
+
} finally {
|
|
119
|
+
await fsApi.rm(temporary, { force: true }).catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function replaceFile(filename, serialized, fsApi) {
|
|
124
|
+
const temporary = `${filename}.${process.pid}.${Date.now()}.tmp`;
|
|
125
|
+
try {
|
|
126
|
+
await fsApi.writeFile(temporary, serialized, { encoding: "utf8", mode: 0o600 });
|
|
127
|
+
await fsApi.rename(temporary, filename);
|
|
128
|
+
await fsApi.chmod(filename, 0o600);
|
|
129
|
+
} finally {
|
|
130
|
+
await fsApi.rm(temporary, { force: true }).catch(() => {});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createHandoffStore({ home = os.homedir(), fsApi = fs, now = () => new Date() } = {}) {
|
|
135
|
+
const directory = path.join(home, "Library", "Application Support", "codextheme", "handoff");
|
|
136
|
+
const pendingPath = path.join(directory, "pending.json");
|
|
137
|
+
const resultPath = path.join(directory, "last-result.json");
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
directory,
|
|
141
|
+
pendingPath,
|
|
142
|
+
resultPath,
|
|
143
|
+
|
|
144
|
+
async createPending(action, parentPid) {
|
|
145
|
+
const createdAt = now().toISOString();
|
|
146
|
+
const job = cleanJob({ schemaVersion: 1, createdAt, parentPid, action: cleanAction(action) });
|
|
147
|
+
await ensureDirectory(directory, fsApi);
|
|
148
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
149
|
+
try {
|
|
150
|
+
await createExclusive(pendingPath, `${JSON.stringify(job, null, 2)}\n`, fsApi);
|
|
151
|
+
return job;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error?.code !== "EEXIST") {
|
|
154
|
+
if (String(error?.code ?? "").startsWith("E_HANDOFF_")) throw error;
|
|
155
|
+
throw handoffError("E_HANDOFF_FAILED", "无法写入本地 Codex 重启任务。");
|
|
156
|
+
}
|
|
157
|
+
let existing;
|
|
158
|
+
try {
|
|
159
|
+
existing = await this.readPending();
|
|
160
|
+
} catch {
|
|
161
|
+
throw handoffError("E_HANDOFF_BUSY", "已有一个 Codex 重启任务正在处理。");
|
|
162
|
+
}
|
|
163
|
+
if (now().getTime() - Date.parse(existing.createdAt) <= MAX_PENDING_AGE_MS) {
|
|
164
|
+
throw handoffError("E_HANDOFF_BUSY", "已有一个 Codex 重启任务正在处理。");
|
|
165
|
+
}
|
|
166
|
+
await fsApi.rm(pendingPath, { force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw handoffError("E_HANDOFF_BUSY", "已有一个 Codex 重启任务正在处理。");
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
async readPending() {
|
|
173
|
+
try {
|
|
174
|
+
return cleanJob(JSON.parse(await fsApi.readFile(pendingPath, "utf8")));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (String(error?.code ?? "").startsWith("E_HANDOFF_")) throw error;
|
|
177
|
+
throw handoffError("E_HANDOFF_FAILED", "无法安全读取本地 Codex 重启任务。");
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async writeResult(value) {
|
|
182
|
+
const result = cleanResult(value);
|
|
183
|
+
await ensureDirectory(directory, fsApi);
|
|
184
|
+
await replaceFile(resultPath, `${JSON.stringify(result, null, 2)}\n`, fsApi);
|
|
185
|
+
return result;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async removePending() {
|
|
189
|
+
await fsApi.rm(pendingPath, { force: true });
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createRestartHandoff({
|
|
195
|
+
home = os.homedir(),
|
|
196
|
+
fsApi = fs,
|
|
197
|
+
now = () => new Date(),
|
|
198
|
+
spawnProcess = spawn,
|
|
199
|
+
execPath = process.execPath,
|
|
200
|
+
workerPath = fileURLToPath(new URL("./handoff-worker.mjs", import.meta.url)),
|
|
201
|
+
parentPid = process.pid,
|
|
202
|
+
} = {}) {
|
|
203
|
+
const store = createHandoffStore({ home, fsApi, now });
|
|
204
|
+
return {
|
|
205
|
+
store,
|
|
206
|
+
async schedule(action) {
|
|
207
|
+
await store.createPending(action, parentPid);
|
|
208
|
+
try {
|
|
209
|
+
const child = spawnProcess(
|
|
210
|
+
execPath,
|
|
211
|
+
[workerPath, store.pendingPath, String(parentPid)],
|
|
212
|
+
{ detached: true, stdio: "ignore" },
|
|
213
|
+
);
|
|
214
|
+
if (!child || typeof child.unref !== "function") throw new Error("invalid child");
|
|
215
|
+
child.unref();
|
|
216
|
+
} catch {
|
|
217
|
+
await store.removePending().catch(() => {});
|
|
218
|
+
throw handoffError("E_HANDOFF_FAILED", "无法启动独立的 Codex 重启任务。");
|
|
219
|
+
}
|
|
220
|
+
return { queued: true, resultPath: store.resultPath };
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
package/src/lifecycle.mjs
CHANGED
|
@@ -32,7 +32,15 @@ async function requireRestartConsent(promptRestart) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async function
|
|
35
|
+
async function delegateRestart({ theme, nextState, restartHandoff }) {
|
|
36
|
+
return {
|
|
37
|
+
theme,
|
|
38
|
+
handoff: await restartHandoff.schedule(nextState),
|
|
39
|
+
appliedAt: null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function applyResolvedTheme({ theme, nextState, runtime, stateStore, promptRestart, restartHandoff, now }) {
|
|
36
44
|
let detection;
|
|
37
45
|
try {
|
|
38
46
|
detection = await runtime.detect();
|
|
@@ -46,6 +54,7 @@ async function applyResolvedTheme({ theme, nextState, runtime, stateStore, promp
|
|
|
46
54
|
let restartExisting = false;
|
|
47
55
|
if (detection.running && !detection.ready) {
|
|
48
56
|
await requireRestartConsent(promptRestart);
|
|
57
|
+
if (restartHandoff) return delegateRestart({ theme, nextState, restartHandoff });
|
|
49
58
|
restartExisting = true;
|
|
50
59
|
}
|
|
51
60
|
|
|
@@ -55,6 +64,7 @@ async function applyResolvedTheme({ theme, nextState, runtime, stateStore, promp
|
|
|
55
64
|
} catch (error) {
|
|
56
65
|
if (error?.code === "CODEDROBE_RESTART_REQUIRED" && !restartExisting) {
|
|
57
66
|
await requireRestartConsent(promptRestart);
|
|
67
|
+
if (restartHandoff) return delegateRestart({ theme, nextState, restartHandoff });
|
|
58
68
|
restartExisting = true;
|
|
59
69
|
try {
|
|
60
70
|
result = await runtime.apply({ theme, restartExisting });
|
|
@@ -75,7 +85,7 @@ async function applyResolvedTheme({ theme, nextState, runtime, stateStore, promp
|
|
|
75
85
|
return { theme, result, appliedAt };
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
export async function applyTheme({ slug, runtime, stateStore, promptRestart, now = () => new Date() }) {
|
|
88
|
+
export async function applyTheme({ slug, runtime, stateStore, promptRestart, restartHandoff, now = () => new Date() }) {
|
|
79
89
|
let theme;
|
|
80
90
|
try {
|
|
81
91
|
theme = await runtime.loadTheme(slug);
|
|
@@ -88,11 +98,12 @@ export async function applyTheme({ slug, runtime, stateStore, promptRestart, now
|
|
|
88
98
|
runtime,
|
|
89
99
|
stateStore,
|
|
90
100
|
promptRestart,
|
|
101
|
+
restartHandoff,
|
|
91
102
|
now,
|
|
92
103
|
});
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
export async function applyPrivateTheme({ bundle, cacheKey, runtime, stateStore, promptRestart, now = () => new Date() }) {
|
|
106
|
+
export async function applyPrivateTheme({ bundle, cacheKey, runtime, stateStore, promptRestart, restartHandoff, now = () => new Date() }) {
|
|
96
107
|
let theme;
|
|
97
108
|
try {
|
|
98
109
|
theme = await runtime.loadThemeBundle(bundle);
|
|
@@ -105,6 +116,7 @@ export async function applyPrivateTheme({ bundle, cacheKey, runtime, stateStore,
|
|
|
105
116
|
runtime,
|
|
106
117
|
stateStore,
|
|
107
118
|
promptRestart,
|
|
119
|
+
restartHandoff,
|
|
108
120
|
now,
|
|
109
121
|
});
|
|
110
122
|
}
|
package/src/main.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { CATALOG, getCatalogEntry } from "./catalog.mjs";
|
|
2
2
|
import { createPrivateCache } from "./cache.mjs";
|
|
3
|
+
import { createRestartHandoff } from "./handoff.mjs";
|
|
3
4
|
import { applyPrivateTheme, applyTheme, CliError, reapplyTheme, restoreTheme } from "./lifecycle.mjs";
|
|
4
5
|
import { privateThemeSource } from "./private-source.mjs";
|
|
5
6
|
import { confirmRestart } from "./prompt.mjs";
|
|
6
7
|
import { runtime as productionRuntime } from "./runtime.mjs";
|
|
7
8
|
import { createStateStore } from "./state.mjs";
|
|
8
9
|
|
|
9
|
-
export const VERSION = "0.2.
|
|
10
|
-
const REAPPLY = "npx --yes @codextheme/cli@0.2.
|
|
11
|
-
const RESTORE = "npx --yes @codextheme/cli@0.2.
|
|
10
|
+
export const VERSION = "0.2.2";
|
|
11
|
+
const REAPPLY = "npx --yes @codextheme/cli@0.2.2 reapply";
|
|
12
|
+
const RESTORE = "npx --yes @codextheme/cli@0.2.2 restore";
|
|
12
13
|
|
|
13
14
|
const HELP = `CodexTheme ${VERSION}
|
|
14
15
|
|
|
@@ -40,11 +41,26 @@ function safePublicError(error) {
|
|
|
40
41
|
"E_PRIVATE_DOWNLOAD",
|
|
41
42
|
"E_PRIVATE_INVALID",
|
|
42
43
|
"E_PRIVATE_CACHE",
|
|
44
|
+
"E_HANDOFF_BUSY",
|
|
45
|
+
"E_HANDOFF_FAILED",
|
|
43
46
|
]);
|
|
44
47
|
if (allowed.has(error?.code)) return { code: error.code, message: error.message, exitCode: 1 };
|
|
45
48
|
return { code: "E_DOM_INCOMPATIBLE", message: "CodexTheme 未能完成本次操作。", exitCode: 1 };
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
function writeApplyResult(stdout, result, name, successMessage, { showReapply = true } = {}) {
|
|
52
|
+
if (result.handoff?.queued) {
|
|
53
|
+
stdout.write(
|
|
54
|
+
`✓ ${name} 已交给独立任务处理\n`
|
|
55
|
+
+ "Codex 将自动关闭并重新打开一次,随后完成主题应用和验证。\n"
|
|
56
|
+
+ "当前 Codex task 可能结束;最终结果会通过 macOS 通知显示。\n",
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const reapply = showReapply ? `重开 Codex 后运行:${REAPPLY}\n` : "";
|
|
61
|
+
stdout.write(`${successMessage}\n${reapply}恢复官方外观:${RESTORE}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
function validateCommand(argv) {
|
|
49
65
|
if (argv.length === 2 && argv[0] === "apply") {
|
|
50
66
|
const theme = getCatalogEntry(argv[1]);
|
|
@@ -75,7 +91,7 @@ export async function run(argv, dependencies = {}) {
|
|
|
75
91
|
try {
|
|
76
92
|
const parsed = validateCommand(argv);
|
|
77
93
|
if ((dependencies.platform ?? process.platform) !== "darwin") {
|
|
78
|
-
throw new CliError("E_PLATFORM",
|
|
94
|
+
throw new CliError("E_PLATFORM", `${VERSION} 版本仅支持 macOS 上的 Codex Desktop。`);
|
|
79
95
|
}
|
|
80
96
|
const services = {
|
|
81
97
|
runtime: dependencies.runtime ?? productionRuntime,
|
|
@@ -83,23 +99,24 @@ export async function run(argv, dependencies = {}) {
|
|
|
83
99
|
promptRestart: dependencies.promptRestart ?? (() => confirmRestart()),
|
|
84
100
|
now: dependencies.now ?? (() => new Date()),
|
|
85
101
|
cache: dependencies.privateCache ?? createPrivateCache(),
|
|
102
|
+
restartHandoff: dependencies.restartHandoff ?? createRestartHandoff(),
|
|
86
103
|
};
|
|
87
104
|
const privateSource = dependencies.privateSource ?? privateThemeSource;
|
|
88
105
|
|
|
89
106
|
if (parsed.command === "apply") {
|
|
90
107
|
const result = await applyTheme({ ...services, slug: parsed.slug });
|
|
91
108
|
const name = result.theme.theme?.displayName?.split(" / ")[0] ?? getCatalogEntry(parsed.slug).name;
|
|
92
|
-
stdout
|
|
109
|
+
writeApplyResult(stdout, result, name, `✓ ${name} 主题已应用并通过验证`);
|
|
93
110
|
} else if (parsed.command === "apply-private") {
|
|
94
111
|
const downloaded = await privateSource.download(parsed.privateId);
|
|
95
112
|
const cacheKey = await services.cache.write(downloaded.serialized);
|
|
96
113
|
const result = await applyPrivateTheme({ ...services, bundle: downloaded.bundle, cacheKey });
|
|
97
114
|
const name = result.theme.theme?.displayName ?? "Private Custom Skin";
|
|
98
|
-
stdout
|
|
115
|
+
writeApplyResult(stdout, result, name, `✓ ${name} 已应用并通过验证`);
|
|
99
116
|
} else if (parsed.command === "reapply") {
|
|
100
117
|
const result = await reapplyTheme(services);
|
|
101
118
|
const name = result.theme.theme?.displayName?.split(" / ")[0] ?? "当前";
|
|
102
|
-
stdout
|
|
119
|
+
writeApplyResult(stdout, result, name, `✓ ${name} 主题已重新应用并通过验证`, { showReapply: false });
|
|
103
120
|
} else {
|
|
104
121
|
await restoreTheme(services);
|
|
105
122
|
stdout.write("✓ 已恢复 Codex 官方外观\n");
|
package/src/runtime.mjs
CHANGED
|
@@ -1,67 +1,100 @@
|
|
|
1
|
-
import
|
|
2
|
-
applySkin,
|
|
3
|
-
discoverApp,
|
|
4
|
-
findRunningPids,
|
|
5
|
-
findTargets,
|
|
6
|
-
getAdapter,
|
|
7
|
-
lintThemePackage,
|
|
8
|
-
readThemePackage,
|
|
9
|
-
resolveThemeTarget,
|
|
10
|
-
restoreSkin,
|
|
11
|
-
validateThemePackage,
|
|
12
|
-
} from "@codextheme/runtime";
|
|
1
|
+
import * as runtimeCore from "@codextheme/runtime";
|
|
13
2
|
import { themeFilename } from "./catalog.mjs";
|
|
14
3
|
|
|
15
|
-
const
|
|
4
|
+
const RENDERER_ABSENT_CODES = new Set(["CODEDROBE_TARGET_TIMEOUT", "ECONNREFUSED"]);
|
|
16
5
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw Object.assign(new Error("Theme failed safety validation."), { code: "E_DOM_INCOMPATIBLE" });
|
|
24
|
-
}
|
|
6
|
+
function restoreComplete(result) {
|
|
7
|
+
const rendererComplete = result?.renderer?.restored === true
|
|
8
|
+
|| RENDERER_ABSENT_CODES.has(result?.renderer?.code);
|
|
9
|
+
const hostComplete = result?.host?.restored === true
|
|
10
|
+
|| result?.host?.reason === "missing-backup";
|
|
11
|
+
return rendererComplete && hostComplete;
|
|
25
12
|
}
|
|
26
13
|
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
const filename = themeFilename(slug);
|
|
30
|
-
if (!filename) throw Object.assign(new Error("Unknown theme."), { code: "E_USAGE" });
|
|
31
|
-
const bundle = await readThemePackage(filename);
|
|
32
|
-
return resolveSafeTheme(bundle);
|
|
33
|
-
},
|
|
14
|
+
export function createRuntime({ core = runtimeCore, platform = process.platform } = {}) {
|
|
15
|
+
const adapter = core.getAdapter("codex");
|
|
34
16
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
17
|
+
function resolveSafeTheme(bundle) {
|
|
18
|
+
try {
|
|
19
|
+
core.validateThemePackage(bundle);
|
|
20
|
+
if (core.lintThemePackage(bundle).length) throw new Error("lint");
|
|
21
|
+
return core.resolveThemeTarget(bundle, adapter.id);
|
|
22
|
+
} catch {
|
|
23
|
+
throw Object.assign(new Error("Theme failed safety validation."), { code: "E_DOM_INCOMPATIBLE" });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
38
26
|
|
|
39
|
-
async
|
|
40
|
-
const discovered = await discoverApp(adapter);
|
|
27
|
+
async function detectCodex() {
|
|
28
|
+
const discovered = await core.discoverApp(adapter, platform);
|
|
41
29
|
if (!discovered) return { installed: false, running: false, ready: false };
|
|
42
30
|
let ready = false;
|
|
43
31
|
try {
|
|
44
|
-
ready = (await findTargets(adapter, adapter.defaultPort)).length > 0;
|
|
32
|
+
ready = (await core.findTargets(adapter, adapter.defaultPort)).length > 0;
|
|
45
33
|
} catch { /* An absent loopback endpoint is an expected detection result. */ }
|
|
46
34
|
let running = ready;
|
|
47
35
|
if (!running) {
|
|
48
36
|
try {
|
|
49
|
-
running = (await findRunningPids(adapter,
|
|
37
|
+
running = (await core.findRunningPids(adapter, platform, discovered.executable)).length > 0;
|
|
50
38
|
} catch { /* applySkin still performs the authoritative launch check. */ }
|
|
51
39
|
}
|
|
52
40
|
return { installed: true, running, ready };
|
|
53
|
-
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
async loadTheme(slug) {
|
|
45
|
+
const filename = themeFilename(slug);
|
|
46
|
+
if (!filename) throw Object.assign(new Error("Unknown theme."), { code: "E_USAGE" });
|
|
47
|
+
const bundle = await core.readThemePackage(filename);
|
|
48
|
+
return resolveSafeTheme(bundle);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async loadThemeBundle(bundle) {
|
|
52
|
+
return resolveSafeTheme(bundle);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
detect: detectCodex,
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
async apply({ theme, restartExisting }) {
|
|
58
|
+
return core.applySkin({
|
|
59
|
+
adapter,
|
|
60
|
+
targetTheme: theme,
|
|
61
|
+
launch: true,
|
|
62
|
+
restartExisting,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async restore() {
|
|
67
|
+
return core.restoreSkin({ adapter });
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async recover() {
|
|
71
|
+
let restored = null;
|
|
72
|
+
try {
|
|
73
|
+
restored = await core.restoreSkin({ adapter });
|
|
74
|
+
} catch { /* Relaunch still runs even when restore itself fails. */ }
|
|
75
|
+
|
|
76
|
+
let detection;
|
|
77
|
+
try {
|
|
78
|
+
detection = await detectCodex();
|
|
79
|
+
} catch {
|
|
80
|
+
detection = { installed: false, running: false, ready: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let launch = null;
|
|
84
|
+
if (detection.installed && !detection.running) {
|
|
85
|
+
try {
|
|
86
|
+
launch = await core.launchApp({ adapter });
|
|
87
|
+
} catch { /* The returned recovery result remains false. */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const running = detection.running || Boolean(launch?.targets);
|
|
91
|
+
return {
|
|
92
|
+
recovered: restoreComplete(restored) && running,
|
|
93
|
+
restore: restored,
|
|
94
|
+
launch,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
63
99
|
|
|
64
|
-
|
|
65
|
-
return restoreSkin({ adapter });
|
|
66
|
-
},
|
|
67
|
-
};
|
|
100
|
+
export const runtime = createRuntime();
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"targets": {
|
|
20
20
|
"codex": {
|
|
21
|
-
"css": ":root.codedrobe-codex-skin {\n color-scheme: dark !important;\n --codextheme-accent: var(--color-token-primary, #9b7bff);\n --codextheme-surface: var(--color-token-bg-primary, #10111a);\n --codextheme-panel: color-mix(in srgb, var(--codextheme-surface) 94%, white 6%);\n --codextheme-panel-soft: color-mix(in srgb, var(--codextheme-surface) 88%, transparent);\n --codextheme-line: color-mix(in srgb, var(--codextheme-accent) 32%, transparent);\n}\n\nhtml.codedrobe-codex-skin body {\n background: #07080d !important;\n color: var(--color-token-text-primary, #f5f3ff) !important;\n}\n\nhtml.codedrobe-codex-skin body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image:\n linear-gradient(90deg, rgba(5, 6, 10, .68) 0%, rgba(5, 6, 10, .28) 46%, rgba(5, 6, 10, .08) 100%),\n var(--codedrobe-image-session-bg);\n background-position: center;\n background-size: cover;\n filter: saturate(.92) contrast(1.04);\n}\n\nhtml.codedrobe-codex-skin body > * {\n position: relative;\n z-index: 1;\n}\n\nhtml.codedrobe-codex-skin aside.app-shell-left-panel {\n background: color-mix(in srgb, var(--codextheme-surface) 82%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: 18px 0 42px rgba(0, 0, 0, .25) !important;\n backdrop-filter: blur(18px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface {\n background: color-mix(in srgb, var(--codextheme-surface) 38%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: inset 0 1px rgba(255, 255, 255, .035), -12px 0 40px rgba(0, 0, 0, .18) !important;\n backdrop-filter: blur(14px) saturate(1.03) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface > header.app-header-tint {\n background: color-mix(in srgb, var(--codextheme-panel) 62%, transparent) !important;\n border-bottom-color: var(--codextheme-line) !important;\n backdrop-filter: blur(18px) !important;\n}\n\nhtml.codedrobe-codex-skin .composer-surface-chrome {\n background: var(--codextheme-panel) !important;\n border: 1px solid var(--codextheme-line) !important;\n box-shadow: 0 18px 50px rgba(0, 0, 0, .28), inset 0 1px rgba(255, 255, 255, .05) !important;\n backdrop-filter: blur(20px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin :is(pre, code, [data-language]) {\n background-color: color-mix(in srgb, var(--codextheme-surface) 97%, black 3%) !important;\n}\n\nhtml.codedrobe-codex-skin :is(button, a, input, textarea, [tabindex]):focus-visible {\n outline: 2px solid var(--codextheme-accent) !important;\n outline-offset: 2px !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home {\n position: relative;\n isolation: isolate;\n background
|
|
21
|
+
"css": ":root.codedrobe-codex-skin {\n color-scheme: dark !important;\n --codextheme-accent: var(--color-token-primary, #9b7bff);\n --codextheme-surface: var(--color-token-bg-primary, #10111a);\n --codextheme-panel: color-mix(in srgb, var(--codextheme-surface) 94%, white 6%);\n --codextheme-panel-soft: color-mix(in srgb, var(--codextheme-surface) 88%, transparent);\n --codextheme-line: color-mix(in srgb, var(--codextheme-accent) 32%, transparent);\n}\n\nhtml.codedrobe-codex-skin body {\n background: #07080d !important;\n color: var(--color-token-text-primary, #f5f3ff) !important;\n}\n\nhtml.codedrobe-codex-skin body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image:\n linear-gradient(90deg, rgba(5, 6, 10, .68) 0%, rgba(5, 6, 10, .28) 46%, rgba(5, 6, 10, .08) 100%),\n var(--codedrobe-image-session-bg);\n background-position: center;\n background-size: cover;\n filter: saturate(.92) contrast(1.04);\n}\n\nhtml.codedrobe-codex-skin body:has(.dream-home)::before {\n background-image:\n linear-gradient(90deg, color-mix(in srgb, var(--codextheme-surface) 94%, transparent), transparent 68%),\n var(--codedrobe-image-hero);\n}\n\nhtml.codedrobe-codex-skin body > * {\n position: relative;\n z-index: 1;\n}\n\nhtml.codedrobe-codex-skin aside.app-shell-left-panel {\n background: color-mix(in srgb, var(--codextheme-surface) 82%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: 18px 0 42px rgba(0, 0, 0, .25) !important;\n backdrop-filter: blur(18px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface {\n background: color-mix(in srgb, var(--codextheme-surface) 38%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: inset 0 1px rgba(255, 255, 255, .035), -12px 0 40px rgba(0, 0, 0, .18) !important;\n backdrop-filter: blur(14px) saturate(1.03) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface > header.app-header-tint {\n background: color-mix(in srgb, var(--codextheme-panel) 62%, transparent) !important;\n border-bottom-color: var(--codextheme-line) !important;\n backdrop-filter: blur(18px) !important;\n}\n\nhtml.codedrobe-codex-skin .composer-surface-chrome {\n background: var(--codextheme-panel) !important;\n border: 1px solid var(--codextheme-line) !important;\n box-shadow: 0 18px 50px rgba(0, 0, 0, .28), inset 0 1px rgba(255, 255, 255, .05) !important;\n backdrop-filter: blur(20px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin :is(pre, code, [data-language]) {\n background-color: color-mix(in srgb, var(--codextheme-surface) 97%, black 3%) !important;\n}\n\nhtml.codedrobe-codex-skin :is(button, a, input, textarea, [tabindex]):focus-visible {\n outline: 2px solid var(--codextheme-accent) !important;\n outline-offset: 2px !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home {\n position: relative;\n isolation: isolate;\n background: transparent !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home::after {\n content: \"\";\n position: absolute;\n inset: 22px;\n z-index: -1;\n border: 1px solid var(--codextheme-line);\n border-radius: 28px;\n pointer-events: none;\n box-shadow: inset 0 0 80px rgba(0, 0, 0, .18);\n}\n\n#codedrobe-codex-skin-chrome {\n pointer-events: none;\n color: var(--color-token-text-primary, #f5f3ff);\n}\n\n#codedrobe-codex-skin-chrome .dream-brand,\n#codedrobe-codex-skin-chrome .dream-signature {\n color: var(--codextheme-accent);\n text-shadow: 0 2px 18px rgba(0, 0, 0, .75);\n}\n\n#codedrobe-codex-skin-chrome .dream-polaroid,\n#codedrobe-codex-skin-chrome .dream-ribbon,\n#codedrobe-codex-skin-chrome .dream-sparkles {\n display: none !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n html.codedrobe-codex-skin *,\n html.codedrobe-codex-skin *::before,\n html.codedrobe-codex-skin *::after {\n animation-duration: .001ms !important;\n animation-iteration-count: 1 !important;\n scroll-behavior: auto !important;\n transition-duration: .001ms !important;\n }\n}\n",
|
|
22
22
|
"options": {
|
|
23
23
|
"rendererProfile": "codex-theme-v1",
|
|
24
24
|
"baseTheme": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"targets": {
|
|
20
20
|
"codex": {
|
|
21
|
-
"css": ":root.codedrobe-codex-skin {\n color-scheme: dark !important;\n --codextheme-accent: var(--color-token-primary, #9b7bff);\n --codextheme-surface: var(--color-token-bg-primary, #10111a);\n --codextheme-panel: color-mix(in srgb, var(--codextheme-surface) 94%, white 6%);\n --codextheme-panel-soft: color-mix(in srgb, var(--codextheme-surface) 88%, transparent);\n --codextheme-line: color-mix(in srgb, var(--codextheme-accent) 32%, transparent);\n}\n\nhtml.codedrobe-codex-skin body {\n background: #07080d !important;\n color: var(--color-token-text-primary, #f5f3ff) !important;\n}\n\nhtml.codedrobe-codex-skin body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image:\n linear-gradient(90deg, rgba(5, 6, 10, .68) 0%, rgba(5, 6, 10, .28) 46%, rgba(5, 6, 10, .08) 100%),\n var(--codedrobe-image-session-bg);\n background-position: center;\n background-size: cover;\n filter: saturate(.92) contrast(1.04);\n}\n\nhtml.codedrobe-codex-skin body > * {\n position: relative;\n z-index: 1;\n}\n\nhtml.codedrobe-codex-skin aside.app-shell-left-panel {\n background: color-mix(in srgb, var(--codextheme-surface) 82%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: 18px 0 42px rgba(0, 0, 0, .25) !important;\n backdrop-filter: blur(18px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface {\n background: color-mix(in srgb, var(--codextheme-surface) 38%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: inset 0 1px rgba(255, 255, 255, .035), -12px 0 40px rgba(0, 0, 0, .18) !important;\n backdrop-filter: blur(14px) saturate(1.03) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface > header.app-header-tint {\n background: color-mix(in srgb, var(--codextheme-panel) 62%, transparent) !important;\n border-bottom-color: var(--codextheme-line) !important;\n backdrop-filter: blur(18px) !important;\n}\n\nhtml.codedrobe-codex-skin .composer-surface-chrome {\n background: var(--codextheme-panel) !important;\n border: 1px solid var(--codextheme-line) !important;\n box-shadow: 0 18px 50px rgba(0, 0, 0, .28), inset 0 1px rgba(255, 255, 255, .05) !important;\n backdrop-filter: blur(20px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin :is(pre, code, [data-language]) {\n background-color: color-mix(in srgb, var(--codextheme-surface) 97%, black 3%) !important;\n}\n\nhtml.codedrobe-codex-skin :is(button, a, input, textarea, [tabindex]):focus-visible {\n outline: 2px solid var(--codextheme-accent) !important;\n outline-offset: 2px !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home {\n position: relative;\n isolation: isolate;\n background
|
|
21
|
+
"css": ":root.codedrobe-codex-skin {\n color-scheme: dark !important;\n --codextheme-accent: var(--color-token-primary, #9b7bff);\n --codextheme-surface: var(--color-token-bg-primary, #10111a);\n --codextheme-panel: color-mix(in srgb, var(--codextheme-surface) 94%, white 6%);\n --codextheme-panel-soft: color-mix(in srgb, var(--codextheme-surface) 88%, transparent);\n --codextheme-line: color-mix(in srgb, var(--codextheme-accent) 32%, transparent);\n}\n\nhtml.codedrobe-codex-skin body {\n background: #07080d !important;\n color: var(--color-token-text-primary, #f5f3ff) !important;\n}\n\nhtml.codedrobe-codex-skin body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image:\n linear-gradient(90deg, rgba(5, 6, 10, .68) 0%, rgba(5, 6, 10, .28) 46%, rgba(5, 6, 10, .08) 100%),\n var(--codedrobe-image-session-bg);\n background-position: center;\n background-size: cover;\n filter: saturate(.92) contrast(1.04);\n}\n\nhtml.codedrobe-codex-skin body:has(.dream-home)::before {\n background-image:\n linear-gradient(90deg, color-mix(in srgb, var(--codextheme-surface) 94%, transparent), transparent 68%),\n var(--codedrobe-image-hero);\n}\n\nhtml.codedrobe-codex-skin body > * {\n position: relative;\n z-index: 1;\n}\n\nhtml.codedrobe-codex-skin aside.app-shell-left-panel {\n background: color-mix(in srgb, var(--codextheme-surface) 82%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: 18px 0 42px rgba(0, 0, 0, .25) !important;\n backdrop-filter: blur(18px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface {\n background: color-mix(in srgb, var(--codextheme-surface) 38%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: inset 0 1px rgba(255, 255, 255, .035), -12px 0 40px rgba(0, 0, 0, .18) !important;\n backdrop-filter: blur(14px) saturate(1.03) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface > header.app-header-tint {\n background: color-mix(in srgb, var(--codextheme-panel) 62%, transparent) !important;\n border-bottom-color: var(--codextheme-line) !important;\n backdrop-filter: blur(18px) !important;\n}\n\nhtml.codedrobe-codex-skin .composer-surface-chrome {\n background: var(--codextheme-panel) !important;\n border: 1px solid var(--codextheme-line) !important;\n box-shadow: 0 18px 50px rgba(0, 0, 0, .28), inset 0 1px rgba(255, 255, 255, .05) !important;\n backdrop-filter: blur(20px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin :is(pre, code, [data-language]) {\n background-color: color-mix(in srgb, var(--codextheme-surface) 97%, black 3%) !important;\n}\n\nhtml.codedrobe-codex-skin :is(button, a, input, textarea, [tabindex]):focus-visible {\n outline: 2px solid var(--codextheme-accent) !important;\n outline-offset: 2px !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home {\n position: relative;\n isolation: isolate;\n background: transparent !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home::after {\n content: \"\";\n position: absolute;\n inset: 22px;\n z-index: -1;\n border: 1px solid var(--codextheme-line);\n border-radius: 28px;\n pointer-events: none;\n box-shadow: inset 0 0 80px rgba(0, 0, 0, .18);\n}\n\n#codedrobe-codex-skin-chrome {\n pointer-events: none;\n color: var(--color-token-text-primary, #f5f3ff);\n}\n\n#codedrobe-codex-skin-chrome .dream-brand,\n#codedrobe-codex-skin-chrome .dream-signature {\n color: var(--codextheme-accent);\n text-shadow: 0 2px 18px rgba(0, 0, 0, .75);\n}\n\n#codedrobe-codex-skin-chrome .dream-polaroid,\n#codedrobe-codex-skin-chrome .dream-ribbon,\n#codedrobe-codex-skin-chrome .dream-sparkles {\n display: none !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n html.codedrobe-codex-skin *,\n html.codedrobe-codex-skin *::before,\n html.codedrobe-codex-skin *::after {\n animation-duration: .001ms !important;\n animation-iteration-count: 1 !important;\n scroll-behavior: auto !important;\n transition-duration: .001ms !important;\n }\n}\n",
|
|
22
22
|
"options": {
|
|
23
23
|
"rendererProfile": "codex-theme-v1",
|
|
24
24
|
"baseTheme": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"targets": {
|
|
20
20
|
"codex": {
|
|
21
|
-
"css": ":root.codedrobe-codex-skin {\n color-scheme: dark !important;\n --codextheme-accent: var(--color-token-primary, #9b7bff);\n --codextheme-surface: var(--color-token-bg-primary, #10111a);\n --codextheme-panel: color-mix(in srgb, var(--codextheme-surface) 94%, white 6%);\n --codextheme-panel-soft: color-mix(in srgb, var(--codextheme-surface) 88%, transparent);\n --codextheme-line: color-mix(in srgb, var(--codextheme-accent) 32%, transparent);\n}\n\nhtml.codedrobe-codex-skin body {\n background: #07080d !important;\n color: var(--color-token-text-primary, #f5f3ff) !important;\n}\n\nhtml.codedrobe-codex-skin body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image:\n linear-gradient(90deg, rgba(5, 6, 10, .68) 0%, rgba(5, 6, 10, .28) 46%, rgba(5, 6, 10, .08) 100%),\n var(--codedrobe-image-session-bg);\n background-position: center;\n background-size: cover;\n filter: saturate(.92) contrast(1.04);\n}\n\nhtml.codedrobe-codex-skin body > * {\n position: relative;\n z-index: 1;\n}\n\nhtml.codedrobe-codex-skin aside.app-shell-left-panel {\n background: color-mix(in srgb, var(--codextheme-surface) 82%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: 18px 0 42px rgba(0, 0, 0, .25) !important;\n backdrop-filter: blur(18px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface {\n background: color-mix(in srgb, var(--codextheme-surface) 38%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: inset 0 1px rgba(255, 255, 255, .035), -12px 0 40px rgba(0, 0, 0, .18) !important;\n backdrop-filter: blur(14px) saturate(1.03) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface > header.app-header-tint {\n background: color-mix(in srgb, var(--codextheme-panel) 62%, transparent) !important;\n border-bottom-color: var(--codextheme-line) !important;\n backdrop-filter: blur(18px) !important;\n}\n\nhtml.codedrobe-codex-skin .composer-surface-chrome {\n background: var(--codextheme-panel) !important;\n border: 1px solid var(--codextheme-line) !important;\n box-shadow: 0 18px 50px rgba(0, 0, 0, .28), inset 0 1px rgba(255, 255, 255, .05) !important;\n backdrop-filter: blur(20px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin :is(pre, code, [data-language]) {\n background-color: color-mix(in srgb, var(--codextheme-surface) 97%, black 3%) !important;\n}\n\nhtml.codedrobe-codex-skin :is(button, a, input, textarea, [tabindex]):focus-visible {\n outline: 2px solid var(--codextheme-accent) !important;\n outline-offset: 2px !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home {\n position: relative;\n isolation: isolate;\n background
|
|
21
|
+
"css": ":root.codedrobe-codex-skin {\n color-scheme: dark !important;\n --codextheme-accent: var(--color-token-primary, #9b7bff);\n --codextheme-surface: var(--color-token-bg-primary, #10111a);\n --codextheme-panel: color-mix(in srgb, var(--codextheme-surface) 94%, white 6%);\n --codextheme-panel-soft: color-mix(in srgb, var(--codextheme-surface) 88%, transparent);\n --codextheme-line: color-mix(in srgb, var(--codextheme-accent) 32%, transparent);\n}\n\nhtml.codedrobe-codex-skin body {\n background: #07080d !important;\n color: var(--color-token-text-primary, #f5f3ff) !important;\n}\n\nhtml.codedrobe-codex-skin body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n background-image:\n linear-gradient(90deg, rgba(5, 6, 10, .68) 0%, rgba(5, 6, 10, .28) 46%, rgba(5, 6, 10, .08) 100%),\n var(--codedrobe-image-session-bg);\n background-position: center;\n background-size: cover;\n filter: saturate(.92) contrast(1.04);\n}\n\nhtml.codedrobe-codex-skin body:has(.dream-home)::before {\n background-image:\n linear-gradient(90deg, color-mix(in srgb, var(--codextheme-surface) 94%, transparent), transparent 68%),\n var(--codedrobe-image-hero);\n}\n\nhtml.codedrobe-codex-skin body > * {\n position: relative;\n z-index: 1;\n}\n\nhtml.codedrobe-codex-skin aside.app-shell-left-panel {\n background: color-mix(in srgb, var(--codextheme-surface) 82%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: 18px 0 42px rgba(0, 0, 0, .25) !important;\n backdrop-filter: blur(18px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface {\n background: color-mix(in srgb, var(--codextheme-surface) 38%, transparent) !important;\n border-color: var(--codextheme-line) !important;\n box-shadow: inset 0 1px rgba(255, 255, 255, .035), -12px 0 40px rgba(0, 0, 0, .18) !important;\n backdrop-filter: blur(14px) saturate(1.03) !important;\n}\n\nhtml.codedrobe-codex-skin main.main-surface > header.app-header-tint {\n background: color-mix(in srgb, var(--codextheme-panel) 62%, transparent) !important;\n border-bottom-color: var(--codextheme-line) !important;\n backdrop-filter: blur(18px) !important;\n}\n\nhtml.codedrobe-codex-skin .composer-surface-chrome {\n background: var(--codextheme-panel) !important;\n border: 1px solid var(--codextheme-line) !important;\n box-shadow: 0 18px 50px rgba(0, 0, 0, .28), inset 0 1px rgba(255, 255, 255, .05) !important;\n backdrop-filter: blur(20px) saturate(1.08) !important;\n}\n\nhtml.codedrobe-codex-skin :is(pre, code, [data-language]) {\n background-color: color-mix(in srgb, var(--codextheme-surface) 97%, black 3%) !important;\n}\n\nhtml.codedrobe-codex-skin :is(button, a, input, textarea, [tabindex]):focus-visible {\n outline: 2px solid var(--codextheme-accent) !important;\n outline-offset: 2px !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home {\n position: relative;\n isolation: isolate;\n background: transparent !important;\n}\n\nhtml.codedrobe-codex-skin .dream-home::after {\n content: \"\";\n position: absolute;\n inset: 22px;\n z-index: -1;\n border: 1px solid var(--codextheme-line);\n border-radius: 28px;\n pointer-events: none;\n box-shadow: inset 0 0 80px rgba(0, 0, 0, .18);\n}\n\n#codedrobe-codex-skin-chrome {\n pointer-events: none;\n color: var(--color-token-text-primary, #f5f3ff);\n}\n\n#codedrobe-codex-skin-chrome .dream-brand,\n#codedrobe-codex-skin-chrome .dream-signature {\n color: var(--codextheme-accent);\n text-shadow: 0 2px 18px rgba(0, 0, 0, .75);\n}\n\n#codedrobe-codex-skin-chrome .dream-polaroid,\n#codedrobe-codex-skin-chrome .dream-ribbon,\n#codedrobe-codex-skin-chrome .dream-sparkles {\n display: none !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n html.codedrobe-codex-skin *,\n html.codedrobe-codex-skin *::before,\n html.codedrobe-codex-skin *::after {\n animation-duration: .001ms !important;\n animation-iteration-count: 1 !important;\n scroll-behavior: auto !important;\n transition-duration: .001ms !important;\n }\n}\n",
|
|
22
22
|
"options": {
|
|
23
23
|
"rendererProfile": "codex-theme-v1",
|
|
24
24
|
"baseTheme": {
|