kennel 0.1.8

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/bin/kennel.mjs +3 -0
  4. package/dist/automations/notifications.d.ts +40 -0
  5. package/dist/automations/notifications.js +220 -0
  6. package/dist/cli/dispatch.d.ts +1 -0
  7. package/dist/cli/dispatch.js +51 -0
  8. package/dist/cli/usage.d.ts +1 -0
  9. package/dist/cli/usage.js +14 -0
  10. package/dist/deploy/cloudflare.d.ts +6 -0
  11. package/dist/deploy/cloudflare.js +91 -0
  12. package/dist/deploy/environment.d.ts +2 -0
  13. package/dist/deploy/environment.js +47 -0
  14. package/dist/deploy/smoke-tests.d.ts +3 -0
  15. package/dist/deploy/smoke-tests.js +71 -0
  16. package/dist/deploy/trigger-dev.d.ts +2 -0
  17. package/dist/deploy/trigger-dev.js +255 -0
  18. package/dist/main.d.ts +1 -0
  19. package/dist/main.js +8 -0
  20. package/dist/mcp/agent-tools.d.ts +8 -0
  21. package/dist/mcp/agent-tools.js +247 -0
  22. package/dist/mcp/config.d.ts +25 -0
  23. package/dist/mcp/config.js +171 -0
  24. package/dist/mcp/data.d.ts +14 -0
  25. package/dist/mcp/data.js +120 -0
  26. package/dist/mcp/function-source.d.ts +7 -0
  27. package/dist/mcp/function-source.js +40 -0
  28. package/dist/mcp/generate.d.ts +1 -0
  29. package/dist/mcp/generate.js +47 -0
  30. package/dist/mcp/install-page.d.ts +1 -0
  31. package/dist/mcp/install-page.js +360 -0
  32. package/dist/notifications/email-worker.d.ts +8 -0
  33. package/dist/notifications/email-worker.js +137 -0
  34. package/dist/package/mcp-data.d.ts +1 -0
  35. package/dist/package/mcp-data.js +1 -0
  36. package/dist/package/mcp-email-worker.d.ts +1 -0
  37. package/dist/package/mcp-email-worker.js +1 -0
  38. package/dist/package/mcp-http.d.ts +1 -0
  39. package/dist/package/mcp-http.js +1 -0
  40. package/dist/package/mcp-stdio.d.ts +1 -0
  41. package/dist/package/mcp-stdio.js +1 -0
  42. package/dist/package/mcp.d.ts +1 -0
  43. package/dist/package/mcp.js +1 -0
  44. package/dist/publish/cloudflare-init.d.ts +12 -0
  45. package/dist/publish/cloudflare-init.js +98 -0
  46. package/dist/publish/config.d.ts +28 -0
  47. package/dist/publish/config.js +190 -0
  48. package/dist/publish/options.d.ts +13 -0
  49. package/dist/publish/options.js +78 -0
  50. package/dist/schema/doctor.d.ts +1 -0
  51. package/dist/schema/doctor.js +69 -0
  52. package/dist/schema/documents.d.ts +18 -0
  53. package/dist/schema/documents.js +127 -0
  54. package/dist/schema/pointers.d.ts +5 -0
  55. package/dist/schema/pointers.js +48 -0
  56. package/dist/schema/types.d.ts +36 -0
  57. package/dist/schema/types.js +1 -0
  58. package/dist/schema/validation.d.ts +8 -0
  59. package/dist/schema/validation.js +212 -0
  60. package/dist/shared/json.d.ts +4 -0
  61. package/dist/shared/json.js +56 -0
  62. package/dist/shared/object.d.ts +2 -0
  63. package/dist/shared/object.js +3 -0
  64. package/dist/shared/package-manager.d.ts +11 -0
  65. package/dist/shared/package-manager.js +39 -0
  66. package/dist/shared/path.d.ts +4 -0
  67. package/dist/shared/path.js +40 -0
  68. package/dist/shared/process.d.ts +7 -0
  69. package/dist/shared/process.js +12 -0
  70. package/dist/shared/project-config.d.ts +2 -0
  71. package/dist/shared/project-config.js +26 -0
  72. package/dist/site/dev.d.ts +10 -0
  73. package/dist/site/dev.js +128 -0
  74. package/dist/site/vitepress.d.ts +21 -0
  75. package/dist/site/vitepress.js +66 -0
  76. package/package.json +74 -0
  77. package/templates/cloudflare/email-code-worker.js +344 -0
