@forinda/kickjs-cli 8.9.0 → 8.10.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.
Files changed (64) hide show
  1. package/README.md +2 -0
  2. package/dist/{agent-docs-DUB-1Z6Q.mjs → agent-docs-TdQs75ND.mjs} +70 -21
  3. package/dist/agent-docs-TdQs75ND.mjs.map +1 -0
  4. package/dist/{agent-docs-bVMPt2m5.mjs → agent-docs-uEhHT_Tq.mjs} +72 -23
  5. package/dist/{build-C30mBgw6.mjs → build-D90mecBH.mjs} +3 -3
  6. package/dist/{build-C30mBgw6.mjs.map → build-D90mecBH.mjs.map} +1 -1
  7. package/dist/{build-xQXbdDpw.mjs → build-xfLqm9dd.mjs} +2 -2
  8. package/dist/{builtins-CtpLZ4FY.mjs → builtins-C3BQ79nW.mjs} +2 -2
  9. package/dist/{builtins-DiWfXc-D.mjs → builtins-CNJXXYW-.mjs} +149 -153
  10. package/dist/cli.mjs +2 -2
  11. package/dist/config-BV6rXylg.mjs +13 -0
  12. package/dist/config-BV6rXylg.mjs.map +1 -0
  13. package/dist/config-CVuSVUvw.mjs +12 -0
  14. package/dist/{doctor-CnsnRW7T.mjs → doctor-a-UkOZYd.mjs} +35 -40
  15. package/dist/doctor-a-UkOZYd.mjs.map +1 -0
  16. package/dist/fullstack-BlNhq-tI.mjs +100 -0
  17. package/dist/fullstack-BlNhq-tI.mjs.map +1 -0
  18. package/dist/fullstack-Zk31eUT1.mjs +99 -0
  19. package/dist/index.d.mts.map +1 -1
  20. package/dist/index.mjs +2 -2
  21. package/dist/package-manager-BUDLqU4F.mjs +12 -0
  22. package/dist/package-manager-BUDLqU4F.mjs.map +1 -0
  23. package/dist/package-manager-Bk2uiXkV.mjs +11 -0
  24. package/dist/{plugin-Bs0soxrU.mjs → plugin-DDENlsHw.mjs} +2 -2
  25. package/dist/{plugin-BfdvW6_r.mjs → plugin-FOqPjQ0u.mjs} +2 -2
  26. package/dist/project-BBiJ64ya.mjs +114 -0
  27. package/dist/project-fa9FRWXx.mjs +115 -0
  28. package/dist/project-fa9FRWXx.mjs.map +1 -0
  29. package/dist/{project-root-LYOxQHXp.mjs → project-root-BMpUTIbO.mjs} +2 -2
  30. package/dist/{project-root-B9BrO1na.mjs → project-root-BZ1wVwrr.mjs} +3 -3
  31. package/dist/{project-root-B9BrO1na.mjs.map → project-root-BZ1wVwrr.mjs.map} +1 -1
  32. package/dist/{prompts-BEAqcbPT.mjs → prompts-C4g1xbsv.mjs} +2 -2
  33. package/dist/{prompts-BEAqcbPT.mjs.map → prompts-C4g1xbsv.mjs.map} +1 -1
  34. package/dist/{rolldown-runtime-Da-NDvKz.mjs → rolldown-runtime-D8pytGn4.mjs} +1 -1
  35. package/dist/{run-plugins-0AfIgIsZ.mjs → run-plugins-CeZS2Opt.mjs} +107 -106
  36. package/dist/run-plugins-CeZS2Opt.mjs.map +1 -0
  37. package/dist/{typegen-BzgWveE_.mjs → typegen-Cj5lp0gl.mjs} +5 -5
  38. package/dist/{typegen-BzgWveE_.mjs.map → typegen-Cj5lp0gl.mjs.map} +1 -1
  39. package/dist/{typegen-up-UkaNc.mjs → typegen-D9opCn2d.mjs} +4 -4
  40. package/dist/{types-ChFFRT8E.mjs → types-BqvSIAQz.mjs} +1 -1
  41. package/package.json +6 -6
  42. package/templates/base/files/README.md +17 -15
  43. package/templates/base/files/kick.config.ts +1 -1
  44. package/templates/feature-storage/feature.json +13 -0
  45. package/templates/feature-storage/files/_dot_env.example.append +6 -0
  46. package/templates/feature-storage/files/_dot_gitignore.append +2 -0
  47. package/templates/feature-storage/files/src/storage.ts +16 -0
  48. package/templates/template-inertia/files/src/client/app.css +1 -1
  49. package/templates/template-inertia/files/vite.client.config.ts +1 -1
  50. package/dist/agent-docs-DUB-1Z6Q.mjs.map +0 -1
  51. package/dist/config-YvIngROH.mjs +0 -12
  52. package/dist/config-ZHuFjsKI.mjs +0 -13
  53. package/dist/config-ZHuFjsKI.mjs.map +0 -1
  54. package/dist/doctor-CnsnRW7T.mjs.map +0 -1
  55. package/dist/fs-Bm2St4gk.mjs +0 -11
  56. package/dist/fs-BvGbbHY0.mjs +0 -12
  57. package/dist/fs-BvGbbHY0.mjs.map +0 -1
  58. package/dist/fullstack-B0g06sSQ.mjs +0 -96
  59. package/dist/fullstack-B0g06sSQ.mjs.map +0 -1
  60. package/dist/fullstack-CZ9XQYk-.mjs +0 -95
  61. package/dist/project-B_qDdkYQ.mjs +0 -113
  62. package/dist/project-B_qDdkYQ.mjs.map +0 -1
  63. package/dist/project-Bp-KfBNb.mjs +0 -112
  64. package/dist/run-plugins-0AfIgIsZ.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v8.9.0
