idcmd 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +23 -4
  2. package/package.json +2 -2
  3. package/src/build.ts +4 -3
  4. package/src/cli/commands/build.ts +7 -0
  5. package/src/cli/commands/client.ts +317 -0
  6. package/src/cli/commands/dev.ts +89 -24
  7. package/src/cli/commands/init.ts +93 -2
  8. package/src/cli/main.ts +12 -0
  9. package/src/cli/runtime-assets.ts +92 -0
  10. package/src/client/index.ts +7 -1
  11. package/src/render/layout-loader.ts +6 -3
  12. package/src/render/layout.tsx +10 -2
  13. package/src/render/page-renderer.ts +12 -2
  14. package/src/render/right-rail-loader.ts +49 -0
  15. package/src/render/right-rail.tsx +10 -6
  16. package/src/search/page.tsx +4 -2
  17. package/src/search/search-page-loader.ts +51 -0
  18. package/src/search/server-page.ts +52 -18
  19. package/templates/default/.github/workflows/ci.yml +24 -0
  20. package/templates/default/README.md +23 -0
  21. package/templates/default/package.json +2 -1
  22. package/templates/default/scripts/check-internal.ts +56 -0
  23. package/templates/default/scripts/check.ts +318 -0
  24. package/templates/default/scripts/smoke.ts +193 -0
  25. package/templates/default/site/client/layout.tsx +237 -2
  26. package/templates/default/site/client/right-rail.tsx +246 -1
  27. package/templates/default/site/{public/_idcmd/llm-menu.js → client/runtime/llm-menu.ts} +27 -18
  28. package/templates/default/site/{public/_idcmd/nav-prefetch.js → client/runtime/nav-prefetch.ts} +3 -3
  29. package/templates/default/site/{public/_idcmd/right-rail-scrollspy.js → client/runtime/right-rail-scrollspy.ts} +73 -32
  30. package/templates/default/site/client/search-page.tsx +87 -1
  31. package/templates/default/tsconfig.json +1 -1
  32. /package/templates/default/site/{public/_idcmd/live-reload.js → client/runtime/live-reload.ts} +0 -0
package/README.md CHANGED
@@ -19,16 +19,33 @@ idcmd dev # tailwind watch + SSR dev server
19
19
  idcmd build # static dist/
20
20
  idcmd preview # serve dist/ locally
21
21
  idcmd deploy # build + validate Vercel static deploy config