@@ -0,0 +1,212 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import * as Ajv2020Module from 'ajv/dist/2020.js';
4
+ import * as addFormatsModule from 'ajv-formats';
5
+ import { isObject } from '../shared/object.js';
6
+ import { displayPath } from '../shared/path.js';
7
+ import { documentConfig, findNearestSchema, IGNORED_DIRECTORIES, loadSchemaFor, matchesDocumentSpec, matchesGlob, parseDocument, parserForPath, SCHEMA_FILES, } from './documents.js';
8
+ import { shouldApplyRule, valuesAtPointer } from './pointers.js';
9
+ const schemaCache = new Map();
10
+ export function createAjv(schema) {
11
+ const Ajv2020 = Ajv2020Module.default ?? Ajv2020Module;
12
+ const addFormats = addFormatsModule.default ?? addFormatsModule;
13
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
14
+ addFormats(ajv);
15
+ return ajv.compile(schema);
16
+ }
17
+ export function cachedSchemaFor(targetPath) {
18
+ const schemaPath = findNearestSchema(targetPath);
19
+ if (!schemaPath)
20
+ return null;
21
+ const cached = schemaCache.get(schemaPath);
22
+ if (cached)
23
+ return cached;
24
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
25
+ const documents = documentConfig(schema);
26
+ const entry = {
27
+ schemaPath,
28
+ schema,
29
+ validate: createAjv(schema),
30
+ documentSpecs: documents.specs,
31
+ usingDefaultDocuments: documents.usingDefault,
32
+ };
33
+ schemaCache.set(schemaPath, entry);
34
+ return entry;
35
+ }
36
+ export function validateFile(filePath, schemaPath, schema, validate, parser = parserForPath(filePath)) {
37
+ const document = parseDocument(filePath, parser);
38
+ const schemaValid = validate(document);
39
+ const kennelErrors = validateKennelRequirements(filePath, schema, document);
40
+ const errors = [...(validate.errors ?? []), ...kennelErrors];
41
+ return {
42
+ filePath,
43
+ schemaPath,
44
+ valid: schemaValid && kennelErrors.length === 0,
45
+ errors,
46
+ };
47
+ }
48
+ function relativeReferenceIsSafe(filePath) {
49
+ return !filePath.startsWith('/') && !filePath.split('/').includes('..');
50
+ }
51
+ export function validateKennelRequirements(filePath, schema, document) {
52
+ const config = schema['x-kennel'];
53
+ if (!isObject(config))
54
+ return [];
55
+ const fileDir = dirname(filePath);
56
+ const errors = [];
57
+ const requiredDirectories = Array.isArray(config.requiredDirectories) ? config.requiredDirectories : [];
58
+ for (const directoryName of requiredDirectories) {
59
+ if (typeof directoryName !== 'string')
60
+ continue;
61
+ const directoryPath = join(fileDir, directoryName);
62
+ if (!existsSync(directoryPath) || !statSync(directoryPath).isDirectory()) {
63
+ errors.push({
64
+ instancePath: '/',
65
+ message: `missing required directory ${directoryName.replace(/\/?$/, '/')}`,
66
+ });
67
+ }
68
+ }
69
+ const requiredFiles = Array.isArray(config.requiredFiles) ? config.requiredFiles : [];
70
+ for (const fileName of requiredFiles) {
71
+ if (typeof fileName !== 'string')
72
+ continue;
73
+ const requiredFilePath = join(fileDir, fileName);
74
+ if (!existsSync(requiredFilePath) || !statSync(requiredFilePath).isFile()) {
75
+ errors.push({
76
+ instancePath: '/',
77
+ message: `missing required file ${fileName}`,
78
+ });
79
+ }
80
+ }
81
+ const directoryContents = Array.isArray(config.directoryContents) ? config.directoryContents : [];
82
+ for (const rule of directoryContents) {
83
+ if (!isObject(rule) || typeof rule.path !== 'string' || !shouldApplyRule(document, rule))
84
+ continue;
85
+ const directoryPath = join(fileDir, rule.path);
86
+ const label = rule.path.replace(/\/?$/, '/');
87
+ if (!existsSync(directoryPath) || !statSync(directoryPath).isDirectory()) {
88
+ errors.push({
89
+ instancePath: '/',
90
+ message: `missing required directory ${label}`,
91
+ });
92
+ continue;
93
+ }
94
+ const glob = typeof rule.glob === 'string' ? rule.glob : '*';
95
+ const minFiles = Number.isInteger(rule.minFiles) ? Number(rule.minFiles) : 1;
96
+ const fileCount = readdirSync(directoryPath, { withFileTypes: true })
97
+ .filter((entry) => entry.isFile() && matchesGlob(entry.name, glob))
98
+ .length;
99
+ if (fileCount < minFiles) {
100
+ errors.push({
101
+ instancePath: '/',
102
+ message: `${label} requires at least ${minFiles} ${glob} file${minFiles === 1 ? '' : 's'}`,
103
+ });
104
+ }
105
+ }
106
+ const fileReferences = Array.isArray(config.fileReferences) ? config.fileReferences : [];
107
+ for (const rule of fileReferences) {
108
+ if (!isObject(rule) || typeof rule.pointer !== 'string' || !shouldApplyRule(document, rule))
109
+ continue;
110
+ const values = valuesAtPointer(document, rule.pointer);
111
+ if (values.length === 0 && rule.required === true) {
112
+ errors.push({
113
+ instancePath: rule.pointer,
114
+ message: 'referenced path is required',
115
+ });
116
+ continue;
117
+ }
118
+ for (const value of values) {
119
+ if (value === null && rule.allowNull === true)
120
+ continue;
121
+ if (value === undefined && rule.required !== true)
122
+ continue;
123
+ if (typeof value !== 'string' || value.trim() === '') {
124
+ errors.push({
125
+ instancePath: rule.pointer,
126
+ message: 'referenced path must be a non-empty string',
127
+ });
128
+ continue;
129
+ }
130
+ if (value.startsWith('http://') || value.startsWith('https://'))
131
+ continue;
132
+ if (!relativeReferenceIsSafe(value)) {
133
+ errors.push({
134
+ instancePath: rule.pointer,
135
+ message: `referenced path must stay inside the document folder (${value})`,
136
+ });
137
+ continue;
138
+ }
139
+ const requiredType = rule.type === 'directory' ? 'directory' : 'file';
140
+ const referencedPath = join(fileDir, value);
141
+ const exists = existsSync(referencedPath);
142
+ const correctType = exists && (requiredType === 'directory'
143
+ ? statSync(referencedPath).isDirectory()
144
+ : statSync(referencedPath).isFile());
145
+ if (!correctType) {
146
+ errors.push({
147
+ instancePath: rule.pointer,
148
+ message: `referenced ${requiredType} does not exist (${value})`,
149
+ });
150
+ }
151
+ }
152
+ }
153
+ return errors;
154
+ }
155
+ export function candidateFiles(targetPath) {
156
+ const target = resolve(targetPath);
157
+ if (statSync(target).isFile()) {
158
+ const entry = cachedSchemaFor(target);
159
+ if (!entry)
160
+ throw new Error(`No ${SCHEMA_FILES.join(' or ')} found from ${target} upward`);
161
+ const spec = entry.documentSpecs.find((documentSpec) => (matchesDocumentSpec(target, entry.schemaPath, documentSpec)));
162
+ if (!spec) {
163
+ throw new Error(`${target} is not listed in ${entry.schemaPath} x-kennel.documents`);
164
+ }
165
+ return [{ filePath: target, schemaEntry: entry, parser: spec.parser }];
166
+ }
167
+ const found = [];
168
+ function walk(directory) {
169
+ const schemaEntry = cachedSchemaFor(directory);
170
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
171
+ if (entry.isDirectory()) {
172
+ if (entry.name.startsWith('.') || IGNORED_DIRECTORIES.has(entry.name))
173
+ continue;
174
+ walk(join(directory, entry.name));
175
+ continue;
176
+ }
177
+ if (!entry.isFile() || !schemaEntry || SCHEMA_FILES.includes(entry.name))
178
+ continue;
179
+ const filePath = join(directory, entry.name);
180
+ const spec = schemaEntry.documentSpecs.find((documentSpec) => (matchesDocumentSpec(filePath, schemaEntry.schemaPath, documentSpec)));
181
+ if (spec) {
182
+ found.push({ filePath, schemaEntry, parser: spec.parser });
183
+ }
184
+ }
185
+ }
186
+ walk(target);
187
+ return found.sort((a, b) => a.filePath.localeCompare(b.filePath));
188
+ }
189
+ export function validateTarget(targetPath) {
190
+ const candidates = candidateFiles(targetPath);
191
+ if (candidates.length === 0)
192
+ throw new Error(`No schema-declared documents found in ${targetPath}`);
193
+ const results = candidates.map(({ filePath, schemaEntry, parser }) => {
194
+ return validateFile(filePath, schemaEntry.schemaPath, schemaEntry.schema, schemaEntry.validate, parser);
195
+ });
196
+ for (const result of results) {
197
+ if (result.valid) {
198
+ console.log(`ok ${displayPath(result.filePath)}`);
199
+ continue;
200
+ }
201
+ console.log(`not ok ${displayPath(result.filePath)}`);
202
+ for (const error of result.errors) {
203
+ console.log(` - ${error.instancePath || '/'} ${error.message}`);
204
+ }
205
+ }
206
+ console.log(`\n${results.filter((result) => result.valid).length}/${results.length} files valid`);
207
+ return results.every((result) => result.valid);
208
+ }
209
+ export function schemaFor(targetPath) {
210
+ const { schemaPath } = loadSchemaFor(targetPath);
211
+ console.log(schemaPath);
212
+ }
@@ -0,0 +1,4 @@
1
+ export declare function readJson<T = any>(filePath: string): T;
2
+ export declare function stripJsonComments(content: string): string;
3
+ export declare function readJsonc<T = any>(filePath: string): T;
4
+ export declare function writeJson(filePath: string, value: unknown): void;
@@ -0,0 +1,56 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ export function readJson(filePath) {
3
+ return JSON.parse(readFileSync(filePath, 'utf8'));
4
+ }
5
+ export function stripJsonComments(content) {
6
+ let output = '';
7
+ let inString = false;
8
+ let escaped = false;
9
+ for (let index = 0; index < content.length; index += 1) {
10
+ const char = content[index];
11
+ const next = content[index + 1];
12
+ if (inString) {
13
+ output += char;
14
+ if (escaped) {
15
+ escaped = false;
16
+ continue;
17
+ }
18
+ if (char === '\\') {
19
+ escaped = true;
20
+ continue;
21
+ }
22
+ if (char === '"')
23
+ inString = false;
24
+ continue;
25
+ }
26
+ if (char === '"') {
27
+ inString = true;
28
+ output += char;
29
+ continue;
30
+ }
31
+ if (char === '/' && next === '/') {
32
+ while (index < content.length && content[index] !== '\n')
33
+ index += 1;
34
+ output += '\n';
35
+ continue;
36
+ }
37
+ if (char === '/' && next === '*') {
38
+ index += 2;
39
+ while (index < content.length && !(content[index] === '*' && content[index + 1] === '/')) {
40
+ if (content[index] === '\n')
41
+ output += '\n';
42
+ index += 1;
43
+ }
44
+ index += 1;
45
+ continue;
46
+ }
47
+ output += char;
48
+ }
49
+ return output;
50
+ }
51
+ export function readJsonc(filePath) {
52
+ return JSON.parse(stripJsonComments(readFileSync(filePath, 'utf8')));
53
+ }
54
+ export function writeJson(filePath, value) {
55
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
56
+ }
@@ -0,0 +1,2 @@
1
+ export type JsonObject = Record<string, unknown>;
2
+ export declare function isObject(value: unknown): value is JsonObject;
@@ -0,0 +1,3 @@
1
+ export function isObject(value) {
2
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
3
+ }
@@ -0,0 +1,11 @@
1
+ type PackageJson = {
2
+ dependencies?: Record<string, unknown>;
3
+ devDependencies?: Record<string, unknown>;
4
+ peerDependencies?: Record<string, unknown>;
5
+ optionalDependencies?: Record<string, unknown>;
6
+ };
7
+ export declare function packageManagerFor(root: string): string;
8
+ export declare function runArgsFor(packageManager: string, script: string, scriptArgs: string[]): string[];
9
+ export declare function localBin(root: string, name: string): string | null;
10
+ export declare function hasDependency(pkg: PackageJson, name: string): boolean;
11
+ export {};
@@ -0,0 +1,39 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { isObject } from './object.js';
4
+ const LOCKFILE_PACKAGE_MANAGERS = [
5
+ ['bun.lock', 'bun'],
6
+ ['bun.lockb', 'bun'],
7
+ ['pnpm-lock.yaml', 'pnpm'],
8
+ ['yarn.lock', 'yarn'],
9
+ ['package-lock.json', 'npm'],
10
+ ];
11
+ export function packageManagerFor(root) {
12
+ for (const [lockfile, packageManager] of LOCKFILE_PACKAGE_MANAGERS) {
13
+ if (existsSync(join(root, lockfile)))
14
+ return packageManager;
15
+ }
16
+ return 'npm';
17
+ }
18
+ export function runArgsFor(packageManager, script, scriptArgs) {
19
+ if (packageManager === 'npm')
20
+ return ['run', script, '--', ...scriptArgs];
21
+ return ['run', script, ...scriptArgs];
22
+ }
23
+ export function localBin(root, name) {
24
+ const suffixes = process.platform === 'win32' ? ['.cmd', '.ps1', ''] : [''];
25
+ for (const suffix of suffixes) {
26
+ const binPath = join(root, 'node_modules', '.bin', `${name}${suffix}`);
27
+ if (existsSync(binPath))
28
+ return binPath;
29
+ }
30
+ return null;
31
+ }
32
+ export function hasDependency(pkg, name) {
33
+ return [
34
+ pkg.dependencies,
35
+ pkg.devDependencies,
36
+ pkg.peerDependencies,
37
+ pkg.optionalDependencies,
38
+ ].some((dependencies) => isObject(dependencies) && dependencies[name] !== undefined);
39
+ }
@@ -0,0 +1,4 @@
1
+ export declare function displayPath(filePath: string): string;
2
+ export declare function nearestFile(startPath: string, fileName: string): string | null;
3
+ export declare function slugifyHostname(value: string): string;
4
+ export declare function normalizeHostname(value: unknown): string;
@@ -0,0 +1,40 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { dirname, relative, resolve } from 'node:path';
3
+ export function displayPath(filePath) {
4
+ const resolved = resolve(filePath);
5
+ const relativePath = relative(process.cwd(), resolved);
6
+ if (relativePath === '')
7
+ return '.';
8
+ if (relativePath.startsWith('..'))
9
+ return resolved;
10
+ return relativePath;
11
+ }
12
+ export function nearestFile(startPath, fileName) {
13
+ let current = resolve(startPath);
14
+ if (existsSync(current) && statSync(current).isFile())
15
+ current = dirname(current);
16
+ while (true) {
17
+ const filePath = resolve(current, fileName);
18
+ if (existsSync(filePath) && statSync(filePath).isFile())
19
+ return filePath;
20
+ const parent = dirname(current);
21
+ if (parent === current)
22
+ return null;
23
+ current = parent;
24
+ }
25
+ }
26
+ export function slugifyHostname(value) {
27
+ const withoutScope = value.replace(/^@[^/]+\//, '');
28
+ return withoutScope
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9-]+/g, '-')
31
+ .replace(/^-+|-+$/g, '');
32
+ }
33
+ export function normalizeHostname(value) {
34
+ if (typeof value !== 'string' || value.trim() === '')
35
+ return '';
36
+ return value
37
+ .trim()
38
+ .replace(/^https?:\/\//, '')
39
+ .replace(/\/.*$/, '');
40
+ }
@@ -0,0 +1,7 @@
1
+ export type ChildCommand = {
2
+ command: string;
3
+ args: string[];
4
+ cwd?: string;
5
+ };
6
+ export declare function commandExists(command: string): boolean;
7
+ export declare function commandLine(command: string, args: string[]): string;
@@ -0,0 +1,12 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export function commandExists(command) {
3
+ const result = spawnSync('sh', ['-c', `command -v ${command} >/dev/null 2>&1`], {
4
+ stdio: 'ignore',
5
+ });
6
+ return result.status === 0;
7
+ }
8
+ export function commandLine(command, args) {
9
+ return [command, ...args]
10
+ .map((arg) => (/^[A-Za-z0-9_./:@=-]+$/.test(arg) ? arg : JSON.stringify(arg)))
11
+ .join(' ');
12
+ }
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_PROJECT_CONFIG = "raggle.json";
2
+ export declare function projectConfigPath(targetPath: string, configFile?: string): string;
@@ -0,0 +1,26 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { dirname, isAbsolute, resolve } from 'node:path';
3
+ import { nearestFile } from './path.js';
4
+ export const DEFAULT_PROJECT_CONFIG = 'raggle.json';
5
+ function targetDirectory(targetPath) {
6
+ const target = resolve(targetPath);
7
+ if (existsSync(target) && statSync(target).isFile())
8
+ return dirname(target);
9
+ return target;
10
+ }
11
+ export function projectConfigPath(targetPath, configFile = DEFAULT_PROJECT_CONFIG) {
12
+ if (!configFile.trim())
13
+ throw new Error('config file name is required');
14
+ if (configFile.includes('/') || configFile.includes('\\') || isAbsolute(configFile)) {
15
+ const configPath = isAbsolute(configFile)
16
+ ? configFile
17
+ : resolve(targetDirectory(targetPath), configFile);
18
+ if (existsSync(configPath) && statSync(configPath).isFile())
19
+ return configPath;
20
+ throw new Error(`No ${configFile} found from ${resolve(targetPath)}`);
21
+ }
22
+ const configPath = nearestFile(targetPath, configFile);
23
+ if (configPath)
24
+ return configPath;
25
+ throw new Error(`No ${configFile} found from ${resolve(targetPath)} upward`);
26
+ }
@@ -0,0 +1,10 @@
1
+ export type DevOptions = {
2
+ targetPath: string;
3
+ port: string;
4
+ script: string | null;
5
+ configFile: string;
6
+ dryRun: boolean;
7
+ extra: string[];
8
+ };
9
+ export declare function splitDevArgs(input: string[]): DevOptions;
10
+ export declare function dev(input: string[]): void;
@@ -0,0 +1,128 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { readJson } from '../shared/json.js';
5
+ import { normalizeHostname, slugifyHostname } from '../shared/path.js';
6
+ import { DEFAULT_PROJECT_CONFIG, projectConfigPath } from '../shared/project-config.js';
7
+ import { commandLine } from '../shared/process.js';
8
+ import { buildScriptDevCommand, buildVitePressDevCommand, detectedPlatform, displayBasePath, } from './vitepress.js';
9
+ export function splitDevArgs(input) {
10
+ const options = {
11
+ targetPath: '.',
12
+ port: '3000',
13
+ script: null,
14
+ configFile: DEFAULT_PROJECT_CONFIG,
15
+ dryRun: false,
16
+ extra: [],
17
+ };
18
+ const args = [...input];
19
+ const passthrough = args.indexOf('--');
20
+ if (passthrough !== -1) {
21
+ options.extra = args.splice(passthrough + 1);
22
+ args.pop();
23
+ }
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const arg = args[index];
26
+ if (arg === '--dry-run') {
27
+ options.dryRun = true;
28
+ continue;
29
+ }
30
+ if (arg === '--port') {
31
+ options.port = args[index + 1];
32
+ index += 1;
33
+ continue;
34
+ }
35
+ if (arg.startsWith('--port=')) {
36
+ options.port = arg.slice('--port='.length);
37
+ continue;
38
+ }
39
+ if (arg === '--script') {
40
+ options.script = args[index + 1];
41
+ index += 1;
42
+ continue;
43
+ }
44
+ if (arg.startsWith('--script=')) {
45
+ options.script = arg.slice('--script='.length);
46
+ continue;
47
+ }
48
+ if (arg === '--config') {
49
+ options.configFile = args[index + 1] ?? '';
50
+ index += 1;
51
+ continue;
52
+ }
53
+ if (arg.startsWith('--config=')) {
54
+ options.configFile = arg.slice('--config='.length);
55
+ continue;
56
+ }
57
+ if (arg.startsWith('-'))
58
+ throw new Error(`Unknown dev option: ${arg}`);
59
+ options.targetPath = arg;
60
+ }
61
+ if (!options.port)
62
+ throw new Error('dev requires a port');
63
+ if (!options.configFile)
64
+ throw new Error('dev requires a config file name');
65
+ return options;
66
+ }
67
+ function localDevHost(config, rawName) {
68
+ const dev = config.dev && typeof config.dev === 'object' && !Array.isArray(config.dev) ? config.dev : {};
69
+ const host = normalizeHostname(dev.host ?? dev.hostname ?? config.host ?? config.hostname);
70
+ if (host)
71
+ return host;
72
+ const label = slugifyHostname(rawName);
73
+ if (!label)
74
+ throw new Error(`Could not derive a local hostname from "${rawName}"`);
75
+ return `${label}.localhost`;
76
+ }
77
+ function devRoot(configRoot, config) {
78
+ const dev = config.dev && typeof config.dev === 'object' && !Array.isArray(config.dev) ? config.dev : {};
79
+ const root = dev.root ?? config.devRoot;
80
+ if (typeof root !== 'string' || root.trim() === '')
81
+ return configRoot;
82
+ return resolve(configRoot, root);
83
+ }
84
+ function devToolRoot(configRoot, config) {
85
+ const dev = config.dev && typeof config.dev === 'object' && !Array.isArray(config.dev) ? config.dev : {};
86
+ const root = dev.toolRoot;
87
+ if (typeof root !== 'string' || root.trim() === '')
88
+ return null;
89
+ return resolve(configRoot, root);
90
+ }
91
+ function devScript(config) {
92
+ const dev = config.dev && typeof config.dev === 'object' && !Array.isArray(config.dev) ? config.dev : {};
93
+ if (typeof dev.script !== 'string' || dev.script.trim() === '')
94
+ return null;
95
+ return dev.script;
96
+ }
97
+ export function dev(input) {
98
+ const options = splitDevArgs(input);
99
+ const configPath = projectConfigPath(options.targetPath, options.configFile);
100
+ const configRoot = dirname(configPath);
101
+ const config = readJson(configPath);
102
+ const root = devRoot(configRoot, config);
103
+ const packagePath = join(root, 'package.json');
104
+ if (!existsSync(packagePath))
105
+ throw new Error(`No package.json found for ${configPath} dev root ${root}`);
106
+ const pkg = readJson(packagePath);
107
+ const rawName = config.name ?? config.project?.name ?? pkg.name ?? config.title;
108
+ if (typeof rawName !== 'string' || rawName.trim() === '') {
109
+ throw new Error(`${configPath} must define name, or package.json must define name`);
110
+ }
111
+ const host = localDevHost(config, rawName);
112
+ const scriptArgs = ['--host', host, '--port', options.port, ...options.extra];
113
+ const script = options.script ?? devScript(config);
114
+ const platform = script ? 'script' : detectedPlatform(root, pkg);
115
+ const toolRoot = devToolRoot(configRoot, config) ?? root;
116
+ const child = platform === 'vitepress'
117
+ ? buildVitePressDevCommand(root, options, scriptArgs, toolRoot)
118
+ : buildScriptDevCommand(root, pkg, { script }, scriptArgs);
119
+ console.log(commandLine(child.command, child.args));
120
+ console.log(`http://${host}:${options.port}${displayBasePath(config)}`);
121
+ if (options.dryRun)
122
+ return;
123
+ const result = spawnSync(child.command, child.args, {
124
+ cwd: child.cwd ?? root,
125
+ stdio: 'inherit',
126
+ });
127
+ process.exit(result.status ?? 1);
128
+ }
@@ -0,0 +1,21 @@
1
+ import type { ChildCommand } from '../shared/process.js';
2
+ export type PackageJson = {
3
+ name?: string;
4
+ scripts?: Record<string, string>;
5
+ dependencies?: Record<string, unknown>;
6
+ devDependencies?: Record<string, unknown>;
7
+ peerDependencies?: Record<string, unknown>;
8
+ optionalDependencies?: Record<string, unknown>;
9
+ };
10
+ export declare function vitepressConfig(root: string): string | null;
11
+ export declare function vitepressOutputDirectory(root: string): string;
12
+ export declare function detectedPlatform(root: string, pkg: PackageJson): 'vitepress' | 'script';
13
+ export declare function displayBasePath(config: {
14
+ base?: unknown;
15
+ }): string;
16
+ export declare function buildScriptDevCommand(root: string, pkg: PackageJson, options: {
17
+ script: string | null;
18
+ }, scriptArgs: string[]): ChildCommand;
19
+ export declare function buildVitePressDevCommand(root: string, _options: {
20
+ script: string | null;
21
+ }, scriptArgs: string[], toolRoot?: string): ChildCommand;
@@ -0,0 +1,66 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { hasDependency, localBin, packageManagerFor, runArgsFor } from '../shared/package-manager.js';
4
+ export function vitepressConfig(root) {
5
+ const configNames = [
6
+ '.vitepress/config.mts',
7
+ '.vitepress/config.ts',
8
+ '.vitepress/config.mjs',
9
+ '.vitepress/config.js',
10
+ 'docs/.vitepress/config.mts',
11
+ 'docs/.vitepress/config.ts',
12
+ 'docs/.vitepress/config.mjs',
13
+ 'docs/.vitepress/config.js',
14
+ ];
15
+ return configNames.find((configName) => existsSync(join(root, configName))) ?? null;
16
+ }
17
+ export function vitepressOutputDirectory(root) {
18
+ const configPath = vitepressConfig(root);
19
+ if (configPath?.startsWith('docs/'))
20
+ return 'docs/.vitepress/dist';
21
+ return '.vitepress/dist';
22
+ }
23
+ export function detectedPlatform(root, pkg) {
24
+ if (vitepressConfig(root) || hasDependency(pkg, 'vitepress'))
25
+ return 'vitepress';
26
+ return 'script';
27
+ }
28
+ export function displayBasePath(config) {
29
+ if (typeof config.base !== 'string' || config.base.trim() === '')
30
+ return '/';
31
+ const base = config.base.trim();
32
+ const normalized = base.replace(/^\/+|\/+$/g, '');
33
+ if (!normalized)
34
+ return '/';
35
+ return `/${normalized}/`;
36
+ }
37
+ export function buildScriptDevCommand(root, pkg, options, scriptArgs) {
38
+ const scripts = pkg.scripts ?? {};
39
+ const script = options.script ?? (scripts['docs:dev'] ? 'docs:dev' : 'dev');
40
+ if (typeof scripts[script] !== 'string') {
41
+ throw new Error(`${join(root, 'package.json')} does not define script "${script}"`);
42
+ }
43
+ const packageManager = packageManagerFor(root);
44
+ return {
45
+ command: packageManager,
46
+ args: runArgsFor(packageManager, script, scriptArgs),
47
+ };
48
+ }
49
+ export function buildVitePressDevCommand(root, _options, scriptArgs, toolRoot = root) {
50
+ const siteRoot = toolRoot === root ? '.' : root;
51
+ const vitepress = localBin(toolRoot, 'vitepress');
52
+ if (vitepress) {
53
+ const args = ['dev', siteRoot];
54
+ args.push(...scriptArgs);
55
+ return { command: vitepress, args, cwd: toolRoot };
56
+ }
57
+ const packageManager = packageManagerFor(toolRoot);
58
+ if (packageManager === 'npm') {
59
+ const args = ['exec', '--', 'vitepress', 'dev', siteRoot];
60
+ args.push(...scriptArgs);
61
+ return { command: packageManager, args, cwd: toolRoot };
62
+ }
63
+ const args = ['x', 'vitepress', 'dev', siteRoot];
64
+ args.push(...scriptArgs);
65
+ return { command: packageManager, args, cwd: toolRoot };
66
+ }