2
+ * @forinda/kickjs-cli v8.10.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as e}from"./rolldown-runtime-Da-NDvKz.mjs";import{a as t}from"./config-YvIngROH.mjs";import{r as n}from"./fs-Bm2St4gk.mjs";import{existsSync as r,readFileSync as i}from"node:fs";import{join as a}from"node:path";import*as o from"@clack/prompts";import s from"picocolors";const c={GET:s.green,POST:s.cyan,PUT:s.yellow,PATCH:s.magenta,DELETE:s.red};function l(e){return(c[e]??s.dim)(e.padEnd(7))}function u(e){let t=`[${e}]`.padEnd(10);switch(e){case`CRITICAL`:return s.red(t);case`WARNING`:return s.yellow(t);case`INFO`:return s.blue(s.dim(t));default:return t}}s.green(`✓`),s.red(`✖`),s.yellow(`⚠`),s.blue(`ℹ`);function d(e){o.intro(s.bgCyan(s.black(` ${e} `)))}function f(e){o.outro(e)}function p(e){o.isCancel(e)&&(o.cancel(`Operation cancelled.`),process.exit(0))}async function m(e){let t=await o.text(e);return p(t),t}async function h(e){let t=await o.select(e);return p(t),t}async function g(e){let t=await o.multiselect(e);return p(t),t}async function _(e){let t=await o.confirm(e);return p(t),t}function v(){return o.spinner()}const y=o.log;function b(e,t,n){return`# CLAUDE.md — ${e}
11
+ import{t as e}from"./rolldown-runtime-D8pytGn4.mjs";import{o as t,r as n,u as r}from"./config-CVuSVUvw.mjs";import{n as i,o as a}from"./package-manager-Bk2uiXkV.mjs";import{existsSync as o,readFileSync as s}from"node:fs";import{join as c}from"node:path";import*as l from"@clack/prompts";import u from"picocolors";const d={GET:u.green,POST:u.cyan,PUT:u.yellow,PATCH:u.magenta,DELETE:u.red};function f(e){return(d[e]??u.dim)(e.padEnd(7))}function p(e){let t=`[${e}]`.padEnd(10);switch(e){case`CRITICAL`:return u.red(t);case`WARNING`:return u.yellow(t);case`INFO`:return u.blue(u.dim(t));default:return t}}u.green(`✓`),u.red(`✖`),u.yellow(`⚠`),u.blue(`ℹ`);function m(e){l.intro(u.bgCyan(u.black(` ${e} `)))}function h(e){l.outro(e)}function g(e){l.isCancel(e)&&(l.cancel(`Operation cancelled.`),process.exit(0))}async function _(e){let t=await l.text(e);return g(t),t}async function v(e){let t=await l.select(e);return g(t),t}async function y(e){let t=await l.multiselect(e);return g(t),t}async function b(e){let t=await l.confirm(e);return g(t),t}function x(){return l.spinner()}const S=l.log;function C(e,t){let n=e;return t===`fullstack`&&n!==`pnpm`?`${r(n,`dev:server`)} + ${r(n,`dev:web`)}`:r(n,`dev`)}function w(e,t,i){return`# CLAUDE.md — ${e}
12
12
 
13
13
  **Read \`./.agents/AGENTS.md\` first.** It is the canonical, multi-agent
14
14
  reference for this project (Claude, Copilot, Codex, Gemini, etc.) —
@@ -56,14 +56,17 @@ prose.
56
56
  ## Quick reference (full version in .agents/AGENTS.md)
57
57
 
58
58
  \`\`\`bash
59
- ${n} install # Install dependencies
60
- kick dev # Dev server with HMR + typegen
61
- kick build && kick start # Production
62
- ${n} run test # Vitest
63
- kick typecheck # tsc/tsgo, whichever the project has
64
- kick format # oxfmt
59
+ ${i} install
60
+ ${C(i,t)} # Dev server with HMR + typegen
61
+ ${r(i,`build`)} && ${r(i,`start`)} # Production
62
+ ${r(i,`test`)} # Vitest
63
+ ${t===`fullstack`?r(i,`typecheck`):`${n(i,`kick`)} typecheck`} # tsc/tsgo, whichever the project has
64
+ ${t===`fullstack`?r(i,`format`):`${n(i,`kick`)} format`} # oxfmt
65
65
  \`\`\`
66
66
 
67
+ \`kick\` is this project's dev dependency, not a global command: every \`kick …\`
68
+ in these docs runs as \`${n(i,`kick`)} …\` in a shell.
69
+
67
70
  ## v4 framework reminders
68
71
 
69
72
  When generating or modifying code in this project, stay aligned with the v4 conventions documented in \`.agents/AGENTS.md\`:
@@ -83,7 +86,7 @@ When generating or modifying code in this project, stay aligned with the v4 conv
83
86
  - **Refresh these files**: \`kick g agents -f\` regenerates \`CLAUDE.md\` at the project root and \`.agents/AGENTS.md\` + \`.agents/GEMINI.md\` + \`.agents/COPILOT.md\` + every \`.agents/skills/<name>/SKILL.md\` from the latest CLI templates. Hand-edited content is overwritten — keep customisation in \`.agents/AGENTS.local.md\` or per-skill \`SKILL.local.md\` files alongside.
84
87
 
85
88
  For everything else (controllers, services, modules, RequestContext API, generators, CLI commands, package additions, env wiring, troubleshooting) → \`.agents/AGENTS.md\`.
86
- `}function x(e,t,n){return`# AGENTS.md — AI Agent Guide for ${e}
89
+ `}function T(e,t,i){return`# AGENTS.md — AI Agent Guide for ${e}
87
90
 
88
91
  This guide is the **canonical, multi-agent reference** for this KickJS
89
92
  application — Claude, Copilot, Codex, Gemini, etc. all read it first.
@@ -92,13 +95,18 @@ add tool-specific affordances on top.
92
95
 
93
96
  ## Before You Start
94
97
 
95
- 1. Run \`${n} install\` to install dependencies
96
- 2. Run \`kick dev\` to verify the app starts${t===`fullstack`?`
98
+ 1. Run \`${i} install\` to install dependencies
99
+ 2. Run \`${C(i,t)}\` to verify the app starts
100
+
101
+ \`kick\` is this project's dev dependency, not a global command: every \`kick …\`
102
+ below runs as \`${n(i,`kick`)} …\` in a shell.${t===`fullstack`?`
97
103
 
98
104
  ## Fullstack workspace layout
99
105
 
100
106
  This is a WORKSPACE root — the KickJS API lives in \`server/\`, the typed web
101
- app in \`web/\`. Run both with \`${n===`pnpm`?`pnpm dev`:`${n} run dev:server + ${n} run dev:web`}\`.
107
+ app in \`web/\`. Run both with \`${C(i,t)}\`. From this root,
108
+ \`${r(i,`test`)}\`, \`${r(i,`typecheck`)}\` and \`${r(i,`format`)}\` reach into
109
+ \`server/\` (and \`web/\` for typecheck).
102
110
 
103
111
  The type loop (do not break it):
104
112
  1. \`server/\` handlers RETURN their payloads → \`kick typegen\` emits
@@ -112,8 +120,8 @@ The type loop (do not break it):
112
120
  relative \`/api/v1\` (Vite proxies it in dev) unless \`VITE_API_URL\` points at
113
121
  an API on another origin.
114
122
 
115
- Rules: kick commands (\`kick g\`, \`kick typegen\`, \`kick dev\`) run in
116
- \`server/\`; never import server runtime code into \`web/\` (the d.ts bridge is
123
+ Rules: kick commands (\`kick g\`, \`kick typegen\`, \`kick dev\`, \`kick typecheck\`)
124
+ run in \`server/\` — the root has no \`kick.config.ts\`; never import server runtime code into \`web/\` (the d.ts bridge is
117
125
  type-only); prefer return-value handlers so responses stay inferable.`:``}
