idcmd 0.0.10 → 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 +27 -5
- package/package.json +2 -3
- package/src/cli/commands/build.ts +2 -1
- package/src/cli/commands/deploy.ts +208 -22
- package/src/cli/commands/dev.ts +2 -1
- package/src/cli/commands/init.ts +56 -4
- package/src/cli/commands/preview.ts +36 -3
- package/src/cli/main.ts +271 -19
- package/src/cli/prompt.ts +105 -0
- package/src/cli/provider-files.ts +225 -0
- package/src/cli/provider.ts +36 -0
- 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/headers.ts +23 -4
- package/src/server/user-routes.ts +1 -1
- package/src/server.ts +22 -2
- package/src/site/cache.ts +108 -0
- package/src/site/config.ts +46 -4
- package/templates/default/.github/workflows/ci.yml +0 -3
- package/templates/default/README.md +6 -5
- package/templates/default/package.json +0 -1
- package/templates/default/scripts/check.ts +0 -1
- package/templates/default/site.jsonc +10 -0
- 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/templates/default/vercel.json +0 -7
package/src/cli/main.ts
CHANGED
|
@@ -13,30 +13,233 @@ 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
|
-
" idcmd init [dir] [--yes] [--name <name>] [--description <text>] [--base-url <url>] [--port <port>] [--no-git]",
|
|
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>]",
|
|
24
37
|
" idcmd build",
|
|
25
38
|
" idcmd preview [--port <port>]",
|
|
26
|
-
" idcmd deploy",
|
|
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
|
+
"",
|
|
63
|
+
].join("\n");
|
|
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
|
+
"",
|
|
29
116
|
].join("\n");
|
|
30
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
|
|
|
34
220
|
const asBooleanFlag = (value: unknown): boolean =>
|
|
35
221
|
value === true || value === "true";
|
|
36
222
|
|
|
223
|
+
const asOptionalBooleanFlag = (value: unknown): boolean | undefined => {
|
|
224
|
+
if (value === false || value === "false") {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return asBooleanFlag(value) ? true : undefined;
|
|
228
|
+
};
|
|
229
|
+
|
|
37
230
|
const handleInit = (parsed: ParsedArgs): Promise<number> => {
|
|
38
231
|
const [dir] = parsed.positionals;
|
|
39
|
-
return initCommand(dir,
|
|
232
|
+
return initCommand(dir, {
|
|
233
|
+
"base-url": asStringFlag(parsed.flags["base-url"]),
|
|
234
|
+
description: asStringFlag(parsed.flags.description),
|
|
235
|
+
fly: asBooleanFlag(parsed.flags.fly),
|
|
236
|
+
git: asOptionalBooleanFlag(parsed.flags.git),
|
|
237
|
+
name: asStringFlag(parsed.flags.name),
|
|
238
|
+
port: asStringFlag(parsed.flags.port),
|
|
239
|
+
railway: asBooleanFlag(parsed.flags.railway),
|
|
240
|
+
vercel: asBooleanFlag(parsed.flags.vercel),
|
|
241
|
+
yes: asBooleanFlag(parsed.flags.yes),
|
|
242
|
+
});
|
|
40
243
|
};
|
|
41
244
|
|
|
42
245
|
const handleDev = (parsed: ParsedArgs): Promise<number> =>
|
|
@@ -49,7 +252,12 @@ const handlePreview = (parsed: ParsedArgs): Promise<number> => {
|
|
|
49
252
|
return previewCommand(port);
|
|
50
253
|
};
|
|
51
254
|
|
|
52
|
-
const handleDeploy = (): Promise<number> =>
|
|
255
|
+
const handleDeploy = (parsed: ParsedArgs): Promise<number> =>
|
|
256
|
+
deployCommand({
|
|
257
|
+
fly: asBooleanFlag(parsed.flags.fly),
|
|
258
|
+
railway: asBooleanFlag(parsed.flags.railway),
|
|
259
|
+
vercel: asBooleanFlag(parsed.flags.vercel),
|
|
260
|
+
});
|
|
53
261
|
|
|
54
262
|
const handleClient = (parsed: ParsedArgs): Promise<number> =>
|
|
55
263
|
clientCommand(parsed.positionals, {
|
|
@@ -60,7 +268,7 @@ const handleClient = (parsed: ParsedArgs): Promise<number> =>
|
|
|
60
268
|
const handlers: Record<string, (parsed: ParsedArgs) => Promise<number>> = {
|
|
61
269
|
build: () => handleBuild(),
|
|
62
270
|
client: (parsed) => handleClient(parsed),
|
|
63
|
-
deploy: () => handleDeploy(),
|
|
271
|
+
deploy: (parsed) => handleDeploy(parsed),
|
|
64
272
|
dev: (parsed) => handleDev(parsed),
|
|
65
273
|
init: (parsed) => handleInit(parsed),
|
|
66
274
|
preview: (parsed) => handlePreview(parsed),
|
|
@@ -72,6 +280,53 @@ const isHelpCommand = (cmd: string | null): boolean =>
|
|
|
72
280
|
const isVersionCommand = (cmd: string): boolean =>
|
|
73
281
|
cmd === "--version" || cmd === "-v";
|
|
74
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
|
+
|
|
75
330
|
export const main = async (argv: string[]): Promise<void> => {
|
|
76
331
|
try {
|
|
77
332
|
const code = await runMain(argv);
|
|
@@ -87,7 +342,7 @@ const runMain = async (argv: string[]): Promise<number> => {
|
|
|
87
342
|
const parsed = parseArgs(argv);
|
|
88
343
|
const cmd = parsed.command;
|
|
89
344
|
|
|
90
|
-
const metaCode = await maybeHandleMetaCommand(
|
|
345
|
+
const metaCode = await maybeHandleMetaCommand(parsed);
|
|
91
346
|
if (metaCode !== null) {
|
|
92
347
|
return metaCode;
|
|
93
348
|
}
|
|
@@ -101,19 +356,16 @@ const runMain = async (argv: string[]): Promise<number> => {
|
|
|
101
356
|
};
|
|
102
357
|
|
|
103
358
|
const maybeHandleMetaCommand = async (
|
|
104
|
-
|
|
359
|
+
parsed: ParsedArgs
|
|
105
360
|
): Promise<number | null> => {
|
|
106
|
-
|
|
107
|
-
console.log(usage());
|
|
108
|
-
return 0;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (cmd && isVersionCommand(cmd)) {
|
|
112
|
-
console.log(await readPackageVersion());
|
|
113
|
-
return 0;
|
|
114
|
-
}
|
|
361
|
+
const cmd = parsed.command;
|
|
115
362
|
|
|
116
|
-
return
|
|
363
|
+
return (
|
|
364
|
+
maybeHandleGeneralHelp(cmd) ??
|
|
365
|
+
(await maybeHandleVersion(cmd)) ??
|
|
366
|
+
maybeHandleHelpCommand(parsed) ??
|
|
367
|
+
maybeHandleSubcommandHelp(parsed)
|
|
368
|
+
);
|
|
117
369
|
};
|
|
118
370
|
|
|
119
371
|
const resolveCommandHandler = (
|
|
@@ -127,6 +379,6 @@ const resolveCommandHandler = (
|
|
|
127
379
|
|
|
128
380
|
const handleUnknownCommand = (cmd: string | null): number => {
|
|
129
381
|
console.error(`Unknown command: ${cmd ?? "(none)"}`);
|
|
130
|
-
console.log(
|
|
382
|
+
console.log(globalHelp());
|
|
131
383
|
return 1;
|
|
132
384
|
};
|
package/src/cli/prompt.ts
CHANGED
|
@@ -72,3 +72,108 @@ export const promptOptionalText = async (
|
|
|
72
72
|
const trimmed = raw.trim();
|
|
73
73
|
return trimmed.length > 0 ? trimmed : defaultValue;
|
|
74
74
|
};
|
|
75
|
+
|
|
76
|
+
export interface PromptSelectOption<T extends string> {
|
|
77
|
+
label: string;
|
|
78
|
+
value: T;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parseNumericChoice = (
|
|
82
|
+
raw: string,
|
|
83
|
+
optionsLength: number
|
|
84
|
+
): number | null => {
|
|
85
|
+
const parsed = Number(raw);
|
|
86
|
+
if (!Number.isInteger(parsed)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const index = parsed - 1;
|
|
90
|
+
if (index < 0 || index >= optionsLength) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return index;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const findOptionByValue = <T extends string>(
|
|
97
|
+
options: readonly PromptSelectOption<T>[],
|
|
98
|
+
raw: string
|
|
99
|
+
): PromptSelectOption<T> | null => {
|
|
100
|
+
const direct = options.find((option) => option.value === raw);
|
|
101
|
+
return direct ?? null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const getDefaultOptionIndex = <T extends string>(
|
|
105
|
+
options: readonly PromptSelectOption<T>[],
|
|
106
|
+
defaultValue: T
|
|
107
|
+
): number => {
|
|
108
|
+
if (options.length === 0) {
|
|
109
|
+
throw new Error("promptSelect requires at least one option.");
|
|
110
|
+
}
|
|
111
|
+
const defaultIndex = options.findIndex(
|
|
112
|
+
(option) => option.value === defaultValue
|
|
113
|
+
);
|
|
114
|
+
if (defaultIndex === -1) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`promptSelect default value "${defaultValue}" is not in options.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return defaultIndex;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const printSelectPrompt = <T extends string>(args: {
|
|
123
|
+
defaultIndex: number;
|
|
124
|
+
options: readonly PromptSelectOption<T>[];
|
|
125
|
+
question: string;
|
|
126
|
+
}): void => {
|
|
127
|
+
process.stdout.write(`${args.question}\n`);
|
|
128
|
+
for (const [index, option] of args.options.entries()) {
|
|
129
|
+
const suffix = index === args.defaultIndex ? " (default)" : "";
|
|
130
|
+
process.stdout.write(` ${String(index + 1)}. ${option.label}${suffix}\n`);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const resolveSelectedValue = <T extends string>(args: {
|
|
135
|
+
defaultValue: T;
|
|
136
|
+
input: string;
|
|
137
|
+
options: readonly PromptSelectOption<T>[];
|
|
138
|
+
}): T | null => {
|
|
139
|
+
if (args.input.length === 0) {
|
|
140
|
+
return args.defaultValue;
|
|
141
|
+
}
|
|
142
|
+
const numericChoice = parseNumericChoice(args.input, args.options.length);
|
|
143
|
+
if (numericChoice !== null) {
|
|
144
|
+
return args.options[numericChoice]?.value ?? args.defaultValue;
|
|
145
|
+
}
|
|
146
|
+
const valueChoice = findOptionByValue(args.options, args.input);
|
|
147
|
+
return valueChoice?.value ?? null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const readSelection = async <T extends string>(args: {
|
|
151
|
+
defaultIndex: number;
|
|
152
|
+
defaultValue: T;
|
|
153
|
+
options: readonly PromptSelectOption<T>[];
|
|
154
|
+
}): Promise<T> => {
|
|
155
|
+
while (true) {
|
|
156
|
+
const input = await readLine(
|
|
157
|
+
`Select [1-${String(args.options.length)}] (${String(args.defaultIndex + 1)}): `
|
|
158
|
+
);
|
|
159
|
+
const value = resolveSelectedValue({
|
|
160
|
+
defaultValue: args.defaultValue,
|
|
161
|
+
input: input.trim(),
|
|
162
|
+
options: args.options,
|
|
163
|
+
});
|
|
164
|
+
if (value !== null) {
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
process.stdout.write("Invalid selection. Enter a number from the list.\n");
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const promptSelect = <T extends string>(
|
|
172
|
+
question: string,
|
|
173
|
+
options: readonly PromptSelectOption<T>[],
|
|
174
|
+
defaultValue: T
|
|
175
|
+
): Promise<T> => {
|
|
176
|
+
const defaultIndex = getDefaultOptionIndex(options, defaultValue);
|
|
177
|
+
printSelectPrompt({ defaultIndex, options, question });
|
|
178
|
+
return readSelection({ defaultIndex, defaultValue, options });
|
|
179
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { ResolvedCachePolicy } from "../site/cache";
|
|
2
|
+
import type { DeployProvider } from "./provider";
|
|
3
|
+
|
|
4
|
+
import { ensureDir } from "./fs";
|
|
5
|
+
import { dirname, joinPath } from "./path";
|
|
6
|
+
|
|
7
|
+
interface ProviderFileOptions {
|
|
8
|
+
cachePolicy: ResolvedCachePolicy;
|
|
9
|
+
packageName: string;
|
|
10
|
+
targetDir: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DOCKERFILE_PATH = "Dockerfile";
|
|
14
|
+
const DOCKERIGNORE_PATH = ".dockerignore";
|
|
15
|
+
const FLY_CONFIG_PATH = "fly.toml";
|
|
16
|
+
const RAILWAY_CONFIG_PATH = "railway.json";
|
|
17
|
+
const VERCEL_CONFIG_PATH = "vercel.json";
|
|
18
|
+
|
|
19
|
+
const DOCKERFILE = `FROM oven/bun:1
|
|
20
|
+
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
|
|
23
|
+
COPY package.json bun.lock* ./
|
|
24
|
+
RUN bun install --frozen-lockfile
|
|
25
|
+
|
|
26
|
+
COPY . .
|
|
27
|
+
|
|
28
|
+
RUN bun run build
|
|
29
|
+
|
|
30
|
+
ENV NODE_ENV=production
|
|
31
|
+
ENV PORT=8080
|
|
32
|
+
|
|
33
|
+
EXPOSE 8080
|
|
34
|
+
|
|
35
|
+
CMD ["bun", "src/server.ts"]
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const DOCKERIGNORE = `.git
|
|
39
|
+
.github
|
|
40
|
+
.gitignore
|
|
41
|
+
node_modules
|
|
42
|
+
public
|
|
43
|
+
.env
|
|
44
|
+
.env.*
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const sanitizeFlyAppName = (value: string): string => {
|
|
48
|
+
const normalized = value
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replaceAll(/[^a-z0-9-]/g, "-")
|
|
51
|
+
.replaceAll(/-+/g, "-")
|
|
52
|
+
.replaceAll(/^-+|-+$/g, "");
|
|
53
|
+
if (normalized.length === 0) {
|
|
54
|
+
return "idcmd-app";
|
|
55
|
+
}
|
|
56
|
+
return normalized.slice(0, 63);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const buildCacheControl = (args: {
|
|
60
|
+
browserCacheControl: string;
|
|
61
|
+
edgeCacheControl: string | null;
|
|
62
|
+
}): string =>
|
|
63
|
+
args.edgeCacheControl
|
|
64
|
+
? `${args.browserCacheControl}, ${args.edgeCacheControl}`
|
|
65
|
+
: args.browserCacheControl;
|
|
66
|
+
|
|
67
|
+
const createVercelHeaders = (
|
|
68
|
+
cachePolicy: ResolvedCachePolicy
|
|
69
|
+
): {
|
|
70
|
+
headers: { key: string; value: string }[];
|
|
71
|
+
source: string;
|
|
72
|
+
}[] => {
|
|
73
|
+
const htmlHeaders = [
|
|
74
|
+
{
|
|
75
|
+
key: "Cache-Control",
|
|
76
|
+
value: buildCacheControl({
|
|
77
|
+
browserCacheControl: cachePolicy.html.browserCacheControl,
|
|
78
|
+
edgeCacheControl: null,
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
if (cachePolicy.html.edgeCacheControl) {
|
|
83
|
+
htmlHeaders.push({
|
|
84
|
+
key: "Vercel-CDN-Cache-Control",
|
|
85
|
+
value: cachePolicy.html.edgeCacheControl,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [
|
|
90
|
+
{ headers: htmlHeaders, source: "/(.*)" },
|
|
91
|
+
{
|
|
92
|
+
headers: [
|
|
93
|
+
{ key: "Cache-Control", value: cachePolicy.static.cacheControl },
|
|
94
|
+
],
|
|
95
|
+
source: "/(.*\\..*)",
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const createVercelConfig = (cachePolicy: ResolvedCachePolicy): string =>
|
|
101
|
+
`${JSON.stringify(
|
|
102
|
+
{
|
|
103
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
104
|
+
buildCommand: "bun run build",
|
|
105
|
+
bunVersion: "1.x",
|
|
106
|
+
headers: createVercelHeaders(cachePolicy),
|
|
107
|
+
installCommand: "bun install",
|
|
108
|
+
outputDirectory: "public",
|
|
109
|
+
},
|
|
110
|
+
null,
|
|
111
|
+
2
|
|
112
|
+
)}\n`;
|
|
113
|
+
|
|
114
|
+
const createFlyToml = (packageName: string): string =>
|
|
115
|
+
`app = "${sanitizeFlyAppName(packageName)}"
|
|
116
|
+
primary_region = "iad"
|
|
117
|
+
|
|
118
|
+
[env]
|
|
119
|
+
NODE_ENV = "production"
|
|
120
|
+
PORT = "8080"
|
|
121
|
+
|
|
122
|
+
[http_service]
|
|
123
|
+
internal_port = 8080
|
|
124
|
+
force_https = true
|
|
125
|
+
auto_stop_machines = "stop"
|
|
126
|
+
auto_start_machines = true
|
|
127
|
+
min_machines_running = 0
|
|
128
|
+
processes = ["app"]
|
|
129
|
+
|
|
130
|
+
[[http_service.checks]]
|
|
131
|
+
grace_period = "10s"
|
|
132
|
+
interval = "30s"
|
|
133
|
+
method = "GET"
|
|
134
|
+
timeout = "5s"
|
|
135
|
+
path = "/health"
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const createRailwayConfig = (): string =>
|
|
139
|
+
`${JSON.stringify(
|
|
140
|
+
{
|
|
141
|
+
$schema: "https://railway.com/railway.schema.json",
|
|
142
|
+
build: {
|
|
143
|
+
builder: "DOCKERFILE",
|
|
144
|
+
},
|
|
145
|
+
deploy: {
|
|
146
|
+
healthcheckPath: "/health",
|
|
147
|
+
healthcheckTimeout: 120,
|
|
148
|
+
restartPolicyMaxRetries: 10,
|
|
149
|
+
restartPolicyType: "ON_FAILURE",
|
|
150
|
+
startCommand: "bun src/server.ts",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
null,
|
|
154
|
+
2
|
|
155
|
+
)}\n`;
|
|
156
|
+
|
|
157
|
+
const writeFile = async (
|
|
158
|
+
targetDir: string,
|
|
159
|
+
relativePath: string,
|
|
160
|
+
text: string
|
|
161
|
+
): Promise<void> => {
|
|
162
|
+
const path = joinPath(targetDir, relativePath);
|
|
163
|
+
await ensureDir(dirname(path));
|
|
164
|
+
await Bun.write(path, text);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const writeVercelFiles = async (
|
|
168
|
+
args: ProviderFileOptions
|
|
169
|
+
): Promise<string[]> => {
|
|
170
|
+
await writeFile(
|
|
171
|
+
args.targetDir,
|
|
172
|
+
VERCEL_CONFIG_PATH,
|
|
173
|
+
createVercelConfig(args.cachePolicy)
|
|
174
|
+
);
|
|
175
|
+
return [VERCEL_CONFIG_PATH];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const writeFlyFiles = async (args: ProviderFileOptions): Promise<string[]> => {
|
|
179
|
+
await writeFile(args.targetDir, DOCKERFILE_PATH, DOCKERFILE);
|
|
180
|
+
await writeFile(args.targetDir, DOCKERIGNORE_PATH, DOCKERIGNORE);
|
|
181
|
+
await writeFile(
|
|
182
|
+
args.targetDir,
|
|
183
|
+
FLY_CONFIG_PATH,
|
|
184
|
+
createFlyToml(args.packageName)
|
|
185
|
+
);
|
|
186
|
+
return [DOCKERFILE_PATH, DOCKERIGNORE_PATH, FLY_CONFIG_PATH];
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const writeRailwayFiles = async (
|
|
190
|
+
args: ProviderFileOptions
|
|
191
|
+
): Promise<string[]> => {
|
|
192
|
+
await writeFile(args.targetDir, DOCKERFILE_PATH, DOCKERFILE);
|
|
193
|
+
await writeFile(args.targetDir, DOCKERIGNORE_PATH, DOCKERIGNORE);
|
|
194
|
+
await writeFile(args.targetDir, RAILWAY_CONFIG_PATH, createRailwayConfig());
|
|
195
|
+
return [DOCKERFILE_PATH, DOCKERIGNORE_PATH, RAILWAY_CONFIG_PATH];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const generateProviderFiles = (args: {
|
|
199
|
+
cachePolicy: ResolvedCachePolicy;
|
|
200
|
+
packageName: string;
|
|
201
|
+
provider: DeployProvider;
|
|
202
|
+
targetDir: string;
|
|
203
|
+
}): Promise<string[]> => {
|
|
204
|
+
const options = {
|
|
205
|
+
cachePolicy: args.cachePolicy,
|
|
206
|
+
packageName: args.packageName,
|
|
207
|
+
targetDir: args.targetDir,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (args.provider === "vercel") {
|
|
211
|
+
return writeVercelFiles(options);
|
|
212
|
+
}
|
|
213
|
+
if (args.provider === "fly") {
|
|
214
|
+
return writeFlyFiles(options);
|
|
215
|
+
}
|
|
216
|
+
return writeRailwayFiles(options);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const providerConfigPaths = {
|
|
220
|
+
dockerfile: DOCKERFILE_PATH,
|
|
221
|
+
dockerignore: DOCKERIGNORE_PATH,
|
|
222
|
+
flyToml: FLY_CONFIG_PATH,
|
|
223
|
+
railwayJson: RAILWAY_CONFIG_PATH,
|
|
224
|
+
vercelJson: VERCEL_CONFIG_PATH,
|
|
225
|
+
} as const;
|