kitfly 0.1.2
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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/VERSION +1 -0
- package/package.json +63 -0
- package/schemas/README.md +32 -0
- package/schemas/site.schema.json +5 -0
- package/schemas/theme.schema.json +5 -0
- package/schemas/v0/site.schema.json +172 -0
- package/schemas/v0/theme.schema.json +210 -0
- package/scripts/build-all.ts +121 -0
- package/scripts/build.ts +601 -0
- package/scripts/bundle.ts +781 -0
- package/scripts/dev.ts +777 -0
- package/scripts/generate-checksums.sh +78 -0
- package/scripts/release/export-release-key.sh +28 -0
- package/scripts/release/release-guard-tag-version.sh +79 -0
- package/scripts/release/sign-release-assets.sh +123 -0
- package/scripts/release/upload-release-assets.sh +76 -0
- package/scripts/release/upload-release-provenance.sh +52 -0
- package/scripts/release/verify-public-key.sh +48 -0
- package/scripts/release/verify-signatures.sh +117 -0
- package/scripts/version-sync.ts +82 -0
- package/src/__tests__/build.test.ts +240 -0
- package/src/__tests__/bundle.test.ts +786 -0
- package/src/__tests__/cli.test.ts +706 -0
- package/src/__tests__/crucible.test.ts +1043 -0
- package/src/__tests__/engine.test.ts +157 -0
- package/src/__tests__/init.test.ts +450 -0
- package/src/__tests__/pipeline.test.ts +1087 -0
- package/src/__tests__/productbook.test.ts +1206 -0
- package/src/__tests__/runbook.test.ts +974 -0
- package/src/__tests__/server-registry.test.ts +1251 -0
- package/src/__tests__/servicebook.test.ts +1248 -0
- package/src/__tests__/shared.test.ts +2005 -0
- package/src/__tests__/styles.test.ts +14 -0
- package/src/__tests__/theme-schema.test.ts +47 -0
- package/src/__tests__/theme.test.ts +554 -0
- package/src/cli.ts +582 -0
- package/src/commands/init.ts +92 -0
- package/src/commands/update.ts +444 -0
- package/src/engine.ts +20 -0
- package/src/logger.ts +15 -0
- package/src/migrations/0000_schema_versioning.ts +67 -0
- package/src/migrations/0001_server_port.ts +52 -0
- package/src/migrations/0002_brand_logo.ts +49 -0
- package/src/migrations/index.ts +26 -0
- package/src/migrations/schema.ts +24 -0
- package/src/server-registry.ts +405 -0
- package/src/shared.ts +1239 -0
- package/src/site/styles.css +931 -0
- package/src/site/template.html +193 -0
- package/src/templates/crucible.ts +1163 -0
- package/src/templates/driver.ts +876 -0
- package/src/templates/handbook.ts +339 -0
- package/src/templates/minimal.ts +139 -0
- package/src/templates/pipeline.ts +966 -0
- package/src/templates/productbook.ts +1032 -0
- package/src/templates/runbook.ts +829 -0
- package/src/templates/schema.ts +119 -0
- package/src/templates/servicebook.ts +1242 -0
- package/src/theme.ts +245 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI argument parsing, version, help, and command routing logic
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
// We need to test the parseArgs function and other CLI logic
|
|
8
|
+
// Since parseArgs is not exported, we'll recreate it here for unit testing
|
|
9
|
+
// This matches the implementation in cli.ts exactly
|
|
10
|
+
|
|
11
|
+
function parseArgs(args: string[]): {
|
|
12
|
+
positional: string[];
|
|
13
|
+
flags: Record<string, string | boolean>;
|
|
14
|
+
} {
|
|
15
|
+
const positional: string[] = [];
|
|
16
|
+
const flags: Record<string, string | boolean> = {};
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (arg.startsWith("--")) {
|
|
21
|
+
const key = arg.slice(2);
|
|
22
|
+
if (key.startsWith("no-")) {
|
|
23
|
+
flags[key.slice(3)] = false;
|
|
24
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
25
|
+
flags[key] = args[++i];
|
|
26
|
+
} else {
|
|
27
|
+
flags[key] = true;
|
|
28
|
+
}
|
|
29
|
+
} else if (arg.startsWith("-")) {
|
|
30
|
+
flags[arg.slice(1)] = true;
|
|
31
|
+
} else {
|
|
32
|
+
positional.push(arg);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { positional, flags };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Recreate getVersion logic for testing
|
|
40
|
+
function getVersion(versionFilePath: string): string {
|
|
41
|
+
try {
|
|
42
|
+
const fs = require("node:fs");
|
|
43
|
+
return fs.readFileSync(versionFilePath, "utf-8").trim();
|
|
44
|
+
} catch {
|
|
45
|
+
return "0.0.0";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Generate help text (matches cli.ts structure)
|
|
50
|
+
function generateHelp(version: string): string {
|
|
51
|
+
return `
|
|
52
|
+
kitfly v${version} - Turn your writing into a website
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
kitfly dev [folder] Start dev server with hot reload
|
|
56
|
+
kitfly build [folder] Build static site to dist/
|
|
57
|
+
kitfly bundle [folder] Build single-file HTML bundle
|
|
58
|
+
kitfly init [name] Create new project from template
|
|
59
|
+
kitfly servers List running dev servers
|
|
60
|
+
kitfly stop <port|all> Stop dev server(s)
|
|
61
|
+
kitfly version Show version
|
|
62
|
+
kitfly help Show this help
|
|
63
|
+
|
|
64
|
+
Dev options:
|
|
65
|
+
--port <n> Server port [env: KITFLY_DEV_PORT] (default: 3333)
|
|
66
|
+
--host <h> Server host [env: KITFLY_DEV_HOST] (default: localhost)
|
|
67
|
+
--daemon, -d Run in background, return immediately
|
|
68
|
+
--json Output JSON (implies --daemon)
|
|
69
|
+
--no-open Don't open browser
|
|
70
|
+
|
|
71
|
+
Build/bundle options:
|
|
72
|
+
--out <dir> Output directory [env: KITFLY_BUILD_OUT] (default: dist)
|
|
73
|
+
--name <file> Bundle filename (default: bundle.html)
|
|
74
|
+
--no-raw Don't include raw markdown
|
|
75
|
+
|
|
76
|
+
Stop options:
|
|
77
|
+
--force Skip graceful shutdown, kill immediately
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
kitfly dev
|
|
81
|
+
kitfly dev ./my-docs --port 4000 --daemon
|
|
82
|
+
kitfly dev ./docs --json
|
|
83
|
+
kitfly servers
|
|
84
|
+
kitfly stop 4000
|
|
85
|
+
kitfly stop all
|
|
86
|
+
kitfly build ./docs --out ./public
|
|
87
|
+
kitfly init my-handbook
|
|
88
|
+
|
|
89
|
+
Documentation: https://kitfly.app
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Command routing logic - returns the detected command and parsed arguments
|
|
94
|
+
function routeCommand(argv: string[]): {
|
|
95
|
+
command: string | undefined;
|
|
96
|
+
positional: string[];
|
|
97
|
+
flags: Record<string, string | boolean>;
|
|
98
|
+
} {
|
|
99
|
+
const [cmd, ...rest] = argv;
|
|
100
|
+
const { positional, flags } = parseArgs(rest);
|
|
101
|
+
return { command: cmd, positional, flags };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate port number (matches cli.ts logic)
|
|
105
|
+
function validatePort(portRaw: string | undefined): {
|
|
106
|
+
valid: boolean;
|
|
107
|
+
port: number;
|
|
108
|
+
error?: string;
|
|
109
|
+
} {
|
|
110
|
+
if (!portRaw) {
|
|
111
|
+
return { valid: true, port: 3333 }; // default port
|
|
112
|
+
}
|
|
113
|
+
const port = parseInt(portRaw, 10);
|
|
114
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
115
|
+
return { valid: false, port: 0, error: `"${portRaw}" is not a valid port number.` };
|
|
116
|
+
}
|
|
117
|
+
return { valid: true, port };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe("parseArgs", () => {
|
|
121
|
+
describe("positional arguments", () => {
|
|
122
|
+
it("parses single positional argument", () => {
|
|
123
|
+
const { positional, flags } = parseArgs(["./docs"]);
|
|
124
|
+
expect(positional).toEqual(["./docs"]);
|
|
125
|
+
expect(flags).toEqual({});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("parses multiple positional arguments", () => {
|
|
129
|
+
const { positional } = parseArgs(["./docs", "output", "extra"]);
|
|
130
|
+
expect(positional).toEqual(["./docs", "output", "extra"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("handles empty arguments", () => {
|
|
134
|
+
const { positional, flags } = parseArgs([]);
|
|
135
|
+
expect(positional).toEqual([]);
|
|
136
|
+
expect(flags).toEqual({});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("long flags with values", () => {
|
|
141
|
+
it("parses --port with value", () => {
|
|
142
|
+
const { flags } = parseArgs(["--port", "4000"]);
|
|
143
|
+
expect(flags.port).toBe("4000");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("parses --host with value", () => {
|
|
147
|
+
const { flags } = parseArgs(["--host", "0.0.0.0"]);
|
|
148
|
+
expect(flags.host).toBe("0.0.0.0");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("parses --out with value", () => {
|
|
152
|
+
const { flags } = parseArgs(["--out", "./public"]);
|
|
153
|
+
expect(flags.out).toBe("./public");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("parses --name with value", () => {
|
|
157
|
+
const { flags } = parseArgs(["--name", "site.html"]);
|
|
158
|
+
expect(flags.name).toBe("site.html");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("parses multiple flags with values", () => {
|
|
162
|
+
const { flags } = parseArgs(["--port", "5000", "--host", "localhost", "--out", "dist"]);
|
|
163
|
+
expect(flags.port).toBe("5000");
|
|
164
|
+
expect(flags.host).toBe("localhost");
|
|
165
|
+
expect(flags.out).toBe("dist");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("boolean flags", () => {
|
|
170
|
+
it("parses --daemon as true", () => {
|
|
171
|
+
const { flags } = parseArgs(["--daemon"]);
|
|
172
|
+
expect(flags.daemon).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("parses --json as true", () => {
|
|
176
|
+
const { flags } = parseArgs(["--json"]);
|
|
177
|
+
expect(flags.json).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("parses --force as true", () => {
|
|
181
|
+
const { flags } = parseArgs(["--force"]);
|
|
182
|
+
expect(flags.force).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("parses --no-open as open: false", () => {
|
|
186
|
+
const { flags } = parseArgs(["--no-open"]);
|
|
187
|
+
expect(flags.open).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("parses --no-raw as raw: false", () => {
|
|
191
|
+
const { flags } = parseArgs(["--no-raw"]);
|
|
192
|
+
expect(flags.raw).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("short flags", () => {
|
|
197
|
+
it("parses -d as true", () => {
|
|
198
|
+
const { flags } = parseArgs(["-d"]);
|
|
199
|
+
expect(flags.d).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("parses -v as true", () => {
|
|
203
|
+
const { flags } = parseArgs(["-v"]);
|
|
204
|
+
expect(flags.v).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("parses -h as true", () => {
|
|
208
|
+
const { flags } = parseArgs(["-h"]);
|
|
209
|
+
expect(flags.h).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("mixed arguments", () => {
|
|
214
|
+
it("parses positional and flags together", () => {
|
|
215
|
+
const { positional, flags } = parseArgs(["./docs", "--port", "4000", "--daemon"]);
|
|
216
|
+
expect(positional).toEqual(["./docs"]);
|
|
217
|
+
expect(flags.port).toBe("4000");
|
|
218
|
+
expect(flags.daemon).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("parses complex dev command arguments", () => {
|
|
222
|
+
const { positional, flags } = parseArgs([
|
|
223
|
+
"./my-docs",
|
|
224
|
+
"--port",
|
|
225
|
+
"4000",
|
|
226
|
+
"--host",
|
|
227
|
+
"0.0.0.0",
|
|
228
|
+
"--daemon",
|
|
229
|
+
"--no-open",
|
|
230
|
+
]);
|
|
231
|
+
expect(positional).toEqual(["./my-docs"]);
|
|
232
|
+
expect(flags.port).toBe("4000");
|
|
233
|
+
expect(flags.host).toBe("0.0.0.0");
|
|
234
|
+
expect(flags.daemon).toBe(true);
|
|
235
|
+
expect(flags.open).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("parses build command arguments", () => {
|
|
239
|
+
const { positional, flags } = parseArgs(["./content", "--out", "./public", "--no-raw"]);
|
|
240
|
+
expect(positional).toEqual(["./content"]);
|
|
241
|
+
expect(flags.out).toBe("./public");
|
|
242
|
+
expect(flags.raw).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("parses bundle command arguments", () => {
|
|
246
|
+
const { positional, flags } = parseArgs([
|
|
247
|
+
"./docs",
|
|
248
|
+
"--out",
|
|
249
|
+
"./dist",
|
|
250
|
+
"--name",
|
|
251
|
+
"handbook.html",
|
|
252
|
+
"--no-raw",
|
|
253
|
+
]);
|
|
254
|
+
expect(positional).toEqual(["./docs"]);
|
|
255
|
+
expect(flags.out).toBe("./dist");
|
|
256
|
+
expect(flags.name).toBe("handbook.html");
|
|
257
|
+
expect(flags.raw).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("edge cases", () => {
|
|
262
|
+
it("handles flag at end without value as boolean true", () => {
|
|
263
|
+
const { flags } = parseArgs(["--verbose"]);
|
|
264
|
+
expect(flags.verbose).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("handles flag followed by another flag (no value)", () => {
|
|
268
|
+
const { flags } = parseArgs(["--daemon", "--json"]);
|
|
269
|
+
expect(flags.daemon).toBe(true);
|
|
270
|
+
expect(flags.json).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("does not consume flag-like value for previous flag", () => {
|
|
274
|
+
const { flags } = parseArgs(["--first", "--second"]);
|
|
275
|
+
expect(flags.first).toBe(true);
|
|
276
|
+
expect(flags.second).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("handles paths with dashes", () => {
|
|
280
|
+
const { positional } = parseArgs(["./my-docs-folder"]);
|
|
281
|
+
expect(positional).toEqual(["./my-docs-folder"]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("handles numeric values", () => {
|
|
285
|
+
const { flags } = parseArgs(["--port", "65535"]);
|
|
286
|
+
expect(flags.port).toBe("65535");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("getVersion", () => {
|
|
292
|
+
it("returns version from VERSION file when it exists", () => {
|
|
293
|
+
const fs = require("node:fs");
|
|
294
|
+
const path = require("node:path");
|
|
295
|
+
const versionPath = path.join(__dirname, "../../VERSION");
|
|
296
|
+
|
|
297
|
+
// Check if VERSION file exists
|
|
298
|
+
let expectedVersion: string;
|
|
299
|
+
try {
|
|
300
|
+
expectedVersion = fs.readFileSync(versionPath, "utf-8").trim();
|
|
301
|
+
} catch {
|
|
302
|
+
expectedVersion = "0.0.0";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const version = getVersion(versionPath);
|
|
306
|
+
expect(version).toBe(expectedVersion);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("returns 0.0.0 when VERSION file does not exist", () => {
|
|
310
|
+
const version = getVersion("/nonexistent/path/VERSION");
|
|
311
|
+
expect(version).toBe("0.0.0");
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("generateHelp", () => {
|
|
316
|
+
it("includes version in help text", () => {
|
|
317
|
+
const help = generateHelp("1.2.3");
|
|
318
|
+
expect(help).toContain("kitfly v1.2.3");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("includes all main commands", () => {
|
|
322
|
+
const help = generateHelp("1.0.0");
|
|
323
|
+
expect(help).toContain("kitfly dev [folder]");
|
|
324
|
+
expect(help).toContain("kitfly build [folder]");
|
|
325
|
+
expect(help).toContain("kitfly bundle [folder]");
|
|
326
|
+
expect(help).toContain("kitfly init [name]");
|
|
327
|
+
expect(help).toContain("kitfly servers");
|
|
328
|
+
expect(help).toContain("kitfly stop <port|all>");
|
|
329
|
+
expect(help).toContain("kitfly version");
|
|
330
|
+
expect(help).toContain("kitfly help");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("includes dev options", () => {
|
|
334
|
+
const help = generateHelp("1.0.0");
|
|
335
|
+
expect(help).toContain("--port <n>");
|
|
336
|
+
expect(help).toContain("--host <h>");
|
|
337
|
+
expect(help).toContain("--daemon, -d");
|
|
338
|
+
expect(help).toContain("--json");
|
|
339
|
+
expect(help).toContain("--no-open");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("includes build/bundle options", () => {
|
|
343
|
+
const help = generateHelp("1.0.0");
|
|
344
|
+
expect(help).toContain("--out <dir>");
|
|
345
|
+
expect(help).toContain("--name <file>");
|
|
346
|
+
expect(help).toContain("--no-raw");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("includes stop options", () => {
|
|
350
|
+
const help = generateHelp("1.0.0");
|
|
351
|
+
expect(help).toContain("--force");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("includes examples", () => {
|
|
355
|
+
const help = generateHelp("1.0.0");
|
|
356
|
+
expect(help).toContain("kitfly dev ./my-docs --port 4000 --daemon");
|
|
357
|
+
expect(help).toContain("kitfly build ./docs --out ./public");
|
|
358
|
+
expect(help).toContain("kitfly init my-handbook");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("includes documentation URL", () => {
|
|
362
|
+
const help = generateHelp("1.0.0");
|
|
363
|
+
expect(help).toContain("https://kitfly.app");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("routeCommand", () => {
|
|
368
|
+
describe("dev command", () => {
|
|
369
|
+
it("routes dev command with folder", () => {
|
|
370
|
+
const { command, positional } = routeCommand(["dev", "./docs"]);
|
|
371
|
+
expect(command).toBe("dev");
|
|
372
|
+
expect(positional).toEqual(["./docs"]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("routes dev command without folder (uses default)", () => {
|
|
376
|
+
const { command, positional } = routeCommand(["dev"]);
|
|
377
|
+
expect(command).toBe("dev");
|
|
378
|
+
expect(positional).toEqual([]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("routes serve as alias for dev", () => {
|
|
382
|
+
const { command, positional } = routeCommand(["serve", "./content"]);
|
|
383
|
+
expect(command).toBe("serve");
|
|
384
|
+
expect(positional).toEqual(["./content"]);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("parses dev command with all options", () => {
|
|
388
|
+
const { command, positional, flags } = routeCommand([
|
|
389
|
+
"dev",
|
|
390
|
+
"./docs",
|
|
391
|
+
"--port",
|
|
392
|
+
"4000",
|
|
393
|
+
"--host",
|
|
394
|
+
"localhost",
|
|
395
|
+
"--daemon",
|
|
396
|
+
"--no-open",
|
|
397
|
+
]);
|
|
398
|
+
expect(command).toBe("dev");
|
|
399
|
+
expect(positional).toEqual(["./docs"]);
|
|
400
|
+
expect(flags.port).toBe("4000");
|
|
401
|
+
expect(flags.host).toBe("localhost");
|
|
402
|
+
expect(flags.daemon).toBe(true);
|
|
403
|
+
expect(flags.open).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe("build command", () => {
|
|
408
|
+
it("routes build command with folder", () => {
|
|
409
|
+
const { command, positional } = routeCommand(["build", "./content"]);
|
|
410
|
+
expect(command).toBe("build");
|
|
411
|
+
expect(positional).toEqual(["./content"]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("parses build command with options", () => {
|
|
415
|
+
const { command, flags } = routeCommand(["build", "./docs", "--out", "./public", "--no-raw"]);
|
|
416
|
+
expect(command).toBe("build");
|
|
417
|
+
expect(flags.out).toBe("./public");
|
|
418
|
+
expect(flags.raw).toBe(false);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("bundle command", () => {
|
|
423
|
+
it("routes bundle command", () => {
|
|
424
|
+
const { command, positional } = routeCommand(["bundle", "./docs"]);
|
|
425
|
+
expect(command).toBe("bundle");
|
|
426
|
+
expect(positional).toEqual(["./docs"]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("parses bundle command with options", () => {
|
|
430
|
+
const { command, flags } = routeCommand(["bundle", "--out", "./dist", "--name", "site.html"]);
|
|
431
|
+
expect(command).toBe("bundle");
|
|
432
|
+
expect(flags.out).toBe("./dist");
|
|
433
|
+
expect(flags.name).toBe("site.html");
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("init command", () => {
|
|
438
|
+
it("routes init command with name", () => {
|
|
439
|
+
const { command, positional } = routeCommand(["init", "my-project"]);
|
|
440
|
+
expect(command).toBe("init");
|
|
441
|
+
expect(positional).toEqual(["my-project"]);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("routes init command without name (will error)", () => {
|
|
445
|
+
const { command, positional } = routeCommand(["init"]);
|
|
446
|
+
expect(command).toBe("init");
|
|
447
|
+
expect(positional).toEqual([]);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("servers command", () => {
|
|
452
|
+
it("routes servers command", () => {
|
|
453
|
+
const { command } = routeCommand(["servers"]);
|
|
454
|
+
expect(command).toBe("servers");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("routes ps as alias for servers", () => {
|
|
458
|
+
const { command } = routeCommand(["ps"]);
|
|
459
|
+
expect(command).toBe("ps");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("parses servers with --json flag", () => {
|
|
463
|
+
const { command, flags } = routeCommand(["servers", "--json"]);
|
|
464
|
+
expect(command).toBe("servers");
|
|
465
|
+
expect(flags.json).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe("stop command", () => {
|
|
470
|
+
it("routes stop command with port", () => {
|
|
471
|
+
const { command, positional } = routeCommand(["stop", "4000"]);
|
|
472
|
+
expect(command).toBe("stop");
|
|
473
|
+
expect(positional).toEqual(["4000"]);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("routes stop all command", () => {
|
|
477
|
+
const { command, positional } = routeCommand(["stop", "all"]);
|
|
478
|
+
expect(command).toBe("stop");
|
|
479
|
+
expect(positional).toEqual(["all"]);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("parses stop with --force flag", () => {
|
|
483
|
+
const { command, flags } = routeCommand(["stop", "3333", "--force"]);
|
|
484
|
+
expect(command).toBe("stop");
|
|
485
|
+
expect(flags.force).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe("version command", () => {
|
|
490
|
+
it("routes version command", () => {
|
|
491
|
+
const { command } = routeCommand(["version"]);
|
|
492
|
+
expect(command).toBe("version");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("routes -v flag", () => {
|
|
496
|
+
const { command } = routeCommand(["-v"]);
|
|
497
|
+
expect(command).toBe("-v");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("routes --version flag", () => {
|
|
501
|
+
const { command } = routeCommand(["--version"]);
|
|
502
|
+
expect(command).toBe("--version");
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe("help command", () => {
|
|
507
|
+
it("routes help command", () => {
|
|
508
|
+
const { command } = routeCommand(["help"]);
|
|
509
|
+
expect(command).toBe("help");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("routes -h flag", () => {
|
|
513
|
+
const { command } = routeCommand(["-h"]);
|
|
514
|
+
expect(command).toBe("-h");
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("routes --help flag", () => {
|
|
518
|
+
const { command } = routeCommand(["--help"]);
|
|
519
|
+
expect(command).toBe("--help");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("routes undefined (no command) to show help", () => {
|
|
523
|
+
const { command } = routeCommand([]);
|
|
524
|
+
expect(command).toBeUndefined();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe("unknown commands", () => {
|
|
529
|
+
it("routes unknown command", () => {
|
|
530
|
+
const { command } = routeCommand(["unknown"]);
|
|
531
|
+
expect(command).toBe("unknown");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("routes misspelled command", () => {
|
|
535
|
+
const { command } = routeCommand(["buidl"]);
|
|
536
|
+
expect(command).toBe("buidl");
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe("validatePort", () => {
|
|
542
|
+
it("returns default port 3333 when no port specified", () => {
|
|
543
|
+
const result = validatePort(undefined);
|
|
544
|
+
expect(result.valid).toBe(true);
|
|
545
|
+
expect(result.port).toBe(3333);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("accepts valid port number", () => {
|
|
549
|
+
const result = validatePort("4000");
|
|
550
|
+
expect(result.valid).toBe(true);
|
|
551
|
+
expect(result.port).toBe(4000);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("accepts minimum valid port", () => {
|
|
555
|
+
const result = validatePort("1");
|
|
556
|
+
expect(result.valid).toBe(true);
|
|
557
|
+
expect(result.port).toBe(1);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("accepts maximum valid port", () => {
|
|
561
|
+
const result = validatePort("65535");
|
|
562
|
+
expect(result.valid).toBe(true);
|
|
563
|
+
expect(result.port).toBe(65535);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("rejects port 0", () => {
|
|
567
|
+
const result = validatePort("0");
|
|
568
|
+
expect(result.valid).toBe(false);
|
|
569
|
+
expect(result.error).toContain("0");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("rejects port above 65535", () => {
|
|
573
|
+
const result = validatePort("65536");
|
|
574
|
+
expect(result.valid).toBe(false);
|
|
575
|
+
expect(result.error).toContain("65536");
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("rejects negative port", () => {
|
|
579
|
+
const result = validatePort("-1");
|
|
580
|
+
expect(result.valid).toBe(false);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("rejects non-numeric port", () => {
|
|
584
|
+
const result = validatePort("abc");
|
|
585
|
+
expect(result.valid).toBe(false);
|
|
586
|
+
expect(result.error).toContain("abc");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("rejects empty string port", () => {
|
|
590
|
+
const result = validatePort("");
|
|
591
|
+
// Empty string is treated like "not provided" by the CLI arg parser.
|
|
592
|
+
expect(result.valid).toBe(true);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("rejects floating point port", () => {
|
|
596
|
+
const result = validatePort("3000.5");
|
|
597
|
+
expect(result.valid).toBe(true); // parseInt will parse to 3000
|
|
598
|
+
expect(result.port).toBe(3000);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe("command argument defaults", () => {
|
|
603
|
+
it("dev defaults folder to current directory", () => {
|
|
604
|
+
const { positional } = routeCommand(["dev"]);
|
|
605
|
+
const folder = positional[0] || ".";
|
|
606
|
+
expect(folder).toBe(".");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("dev defaults port to 3333", () => {
|
|
610
|
+
const { flags } = routeCommand(["dev"]);
|
|
611
|
+
const port = flags.port ? parseInt(flags.port as string, 10) : 3333;
|
|
612
|
+
expect(port).toBe(3333);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("dev defaults host to localhost", () => {
|
|
616
|
+
const { flags } = routeCommand(["dev"]);
|
|
617
|
+
const host = (flags.host as string) || "localhost";
|
|
618
|
+
expect(host).toBe("localhost");
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("dev defaults open to true", () => {
|
|
622
|
+
const { flags } = routeCommand(["dev"]);
|
|
623
|
+
const open = flags.open !== false;
|
|
624
|
+
expect(open).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("build defaults folder to current directory", () => {
|
|
628
|
+
const { positional } = routeCommand(["build"]);
|
|
629
|
+
const folder = positional[0] || ".";
|
|
630
|
+
expect(folder).toBe(".");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("build defaults out to dist", () => {
|
|
634
|
+
const { flags } = routeCommand(["build"]);
|
|
635
|
+
const out = (flags.out as string) || "dist";
|
|
636
|
+
expect(out).toBe("dist");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("build defaults raw to true", () => {
|
|
640
|
+
const { flags } = routeCommand(["build"]);
|
|
641
|
+
const raw = flags.raw !== false;
|
|
642
|
+
expect(raw).toBe(true);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("bundle defaults name to bundle.html", () => {
|
|
646
|
+
const { flags } = routeCommand(["bundle"]);
|
|
647
|
+
const name = (flags.name as string) || "bundle.html";
|
|
648
|
+
expect(name).toBe("bundle.html");
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
describe("daemon mode detection", () => {
|
|
653
|
+
it("detects daemon mode from --daemon flag", () => {
|
|
654
|
+
const { flags } = routeCommand(["dev", "--daemon"]);
|
|
655
|
+
const daemon = flags.daemon === true || flags.d === true || flags.json === true;
|
|
656
|
+
expect(daemon).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("detects daemon mode from -d flag", () => {
|
|
660
|
+
const { flags } = routeCommand(["dev", "-d"]);
|
|
661
|
+
const daemon = flags.daemon === true || flags.d === true || flags.json === true;
|
|
662
|
+
expect(daemon).toBe(true);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("detects daemon mode from --json flag (implied)", () => {
|
|
666
|
+
const { flags } = routeCommand(["dev", "--json"]);
|
|
667
|
+
const daemon = flags.daemon === true || flags.d === true || flags.json === true;
|
|
668
|
+
expect(daemon).toBe(true);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("detects non-daemon mode when no flags", () => {
|
|
672
|
+
const { flags } = routeCommand(["dev"]);
|
|
673
|
+
const daemon = flags.daemon === true || flags.d === true || flags.json === true;
|
|
674
|
+
expect(daemon).toBe(false);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
describe("host warning detection", () => {
|
|
679
|
+
it("detects 0.0.0.0 as all-interfaces binding", () => {
|
|
680
|
+
const { flags } = routeCommand(["dev", "--host", "0.0.0.0"]);
|
|
681
|
+
const host = flags.host as string;
|
|
682
|
+
const shouldWarn = host === "0.0.0.0" || host === "::";
|
|
683
|
+
expect(shouldWarn).toBe(true);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("detects :: as all-interfaces binding", () => {
|
|
687
|
+
const { flags } = routeCommand(["dev", "--host", "::"]);
|
|
688
|
+
const host = flags.host as string;
|
|
689
|
+
const shouldWarn = host === "0.0.0.0" || host === "::";
|
|
690
|
+
expect(shouldWarn).toBe(true);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("does not warn for localhost", () => {
|
|
694
|
+
const { flags } = routeCommand(["dev", "--host", "localhost"]);
|
|
695
|
+
const host = flags.host as string;
|
|
696
|
+
const shouldWarn = host === "0.0.0.0" || host === "::";
|
|
697
|
+
expect(shouldWarn).toBe(false);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("does not warn for 127.0.0.1", () => {
|
|
701
|
+
const { flags } = routeCommand(["dev", "--host", "127.0.0.1"]);
|
|
702
|
+
const host = flags.host as string;
|
|
703
|
+
const shouldWarn = host === "0.0.0.0" || host === "::";
|
|
704
|
+
expect(shouldWarn).toBe(false);
|
|
705
|
+
});
|
|
706
|
+
});
|