118
126
  3. Read the [KickJS documentation](https://kickjs.app/) for framework details
119
127
 
@@ -203,6 +211,11 @@ mistakes:
203
211
  imports from \`src/config\`: read values with \`getEnv('KEY')\`, an injected
204
212
  \`ConfigService\` (\`config.get('KEY')\`), or \`@Value('KEY')\` typed as
205
213
  \`Env<'KEY'>\` — never \`process.env\`. Add a key to the schema before reading it.
214
+ A concern with several keys (a database, a mail provider) gets a typed section
215
+ instead: \`defineSettings('DB', schema)\` in the module's own
216
+ \`<name>.settings.ts\` — not in \`src/config\` — declared by the module that
217
+ needs it (\`settings: () => [DbSettings]\`) and read through
218
+ \`@Inject(DbSettings)\` or \`DbSettings.get()\`. Its variables are \`DB_*\`.
206
219
 
207
220
  - **Module entry files MUST be named \`<name>.module.ts\`** — see the Vite
208
221
  HMR contract at the top of "Module Pattern" below. The CLI enforces this;
@@ -353,7 +366,7 @@ See the **\`kickjs-write-controller-test\`** skill for the canonical test — it
353
366
  carries the call shape, the DI reset, and the env side-effect import, each of
354
367
  which has its own failure mode.
355
368
 
356
- Run with \`${n} run test\` (\`test:watch\` for watch mode).
369
+ Run with \`${i} run test\` (\`test:watch\` for watch mode).
357
370
 
358
371
  ## Environment Variables
359
372
 
@@ -488,7 +501,7 @@ page and lists the local tools (\`kick explain\`, \`kick doctor\`, \`kick inspec
488
501
 
489
502
  Start at <https://kickjs.app/>.
490
503
 
491
- `}function S(e,t=`define`){let n=t===`class`?`UserModule`:`UserModule()`;return[{slug:`add-module`,frontmatterName:`kickjs-add-module`,description:`Use when the user asks to add a new feature module (controller + service + repo + DTOs).`,body:`**Trigger phrases**: "add a users module", "scaffold tasks", "new feature for X".
504
+ `}function E(e,t=`define`){let i=t===`class`?`UserModule`:`UserModule()`;return[{slug:`add-module`,frontmatterName:`kickjs-add-module`,description:`Use when the user asks to add a new feature module (controller + service + repo + DTOs).`,body:`**Trigger phrases**: "add a users module", "scaffold tasks", "new feature for X".
492
505
 
493
506
  **Steps**:
494
507
  1. Run \`kick g module <name>\` (use plural form if the project pluralizes — check \`kick.config.ts\`).
@@ -562,7 +575,7 @@ describe('UserController', () => {
562
575
  // configured with. Pass \`runtime\` to \`createTestApp\` to match production.
563
576
  const { app } = await createTestApp({
564
577
  // Matches this project's \`modules.style\`.
565
- modules: [${n}],
578
+ modules: [${i}],
566
579
  })
567
580
  const res = await request(app.handle.bind(app)).get('/api/v1/users')
568
581
  expect(res.status).toBe(200)
@@ -592,7 +605,43 @@ async create(ctx: Ctx<KickRoutes.TodoController['create']>) {
592
605
 
593
606
  **Nuances**:
594
607
  - \`@Inject\` and \`@Autowired\` are interchangeable — same runtime, same types; pick by readability.
595
- - \`@Value('MISSING_KEY')\` with no default **throws on property access**, not at construction — tests that exercise the getter will surface the missing-env issue.`},{slug:`env-wiring-check`,frontmatterName:`kickjs-env-wiring-check`,description:`Use when ConfigService.get('SOME_KEY') returns undefined or @Value silently falls back to process.env.`,body:"**Fastest path**: `kick explain \"ConfigService.get('MY_KEY') returned undefined\"` —\npipe a failing run straight in if you prefer (`pnpm test 2>&1 | kick explain`). It\ndistinguishes the entry-file cause from the test-file one, which is the step most\npeople lose time on. The manual checks below are the same reasoning.\n\n**Diagnosis (in order)**:\n1. Open `src/index.ts`. The **first non-`reflect-metadata`** import MUST be `import './config'`.\n2. Open `src/config/index.ts`. It MUST run the loader as a top-level side effect — not just declare the schema. This is what `kick new` generates:\n ```ts\n import { loadEnvFromSchema } from '@forinda/kickjs/config'\n import { fromZod } from '@forinda/kickjs-schema/zod'\n const envSchema = fromZod(z.object({ DATABASE_URL: z.url() }))\n export const env = loadEnvFromSchema(envSchema)\n ```\n3. The new key MUST be declared in the schema. `@Value('NEW_KEY')` accepts any string at the type level and **falls back to raw `process.env`** when the schema doesn't know the key — silently skipping its parsing and defaults.\n4. After adding a key, re-run `kick typegen` (or restart `kick dev` if the typegen watcher missed it) so the global `KickEnv` augmentation picks it up.\n\n5. **In a test?** `createTestApp` never loads `src/index.ts`, so the entry's\n `import './config'` never runs no matter how correct it is. The test file\n must import it itself:\n ```ts\n import '@/config'\n ```\n Symptom is identical to the missing-entry-import case, and step 1 will look\n fine, which is what makes it slow to spot.\n\n**Why `@Value` \"works\" but `ConfigService.get` doesn't**: `@Value` has the `process.env` fallback that masks missing-side-effect-import bugs; `ConfigService` has none. If `@Value('FOO')` returns a value but `ConfigService.get('FOO')` returns `undefined`, the side-effect import of `./config` is missing.\n\n**`reloadEnv` vs `resetEnvCache`** — distinct, frequently mixed up:\n- `reloadEnv()` — re-reads `process.env` against the **already registered** schema. Use in HMR plugins after `.env` file changes. Schema survives.\n- `resetEnvCache()` — drops the registered schema entirely. **Test-only.** Calling it between dev requests drops the project's keys.\n\n**Nuances**:\n- `dotenv` is an **optional peer dep** in v5+ — projects upgrading from older versions may need to add it explicitly.\n- For HMR-friendly `.env` edits, add `envWatchPlugin()` to `vite.config.ts` — calls `reloadEnv()` automatically.\n\n**Fix recipe**: add the key to the schema; add `import './config'` as the first non-reflect-metadata import in `src/index.ts`; add `import '@/config'` to any test that reads config; re-run `kick typegen`."},{slug:`bootstrap-export`,frontmatterName:`kickjs-bootstrap-export`,description:`Use when HMR is silently doing full restarts on every save, or createTestApp can't find the app handle.`,body:"**Check** `src/index.ts`'s last line:\n\n```ts\n// CORRECT — Vite plugin + createTestApp import the named `app` symbol\nexport const app = await bootstrap({ ... })\n\n// WRONG — HMR degrades to full restart, createTestApp loses the handle\nawait bootstrap({ ... })\n```\n\nThe Vite plugin imports the named `app` symbol via `virtual:kickjs/app`; testing helpers do too. Without the export, both fall back to slower paths (full restart on save, mock handle in tests) **without warning**.\n\n**Red flags**:\n- A bare `await bootstrap(...)` with no `export` — fix by adding `export const app =`.\n- Re-assigning `app` later in the file (`app = somethingElse`) — Vite imports by reference at module-load time; reassignments don't propagate.\n- Multiple files calling `bootstrap()` — only the entry should. Tests use `createTestApp` instead."},{slug:`thin-entry-file`,frontmatterName:`kickjs-thin-entry-file`,description:`Use when src/index.ts is accumulating module/middleware/plugin/adapter literals.`,body:`**Refactor target**:
608
+ - \`@Value('MISSING_KEY')\` with no default **throws on property access**, not at construction — tests that exercise the getter will surface the missing-env issue.`},{slug:`env-wiring-check`,frontmatterName:`kickjs-env-wiring-check`,description:`Use when ConfigService.get('SOME_KEY') returns undefined or @Value silently falls back to process.env.`,body:`**Fastest path**: \`kick explain "ConfigService.get('MY_KEY') returned undefined"\` —
609
+ pipe a failing run straight in if you prefer (\`${r(e,`test`)} 2>&1 | ${n(e,`kick`)} explain\`). It
610
+ distinguishes the entry-file cause from the test-file one, which is the step most
611
+ people lose time on. The manual checks below are the same reasoning.
612
+
613
+ **Diagnosis (in order)**:
614
+ 1. Open \`src/index.ts\`. The **first non-\`reflect-metadata\`** import MUST be \`import './config'\`.
615
+ 2. Open \`src/config/index.ts\`. It MUST run the loader as a top-level side effect — not just declare the schema. This is what \`kick new\` generates:
616
+ \`\`\`ts
617
+ import { loadEnvFromSchema } from '@forinda/kickjs/config'
618
+ import { fromZod } from '@forinda/kickjs-schema/zod'
619
+ const envSchema = fromZod(z.object({ DATABASE_URL: z.url() }))
620
+ export const env = loadEnvFromSchema(envSchema)
621
+ \`\`\`
622
+ 3. The new key MUST be declared in the schema. \`@Value('NEW_KEY')\` accepts any string at the type level and **falls back to raw \`process.env\`** when the schema doesn't know the key — silently skipping its parsing and defaults.
623
+ 4. After adding a key, re-run \`kick typegen\` (or restart \`kick dev\` if the typegen watcher missed it) so the global \`KickEnv\` augmentation picks it up.
624
+
625
+ 5. **In a test?** \`createTestApp\` never loads \`src/index.ts\`, so the entry's
626
+ \`import './config'\` never runs no matter how correct it is. The test file
627
+ must import it itself:
628
+ \`\`\`ts
629
+ import '@/config'
630
+ \`\`\`
631
+ Symptom is identical to the missing-entry-import case, and step 1 will look
632
+ fine, which is what makes it slow to spot.
633
+
634
+ **Why \`@Value\` "works" but \`ConfigService.get\` doesn't**: \`@Value\` has the \`process.env\` fallback that masks missing-side-effect-import bugs; \`ConfigService\` has none. If \`@Value('FOO')\` returns a value but \`ConfigService.get('FOO')\` returns \`undefined\`, the side-effect import of \`./config\` is missing.
635
+
636
+ **\`reloadEnv\` vs \`resetEnvCache\`** — distinct, frequently mixed up:
637
+ - \`reloadEnv()\` — re-reads \`process.env\` against the **already registered** schema. Use in HMR plugins after \`.env\` file changes. Schema survives.
638
+ - \`resetEnvCache()\` — drops the registered schema entirely. **Test-only.** Calling it between dev requests drops the project's keys.
639
+
640
+ **Nuances**:
641
+ - \`dotenv\` is an **optional peer dep** in v5+ — projects upgrading from older versions may need to add it explicitly.
642
+ - For HMR-friendly \`.env\` edits, add \`envWatchPlugin()\` to \`vite.config.ts\` — calls \`reloadEnv()\` automatically.
643
+
644
+ **Fix recipe**: add the key to the schema; add \`import './config'\` as the first non-reflect-metadata import in \`src/index.ts\`; add \`import '@/config'\` to any test that reads config; re-run \`kick typegen\`.`},{slug:`bootstrap-export`,frontmatterName:`kickjs-bootstrap-export`,description:`Use when HMR is silently doing full restarts on every save, or createTestApp can't find the app handle.`,body:"**Check** `src/index.ts`'s last line:\n\n```ts\n// CORRECT — Vite plugin + createTestApp import the named `app` symbol\nexport const app = await bootstrap({ ... })\n\n// WRONG — HMR degrades to full restart, createTestApp loses the handle\nawait bootstrap({ ... })\n```\n\nThe Vite plugin imports the named `app` symbol via `virtual:kickjs/app`; testing helpers do too. Without the export, both fall back to slower paths (full restart on save, mock handle in tests) **without warning**.\n\n**Red flags**:\n- A bare `await bootstrap(...)` with no `export` — fix by adding `export const app =`.\n- Re-assigning `app` later in the file (`app = somethingElse`) — Vite imports by reference at module-load time; reassignments don't propagate.\n- Multiple files calling `bootstrap()` — only the entry should. Tests use `createTestApp` instead."},{slug:`thin-entry-file`,frontmatterName:`kickjs-thin-entry-file`,description:`Use when src/index.ts is accumulating module/middleware/plugin/adapter literals.`,body:`**Refactor target**:
596
645
 
597
646
  \`\`\`ts
598
647
  // src/modules/index.ts — fluent chain (default for \`modules.style: 'define'\`)
@@ -788,7 +837,7 @@ fine and fails at runtime.
788
837
  **Local, and usually faster**
789
838
 
790
839
  - \`kick explain "<error message>"\` — matches known failures and gives the fix.
791
- Pipe a failing run in: \`${e} test 2>&1 | kick explain\`.
840
+ Pipe a failing run in: \`${r(e,`test`)} 2>&1 | ${n(e,`kick`)} explain\`.
792
841
  - \`kick doctor\` — checks this project's wiring.
793
842
  - \`kick inspect\` — the live route / adapter table.
794
843
  - \`.kickjs/types/\` — generated types are ground truth for what exists.
@@ -797,7 +846,7 @@ fine and fails at runtime.
797
846
 
798
847
  **Rule**: if you would be guessing, look it up. If a doc page contradicts the
799
848
  installed \`.d.mts\`, trust the \`.d.mts\` and say so — the docs may be behind the
800
- version this project has.`},{slug:`refresh-agent-docs`,frontmatterName:`kickjs-refresh-agent-docs`,description:`Use after a KickJS version bump to sync the .agents/ docs with the latest CLI templates.`,body:"**Steps**:\n1. `kick g agents -f --only both` — overwrites `CLAUDE.md` (root) and `.agents/AGENTS.md`.\n2. `kick g agents -f --only skills` — refreshes every `.agents/skills/<slug>/SKILL.md`.\n3. `kick g agents -f --only gemini` / `--only copilot` — refresh the per-agent files when needed.\n4. Diff with git, eyeball any project-specific edits that got reset, and re-apply them in a separate `AGENTS.local.md` or per-skill `SKILL.local.md` alongside.\n5. Commit as `docs(agents): sync from CLI vX.Y`.\n\n**`.agents/` layout** (post-restructure):\n\n```\nCLAUDE.md # at root — Claude Code auto-loads from here\n.agents/\n├── AGENTS.md # canonical multi-agent reference\n├── GEMINI.md # Gemini-specific notes\n├── COPILOT.md # Copilot CLI notes\n└── skills/\n ├── add-module/SKILL.md\n ├── add-adapter/SKILL.md\n └── … # one SKILL.md per skill, frontmatter-namespaced\n```\n\nCustomisation goes in `.local.md` siblings (`AGENTS.local.md`, `skills/<slug>/SKILL.local.md`) — those are never overwritten."},{slug:`deny-list`,frontmatterName:`kickjs-deny-list`,description:`Patterns to refuse outright when the user asks for them — they break v4 invariants.`,body:"**Module / adapter / plugin shape**:\n- `class implements AppAdapter` → use `defineAdapter()`.\n- `class implements KickPlugin` / function returning `KickPlugin` → use `definePlugin()`.\n- `class implements AppModule` for new code → use `defineModule()`.\n- `bootstrap({ adapters: [MyAdapter] })` (factory) → `MyAdapter()` (instance, with parens).\n- `@Controller('/path')` with a path argument → `@Controller()`, with the prefix in `routes().path`.\n- Module file named `<name>.ts` (no `.module` suffix) → rename to `<name>.module.ts`. Vite HMR's glob doesn't pick up the unsuffixed form.\n\n**DI**:\n- `new Container()` or `Container.getInstance().reset()` in tests → use `Container.reset()` in `beforeEach` (or `Container.create()` for fully isolated graphs).\n- DI tokens with `:` separator (`'app:db:url'`) or in PascalCase → use slash-delimited lower-case (`'app/db/url'`). First-party uses reserved `'kick/'` prefix.\n- `Symbol.for(...)` for DI tokens — globally interned, **collides across files**. Use `createToken<T>('name')`.\n- Raw string tokens (`@Inject('config')`) — silent collisions; widens to `unknown`. Use `createToken<T>`.\n- Injecting a `Scope.REQUEST` service into a `SINGLETON` — container throws at resolve time.\n\n**Bootstrap / entry file**:\n- `bootstrap({ ... })` without `export const app = ...` → always export. HMR degrades to full restart and `createTestApp` loses the handle.\n- `bootstrap({ register: ... })` — that option doesn't exist. Use an inline plugin in `plugins`.\n\n**Middleware**:\n- Using `(ctx, next)` for global middleware in `bootstrap({ middlewares })` — global middleware uses raw Express `(req, res, next)`. Wrong signature throws \"Cannot read properties of undefined\".\n- Using `(req, res, next)` for an `@Middleware()` decorator — those use `(ctx, next)`.\n- `@Middleware()` whose only output is `ctx.set('x', v)` — should be a context decorator (typed, ordered, testable).\n\n**Context contributors**:\n- `ctx.tenant = x` from a contributor — only sticks to one `RequestContext` instance. **Return the value** so the runner writes it via `ctx.set(key, value)`.\n- Omitting the `declare module '@forinda/kickjs'` block — without it `ctx.get('tenant')` is `unknown`.\n- `getRequestValue<string>('traceId')` — generic is the **key** type, not value type.\n\n**Env / config**:\n- `@Value('NEW_KEY')` without the key in the Zod schema — silent fallback to raw `process.env`, no coercion.\n- `resetEnvCache()` outside tests — drops the registered schema.\n\n**List endpoints**:\n- Reading `req.query.status` directly — bypasses the allow-list. Use `ctx.qs({ filterable })`.\n- Returning a bare array from a list endpoint — breaks the `PaginatedResponse<T>` contract. Use `ctx.paginate()`.\n\n**Assets**:\n- Hand-rolled `__dirname` arithmetic for template paths — use `assets.<ns>.<key>()` and add the namespace to `kick.config.ts assetMap`."}]}function C(e,t,n,r=`define`){let i=`<!-- Generated by \`kick g agents\` for ${e}. Edits are overwritten on the next refresh; keep customisation in a SKILL.local.md alongside. -->`;return S(n,r).map(e=>({slug:e.slug,content:`---
849
+ version this project has.`},{slug:`refresh-agent-docs`,frontmatterName:`kickjs-refresh-agent-docs`,description:`Use after a KickJS version bump to sync the .agents/ docs with the latest CLI templates.`,body:"**Steps**:\n1. `kick g agents -f --only both` — overwrites `CLAUDE.md` (root) and `.agents/AGENTS.md`.\n2. `kick g agents -f --only skills` — refreshes every `.agents/skills/<slug>/SKILL.md`.\n3. `kick g agents -f --only gemini` / `--only copilot` — refresh the per-agent files when needed.\n4. Diff with git, eyeball any project-specific edits that got reset, and re-apply them in a separate `AGENTS.local.md` or per-skill `SKILL.local.md` alongside.\n5. Commit as `docs(agents): sync from CLI vX.Y`.\n\n**`.agents/` layout** (post-restructure):\n\n```\nCLAUDE.md # at root — Claude Code auto-loads from here\n.agents/\n├── AGENTS.md # canonical multi-agent reference\n├── GEMINI.md # Gemini-specific notes\n├── COPILOT.md # Copilot CLI notes\n└── skills/\n ├── add-module/SKILL.md\n ├── add-adapter/SKILL.md\n └── … # one SKILL.md per skill, frontmatter-namespaced\n```\n\nCustomisation goes in `.local.md` siblings (`AGENTS.local.md`, `skills/<slug>/SKILL.local.md`) — those are never overwritten."},{slug:`deny-list`,frontmatterName:`kickjs-deny-list`,description:`Patterns to refuse outright when the user asks for them — they break v4 invariants.`,body:"**Module / adapter / plugin shape**:\n- `class implements AppAdapter` → use `defineAdapter()`.\n- `class implements KickPlugin` / function returning `KickPlugin` → use `definePlugin()`.\n- `class implements AppModule` for new code → use `defineModule()`.\n- `bootstrap({ adapters: [MyAdapter] })` (factory) → `MyAdapter()` (instance, with parens).\n- `@Controller('/path')` with a path argument → `@Controller()`, with the prefix in `routes().path`.\n- Module file named `<name>.ts` (no `.module` suffix) → rename to `<name>.module.ts`. Vite HMR's glob doesn't pick up the unsuffixed form.\n\n**DI**:\n- `new Container()` or `Container.getInstance().reset()` in tests → use `Container.reset()` in `beforeEach` (or `Container.create()` for fully isolated graphs).\n- DI tokens with `:` separator (`'app:db:url'`) or in PascalCase → use slash-delimited lower-case (`'app/db/url'`). First-party uses reserved `'kick/'` prefix.\n- `Symbol.for(...)` for DI tokens — globally interned, **collides across files**. Use `createToken<T>('name')`.\n- Raw string tokens (`@Inject('config')`) — silent collisions; widens to `unknown`. Use `createToken<T>`.\n- Injecting a `Scope.REQUEST` service into a `SINGLETON` — container throws at resolve time.\n\n**Bootstrap / entry file**:\n- `bootstrap({ ... })` without `export const app = ...` → always export. HMR degrades to full restart and `createTestApp` loses the handle.\n- `bootstrap({ register: ... })` — that option doesn't exist. Use an inline plugin in `plugins`.\n\n**Middleware**:\n- Using `(ctx, next)` for global middleware in `bootstrap({ middlewares })` — global middleware uses raw Express `(req, res, next)`. Wrong signature throws \"Cannot read properties of undefined\".\n- Using `(req, res, next)` for an `@Middleware()` decorator — those use `(ctx, next)`.\n- `@Middleware()` whose only output is `ctx.set('x', v)` — should be a context decorator (typed, ordered, testable).\n\n**Context contributors**:\n- `ctx.tenant = x` from a contributor — only sticks to one `RequestContext` instance. **Return the value** so the runner writes it via `ctx.set(key, value)`.\n- Omitting the `declare module '@forinda/kickjs'` block — without it `ctx.get('tenant')` is `unknown`.\n- `getRequestValue<string>('traceId')` — generic is the **key** type, not value type.\n\n**Env / config**:\n- `@Value('NEW_KEY')` without the key in the Zod schema — silent fallback to raw `process.env`, no coercion.\n- `resetEnvCache()` outside tests — drops the registered schema.\n\n**List endpoints**:\n- Reading `req.query.status` directly — bypasses the allow-list. Use `ctx.qs({ filterable })`.\n- Returning a bare array from a list endpoint — breaks the `PaginatedResponse<T>` contract. Use `ctx.paginate()`.\n\n**Assets**:\n- Hand-rolled `__dirname` arithmetic for template paths — use `assets.<ns>.<key>()` and add the namespace to `kick.config.ts assetMap`."}]}function D(e,t,n,r=`define`){let i=`<!-- Generated by \`kick g agents\` for ${e}. Edits are overwritten on the next refresh; keep customisation in a SKILL.local.md alongside. -->`;return E(n,r).map(e=>({slug:e.slug,content:`---
801
850
  name: ${e.frontmatterName}
802
851
  description: ${e.description}
803
852
  ---
@@ -805,7 +854,7 @@ description: ${e.description}
805
854
  ${i}
806
855
 
807
856
  ${e.body}
808
- `}))}function w(e,t,n){return`# GEMINI.md — ${e}
857
+ `}))}function O(e,t,n){return`# GEMINI.md — ${e}
809
858
 
810
859
  **Read \`./AGENTS.md\` first.** It is the canonical, multi-agent
811
860
  reference for this project — every convention, structure, decorator
@@ -839,7 +888,7 @@ without us copy-pasting.
839
888
  \`kick g agents --only gemini -f\` regenerates this file from the
840
889
  CLI template. Hand-edited content is overwritten — keep customisation
841
890
  in \`.agents/GEMINI.local.md\`.
842
- `}function T(e,t,n){return`# COPILOT.md — ${e}
891
+ `}function k(e,t,n){return`# COPILOT.md — ${e}
843
892
 
844
893
  **Read \`./AGENTS.md\` first.** It is the canonical, multi-agent
845
894
  reference for this project — every convention, structure, decorator
@@ -872,4 +921,4 @@ Codex / Cursor / Gemini / Claude Code without copy-pasting.
872
921
  \`kick g agents --only copilot -f\` regenerates this file from the
873
922
  CLI template. Hand-edited content is overwritten — keep customisation
874
923
  in \`.agents/COPILOT.local.md\`.
875
- `}var E=/* @__PURE__ */ e({generateAgentDocs:()=>N});const D=`.agents`,O=/* @__PURE__ */ new Set([`rest`,`minimal`,`fullstack`,`inertia`]);function k(e,t){if(t)return t;try{let t=JSON.parse(i(a(e,`package.json`),`utf-8`));if(t.name)return t.name.replace(/^@[^/]+\//,``)}catch{}return e.split(`/`).findLast(Boolean)??`app`}function A(e,t){if(t)return t;try{let t=JSON.parse(i(a(e,`package.json`),`utf-8`));if(t.packageManager)return t.packageManager.split(`@`)[0]}catch{}return`pnpm`}async function j(e){try{return(await t(e))?.modules?.style===`class`?`class`:`define`}catch{return`define`}}async function M(e,n){if(n)return n;try{let n=(await t(e))?.pattern;if(n&&O.has(n))return n}catch{}return`rest`}async function N(e){let t=e.only??`all`,i=k(e.outDir,e.name),o=A(e.outDir,e.pm),s=await M(e.outDir,e.template),c=await j(e.outDir),l=t===`agents`||t===`both`||t===`all`,u=t===`claude`||t===`both`||t===`all`,d=t===`skills`||t===`all`,f=t===`gemini`||t===`all`,p=t===`copilot`||t===`all`,m=[];if(l&&m.push({file:a(e.outDir,D,`AGENTS.md`),render:()=>x(i,s,o)}),u&&m.push({file:a(e.outDir,`CLAUDE.md`),render:()=>b(i,s,o)}),d)for(let t of C(i,s,o,c))m.push({file:a(e.outDir,D,`skills`,t.slug,`SKILL.md`),render:()=>t.content});f&&m.push({file:a(e.outDir,D,`GEMINI.md`),render:()=>w(i,s,o)}),p&&m.push({file:a(e.outDir,D,`COPILOT.md`),render:()=>T(i,s,o)});let h=[];for(let{file:t,render:i}of m)r(t)&&!e.force&&!await _({message:`${t.replace(e.outDir+`/`,``)} already exists. Overwrite?`,initialValue:!1})?console.log(` Skipped — existing ${t.replace(e.outDir+`/`,``)} preserved.`):(await n(t,i()),h.push(t));return h}export{y as a,h as c,l as d,s as f,d as i,v as l,N as n,g as o,u as p,_ as r,f as s,E as t,m as u};
924
+ `}var A=/* @__PURE__ */ e({generateAgentDocs:()=>I});const j=`.agents`,M=/* @__PURE__ */ new Set([`rest`,`minimal`,`fullstack`,`inertia`]);function N(e,t){if(t)return t;try{let t=JSON.parse(s(c(e,`package.json`),`utf-8`));if(t.name)return t.name.replace(/^@[^/]+\//,``)}catch{}return e.split(`/`).findLast(Boolean)??`app`}async function P(e){try{return(await t(e))?.modules?.style===`class`?`class`:`define`}catch{return`define`}}async function F(e,n){if(n)return n;try{let n=(await t(e))?.pattern;if(n&&M.has(n))return n}catch{}return`rest`}async function I(e){let t=e.only??`all`,n=N(e.outDir,e.name),r=await i(e.pm,e.outDir),s=await F(e.outDir,e.template),l=await P(e.outDir),u=t===`agents`||t===`both`||t===`all`,d=t===`claude`||t===`both`||t===`all`,f=t===`skills`||t===`all`,p=t===`gemini`||t===`all`,m=t===`copilot`||t===`all`,h=[];if(u&&h.push({file:c(e.outDir,j,`AGENTS.md`),render:()=>T(n,s,r)}),d&&h.push({file:c(e.outDir,`CLAUDE.md`),render:()=>w(n,s,r)}),f)for(let t of D(n,s,r,l))h.push({file:c(e.outDir,j,`skills`,t.slug,`SKILL.md`),render:()=>t.content});p&&h.push({file:c(e.outDir,j,`GEMINI.md`),render:()=>O(n,s,r)}),m&&h.push({file:c(e.outDir,j,`COPILOT.md`),render:()=>k(n,s,r)});let g=[];for(let{file:t,render:n}of h)o(t)&&!e.force&&!await b({message:`${t.replace(e.outDir+`/`,``)} already exists. Overwrite?`,initialValue:!1})?console.log(` Skipped — existing ${t.replace(e.outDir+`/`,``)} preserved.`):(await a(t,n()),g.push(t));return g}export{S as a,v as c,f as d,u as f,m as i,x as l,I as n,y as o,p,b as r,h as s,A as t,_ as u};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v8.9.0
2
+ * @forinda/kickjs-cli v8.10.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,6 +8,6 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as e}from"./rolldown-runtime-Da-NDvKz.mjs";import{dirname as t,isAbsolute as n,join as r,relative as i,resolve as a}from"node:path";import{cpSync as o,existsSync as s,mkdirSync as c,statSync as l,writeFileSync as u}from"node:fs";import{glob as d}from"glob";import{groupAssetKeys as f}from"@forinda/kickjs";var p=/* @__PURE__ */ e({ASSET_MANIFEST_VERSION:()=>1,buildAssets:()=>m});async function m(e,t){let{cwd:n,silent:o=!1}=t,s=t.distDir??e?.build?.outDir??`dist`,l=e?.assetMap;if(!l||Object.keys(l).length===0)return null;let d=o?()=>{}:console.log,f=a(n,s);c(f,{recursive:!0});let p=[],m={};for(let[e,t]of Object.entries(l)){let r=await h(e,t,n,f);p.push(r.entrySummary),Object.assign(m,r.manifestSlice),d(` ✓ ${e}: ${r.entrySummary.filesCopied} file(s) → ${r.entrySummary.dest}`)}let g={version:1,entries:m},_=r(f,`.kickjs-assets.json`);return u(_,JSON.stringify(g,null,2)+`
11
+ import{t as e}from"./rolldown-runtime-D8pytGn4.mjs";import{dirname as t,isAbsolute as n,join as r,relative as i,resolve as a}from"node:path";import{cpSync as o,existsSync as s,mkdirSync as c,statSync as l,writeFileSync as u}from"node:fs";import{glob as d}from"glob";import{groupAssetKeys as f}from"@forinda/kickjs";var p=/* @__PURE__ */ e({ASSET_MANIFEST_VERSION:()=>1,buildAssets:()=>m});async function m(e,t){let{cwd:n,silent:o=!1}=t,s=t.distDir??e?.build?.outDir??`dist`,l=e?.assetMap;if(!l||Object.keys(l).length===0)return null;let d=o?()=>{}:console.log,f=a(n,s);c(f,{recursive:!0});let p=[],m={};for(let[e,t]of Object.entries(l)){let r=await h(e,t,n,f);p.push(r.entrySummary),Object.assign(m,r.manifestSlice),d(` ✓ ${e}: ${r.entrySummary.filesCopied} file(s) → ${r.entrySummary.dest}`)}let g={version:1,entries:m},_=r(f,`.kickjs-assets.json`);return u(_,JSON.stringify(g,null,2)+`
12
12
  `,`utf-8`),d(` ✓ wrote manifest → ${i(n,_)} (${Object.keys(m).length} entries)`),{manifestPath:_,entries:p,manifest:g}}async function h(e,n,l,u){let p=a(l,n.src),m=n.dest?a(l,n.dest):r(u,e);if(v(m,l))return console.warn(` ⚠ assetMap.${e}.dest ('${n.dest}') resolves outside the project root — skipping copy`),{entrySummary:{namespace:e,src:n.src,dest:i(l,m),filesCopied:0},manifestSlice:{}};if(!s(p)||!y(p))return{entrySummary:{namespace:e,src:n.src,dest:i(l,m),filesCopied:0},manifestSlice:{}};let h=n.glob??`**/*`,b=await d(h,{cwd:p,nodir:!0,dot:!1,posix:!0});c(m,{recursive:!0});let x={},S=[...b].toSorted(),{pairs:C,collisionGroupsResolved:w}=f(e,S,{strategy:n.keys??`auto`}),T=0;for(let{rel:e,key:n}of C){let i=r(p,e),a=r(m,e);x[n]=_(u,a),!g(i,a)&&(c(t(a),{recursive:!0}),o(i,a),T++)}return w>0&&console.log(` ℹ assetMap.${e}: auto-resolved ${w} basename collision(s) by keeping extensions (set 'keys: "strip"' to opt back into legacy last-write-wins behaviour, or 'keys: "with-extension"' to keep all keys verbose).`),{entrySummary:{namespace:e,src:n.src,dest:i(l,m),filesCopied:T},manifestSlice:x}}function g(e,t){if(!s(t))return!1;try{let n=l(e),r=l(t);return r.size===n.size&&r.mtimeMs>=n.mtimeMs}catch{return!1}}function _(e,t){return i(e,t).split(/[\\/]/).filter(Boolean).join(`/`)}function v(e,t){let r=i(t,e);return r===``?!1:r.startsWith(`..`)||n(r)}function y(e){try{return l(e).isDirectory()}catch{return!1}}export{p as n,m as t};
13
- //# sourceMappingURL=build-C30mBgw6.mjs.map
13
+ //# sourceMappingURL=build-D90mecBH.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-C30mBgw6.mjs","names":["__exportAll","build_exports","buildAssets","opts","distDir","config","map","log","silent","distAbs","resolve","cwd","mkdirSync","summary","manifestEntries","namespace","entry","result","processEntry","manifest","manifestPath","join","writeFileSync","relative","srcAbs","destAbs","escapesRoot","existsSync","isDirectorySync","pattern","matches","glob","manifestSlice","sorted","groupAssetKeys","filesCopied","relPath","pairs","srcFile","destFile","key","toManifestRelative","dirname","cpSync","collisionGroupsResolved","isUpToDate","s","statSync","d","manifestDir","rel","root","path","isAbsolute"],"sources":["../src/asset-manager/build.ts"],"sourcesContent":["/**\n * Build pipeline for `assetMap` entries (asset-manager PR 2).\n *\n * For each entry, walks `src/<...>` matching the configured `glob`,\n * copies matches into `dest` (default `dist/<name>/`), then emits a\n * `dist/.kickjs-assets.json` manifest mapping logical\n * `<namespace>/<key>` keys to repo-relative paths inside `dist/`.\n *\n * Pure function on top of `node:fs` + `glob` — no shell, no side\n * effects beyond the configured directory writes. The build entry-\n * point in `commands/run.ts` calls `buildAssets` after the existing\n * `copyDirs` step.\n *\n * @module @forinda/kickjs-cli/asset-manager/build\n */\n\nimport { cpSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path'\nimport { glob } from 'glob'\nimport { groupAssetKeys } from '@forinda/kickjs'\nimport type { AssetMapEntry, KickConfig } from '../config'\n\n/** Wire-format version for `dist/.kickjs-assets.json`. Bump on shape change. */\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\n/** On-disk manifest format (`dist/.kickjs-assets.json`). */\nexport interface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n /**\n * Logical key → manifest-relative path. Logical key is\n * `<namespace>/<key>` where `<key>` is the file path under `src`\n * with the extension stripped + path separators normalised.\n *\n * Path values are relative to the manifest file's directory so the\n * runtime can resolve them with a single `path.resolve(manifestDir,\n * entry)` regardless of where dist/ lives.\n */\n entries: Record<string, string>\n}\n\nexport interface BuildAssetsOptions {\n /** Project root — resolved for every relative path in the entry. */\n cwd: string\n /**\n * Output dir for the manifest + per-namespace asset copies. When\n * omitted, falls back to `config.build?.outDir` (resolved against\n * `cwd`), then to `dist/` under cwd. Adopters with a custom Vite\n * `build.outDir` should set `kick.config.ts.build.outDir` to match.\n */\n distDir?: string\n /** Suppress per-entry log lines. Default: false. */\n silent?: boolean\n}\n\n/** One entry in the per-build summary returned by `buildAssets`. */\nexport interface BuildAssetsEntryResult {\n namespace: string\n src: string\n dest: string\n /**\n * Number of files actually written this run. On an incremental\n * rebuild where nothing changed this is 0 even though the manifest\n * still lists every matched file.\n */\n filesCopied: number\n}\n\n/** Aggregated outcome of `buildAssets`. */\nexport interface BuildAssetsResult {\n manifestPath: string\n entries: BuildAssetsEntryResult[]\n /** `entries` merged into a single record — useful for tests + tooling. */\n manifest: AssetManifest\n}\n\n/**\n * Run the full asset build for a loaded config:\n *\n * 1. For each `assetMap` entry, glob → copy → manifest stub.\n * 2. Write `dist/.kickjs-assets.json`.\n *\n * Returns a summary including the manifest contents. No-op (and no\n * manifest written) when `assetMap` is empty / missing — the build\n * pipeline shouldn't litter `dist/` with empty manifests for\n * adopters who don't use the feature.\n */\nexport async function buildAssets(\n config: KickConfig | null,\n opts: BuildAssetsOptions,\n): Promise<BuildAssetsResult | null> {\n const { cwd, silent = false } = opts\n // Resolution order: explicit opts.distDir → config.build.outDir → 'dist'.\n // The CLI build command passes nothing explicit, so adopters control\n // the output via kick.config.ts.build.outDir alone.\n const distDir = opts.distDir ?? config?.build?.outDir ?? 'dist'\n const map = config?.assetMap\n if (!map || Object.keys(map).length === 0) return null\n\n const log = silent ? () => {} : console.log\n\n const distAbs = resolve(cwd, distDir)\n mkdirSync(distAbs, { recursive: true })\n\n const summary: BuildAssetsEntryResult[] = []\n const manifestEntries: Record<string, string> = {}\n\n for (const [namespace, entry] of Object.entries(map)) {\n const result = await processEntry(namespace, entry, cwd, distAbs)\n summary.push(result.entrySummary)\n Object.assign(manifestEntries, result.manifestSlice)\n log(\n ` ✓ ${namespace}: ${result.entrySummary.filesCopied} file(s) → ${result.entrySummary.dest}`,\n )\n }\n\n const manifest: AssetManifest = {\n version: ASSET_MANIFEST_VERSION,\n entries: manifestEntries,\n }\n const manifestPath = join(distAbs, '.kickjs-assets.json')\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n log(\n ` ✓ wrote manifest → ${relative(cwd, manifestPath)} (${Object.keys(manifestEntries).length} entries)`,\n )\n\n return { manifestPath, entries: summary, manifest }\n}\n\n/** Per-entry inner pipeline — extracted for unit-test reuse. */\nasync function processEntry(\n namespace: string,\n entry: AssetMapEntry,\n cwd: string,\n distAbs: string,\n): Promise<{\n entrySummary: BuildAssetsEntryResult\n manifestSlice: Record<string, string>\n}> {\n const srcAbs = resolve(cwd, entry.src)\n const destAbs = entry.dest ? resolve(cwd, entry.dest) : join(distAbs, namespace)\n\n // Defensive: refuse to write outside the project root (cwd) even\n // though validateAssetMap warned about it at config-load time. The\n // build step shouldn't trust upstream warnings — a typo like\n // `dest: '../../'` would otherwise sprinkle files outside the\n // workspace despite the warning being printed.\n if (escapesRoot(destAbs, cwd)) {\n console.warn(\n ` ⚠ assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — skipping copy`,\n )\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n // Treat src-not-a-directory the same as src-missing — `glob` would\n // throw if pointed at a file, surfacing as a generic build failure\n // instead of a clean 0-files entry. Matches the validator's warning\n // shape (already emitted at config-load time for the missing case).\n if (!existsSync(srcAbs) || !isDirectorySync(srcAbs)) {\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n const pattern = entry.glob ?? '**/*'\n // `glob` returns paths relative to `cwd` when `cwd` is set —\n // exactly the slugs we want for the manifest keys.\n const matches = await glob(pattern, {\n cwd: srcAbs,\n nodir: true,\n dot: false,\n posix: true,\n })\n\n mkdirSync(destAbs, { recursive: true })\n\n const manifestSlice: Record<string, string> = {}\n // Sort the walk so the manifest is byte-stable across platforms.\n // The `groupAssetKeys` helper preserves input order, so a stable\n // sort here makes the resulting manifest stable too.\n const sorted = [...matches].toSorted()\n const { pairs, collisionGroupsResolved } = groupAssetKeys(namespace, sorted, {\n strategy: entry.keys ?? 'auto',\n })\n\n // Copy the files and write the keyed manifest slice. Both come from\n // the same `pairs` order so the on-disk layout matches the\n // manifest's iteration order — easier to grep in cold-start\n // debugging.\n //\n // Incremental: skip the copy when the destination is already\n // up-to-date (exists, same size, mtime ≥ source). `cpSync` stamps the\n // copy with the current time, so an unchanged source always satisfies\n // `dest.mtime ≥ src.mtime` on the next run — turning a re-build into a\n // pure stat sweep instead of re-copying every asset. The manifest\n // slice is written unconditionally so the manifest stays complete.\n let filesCopied = 0\n for (const { rel: relPath, key } of pairs) {\n const srcFile = join(srcAbs, relPath)\n const destFile = join(destAbs, relPath)\n manifestSlice[key] = toManifestRelative(distAbs, destFile)\n if (isUpToDate(srcFile, destFile)) continue\n mkdirSync(dirname(destFile), { recursive: true })\n cpSync(srcFile, destFile)\n filesCopied++\n }\n\n if (collisionGroupsResolved > 0) {\n console.log(\n ` ℹ assetMap.${namespace}: auto-resolved ${collisionGroupsResolved} basename collision(s) by keeping extensions ` +\n `(set 'keys: \"strip\"' to opt back into legacy last-write-wins behaviour, or 'keys: \"with-extension\"' to keep all keys verbose).`,\n )\n }\n\n return {\n entrySummary: {\n namespace,\n src: entry.src,\n dest: relative(cwd, destAbs),\n filesCopied,\n },\n manifestSlice,\n }\n}\n\n/**\n * Is `destFile` already a current copy of `srcFile`? True when the\n * destination exists with the same byte size and an mtime no older than\n * the source. Used to skip redundant copies on an incremental rebuild.\n */\nfunction isUpToDate(srcFile: string, destFile: string): boolean {\n if (!existsSync(destFile)) return false\n try {\n const s = statSync(srcFile)\n const d = statSync(destFile)\n return d.size === s.size && d.mtimeMs >= s.mtimeMs\n } catch {\n return false\n }\n}\n\n/**\n * Make `destFile` relative to the manifest's directory + force POSIX\n * separators so the manifest is byte-stable across platforms.\n */\nfunction toManifestRelative(manifestDir: string, destFile: string): string {\n const rel = relative(manifestDir, destFile)\n // path.relative returns OS-native separators; the manifest is JSON\n // and the runtime uses path.resolve which handles either, but a\n // forward-slash manifest is grep-friendly + diff-stable.\n return rel.split(/[\\\\/]/).filter(Boolean).join('/')\n}\n\n/**\n * Pure manifest writer — handy for tests that want to assert against\n * a hand-crafted manifest without exercising the full pipeline.\n */\nexport function writeAssetManifest(distDir: string, manifest: AssetManifest): string {\n const path = join(distDir, '.kickjs-assets.json')\n mkdirSync(distDir, { recursive: true })\n writeFileSync(path, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n return path\n}\n\n/**\n * Read + parse a manifest from disk. Returns `null` on missing or\n * malformed file rather than throwing — the runtime resolver wants\n * to fall through to dev-mode lookup in that case.\n */\nexport function readAssetManifest(distDir: string): AssetManifest | null {\n const path = join(distDir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require('node:fs') as typeof import('node:fs')\n const raw = fs.readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return parsed as AssetManifest\n } catch {\n return null\n }\n}\n\n/**\n * Project-root escape check that's safe across symlinks + drive letters.\n * `path.relative` returns `..` segments when the target sits above root,\n * and an absolute path when the two live on different roots (Windows).\n * `startsWith(root)` would miss both cases.\n */\nfunction escapesRoot(path: string, root: string): boolean {\n const rel = relative(root, path)\n if (rel === '') return false\n return rel.startsWith('..') || isAbsolute(rel)\n}\n\n/** Pure helper — `false` for missing, non-dir, or unreadable paths. */\nfunction isDirectorySync(path: string): boolean {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build-D90mecBH.mjs","names":["__exportAll","build_exports","buildAssets","opts","distDir","config","map","log","silent","distAbs","resolve","cwd","mkdirSync","summary","manifestEntries","namespace","entry","result","processEntry","manifest","manifestPath","join","writeFileSync","relative","srcAbs","destAbs","escapesRoot","existsSync","isDirectorySync","pattern","matches","glob","manifestSlice","sorted","groupAssetKeys","filesCopied","relPath","pairs","srcFile","destFile","key","toManifestRelative","dirname","cpSync","collisionGroupsResolved","isUpToDate","s","statSync","d","manifestDir","rel","root","path","isAbsolute"],"sources":["../src/asset-manager/build.ts"],"sourcesContent":["/**\n * Build pipeline for `assetMap` entries (asset-manager PR 2).\n *\n * For each entry, walks `src/<...>` matching the configured `glob`,\n * copies matches into `dest` (default `dist/<name>/`), then emits a\n * `dist/.kickjs-assets.json` manifest mapping logical\n * `<namespace>/<key>` keys to repo-relative paths inside `dist/`.\n *\n * Pure function on top of `node:fs` + `glob` — no shell, no side\n * effects beyond the configured directory writes. The build entry-\n * point in `commands/run.ts` calls `buildAssets` after the existing\n * `copyDirs` step.\n *\n * @module @forinda/kickjs-cli/asset-manager/build\n */\n\nimport { cpSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path'\nimport { glob } from 'glob'\nimport { groupAssetKeys } from '@forinda/kickjs'\nimport type { AssetMapEntry, KickConfig } from '../config'\n\n/** Wire-format version for `dist/.kickjs-assets.json`. Bump on shape change. */\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\n/** On-disk manifest format (`dist/.kickjs-assets.json`). */\nexport interface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n /**\n * Logical key → manifest-relative path. Logical key is\n * `<namespace>/<key>` where `<key>` is the file path under `src`\n * with the extension stripped + path separators normalised.\n *\n * Path values are relative to the manifest file's directory so the\n * runtime can resolve them with a single `path.resolve(manifestDir,\n * entry)` regardless of where dist/ lives.\n */\n entries: Record<string, string>\n}\n\nexport interface BuildAssetsOptions {\n /** Project root — resolved for every relative path in the entry. */\n cwd: string\n /**\n * Output dir for the manifest + per-namespace asset copies. When\n * omitted, falls back to `config.build?.outDir` (resolved against\n * `cwd`), then to `dist/` under cwd. Adopters with a custom Vite\n * `build.outDir` should set `kick.config.ts.build.outDir` to match.\n */\n distDir?: string\n /** Suppress per-entry log lines. Default: false. */\n silent?: boolean\n}\n\n/** One entry in the per-build summary returned by `buildAssets`. */\nexport interface BuildAssetsEntryResult {\n namespace: string\n src: string\n dest: string\n /**\n * Number of files actually written this run. On an incremental\n * rebuild where nothing changed this is 0 even though the manifest\n * still lists every matched file.\n */\n filesCopied: number\n}\n\n/** Aggregated outcome of `buildAssets`. */\nexport interface BuildAssetsResult {\n manifestPath: string\n entries: BuildAssetsEntryResult[]\n /** `entries` merged into a single record — useful for tests + tooling. */\n manifest: AssetManifest\n}\n\n/**\n * Run the full asset build for a loaded config:\n *\n * 1. For each `assetMap` entry, glob → copy → manifest stub.\n * 2. Write `dist/.kickjs-assets.json`.\n *\n * Returns a summary including the manifest contents. No-op (and no\n * manifest written) when `assetMap` is empty / missing — the build\n * pipeline shouldn't litter `dist/` with empty manifests for\n * adopters who don't use the feature.\n */\nexport async function buildAssets(\n config: KickConfig | null,\n opts: BuildAssetsOptions,\n): Promise<BuildAssetsResult | null> {\n const { cwd, silent = false } = opts\n // Resolution order: explicit opts.distDir → config.build.outDir → 'dist'.\n // The CLI build command passes nothing explicit, so adopters control\n // the output via kick.config.ts.build.outDir alone.\n const distDir = opts.distDir ?? config?.build?.outDir ?? 'dist'\n const map = config?.assetMap\n if (!map || Object.keys(map).length === 0) return null\n\n const log = silent ? () => {} : console.log\n\n const distAbs = resolve(cwd, distDir)\n mkdirSync(distAbs, { recursive: true })\n\n const summary: BuildAssetsEntryResult[] = []\n const manifestEntries: Record<string, string> = {}\n\n for (const [namespace, entry] of Object.entries(map)) {\n const result = await processEntry(namespace, entry, cwd, distAbs)\n summary.push(result.entrySummary)\n Object.assign(manifestEntries, result.manifestSlice)\n log(\n ` ✓ ${namespace}: ${result.entrySummary.filesCopied} file(s) → ${result.entrySummary.dest}`,\n )\n }\n\n const manifest: AssetManifest = {\n version: ASSET_MANIFEST_VERSION,\n entries: manifestEntries,\n }\n const manifestPath = join(distAbs, '.kickjs-assets.json')\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n log(\n ` ✓ wrote manifest → ${relative(cwd, manifestPath)} (${Object.keys(manifestEntries).length} entries)`,\n )\n\n return { manifestPath, entries: summary, manifest }\n}\n\n/** Per-entry inner pipeline — extracted for unit-test reuse. */\nasync function processEntry(\n namespace: string,\n entry: AssetMapEntry,\n cwd: string,\n distAbs: string,\n): Promise<{\n entrySummary: BuildAssetsEntryResult\n manifestSlice: Record<string, string>\n}> {\n const srcAbs = resolve(cwd, entry.src)\n const destAbs = entry.dest ? resolve(cwd, entry.dest) : join(distAbs, namespace)\n\n // Defensive: refuse to write outside the project root (cwd) even\n // though validateAssetMap warned about it at config-load time. The\n // build step shouldn't trust upstream warnings — a typo like\n // `dest: '../../'` would otherwise sprinkle files outside the\n // workspace despite the warning being printed.\n if (escapesRoot(destAbs, cwd)) {\n console.warn(\n ` ⚠ assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — skipping copy`,\n )\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n // Treat src-not-a-directory the same as src-missing — `glob` would\n // throw if pointed at a file, surfacing as a generic build failure\n // instead of a clean 0-files entry. Matches the validator's warning\n // shape (already emitted at config-load time for the missing case).\n if (!existsSync(srcAbs) || !isDirectorySync(srcAbs)) {\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n const pattern = entry.glob ?? '**/*'\n // `glob` returns paths relative to `cwd` when `cwd` is set —\n // exactly the slugs we want for the manifest keys.\n const matches = await glob(pattern, {\n cwd: srcAbs,\n nodir: true,\n dot: false,\n posix: true,\n })\n\n mkdirSync(destAbs, { recursive: true })\n\n const manifestSlice: Record<string, string> = {}\n // Sort the walk so the manifest is byte-stable across platforms.\n // The `groupAssetKeys` helper preserves input order, so a stable\n // sort here makes the resulting manifest stable too.\n const sorted = [...matches].toSorted()\n const { pairs, collisionGroupsResolved } = groupAssetKeys(namespace, sorted, {\n strategy: entry.keys ?? 'auto',\n })\n\n // Copy the files and write the keyed manifest slice. Both come from\n // the same `pairs` order so the on-disk layout matches the\n // manifest's iteration order — easier to grep in cold-start\n // debugging.\n //\n // Incremental: skip the copy when the destination is already\n // up-to-date (exists, same size, mtime ≥ source). `cpSync` stamps the\n // copy with the current time, so an unchanged source always satisfies\n // `dest.mtime ≥ src.mtime` on the next run — turning a re-build into a\n // pure stat sweep instead of re-copying every asset. The manifest\n // slice is written unconditionally so the manifest stays complete.\n let filesCopied = 0\n for (const { rel: relPath, key } of pairs) {\n const srcFile = join(srcAbs, relPath)\n const destFile = join(destAbs, relPath)\n manifestSlice[key] = toManifestRelative(distAbs, destFile)\n if (isUpToDate(srcFile, destFile)) continue\n mkdirSync(dirname(destFile), { recursive: true })\n cpSync(srcFile, destFile)\n filesCopied++\n }\n\n if (collisionGroupsResolved > 0) {\n console.log(\n ` ℹ assetMap.${namespace}: auto-resolved ${collisionGroupsResolved} basename collision(s) by keeping extensions ` +\n `(set 'keys: \"strip\"' to opt back into legacy last-write-wins behaviour, or 'keys: \"with-extension\"' to keep all keys verbose).`,\n )\n }\n\n return {\n entrySummary: {\n namespace,\n src: entry.src,\n dest: relative(cwd, destAbs),\n filesCopied,\n },\n manifestSlice,\n }\n}\n\n/**\n * Is `destFile` already a current copy of `srcFile`? True when the\n * destination exists with the same byte size and an mtime no older than\n * the source. Used to skip redundant copies on an incremental rebuild.\n */\nfunction isUpToDate(srcFile: string, destFile: string): boolean {\n if (!existsSync(destFile)) return false\n try {\n const s = statSync(srcFile)\n const d = statSync(destFile)\n return d.size === s.size && d.mtimeMs >= s.mtimeMs\n } catch {\n return false\n }\n}\n\n/**\n * Make `destFile` relative to the manifest's directory + force POSIX\n * separators so the manifest is byte-stable across platforms.\n */\nfunction toManifestRelative(manifestDir: string, destFile: string): string {\n const rel = relative(manifestDir, destFile)\n // path.relative returns OS-native separators; the manifest is JSON\n // and the runtime uses path.resolve which handles either, but a\n // forward-slash manifest is grep-friendly + diff-stable.\n return rel.split(/[\\\\/]/).filter(Boolean).join('/')\n}\n\n/**\n * Pure manifest writer — handy for tests that want to assert against\n * a hand-crafted manifest without exercising the full pipeline.\n */\nexport function writeAssetManifest(distDir: string, manifest: AssetManifest): string {\n const path = join(distDir, '.kickjs-assets.json')\n mkdirSync(distDir, { recursive: true })\n writeFileSync(path, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n return path\n}\n\n/**\n * Read + parse a manifest from disk. Returns `null` on missing or\n * malformed file rather than throwing — the runtime resolver wants\n * to fall through to dev-mode lookup in that case.\n */\nexport function readAssetManifest(distDir: string): AssetManifest | null {\n const path = join(distDir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require('node:fs') as typeof import('node:fs')\n const raw = fs.readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return parsed as AssetManifest\n } catch {\n return null\n }\n}\n\n/**\n * Project-root escape check that's safe across symlinks + drive letters.\n * `path.relative` returns `..` segments when the target sits above root,\n * and an absolute path when the two live on different roots (Windows).\n * `startsWith(root)` would miss both cases.\n */\nfunction escapesRoot(path: string, root: string): boolean {\n const rel = relative(root, path)\n if (rel === '') return false\n return rel.startsWith('..') || isAbsolute(rel)\n}\n\n/** Pure helper — `false` for missing, non-dir, or unreadable paths. */\nfunction isDirectorySync(path: string): boolean {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v8.9.0
2
+ * @forinda/kickjs-cli v8.10.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as e}from"./rolldown-runtime-Da-NDvKz.mjs";import{cpSync as t,existsSync as n,mkdirSync as r,statSync as i,writeFileSync as a}from"node:fs";import{dirname as o,isAbsolute as s,join as c,relative as l,resolve as u}from"node:path";import{glob as d}from"glob";import{groupAssetKeys as f}from"@forinda/kickjs";var p=/* @__PURE__ */ e({ASSET_MANIFEST_VERSION:()=>1,buildAssets:()=>m});async function m(e,t){let{cwd:n,silent:i=!1}=t,o=t.distDir??e?.build?.outDir??`dist`,s=e?.assetMap;if(!s||Object.keys(s).length===0)return null;let d=i?()=>{}:console.log,f=u(n,o);r(f,{recursive:!0});let p=[],m={};for(let[e,t]of Object.entries(s)){let r=await h(e,t,n,f);p.push(r.entrySummary),Object.assign(m,r.manifestSlice),d(` ✓ ${e}: ${r.entrySummary.filesCopied} file(s) → ${r.entrySummary.dest}`)}let g={version:1,entries:m},_=c(f,`.kickjs-assets.json`);return a(_,JSON.stringify(g,null,2)+`
11
+ import{t as e}from"./rolldown-runtime-D8pytGn4.mjs";import{cpSync as t,existsSync as n,mkdirSync as r,statSync as i,writeFileSync as a}from"node:fs";import{dirname as o,isAbsolute as s,join as c,relative as l,resolve as u}from"node:path";import{glob as d}from"glob";import{groupAssetKeys as f}from"@forinda/kickjs";var p=/* @__PURE__ */ e({ASSET_MANIFEST_VERSION:()=>1,buildAssets:()=>m});async function m(e,t){let{cwd:n,silent:i=!1}=t,o=t.distDir??e?.build?.outDir??`dist`,s=e?.assetMap;if(!s||Object.keys(s).length===0)return null;let d=i?()=>{}:console.log,f=u(n,o);r(f,{recursive:!0});let p=[],m={};for(let[e,t]of Object.entries(s)){let r=await h(e,t,n,f);p.push(r.entrySummary),Object.assign(m,r.manifestSlice),d(` ✓ ${e}: ${r.entrySummary.filesCopied} file(s) → ${r.entrySummary.dest}`)}let g={version:1,entries:m},_=c(f,`.kickjs-assets.json`);return a(_,JSON.stringify(g,null,2)+`
12
12
  `,`utf-8`),d(` ✓ wrote manifest → ${l(n,_)} (${Object.keys(m).length} entries)`),{manifestPath:_,entries:p,manifest:g}}async function h(e,i,a,s){let p=u(a,i.src),m=i.dest?u(a,i.dest):c(s,e);if(v(m,a))return console.warn(` ⚠ assetMap.${e}.dest ('${i.dest}') resolves outside the project root — skipping copy`),{entrySummary:{namespace:e,src:i.src,dest:l(a,m),filesCopied:0},manifestSlice:{}};if(!n(p)||!y(p))return{entrySummary:{namespace:e,src:i.src,dest:l(a,m),filesCopied:0},manifestSlice:{}};let h=i.glob??`**/*`,b=await d(h,{cwd:p,nodir:!0,dot:!1,posix:!0});r(m,{recursive:!0});let x={},S=[...b].toSorted(),{pairs:C,collisionGroupsResolved:w}=f(e,S,{strategy:i.keys??`auto`}),T=0;for(let{rel:e,key:n}of C){let i=c(p,e),a=c(m,e);x[n]=_(s,a),!g(i,a)&&(r(o(a),{recursive:!0}),t(i,a),T++)}return w>0&&console.log(` ℹ assetMap.${e}: auto-resolved ${w} basename collision(s) by keeping extensions (set 'keys: "strip"' to opt back into legacy last-write-wins behaviour, or 'keys: "with-extension"' to keep all keys verbose).`),{entrySummary:{namespace:e,src:i.src,dest:l(a,m),filesCopied:T},manifestSlice:x}}function g(e,t){if(!n(t))return!1;try{let n=i(e),r=i(t);return r.size===n.size&&r.mtimeMs>=n.mtimeMs}catch{return!1}}function _(e,t){return l(e,t).split(/[\\/]/).filter(Boolean).join(`/`)}function v(e,t){let n=l(t,e);return n===``?!1:n.startsWith(`..`)||s(n)}function y(e){try{return i(e).isDirectory()}catch{return!1}}export{p as n,m as t};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v8.9.0
2
+ * @forinda/kickjs-cli v8.10.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,4 +8,4 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- export{t as builtinCliPlugins}from"./run-plugins-0AfIgIsZ.mjs";
11
+ export{t as builtinCliPlugins}from"./run-plugins-CeZS2Opt.mjs";