22
+ idcmd client ... # add/update local site/client implementations
22
23
  ```
23
24
 
24
25
  ## Layout (V1)
25
26
 
26
27
  - `site/content/<slug>.md` -> `/<slug>/` (`index.md` -> `/`)
28
+ - `site/client/*` is local source code (you own and edit these files)
29
+ - `site/client/runtime/*.ts` is local browser runtime code (compiled to `site/public/_idcmd/*.js`)
27
30
  - `site/styles/tailwind.css` -> `site/public/styles.css` (dev) / `dist/styles.css` (build)
28
31
  - `site/public/` static assets
29
32
  - `site/server/routes/**` file-based server routes (dev/server-host only)
30
33
  - `site/site.jsonc` site config
31
34
 
35
+ ## Syncing Local Client Code
36
+
37
+ Use these commands to pull baseline UI implementations into your project:
38
+
39
+ ```bash
40
+ idcmd client add all
41
+ idcmd client update all --dry-run
42
+ idcmd client update layout --yes
43
+ idcmd client update runtime --yes
44
+ ```
45
+
46
+ `add` creates missing files. `update` overwrites changed files and requires `--yes` unless `--dry-run` is used.
47
+ Runtime files in `site/client/runtime/` are compiled automatically by `idcmd dev` and `idcmd build`.
48
+
32
49
  ## Example: Add A Page
33
50
 
34
51
  Create `site/content/hello.md`:
@@ -63,6 +80,7 @@ It responds at `/api/hello`.
63
80
  `tickets/ROADMAP.md` is the source of truth. For V1, we explicitly target:
64
81
 
65
82
  - Content routes ship `0` bytes of JavaScript by default (both SSR output and built HTML).
83
+ - Content routes ship a small, opinionated JavaScript runtime by default (prefetch + optional right-rail behavior).
66
84
  - Search index size `<= 5 MB` for `<= 2,000` pages.
67
85
  - Build completes in `<= 60s` for `<= 2,000` pages on a typical laptop.
68
86
 
@@ -90,8 +108,9 @@ It responds at `/api/hello`.
90
108
 
91
109
  ### JS policy
92
110
 
93
- - Content routes ship `0` bytes of JavaScript by default.
94
- - Allowed scripts:
111
+ - Content routes ship lightweight runtime scripts by default.
112
+ - Script behavior:
113
+ - Always: `/_idcmd/nav-prefetch.js`
95
114
  - Dev only: `/_idcmd/live-reload.js`
96
- - Optional: `/_idcmd/right-rail-scrollspy.js` only when scrollspy is enabled and the computed TOC is non-empty
97
- - Search page is SSR-only by default (no client JS).
115
+ - Right rail enabled: `/_idcmd/llm-menu.js`
116
+ - Right rail scrollspy enabled with non-empty TOC: `/_idcmd/right-rail-scrollspy.js`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idcmd",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/rustydotwtf/idcmd"
@@ -28,7 +28,7 @@
28
28
  "build": "bun run build:css && bun src/build.ts",
29
29
  "preview": "bunx serve dist",
30
30
  "start": "bun src/server.ts",
31
- "check": "ultracite check && bun run typecheck && bun run test",
31
+ "check": "bun run scripts/check.ts",
32
32
  "test": "bun test",
33
33
  "typecheck": "tsc --noEmit -p tsconfig.json",
34
34
  "fix": "ultracite fix",
package/src/build.ts CHANGED
@@ -5,7 +5,7 @@ import { scanContentFiles, slugFromContentFile } from "./content/paths";
5
5
  import { getProjectPaths } from "./project/paths";
6
6
  import { renderDocument, renderMarkdownPage } from "./render/page-renderer";
7
7
  import { generateSearchIndexFromContent } from "./search/index";
8
- import { renderSearchPageContent } from "./search/page";
8
+ import { getRenderSearchPageContent } from "./search/search-page-loader";
9
9
  import {
10
10
  collectSitemapPagesFromContent,
11
11
  generateRobotsTxt,
@@ -100,13 +100,14 @@ const cssSource = await resolveCssSource();
100
100
  const inlineCss = cssSource ? await Bun.file(cssSource).text() : undefined;
101
101
  const cssPath = inlineCss ? undefined : "/styles.css";
102
102
 
103
- const renderStaticSearchPage = (): Promise<string> => {
103
+ const renderStaticSearchPage = async (): Promise<string> => {
104
+ const renderSearchPage = await getRenderSearchPageContent();
104
105
  const topPages = navigation
105
106
  .flatMap((group) => group.items)
106
107
  .slice(0, 8)
107
108
  .map((item) => ({ href: item.href, title: item.title }));
108
109
 
109
- const contentHtml = renderSearchPageContent({
110
+ const contentHtml = renderSearchPage({
110
111
  minQueryLength: MIN_SEARCH_QUERY_LENGTH,
111
112
  query: "",
112
113
  results: [],
@@ -1,3 +1,5 @@
1
+ import { compileRuntimeAssetsOnce } from "../runtime-assets";
2
+
1
3
  const findTailwindInput = async (): Promise<string> => {
2
4
  const candidates = ["site/styles/tailwind.css", "content/styles.css"];
3
5
  for (const path of candidates) {
@@ -16,6 +18,11 @@ const idcmdBuildEntry = (): string =>
16
18
  Bun.fileURLToPath(new URL("../../build.ts", import.meta.url));
17
19
 
18
20
  export const buildCommand = async (): Promise<number> => {
21
+ const runtimeCode = await compileRuntimeAssetsOnce();
22
+ if (runtimeCode !== 0) {
23
+ return runtimeCode;
24
+ }
25
+
19
26
  const tailwindInput = await findTailwindInput();
20
27
 
21
28
  const cssProc = Bun.spawn(
@@ -0,0 +1,317 @@
1
+ import { ensureDir } from "../fs";
2
+ import { dirname, joinPath } from "../path";
3
+
4
+ const TEMPLATE_CLIENT_DIR = joinPath(
5
+ import.meta.dir,
6
+ "..",
7
+ "..",
8
+ "..",
9
+ "templates",
10
+ "default",
11
+ "site",
12
+ "client"
13
+ );
14
+ const TEMPLATE_RUNTIME_DIR = joinPath(TEMPLATE_CLIENT_DIR, "runtime");
15
+
16
+ const SITE_CLIENT_DIR = joinPath("site", "client");
17
+ const SITE_RUNTIME_DIR = joinPath(SITE_CLIENT_DIR, "runtime");
18
+ const SITE_CONFIG_PATH = joinPath("site", "site.jsonc");
19
+
20
+ const CLIENT_PARTS = [
21
+ "layout",
22
+ "right-rail",
23
+ "search-page",
24
+ "runtime",
25
+ ] as const;
26
+ type ClientPart = (typeof CLIENT_PARTS)[number];
27
+ type ClientAction = "add" | "update";
28
+
29
+ const RUNTIME_FILES = [
30
+ "live-reload.ts",
31
+ "llm-menu.ts",
32
+ "nav-prefetch.ts",
33
+ "right-rail-scrollspy.ts",
34
+ ] as const;
35
+
36
+ interface ClientFileSpec {
37
+ fileName: string;
38
+ targetPath: string;
39
+ templatePath: string;
40
+ }
41
+
42
+ export interface ClientFlags {
43
+ dryRun?: boolean;
44
+ yes?: boolean;
45
+ }
46
+
47
+ interface ParsedClientArgs {
48
+ action: ClientAction;
49
+ parts: ClientPart[];
50
+ }
51
+
52
+ interface FilePlan {
53
+ fileName: string;
54
+ nextText: string;
55
+ part: ClientPart;
56
+ reason: string;
57
+ shouldWrite: boolean;
58
+ targetPath: string;
59
+ }
60
+
61
+ const getFileSpecsForPart = (part: ClientPart): ClientFileSpec[] => {
62
+ if (part === "layout") {
63
+ return [
64
+ {
65
+ fileName: "layout.tsx",
66
+ targetPath: joinPath(SITE_CLIENT_DIR, "layout.tsx"),
67
+ templatePath: joinPath(TEMPLATE_CLIENT_DIR, "layout.tsx"),
68
+ },
69
+ ];
70
+ }
71
+
72
+ if (part === "right-rail") {
73
+ return [
74
+ {
75
+ fileName: "right-rail.tsx",
76
+ targetPath: joinPath(SITE_CLIENT_DIR, "right-rail.tsx"),
77
+ templatePath: joinPath(TEMPLATE_CLIENT_DIR, "right-rail.tsx"),
78
+ },
79
+ ];
80
+ }
81
+
82
+ if (part === "search-page") {
83
+ return [
84
+ {
85
+ fileName: "search-page.tsx",
86
+ targetPath: joinPath(SITE_CLIENT_DIR, "search-page.tsx"),
87
+ templatePath: joinPath(TEMPLATE_CLIENT_DIR, "search-page.tsx"),
88
+ },
89
+ ];
90
+ }
91
+
92
+ return RUNTIME_FILES.map((fileName) => ({
93
+ fileName,
94
+ targetPath: joinPath(SITE_RUNTIME_DIR, fileName),
95
+ templatePath: joinPath(TEMPLATE_RUNTIME_DIR, fileName),
96
+ }));
97
+ };
98
+
99
+ const isClientAction = (value: string): value is ClientAction =>
100
+ value === "add" || value === "update";
101
+
102
+ const isClientPart = (value: string): value is ClientPart =>
103
+ CLIENT_PARTS.includes(value as ClientPart);
104
+
105
+ const parseClientPart = (value: string | undefined): ClientPart[] => {
106
+ if (!value || value === "all") {
107
+ return [...CLIENT_PARTS];
108
+ }
109
+
110
+ if (!isClientPart(value)) {
111
+ throw new Error(
112
+ `Unknown client part: ${value}. Expected one of ${CLIENT_PARTS.join(", ")} or all.`
113
+ );
114
+ }
115
+
116
+ return [value];
117
+ };
118
+
119
+ const parseClientArgs = (positionals: string[]): ParsedClientArgs => {
120
+ const [actionRaw, partRaw] = positionals;
121
+ if (!actionRaw || !isClientAction(actionRaw)) {
122
+ throw new Error(
123
+ "Usage: idcmd client <add|update> <layout|right-rail|search-page|runtime|all> [--dry-run] [--yes]"
124
+ );
125
+ }
126
+
127
+ return {
128
+ action: actionRaw,
129
+ parts: parseClientPart(partRaw),
130
+ };
131
+ };
132
+
133
+ const ensureSiteLayout = async (): Promise<void> => {
134
+ if (!(await Bun.file(SITE_CONFIG_PATH).exists())) {
135
+ throw new Error(
136
+ `Could not find ${SITE_CONFIG_PATH}. Run this command from an idcmd site project root.`
137
+ );
138
+ }
139
+ };
140
+
141
+ const readTemplateFile = async (spec: ClientFileSpec): Promise<string> => {
142
+ const file = Bun.file(spec.templatePath);
143
+ if (!(await file.exists())) {
144
+ throw new Error(`Missing template file: ${spec.templatePath}`);
145
+ }
146
+ return file.text();
147
+ };
148
+
149
+ const classifyAddPlan = async (
150
+ part: ClientPart,
151
+ spec: ClientFileSpec,
152
+ nextText: string
153
+ ): Promise<FilePlan> => {
154
+ const exists = await Bun.file(spec.targetPath).exists();
155
+
156
+ return {
157
+ fileName: spec.fileName,
158
+ nextText,
159
+ part,
160
+ reason: exists ? "exists" : "missing",
161
+ shouldWrite: !exists,
162
+ targetPath: spec.targetPath,
163
+ };
164
+ };
165
+
166
+ const classifyUpdatePlan = async (
167
+ part: ClientPart,
168
+ spec: ClientFileSpec,
169
+ nextText: string
170
+ ): Promise<FilePlan> => {
171
+ const file = Bun.file(spec.targetPath);
172
+
173
+ if (!(await file.exists())) {
174
+ return {
175
+ fileName: spec.fileName,
176
+ nextText,
177
+ part,
178
+ reason: "missing",
179
+ shouldWrite: false,
180
+ targetPath: spec.targetPath,
181
+ };
182
+ }
183
+
184
+ const currentText = await file.text();
185
+ const isUpToDate = currentText === nextText;
186
+
187
+ return {
188
+ fileName: spec.fileName,
189
+ nextText,
190
+ part,
191
+ reason: isUpToDate ? "up-to-date" : "changed",
192
+ shouldWrite: !isUpToDate,
193
+ targetPath: spec.targetPath,
194
+ };
195
+ };
196
+
197
+ const buildPlan = async (args: ParsedClientArgs): Promise<FilePlan[]> => {
198
+ const entries: FilePlan[] = [];
199
+ for (const part of args.parts) {
200
+ const files = getFileSpecsForPart(part);
201
+ for (const file of files) {
202
+ // eslint-disable-next-line no-await-in-loop
203
+ const nextText = await readTemplateFile(file);
204
+ // eslint-disable-next-line no-await-in-loop
205
+ const item =
206
+ args.action === "add"
207
+ ? await classifyAddPlan(part, file, nextText)
208
+ : await classifyUpdatePlan(part, file, nextText);
209
+ entries.push(item);
210
+ }
211
+ }
212
+ return entries;
213
+ };
214
+
215
+ const getWriteLabel = (action: ClientAction): "create" | "update" =>
216
+ action === "add" ? "create" : "update";
217
+
218
+ const getSkipReason = (action: ClientAction, item: FilePlan): string => {
219
+ if (action === "add") {
220
+ return item.reason;
221
+ }
222
+
223
+ return item.reason === "missing"
224
+ ? `missing; run: idcmd client add ${item.part}`
225
+ : item.reason;
226
+ };
227
+
228
+ const formatPlanLine = (action: ClientAction, item: FilePlan): string => {
229
+ if (item.shouldWrite) {
230
+ return `[${getWriteLabel(action)}] ${item.targetPath}`;
231
+ }
232
+
233
+ return `[skip] ${item.targetPath} (${getSkipReason(action, item)})`;
234
+ };
235
+
236
+ const printPlan = (action: ClientAction, plan: FilePlan[]): void => {
237
+ for (const item of plan) {
238
+ console.log(formatPlanLine(action, item));
239
+ }
240
+ };
241
+
242
+ const ensureUpdateConfirmed = (
243
+ flags: ClientFlags,
244
+ plan: FilePlan[],
245
+ action: ClientAction
246
+ ): void => {
247
+ if (action !== "update") {
248
+ return;
249
+ }
250
+
251
+ if (flags.dryRun) {
252
+ return;
253
+ }
254
+
255
+ const pending = plan.some((item) => item.shouldWrite);
256
+ if (pending && !flags.yes) {
257
+ throw new Error(
258
+ "Refusing to overwrite client files without --yes. Re-run with: idcmd client update <part> --yes"
259
+ );
260
+ }
261
+ };
262
+
263
+ const applyPlan = async (plan: FilePlan[]): Promise<void> => {
264
+ for (const item of plan) {
265
+ if (!item.shouldWrite) {
266
+ continue;
267
+ }
268
+
269
+ // eslint-disable-next-line no-await-in-loop
270
+ await ensureDir(dirname(item.targetPath));
271
+ // eslint-disable-next-line no-await-in-loop
272
+ await Bun.write(item.targetPath, item.nextText);
273
+ }
274
+ };
275
+
276
+ const printDryRunFooter = (plan: FilePlan[]): void => {
277
+ const count = plan.filter((item) => item.shouldWrite).length;
278
+ console.log(`[dry-run] ${count} file(s) would change.`);
279
+ };
280
+
281
+ const printAppliedFooter = (plan: FilePlan[]): void => {
282
+ const count = plan.filter((item) => item.shouldWrite).length;
283
+ console.log(`Applied ${count} change(s).`);
284
+ };
285
+
286
+ const maybeHandleDryRun = (flags: ClientFlags, plan: FilePlan[]): boolean => {
287
+ if (!flags.dryRun) {
288
+ return false;
289
+ }
290
+
291
+ printDryRunFooter(plan);
292
+ return true;
293
+ };
294
+
295
+ const applyPlanWithSummary = async (plan: FilePlan[]): Promise<void> => {
296
+ await applyPlan(plan);
297
+ printAppliedFooter(plan);
298
+ };
299
+
300
+ export const clientCommand = async (
301
+ positionals: string[],
302
+ flags: ClientFlags
303
+ ): Promise<number> => {
304
+ await ensureSiteLayout();
305
+ const parsed = parseClientArgs(positionals);
306
+ const plan = await buildPlan(parsed);
307
+
308
+ printPlan(parsed.action, plan);
309
+ ensureUpdateConfirmed(flags, plan, parsed.action);
310
+
311
+ if (maybeHandleDryRun(flags, plan)) {
312
+ return 0;
313
+ }
314
+
315
+ await applyPlanWithSummary(plan);
316
+ return 0;
317
+ };
@@ -1,4 +1,8 @@
1
1
  import { parsePort } from "../normalize";
2
+ import {
3
+ compileRuntimeAssetsOnce,
4
+ watchRuntimeAssets,
5
+ } from "../runtime-assets";
2
6
 
3
7
  const DEFAULT_PORT = 4000;
4
8
 
@@ -33,12 +37,11 @@ const installSignalHandlers = (shutdown: () => void): void => {
33
37
  process.on("SIGTERM", shutdown);
34
38
  };
35
39
 
36
- export const devCommand = async (flags: DevFlags): Promise<number> => {
37
- const port = parsePort(flags.port, DEFAULT_PORT);
38
- const tailwindInput = await findTailwindInput();
39
- const tailwindOutput = await resolveTailwindOutput();
40
-
41
- const cssProc = Bun.spawn(
40
+ const spawnCssProcess = (
41
+ tailwindInput: string,
42
+ tailwindOutput: string
43
+ ): ReturnType<typeof Bun.spawn> =>
44
+ Bun.spawn(
42
45
  [
43
46
  "bunx",
44
47
  "@tailwindcss/cli",
@@ -46,35 +49,97 @@ export const devCommand = async (flags: DevFlags): Promise<number> => {
46
49
  tailwindInput,
47
50
  "-o",
48
51
  tailwindOutput,
49
- "--watch",
52
+ // Tailwind v4 exits watch mode when stdin is closed unless `always` is specified.
53
+ "--watch=always",
50
54
  ],
51
55
  { stderr: "inherit", stdout: "inherit" }
52
56
  );
53
57
 
54
- const serverProc = Bun.spawn(["bun", "--hot", idcmdServerEntry()], {
58
+ const spawnServerProcess = (port: number): ReturnType<typeof Bun.spawn> =>
59
+ Bun.spawn(["bun", "--hot", idcmdServerEntry()], {
55
60
  env: { ...process.env, NODE_ENV: "development", PORT: String(port) },
56
61
  stderr: "inherit",
57
62
  stdout: "inherit",
58
63
  });
59
64
 
60
- const shutdown = (): void => {
61
- try {
62
- cssProc.kill("SIGTERM");
63
- } catch {
64
- // ignore
65
- }
66
- try {
67
- serverProc.kill("SIGTERM");
68
- } catch {
69
- // ignore
70
- }
65
+ const ensureRuntimeAssetsReady = async (): Promise<{
66
+ runtimeProc: ReturnType<typeof Bun.spawn> | null;
67
+ runtimeSetupCode: number;
68
+ }> => {
69
+ const runtimeCode = await compileRuntimeAssetsOnce();
70
+ if (runtimeCode !== 0) {
71
+ return { runtimeProc: null, runtimeSetupCode: runtimeCode };
72
+ }
73
+
74
+ return {
75
+ runtimeProc: await watchRuntimeAssets(),
76
+ runtimeSetupCode: 0,
71
77
  };
78
+ };
72
79
 
73
- installSignalHandlers(shutdown);
80
+ const killProcess = (proc: ReturnType<typeof Bun.spawn> | null): void => {
81
+ if (!proc) {
82
+ return;
83
+ }
84
+ try {
85
+ proc.kill("SIGTERM");
86
+ } catch {
87
+ // ignore
88
+ }
89
+ };
74
90
 
75
- const [cssExit, serverExit] = await Promise.all([
76
- cssProc.exited,
77
- serverProc.exited,
91
+ const resolveDevExitCode = (args: {
92
+ cssExit: number;
93
+ runtimeExit: number;
94
+ serverExit: number;
95
+ }): number => {
96
+ if (args.serverExit !== 0) {
97
+ return args.serverExit;
98
+ }
99
+ if (args.cssExit !== 0) {
100
+ return args.cssExit;
101
+ }
102
+ return args.runtimeExit;
103
+ };
104
+
105
+ const installDevSignalHandlers = (args: {
106
+ cssProc: ReturnType<typeof Bun.spawn>;
107
+ runtimeProc: ReturnType<typeof Bun.spawn> | null;
108
+ serverProc: ReturnType<typeof Bun.spawn>;
109
+ }): void => {
110
+ installSignalHandlers(() => {
111
+ killProcess(args.cssProc);
112
+ killProcess(args.serverProc);
113
+ killProcess(args.runtimeProc);
114
+ });
115
+ };
116
+
117
+ const waitForDevExit = async (args: {
118
+ cssProc: ReturnType<typeof Bun.spawn>;
119
+ runtimeProc: ReturnType<typeof Bun.spawn> | null;
120
+ serverProc: ReturnType<typeof Bun.spawn>;
121
+ }): Promise<{ cssExit: number; runtimeExit: number; serverExit: number }> => {
122
+ const [cssExit, serverExit, runtimeExit] = await Promise.all([
123
+ args.cssProc.exited,
124
+ args.serverProc.exited,
125
+ args.runtimeProc?.exited ?? Promise.resolve(0),
78
126
  ]);
79
- return serverExit === 0 ? cssExit : serverExit;
127
+ return { cssExit, runtimeExit, serverExit };
128
+ };
129
+
130
+ export const devCommand = async (flags: DevFlags): Promise<number> => {
131
+ const port = parsePort(flags.port, DEFAULT_PORT);
132
+ const { runtimeProc, runtimeSetupCode } = await ensureRuntimeAssetsReady();
133
+ if (runtimeSetupCode !== 0) {
134
+ return runtimeSetupCode;
135
+ }
136
+
137
+ const tailwindInput = await findTailwindInput();
138
+ const tailwindOutput = await resolveTailwindOutput();
139
+ const cssProc = spawnCssProcess(tailwindInput, tailwindOutput);
140
+ const serverProc = spawnServerProcess(port);
141
+ installDevSignalHandlers({ cssProc, runtimeProc, serverProc });
142
+ return resolveDevExitCode(
143
+ await waitForDevExit({ cssProc, runtimeProc, serverProc })
144
+ );
80
145
  };