@cosmicdrift/kumiko-cli 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # @cosmicdrift/kumiko-cli
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b8e1d48: New package `@cosmicdrift/kumiko-cli` — provides `kumiko` bin for
8
+ `bunx @cosmicdrift/kumiko-cli new app <name>` and `add feature <name>`.
9
+ Fixes the walkthrough's broken `bunx @cosmicdrift/kumiko-framework`
10
+ promise (bin-name ≠ pkg-name). Delegates to scaffoldApp +
11
+ scaffoldAppFeature from `@cosmicdrift/kumiko-dev-server`.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [b8e1d48]
16
+ - Updated dependencies [ce23d48]
17
+ - @cosmicdrift/kumiko-dev-server@0.14.0
package/bin/cli.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import { runCli } from "../src/index";
3
+
4
+ process.exit(await runCli({ argv: process.argv.slice(2) }));
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-cli",
3
+ "version": "0.2.0",
4
+ "description": "Standalone CLI for scaffolding Kumiko apps. `bunx @cosmicdrift/kumiko-cli new app <name>` produces a runnable workspace; `add feature <name>` mounts a feature automatically.",
5
+ "license": "BUSL-1.1",
6
+ "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/CosmicDriftGameStudio/kumiko-framework.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/CosmicDriftGameStudio/kumiko-framework/issues"
14
+ },
15
+ "homepage": "https://kumiko.so",
16
+ "type": "module",
17
+ "kumiko": {
18
+ "runtime": "dev"
19
+ },
20
+ "bin": {
21
+ "kumiko": "./bin/cli.ts"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./src/index.ts",
26
+ "default": "./src/index.ts"
27
+ }
28
+ },
29
+ "dependencies": {
30
+ "@cosmicdrift/kumiko-dev-server": "0.14.0"
31
+ },
32
+ "publishConfig": {
33
+ "registry": "https://registry.npmjs.org",
34
+ "access": "public"
35
+ },
36
+ "files": [
37
+ "bin",
38
+ "src",
39
+ "README.md",
40
+ "LICENSE"
41
+ ]
42
+ }
@@ -0,0 +1,121 @@
1
+ // Drives runCli in-process — captures stdout/stderr via the injected
2
+ // Output, exercises the 3-command-walkthrough end-to-end (new app +
3
+ // add feature) into a tmp directory.
4
+
5
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
9
+ import { runCli } from "../index";
10
+
11
+ type Captured = { logs: string[]; errs: string[] };
12
+
13
+ function capture(): {
14
+ out: { log: (s: string) => void; err: (s: string) => void };
15
+ captured: Captured;
16
+ } {
17
+ const captured: Captured = { logs: [], errs: [] };
18
+ return {
19
+ captured,
20
+ out: {
21
+ log: (s) => captured.logs.push(s),
22
+ err: (s) => captured.errs.push(s),
23
+ },
24
+ };
25
+ }
26
+
27
+ describe("kumiko cli", () => {
28
+ let tmp: string;
29
+
30
+ beforeEach(() => {
31
+ tmp = mkdtempSync(join(tmpdir(), "kumiko-cli-"));
32
+ });
33
+ afterEach(() => {
34
+ rmSync(tmp, { recursive: true, force: true });
35
+ });
36
+
37
+ test("--help prints commands + docs link", async () => {
38
+ const { out, captured } = capture();
39
+ const code = await runCli({ argv: ["--help"], out });
40
+ expect(code).toBe(0);
41
+ const joined = captured.logs.join("\n");
42
+ expect(joined).toContain("kumiko new app");
43
+ expect(joined).toContain("kumiko add feature");
44
+ expect(joined).toContain("docs.kumiko.so");
45
+ });
46
+
47
+ test("no args prints help", async () => {
48
+ const { out, captured } = capture();
49
+ const code = await runCli({ argv: [], out });
50
+ expect(code).toBe(0);
51
+ expect(captured.logs.join("\n")).toContain("kumiko new app");
52
+ });
53
+
54
+ test("--version prints semver", async () => {
55
+ const { out, captured } = capture();
56
+ const code = await runCli({ argv: ["--version"], out });
57
+ expect(code).toBe(0);
58
+ expect(captured.logs[0]).toMatch(/^\d+\.\d+\.\d+$/);
59
+ });
60
+
61
+ test("unknown command exits 1", async () => {
62
+ const { out, captured } = capture();
63
+ const code = await runCli({ argv: ["bogus"], out });
64
+ expect(code).toBe(1);
65
+ expect(captured.errs.join("\n")).toContain("unknown command");
66
+ });
67
+
68
+ test("new app <name> scaffolds the 6 walkthrough files", async () => {
69
+ const { out, captured } = capture();
70
+ const code = await runCli({ argv: ["new", "app", "my-notes"], cwd: tmp, out });
71
+ expect(code).toBe(0);
72
+ const appRoot = join(tmp, "my-notes");
73
+ for (const f of [
74
+ "package.json",
75
+ "tsconfig.json",
76
+ "src/run-config.ts",
77
+ "bin/main.ts",
78
+ ".env.example",
79
+ "README.md",
80
+ ]) {
81
+ expect(() => readFileSync(join(appRoot, f), "utf-8")).not.toThrow();
82
+ }
83
+ expect(captured.logs.join("\n")).toContain("Scaffolded my-notes");
84
+ });
85
+
86
+ test("new app rejects bad name", async () => {
87
+ const { out, captured } = capture();
88
+ const code = await runCli({ argv: ["new", "app", "My Notes"], cwd: tmp, out });
89
+ expect(code).toBe(1);
90
+ expect(captured.errs.join("\n")).toContain("kebab-case");
91
+ });
92
+
93
+ test("new app without name exits 1 with usage hint", async () => {
94
+ const { out, captured } = capture();
95
+ const code = await runCli({ argv: ["new", "app"], cwd: tmp, out });
96
+ expect(code).toBe(1);
97
+ expect(captured.errs.join("\n")).toContain("missing <name>");
98
+ });
99
+
100
+ test("add feature mounts into scaffolded app", async () => {
101
+ const { out: out1 } = capture();
102
+ await runCli({ argv: ["new", "app", "my-notes"], cwd: tmp, out: out1 });
103
+ const appRoot = join(tmp, "my-notes");
104
+
105
+ const { out: out2, captured } = capture();
106
+ const code = await runCli({ argv: ["add", "feature", "notes"], cwd: appRoot, out: out2 });
107
+ expect(code).toBe(0);
108
+
109
+ const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
110
+ expect(runConfig).toContain(`import { notesFeature } from "./features/notes";`);
111
+ expect(runConfig).toContain("notesFeature");
112
+ expect(captured.logs.join("\n")).toContain("auto-mounted");
113
+ });
114
+
115
+ test("add feature without name exits 1", async () => {
116
+ const { out, captured } = capture();
117
+ const code = await runCli({ argv: ["add", "feature"], cwd: tmp, out });
118
+ expect(code).toBe(1);
119
+ expect(captured.errs.join("\n")).toContain("missing <name>");
120
+ });
121
+ });
package/src/index.ts ADDED
@@ -0,0 +1,117 @@
1
+ // biome-ignore-all lint/suspicious/noConsole: CLI-Script, console ist Feature.
2
+ //
3
+ // runCli — programmatic entry-point for the kumiko CLI. The bin-shim
4
+ // (../bin/cli.ts) is a 3-line wrapper that forwards process.argv. Tests
5
+ // drive runCli directly with a captured Output so no subprocess is needed.
6
+ //
7
+ // Scope (DX-1.2 minimum-viable): `new app <name>` + `add feature <name>`.
8
+ // Other commands (dev, build, check, …) stay in the in-repo bin/kumiko.ts
9
+ // for now — that one operates on the framework workspace, not on a
10
+ // user-app workspace.
11
+
12
+ import { scaffoldApp, scaffoldAppFeature } from "@cosmicdrift/kumiko-dev-server";
13
+
14
+ export type Output = {
15
+ readonly log: (line: string) => void;
16
+ readonly err: (line: string) => void;
17
+ };
18
+
19
+ export type RunCliOptions = {
20
+ readonly argv: readonly string[];
21
+ readonly cwd?: string;
22
+ readonly out?: Output;
23
+ };
24
+
25
+ const DEFAULT_OUT: Output = {
26
+ log: (line) => console.log(line),
27
+ err: (line) => console.error(line),
28
+ };
29
+
30
+ const VERSION = "0.1.0";
31
+
32
+ export async function runCli(options: RunCliOptions): Promise<number> {
33
+ const out = options.out ?? DEFAULT_OUT;
34
+ const argv = [...options.argv];
35
+ const cwd = options.cwd ?? process.cwd();
36
+
37
+ const first = argv[0];
38
+ if (!first || first === "-h" || first === "--help") {
39
+ printHelp(out);
40
+ return 0;
41
+ }
42
+ if (first === "-v" || first === "--version") {
43
+ out.log(VERSION);
44
+ return 0;
45
+ }
46
+
47
+ if (first === "new") return runNew(argv.slice(1), out, cwd);
48
+ if (first === "add") return runAdd(argv.slice(1), out, cwd);
49
+
50
+ out.err(`kumiko: unknown command "${first}". Run \`kumiko --help\` for usage.`);
51
+ return 1;
52
+ }
53
+
54
+ function runNew(args: readonly string[], out: Output, cwd: string): number {
55
+ const [subject, name] = args;
56
+ if (subject !== "app") {
57
+ out.err(`kumiko new: only "new app <name>" is supported. Got "${subject ?? "(nothing)"}".`);
58
+ return 1;
59
+ }
60
+ if (!name) {
61
+ out.err("kumiko new app: missing <name>. Example: `kumiko new app my-shop`.");
62
+ return 1;
63
+ }
64
+ try {
65
+ const result = scaffoldApp({ name, destination: `${cwd}/${name}` });
66
+ out.log(`✓ Scaffolded ${result.appName} → ${result.destination}`);
67
+ out.log("");
68
+ for (const f of result.files) out.log(` ${f}`);
69
+ out.log("");
70
+ out.log("Next:");
71
+ out.log(` cd ${name}`);
72
+ out.log(" yarn install && cp .env.example .env");
73
+ out.log(" bun run boot");
74
+ return 0;
75
+ } catch (e) {
76
+ out.err(`kumiko new app: ${(e as Error).message}`);
77
+ return 1;
78
+ }
79
+ }
80
+
81
+ function runAdd(args: readonly string[], out: Output, cwd: string): number {
82
+ const [subject, name] = args;
83
+ if (subject !== "feature") {
84
+ out.err(`kumiko add: only "add feature <name>" is supported. Got "${subject ?? "(nothing)"}".`);
85
+ return 1;
86
+ }
87
+ if (!name) {
88
+ out.err("kumiko add feature: missing <name>. Example: `kumiko add feature notes`.");
89
+ return 1;
90
+ }
91
+ try {
92
+ const result = scaffoldAppFeature({ name, appRoot: cwd });
93
+ out.log(`✓ Added feature ${result.featureName}:`);
94
+ for (const f of result.files) out.log(` ${f}`);
95
+ out.log(
96
+ result.autoMounted
97
+ ? " src/run-config.ts (auto-mounted)"
98
+ : " ⚠ src/run-config.ts not auto-mounted — hand-edit APP_FEATURES.",
99
+ );
100
+ return 0;
101
+ } catch (e) {
102
+ out.err(`kumiko add feature: ${(e as Error).message}`);
103
+ return 1;
104
+ }
105
+ }
106
+
107
+ function printHelp(out: Output): void {
108
+ out.log(`kumiko v${VERSION} — scaffold Kumiko apps`);
109
+ out.log("");
110
+ out.log("Commands:");
111
+ out.log(" kumiko new app <name> Scaffold a new app workspace");
112
+ out.log(" kumiko add feature <name> Add + auto-mount a feature");
113
+ out.log(" kumiko --version Print version");
114
+ out.log(" kumiko --help This help");
115
+ out.log("");
116
+ out.log("Docs: https://docs.kumiko.so");
117
+ }