idcmd 0.0.11 → 0.0.12
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 +12 -4
- package/package.json +2 -3
- package/src/cli/commands/build.ts +2 -1
- package/src/cli/commands/dev.ts +2 -1
- package/src/cli/commands/init.ts +12 -3
- package/src/cli/commands/preview.ts +36 -3
- package/src/cli/main.ts +244 -14
- package/src/render/layout.tsx +28 -31
- package/src/render/right-rail.tsx +5 -3
- package/src/search/index.ts +19 -24
- package/src/search/page.tsx +13 -11
- package/src/server/user-routes.ts +1 -1
- package/templates/default/.github/workflows/ci.yml +0 -3
- package/templates/default/README.md +1 -2
- package/templates/default/package.json +0 -1
- package/templates/default/src/server.ts +1 -1
- package/templates/default/src/ui/layout.tsx +27 -26
- package/templates/default/src/ui/right-rail.tsx +6 -3
- package/templates/default/src/ui/search-page.tsx +13 -10
- package/templates/default/tsconfig.json +1 -1
- package/templates/default/scripts/smoke.ts +0 -223
package/README.md
CHANGED
|
@@ -22,6 +22,14 @@ idcmd deploy # build + generate deploy files (Vercel/Fly/Railway)
|
|
|
22
22
|
idcmd client ... # add/update local src implementations
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
For full command docs (flags, examples, side effects), use:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
idcmd --help
|
|
29
|
+
idcmd init --help
|
|
30
|
+
idcmd deploy --help
|
|
31
|
+
```
|
|
32
|
+
|
|
25
33
|
### Deploy targets
|
|
26
34
|
|
|
27
35
|
```bash
|
|
@@ -36,7 +44,7 @@ idcmd deploy --vercel
|
|
|
36
44
|
|
|
37
45
|
`idcmd init --yes` is provider-neutral by default (no provider files generated).
|
|
38
46
|
|
|
39
|
-
## Layout
|
|
47
|
+
## Layout
|
|
40
48
|
|
|
41
49
|
- `content/<slug>.md` -> `/<slug>/` (`index.md` -> `/`)
|
|
42
50
|
- `src/ui/*` is local UI source code (you own and edit these files)
|
|
@@ -80,7 +88,7 @@ This is a new page.
|
|
|
80
88
|
|
|
81
89
|
It renders at `/hello/`.
|
|
82
90
|
|
|
83
|
-
## Custom Server Routes
|
|
91
|
+
## Custom Server Routes
|
|
84
92
|
|
|
85
93
|
Add `src/routes/api/hello.ts`:
|
|
86
94
|
|
|
@@ -90,9 +98,9 @@ export const GET = (): Response => Response.json({ ok: true });
|
|
|
90
98
|
|
|
91
99
|
It responds at `/api/hello`.
|
|
92
100
|
|
|
93
|
-
##
|
|
101
|
+
## Definition Of Done
|
|
94
102
|
|
|
95
|
-
|
|
103
|
+
We explicitly target:
|
|
96
104
|
|
|
97
105
|
- Content routes ship `0` bytes of JavaScript by default (both SSR output and built HTML).
|
|
98
106
|
- Content routes ship a small, opinionated JavaScript runtime by default (prefetch + optional right-rail behavior).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idcmd",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/rustydotwtf/idcmd"
|
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
"prepare": "lefthook install"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"
|
|
39
|
-
"preact-render-to-string": "^6.6.5",
|
|
38
|
+
"@kitajs/html": "^4.2.13",
|
|
40
39
|
"shiki": "^3.22.0",
|
|
41
40
|
"zod": "^3.24.0"
|
|
42
41
|
},
|
package/src/cli/commands/dev.ts
CHANGED
package/src/cli/commands/init.ts
CHANGED
|
@@ -116,7 +116,7 @@ const fillPackageJson = (args: {
|
|
|
116
116
|
.replaceAll("__IDCMD_IDCMD_VERSION__", `^${args.idcmdVersion}`)
|
|
117
117
|
.replaceAll("__IDCMD_DEV_PORT__", String(args.port));
|
|
118
118
|
|
|
119
|
-
const
|
|
119
|
+
const fillSiteNameTokens = (args: { siteName: string; text: string }): string =>
|
|
120
120
|
args.text
|
|
121
121
|
.replaceAll("__IDCMD_SITE_NAME__", args.siteName)
|
|
122
122
|
.replaceAll("IDCMD_SITE_NAME", args.siteName);
|
|
@@ -253,7 +253,14 @@ const applySubstitutions = async (args: {
|
|
|
253
253
|
);
|
|
254
254
|
|
|
255
255
|
await replaceInFile(joinPath(args.targetDir, "README.md"), (text) =>
|
|
256
|
-
|
|
256
|
+
fillSiteNameTokens({
|
|
257
|
+
siteName: args.siteName,
|
|
258
|
+
text,
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
await replaceInFile(joinPath(args.targetDir, "content", "index.md"), (text) =>
|
|
263
|
+
fillSiteNameTokens({
|
|
257
264
|
siteName: args.siteName,
|
|
258
265
|
text,
|
|
259
266
|
})
|
|
@@ -282,7 +289,9 @@ const printNextSteps = (dir: string): void => {
|
|
|
282
289
|
const assertEmptyTargetDir = async (targetDir: string): Promise<void> => {
|
|
283
290
|
const empty = await isDirEmpty(targetDir);
|
|
284
291
|
if (!empty) {
|
|
285
|
-
throw new Error(
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Target directory is not empty: ${targetDir}\nUse an empty directory or run \`idcmd init <new-directory>\`.`
|
|
294
|
+
);
|
|
286
295
|
}
|
|
287
296
|
};
|
|
288
297
|
|
|
@@ -6,12 +6,15 @@ import {
|
|
|
6
6
|
const stripLeadingSlash = (pathname: string): string =>
|
|
7
7
|
pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
8
8
|
|
|
9
|
-
const tryServeFile = async (
|
|
9
|
+
const tryServeFile = async (
|
|
10
|
+
relativePath: string,
|
|
11
|
+
status = 200
|
|
12
|
+
): Promise<Response | null> => {
|
|
10
13
|
const file = Bun.file(`public/${stripLeadingSlash(relativePath)}`);
|
|
11
14
|
if (!(await file.exists())) {
|
|
12
15
|
return null;
|
|
13
16
|
}
|
|
14
|
-
return new Response(file);
|
|
17
|
+
return new Response(file, { status });
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
const toHtmlEntryPath = (canonicalPathname: string): string =>
|
|
@@ -32,10 +35,36 @@ const serveHtml = async (pathname: string): Promise<Response> => {
|
|
|
32
35
|
return served;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
const notFound = await tryServeFile("404.html");
|
|
38
|
+
const notFound = await tryServeFile("404.html", 404);
|
|
36
39
|
return notFound ?? new Response("Not Found", { status: 404 });
|
|
37
40
|
};
|
|
38
41
|
|
|
42
|
+
type ShutdownSignal = "SIGINT" | "SIGTERM";
|
|
43
|
+
|
|
44
|
+
const waitForShutdownSignal = (): Promise<ShutdownSignal> => {
|
|
45
|
+
const { promise, resolve } = Promise.withResolvers<ShutdownSignal>();
|
|
46
|
+
|
|
47
|
+
const handleSigInt = (): void => {
|
|
48
|
+
cleanup();
|
|
49
|
+
resolve("SIGINT");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleSigTerm = (): void => {
|
|
53
|
+
cleanup();
|
|
54
|
+
resolve("SIGTERM");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const cleanup = (): void => {
|
|
58
|
+
process.off("SIGINT", handleSigInt);
|
|
59
|
+
process.off("SIGTERM", handleSigTerm);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
process.on("SIGINT", handleSigInt);
|
|
63
|
+
process.on("SIGTERM", handleSigTerm);
|
|
64
|
+
|
|
65
|
+
return promise;
|
|
66
|
+
};
|
|
67
|
+
|
|
39
68
|
export const previewCommand = async (port: number): Promise<number> => {
|
|
40
69
|
const exists = await Bun.file("public/index.html").exists();
|
|
41
70
|
if (!exists) {
|
|
@@ -56,5 +85,9 @@ export const previewCommand = async (port: number): Promise<number> => {
|
|
|
56
85
|
|
|
57
86
|
// eslint-disable-next-line no-console
|
|
58
87
|
console.log(`Preview running at http://localhost:${server.port}`);
|
|
88
|
+
|
|
89
|
+
await waitForShutdownSignal();
|
|
90
|
+
server.stop(true);
|
|
91
|
+
|
|
59
92
|
return 0;
|
|
60
93
|
};
|
package/src/cli/main.ts
CHANGED
|
@@ -13,11 +13,24 @@ import { parsePort } from "./normalize";
|
|
|
13
13
|
import { readPackageVersion } from "./version";
|
|
14
14
|
|
|
15
15
|
const DEFAULT_PREVIEW_PORT = 4173;
|
|
16
|
+
const DEFAULT_DEV_PORT = 4000;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
type CommandName = "init" | "dev" | "build" | "preview" | "deploy" | "client";
|
|
19
|
+
|
|
20
|
+
const isCommandName = (value: string): value is CommandName =>
|
|
21
|
+
value === "init" ||
|
|
22
|
+
value === "dev" ||
|
|
23
|
+
value === "build" ||
|
|
24
|
+
value === "preview" ||
|
|
25
|
+
value === "deploy" ||
|
|
26
|
+
value === "client";
|
|
27
|
+
|
|
28
|
+
const globalHelp = (): string =>
|
|
18
29
|
[
|
|
19
30
|
"idcmd",
|
|
20
31
|
"",
|
|
32
|
+
"Static docs site CLI optimized for agent workflows.",
|
|
33
|
+
"",
|
|
21
34
|
"Usage:",
|
|
22
35
|
" idcmd init [dir] [--yes] [--name <name>] [--description <text>] [--base-url <url>] [--port <port>] [--no-git] [--vercel|--fly|--railway]",
|
|
23
36
|
" idcmd dev [--port <port>]",
|
|
@@ -26,8 +39,181 @@ const usage = (): string =>
|
|
|
26
39
|
" idcmd deploy [--vercel|--fly|--railway]",
|
|
27
40
|
" idcmd client <add|update> <layout|right-rail|search-page|runtime|all> [--dry-run] [--yes]",
|
|
28
41
|
"",
|
|
42
|
+
"Agent Quickstart:",
|
|
43
|
+
" 1. idcmd init my-docs --yes --no-git",
|
|
44
|
+
" 2. cd my-docs && bun install",
|
|
45
|
+
` 3. idcmd dev --port ${String(DEFAULT_DEV_PORT)}`,
|
|
46
|
+
" 4. idcmd build",
|
|
47
|
+
` 5. idcmd preview --port ${String(DEFAULT_PREVIEW_PORT)}`,
|
|
48
|
+
" 6. idcmd deploy --vercel|--fly|--railway",
|
|
49
|
+
"",
|
|
50
|
+
"Commands:",
|
|
51
|
+
" init Scaffold a new site in a target directory",
|
|
52
|
+
" dev Run SSR server + Tailwind/runtime watchers",
|
|
53
|
+
" build Generate static output in public/",
|
|
54
|
+
" preview Serve public/ locally with canonical routing",
|
|
55
|
+
" deploy Build + generate provider config files",
|
|
56
|
+
" client Add/update local UI/runtime baseline files",
|
|
57
|
+
"",
|
|
58
|
+
"Help:",
|
|
59
|
+
" idcmd --help",
|
|
60
|
+
" idcmd <command> --help",
|
|
61
|
+
" idcmd --version",
|
|
62
|
+
"",
|
|
29
63
|
].join("\n");
|
|
30
64
|
|
|
65
|
+
const initHelp = (): string =>
|
|
66
|
+
[
|
|
67
|
+
"idcmd init",
|
|
68
|
+
"",
|
|
69
|
+
"Scaffold a new idcmd site from the default template.",
|
|
70
|
+
"",
|
|
71
|
+
"Usage:",
|
|
72
|
+
" idcmd init [dir] [--yes] [--name <name>] [--description <text>] [--base-url <url>] [--port <port>] [--no-git] [--vercel|--fly|--railway]",
|
|
73
|
+
"",
|
|
74
|
+
"Flags:",
|
|
75
|
+
" --yes Non-interactive defaults",
|
|
76
|
+
" --name Site name in site.jsonc",
|
|
77
|
+
" --description Site description in site.jsonc",
|
|
78
|
+
" --base-url Base URL for canonicals + sitemap",
|
|
79
|
+
` --port Default dev script port (default: ${String(DEFAULT_DEV_PORT)})`,
|
|
80
|
+
" --no-git Skip `git init` in target dir",
|
|
81
|
+
" --vercel Generate vercel.json",
|
|
82
|
+
" --fly Generate fly.toml + Docker files",
|
|
83
|
+
" --railway Generate railway.json + Docker files",
|
|
84
|
+
"",
|
|
85
|
+
"Side effects:",
|
|
86
|
+
" - Writes template files into target directory",
|
|
87
|
+
" - Fails if target directory is not empty",
|
|
88
|
+
" - Runs `git init` unless --no-git is set",
|
|
89
|
+
"",
|
|
90
|
+
"Examples:",
|
|
91
|
+
" idcmd init my-docs --yes --no-git",
|
|
92
|
+
" idcmd init apps/docs --yes --base-url https://docs.example.com",
|
|
93
|
+
"",
|
|
94
|
+
].join("\n");
|
|
95
|
+
|
|
96
|
+
const devHelp = (): string =>
|
|
97
|
+
[
|
|
98
|
+
"idcmd dev",
|
|
99
|
+
"",
|
|
100
|
+
"Run local development: SSR server + Tailwind + runtime bundling in watch mode.",
|
|
101
|
+
"",
|
|
102
|
+
"Usage:",
|
|
103
|
+
" idcmd dev [--port <port>]",
|
|
104
|
+
"",
|
|
105
|
+
"Flags:",
|
|
106
|
+
` --port HTTP port (default: ${String(DEFAULT_DEV_PORT)})`,
|
|
107
|
+
"",
|
|
108
|
+
"Side effects:",
|
|
109
|
+
" - Writes compiled runtime scripts to public/_idcmd/*.js",
|
|
110
|
+
" - Writes compiled CSS to public/styles.css",
|
|
111
|
+
"",
|
|
112
|
+
"Examples:",
|
|
113
|
+
" idcmd dev",
|
|
114
|
+
" idcmd dev --port 4010",
|
|
115
|
+
"",
|
|
116
|
+
].join("\n");
|
|
117
|
+
|
|
118
|
+
const buildHelp = (): string =>
|
|
119
|
+
[
|
|
120
|
+
"idcmd build",
|
|
121
|
+
"",
|
|
122
|
+
"Build static site output in public/.",
|
|
123
|
+
"",
|
|
124
|
+
"Usage:",
|
|
125
|
+
" idcmd build",
|
|
126
|
+
"",
|
|
127
|
+
"What it does:",
|
|
128
|
+
" - Compiles runtime assets from src/runtime/ to public/_idcmd/",
|
|
129
|
+
" - Builds Tailwind CSS to public/styles.css",
|
|
130
|
+
" - Renders markdown pages and metadata outputs (llms.txt, search index, sitemap/robots when baseUrl is set)",
|
|
131
|
+
"",
|
|
132
|
+
"Examples:",
|
|
133
|
+
" idcmd build",
|
|
134
|
+
"",
|
|
135
|
+
].join("\n");
|
|
136
|
+
|
|
137
|
+
const previewHelp = (): string =>
|
|
138
|
+
[
|
|
139
|
+
"idcmd preview",
|
|
140
|
+
"",
|
|
141
|
+
"Serve generated public/ output locally with canonical route behavior.",
|
|
142
|
+
"",
|
|
143
|
+
"Usage:",
|
|
144
|
+
" idcmd preview [--port <port>]",
|
|
145
|
+
"",
|
|
146
|
+
"Flags:",
|
|
147
|
+
` --port HTTP port (default: ${String(DEFAULT_PREVIEW_PORT)})`,
|
|
148
|
+
"",
|
|
149
|
+
"Notes:",
|
|
150
|
+
" - Requires public/index.html (run idcmd build first)",
|
|
151
|
+
"",
|
|
152
|
+
"Examples:",
|
|
153
|
+
" idcmd preview",
|
|
154
|
+
" idcmd preview --port 5000",
|
|
155
|
+
"",
|
|
156
|
+
].join("\n");
|
|
157
|
+
|
|
158
|
+
const deployHelp = (): string =>
|
|
159
|
+
[
|
|
160
|
+
"idcmd deploy",
|
|
161
|
+
"",
|
|
162
|
+
"Build project and print provider-specific deployment guidance.",
|
|
163
|
+
"",
|
|
164
|
+
"Usage:",
|
|
165
|
+
" idcmd deploy [--vercel|--fly|--railway]",
|
|
166
|
+
"",
|
|
167
|
+
"Flags (choose one provider):",
|
|
168
|
+
" --vercel Generate vercel.json",
|
|
169
|
+
" --fly Generate fly.toml + Docker files",
|
|
170
|
+
" --railway Generate railway.json + Docker files",
|
|
171
|
+
"",
|
|
172
|
+
"Side effects:",
|
|
173
|
+
" - Runs idcmd build first",
|
|
174
|
+
" - May write provider config files in project root",
|
|
175
|
+
"",
|
|
176
|
+
"Examples:",
|
|
177
|
+
" idcmd deploy --vercel",
|
|
178
|
+
" idcmd deploy --fly",
|
|
179
|
+
"",
|
|
180
|
+
].join("\n");
|
|
181
|
+
|
|
182
|
+
const clientHelp = (): string =>
|
|
183
|
+
[
|
|
184
|
+
"idcmd client",
|
|
185
|
+
"",
|
|
186
|
+
"Copy baseline local implementation files into src/ui/ and src/runtime/.",
|
|
187
|
+
"",
|
|
188
|
+
"Usage:",
|
|
189
|
+
" idcmd client <add|update> <layout|right-rail|search-page|runtime|all> [--dry-run] [--yes]",
|
|
190
|
+
"",
|
|
191
|
+
"Flags:",
|
|
192
|
+
" --dry-run Preview changes without writing files",
|
|
193
|
+
" --yes Skip overwrite prompts for update mode",
|
|
194
|
+
"",
|
|
195
|
+
"Side effects:",
|
|
196
|
+
" - add: creates missing files only",
|
|
197
|
+
" - update: overwrites selected files (requires --yes unless --dry-run)",
|
|
198
|
+
"",
|
|
199
|
+
"Examples:",
|
|
200
|
+
" idcmd client add all",
|
|
201
|
+
" idcmd client update runtime --dry-run",
|
|
202
|
+
" idcmd client update layout --yes",
|
|
203
|
+
"",
|
|
204
|
+
].join("\n");
|
|
205
|
+
|
|
206
|
+
const HELP_BUILDERS: Record<CommandName, () => string> = {
|
|
207
|
+
build: buildHelp,
|
|
208
|
+
client: clientHelp,
|
|
209
|
+
deploy: deployHelp,
|
|
210
|
+
dev: devHelp,
|
|
211
|
+
init: initHelp,
|
|
212
|
+
preview: previewHelp,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const commandHelp = (command: CommandName): string => HELP_BUILDERS[command]();
|
|
216
|
+
|
|
31
217
|
const asStringFlag = (value: unknown): string | undefined =>
|
|
32
218
|
typeof value === "string" ? value : undefined;
|
|
33
219
|
|
|
@@ -94,6 +280,53 @@ const isHelpCommand = (cmd: string | null): boolean =>
|
|
|
94
280
|
const isVersionCommand = (cmd: string): boolean =>
|
|
95
281
|
cmd === "--version" || cmd === "-v";
|
|
96
282
|
|
|
283
|
+
const hasSubcommandHelpFlag = (parsed: ParsedArgs): boolean =>
|
|
284
|
+
parsed.flags.help === true ||
|
|
285
|
+
parsed.positionals.includes("--help") ||
|
|
286
|
+
parsed.positionals.includes("-h");
|
|
287
|
+
|
|
288
|
+
const maybeHandleGeneralHelp = (cmd: string | null): number | null => {
|
|
289
|
+
if (!isHelpCommand(cmd)) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
console.log(globalHelp());
|
|
293
|
+
return 0;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const maybeHandleVersion = async (
|
|
297
|
+
cmd: string | null
|
|
298
|
+
): Promise<number | null> => {
|
|
299
|
+
if (!cmd || !isVersionCommand(cmd)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
console.log(await readPackageVersion());
|
|
303
|
+
return 0;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const maybeHandleHelpCommand = (parsed: ParsedArgs): number | null => {
|
|
307
|
+
if (parsed.command !== "help") {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const [topic] = parsed.positionals;
|
|
311
|
+
if (topic && isCommandName(topic)) {
|
|
312
|
+
console.log(commandHelp(topic));
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
console.log(globalHelp());
|
|
316
|
+
return 0;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const maybeHandleSubcommandHelp = (parsed: ParsedArgs): number | null => {
|
|
320
|
+
if (!parsed.command || !isCommandName(parsed.command)) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
if (!hasSubcommandHelpFlag(parsed)) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
console.log(commandHelp(parsed.command));
|
|
327
|
+
return 0;
|
|
328
|
+
};
|
|
329
|
+
|
|
97
330
|
export const main = async (argv: string[]): Promise<void> => {
|
|
98
331
|
try {
|
|
99
332
|
const code = await runMain(argv);
|
|
@@ -109,7 +342,7 @@ const runMain = async (argv: string[]): Promise<number> => {
|
|
|
109
342
|
const parsed = parseArgs(argv);
|
|
110
343
|
const cmd = parsed.command;
|
|
111
344
|
|
|
112
|
-
const metaCode = await maybeHandleMetaCommand(
|
|
345
|
+
const metaCode = await maybeHandleMetaCommand(parsed);
|
|
113
346
|
if (metaCode !== null) {
|
|
114
347
|
return metaCode;
|
|
115
348
|
}
|
|
@@ -123,19 +356,16 @@ const runMain = async (argv: string[]): Promise<number> => {
|
|
|
123
356
|
};
|
|
124
357
|
|
|
125
358
|
const maybeHandleMetaCommand = async (
|
|
126
|
-
|
|
359
|
+
parsed: ParsedArgs
|
|
127
360
|
): Promise<number | null> => {
|
|
128
|
-
|
|
129
|
-
console.log(usage());
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (cmd && isVersionCommand(cmd)) {
|
|
134
|
-
console.log(await readPackageVersion());
|
|
135
|
-
return 0;
|
|
136
|
-
}
|
|
361
|
+
const cmd = parsed.command;
|
|
137
362
|
|
|
138
|
-
return
|
|
363
|
+
return (
|
|
364
|
+
maybeHandleGeneralHelp(cmd) ??
|
|
365
|
+
(await maybeHandleVersion(cmd)) ??
|
|
366
|
+
maybeHandleHelpCommand(parsed) ??
|
|
367
|
+
maybeHandleSubcommandHelp(parsed)
|
|
368
|
+
);
|
|
139
369
|
};
|
|
140
370
|
|
|
141
371
|
const resolveCommandHandler = (
|
|
@@ -149,6 +379,6 @@ const resolveCommandHandler = (
|
|
|
149
379
|
|
|
150
380
|
const handleUnknownCommand = (cmd: string | null): number => {
|
|
151
381
|
console.error(`Unknown command: ${cmd ?? "(none)"}`);
|
|
152
|
-
console.log(
|
|
382
|
+
console.log(globalHelp());
|
|
153
383
|
return 1;
|
|
154
384
|
};
|
package/src/render/layout.tsx
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
/* eslint-disable react/
|
|
2
|
-
import type { JSX } from "preact";
|
|
3
|
-
|
|
4
|
-
import { render } from "preact-render-to-string";
|
|
1
|
+
/* eslint-disable react/jsx-key */
|
|
5
2
|
|
|
6
3
|
import type { NavGroup, NavItem } from "../content/navigation";
|
|
7
4
|
import type { ResolvedRightRailConfig } from "../site/config";
|
|
@@ -10,6 +7,8 @@ import type { TocItem } from "./toc";
|
|
|
10
7
|
|
|
11
8
|
import { RightRail } from "./right-rail";
|
|
12
9
|
|
|
10
|
+
const escapeText = (value: string): string => Bun.escapeHTML(value);
|
|
11
|
+
|
|
13
12
|
export interface LayoutProps {
|
|
14
13
|
title: string;
|
|
15
14
|
siteName: string;
|
|
@@ -31,10 +30,7 @@ export interface LayoutProps {
|
|
|
31
30
|
export type RenderLayout = (props: LayoutProps) => string;
|
|
32
31
|
|
|
33
32
|
const Icon = ({ svg }: { svg: string }): JSX.Element => (
|
|
34
|
-
<span
|
|
35
|
-
class="inline-flex w-[18px] h-[18px]"
|
|
36
|
-
dangerouslySetInnerHTML={{ __html: svg }}
|
|
37
|
-
/>
|
|
33
|
+
<span class="inline-flex w-[18px] h-[18px]">{svg}</span>
|
|
38
34
|
);
|
|
39
35
|
|
|
40
36
|
const isActiveLink = (item: NavItem, currentPath: string): boolean =>
|
|
@@ -59,7 +55,7 @@ const NavLink = ({
|
|
|
59
55
|
class={`flex items-center gap-3 px-3 py-1.5 text-sm hover:text-sidebar-foreground transition-colors ${activeClass}`}
|
|
60
56
|
>
|
|
61
57
|
<Icon svg={item.iconSvg} />
|
|
62
|
-
<span>{item.title}</span>
|
|
58
|
+
<span>{escapeText(item.title)}</span>
|
|
63
59
|
</a>
|
|
64
60
|
);
|
|
65
61
|
};
|
|
@@ -73,11 +69,11 @@ const NavGroupComponent = ({
|
|
|
73
69
|
}): JSX.Element => (
|
|
74
70
|
<div class="py-2">
|
|
75
71
|
<div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
76
|
-
{group.label}
|
|
72
|
+
{escapeText(group.label)}
|
|
77
73
|
</div>
|
|
78
74
|
<nav class="space-y-1">
|
|
79
75
|
{group.items.map((item) => (
|
|
80
|
-
<NavLink
|
|
76
|
+
<NavLink item={item} currentPath={currentPath} />
|
|
81
77
|
))}
|
|
82
78
|
</nav>
|
|
83
79
|
</div>
|
|
@@ -100,16 +96,12 @@ const Sidebar = ({
|
|
|
100
96
|
data-prefetch="hover"
|
|
101
97
|
>
|
|
102
98
|
<span class="text-muted-foreground">~/</span>
|
|
103
|
-
{siteName}
|
|
99
|
+
{escapeText(siteName)}
|
|
104
100
|
</a>
|
|
105
101
|
</div>
|
|
106
102
|
<div class="sidebar-content">
|
|
107
103
|
{navigation.map((group) => (
|
|
108
|
-
<NavGroupComponent
|
|
109
|
-
key={group.id}
|
|
110
|
-
group={group}
|
|
111
|
-
currentPath={currentPath}
|
|
112
|
-
/>
|
|
104
|
+
<NavGroupComponent group={group} currentPath={currentPath} />
|
|
113
105
|
))}
|
|
114
106
|
</div>
|
|
115
107
|
</aside>
|
|
@@ -121,19 +113,20 @@ const SearchForm = ({ query }: { query?: string }): JSX.Element => (
|
|
|
121
113
|
action="/search/"
|
|
122
114
|
class="flex w-full items-center"
|
|
123
115
|
role="search"
|
|
124
|
-
|
|
116
|
+
novalidate
|
|
125
117
|
>
|
|
126
|
-
|
|
118
|
+
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
|
119
|
+
<label for="site-search" class="sr-only">
|
|
127
120
|
Search pages
|
|
128
121
|
</label>
|
|
129
122
|
<input
|
|
130
123
|
id="site-search"
|
|
131
124
|
name="q"
|
|
132
125
|
type="search"
|
|
133
|
-
|
|
126
|
+
autocomplete="off"
|
|
134
127
|
spellcheck={false}
|
|
135
128
|
placeholder="Search..."
|
|
136
|
-
|
|
129
|
+
value={escapeText(query ?? "")}
|
|
137
130
|
class="w-full border-b border-input bg-transparent px-1 py-1.5 text-sm placeholder:text-muted-foreground focus:border-foreground focus:outline-none transition-colors"
|
|
138
131
|
/>
|
|
139
132
|
</form>
|
|
@@ -155,7 +148,7 @@ const TopNavbar = ({
|
|
|
155
148
|
data-prefetch="hover"
|
|
156
149
|
>
|
|
157
150
|
<span class="text-muted-foreground">~/</span>
|
|
158
|
-
{siteName}
|
|
151
|
+
{escapeText(siteName)}
|
|
159
152
|
</a>
|
|
160
153
|
<div class="not-prose w-full max-w-xs ml-auto">
|
|
161
154
|
<SearchForm query={query} />
|
|
@@ -183,14 +176,16 @@ const DocumentHead = ({
|
|
|
183
176
|
<head>
|
|
184
177
|
<meta charset="utf-8" />
|
|
185
178
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
186
|
-
<title>{title}</title>
|
|
187
|
-
{description ?
|
|
179
|
+
<title>{escapeText(title)}</title>
|
|
180
|
+
{description ? (
|
|
181
|
+
<meta name="description" content={escapeText(description)} />
|
|
182
|
+
) : null}
|
|
188
183
|
{canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
|
|
189
184
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
190
185
|
<link
|
|
191
186
|
rel="preconnect"
|
|
192
187
|
href="https://fonts.gstatic.com"
|
|
193
|
-
|
|
188
|
+
crossorigin="anonymous"
|
|
194
189
|
/>
|
|
195
190
|
<link
|
|
196
191
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
|
|
@@ -269,8 +264,10 @@ const DocumentBody = ({
|
|
|
269
264
|
<div class="mx-auto flex w-full max-w-6xl items-start gap-10">
|
|
270
265
|
<article
|
|
271
266
|
class={`prose min-w-0 flex-1${currentPath === "/" ? " prose-home" : ""}`}
|
|
272
|
-
|
|
273
|
-
|
|
267
|
+
>
|
|
268
|
+
{/* content is pre-rendered markdown HTML */}
|
|
269
|
+
{content}
|
|
270
|
+
</article>
|
|
274
271
|
{shouldShowRightRail ? (
|
|
275
272
|
<RightRailComponent
|
|
276
273
|
canonicalUrl={canonicalUrl}
|
|
@@ -282,12 +279,12 @@ const DocumentBody = ({
|
|
|
282
279
|
</div>
|
|
283
280
|
</main>
|
|
284
281
|
<footer class="site-footer">
|
|
285
|
-
Built with
|
|
286
|
-
|
|
282
|
+
Built with idcmd SSR + Tailwind | Zero JavaScript on content
|
|
283
|
+
pages
|
|
287
284
|
</footer>
|
|
288
285
|
</div>
|
|
289
286
|
{scriptPaths.map((scriptPath) => (
|
|
290
|
-
<script
|
|
287
|
+
<script defer src={scriptPath} />
|
|
291
288
|
))}
|
|
292
289
|
</body>
|
|
293
290
|
);
|
|
@@ -344,4 +341,4 @@ const Layout = ({
|
|
|
344
341
|
};
|
|
345
342
|
|
|
346
343
|
export const renderLayout: RenderLayout = (props) =>
|
|
347
|
-
`<!DOCTYPE html>${
|
|
344
|
+
`<!DOCTYPE html>${<Layout {...props} />}`;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable react/jsx-key */
|
|
2
2
|
|
|
3
3
|
import type { ResolvedRightRailConfig } from "../site/config";
|
|
4
4
|
import type { TocItem } from "./toc";
|
|
5
5
|
|
|
6
|
+
const escapeText = (value: string): string => Bun.escapeHTML(value);
|
|
7
|
+
|
|
6
8
|
const CaretDownIcon = (): JSX.Element => (
|
|
7
9
|
<svg
|
|
8
10
|
width="16"
|
|
@@ -171,13 +173,13 @@ const OnThisPage = ({ items }: { items: TocItem[] }): JSX.Element => (
|
|
|
171
173
|
<div class="toc-scroll min-h-0 flex-1" data-toc-scroll-container="1">
|
|
172
174
|
<ul class="space-y-2 text-sm text-muted-foreground">
|
|
173
175
|
{items.map((item) => (
|
|
174
|
-
<li
|
|
176
|
+
<li class={item.level >= 3 ? "pl-3" : ""}>
|
|
175
177
|
<a
|
|
176
178
|
href={`#${encodeURIComponent(item.id)}`}
|
|
177
179
|
class="hover:text-foreground"
|
|
178
180
|
data-toc-link="1"
|
|
179
181
|
>
|
|
180
|
-
{item.text}
|
|
182
|
+
{escapeText(item.text)}
|
|
181
183
|
</a>
|
|
182
184
|
</li>
|
|
183
185
|
))}
|