@cosmicdrift/kumiko-cli 0.413.1 → 0.414.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-cli",
3
- "version": "0.413.1",
3
+ "version": "0.414.0",
4
4
  "description": "CLI for Kumiko apps. `bunx @cosmicdrift/kumiko-cli new app <name>` scaffolds a runnable workspace; `add feature <name>` mounts a feature; `check` runs boot validation plus the guard suites with the step list derived from kumiko.json; `agent lint`, `project` and `consumer` operate on an existing app; `handbuch build` renders the HTML handbuch.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -36,19 +36,19 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@cosmicdrift/kumiko-dev-server": "0.413.1",
40
- "@cosmicdrift/kumiko-feature-ast": "0.413.1",
41
- "@cosmicdrift/kumiko-guards": "0.413.1",
42
- "@cosmicdrift/kumiko-server-runtime": "0.413.1",
43
- "@cosmicdrift/kumiko-testing": "0.413.1",
39
+ "@cosmicdrift/kumiko-dev-server": "0.414.0",
40
+ "@cosmicdrift/kumiko-feature-ast": "0.414.0",
41
+ "@cosmicdrift/kumiko-guards": "0.414.0",
42
+ "@cosmicdrift/kumiko-server-runtime": "0.414.0",
43
+ "@cosmicdrift/kumiko-testing": "0.414.0",
44
44
  "marked": "^18.1.2",
45
45
  "zod": "^4.6.7"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "sharp": "^0.35.5",
49
- "@cosmicdrift/kumiko-bundled-features": "0.413.1",
50
- "@cosmicdrift/kumiko-framework": "0.413.1",
51
- "@cosmicdrift/kumiko-renderer": "0.413.1"
49
+ "@cosmicdrift/kumiko-bundled-features": "0.414.0",
50
+ "@cosmicdrift/kumiko-framework": "0.414.0",
51
+ "@cosmicdrift/kumiko-renderer": "0.414.0"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "sharp": {
@@ -65,9 +65,9 @@
65
65
  }
66
66
  },
67
67
  "devDependencies": {
68
- "@cosmicdrift/kumiko-bundled-features": "0.413.1",
69
- "@cosmicdrift/kumiko-framework": "0.413.1",
70
- "@cosmicdrift/kumiko-renderer": "0.413.1",
68
+ "@cosmicdrift/kumiko-bundled-features": "0.414.0",
69
+ "@cosmicdrift/kumiko-framework": "0.414.0",
70
+ "@cosmicdrift/kumiko-renderer": "0.414.0",
71
71
  "sharp": "^0.35.5"
72
72
  },
