Skip to content

Commit 4c13447

Browse files
authored
feat(cli): implement kirie-cli v0 (#29)
1 parent 5c8cc9f commit 4c13447

11 files changed

Lines changed: 1231 additions & 15 deletions

File tree

packages/cli/package.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "@gd-kirie/cli",
3+
"version": "0.1.1",
4+
"type": "module",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/moeru-ai/godot-kirie.git",
8+
"directory": "packages/cli"
9+
},
10+
"bin": {
11+
"kirie": "./dist/cli.js"
12+
},
13+
"main": "./dist/index.js",
14+
"types": "./dist/index.d.ts",
15+
"exports": {
16+
".": {
17+
"import": "./src/index.ts"
18+
}
19+
},
20+
"files": [
21+
"dist"
22+
],
23+
"scripts": {
24+
"build": "tsdown",
25+
"test:run": "vitest run",
26+
"typecheck": "tsc -p tsconfig.json --noEmit"
27+
},
28+
"dependencies": {
29+
"citty": "^0.2.2",
30+
"execa": "9.6.1",
31+
"vite": "^8.0.16"
32+
},
33+
"devDependencies": {
34+
"@types/node": "^24.10.2",
35+
"tsdown": "^0.22.2",
36+
"typescript": "^5.9.3",
37+
"vitest": "^4.0.15"
38+
},
39+
"publishConfig": {
40+
"access": "public",
41+
"registry": "https://registry.npmjs.org",
42+
"exports": {
43+
".": {
44+
"types": "./dist/index.d.ts",
45+
"import": "./dist/index.js"
46+
}
47+
}
48+
}
49+
}

packages/cli/src/cli.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env node
2+
import { defineCommand, runMain } from "citty";
3+
4+
import packageJson from "../package.json" with { type: "json" };
5+
import { runDev } from "./dev";
6+
7+
const main = defineCommand({
8+
meta: {
9+
description: "Kirie development tools.",
10+
name: "kirie",
11+
version: packageJson.version,
12+
},
13+
subCommands: {
14+
dev: defineCommand({
15+
meta: {
16+
description: "Start Vite and launch Godot for desktop development.",
17+
name: "dev",
18+
},
19+
run: () => runDev(),
20+
}),
21+
},
22+
});
23+
24+
await runMain(main);

packages/cli/src/config.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import path from "node:path";
2+
import { loadConfigFromFile, type UserConfig } from "vite";
3+
4+
export interface KirieConfig extends Record<string, unknown> {
5+
godot?: {
6+
args?: string[];
7+
command?: string;
8+
project?: string;
9+
};
10+
web?: {
11+
root?: string;
12+
vite?: UserConfig;
13+
};
14+
}
15+
16+
export interface ResolvedKirieConfig {
17+
configFile: string;
18+
cwd: string;
19+
godot: {
20+
args: string[];
21+
command: string;
22+
project: string;
23+
};
24+
web: {
25+
root: string;
26+
vite: UserConfig;
27+
};
28+
}
29+
30+
export function defineKirieConfig(config: KirieConfig): KirieConfig {
31+
return config;
32+
}
33+
34+
export async function loadKirieConfig(cwd: string = process.cwd()): Promise<ResolvedKirieConfig> {
35+
const configFile = path.join(cwd, "kirie.config.ts");
36+
const result = await loadConfigFromFile(
37+
{
38+
command: "serve",
39+
isPreview: false,
40+
mode: "development",
41+
},
42+
configFile,
43+
cwd,
44+
);
45+
46+
if (!result) {
47+
throw new Error("Missing kirie.config.ts.");
48+
}
49+
50+
return resolveKirieConfig(result.config as KirieConfig, {
51+
configFile: result.path,
52+
cwd,
53+
});
54+
}
55+
56+
export function resolveKirieConfig(
57+
input: KirieConfig | undefined,
58+
context: { configFile: string; cwd: string },
59+
): ResolvedKirieConfig {
60+
const config = input ?? {};
61+
const godot = config.godot ?? {};
62+
const web = config.web ?? {};
63+
const project = path.resolve(context.cwd, godot.project ?? ".");
64+
const webRoot = path.resolve(project, web.root ?? "src-web");
65+
66+
return {
67+
configFile: context.configFile,
68+
cwd: context.cwd,
69+
godot: {
70+
args: godot.args ?? [],
71+
command: godot.command ?? "godot",
72+
project,
73+
},
74+
web: {
75+
root: webRoot,
76+
vite: web.vite ?? {},
77+
},
78+
};
79+
}

