@kyuujs/cli 0.1.0-alpha.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/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +13 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/build.d.ts +16 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +40 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +53 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/new.d.ts +12 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +83 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/start.d.ts +22 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +64 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/dispatcher.d.ts +20 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +51 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/help.d.ts +5 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +24 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/project/git.d.ts +12 -0
- package/dist/project/git.d.ts.map +1 -0
- package/dist/project/git.js +23 -0
- package/dist/project/git.js.map +1 -0
- package/dist/project/options.d.ts +11 -0
- package/dist/project/options.d.ts.map +1 -0
- package/dist/project/options.js +2 -0
- package/dist/project/options.js.map +1 -0
- package/dist/project/package-manager.d.ts +13 -0
- package/dist/project/package-manager.d.ts.map +1 -0
- package/dist/project/package-manager.js +25 -0
- package/dist/project/package-manager.js.map +1 -0
- package/dist/project/scaffolder.d.ts +6 -0
- package/dist/project/scaffolder.d.ts.map +1 -0
- package/dist/project/scaffolder.js +113 -0
- package/dist/project/scaffolder.js.map +1 -0
- package/dist/project/validation.d.ts +10 -0
- package/dist/project/validation.d.ts.map +1 -0
- package/dist/project/validation.js +54 -0
- package/dist/project/validation.js.map +1 -0
- package/dist/project/wizard.d.ts +17 -0
- package/dist/project/wizard.d.ts.map +1 -0
- package/dist/project/wizard.js +72 -0
- package/dist/project/wizard.js.map +1 -0
- package/dist/runner/app-runner.d.ts +2 -0
- package/dist/runner/app-runner.d.ts.map +1 -0
- package/dist/runner/app-runner.js +54 -0
- package/dist/runner/app-runner.js.map +1 -0
- package/dist/runner/dev-runner.d.ts +15 -0
- package/dist/runner/dev-runner.d.ts.map +1 -0
- package/dist/runner/dev-runner.js +209 -0
- package/dist/runner/dev-runner.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/package.json +21 -0
- package/src/bin.ts +13 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/dev.ts +73 -0
- package/src/commands/new.ts +107 -0
- package/src/commands/start.ts +91 -0
- package/src/dispatcher.ts +79 -0
- package/src/help.ts +23 -0
- package/src/index.ts +90 -0
- package/src/project/git.ts +31 -0
- package/src/project/options.ts +11 -0
- package/src/project/package-manager.ts +37 -0
- package/src/project/scaffolder.ts +140 -0
- package/src/project/validation.ts +67 -0
- package/src/project/wizard.ts +89 -0
- package/src/runner/app-runner.ts +61 -0
- package/src/runner/dev-runner.ts +256 -0
- package/src/version.ts +15 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { handleBuildCommand, type BuildCommandOptions } from "./commands/build.js";
|
|
2
|
+
import { handleDevCommand, type DevCommandOptions } from "./commands/dev.js";
|
|
3
|
+
import { handleNewCommand, type NewCommandOptions } from "./commands/new.js";
|
|
4
|
+
import { handleStartCommand, type StartCommandOptions } from "./commands/start.js";
|
|
5
|
+
import { getHelpText } from "./help.js";
|
|
6
|
+
import { getVersion } from "./version.js";
|
|
7
|
+
|
|
8
|
+
export interface BaseCliOptions {
|
|
9
|
+
stdout?: (msg: string) => void;
|
|
10
|
+
stderr?: (msg: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RunCliOptions
|
|
14
|
+
extends
|
|
15
|
+
BaseCliOptions,
|
|
16
|
+
NewCommandOptions,
|
|
17
|
+
DevCommandOptions,
|
|
18
|
+
BuildCommandOptions,
|
|
19
|
+
StartCommandOptions {}
|
|
20
|
+
|
|
21
|
+
export interface CliResult {
|
|
22
|
+
exitCode: number;
|
|
23
|
+
output: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Main command dispatcher for Kyuu CLI.
|
|
28
|
+
* Inspects args for flags and commands, delegating to appropriate handlers.
|
|
29
|
+
*/
|
|
30
|
+
export async function runCli(args: string[] = [], options: RunCliOptions = {}): Promise<CliResult> {
|
|
31
|
+
const writeOut = options.stdout ?? ((msg: string) => process.stdout.write(msg + "\n"));
|
|
32
|
+
const writeErr = options.stderr ?? ((msg: string) => process.stderr.write(msg + "\n"));
|
|
33
|
+
|
|
34
|
+
const hasHelp = args.includes("--help") || args.includes("-h");
|
|
35
|
+
const hasVersion = args.includes("--version") || args.includes("-v");
|
|
36
|
+
|
|
37
|
+
// 1. Help flag or bare invocation
|
|
38
|
+
if (args.length === 0 || hasHelp) {
|
|
39
|
+
const text = getHelpText();
|
|
40
|
+
writeOut(text);
|
|
41
|
+
return { exitCode: 0, output: text };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Version flag
|
|
45
|
+
if (hasVersion) {
|
|
46
|
+
const version = getVersion();
|
|
47
|
+
writeOut(version);
|
|
48
|
+
return { exitCode: 0, output: version };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Command resolution
|
|
52
|
+
const firstArg = args[0];
|
|
53
|
+
|
|
54
|
+
if (firstArg === "dev") {
|
|
55
|
+
return handleDevCommand(args.slice(1), options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (firstArg === "build") {
|
|
59
|
+
return handleBuildCommand(args.slice(1), options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (firstArg === "start") {
|
|
63
|
+
return handleStartCommand(args.slice(1), options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (firstArg === "new") {
|
|
67
|
+
return handleNewCommand(args.slice(1), options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (firstArg.startsWith("-")) {
|
|
71
|
+
const errText = `Unknown option: ${firstArg}\n\nRun "kyuu --help" for available options.`;
|
|
72
|
+
writeErr(errText);
|
|
73
|
+
return { exitCode: 1, output: errText };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const errText = `Unknown command: ${firstArg}\n\nRun "kyuu --help" for available commands.`;
|
|
77
|
+
writeErr(errText);
|
|
78
|
+
return { exitCode: 1, output: errText };
|
|
79
|
+
}
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the concise help text for the Kyuu CLI foundation.
|
|
3
|
+
*/
|
|
4
|
+
export function getHelpText(): string {
|
|
5
|
+
return [
|
|
6
|
+
"Kyuu CLI",
|
|
7
|
+
"",
|
|
8
|
+
"Usage:",
|
|
9
|
+
" kyuu <command> [options]",
|
|
10
|
+
"",
|
|
11
|
+
"Commands:",
|
|
12
|
+
" dev Start development server with live reload",
|
|
13
|
+
" build Build production application artifact",
|
|
14
|
+
" start Start production application server",
|
|
15
|
+
" new Scaffold a new Kyuu project",
|
|
16
|
+
"",
|
|
17
|
+
"Options:",
|
|
18
|
+
" -h, --help Show help",
|
|
19
|
+
" -v, --version Show version",
|
|
20
|
+
"",
|
|
21
|
+
'Run "kyuu --help" for available commands.',
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
loadApplicationContext,
|
|
5
|
+
type ApplicationContext,
|
|
6
|
+
type ResolvedKyuuConfig,
|
|
7
|
+
} from "@kyuujs/core";
|
|
8
|
+
|
|
9
|
+
export type { ResolvedKyuuConfig };
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
handleBuildCommand,
|
|
13
|
+
type BuildCommandOptions,
|
|
14
|
+
type BuildCommandResult,
|
|
15
|
+
} from "./commands/build.js";
|
|
16
|
+
export { handleDevCommand, type DevCommandOptions, type DevCommandResult } from "./commands/dev.js";
|
|
17
|
+
export { handleNewCommand, type NewCommandOptions } from "./commands/new.js";
|
|
18
|
+
export {
|
|
19
|
+
handleStartCommand,
|
|
20
|
+
type StartCommandOptions,
|
|
21
|
+
type StartCommandResult,
|
|
22
|
+
} from "./commands/start.js";
|
|
23
|
+
export { runCli, type BaseCliOptions, type CliResult, type RunCliOptions } from "./dispatcher.js";
|
|
24
|
+
export { getHelpText } from "./help.js";
|
|
25
|
+
export { initializeGitRepo, type GitInitResult } from "./project/git.js";
|
|
26
|
+
export {
|
|
27
|
+
type PackageManager,
|
|
28
|
+
type ProjectCreationOptions,
|
|
29
|
+
type ProjectLanguage,
|
|
30
|
+
} from "./project/options.js";
|
|
31
|
+
export { installDependencies, type InstallResult } from "./project/package-manager.js";
|
|
32
|
+
export { scaffoldProject } from "./project/scaffolder.js";
|
|
33
|
+
export { validateProjectName, type ValidationResult } from "./project/validation.js";
|
|
34
|
+
export { CancelledError, runWizard, type WizardOptions } from "./project/wizard.js";
|
|
35
|
+
export {
|
|
36
|
+
startDevServer,
|
|
37
|
+
type DevServerController,
|
|
38
|
+
type DevServerOptions,
|
|
39
|
+
} from "./runner/dev-runner.js";
|
|
40
|
+
export { getVersion } from "./version.js";
|
|
41
|
+
|
|
42
|
+
export interface CliOptions {
|
|
43
|
+
projectRoot?: string;
|
|
44
|
+
mode?: "development" | "production";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CliStartResult {
|
|
48
|
+
context: ApplicationContext;
|
|
49
|
+
server: Server;
|
|
50
|
+
config: ResolvedKyuuConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Common startup pipeline for Kyuu CLI commands.
|
|
55
|
+
* Establishes project root, delegates to loadApplicationContext for automatic config discovery,
|
|
56
|
+
* and starts the HTTP server on the resolved host and port.
|
|
57
|
+
*/
|
|
58
|
+
export async function runCliCommand(
|
|
59
|
+
mode: "development" | "production",
|
|
60
|
+
options?: CliOptions,
|
|
61
|
+
): Promise<CliStartResult> {
|
|
62
|
+
const projectRoot = options?.projectRoot ? resolve(options.projectRoot) : process.cwd();
|
|
63
|
+
|
|
64
|
+
const context = await loadApplicationContext({
|
|
65
|
+
projectRoot,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const server = context.app.listen();
|
|
69
|
+
|
|
70
|
+
if (!server.listening) {
|
|
71
|
+
await new Promise<void>((res, rej) => {
|
|
72
|
+
server.once("listening", res);
|
|
73
|
+
server.once("error", rej);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
context,
|
|
79
|
+
server,
|
|
80
|
+
config: context.config,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runDev(options?: CliOptions): Promise<CliStartResult> {
|
|
85
|
+
return runCliCommand("development", options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function runStart(options?: CliOptions): Promise<CliStartResult> {
|
|
89
|
+
return runCliCommand("production", options);
|
|
90
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export interface GitInitResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initializes a Git repository in the target directory if requested.
|
|
13
|
+
*/
|
|
14
|
+
export async function initializeGitRepo(
|
|
15
|
+
directory: string,
|
|
16
|
+
execFn: typeof execFileAsync = execFileAsync,
|
|
17
|
+
): Promise<GitInitResult> {
|
|
18
|
+
try {
|
|
19
|
+
await execFn("git", ["init"], {
|
|
20
|
+
cwd: directory,
|
|
21
|
+
shell: process.platform === "win32",
|
|
22
|
+
});
|
|
23
|
+
return { success: true };
|
|
24
|
+
} catch (err: unknown) {
|
|
25
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
error: message,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ProjectLanguage = "typescript" | "javascript";
|
|
2
|
+
export type PackageManager = "pnpm" | "npm" | "yarn";
|
|
3
|
+
|
|
4
|
+
export interface ProjectCreationOptions {
|
|
5
|
+
name: string;
|
|
6
|
+
directory: string;
|
|
7
|
+
language: ProjectLanguage;
|
|
8
|
+
packageManager: PackageManager;
|
|
9
|
+
initializeGit: boolean;
|
|
10
|
+
skipInstall?: boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { PackageManager } from "./options.js";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export interface InstallResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Installs project dependencies using the selected package manager.
|
|
14
|
+
*/
|
|
15
|
+
export async function installDependencies(
|
|
16
|
+
directory: string,
|
|
17
|
+
pm: PackageManager,
|
|
18
|
+
execFn: typeof execFileAsync = execFileAsync,
|
|
19
|
+
): Promise<InstallResult> {
|
|
20
|
+
try {
|
|
21
|
+
const cmd = pm;
|
|
22
|
+
const args = ["install"];
|
|
23
|
+
|
|
24
|
+
await execFn(cmd, args, {
|
|
25
|
+
cwd: directory,
|
|
26
|
+
shell: process.platform === "win32",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return { success: true };
|
|
30
|
+
} catch (err: unknown) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: message,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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();
|