@cfbender/cesium 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +304 -0
- package/CHANGELOG.md +335 -0
- package/LICENSE +21 -0
- package/README.md +479 -0
- package/agents/cesium.md +39 -0
- package/assets/styleguide.html +857 -0
- package/package.json +61 -0
- package/src/cli/commands/ls.ts +186 -0
- package/src/cli/commands/open.ts +208 -0
- package/src/cli/commands/prune.ts +348 -0
- package/src/cli/commands/restart.ts +38 -0
- package/src/cli/commands/serve.ts +214 -0
- package/src/cli/commands/stop.ts +130 -0
- package/src/cli/commands/theme.ts +333 -0
- package/src/cli/index.ts +78 -0
- package/src/config.ts +94 -0
- package/src/index.ts +35 -0
- package/src/prompt/system-fragment.md +97 -0
- package/src/render/client-js.ts +316 -0
- package/src/render/controls.ts +302 -0
- package/src/render/critique.ts +360 -0
- package/src/render/extract.ts +83 -0
- package/src/render/scrub.ts +141 -0
- package/src/render/theme.ts +712 -0
- package/src/render/validate.ts +524 -0
- package/src/render/wrap.ts +165 -0
- package/src/server/api.ts +166 -0
- package/src/server/http.ts +195 -0
- package/src/server/lifecycle.ts +331 -0
- package/src/server/stop.ts +124 -0
- package/src/storage/index-cache.ts +71 -0
- package/src/storage/index-gen.ts +447 -0
- package/src/storage/lock.ts +108 -0
- package/src/storage/mutate.ts +396 -0
- package/src/storage/paths.ts +159 -0
- package/src/storage/project-summaries.ts +19 -0
- package/src/storage/theme-write.ts +19 -0
- package/src/storage/write.ts +75 -0
- package/src/tools/ask.ts +353 -0
- package/src/tools/critique.ts +66 -0
- package/src/tools/publish.ts +404 -0
- package/src/tools/stop.ts +53 -0
- package/src/tools/styleguide.ts +23 -0
- package/src/tools/wait.ts +192 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// cesium stop — kill the running cesium server cross-process via PID file.
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { loadConfig, type CesiumConfig } from "../../config.ts";
|
|
5
|
+
import { stopServer } from "../../server/stop.ts";
|
|
6
|
+
import type { StopServerArgs } from "../../server/stop.ts";
|
|
7
|
+
|
|
8
|
+
export interface StopContext {
|
|
9
|
+
stdout: { write: (s: string) => void };
|
|
10
|
+
stderr: { write: (s: string) => void };
|
|
11
|
+
loadConfig?: () => CesiumConfig;
|
|
12
|
+
// Test injection points:
|
|
13
|
+
isAlive?: (pid: number) => boolean;
|
|
14
|
+
killProcess?: (pid: number, signal: NodeJS.Signals) => void;
|
|
15
|
+
sleep?: (ms: number) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function defaultCtx(): StopContext {
|
|
19
|
+
return {
|
|
20
|
+
stdout: process.stdout,
|
|
21
|
+
stderr: process.stderr,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StopOptions {
|
|
26
|
+
force: boolean;
|
|
27
|
+
timeout: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse stop-command argv. Returns null on parse error. */
|
|
31
|
+
export function parseStopArgs(
|
|
32
|
+
argv: string[],
|
|
33
|
+
ctx: Pick<StopContext, "stdout" | "stderr">,
|
|
34
|
+
): StopOptions | null | "help" {
|
|
35
|
+
let values: { force: boolean; timeout: string | undefined; help: boolean };
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const parsed = parseArgs({
|
|
39
|
+
args: argv,
|
|
40
|
+
options: {
|
|
41
|
+
force: { type: "boolean", short: "f", default: false },
|
|
42
|
+
timeout: { type: "string" },
|
|
43
|
+
help: { type: "boolean", short: "h", default: false },
|
|
44
|
+
},
|
|
45
|
+
allowPositionals: false,
|
|
46
|
+
strict: true,
|
|
47
|
+
});
|
|
48
|
+
values = parsed.values as typeof values;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const e = err as Error;
|
|
51
|
+
ctx.stderr.write(`cesium stop: ${e.message}\n`);
|
|
52
|
+
ctx.stderr.write(`Usage: cesium stop [--force] [--timeout <ms>]\n`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (values.help) {
|
|
57
|
+
ctx.stdout.write(
|
|
58
|
+
[
|
|
59
|
+
"Usage: cesium stop [options]",
|
|
60
|
+
"",
|
|
61
|
+
"Options:",
|
|
62
|
+
" --force, -f SIGKILL immediately — skip the SIGTERM grace period",
|
|
63
|
+
" --timeout <ms> Grace period in ms before SIGKILL (default: 3000)",
|
|
64
|
+
" --help, -h Show this help message",
|
|
65
|
+
"",
|
|
66
|
+
"Stops the running cesium server via its PID file. Idempotent when no",
|
|
67
|
+
"server is running.",
|
|
68
|
+
"",
|
|
69
|
+
].join("\n"),
|
|
70
|
+
);
|
|
71
|
+
return "help";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let timeout = 3000;
|
|
75
|
+
if (values.timeout !== undefined) {
|
|
76
|
+
const t = parseInt(values.timeout, 10);
|
|
77
|
+
if (isNaN(t) || t < 0) {
|
|
78
|
+
ctx.stderr.write(`cesium stop: --timeout must be a non-negative integer\n`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
timeout = t;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { force: values.force, timeout };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function stopCommand(argv: string[], ctx?: Partial<StopContext>): Promise<number> {
|
|
88
|
+
const resolved: StopContext = { ...defaultCtx(), ...ctx };
|
|
89
|
+
|
|
90
|
+
const parseResult = parseStopArgs(argv, resolved);
|
|
91
|
+
if (parseResult === null) return 1;
|
|
92
|
+
if (parseResult === "help") return 0;
|
|
93
|
+
|
|
94
|
+
const opts = parseResult;
|
|
95
|
+
const cfg = (resolved.loadConfig ?? loadConfig)();
|
|
96
|
+
|
|
97
|
+
const stopArgs: StopServerArgs = {
|
|
98
|
+
stateDir: cfg.stateDir,
|
|
99
|
+
force: opts.force,
|
|
100
|
+
timeoutMs: opts.timeout,
|
|
101
|
+
};
|
|
102
|
+
if (resolved.isAlive !== undefined) {
|
|
103
|
+
stopArgs.isAlive = resolved.isAlive;
|
|
104
|
+
}
|
|
105
|
+
if (resolved.killProcess !== undefined) {
|
|
106
|
+
stopArgs.killProcess = resolved.killProcess;
|
|
107
|
+
}
|
|
108
|
+
if (resolved.sleep !== undefined) {
|
|
109
|
+
stopArgs.sleep = resolved.sleep;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const outcome = await stopServer(stopArgs);
|
|
113
|
+
|
|
114
|
+
switch (outcome.kind) {
|
|
115
|
+
case "not-running":
|
|
116
|
+
resolved.stdout.write("no cesium server running\n");
|
|
117
|
+
return 0;
|
|
118
|
+
case "stale":
|
|
119
|
+
resolved.stdout.write("server not running (stale PID file removed)\n");
|
|
120
|
+
return 0;
|
|
121
|
+
case "stopped":
|
|
122
|
+
resolved.stdout.write(`stopped cesium server (pid ${outcome.pid}, port ${outcome.port})\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
case "permission-denied":
|
|
125
|
+
resolved.stderr.write(
|
|
126
|
+
`cesium stop: permission denied — process ${outcome.pid} is owned by another user\n`,
|
|
127
|
+
);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// cesium theme — show or apply the configured theme.
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { loadConfig, type CesiumConfig } from "../../config.ts";
|
|
6
|
+
import {
|
|
7
|
+
themeFromPreset,
|
|
8
|
+
mergeTheme,
|
|
9
|
+
type ThemeTokens,
|
|
10
|
+
type ThemePalette,
|
|
11
|
+
} from "../../render/theme.ts";
|
|
12
|
+
import { writeThemeCss, themeCssPath } from "../../storage/theme-write.ts";
|
|
13
|
+
import { themeTokensCss } from "../../render/theme.ts";
|
|
14
|
+
import { atomicWrite } from "../../storage/write.ts";
|
|
15
|
+
import { readdir } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
export interface ThemeContext {
|
|
19
|
+
stdout: { write: (s: string) => void };
|
|
20
|
+
stderr: { write: (s: string) => void };
|
|
21
|
+
loadConfig?: () => CesiumConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultCtx(): ThemeContext {
|
|
25
|
+
return {
|
|
26
|
+
stdout: process.stdout,
|
|
27
|
+
stderr: process.stderr,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveTheme(cfg: CesiumConfig): { theme: ThemeTokens; presetLabel: string } {
|
|
32
|
+
const theme = mergeTheme(themeFromPreset(cfg.themePreset), cfg.theme);
|
|
33
|
+
let presetLabel: string;
|
|
34
|
+
if (cfg.themePreset === undefined) {
|
|
35
|
+
presetLabel = "claret-dark (default)";
|
|
36
|
+
} else if (cfg.themePreset === "claret") {
|
|
37
|
+
presetLabel = "claret-dark (alias for claret)";
|
|
38
|
+
} else {
|
|
39
|
+
presetLabel = cfg.themePreset;
|
|
40
|
+
}
|
|
41
|
+
return { theme, presetLabel };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printTokenTable(
|
|
45
|
+
out: { write: (s: string) => void },
|
|
46
|
+
theme: ThemeTokens,
|
|
47
|
+
presetLabel: string,
|
|
48
|
+
cssPath: string,
|
|
49
|
+
writeNeeded: boolean,
|
|
50
|
+
): void {
|
|
51
|
+
out.write(`Resolved theme: ${presetLabel}\n\n`);
|
|
52
|
+
out.write("Tokens:\n");
|
|
53
|
+
const { colors } = theme;
|
|
54
|
+
const keys: (keyof ThemePalette)[] = [
|
|
55
|
+
"bg",
|
|
56
|
+
"surface",
|
|
57
|
+
"surface2",
|
|
58
|
+
"oat",
|
|
59
|
+
"rule",
|
|
60
|
+
"ink",
|
|
61
|
+
"inkSoft",
|
|
62
|
+
"muted",
|
|
63
|
+
"accent",
|
|
64
|
+
"olive",
|
|
65
|
+
"codeBg",
|
|
66
|
+
"codeFg",
|
|
67
|
+
];
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
out.write(` ${key.padEnd(12)}${colors[key]}\n`);
|
|
70
|
+
}
|
|
71
|
+
out.write("\n");
|
|
72
|
+
const suffix = writeNeeded ? " (write needed)" : "";
|
|
73
|
+
out.write(`theme.css path: ${cssPath}${suffix}\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function isWriteNeeded(cssPath: string, theme: ThemeTokens): Promise<boolean> {
|
|
77
|
+
const expected = themeTokensCss(theme) + "\n";
|
|
78
|
+
try {
|
|
79
|
+
const existing = await readFile(cssPath, "utf8");
|
|
80
|
+
return existing !== expected;
|
|
81
|
+
} catch {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Retrofit logic ───────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compute the relative path to theme.css from a given file path within stateDir.
|
|
90
|
+
* - artifact: <stateDir>/projects/<slug>/artifacts/<file>.html → ../../../theme.css
|
|
91
|
+
* - project index: <stateDir>/projects/<slug>/index.html → ../../theme.css
|
|
92
|
+
* - global index: <stateDir>/index.html → theme.css
|
|
93
|
+
*/
|
|
94
|
+
function relThemeCssPath(filePath: string, stateDir: string): string {
|
|
95
|
+
// Normalize: strip stateDir prefix
|
|
96
|
+
const norm = stateDir.endsWith("/") ? stateDir : stateDir + "/";
|
|
97
|
+
const rel = filePath.startsWith(norm) ? filePath.slice(norm.length) : filePath;
|
|
98
|
+
const parts = rel.split("/");
|
|
99
|
+
// parts.length - 1 = directory depth
|
|
100
|
+
const depth = parts.length - 1;
|
|
101
|
+
if (depth === 0) return "theme.css"; // root = global index
|
|
102
|
+
if (depth === 2) return "../../theme.css"; // projects/<slug>/index.html
|
|
103
|
+
if (depth === 3) return "../../../theme.css"; // projects/<slug>/artifacts/*.html
|
|
104
|
+
// Fallback: go up depth directories
|
|
105
|
+
return "../".repeat(depth) + "theme.css";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function retrofitFile(
|
|
109
|
+
filePath: string,
|
|
110
|
+
stateDir: string,
|
|
111
|
+
out: { write: (s: string) => void },
|
|
112
|
+
): Promise<boolean> {
|
|
113
|
+
let html: string;
|
|
114
|
+
try {
|
|
115
|
+
html = await readFile(filePath, "utf8");
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const relPath = relThemeCssPath(filePath, stateDir);
|
|
121
|
+
const linkTag = `<link rel="stylesheet" href="${relPath}">`;
|
|
122
|
+
|
|
123
|
+
// Check if already has this exact link
|
|
124
|
+
if (html.includes(linkTag)) return false;
|
|
125
|
+
|
|
126
|
+
// Insert before </head>
|
|
127
|
+
const newHtml = html.replace("</head>", `${linkTag}\n</head>`);
|
|
128
|
+
if (newHtml === html) return false; // no </head> found — skip
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await atomicWrite(filePath, newHtml);
|
|
132
|
+
out.write(` retrofitted ${filePath}\n`);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function retrofitAll(
|
|
140
|
+
stateDir: string,
|
|
141
|
+
out: { write: (s: string) => void },
|
|
142
|
+
): Promise<{ artifacts: number; indexes: number }> {
|
|
143
|
+
// Collect all file paths to potentially retrofit
|
|
144
|
+
const tasks: Array<{ filePath: string; kind: "artifact" | "index" }> = [];
|
|
145
|
+
|
|
146
|
+
// Global index
|
|
147
|
+
tasks.push({ filePath: join(stateDir, "index.html"), kind: "index" });
|
|
148
|
+
|
|
149
|
+
// Walk projects
|
|
150
|
+
const projectsDir = join(stateDir, "projects");
|
|
151
|
+
let slugs: string[] = [];
|
|
152
|
+
try {
|
|
153
|
+
slugs = await readdir(projectsDir);
|
|
154
|
+
} catch {
|
|
155
|
+
// No projects dir yet
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const perSlugFiles = await Promise.all(
|
|
159
|
+
slugs.map(async (slug) => {
|
|
160
|
+
const projectDir = join(projectsDir, slug);
|
|
161
|
+
const results: Array<{ filePath: string; kind: "artifact" | "index" }> = [];
|
|
162
|
+
|
|
163
|
+
results.push({ filePath: join(projectDir, "index.html"), kind: "index" });
|
|
164
|
+
|
|
165
|
+
const artifactsDir = join(projectDir, "artifacts");
|
|
166
|
+
let files: string[] = [];
|
|
167
|
+
try {
|
|
168
|
+
files = (await readdir(artifactsDir)).filter((f) => f.endsWith(".html"));
|
|
169
|
+
} catch {
|
|
170
|
+
// No artifacts dir yet
|
|
171
|
+
}
|
|
172
|
+
for (const filename of files) {
|
|
173
|
+
results.push({ filePath: join(artifactsDir, filename), kind: "artifact" });
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
for (const batch of perSlugFiles) {
|
|
180
|
+
for (const item of batch) {
|
|
181
|
+
tasks.push(item);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Retrofit all files in parallel
|
|
186
|
+
const results = await Promise.all(
|
|
187
|
+
tasks.map(async ({ filePath, kind }) => {
|
|
188
|
+
const changed = await retrofitFile(filePath, stateDir, out);
|
|
189
|
+
return { changed, kind };
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
let artifacts = 0;
|
|
194
|
+
let indexes = 0;
|
|
195
|
+
for (const { changed, kind } of results) {
|
|
196
|
+
if (changed) {
|
|
197
|
+
if (kind === "artifact") artifacts++;
|
|
198
|
+
else indexes++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { artifacts, indexes };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Command ──────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export async function themeCommand(argv: string[], ctx?: Partial<ThemeContext>): Promise<number> {
|
|
208
|
+
const resolved: ThemeContext = { ...defaultCtx(), ...ctx };
|
|
209
|
+
|
|
210
|
+
const subcommand = argv[0];
|
|
211
|
+
const rest = argv.slice(1);
|
|
212
|
+
|
|
213
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
214
|
+
resolved.stdout.write(
|
|
215
|
+
[
|
|
216
|
+
"Usage: cesium theme <subcommand> [options]",
|
|
217
|
+
"",
|
|
218
|
+
"Subcommands:",
|
|
219
|
+
" show Print resolved theme tokens",
|
|
220
|
+
" apply [--rewrite-artifacts] Write theme.css from current config",
|
|
221
|
+
"",
|
|
222
|
+
"Options:",
|
|
223
|
+
" --help, -h Show this help message",
|
|
224
|
+
"",
|
|
225
|
+
].join("\n"),
|
|
226
|
+
);
|
|
227
|
+
return subcommand ? 0 : 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (subcommand === "show") {
|
|
231
|
+
return themeShowCommand(rest, resolved);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (subcommand === "apply") {
|
|
235
|
+
return themeApplyCommand(rest, resolved);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
resolved.stderr.write(`cesium theme: unknown subcommand: ${subcommand}\n`);
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function themeShowCommand(argv: string[], ctx: ThemeContext): Promise<number> {
|
|
243
|
+
let values: { help: boolean };
|
|
244
|
+
try {
|
|
245
|
+
const parsed = parseArgs({
|
|
246
|
+
args: argv,
|
|
247
|
+
options: { help: { type: "boolean", short: "h", default: false } },
|
|
248
|
+
allowPositionals: false,
|
|
249
|
+
strict: true,
|
|
250
|
+
});
|
|
251
|
+
values = parsed.values as typeof values;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
const e = err as Error;
|
|
254
|
+
ctx.stderr.write(`cesium theme show: ${e.message}\n`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (values.help) {
|
|
259
|
+
ctx.stdout.write("Usage: cesium theme show\n\nPrint resolved theme tokens.\n\n");
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
264
|
+
const { theme, presetLabel } = resolveTheme(cfg);
|
|
265
|
+
const cssPath = themeCssPath(cfg.stateDir);
|
|
266
|
+
const writeNeeded = await isWriteNeeded(cssPath, theme);
|
|
267
|
+
|
|
268
|
+
printTokenTable(ctx.stdout, theme, presetLabel, cssPath, writeNeeded);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function themeApplyCommand(argv: string[], ctx: ThemeContext): Promise<number> {
|
|
273
|
+
let values: { "rewrite-artifacts": boolean; help: boolean };
|
|
274
|
+
try {
|
|
275
|
+
const parsed = parseArgs({
|
|
276
|
+
args: argv,
|
|
277
|
+
options: {
|
|
278
|
+
"rewrite-artifacts": { type: "boolean", default: false },
|
|
279
|
+
help: { type: "boolean", short: "h", default: false },
|
|
280
|
+
},
|
|
281
|
+
allowPositionals: false,
|
|
282
|
+
strict: true,
|
|
283
|
+
});
|
|
284
|
+
values = parsed.values as typeof values;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const e = err as Error;
|
|
287
|
+
ctx.stderr.write(`cesium theme apply: ${e.message}\n`);
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (values.help) {
|
|
292
|
+
ctx.stdout.write(
|
|
293
|
+
[
|
|
294
|
+
"Usage: cesium theme apply [--rewrite-artifacts]",
|
|
295
|
+
"",
|
|
296
|
+
"Write theme.css from the current config.",
|
|
297
|
+
"",
|
|
298
|
+
"Options:",
|
|
299
|
+
" --rewrite-artifacts Retrofit existing artifacts and index pages with the theme link",
|
|
300
|
+
" --help, -h Show this help message",
|
|
301
|
+
"",
|
|
302
|
+
].join("\n"),
|
|
303
|
+
);
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
308
|
+
const { theme, presetLabel } = resolveTheme(cfg);
|
|
309
|
+
const cssPath = await writeThemeCss(cfg.stateDir, theme);
|
|
310
|
+
|
|
311
|
+
if (values["rewrite-artifacts"]) {
|
|
312
|
+
const { artifacts, indexes } = await retrofitAll(cfg.stateDir, ctx.stdout);
|
|
313
|
+
ctx.stdout.write(
|
|
314
|
+
[
|
|
315
|
+
`Wrote ${cssPath} (${presetLabel} preset).`,
|
|
316
|
+
`Retrofitted ${artifacts} artifact${artifacts !== 1 ? "s" : ""} and ${indexes} index${indexes !== 1 ? "es" : ""} with the theme link.`,
|
|
317
|
+
"Files without prior theme links now pick up theme.css.",
|
|
318
|
+
"",
|
|
319
|
+
].join("\n"),
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
ctx.stdout.write(
|
|
323
|
+
[
|
|
324
|
+
`Wrote ${cssPath} (${presetLabel} preset).`,
|
|
325
|
+
"Existing artifacts continue rendering with their inline fallback theme.",
|
|
326
|
+
"Run with --rewrite-artifacts to retrofit them.",
|
|
327
|
+
"",
|
|
328
|
+
].join("\n"),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { parseArgs as _parseArgs } from "node:util";
|
|
4
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
5
|
+
import { lsCommand } from "./commands/ls.ts";
|
|
6
|
+
import { openCommand } from "./commands/open.ts";
|
|
7
|
+
import { serveCommand } from "./commands/serve.ts";
|
|
8
|
+
import { stopCommand } from "./commands/stop.ts";
|
|
9
|
+
import { restartCommand } from "./commands/restart.ts";
|
|
10
|
+
import { pruneCommand } from "./commands/prune.ts";
|
|
11
|
+
import { themeCommand } from "./commands/theme.ts";
|
|
12
|
+
|
|
13
|
+
const subcommand = process.argv[2];
|
|
14
|
+
const rest = process.argv.slice(3);
|
|
15
|
+
|
|
16
|
+
export const CESIUM_VERSION: string = pkg.version;
|
|
17
|
+
|
|
18
|
+
const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
|
|
19
|
+
ls: lsCommand,
|
|
20
|
+
open: openCommand,
|
|
21
|
+
serve: serveCommand,
|
|
22
|
+
stop: stopCommand,
|
|
23
|
+
restart: restartCommand,
|
|
24
|
+
prune: pruneCommand,
|
|
25
|
+
theme: themeCommand,
|
|
26
|
+
version: async () => {
|
|
27
|
+
process.stdout.write(`cesium ${CESIUM_VERSION}\n`);
|
|
28
|
+
return 0;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function printHelp(): void {
|
|
33
|
+
process.stdout.write(
|
|
34
|
+
[
|
|
35
|
+
"cesium — artifact manager for opencode sessions",
|
|
36
|
+
"",
|
|
37
|
+
"Usage: cesium <command> [options]",
|
|
38
|
+
"",
|
|
39
|
+
"Commands:",
|
|
40
|
+
" ls List artifacts in the current project (or all with --all)",
|
|
41
|
+
" open Open an artifact by id prefix in the browser",
|
|
42
|
+
" serve Start the local HTTP server in the foreground",
|
|
43
|
+
" stop Stop the running cesium server",
|
|
44
|
+
" restart Stop and re-start the cesium server",
|
|
45
|
+
" prune Delete artifacts older than a given duration",
|
|
46
|
+
" theme Show or apply the configured theme",
|
|
47
|
+
" version Print the cesium version",
|
|
48
|
+
"",
|
|
49
|
+
"Options:",
|
|
50
|
+
" --help, -h Show this help message",
|
|
51
|
+
" --version, -v Print the cesium version",
|
|
52
|
+
"",
|
|
53
|
+
"Run 'cesium <command> --help' for command-specific options.",
|
|
54
|
+
"",
|
|
55
|
+
].join("\n"),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main(): Promise<void> {
|
|
60
|
+
if (subcommand === "--version" || subcommand === "-v") {
|
|
61
|
+
process.stdout.write(`cesium ${CESIUM_VERSION}\n`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
65
|
+
printHelp();
|
|
66
|
+
process.exit(subcommand ? 0 : 1);
|
|
67
|
+
}
|
|
68
|
+
const fn = COMMANDS[subcommand];
|
|
69
|
+
if (!fn) {
|
|
70
|
+
process.stderr.write(`cesium: unknown command: ${subcommand}\n`);
|
|
71
|
+
printHelp();
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const code = await fn(rest);
|
|
75
|
+
process.exit(code);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await main();
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Config loader — reads and validates ~/.config/opencode/cesium.json.
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { ThemePalette } from "./render/theme.ts";
|
|
7
|
+
|
|
8
|
+
export interface CesiumConfig {
|
|
9
|
+
stateDir: string;
|
|
10
|
+
port: number;
|
|
11
|
+
portMax: number;
|
|
12
|
+
idleTimeoutMs: number;
|
|
13
|
+
hostname: string;
|
|
14
|
+
themePreset?: string;
|
|
15
|
+
theme?: Partial<ThemePalette>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function defaultConfig(env?: NodeJS.ProcessEnv): CesiumConfig {
|
|
19
|
+
const e = env ?? process.env;
|
|
20
|
+
const xdgState = e["XDG_STATE_HOME"];
|
|
21
|
+
const stateDir = xdgState
|
|
22
|
+
? join(xdgState, "cesium")
|
|
23
|
+
: join(homedir(), ".local", "state", "cesium");
|
|
24
|
+
return {
|
|
25
|
+
stateDir,
|
|
26
|
+
port: 3030,
|
|
27
|
+
portMax: 3050,
|
|
28
|
+
idleTimeoutMs: 30 * 60 * 1000,
|
|
29
|
+
hostname: "127.0.0.1",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RawConfig {
|
|
34
|
+
stateDir?: unknown;
|
|
35
|
+
port?: unknown;
|
|
36
|
+
portMax?: unknown;
|
|
37
|
+
idleTimeoutMs?: unknown;
|
|
38
|
+
hostname?: unknown;
|
|
39
|
+
themePreset?: unknown;
|
|
40
|
+
theme?: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadConfig(opts?: { configPath?: string; env?: NodeJS.ProcessEnv }): CesiumConfig {
|
|
44
|
+
const env = opts?.env ?? process.env;
|
|
45
|
+
const base = defaultConfig(env);
|
|
46
|
+
|
|
47
|
+
const configPath =
|
|
48
|
+
opts?.configPath ??
|
|
49
|
+
join(env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config"), "opencode", "cesium.json");
|
|
50
|
+
|
|
51
|
+
let fileConfig: RawConfig = {};
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(configPath, "utf8");
|
|
54
|
+
const parsed: unknown = JSON.parse(raw);
|
|
55
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
56
|
+
fileConfig = parsed as RawConfig;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// File missing or unreadable — use defaults
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const merged: CesiumConfig = { ...base };
|
|
63
|
+
|
|
64
|
+
if (typeof fileConfig.stateDir === "string") merged.stateDir = fileConfig.stateDir;
|
|
65
|
+
if (typeof fileConfig.port === "number") merged.port = fileConfig.port;
|
|
66
|
+
if (typeof fileConfig.portMax === "number") merged.portMax = fileConfig.portMax;
|
|
67
|
+
if (typeof fileConfig.idleTimeoutMs === "number") merged.idleTimeoutMs = fileConfig.idleTimeoutMs;
|
|
68
|
+
if (typeof fileConfig.hostname === "string" && fileConfig.hostname.length > 0) {
|
|
69
|
+
merged.hostname = fileConfig.hostname;
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
fileConfig.theme !== null &&
|
|
73
|
+
typeof fileConfig.theme === "object" &&
|
|
74
|
+
!Array.isArray(fileConfig.theme)
|
|
75
|
+
) {
|
|
76
|
+
merged.theme = fileConfig.theme as Partial<ThemePalette>;
|
|
77
|
+
}
|
|
78
|
+
if (typeof fileConfig.themePreset === "string") merged.themePreset = fileConfig.themePreset;
|
|
79
|
+
|
|
80
|
+
// Env overrides
|
|
81
|
+
const envPort = env["CESIUM_PORT"];
|
|
82
|
+
if (envPort !== undefined) {
|
|
83
|
+
const p = parseInt(envPort, 10);
|
|
84
|
+
if (!isNaN(p)) merged.port = p;
|
|
85
|
+
}
|
|
86
|
+
const envStateDir = env["CESIUM_STATE_DIR"];
|
|
87
|
+
if (envStateDir !== undefined) merged.stateDir = envStateDir;
|
|
88
|
+
const envHost = env["CESIUM_HOSTNAME"];
|
|
89
|
+
if (envHost !== undefined && envHost.length > 0) merged.hostname = envHost;
|
|
90
|
+
const envThemePreset = env["CESIUM_THEME_PRESET"];
|
|
91
|
+
if (envThemePreset !== undefined) merged.themePreset = envThemePreset;
|
|
92
|
+
|
|
93
|
+
return merged;
|
|
94
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Plugin entry point — registers cesium tools and injects the system-prompt fragment.
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import type { Plugin, Hooks } from "@opencode-ai/plugin";
|
|
7
|
+
import { createPublishTool } from "./tools/publish.ts";
|
|
8
|
+
import { createAskTool } from "./tools/ask.ts";
|
|
9
|
+
import { createWaitTool } from "./tools/wait.ts";
|
|
10
|
+
import { createStyleguideTool } from "./tools/styleguide.ts";
|
|
11
|
+
import { createCritiqueTool } from "./tools/critique.ts";
|
|
12
|
+
import { createStopTool } from "./tools/stop.ts";
|
|
13
|
+
|
|
14
|
+
const PROMPT_FRAGMENT = await readFile(
|
|
15
|
+
join(dirname(fileURLToPath(import.meta.url)), "prompt/system-fragment.md"),
|
|
16
|
+
"utf8",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const CesiumPlugin: Plugin = async (ctx): Promise<Hooks> => {
|
|
20
|
+
return {
|
|
21
|
+
tool: {
|
|
22
|
+
cesium_publish: createPublishTool(ctx),
|
|
23
|
+
cesium_ask: createAskTool(ctx),
|
|
24
|
+
cesium_wait: createWaitTool(ctx),
|
|
25
|
+
cesium_styleguide: createStyleguideTool(ctx),
|
|
26
|
+
cesium_critique: createCritiqueTool(ctx),
|
|
27
|
+
cesium_stop: createStopTool(ctx),
|
|
28
|
+
},
|
|
29
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
30
|
+
output.system.push(PROMPT_FRAGMENT);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default CesiumPlugin;
|