@kidd-cli/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/commands/add/command.d.mts +6 -0
- package/dist/commands/add/command.mjs +137 -0
- package/dist/commands/add/index.d.mts +6 -0
- package/dist/commands/add/index.mjs +7 -0
- package/dist/commands/add/middleware.d.mts +6 -0
- package/dist/commands/add/middleware.mjs +101 -0
- package/dist/commands/build.d.mts +6 -0
- package/dist/commands/build.mjs +163 -0
- package/dist/commands/commands.d.mts +13 -0
- package/dist/commands/commands.mjs +135 -0
- package/dist/commands/dev.d.mts +12 -0
- package/dist/commands/dev.mjs +68 -0
- package/dist/commands/doctor.d.mts +6 -0
- package/dist/commands/doctor.mjs +680 -0
- package/dist/commands/init.d.mts +6 -0
- package/dist/commands/init.mjs +167 -0
- package/dist/detect-DDE1hlQ8.mjs +73 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +41 -0
- package/dist/lib/templates/command/command.ts.liquid +12 -0
- package/dist/lib/templates/middleware/middleware.ts.liquid +9 -0
- package/dist/lib/templates/project/gitignore.liquid +3 -0
- package/dist/lib/templates/project/package.json.liquid +29 -0
- package/dist/lib/templates/project/src/commands/hello.ts.liquid +12 -0
- package/dist/lib/templates/project/src/index.ts.liquid +8 -0
- package/dist/lib/templates/project/tsconfig.json.liquid +19 -0
- package/dist/lib/templates/project/tsdown.config.ts.liquid +9 -0
- package/dist/lib/templates/project/vitest.config.ts.liquid +8 -0
- package/dist/write-DDGnajpV.mjs +166 -0
- package/package.json +43 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { n as renderTemplate, t as writeFiles } from "../write-DDGnajpV.mjs";
|
|
2
|
+
import { command } from "@kidd-cli/core";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/init.ts
|
|
7
|
+
const KEBAB_CASE_CHARS_RE = /^[a-z][\da-z-]*$/;
|
|
8
|
+
const initCommand = command({
|
|
9
|
+
args: z.object({
|
|
10
|
+
description: z.string().describe("Project description").optional(),
|
|
11
|
+
example: z.boolean().describe("Include example command").optional(),
|
|
12
|
+
name: z.string().describe("Project name (kebab-case)").optional(),
|
|
13
|
+
pm: z.enum([
|
|
14
|
+
"pnpm",
|
|
15
|
+
"yarn",
|
|
16
|
+
"npm"
|
|
17
|
+
]).describe("Package manager").optional()
|
|
18
|
+
}),
|
|
19
|
+
description: "Scaffold a new kidd CLI project",
|
|
20
|
+
handler: async (ctx) => {
|
|
21
|
+
const projectName = await resolveProjectName(ctx);
|
|
22
|
+
const projectDescription = await resolveDescription(ctx);
|
|
23
|
+
const packageManager = await resolvePackageManager(ctx);
|
|
24
|
+
const includeExample = await resolveIncludeExample(ctx);
|
|
25
|
+
ctx.spinner.start("Scaffolding project...");
|
|
26
|
+
const [renderError, rendered] = await renderTemplate({
|
|
27
|
+
templateDir: join(import.meta.dirname, "..", "lib", "templates", "project"),
|
|
28
|
+
variables: {
|
|
29
|
+
description: projectDescription,
|
|
30
|
+
name: projectName,
|
|
31
|
+
packageManager
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (renderError) {
|
|
35
|
+
ctx.spinner.stop("Failed");
|
|
36
|
+
return ctx.fail(renderError.message);
|
|
37
|
+
}
|
|
38
|
+
const [writeError] = await writeFiles({
|
|
39
|
+
files: selectFiles(includeExample, rendered),
|
|
40
|
+
outputDir: join(process.cwd(), projectName),
|
|
41
|
+
overwrite: false
|
|
42
|
+
});
|
|
43
|
+
if (writeError) {
|
|
44
|
+
ctx.spinner.stop("Failed");
|
|
45
|
+
return ctx.fail(writeError.message);
|
|
46
|
+
}
|
|
47
|
+
ctx.spinner.stop("Project created!");
|
|
48
|
+
ctx.output.raw("");
|
|
49
|
+
ctx.output.raw(`Next steps:`);
|
|
50
|
+
ctx.output.raw(` cd ${projectName}`);
|
|
51
|
+
ctx.output.raw(` ${packageManager} install`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Check whether a string is valid kebab-case.
|
|
56
|
+
*
|
|
57
|
+
* @param value - The string to validate.
|
|
58
|
+
* @returns True when the string is kebab-case.
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
function isKebabCase(value) {
|
|
62
|
+
if (!KEBAB_CASE_CHARS_RE.test(value)) return false;
|
|
63
|
+
if (value.endsWith("-")) return false;
|
|
64
|
+
if (value.includes("--")) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the project name from args or prompt.
|
|
69
|
+
*
|
|
70
|
+
* @param ctx - Command context.
|
|
71
|
+
* @returns The validated project name.
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
async function resolveProjectName(ctx) {
|
|
75
|
+
if (ctx.args.name) {
|
|
76
|
+
if (!isKebabCase(ctx.args.name)) return ctx.fail("Project name must be kebab-case (e.g. my-cli)");
|
|
77
|
+
return ctx.args.name;
|
|
78
|
+
}
|
|
79
|
+
return ctx.prompts.text({
|
|
80
|
+
message: "Project name",
|
|
81
|
+
placeholder: "my-cli",
|
|
82
|
+
validate: (value) => {
|
|
83
|
+
if (value === void 0 || !isKebabCase(value)) return "Must be kebab-case (e.g. my-cli)";
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the project description from args or prompt.
|
|
89
|
+
*
|
|
90
|
+
* @param ctx - Command context.
|
|
91
|
+
* @returns The project description string.
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
async function resolveDescription(ctx) {
|
|
95
|
+
if (ctx.args.description) return ctx.args.description;
|
|
96
|
+
return ctx.prompts.text({
|
|
97
|
+
defaultValue: "A CLI built with kidd",
|
|
98
|
+
message: "Description",
|
|
99
|
+
placeholder: "A CLI built with kidd"
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the package manager from args or prompt.
|
|
104
|
+
*
|
|
105
|
+
* @param ctx - Command context.
|
|
106
|
+
* @returns The selected package manager.
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
async function resolvePackageManager(ctx) {
|
|
110
|
+
if (ctx.args.pm) return ctx.args.pm;
|
|
111
|
+
return ctx.prompts.select({
|
|
112
|
+
message: "Package manager",
|
|
113
|
+
options: [
|
|
114
|
+
{
|
|
115
|
+
label: "pnpm",
|
|
116
|
+
value: "pnpm"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
label: "yarn",
|
|
120
|
+
value: "yarn"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: "npm",
|
|
124
|
+
value: "npm"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve whether to include the example command from args or prompt.
|
|
131
|
+
*
|
|
132
|
+
* @param ctx - Command context.
|
|
133
|
+
* @returns True when the example hello command should be included.
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
async function resolveIncludeExample(ctx) {
|
|
137
|
+
if (ctx.args.example !== void 0 && ctx.args.example !== null) return ctx.args.example;
|
|
138
|
+
return ctx.prompts.confirm({
|
|
139
|
+
initialValue: true,
|
|
140
|
+
message: "Include example command?"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Select the rendered files to write, optionally excluding the example command.
|
|
145
|
+
*
|
|
146
|
+
* @param includeExample - Whether to include the example hello command.
|
|
147
|
+
* @param rendered - The full set of rendered files.
|
|
148
|
+
* @returns The filtered file list.
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
function selectFiles(includeExample, rendered) {
|
|
152
|
+
if (includeExample) return rendered;
|
|
153
|
+
return rendered.filter(excludeHelloCommand);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Filter predicate that excludes the hello.ts example command.
|
|
157
|
+
*
|
|
158
|
+
* @param file - A rendered file to check.
|
|
159
|
+
* @returns True when the file is not the hello command.
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
function excludeHelloCommand(file) {
|
|
163
|
+
return !file.relativePath.includes("commands/hello.ts");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { initCommand as default };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import { attemptAsync, ok, toErrorMessage } from "@kidd-cli/utils/fp";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/detect.ts
|
|
6
|
+
/**
|
|
7
|
+
* Detect whether the given directory contains a kidd-based CLI project.
|
|
8
|
+
*
|
|
9
|
+
* Looks for a `package.json` with `kidd` listed in `dependencies` or
|
|
10
|
+
* `devDependencies`, and checks for a `src/commands/` directory.
|
|
11
|
+
*
|
|
12
|
+
* @param cwd - The directory to inspect.
|
|
13
|
+
* @returns An async Result containing project info or null when no kidd project is found.
|
|
14
|
+
*/
|
|
15
|
+
async function detectProject(cwd) {
|
|
16
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
17
|
+
if (!await fileExists(packageJsonPath)) return ok(null);
|
|
18
|
+
const [readError, pkg] = await readPackageJson(packageJsonPath);
|
|
19
|
+
if (readError) return [readError, null];
|
|
20
|
+
const deps = pkg.dependencies ?? {};
|
|
21
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
22
|
+
const hasKiddDep = "@kidd-cli/core" in deps || "@kidd-cli/core" in devDeps;
|
|
23
|
+
if (!hasKiddDep) return ok(null);
|
|
24
|
+
const commandsPath = join(cwd, "src", "commands");
|
|
25
|
+
if (await fileExists(commandsPath)) return ok({
|
|
26
|
+
commandsDir: commandsPath,
|
|
27
|
+
hasKiddDep,
|
|
28
|
+
rootDir: cwd
|
|
29
|
+
});
|
|
30
|
+
return ok({
|
|
31
|
+
commandsDir: null,
|
|
32
|
+
hasKiddDep,
|
|
33
|
+
rootDir: cwd
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read and parse a package.json file.
|
|
38
|
+
*
|
|
39
|
+
* @param filePath - Absolute path to the package.json.
|
|
40
|
+
* @returns A Result tuple with the parsed package data or a GenerateError.
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
async function readPackageJson(filePath) {
|
|
44
|
+
const [readError, content] = await attemptAsync(() => readFile(filePath, "utf8"));
|
|
45
|
+
if (readError || content === null || content === void 0) return [{
|
|
46
|
+
message: `Failed to read package.json: ${toErrorMessage(readError)}`,
|
|
47
|
+
path: filePath,
|
|
48
|
+
type: "read_error"
|
|
49
|
+
}, null];
|
|
50
|
+
try {
|
|
51
|
+
return ok(JSON.parse(content));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return [{
|
|
54
|
+
message: `Failed to parse package.json: ${toErrorMessage(error)}`,
|
|
55
|
+
path: filePath,
|
|
56
|
+
type: "read_error"
|
|
57
|
+
}, null];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check whether a path exists on disk.
|
|
62
|
+
*
|
|
63
|
+
* @param filePath - The path to check.
|
|
64
|
+
* @returns True when the path is accessible, false otherwise.
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
async function fileExists(filePath) {
|
|
68
|
+
const [err] = await attemptAsync(() => access(filePath));
|
|
69
|
+
return err === null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { detectProject as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cli } from "@kidd-cli/core";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readManifest } from "@kidd-cli/utils/manifest";
|
|
4
|
+
|
|
5
|
+
//#region src/manifest.ts
|
|
6
|
+
/**
|
|
7
|
+
* Read and validate the CLI package manifest.
|
|
8
|
+
*
|
|
9
|
+
* Reads package.json one directory above `baseDir` (the dist output sits
|
|
10
|
+
* one level below the package root) and ensures all required fields are
|
|
11
|
+
* present. Throws immediately if the manifest cannot be read or any
|
|
12
|
+
* required field is missing — this is an unrecoverable entry-point guard.
|
|
13
|
+
*
|
|
14
|
+
* @param baseDir - The directory the CLI entry file lives in (typically `import.meta.dirname`).
|
|
15
|
+
* @returns A validated {@link CLIManifest} with all required fields.
|
|
16
|
+
*/
|
|
17
|
+
async function loadCLIManifest(baseDir) {
|
|
18
|
+
const [manifestError, manifest] = await readManifest(join(baseDir, ".."));
|
|
19
|
+
if (manifestError) throw new Error(`Failed to read CLI manifest: ${manifestError.message}`);
|
|
20
|
+
if (!manifest.name) throw new Error("CLI manifest is missing required field: name");
|
|
21
|
+
if (!manifest.version) throw new Error("CLI manifest is missing required field: version");
|
|
22
|
+
if (!manifest.description) throw new Error("CLI manifest is missing required field: description");
|
|
23
|
+
return {
|
|
24
|
+
description: manifest.description,
|
|
25
|
+
name: manifest.name,
|
|
26
|
+
version: manifest.version
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/index.ts
|
|
32
|
+
const manifest = await loadCLIManifest(import.meta.dirname);
|
|
33
|
+
await cli({
|
|
34
|
+
commands: `${import.meta.dirname}/commands`,
|
|
35
|
+
description: manifest.description,
|
|
36
|
+
name: manifest.name,
|
|
37
|
+
version: manifest.version
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { command } from '@kidd-cli/core'
|
|
2
|
+
{% if includeArgs %}import { z } from 'zod'
|
|
3
|
+
{% endif %}
|
|
4
|
+
export default command({
|
|
5
|
+
description: '{{ description }}',
|
|
6
|
+
{% if includeArgs %} args: z.object({
|
|
7
|
+
// Add your args here
|
|
8
|
+
}),
|
|
9
|
+
{% endif %} handler: async (ctx) => {
|
|
10
|
+
// Implement your command here
|
|
11
|
+
},
|
|
12
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ name }}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "{{ description }}",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"{{ name }}": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsdown",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@kidd-cli/core": "^0.0.0",
|
|
22
|
+
"zod": "^3.24.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsdown": "^0.21.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^4.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { command } from '@kidd-cli/core'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export default command({
|
|
5
|
+
description: 'Say hello',
|
|
6
|
+
args: z.object({
|
|
7
|
+
name: z.string().describe('Name to greet').default('world'),
|
|
8
|
+
}),
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
ctx.output.raw(`Hello, ${ctx.args.name}!`)
|
|
11
|
+
},
|
|
12
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"isolatedDeclarations": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"outDir": "dist"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { dirname, join, relative } from "node:path";
|
|
2
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { attemptAsync, ok, toErrorMessage } from "@kidd-cli/utils/fp";
|
|
4
|
+
import { Liquid } from "liquidjs";
|
|
5
|
+
|
|
6
|
+
//#region src/lib/render.ts
|
|
7
|
+
/**
|
|
8
|
+
* Render all `.liquid` templates in a directory using LiquidJS.
|
|
9
|
+
*
|
|
10
|
+
* Recursively collects `.liquid` files under `templateDir`, renders each
|
|
11
|
+
* with the provided variables, and strips the `.liquid` extension from
|
|
12
|
+
* the output path. Files named `gitignore.liquid` are mapped to `.gitignore`.
|
|
13
|
+
*
|
|
14
|
+
* @param params - Template directory and variable bindings.
|
|
15
|
+
* @returns An async Result containing rendered files or a GenerateError.
|
|
16
|
+
*/
|
|
17
|
+
async function renderTemplate(params) {
|
|
18
|
+
const engine = new Liquid({ root: params.templateDir });
|
|
19
|
+
const entries = await collectLiquidFiles(params.templateDir);
|
|
20
|
+
if (entries.length === 0) return ok([]);
|
|
21
|
+
const results = await Promise.all(entries.map(async (entry) => {
|
|
22
|
+
const [renderError, content] = await renderSingleFile(engine, join(params.templateDir, entry), params.variables);
|
|
23
|
+
if (renderError) return renderError;
|
|
24
|
+
return {
|
|
25
|
+
content,
|
|
26
|
+
relativePath: mapOutputPath(entry)
|
|
27
|
+
};
|
|
28
|
+
}));
|
|
29
|
+
const firstError = results.find(isGenerateError);
|
|
30
|
+
if (firstError) return [firstError, null];
|
|
31
|
+
return ok(results);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Recursively collect all `.liquid` file paths relative to root.
|
|
35
|
+
*
|
|
36
|
+
* @param root - The directory to scan.
|
|
37
|
+
* @returns Relative paths of all `.liquid` files.
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
async function collectLiquidFiles(root) {
|
|
41
|
+
return (await readdir(root, {
|
|
42
|
+
recursive: true,
|
|
43
|
+
withFileTypes: true
|
|
44
|
+
})).filter((entry) => entry.isFile() && entry.name.endsWith(".liquid")).map((entry) => {
|
|
45
|
+
const parent = entry.parentPath;
|
|
46
|
+
return relative(root, join(parent, entry.name));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Render a single `.liquid` file with the given variables.
|
|
51
|
+
*
|
|
52
|
+
* @param engine - The LiquidJS engine instance.
|
|
53
|
+
* @param absolutePath - Absolute path to the `.liquid` file.
|
|
54
|
+
* @param variables - Template variable bindings.
|
|
55
|
+
* @returns A Result tuple with the rendered content or a GenerateError.
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
async function renderSingleFile(engine, absolutePath, variables) {
|
|
59
|
+
try {
|
|
60
|
+
const template = await readFile(absolutePath, "utf8");
|
|
61
|
+
return ok(await engine.parseAndRender(template, variables));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return [{
|
|
64
|
+
message: `Failed to render template: ${toErrorMessage(error)}`,
|
|
65
|
+
path: absolutePath,
|
|
66
|
+
type: "render_error"
|
|
67
|
+
}, null];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Map a `.liquid` relative path to its output path.
|
|
72
|
+
*
|
|
73
|
+
* Strips the `.liquid` extension and renames bare `gitignore` segments
|
|
74
|
+
* to `.gitignore` so dotfiles survive version control.
|
|
75
|
+
*
|
|
76
|
+
* @param liquidPath - Relative path ending in `.liquid`.
|
|
77
|
+
* @returns The output-relative path without the `.liquid` suffix.
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
function mapOutputPath(liquidPath) {
|
|
81
|
+
return liquidPath.replace(/\.liquid$/, "").replaceAll(/(^|\/)gitignore($|\/)/g, "$1.gitignore$2");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Type guard for GenerateError objects.
|
|
85
|
+
*
|
|
86
|
+
* @param value - The value to check.
|
|
87
|
+
* @returns True when value has a `type` and `message` property matching GenerateError.
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
function isGenerateError(value) {
|
|
91
|
+
if (typeof value !== "object" || value === null) return false;
|
|
92
|
+
return "type" in value && "message" in value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/lib/write.ts
|
|
97
|
+
/**
|
|
98
|
+
* Write rendered files to disk with optional conflict detection.
|
|
99
|
+
*
|
|
100
|
+
* For each file, resolves the target path under `outputDir`, creates parent
|
|
101
|
+
* directories as needed, and writes the content. When `overwrite` is false,
|
|
102
|
+
* existing files are skipped rather than overwritten.
|
|
103
|
+
*
|
|
104
|
+
* @param params - Files to write, target directory, and overwrite flag.
|
|
105
|
+
* @returns An async Result with counts of written/skipped files or a GenerateError.
|
|
106
|
+
*/
|
|
107
|
+
async function writeFiles(params) {
|
|
108
|
+
const written = [];
|
|
109
|
+
const skipped = [];
|
|
110
|
+
const results = await Promise.all(params.files.map((file) => writeSingleFile(file, params.outputDir, params.overwrite)));
|
|
111
|
+
const firstError = results.find((r) => r[0] !== null);
|
|
112
|
+
if (firstError) return [firstError[0], null];
|
|
113
|
+
const validStatuses = results.filter((r) => r[1] !== null);
|
|
114
|
+
const writtenPaths = validStatuses.filter(([, status]) => status.action === "written").map(([, status]) => status.path);
|
|
115
|
+
const skippedPaths = validStatuses.filter(([, status]) => status.action === "skipped").map(([, status]) => status.path);
|
|
116
|
+
writtenPaths.map((p) => written.push(p));
|
|
117
|
+
skippedPaths.map((p) => skipped.push(p));
|
|
118
|
+
return ok({
|
|
119
|
+
skipped,
|
|
120
|
+
written
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Write a single rendered file to disk.
|
|
125
|
+
*
|
|
126
|
+
* @param file - The rendered file to write.
|
|
127
|
+
* @param outputDir - The root output directory.
|
|
128
|
+
* @param overwrite - Whether to overwrite existing files.
|
|
129
|
+
* @returns A Result tuple with the write status or a GenerateError.
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
async function writeSingleFile(file, outputDir, overwrite) {
|
|
133
|
+
const targetPath = join(outputDir, file.relativePath);
|
|
134
|
+
try {
|
|
135
|
+
if (await fileExists(targetPath) && !overwrite) return ok({
|
|
136
|
+
action: "skipped",
|
|
137
|
+
path: file.relativePath
|
|
138
|
+
});
|
|
139
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
140
|
+
await writeFile(targetPath, file.content, "utf8");
|
|
141
|
+
return ok({
|
|
142
|
+
action: "written",
|
|
143
|
+
path: file.relativePath
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return [{
|
|
147
|
+
message: `Failed to write file: ${toErrorMessage(error)}`,
|
|
148
|
+
path: targetPath,
|
|
149
|
+
type: "write_error"
|
|
150
|
+
}, null];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check whether a path exists on disk.
|
|
155
|
+
*
|
|
156
|
+
* @param filePath - The path to check.
|
|
157
|
+
* @returns True when the path is accessible, false otherwise.
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
async function fileExists(filePath) {
|
|
161
|
+
const [err] = await attemptAsync(() => access(filePath));
|
|
162
|
+
return err === null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
export { renderTemplate as n, writeFiles as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kidd-cli/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DX companion CLI for the kidd framework",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"codegen",
|
|
8
|
+
"kidd",
|
|
9
|
+
"scaffolding"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"bin": {
|
|
13
|
+
"kidd": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"fs-extra": "^11.3.3",
|
|
21
|
+
"liquidjs": "^10.24.0",
|
|
22
|
+
"picocolors": "^1.1.1",
|
|
23
|
+
"zod": "^4.3.6",
|
|
24
|
+
"@kidd-cli/bundler": "0.1.0",
|
|
25
|
+
"@kidd-cli/core": "0.1.0",
|
|
26
|
+
"@kidd-cli/utils": "0.1.0",
|
|
27
|
+
"@kidd-cli/config": "0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/fs-extra": "^11.0.4",
|
|
31
|
+
"tsdown": "0.21.0-beta.2",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^4.0.18"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown && mkdir -p dist/lib && cp -r src/lib/templates dist/lib/templates",
|
|
37
|
+
"typecheck": "tsgo --noEmit",
|
|
38
|
+
"lint": "oxlint --ignore-pattern node_modules",
|
|
39
|
+
"lint:fix": "oxlint --fix --ignore-pattern node_modules",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
42
|
+
}
|
|
43
|
+
}
|