packages/cli/src/dev.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import fs from "node:fs/promises";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
6+
import { runDev } from "./dev";
7+
8+
const testProjects: string[] = [];
9+
10+
afterEach(async () => {
11+
await Promise.all(
12+
testProjects.splice(0).map((project) => fs.rm(project, { force: true, recursive: true })),
13+
);
14+
});
15+
16+
describe("runDev", () => {
17+
it("starts Vite and launches Godot with the resolved dev URL", async () => {
18+
const project = await createProject();
19+
const captureFile = path.join(project, "godot-capture.json");
20+
21+
await fs.writeFile(
22+
path.join(project, "fake-godot.mjs"),
23+
`import { writeFileSync } from "node:fs";
24+
25+
writeFileSync("godot-capture.json", JSON.stringify({
26+
argv: process.argv.slice(2),
27+
cwd: process.cwd(),
28+
env: {
29+
KIRIE_DEV: process.env.KIRIE_DEV,
30+
KIRIE_WEB_URL: process.env.KIRIE_WEB_URL,
31+
},
32+
}));
33+
`,
34+
);
35+
await writeConfig(
36+
project,
37+
`{
38+
godot: {
39+
command: process.execPath,
40+
args: ["fake-godot.mjs"],
41+
},
42+
web: {
43+
vite: {
44+
logLevel: "silent",
45+
},
46+
},
47+
}
48+
`,
49+
);
50+
51+
await runDev({
52+
cwd: project,
53+
});
54+
55+
const realProject = await fs.realpath(project);
56+
const capture = JSON.parse(await fs.readFile(captureFile, "utf8")) as {
57+
argv: string[];
58+
cwd: string;
59+
env: {
60+
KIRIE_DEV?: string;
61+
KIRIE_WEB_URL?: string;
62+
};
63+
};
64+
65+
expect(capture.argv).toEqual(["--path", project]);
66+
expect(capture.cwd).toBe(realProject);
67+
expect(capture.env.KIRIE_DEV).toBe("1");
68+
expect(capture.env.KIRIE_WEB_URL).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/$/);
69+
});
70+
71+
it("rejects Kirie-owned Vite options", async () => {
72+
const project = await createProject();
73+
await writeConfig(
74+
project,
75+
`{
76+
web: {
77+
vite: {
78+
server: {
79+
port: 4321,
80+
},
81+
},
82+
},
83+
}
84+
`,
85+
);
86+
87+
await expect(
88+
runDev({
89+
cwd: project,
90+
}),
91+
).rejects.toThrow("web.vite.server.port is owned by Kirie");
92+
});
93+
94+
it("fails clearly when src-web/index.html is missing", async () => {
95+
const project = await createProject({ index: false });
96+
await writeConfig(
97+
project,
98+
`{
99+
web: {
100+
vite: {
101+
logLevel: "silent",
102+
},
103+
},
104+
}
105+
`,
106+
);
107+
108+
await expect(
109+
runDev({
110+
cwd: project,
111+
}),
112+
).rejects.toThrow(/Kirie dev requires .*index\.html/);
113+
});
114+
});
115+
116+
async function createProject(options: { index?: boolean } = {}): Promise<string> {
117+
const project = await fs.mkdtemp(path.join(os.tmpdir(), "kirie-cli-"));
118+
testProjects.push(project);
119+
await fs.mkdir(path.join(project, "src-web"), { recursive: true });
120+
await fs.writeFile(path.join(project, "project.godot"), "");
121+
122+
if (options.index ?? true) {
123+
await fs.writeFile(
124+
path.join(project, "src-web", "index.html"),
125+
'<main id="app">Kirie dev test</main>',
126+
);
127+
}
128+
129+
return project;
130+
}
131+
132+
async function writeConfig(project: string, content: string): Promise<void> {
133+
await fs.writeFile(path.join(project, "kirie.config.ts"), `export default ${content};\n`);
134+
}

packages/cli/src/dev.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { loadKirieConfig } from "./config";
2+
import { launchGodot } from "./godot";
3+
import { startViteDevServer } from "./vite";
4+
5+
export interface DevOptions {
6+
cwd?: string;
7+
}
8+
9+
export async function runDev(options: DevOptions = {}): Promise<void> {
10+
const config = await loadKirieConfig(options.cwd);
11+
const vite = await startViteDevServer(config);
12+
let godot: ReturnType<typeof launchGodot> | undefined;
13+
14+
try {
15+
console.log(`Kirie dev server: ${vite.url}`);
16+
godot = launchGodot(config, vite.url);
17+
await godot;
18+
} finally {
19+
godot?.kill("SIGTERM");
20+
await vite.server.close();
21+
}
22+
}

packages/cli/src/godot.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { execa } from "execa";
2+
3+
import type { ResolvedKirieConfig } from "./config";
4+
5+
export function launchGodot(config: ResolvedKirieConfig, webUrl: string): ReturnType<typeof execa> {
6+
return execa(config.godot.command, [...config.godot.args, "--path", config.godot.project], {
7+
cwd: config.godot.project,
8+
env: {
9+
KIRIE_DEV: "1",
10+
KIRIE_WEB_URL: webUrl,
11+
},
12+
forceKillAfterDelay: 5_000,
13+
stdio: "inherit",
14+
});
15+
}

packages/cli/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type {
2+
KirieConfig,
3+
ResolvedKirieConfig,
4+
} from "./config";
5+
export { defineKirieConfig, loadKirieConfig, resolveKirieConfig } from "./config";
6+
export type { DevOptions } from "./dev";
7+
export { runDev } from "./dev";

0 commit comments

Comments
 (0)