@kyuujs/cli 0.1.0-alpha.0 → 0.1.0-alpha.1

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.
@@ -1,140 +0,0 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import type { ProjectCreationOptions } from "./options.js";
4
- import { getVersion } from "../version.js";
5
-
6
- /**
7
- * Scaffolds project files and directory structure based on ProjectCreationOptions.
8
- */
9
- export async function scaffoldProject(options: ProjectCreationOptions): Promise<void> {
10
- const version = getVersion();
11
- const kyuuDepVersion = `^${version}`;
12
-
13
- mkdirSync(options.directory, { recursive: true });
14
-
15
- if (options.language === "typescript") {
16
- // 1. kyuu.config.ts
17
- writeFile(
18
- join(options.directory, "kyuu.config.ts"),
19
- `import { defineConfig } from "@kyuujs/core";
20
-
21
- export default defineConfig({
22
- server: {
23
- port: 3000,
24
- host: "127.0.0.1",
25
- },
26
- });
27
- `,
28
- );
29
-
30
- // 2. package.json
31
- const pkgContent = {
32
- name: options.name,
33
- version: "0.1.0",
34
- private: true,
35
- type: "module",
36
- scripts: {
37
- dev: "kyuu dev",
38
- build: "kyuu build",
39
- start: "kyuu start",
40
- },
41
- dependencies: {
42
- "@kyuujs/core": kyuuDepVersion,
43
- },
44
- devDependencies: {
45
- "@kyuujs/cli": kyuuDepVersion,
46
- typescript: "^5.0.0",
47
- },
48
- };
49
- writeFile(join(options.directory, "package.json"), JSON.stringify(pkgContent, null, 2) + "\n");
50
-
51
- // 3. tsconfig.json
52
- const tsconfigContent = {
53
- compilerOptions: {
54
- target: "ES2022",
55
- module: "NodeNext",
56
- moduleResolution: "NodeNext",
57
- esModuleInterop: true,
58
- strict: true,
59
- skipLibCheck: true,
60
- outDir: "dist",
61
- },
62
- include: ["src/**/*", "kyuu.config.ts"],
63
- };
64
- writeFile(
65
- join(options.directory, "tsconfig.json"),
66
- JSON.stringify(tsconfigContent, null, 2) + "\n",
67
- );
68
-
69
- // 4. src/app/route.ts
70
- writeFile(
71
- join(options.directory, "src", "app", "route.ts"),
72
- `import type { Request, Response } from "@kyuujs/core";
73
-
74
- export const GET = async (_req: Request, res: Response) => {
75
- return res.json({ message: "Hello from Kyuu!" });
76
- };
77
- `,
78
- );
79
- } else {
80
- // JavaScript project scaffolding
81
- // 1. kyuu.config.js
82
- writeFile(
83
- join(options.directory, "kyuu.config.js"),
84
- `import { defineConfig } from "@kyuujs/core";
85
-
86
- export default defineConfig({
87
- server: {
88
- port: 3000,
89
- host: "127.0.0.1",
90
- },
91
- });
92
- `,
93
- );
94
-
95
- // 2. package.json
96
- const pkgContent = {
97
- name: options.name,
98
- version: "0.1.0",
99
- private: true,
100
- type: "module",
101
- scripts: {
102
- dev: "kyuu dev",
103
- start: "kyuu start",
104
- },
105
- dependencies: {
106
- "@kyuujs/core": kyuuDepVersion,
107
- },
108
- devDependencies: {
109
- "@kyuujs/cli": kyuuDepVersion,
110
- },
111
- };
112
- writeFile(join(options.directory, "package.json"), JSON.stringify(pkgContent, null, 2) + "\n");
113
-
114
- // 3. src/app/route.js
115
- writeFile(
116
- join(options.directory, "src", "app", "route.js"),
117
- `export const GET = async (_req, res) => {
118
- return res.json({ message: "Hello from Kyuu!" });
119
- };
120
- `,
121
- );
122
- }
123
-
124
- // Common files
125
- // .gitignore
126
- writeFile(
127
- join(options.directory, ".gitignore"),
128
- `node_modules/
129
- dist/
130
- .kyuu/
131
- .env
132
- *.log
133
- `,
134
- );
135
- }
136
-
137
- function writeFile(path: string, content: string): void {
138
- mkdirSync(dirname(path), { recursive: true });
139
- writeFileSync(path, content, "utf8");
140
- }
@@ -1,67 +0,0 @@
1
- import { existsSync, readdirSync } from "node:fs";
2
- import { isAbsolute, resolve } from "node:path";
3
-
4
- export interface ValidationResult {
5
- valid: boolean;
6
- error?: string;
7
- targetDirectory?: string;
8
- }
9
-
10
- /**
11
- * Validates project name and resolves the target directory.
12
- */
13
- export function validateProjectName(name: string, cwd: string = process.cwd()): ValidationResult {
14
- if (!name || name.trim().length === 0) {
15
- return { valid: false, error: "Project name cannot be empty." };
16
- }
17
-
18
- const trimmed = name.trim();
19
-
20
- // Prevent path traversal and relative navigation in project name
21
- if (
22
- trimmed.includes("/") ||
23
- trimmed.includes("\\") ||
24
- trimmed.includes("..") ||
25
- isAbsolute(trimmed)
26
- ) {
27
- return {
28
- valid: false,
29
- error: `Invalid project name "${trimmed}". Path traversal and absolute paths are not allowed.`,
30
- };
31
- }
32
-
33
- // Prevent invalid npm package / directory name characters
34
- const validNameRegex = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i;
35
- if (!validNameRegex.test(trimmed)) {
36
- return {
37
- valid: false,
38
- error: `Invalid project name "${trimmed}". Must be a valid package/directory name.`,
39
- };
40
- }
41
-
42
- const targetDirectory = resolve(cwd, trimmed);
43
-
44
- if (existsSync(targetDirectory)) {
45
- try {
46
- const files = readdirSync(targetDirectory);
47
- if (files.length > 0) {
48
- return {
49
- valid: false,
50
- error: `Target directory already exists and is non-empty: ${targetDirectory}`,
51
- targetDirectory,
52
- };
53
- }
54
- } catch {
55
- return {
56
- valid: false,
57
- error: `Target directory cannot be read: ${targetDirectory}`,
58
- targetDirectory,
59
- };
60
- }
61
- }
62
-
63
- return {
64
- valid: true,
65
- targetDirectory,
66
- };
67
- }
@@ -1,89 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { stdin as input, stdout as output } from "node:process";
3
- import type { PackageManager, ProjectCreationOptions, ProjectLanguage } from "./options.js";
4
-
5
- export interface WizardOptions {
6
- name: string;
7
- directory: string;
8
- defaults?: Partial<ProjectCreationOptions>;
9
- interactive?: boolean;
10
- input?: NodeJS.ReadableStream;
11
- output?: NodeJS.WritableStream;
12
- }
13
-
14
- export class CancelledError extends Error {
15
- constructor() {
16
- super("Project creation cancelled by user.");
17
- this.name = "CancelledError";
18
- }
19
- }
20
-
21
- /**
22
- * Runs the interactive project creation wizard to resolve ProjectCreationOptions.
23
- */
24
- export async function runWizard(options: WizardOptions): Promise<ProjectCreationOptions> {
25
- const isInteractive = options.interactive ?? Boolean(process.stdout.isTTY && !process.env.CI);
26
-
27
- // If non-interactive, return options immediately using defaults
28
- if (!isInteractive) {
29
- return {
30
- name: options.name,
31
- directory: options.directory,
32
- language: options.defaults?.language ?? "typescript",
33
- packageManager: options.defaults?.packageManager ?? "pnpm",
34
- initializeGit: options.defaults?.initializeGit ?? true,
35
- skipInstall: options.defaults?.skipInstall ?? false,
36
- };
37
- }
38
-
39
- const rl = createInterface({
40
- input: options.input ?? input,
41
- output: options.output ?? output,
42
- });
43
-
44
- let cancelled = false;
45
- rl.on("SIGINT", () => {
46
- cancelled = true;
47
- rl.close();
48
- });
49
-
50
- try {
51
- // Question 1: Language
52
- const langAnswer = await rl.question(
53
- "\nWhich language would you like to use?\n 1) TypeScript (default)\n 2) JavaScript\nSelect [1-2]: ",
54
- );
55
- if (cancelled) throw new CancelledError();
56
- const language: ProjectLanguage = langAnswer.trim() === "2" ? "javascript" : "typescript";
57
-
58
- // Question 2: Package Manager
59
- const pmAnswer = await rl.question(
60
- "\nWhich package manager would you like to use?\n 1) pnpm (default)\n 2) npm\n 3) yarn\nSelect [1-3]: ",
61
- );
62
- if (cancelled) throw new CancelledError();
63
- let packageManager: PackageManager = "pnpm";
64
- if (pmAnswer.trim() === "2") packageManager = "npm";
65
- if (pmAnswer.trim() === "3") packageManager = "yarn";
66
-
67
- // Question 3: Git
68
- const gitAnswer = await rl.question("\nInitialize a Git repository? [Y/n]: ");
69
- if (cancelled) throw new CancelledError();
70
- const initializeGit = !gitAnswer.trim().toLowerCase().startsWith("n");
71
-
72
- rl.close();
73
-
74
- return {
75
- name: options.name,
76
- directory: options.directory,
77
- language,
78
- packageManager,
79
- initializeGit,
80
- skipInstall: options.defaults?.skipInstall ?? false,
81
- };
82
- } catch (err) {
83
- rl.close();
84
- if (cancelled || (err instanceof Error && err.name === "AbortError")) {
85
- throw new CancelledError();
86
- }
87
- throw err;
88
- }
89
- }
@@ -1,61 +0,0 @@
1
- import { loadApplicationContext } from "@kyuujs/core";
2
-
3
- async function run(): Promise<void> {
4
- const projectRoot =
5
- process.env.KYUU_PROJECT_ROOT ?? process.env.KYUU_PROJECT_ROOT ?? process.cwd();
6
-
7
- try {
8
- const context = await loadApplicationContext({ projectRoot });
9
- const server = context.app.listen();
10
-
11
- if (!server.listening) {
12
- await new Promise<void>((res, rej) => {
13
- server.once("listening", res);
14
- server.once("error", rej);
15
- });
16
- }
17
-
18
- const address = server.address();
19
- let port = context.config.server.port;
20
- let host = context.config.server.host;
21
- if (address && typeof address === "object") {
22
- port = address.port;
23
- host = address.address;
24
- }
25
-
26
- const url = `http://${host === "0.0.0.0" || host === "127.0.0.1" ? "localhost" : host}:${port}`;
27
-
28
- if (process.send) {
29
- process.send({ type: "started", port, host, url });
30
- }
31
-
32
- const cleanup = async () => {
33
- try {
34
- await context.app.close();
35
- } catch {
36
- // Ignore shutdown errors
37
- }
38
- process.exit(0);
39
- };
40
-
41
- process.on("SIGINT", cleanup);
42
- process.on("SIGTERM", cleanup);
43
- process.on("message", (msg) => {
44
- if (msg === "shutdown") {
45
- cleanup();
46
- }
47
- });
48
- } catch (err: unknown) {
49
- const errorMsg = err instanceof Error ? err.message : String(err);
50
- if (process.send) {
51
- process.send({
52
- type: "error",
53
- error: errorMsg,
54
- stack: err instanceof Error ? err.stack : undefined,
55
- });
56
- }
57
- process.exit(1);
58
- }
59
- }
60
-
61
- run();
@@ -1,256 +0,0 @@
1
- import { fork, type ChildProcess } from "node:child_process";
2
- import { existsSync, watch as fsWatch, type FSWatcher } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- export interface DevServerOptions {
7
- projectRoot: string;
8
- watch?: boolean;
9
- stdout?: (msg: string) => void;
10
- stderr?: (msg: string) => void;
11
- }
12
-
13
- export interface DevServerController {
14
- url?: string;
15
- port?: number;
16
- host?: string;
17
- stop: () => Promise<void>;
18
- restart: () => Promise<void>;
19
- }
20
-
21
- function getRunnerPathAndArgs(): { execPath: string; execArgs: string[] } {
22
- const currentFile = fileURLToPath(import.meta.url);
23
- const currentDir = dirname(currentFile);
24
-
25
- const jsInSameDir = resolve(currentDir, "app-runner.js");
26
- if (existsSync(jsInSameDir)) {
27
- return { execPath: jsInSameDir, execArgs: ["--experimental-strip-types"] };
28
- }
29
-
30
- const jsInDist = resolve(currentDir, "..", "..", "dist", "runner", "app-runner.js");
31
- if (existsSync(jsInDist)) {
32
- return { execPath: jsInDist, execArgs: ["--experimental-strip-types"] };
33
- }
34
-
35
- const tsInSameDir = resolve(currentDir, "app-runner.ts");
36
- if (existsSync(tsInSameDir)) {
37
- return { execPath: tsInSameDir, execArgs: ["--experimental-strip-types"] };
38
- }
39
-
40
- return { execPath: jsInSameDir, execArgs: ["--experimental-strip-types"] };
41
- }
42
-
43
- export async function startDevServer(options: DevServerOptions): Promise<DevServerController> {
44
- const writeOut = options.stdout ?? ((msg: string) => process.stdout.write(msg + "\n"));
45
- const writeErr = options.stderr ?? ((msg: string) => process.stderr.write(msg + "\n"));
46
- const shouldWatch = options.watch ?? true;
47
- const projectRoot = resolve(options.projectRoot);
48
-
49
- let currentChild: ChildProcess | null = null;
50
- let watchers: FSWatcher[] = [];
51
- let debounceTimer: NodeJS.Timeout | null = null;
52
-
53
- let currentUrl: string | undefined;
54
- let currentPort: number | undefined;
55
- let currentHost: string | undefined;
56
-
57
- const { execPath, execArgs } = getRunnerPathAndArgs();
58
-
59
- const stopChild = async (): Promise<void> => {
60
- if (!currentChild) return;
61
- const child = currentChild;
62
- currentChild = null;
63
-
64
- if (child.exitCode !== null || child.killed) {
65
- return;
66
- }
67
-
68
- await new Promise<void>((res) => {
69
- const timer = setTimeout(() => {
70
- try {
71
- child.kill("SIGKILL");
72
- } catch {
73
- // Ignore kill error
74
- }
75
- res();
76
- }, 2000);
77
-
78
- child.once("exit", () => {
79
- clearTimeout(timer);
80
- res();
81
- });
82
-
83
- try {
84
- if (child.connected) {
85
- child.send("shutdown");
86
- } else {
87
- child.kill("SIGINT");
88
- }
89
- } catch {
90
- child.kill("SIGINT");
91
- }
92
- });
93
- };
94
-
95
- const spawnChild = async (): Promise<boolean> => {
96
- await stopChild();
97
-
98
- return new Promise<boolean>((res) => {
99
- const child = fork(execPath, [], {
100
- execArgv: execArgs,
101
- env: {
102
- ...process.env,
103
- KYUU_PROJECT_ROOT: projectRoot,
104
- NODE_ENV: "development",
105
- },
106
- stdio: ["inherit", "pipe", "pipe", "ipc"],
107
- });
108
-
109
- currentChild = child;
110
- let handled = false;
111
-
112
- child.stdout?.on("data", (chunk: Buffer) => {
113
- writeOut(chunk.toString("utf8").trimEnd());
114
- });
115
-
116
- child.stderr?.on("data", (chunk: Buffer) => {
117
- writeErr(chunk.toString("utf8").trimEnd());
118
- });
119
-
120
- child.on("message", (msg: unknown) => {
121
- if (typeof msg === "object" && msg !== null && "type" in msg) {
122
- const payload = msg as {
123
- type: string;
124
- port?: number;
125
- host?: string;
126
- url?: string;
127
- error?: string;
128
- };
129
- if (payload.type === "started") {
130
- handled = true;
131
- currentPort = payload.port;
132
- currentHost = payload.host;
133
- currentUrl = payload.url;
134
-
135
- const banner = [
136
- "Kyuu",
137
- "",
138
- "✓ Configuration loaded",
139
- "✓ Routes loaded",
140
- "✓ Server started",
141
- "",
142
- `Local: ${payload.url}`,
143
- ].join("\n");
144
- writeOut(banner);
145
- res(true);
146
- } else if (payload.type === "error") {
147
- handled = true;
148
- writeErr(`Unable to start Kyuu server.\n\n${payload.error}`);
149
- res(false);
150
- }
151
- }
152
- });
153
-
154
- child.on("exit", (code) => {
155
- if (!handled) {
156
- handled = true;
157
- if (code !== 0) {
158
- writeErr(`Application process exited with code ${code}.`);
159
- }
160
- res(false);
161
- }
162
- });
163
- });
164
- };
165
-
166
- const restart = async (): Promise<void> => {
167
- writeOut("\nFile change detected. Restarting application...");
168
- const success = await spawnChild();
169
- if (!success) {
170
- writeErr("\n✗ Failed to restart Kyuu application.\nWaiting for changes...");
171
- }
172
- };
173
-
174
- // Initial spawn
175
- await spawnChild();
176
-
177
- // Watcher setup
178
- if (shouldWatch) {
179
- const handleFileChange = (_eventType: string, filename: string | null) => {
180
- if (filename) {
181
- const lower = filename.toLowerCase();
182
- if (
183
- lower.includes("node_modules") ||
184
- lower.includes(".git") ||
185
- lower.includes("dist") ||
186
- lower.includes("coverage")
187
- ) {
188
- return;
189
- }
190
- }
191
-
192
- if (debounceTimer) {
193
- clearTimeout(debounceTimer);
194
- }
195
-
196
- debounceTimer = setTimeout(() => {
197
- void restart();
198
- }, 150);
199
- };
200
-
201
- try {
202
- // Watch project root for kyuu.config changes
203
- const rootWatcher = fsWatch(projectRoot, { recursive: false }, handleFileChange);
204
- watchers.push(rootWatcher);
205
-
206
- // Watch src directory for app route changes
207
- const srcDir = join(projectRoot, "src");
208
- if (existsSync(srcDir)) {
209
- const srcWatcher = fsWatch(srcDir, { recursive: true }, handleFileChange);
210
- watchers.push(srcWatcher);
211
- }
212
- } catch {
213
- // Fallback if watching fails
214
- }
215
- }
216
-
217
- const cleanupAll = async (): Promise<void> => {
218
- if (debounceTimer) {
219
- clearTimeout(debounceTimer);
220
- }
221
- for (const w of watchers) {
222
- try {
223
- w.close();
224
- } catch {
225
- // Ignore close error
226
- }
227
- }
228
- watchers = [];
229
- await stopChild();
230
- };
231
-
232
- // Signal handlers
233
- const sigintHandler = () => {
234
- void cleanupAll().then(() => process.exit(0));
235
- };
236
- process.once("SIGINT", sigintHandler);
237
- process.once("SIGTERM", sigintHandler);
238
-
239
- return {
240
- get url() {
241
- return currentUrl;
242
- },
243
- get port() {
244
- return currentPort;
245
- },
246
- get host() {
247
- return currentHost;
248
- },
249
- stop: async () => {
250
- process.removeListener("SIGINT", sigintHandler);
251
- process.removeListener("SIGTERM", sigintHandler);
252
- await cleanupAll();
253
- },
254
- restart,
255
- };
256
- }
package/src/version.ts DELETED
@@ -1,15 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
-
3
- /**
4
- * Returns the authoritative version string from package.json.
5
- */
6
- export function getVersion(): string {
7
- try {
8
- const pkgUrl = new URL("../package.json", import.meta.url);
9
- const pkgContent = readFileSync(pkgUrl, "utf8");
10
- const pkg = JSON.parse(pkgContent) as { version?: string };
11
- return pkg.version ?? "0.0.0";
12
- } catch {
13
- return "0.0.0";
14
- }
15
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "composite": true,
5
- "rootDir": "src",
6
- "outDir": "dist"
7
- },
8
- "include": ["src"],
9
- "references": [{ "path": "../core" }]
10
- }