idcmd 0.0.11 → 0.0.13

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 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 (V1)
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 (V1)
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
- ## V1 Definition Of Done
101
+ ## Definition Of Done
94
102
 
95
- `tickets/ROADMAP.md` is the source of truth. For V1, we explicitly target:
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.11",
3
+ "version": "0.0.13",
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
- "preact": "^10.28.3",
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
  },
@@ -25,7 +25,8 @@ export const buildCommand = async (): Promise<number> => {
25
25
  const cssProc = Bun.spawn(
26
26
  [
27
27
  "bunx",
28
- "@tailwindcss/cli",
28
+ "--no-install",
29
+ "tailwindcss",
29
30
  "-i",
30
31
  tailwindInput,
31
32
  "-o",
@@ -38,7 +38,8 @@ const spawnCssProcess = (
38
38
  Bun.spawn(
39
39
  [
40
40
  "bunx",
41
- "@tailwindcss/cli",
41
+ "--no-install",
42
+ "tailwindcss",
42
43
  "-i",
43
44
  tailwindInput,
44
45
  "-o",
@@ -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 fillReadme = (args: { siteName: string; text: string }): string =>
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
- fillReadme({
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(`Target directory is not empty: ${targetDir}`);
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 (relativePath: string): Promise<Response | null> => {
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
- const usage = (): string =>
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(cmd);
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
- cmd: string | null
359
+ parsed: ParsedArgs
127
360
  ): Promise<number | null> => {
128
- if (isHelpCommand(cmd)) {
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 null;
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(usage());
382
+ console.log(globalHelp());
153
383
  return 1;
154
384
  };