73
73
  "publishConfig": {
@@ -17,7 +17,6 @@ import type { CliCommand, CliCommandContext } from "./types";
17
17
  const USAGE = "Usage: kumiko api-docs [--out <dir>] [--locales en,de] [--check]";
18
18
  const DEFAULT_OUT_DIR = "api-docs";
19
19
  const DEFAULT_EXAMPLES_DIR = "api-examples";
20
- const INDEX_FILE = "index.json";
21
20
  const WEBHOOK_EXAMPLES_SUBDIR = "webhooks";
22
21
  const WEBHOOK_EXAMPLES_PREFIX = `${WEBHOOK_EXAMPLES_SUBDIR}/`;
23
22
 
@@ -91,17 +90,17 @@ function appSource(cwd: string): { name: string; version: string } {
91
90
  }
92
91
 
93
92
  /** Files the previous run listed in its index.json, so stale pages can go without touching foreign files. */
94
- function previouslyGenerated(outDir: string): string[] {
95
- const indexPath = join(outDir, INDEX_FILE);
93
+ function previouslyGenerated(outDir: string, indexFile: string): string[] {
94
+ const indexPath = join(outDir, indexFile);
96
95
  if (!existsSync(indexPath)) return [];
97
96
  try {
98
97
  const parsed = IndexFileSchema.safeParse(readJson(indexPath));
99
98
  return parsed.success
100
- ? [INDEX_FILE, ...parsed.data.pages.map((page) => page.path)]
101
- : [INDEX_FILE];
99
+ ? [indexFile, ...parsed.data.pages.map((page) => page.path)]
100
+ : [indexFile];
102
101
  } catch {
103
102
  // skip: an unreadable index.json is rewritten below, so there is nothing stale to learn from it
104
- return [INDEX_FILE];
103
+ return [indexFile];
105
104
  }
106
105
  }
107
106
 
@@ -135,8 +134,8 @@ function readExamples<T>(
135
134
  return examples;
136
135
  }
137
136
 
138
- function writeOutput(outDir: string, files: ReadonlyMap<string, string>): void {
139
- for (const stale of previouslyGenerated(outDir)) {
137
+ function writeOutput(outDir: string, files: ReadonlyMap<string, string>, indexFile: string): void {
138
+ for (const stale of previouslyGenerated(outDir, indexFile)) {
140
139
  const target = resolve(outDir, stale);
141
140
  if (
142
141
  !files.has(stale) &&
@@ -156,6 +155,7 @@ function writeOutput(outDir: string, files: ReadonlyMap<string, string>): void {
156
155
 
157
156
  type Values = { out?: string; locales?: string; check?: boolean };
158
157
  type AgentTools = typeof import("@cosmicdrift/kumiko-bundled-features/agent-tools");
158
+ type ResolvedApiDocsConfig = ReturnType<AgentTools["resolveApiDocsConfig"]>;
159
159
 
160
160
  function errorBlock(ctx: CliCommandContext, message: string): number {
161
161
  ctx.out.err("");
@@ -198,15 +198,14 @@ function report(
198
198
  for (const line of problemLines) ctx.out.err(` ${line}`);
199
199
  return 1;
200
200
  }
201
- writeOutput(target.outDir, files);
201
+ writeOutput(target.outDir, files, tools.INDEX_FILE);
202
202
  for (const line of problemLines) ctx.out.log(` warning ${line}`);
203
203
  ctx.out.log(` ✓ ${files.size} files written to ${target.outDir}`);
204
204
  return 0;
205
205
  }
206
206
 
207
207
  type Target = {
208
- locales: readonly string[];
209
- baseUrl: string;
208
+ resolved: ResolvedApiDocsConfig;
210
209
  outDir: string;
211
210
  onDisk: Map<string, string>;
212
211
  check: boolean;
@@ -219,9 +218,11 @@ function resolveTarget(
219
218
  apiDocs: AppApiDocsConfig,
220
219
  values: Values,
221
220
  ): Target | number {
222
- const locales = values.locales?.split(",").map((locale) => locale.trim()) ??
223
- apiDocs.locales ?? ["en"];
224
- const badLocales = localeProblem(locales, tools);
221
+ const resolved = tools.resolveApiDocsConfig(apiDocs, {
222
+ appName: appSource(ctx.cwd).name,
223
+ locales: values.locales?.split(",").map((locale) => locale.trim()),
224
+ });
225
+ const badLocales = localeProblem(resolved.locales, tools);
225
226
  if (badLocales !== undefined) return usageError(ctx, badLocales);
226
227
  if (apiDocs.baseUrl === undefined) {
227
228
  return errorBlock(
@@ -238,13 +239,13 @@ function resolveTarget(
238
239
  }
239
240
  const onDisk = readOutDir(outDir);
240
241
  const check = values.check === true;
241
- if (!check && onDisk.size > 0 && !onDisk.has(INDEX_FILE)) {
242
+ if (!check && onDisk.size > 0 && !onDisk.has(tools.INDEX_FILE)) {
242
243
  return errorBlock(
243
244
  ctx,
244
- `${outDir} is not empty and holds no ${INDEX_FILE}, so it was not generated by this command. Pick another --out.`,
245
+ `${outDir} is not empty and holds no ${tools.INDEX_FILE}, so it was not generated by this command. Pick another --out.`,
245
246
  );
246
247
  }
247
- return { locales, baseUrl: apiDocs.baseUrl, outDir, onDisk, check };
248
+ return { resolved, outDir, onDisk, check };
248
249
  }
249
250
 
250
251
  async function generate(
@@ -259,7 +260,7 @@ async function generate(
259
260
  const apiDocs = config.apiDocs ?? {};
260
261
  const target = resolveTarget(ctx, tools, apiDocs, values);
261
262
  if (typeof target === "number") return target;
262
- const { locales, baseUrl, outDir, onDisk, check } = target;
263
+ const { resolved, outDir, onDisk, check } = target;
263
264
 
264
265
  const examplesDir = resolve(ctx.cwd, apiDocs.examplesDir ?? DEFAULT_EXAMPLES_DIR);
265
266
  const examples = readExamples(
@@ -282,12 +283,7 @@ async function generate(
282
283
  registry: createRegistry(
283
284
  composeFeatures(allAppFeatures(config), { includeBundled: config.includeBundled === true }),
284
285
  ),
285
- config: {
286
- ...apiDocs,
287
- locales,
288
- baseUrl,
289
- appName: apiDocs.appName ?? appSource(ctx.cwd).name,
290
- },
286
+ config: resolved,
291
287
  source: { framework: frameworkVersion(), app: appSource(ctx.cwd) },
292
288
  examples,
293
289
  webhookExamples,
@@ -0,0 +1,36 @@
1
+ import { dirname, join } from "node:path";
2
+ import type { Output } from "../output";
3
+
4
+ function resolveBiomeBin(repoRoot: string): string | undefined {
5
+ try {
6
+ return join(dirname(Bun.resolveSync("@biomejs/biome/package.json", repoRoot)), "bin", "biome");
7
+ } catch {
8
+ return undefined;
9
+ }
10
+ }
11
+
12
+ /** Runs the repo's own Biome so builtin rules and the biome-rules plugins count towards `kumiko check`. */
13
+ export async function runLintCheckStep(out: Output, repoRoot: string): Promise<number> {
14
+ const biomeBin = resolveBiomeBin(repoRoot);
15
+ if (biomeBin === undefined) {
16
+ out.err(
17
+ " ✗ @biomejs/biome is not installed in this repo — kumiko check needs it for the lint step.",
18
+ );
19
+ out.err(" Fix: `bun add -d @biomejs/biome`");
20
+ return 1;
21
+ }
22
+ const proc = Bun.spawn(["bun", biomeBin, "lint", "."], {
23
+ cwd: repoRoot,
24
+ stdout: "pipe",
25
+ stderr: "pipe",
26
+ });
27
+ const [stdout, stderr, code] = await Promise.all([
28
+ new Response(proc.stdout).text(),
29
+ new Response(proc.stderr).text(),
30
+ proc.exited,
31
+ ]);
32
+ for (const line of `${stdout}${stderr}`.split("\n")) {
33
+ if (line.trim().length > 0) out.log(` ${line}`);
34
+ }
35
+ return code === 0 ? 0 : 1;
36
+ }
@@ -1,17 +1,30 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { REPO_MANIFEST_FILE, type RepoManifest } from "@cosmicdrift/kumiko-guards/repo-manifest";
4
+ import type { Output } from "../output";
4
5
  import type { CliCommand, CliCommandContext } from "./types";
5
6
 
6
7
  const APP_SCHEMA_FILE = "kumiko/schema.ts";
8
+ const APP_CONFIG_FILE = "kumiko.config.ts";
9
+ const BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"] as const;
7
10
 
8
- export type CheckStepId = "boot" | "guards" | "ui" | "checks" | "managed" | "scripts";
11
+ export type CheckStepId =
12
+ | "boot"
13
+ | "guards"
14
+ | "ui"
15
+ | "checks"
16
+ | "lint"
17
+ | "agent"
18
+ | "managed"
19
+ | "scripts";
9
20
 
10
21
  const CHECK_STEP_IDS: readonly CheckStepId[] = [
11
22
  "boot",
12
23
  "guards",
13
24
  "ui",
14
25
  "checks",
26
+ "lint",
27
+ "agent",
15
28
  "managed",
16
29
  "scripts",
17
30
  ];
@@ -29,6 +42,10 @@ export type RepoFacts = {
29
42
  /** A `kind: "app"` repo without one is an app by role only (an Astro site, a
30
43
  * docs repo) — there is no composed feature set for validateBoot to check. */
31
44
  readonly hasAppSchema: boolean;
45
+ /** `kumiko agent lint` loads kumiko.config.ts and fails without it. */
46
+ readonly hasAppConfig: boolean;
47
+ /** The repo lints with Biome (builtin rules plus the biome-rules plugins); without a config there is nothing to run. */
48
+ readonly hasBiomeConfig: boolean;
32
49
  readonly manifestSource: "file" | "derived";
33
50
  };
34
51
 
@@ -82,23 +99,42 @@ export function resolveCheckSteps(manifest: RepoManifest, facts: RepoFacts): Che
82
99
  label: "Repo checks",
83
100
  why: `kind "${manifest.kind}" — testGlobs ${manifest.testGlobs.join(", ")}`,
84
101
  });
85
- if (manifest.kind === "app") {
86
- steps.push(
87
- {
88
- id: "managed",
89
- label: "Managed files",
90
- why: 'kind "app" — framework-owned files against the installed template',
91
- },
92
- {
93
- id: "scripts",
94
- label: "Standard scripts",
95
- why: 'kind "app" — package.json scripts against the script standard',
96
- },
97
- );
98
- }
102
+ steps.push(...resolveLintAndAppSteps(manifest, facts));
99
103
  return { kind: "steps", steps, waivers };
100
104
  }
101
105
 
106
+ function resolveLintAndAppSteps(manifest: RepoManifest, facts: RepoFacts): readonly CheckStep[] {
107
+ const steps: CheckStep[] = [];
108
+ if (facts.hasBiomeConfig) {
109
+ steps.push({
110
+ id: "lint",
111
+ label: "Biome lint",
112
+ why: `${BIOME_CONFIG_FILES.join(" or ")} present — biome lint over the repo (builtin rules and the biome-rules plugins that replaced single-file guards)`,
113
+ });
114
+ }
115
+ if (manifest.kind !== "app") return steps;
116
+ if (facts.hasAppConfig) {
117
+ steps.push({
118
+ id: "agent",
119
+ label: "Agent docs",
120
+ why: `kind "app" with ${APP_CONFIG_FILE} — kumiko agent lint over its composed features`,
121
+ });
122
+ }
123
+ steps.push(
124
+ {
125
+ id: "managed",
126
+ label: "Managed files",
127
+ why: 'kind "app" — framework-owned files against the installed template',
128
+ },
129
+ {
130
+ id: "scripts",
131
+ label: "Standard scripts",
132
+ why: 'kind "app" — package.json scripts against the script standard',
133
+ },
134
+ );
135
+ return steps;
136
+ }
137
+
102
138
  export type GuardsCli = Pick<
103
139
  typeof import("@cosmicdrift/kumiko-guards"),
104
140
  "cliFlagsError" | "findLocalRepo" | "runGuardsCli" | "runRepoChecksCli" | "runUiGuardsCli"
@@ -112,6 +148,8 @@ export type SchemaCli = Pick<
112
148
  export type AppCheckSteps = {
113
149
  readonly runManagedCheckStep: typeof import("./check-managed").runManagedCheckStep;
114
150
  readonly runScriptsCheckStep: typeof import("./check-scripts").runScriptsCheckStep;
151
+ readonly runLintCheckStep: typeof import("./check-lint").runLintCheckStep;
152
+ readonly runAgentLint: (out: Output, repoRoot: string) => Promise<number>;
115
153
  };
116
154
 
117
155
  export type CheckDeps = {
@@ -127,6 +165,9 @@ const DEFAULT_DEPS: CheckDeps = {
127
165
  loadAppSteps: async () => ({
128
166
  runManagedCheckStep: (await import("./check-managed")).runManagedCheckStep,
129
167
  runScriptsCheckStep: (await import("./check-scripts")).runScriptsCheckStep,
168
+ runLintCheckStep: (await import("./check-lint")).runLintCheckStep,
169
+ runAgentLint: async (out, repoRoot) =>
170
+ (await import("./agent")).agentCommand.run({ argv: ["lint"], cwd: repoRoot, out }),
130
171
  }),
131
172
  };
132
173
 
@@ -239,6 +280,8 @@ function findCheckedRepo(ctx: CliCommandContext, guards: GuardsCli): LocalRepo |
239
280
  function printCheckPlan(ctx: CliCommandContext, repo: LocalRepo): readonly CheckStep[] | undefined {
240
281
  const plan = resolveCheckSteps(repo.manifest, {
241
282
  hasAppSchema: existsSync(join(repo.absPath, APP_SCHEMA_FILE)),
283
+ hasAppConfig: existsSync(join(repo.absPath, APP_CONFIG_FILE)),
284
+ hasBiomeConfig: BIOME_CONFIG_FILES.some((file) => existsSync(join(repo.absPath, file))),
242
285
  manifestSource: repo.manifestSource,
243
286
  });
244
287
  if (plan.kind === "error") {
@@ -287,6 +330,10 @@ async function runStep(
287
330
  // findLocalRepo walks upwards, so ctx.cwd may be a subdirectory without kumiko/schema.ts.
288
331
  return await runSchemaCli(["validate"], repo.absPath, ctx.out);
289
332
  }
333
+ case "lint":
334
+ return (await deps.loadAppSteps()).runLintCheckStep(ctx.out, repo.absPath);
335
+ case "agent":
336
+ return (await deps.loadAppSteps()).runAgentLint(ctx.out, repo.absPath);
290
337
  case "managed":
291
338
  return (await deps.loadAppSteps()).runManagedCheckStep(
292
339
  ctx.out,
@@ -319,6 +366,8 @@ export const checkCommand: CliCommand = {
319
366
  " ui uiRoots declared UI guards (App-Mounting 2.0)",
320
367
  ' kind "app" must declare uiRoots; uiRoots: [] skips the UI guards on purpose',
321
368
  ' checks kind != "tooling" repo checks over sourceRoots + testGlobs',
369
+ " lint biome.json present biome lint (builtin rules and the biome-rules GritQL plugins)",
370
+ ` agent kind "app" + ${APP_CONFIG_FILE} kumiko agent lint (AI-agent doc gaps in the composed features)`,
322
371
  ' managed kind "app" framework-owned files against the installed template',
323
372
  ' scripts kind "app" package.json scripts against the script standard',
324
373
  "",
package/src/index.ts CHANGED
@@ -24,7 +24,6 @@ export type RunCliOptions = {
24
24
 
25
25
  const DEFAULT_OUT: Output = {
26
26
  log: (line) => console.log(line),
27
- // @wrapper-known semantic-alias
28
27
  err: (line) => console.error(line),
29
28
  };
30
29