@agimon-ai/doompi-build 0.0.1-alpha.10

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 (103) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +82 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/browserFile.cjs +1 -0
  5. package/dist/browserFile.d.mts +2 -0
  6. package/dist/browserFile.mjs +1 -0
  7. package/dist/constants/contributions.cjs +2 -0
  8. package/dist/constants/contributions.cjs.map +1 -0
  9. package/dist/constants/contributions.mjs +2 -0
  10. package/dist/constants/contributions.mjs.map +1 -0
  11. package/dist/constants/layout.cjs +2 -0
  12. package/dist/constants/layout.cjs.map +1 -0
  13. package/dist/constants/layout.d.mts +59 -0
  14. package/dist/constants/layout.d.mts.map +1 -0
  15. package/dist/constants/layout.mjs +2 -0
  16. package/dist/constants/layout.mjs.map +1 -0
  17. package/dist/index.cjs +1 -0
  18. package/dist/index.d.mts +16 -0
  19. package/dist/index.mjs +1 -0
  20. package/dist/services/browserFile/index.cjs +2 -0
  21. package/dist/services/browserFile/index.cjs.map +1 -0
  22. package/dist/services/browserFile/index.d.mts +28 -0
  23. package/dist/services/browserFile/index.d.mts.map +1 -0
  24. package/dist/services/browserFile/index.mjs +2 -0
  25. package/dist/services/browserFile/index.mjs.map +1 -0
  26. package/dist/services/filename/index.cjs +2 -0
  27. package/dist/services/filename/index.cjs.map +1 -0
  28. package/dist/services/filename/index.d.mts +29 -0
  29. package/dist/services/filename/index.d.mts.map +1 -0
  30. package/dist/services/filename/index.mjs +2 -0
  31. package/dist/services/filename/index.mjs.map +1 -0
  32. package/dist/services/generate/index.cjs +2 -0
  33. package/dist/services/generate/index.cjs.map +1 -0
  34. package/dist/services/generate/index.d.mts +15 -0
  35. package/dist/services/generate/index.d.mts.map +1 -0
  36. package/dist/services/generate/index.mjs +2 -0
  37. package/dist/services/generate/index.mjs.map +1 -0
  38. package/dist/services/generate/type.d.mts +28 -0
  39. package/dist/services/generate/type.d.mts.map +1 -0
  40. package/dist/services/identity/index.cjs +2 -0
  41. package/dist/services/identity/index.cjs.map +1 -0
  42. package/dist/services/identity/index.mjs +2 -0
  43. package/dist/services/identity/index.mjs.map +1 -0
  44. package/dist/services/renderClient/index.cjs +3 -0
  45. package/dist/services/renderClient/index.cjs.map +1 -0
  46. package/dist/services/renderClient/index.mjs +3 -0
  47. package/dist/services/renderClient/index.mjs.map +1 -0
  48. package/dist/services/renderEntry/index.cjs +7 -0
  49. package/dist/services/renderEntry/index.cjs.map +1 -0
  50. package/dist/services/renderEntry/index.d.mts +24 -0
  51. package/dist/services/renderEntry/index.d.mts.map +1 -0
  52. package/dist/services/renderEntry/index.mjs +7 -0
  53. package/dist/services/renderEntry/index.mjs.map +1 -0
  54. package/dist/services/renderEntry/type.d.mts +14 -0
  55. package/dist/services/renderEntry/type.d.mts.map +1 -0
  56. package/dist/services/resolveTarget/index.cjs +2 -0
  57. package/dist/services/resolveTarget/index.cjs.map +1 -0
  58. package/dist/services/resolveTarget/index.d.mts +16 -0
  59. package/dist/services/resolveTarget/index.d.mts.map +1 -0
  60. package/dist/services/resolveTarget/index.mjs +2 -0
  61. package/dist/services/resolveTarget/index.mjs.map +1 -0
  62. package/dist/services/resolveTarget/type.d.mts +34 -0
  63. package/dist/services/resolveTarget/type.d.mts.map +1 -0
  64. package/dist/services/scan/index.cjs +2 -0
  65. package/dist/services/scan/index.cjs.map +1 -0
  66. package/dist/services/scan/index.d.mts +19 -0
  67. package/dist/services/scan/index.d.mts.map +1 -0
  68. package/dist/services/scan/index.mjs +2 -0
  69. package/dist/services/scan/index.mjs.map +1 -0
  70. package/dist/services/scan/type.d.mts +25 -0
  71. package/dist/services/scan/type.d.mts.map +1 -0
  72. package/dist/services/segments/index.cjs +2 -0
  73. package/dist/services/segments/index.cjs.map +1 -0
  74. package/dist/services/segments/index.d.mts +26 -0
  75. package/dist/services/segments/index.d.mts.map +1 -0
  76. package/dist/services/segments/index.mjs +2 -0
  77. package/dist/services/segments/index.mjs.map +1 -0
  78. package/dist/services/syncManifest/index.cjs +2 -0
  79. package/dist/services/syncManifest/index.cjs.map +1 -0
  80. package/dist/services/syncManifest/index.d.mts +21 -0
  81. package/dist/services/syncManifest/index.d.mts.map +1 -0
  82. package/dist/services/syncManifest/index.mjs +2 -0
  83. package/dist/services/syncManifest/index.mjs.map +1 -0
  84. package/dist/services/syncManifest/type.d.mts +15 -0
  85. package/dist/services/syncManifest/type.d.mts.map +1 -0
  86. package/dist/services/tsdownPreset/index.cjs +2 -0
  87. package/dist/services/tsdownPreset/index.cjs.map +1 -0
  88. package/dist/services/tsdownPreset/index.d.mts +82 -0
  89. package/dist/services/tsdownPreset/index.d.mts.map +1 -0
  90. package/dist/services/tsdownPreset/index.mjs +2 -0
  91. package/dist/services/tsdownPreset/index.mjs.map +1 -0
  92. package/dist/services/writeGenerated/index.cjs +2 -0
  93. package/dist/services/writeGenerated/index.cjs.map +1 -0
  94. package/dist/services/writeGenerated/index.d.mts +23 -0
  95. package/dist/services/writeGenerated/index.d.mts.map +1 -0
  96. package/dist/services/writeGenerated/index.mjs +2 -0
  97. package/dist/services/writeGenerated/index.mjs.map +1 -0
  98. package/dist/tsdown.cjs +1 -0
  99. package/dist/tsdown.d.mts +2 -0
  100. package/dist/tsdown.mjs +1 -0
  101. package/dist/types/extensionGraph.d.mts +75 -0
  102. package/dist/types/extensionGraph.d.mts.map +1 -0
  103. package/package.json +72 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["BACKEND_SURFACES","FRONTEND_SURFACES","BACKEND_PLATFORMS","FRONTEND_PLATFORMS","DEFAULT_ROUTING_ROOT","ROUTING_ROOT_ALIAS","fs","path","GENERATED_ENTRY_NAMES","classifySegment","ROUTED_SURFACES","GATE_SEGMENTS","parseFilename","ROOT_FILE_NAME"],"sources":["../../../src/services/scan/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n BACKEND_GROUP,\n BACKEND_PLATFORMS,\n BACKEND_SURFACES,\n DEFAULT_ROUTING_ROOT,\n FRONTEND_GROUP,\n FRONTEND_PLATFORMS,\n FRONTEND_SURFACES,\n GATE_SEGMENTS,\n GENERATED_ENTRY_NAMES,\n ROOT_FILE_NAME,\n ROUTE_FILE_NAME,\n ROUTED_SURFACES,\n ROUTING_ROOT_ALIAS,\n SESSION_SEGMENT,\n WORKSPACE_SEGMENT,\n} from '../../constants/layout';\nimport type {\n ExtensionEntry,\n ExtensionGate,\n ExtensionGraph,\n ExtensionNotice,\n ExtensionScope,\n ExtensionSide,\n RouteSegment,\n} from '../../types/extensionGraph';\nimport { type ParsedFilename, parseFilename } from '../filename';\nimport { classifySegment } from '../segments';\nimport type { ScanOptions } from './type';\n\ninterface WalkState {\n readonly scope: ExtensionScope;\n readonly side: ExtensionSide | undefined;\n readonly gates: readonly ExtensionGate[];\n readonly surface: string | undefined;\n readonly route: readonly RouteSegment[];\n /** A gate folder has been entered and the next plain segment names its id. */\n readonly pendingGate: string | undefined;\n}\n\nconst INITIAL: WalkState = {\n scope: 'global',\n side: undefined,\n gates: [],\n surface: undefined,\n route: [],\n pendingGate: undefined,\n};\n\nfunction surfacesFor(side: ExtensionSide): readonly string[] {\n return side === 'backend' ? BACKEND_SURFACES : FRONTEND_SURFACES;\n}\n\nfunction platformsFor(side: ExtensionSide, options: ScanOptions): readonly string[] {\n const configured = side === 'backend' ? options.platforms?.backend : options.platforms?.frontend;\n return configured ?? (side === 'backend' ? BACKEND_PLATFORMS : FRONTEND_PLATFORMS);\n}\n\n/** The nearest surface a misspelled folder probably meant, or undefined. */\nfunction nearestSurface(name: string, side: ExtensionSide): string | undefined {\n const surfaces = surfacesFor(side);\n if (surfaces.includes(name)) return undefined;\n const singular = name.endsWith('s') ? name.slice(0, -1) : `${name}s`;\n return surfaces.includes(singular) ? singular : undefined;\n}\n\n/** Reads cardinality only from the routed helper's options argument. */\nfunction routedCardinality(source: string): ExtensionEntry['cardinality'] | undefined {\n const declaration =\n /export\\s+default\\s+defineRoutedContribution\\s*\\([\\s\\S]*,\\s*\\{\\s*cardinality\\s*:\\s*['\"](optional|many|collection)['\"]/u.exec(\n source,\n );\n return declaration?.[1] as ExtensionEntry['cardinality'] | undefined;\n}\n\n/** Resolves the routing root, accepting the singular alias when only it exists. */\nexport function resolveRoutingRoot(packageDir: string, root: string | undefined): string | undefined {\n const candidates = root !== undefined ? [root] : [DEFAULT_ROUTING_ROOT, ROUTING_ROOT_ALIAS];\n return candidates.find((candidate) => fs.existsSync(path.join(packageDir, candidate)));\n}\n\n/**\n * Reads one package's routing root into a description of what it declares.\n *\n * Filesystem in, data out. No globbing, which is what keeps parenthesised\n * group folders safe: tsdown's globber would read a bare `(name)` as an\n * extglob group, and a directory walk never sees a pattern at all.\n *\n * Nothing here throws. A folder the convention does not recognise becomes a\n * notice and its subtree is skipped, so one bad directory never costs a\n * package its other contributions.\n */\nexport function scanExtensions(options: ScanOptions): ExtensionGraph {\n const root = resolveRoutingRoot(options.packageDir, options.root);\n if (root === undefined) return { root: options.root ?? DEFAULT_ROUTING_ROOT, entries: [], notices: [] };\n\n const backendGroup = options.sides?.backend ?? BACKEND_GROUP;\n const frontendGroup = options.sides?.frontend ?? FRONTEND_GROUP;\n const generated = options.generatedEntries ?? GENERATED_ENTRY_NAMES;\n\n const entries: ExtensionEntry[] = [];\n const notices: ExtensionNotice[] = [];\n const note = (relativePath: string, message: string): void => void notices.push({ path: relativePath, message });\n\n /**\n * Every public routed file names its platform, so a host is never inferred.\n *\n * Checked at each push rather than once at the top of readFile, because a\n * file sitting somewhere the convention does not define has a position\n * problem, and reporting the missing suffix instead would hide it.\n */\n const notePlatform = (relative: string, parsed: ParsedFilename, side: ExtensionSide): void => {\n if (parsed.platform !== undefined) return;\n note(\n relative,\n `names no platform; add one of ${platformsFor(side, options).join(', ')} before .${parsed.extension} (without one the host is inferred from the side rather than declared)`,\n );\n };\n\n const walk = (relativeDir: string, state: WalkState): void => {\n const absolute = path.join(options.packageDir, relativeDir);\n const listing = fs.readdirSync(absolute, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));\n\n for (const item of listing) {\n const relative = `${relativeDir}/${item.name}`;\n if (item.isDirectory()) walkDirectory(relative, item.name, state);\n else if (item.isFile()) readFile(relative, item.name, state);\n }\n };\n\n const walkDirectory = (relative: string, name: string, state: WalkState): void => {\n const segment = classifySegment(name);\n\n if (segment.kind === 'private') return;\n\n if (segment.kind === 'group') {\n if (state.side === undefined && segment.name === backendGroup)\n return walk(relative, { ...state, side: 'backend' });\n if (state.side === undefined && segment.name === frontendGroup)\n return walk(relative, { ...state, side: 'frontend' });\n return walk(relative, state);\n }\n\n if (segment.kind === 'dynamic') {\n if (state.surface !== undefined && ROUTED_SURFACES.includes(state.surface)) {\n return walk(relative, { ...state, route: [...state.route, { param: segment.param }] });\n }\n return note(relative, 'a dynamic segment is only meaningful inside a routed surface such as api/');\n }\n\n if (state.pendingGate !== undefined) {\n const gate: ExtensionGate = { kind: state.pendingGate, id: segment.name };\n return walk(relative, { ...state, gates: [...state.gates, gate], pendingGate: undefined });\n }\n\n if (state.surface !== undefined) {\n if (ROUTED_SURFACES.includes(state.surface)) {\n return walk(relative, { ...state, route: [...state.route, { literal: segment.name }] });\n }\n return note(relative, `${state.surface}/ takes files directly; put helpers in a _private folder`);\n }\n\n if (state.side === undefined) {\n if (segment.name === WORKSPACE_SEGMENT && state.scope === 'global') {\n return walk(relative, { ...state, scope: 'workspace' });\n }\n if (segment.name === SESSION_SEGMENT && state.scope === 'workspace') {\n return walk(relative, { ...state, scope: 'session' });\n }\n return note(relative, `expected a scope segment or a side group, such as (${backendGroup})`);\n }\n\n if (GATE_SEGMENTS.includes(segment.name)) return walk(relative, { ...state, pendingGate: segment.name });\n\n if (surfacesFor(state.side).includes(segment.name)) return walk(relative, { ...state, surface: segment.name });\n\n const nearest = nearestSurface(segment.name, state.side);\n return note(\n relative,\n nearest === undefined\n ? `'${segment.name}' is not a ${state.side} surface`\n : `'${segment.name}' is not a surface, did you mean '${nearest}'`,\n );\n };\n\n const readFile = (relative: string, fileName: string, state: WalkState): void => {\n if (state.side === undefined) {\n const base = fileName.slice(0, fileName.lastIndexOf('.'));\n if (state.scope === 'global' && generated.includes(base)) return;\n // A direct child of the routing root is a host entry, not a\n // contribution: either one this build generates, or one the package\n // hand-writes and names itself. doompi-profile ships a second Pi entry\n // the composition resolver activates by name, and a package that has\n // not adopted the layout has all of its entries here. A contribution\n // always sits at least one directory deep, inside a side group.\n if (state.scope === 'global') return;\n if (parseFilename(fileName, []) === undefined) return;\n return note(relative, 'a contribution needs a side group and a surface folder');\n }\n\n const platforms = platformsFor(state.side, options);\n const parsed = parseFilename(fileName, [...platforms, 'mcp']);\n if (parsed === undefined) return;\n if (\n parsed.platform === 'mcp' &&\n (state.side !== 'backend' ||\n state.scope !== 'session' ||\n (state.surface !== 'tool' && state.surface !== 'skill' && state.surface !== 'resource'))\n ) {\n return note(relative, 'MCP declarations belong in a session backend tool/, skill/, or resource/ surface');\n }\n const source = fs.readFileSync(path.join(options.packageDir, relative), 'utf8');\n const cardinality = routedCardinality(source);\n\n if (state.surface === undefined) {\n // Inside `mode/plan/`, a file called `mode.*` declares the gate itself\n // rather than a contribution gated by it, and the gate's id names it.\n const gate = state.gates[state.gates.length - 1];\n if (gate !== undefined && parsed.name === gate.kind) {\n notePlatform(relative, parsed, state.side);\n entries.push({\n file: relative,\n scope: state.scope,\n side: state.side,\n gates: state.gates.slice(0, -1),\n surface: gate.kind,\n route: [],\n name: gate.id,\n target: parsed.target,\n platform: parsed.platform,\n ...(cardinality ? { cardinality } : {}),\n role: 'contribution',\n });\n return;\n }\n\n // The scope's constructor. Not gated: it owns the whole subtree's\n // lifetime, and a gate that could switch it off would leave everything\n // below it holding a context nothing built.\n if (parsed.name === ROOT_FILE_NAME && gate === undefined) {\n notePlatform(relative, parsed, state.side);\n entries.push({\n file: relative,\n scope: state.scope,\n side: state.side,\n gates: [],\n surface: undefined,\n route: [],\n name: parsed.name,\n target: parsed.target,\n platform: parsed.platform,\n role: 'root',\n });\n return;\n }\n\n return note(\n relative,\n gate === undefined\n ? `only ${ROOT_FILE_NAME}.* is read at a side root; move each contribution into its named surface folder`\n : `inside ${gate.kind}/${gate.id}/ a contribution needs a surface folder, or name the file ${gate.kind}.* to declare the gate`,\n );\n }\n\n // Inside a routed surface only the leaf file is a route. Anything beside\n // it is colocation, which is the whole reason Next.js reserves the name.\n if (ROUTED_SURFACES.includes(state.surface) && parsed.name !== ROUTE_FILE_NAME) return;\n\n notePlatform(relative, parsed, state.side);\n entries.push({\n file: relative,\n scope: state.scope,\n side: state.side,\n gates: state.gates,\n surface: state.surface,\n route: state.route,\n name: parsed.name,\n target: parsed.target,\n platform: parsed.platform,\n ...(cardinality ? { cardinality } : {}),\n role: 'contribution',\n });\n };\n\n walk(root, INITIAL);\n return { root, entries, notices };\n}\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { ExtensionGraph } from "../../types/extensionGraph.mjs";
2
+ import { ScanOptions } from "./type.mjs";
3
+ //#region src/services/scan/index.d.ts
4
+ /** Resolves the routing root, accepting the singular alias when only it exists. */
5
+ export declare function resolveRoutingRoot(packageDir: string, root: string | undefined): string | undefined;
6
+ /**
7
+ * Reads one package's routing root into a description of what it declares.
8
+ *
9
+ * Filesystem in, data out. No globbing, which is what keeps parenthesised
10
+ * group folders safe: tsdown's globber would read a bare `(name)` as an
11
+ * extglob group, and a directory walk never sees a pattern at all.
12
+ *
13
+ * Nothing here throws. A folder the convention does not recognise becomes a
14
+ * notice and its subtree is skipped, so one bad directory never costs a
15
+ * package its other contributions.
16
+ */
17
+ export declare function scanExtensions(options: ScanOptions): ExtensionGraph;
18
+ //#endregion
19
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/scan/index.ts"],"mappings":";;;;wBA+EgB,mBAAmB,oBAAoB;;;;;;;;;;;;wBAgBvC,eAAe,SAAS,cAAc"}
@@ -0,0 +1,2 @@
1
+ import{BACKEND_PLATFORMS as e,BACKEND_SURFACES as t,DEFAULT_ROUTING_ROOT as n,FRONTEND_PLATFORMS as r,FRONTEND_SURFACES as i,GATE_SEGMENTS as a,GENERATED_ENTRY_NAMES as o,ROOT_FILE_NAME as s,ROUTED_SURFACES as c,ROUTING_ROOT_ALIAS as l}from"../../constants/layout.mjs";import{parseFilename as u}from"../filename/index.mjs";import{classifySegment as d}from"../segments/index.mjs";import f from"node:fs";import p from"node:path";const m={scope:`global`,side:void 0,gates:[],surface:void 0,route:[],pendingGate:void 0};function h(e){return e===`backend`?t:i}function g(t,n){return(t===`backend`?n.platforms?.backend:n.platforms?.frontend)??(t===`backend`?e:r)}function _(e,t){let n=h(t);if(n.includes(e))return;let r=e.endsWith(`s`)?e.slice(0,-1):`${e}s`;return n.includes(r)?r:void 0}function v(e){return/export\s+default\s+defineRoutedContribution\s*\([\s\S]*,\s*\{\s*cardinality\s*:\s*['"](optional|many|collection)['"]/u.exec(e)?.[1]}function y(e,t){return(t===void 0?[n,l]:[t]).find(t=>f.existsSync(p.join(e,t)))}function b(e){let t=y(e.packageDir,e.root);if(t===void 0)return{root:e.root??`src/extensions`,entries:[],notices:[]};let n=e.sides?.backend??`backend`,r=e.sides?.frontend??`frontend`,i=e.generatedEntries??o,l=[],b=[],x=(e,t)=>void b.push({path:e,message:t}),S=(t,n,r)=>{n.platform===void 0&&x(t,`names no platform; add one of ${g(r,e).join(`, `)} before .${n.extension} (without one the host is inferred from the side rather than declared)`)},C=(t,n)=>{let r=p.join(e.packageDir,t),i=f.readdirSync(r,{withFileTypes:!0}).sort((e,t)=>e.name.localeCompare(t.name));for(let e of i){let r=`${t}/${e.name}`;e.isDirectory()?w(r,e.name,n):e.isFile()&&T(r,e.name,n)}},w=(e,t,i)=>{let o=d(t);if(o.kind===`private`)return;if(o.kind===`group`)return i.side===void 0&&o.name===n?C(e,{...i,side:`backend`}):i.side===void 0&&o.name===r?C(e,{...i,side:`frontend`}):C(e,i);if(o.kind===`dynamic`)return i.surface!==void 0&&c.includes(i.surface)?C(e,{...i,route:[...i.route,{param:o.param}]}):x(e,`a dynamic segment is only meaningful inside a routed surface such as api/`);if(i.pendingGate!==void 0){let t={kind:i.pendingGate,id:o.name};return C(e,{...i,gates:[...i.gates,t],pendingGate:void 0})}if(i.surface!==void 0)return c.includes(i.surface)?C(e,{...i,route:[...i.route,{literal:o.name}]}):x(e,`${i.surface}/ takes files directly; put helpers in a _private folder`);if(i.side===void 0)return o.name===`workspaces`&&i.scope===`global`?C(e,{...i,scope:`workspace`}):o.name===`sessions`&&i.scope===`workspace`?C(e,{...i,scope:`session`}):x(e,`expected a scope segment or a side group, such as (${n})`);if(a.includes(o.name))return C(e,{...i,pendingGate:o.name});if(h(i.side).includes(o.name))return C(e,{...i,surface:o.name});let s=_(o.name,i.side);return x(e,s===void 0?`'${o.name}' is not a ${i.side} surface`:`'${o.name}' is not a surface, did you mean '${s}'`)},T=(t,n,r)=>{if(r.side===void 0){let e=n.slice(0,n.lastIndexOf(`.`));return r.scope===`global`&&i.includes(e)||r.scope===`global`||u(n,[])===void 0?void 0:x(t,`a contribution needs a side group and a surface folder`)}let a=g(r.side,e),o=u(n,[...a,`mcp`]);if(o===void 0)return;if(o.platform===`mcp`&&(r.side!==`backend`||r.scope!==`session`||r.surface!==`tool`&&r.surface!==`skill`&&r.surface!==`resource`))return x(t,`MCP declarations belong in a session backend tool/, skill/, or resource/ surface`);let d=v(f.readFileSync(p.join(e.packageDir,t),`utf8`));if(r.surface===void 0){let e=r.gates[r.gates.length-1];if(e!==void 0&&o.name===e.kind){S(t,o,r.side),l.push({file:t,scope:r.scope,side:r.side,gates:r.gates.slice(0,-1),surface:e.kind,route:[],name:e.id,target:o.target,platform:o.platform,...d?{cardinality:d}:{},role:`contribution`});return}if(o.name===`root`&&e===void 0){S(t,o,r.side),l.push({file:t,scope:r.scope,side:r.side,gates:[],surface:void 0,route:[],name:o.name,target:o.target,platform:o.platform,role:`root`});return}return x(t,e===void 0?`only ${s}.* is read at a side root; move each contribution into its named surface folder`:`inside ${e.kind}/${e.id}/ a contribution needs a surface folder, or name the file ${e.kind}.* to declare the gate`)}c.includes(r.surface)&&o.name!==`route`||(S(t,o,r.side),l.push({file:t,scope:r.scope,side:r.side,gates:r.gates,surface:r.surface,route:r.route,name:o.name,target:o.target,platform:o.platform,...d?{cardinality:d}:{},role:`contribution`}))};return C(t,m),{root:t,entries:l,notices:b}}export{y as resolveRoutingRoot,b as scanExtensions};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/scan/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n BACKEND_GROUP,\n BACKEND_PLATFORMS,\n BACKEND_SURFACES,\n DEFAULT_ROUTING_ROOT,\n FRONTEND_GROUP,\n FRONTEND_PLATFORMS,\n FRONTEND_SURFACES,\n GATE_SEGMENTS,\n GENERATED_ENTRY_NAMES,\n ROOT_FILE_NAME,\n ROUTE_FILE_NAME,\n ROUTED_SURFACES,\n ROUTING_ROOT_ALIAS,\n SESSION_SEGMENT,\n WORKSPACE_SEGMENT,\n} from '../../constants/layout';\nimport type {\n ExtensionEntry,\n ExtensionGate,\n ExtensionGraph,\n ExtensionNotice,\n ExtensionScope,\n ExtensionSide,\n RouteSegment,\n} from '../../types/extensionGraph';\nimport { type ParsedFilename, parseFilename } from '../filename';\nimport { classifySegment } from '../segments';\nimport type { ScanOptions } from './type';\n\ninterface WalkState {\n readonly scope: ExtensionScope;\n readonly side: ExtensionSide | undefined;\n readonly gates: readonly ExtensionGate[];\n readonly surface: string | undefined;\n readonly route: readonly RouteSegment[];\n /** A gate folder has been entered and the next plain segment names its id. */\n readonly pendingGate: string | undefined;\n}\n\nconst INITIAL: WalkState = {\n scope: 'global',\n side: undefined,\n gates: [],\n surface: undefined,\n route: [],\n pendingGate: undefined,\n};\n\nfunction surfacesFor(side: ExtensionSide): readonly string[] {\n return side === 'backend' ? BACKEND_SURFACES : FRONTEND_SURFACES;\n}\n\nfunction platformsFor(side: ExtensionSide, options: ScanOptions): readonly string[] {\n const configured = side === 'backend' ? options.platforms?.backend : options.platforms?.frontend;\n return configured ?? (side === 'backend' ? BACKEND_PLATFORMS : FRONTEND_PLATFORMS);\n}\n\n/** The nearest surface a misspelled folder probably meant, or undefined. */\nfunction nearestSurface(name: string, side: ExtensionSide): string | undefined {\n const surfaces = surfacesFor(side);\n if (surfaces.includes(name)) return undefined;\n const singular = name.endsWith('s') ? name.slice(0, -1) : `${name}s`;\n return surfaces.includes(singular) ? singular : undefined;\n}\n\n/** Reads cardinality only from the routed helper's options argument. */\nfunction routedCardinality(source: string): ExtensionEntry['cardinality'] | undefined {\n const declaration =\n /export\\s+default\\s+defineRoutedContribution\\s*\\([\\s\\S]*,\\s*\\{\\s*cardinality\\s*:\\s*['\"](optional|many|collection)['\"]/u.exec(\n source,\n );\n return declaration?.[1] as ExtensionEntry['cardinality'] | undefined;\n}\n\n/** Resolves the routing root, accepting the singular alias when only it exists. */\nexport function resolveRoutingRoot(packageDir: string, root: string | undefined): string | undefined {\n const candidates = root !== undefined ? [root] : [DEFAULT_ROUTING_ROOT, ROUTING_ROOT_ALIAS];\n return candidates.find((candidate) => fs.existsSync(path.join(packageDir, candidate)));\n}\n\n/**\n * Reads one package's routing root into a description of what it declares.\n *\n * Filesystem in, data out. No globbing, which is what keeps parenthesised\n * group folders safe: tsdown's globber would read a bare `(name)` as an\n * extglob group, and a directory walk never sees a pattern at all.\n *\n * Nothing here throws. A folder the convention does not recognise becomes a\n * notice and its subtree is skipped, so one bad directory never costs a\n * package its other contributions.\n */\nexport function scanExtensions(options: ScanOptions): ExtensionGraph {\n const root = resolveRoutingRoot(options.packageDir, options.root);\n if (root === undefined) return { root: options.root ?? DEFAULT_ROUTING_ROOT, entries: [], notices: [] };\n\n const backendGroup = options.sides?.backend ?? BACKEND_GROUP;\n const frontendGroup = options.sides?.frontend ?? FRONTEND_GROUP;\n const generated = options.generatedEntries ?? GENERATED_ENTRY_NAMES;\n\n const entries: ExtensionEntry[] = [];\n const notices: ExtensionNotice[] = [];\n const note = (relativePath: string, message: string): void => void notices.push({ path: relativePath, message });\n\n /**\n * Every public routed file names its platform, so a host is never inferred.\n *\n * Checked at each push rather than once at the top of readFile, because a\n * file sitting somewhere the convention does not define has a position\n * problem, and reporting the missing suffix instead would hide it.\n */\n const notePlatform = (relative: string, parsed: ParsedFilename, side: ExtensionSide): void => {\n if (parsed.platform !== undefined) return;\n note(\n relative,\n `names no platform; add one of ${platformsFor(side, options).join(', ')} before .${parsed.extension} (without one the host is inferred from the side rather than declared)`,\n );\n };\n\n const walk = (relativeDir: string, state: WalkState): void => {\n const absolute = path.join(options.packageDir, relativeDir);\n const listing = fs.readdirSync(absolute, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));\n\n for (const item of listing) {\n const relative = `${relativeDir}/${item.name}`;\n if (item.isDirectory()) walkDirectory(relative, item.name, state);\n else if (item.isFile()) readFile(relative, item.name, state);\n }\n };\n\n const walkDirectory = (relative: string, name: string, state: WalkState): void => {\n const segment = classifySegment(name);\n\n if (segment.kind === 'private') return;\n\n if (segment.kind === 'group') {\n if (state.side === undefined && segment.name === backendGroup)\n return walk(relative, { ...state, side: 'backend' });\n if (state.side === undefined && segment.name === frontendGroup)\n return walk(relative, { ...state, side: 'frontend' });\n return walk(relative, state);\n }\n\n if (segment.kind === 'dynamic') {\n if (state.surface !== undefined && ROUTED_SURFACES.includes(state.surface)) {\n return walk(relative, { ...state, route: [...state.route, { param: segment.param }] });\n }\n return note(relative, 'a dynamic segment is only meaningful inside a routed surface such as api/');\n }\n\n if (state.pendingGate !== undefined) {\n const gate: ExtensionGate = { kind: state.pendingGate, id: segment.name };\n return walk(relative, { ...state, gates: [...state.gates, gate], pendingGate: undefined });\n }\n\n if (state.surface !== undefined) {\n if (ROUTED_SURFACES.includes(state.surface)) {\n return walk(relative, { ...state, route: [...state.route, { literal: segment.name }] });\n }\n return note(relative, `${state.surface}/ takes files directly; put helpers in a _private folder`);\n }\n\n if (state.side === undefined) {\n if (segment.name === WORKSPACE_SEGMENT && state.scope === 'global') {\n return walk(relative, { ...state, scope: 'workspace' });\n }\n if (segment.name === SESSION_SEGMENT && state.scope === 'workspace') {\n return walk(relative, { ...state, scope: 'session' });\n }\n return note(relative, `expected a scope segment or a side group, such as (${backendGroup})`);\n }\n\n if (GATE_SEGMENTS.includes(segment.name)) return walk(relative, { ...state, pendingGate: segment.name });\n\n if (surfacesFor(state.side).includes(segment.name)) return walk(relative, { ...state, surface: segment.name });\n\n const nearest = nearestSurface(segment.name, state.side);\n return note(\n relative,\n nearest === undefined\n ? `'${segment.name}' is not a ${state.side} surface`\n : `'${segment.name}' is not a surface, did you mean '${nearest}'`,\n );\n };\n\n const readFile = (relative: string, fileName: string, state: WalkState): void => {\n if (state.side === undefined) {\n const base = fileName.slice(0, fileName.lastIndexOf('.'));\n if (state.scope === 'global' && generated.includes(base)) return;\n // A direct child of the routing root is a host entry, not a\n // contribution: either one this build generates, or one the package\n // hand-writes and names itself. doompi-profile ships a second Pi entry\n // the composition resolver activates by name, and a package that has\n // not adopted the layout has all of its entries here. A contribution\n // always sits at least one directory deep, inside a side group.\n if (state.scope === 'global') return;\n if (parseFilename(fileName, []) === undefined) return;\n return note(relative, 'a contribution needs a side group and a surface folder');\n }\n\n const platforms = platformsFor(state.side, options);\n const parsed = parseFilename(fileName, [...platforms, 'mcp']);\n if (parsed === undefined) return;\n if (\n parsed.platform === 'mcp' &&\n (state.side !== 'backend' ||\n state.scope !== 'session' ||\n (state.surface !== 'tool' && state.surface !== 'skill' && state.surface !== 'resource'))\n ) {\n return note(relative, 'MCP declarations belong in a session backend tool/, skill/, or resource/ surface');\n }\n const source = fs.readFileSync(path.join(options.packageDir, relative), 'utf8');\n const cardinality = routedCardinality(source);\n\n if (state.surface === undefined) {\n // Inside `mode/plan/`, a file called `mode.*` declares the gate itself\n // rather than a contribution gated by it, and the gate's id names it.\n const gate = state.gates[state.gates.length - 1];\n if (gate !== undefined && parsed.name === gate.kind) {\n notePlatform(relative, parsed, state.side);\n entries.push({\n file: relative,\n scope: state.scope,\n side: state.side,\n gates: state.gates.slice(0, -1),\n surface: gate.kind,\n route: [],\n name: gate.id,\n target: parsed.target,\n platform: parsed.platform,\n ...(cardinality ? { cardinality } : {}),\n role: 'contribution',\n });\n return;\n }\n\n // The scope's constructor. Not gated: it owns the whole subtree's\n // lifetime, and a gate that could switch it off would leave everything\n // below it holding a context nothing built.\n if (parsed.name === ROOT_FILE_NAME && gate === undefined) {\n notePlatform(relative, parsed, state.side);\n entries.push({\n file: relative,\n scope: state.scope,\n side: state.side,\n gates: [],\n surface: undefined,\n route: [],\n name: parsed.name,\n target: parsed.target,\n platform: parsed.platform,\n role: 'root',\n });\n return;\n }\n\n return note(\n relative,\n gate === undefined\n ? `only ${ROOT_FILE_NAME}.* is read at a side root; move each contribution into its named surface folder`\n : `inside ${gate.kind}/${gate.id}/ a contribution needs a surface folder, or name the file ${gate.kind}.* to declare the gate`,\n );\n }\n\n // Inside a routed surface only the leaf file is a route. Anything beside\n // it is colocation, which is the whole reason Next.js reserves the name.\n if (ROUTED_SURFACES.includes(state.surface) && parsed.name !== ROUTE_FILE_NAME) return;\n\n notePlatform(relative, parsed, state.side);\n entries.push({\n file: relative,\n scope: state.scope,\n side: state.side,\n gates: state.gates,\n surface: state.surface,\n route: state.route,\n name: parsed.name,\n target: parsed.target,\n platform: parsed.platform,\n ...(cardinality ? { cardinality } : {}),\n role: 'contribution',\n });\n };\n\n walk(root, INITIAL);\n return { root, entries, notices };\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ //#region src/services/scan/type.d.ts
2
+ /** What the scan needs to know before it walks a package. */
3
+ export interface ScanOptions {
4
+ /** Absolute path of the package being scanned. */
5
+ readonly packageDir: string;
6
+ /**
7
+ * Routing root, package-relative. Defaults to `src/extensions`, falling back
8
+ * to `src/extension` when only that one exists.
9
+ */
10
+ readonly root?: string;
11
+ /** Group folder names that select a build side. */
12
+ readonly sides?: {
13
+ readonly backend?: string;
14
+ readonly frontend?: string;
15
+ };
16
+ /** Recognised platform suffixes per side. */
17
+ readonly platforms?: {
18
+ readonly backend?: readonly string[];
19
+ readonly frontend?: readonly string[];
20
+ };
21
+ /** Base names of the generated entries at the root, which the scan skips. */
22
+ readonly generatedEntries?: readonly string[];
23
+ }
24
+ //#endregion
25
+ //# sourceMappingURL=type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type.d.mts","names":[],"sources":["../../../src/services/scan/type.ts"],"mappings":";;iBACiB;;WAEN;;;;;WAKA;;WAEA;aAAmB;aAA2B;;;WAE9C;aAAuB;aAAsC;;;WAE7D"}
@@ -0,0 +1,2 @@
1
+ function e(e,t,n){if(!(e.length<=t.length+n.length)&&e.startsWith(t)&&e.endsWith(n))return e.slice(t.length,e.length-n.length)}function t(t){if(t.startsWith(`_`))return{kind:`private`,name:t.slice(1)};let n=e(t,`(`,`)`);if(n!==void 0)return{kind:`group`,name:n};let r=e(t,`[`,`]`);if(r!==void 0){let e=r.startsWith(`...`);return{kind:`dynamic`,param:{name:e?r.slice(3):r,catchAll:e}}}return{kind:`plain`,name:t}}exports.classifySegment=t;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/services/segments/index.ts"],"sourcesContent":["import {\n CATCH_ALL_MARKER,\n DYNAMIC_PREFIX,\n DYNAMIC_SUFFIX,\n GROUP_PREFIX,\n GROUP_SUFFIX,\n PRIVATE_PREFIX,\n} from '../../constants/layout';\nimport type { RouteParam } from '../../types/extensionGraph';\n\n/**\n * What one folder name means, using Next.js spelling throughout.\n *\n * A group is organisational and invisible to the path. A private folder is\n * colocation and is never scanned. A dynamic segment is a route parameter.\n * Everything else is a plain segment whose meaning depends on where it sits.\n */\nexport type Segment =\n | { readonly kind: 'group'; readonly name: string }\n | { readonly kind: 'private'; readonly name: string }\n | { readonly kind: 'dynamic'; readonly param: RouteParam }\n | { readonly kind: 'plain'; readonly name: string };\n\nfunction wrapped(segment: string, open: string, close: string): string | undefined {\n if (segment.length <= open.length + close.length) return undefined;\n if (!segment.startsWith(open) || !segment.endsWith(close)) return undefined;\n return segment.slice(open.length, segment.length - close.length);\n}\n\n/** Classifies one folder name. Never throws: an unusable name is simply plain. */\nexport function classifySegment(segment: string): Segment {\n if (segment.startsWith(PRIVATE_PREFIX)) return { kind: 'private', name: segment.slice(PRIVATE_PREFIX.length) };\n\n const group = wrapped(segment, GROUP_PREFIX, GROUP_SUFFIX);\n if (group !== undefined) return { kind: 'group', name: group };\n\n const dynamic = wrapped(segment, DYNAMIC_PREFIX, DYNAMIC_SUFFIX);\n if (dynamic !== undefined) {\n const catchAll = dynamic.startsWith(CATCH_ALL_MARKER);\n return {\n kind: 'dynamic',\n param: { name: catchAll ? dynamic.slice(CATCH_ALL_MARKER.length) : dynamic, catchAll },\n };\n }\n\n return { kind: 'plain', name: segment };\n}\n"],"mappings":"AAuBA,SAAS,EAAQ,EAAiB,EAAc,EAAmC,CAC7E,OAAQ,QAAU,EAAK,OAAS,EAAM,SACrC,EAAQ,WAAW,CAAI,GAAM,EAAQ,SAAS,CAAK,EACxD,OAAO,EAAQ,MAAM,EAAK,OAAQ,EAAQ,OAAS,EAAM,MAAM,CACjE,CAGA,SAAgB,EAAgB,EAA0B,CACxD,GAAI,EAAQ,WAAA,GAAyB,EAAG,MAAO,CAAE,KAAM,UAAW,KAAM,EAAQ,MAAA,CAA2B,CAAE,EAE7G,IAAM,EAAQ,EAAQ,EAAA,IAAA,GAAmC,EACzD,GAAI,IAAU,IAAA,GAAW,MAAO,CAAE,KAAM,QAAS,KAAM,CAAM,EAE7D,IAAM,EAAU,EAAQ,EAAA,IAAA,GAAuC,EAC/D,GAAI,IAAY,IAAA,GAAW,CACzB,IAAM,EAAW,EAAQ,WAAA,KAA2B,EACpD,MAAO,CACL,KAAM,UACN,MAAO,CAAE,KAAM,EAAW,EAAQ,MAAA,CAA6B,EAAI,EAAS,UAAS,CACvF,CACF,CAEA,MAAO,CAAE,KAAM,QAAS,KAAM,CAAQ,CACxC"}
@@ -0,0 +1,26 @@
1
+ import { RouteParam } from "../../types/extensionGraph.mjs";
2
+ //#region src/services/segments/index.d.ts
3
+ /**
4
+ * What one folder name means, using Next.js spelling throughout.
5
+ *
6
+ * A group is organisational and invisible to the path. A private folder is
7
+ * colocation and is never scanned. A dynamic segment is a route parameter.
8
+ * Everything else is a plain segment whose meaning depends on where it sits.
9
+ */
10
+ export type Segment = {
11
+ readonly kind: 'group';
12
+ readonly name: string;
13
+ } | {
14
+ readonly kind: 'private';
15
+ readonly name: string;
16
+ } | {
17
+ readonly kind: 'dynamic';
18
+ readonly param: RouteParam;
19
+ } | {
20
+ readonly kind: 'plain';
21
+ readonly name: string;
22
+ };
23
+ /** Classifies one folder name. Never throws: an unusable name is simply plain. */
24
+ export declare function classifySegment(segment: string): Segment;
25
+ //#endregion
26
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/segments/index.ts"],"mappings":";;;;;;;;;YAiBY;WACG;WAAwB;;WACxB;WAA0B;;WAC1B;WAA0B,OAAO;;WACjC;WAAwB;;;wBASvB,gBAAgB,kBAAkB"}
@@ -0,0 +1,2 @@
1
+ function e(e,t,n){if(!(e.length<=t.length+n.length)&&e.startsWith(t)&&e.endsWith(n))return e.slice(t.length,e.length-n.length)}function t(t){if(t.startsWith(`_`))return{kind:`private`,name:t.slice(1)};let n=e(t,`(`,`)`);if(n!==void 0)return{kind:`group`,name:n};let r=e(t,`[`,`]`);if(r!==void 0){let e=r.startsWith(`...`);return{kind:`dynamic`,param:{name:e?r.slice(3):r,catchAll:e}}}return{kind:`plain`,name:t}}export{t as classifySegment};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/segments/index.ts"],"sourcesContent":["import {\n CATCH_ALL_MARKER,\n DYNAMIC_PREFIX,\n DYNAMIC_SUFFIX,\n GROUP_PREFIX,\n GROUP_SUFFIX,\n PRIVATE_PREFIX,\n} from '../../constants/layout';\nimport type { RouteParam } from '../../types/extensionGraph';\n\n/**\n * What one folder name means, using Next.js spelling throughout.\n *\n * A group is organisational and invisible to the path. A private folder is\n * colocation and is never scanned. A dynamic segment is a route parameter.\n * Everything else is a plain segment whose meaning depends on where it sits.\n */\nexport type Segment =\n | { readonly kind: 'group'; readonly name: string }\n | { readonly kind: 'private'; readonly name: string }\n | { readonly kind: 'dynamic'; readonly param: RouteParam }\n | { readonly kind: 'plain'; readonly name: string };\n\nfunction wrapped(segment: string, open: string, close: string): string | undefined {\n if (segment.length <= open.length + close.length) return undefined;\n if (!segment.startsWith(open) || !segment.endsWith(close)) return undefined;\n return segment.slice(open.length, segment.length - close.length);\n}\n\n/** Classifies one folder name. Never throws: an unusable name is simply plain. */\nexport function classifySegment(segment: string): Segment {\n if (segment.startsWith(PRIVATE_PREFIX)) return { kind: 'private', name: segment.slice(PRIVATE_PREFIX.length) };\n\n const group = wrapped(segment, GROUP_PREFIX, GROUP_SUFFIX);\n if (group !== undefined) return { kind: 'group', name: group };\n\n const dynamic = wrapped(segment, DYNAMIC_PREFIX, DYNAMIC_SUFFIX);\n if (dynamic !== undefined) {\n const catchAll = dynamic.startsWith(CATCH_ALL_MARKER);\n return {\n kind: 'dynamic',\n param: { name: catchAll ? dynamic.slice(CATCH_ALL_MARKER.length) : dynamic, catchAll },\n };\n }\n\n return { kind: 'plain', name: segment };\n}\n"],"mappings":"AAuBA,SAAS,EAAQ,EAAiB,EAAc,EAAmC,CAC7E,OAAQ,QAAU,EAAK,OAAS,EAAM,SACrC,EAAQ,WAAW,CAAI,GAAM,EAAQ,SAAS,CAAK,EACxD,OAAO,EAAQ,MAAM,EAAK,OAAQ,EAAQ,OAAS,EAAM,MAAM,CACjE,CAGA,SAAgB,EAAgB,EAA0B,CACxD,GAAI,EAAQ,WAAA,GAAyB,EAAG,MAAO,CAAE,KAAM,UAAW,KAAM,EAAQ,MAAA,CAA2B,CAAE,EAE7G,IAAM,EAAQ,EAAQ,EAAA,IAAA,GAAmC,EACzD,GAAI,IAAU,IAAA,GAAW,MAAO,CAAE,KAAM,QAAS,KAAM,CAAM,EAE7D,IAAM,EAAU,EAAQ,EAAA,IAAA,GAAuC,EAC/D,GAAI,IAAY,IAAA,GAAW,CACzB,IAAM,EAAW,EAAQ,WAAA,KAA2B,EACpD,MAAO,CACL,KAAM,UACN,MAAO,CAAE,KAAM,EAAW,EAAQ,MAAA,CAA6B,EAAI,EAAS,UAAS,CACvF,CACF,CAEA,MAAO,CAAE,KAAM,QAAS,KAAM,CAAQ,CACxC"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../constants/layout.cjs"),n=require("../identity/index.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);const a=[`global`,`workspace`,`session`],o=`apiContracts`,s=`src/exports/${o}.ts`,c=`./extensions/pi`,l=`./extensions/web`,u=`./dist/extensions/web.mjs`,d=`./${t.GENERATED_DIR}/mcp.ts`;function f(e){let t=e.entries.filter(e=>e.side===`backend`);if(t.length===0)return[];let n=Math.min(...t.map(e=>a.indexOf(e.scope)));return a.slice(n)}function p(e){let t=new Set(e.entries.filter(e=>e.side===`frontend`).map(e=>e.scope));return a.filter(e=>t.has(e))}function m(e,t){return[...new Set(e.entries.filter(e=>e.side===`frontend`&&e.surface===`channel`).map(e=>n.toSnake(e.name)))].sort()}function h(e,t){let n={};for(let[a,o]of Object.entries(t)){if(a===`./package.json`||typeof o!=`object`||!o){n[a]=o;continue}let t=o,s=t.import??t.default,c=s?.endsWith(`.mjs`)===!0?`${s.slice(0,-4)}.d.mts`:void 0;n[a]=t.types===void 0&&c!==void 0&&r.default.existsSync(i.default.join(e,c))?{types:c,...t}:t}return n}function g(e){let{packageDir:n,graph:a,targets:d,pluginId:g}=e,_={...e.manifest};if(_.exports!==void 0&&typeof _.exports==`object`){let e=h(n,_.exports);d.includes(`cli`)?e[c]={types:`./dist/extensions/pi.d.mts`,import:`./dist/extensions/pi.mjs`,require:`./dist/extensions/pi.cjs`}:delete e[c],d.includes(`web`)?e[l]={import:u}:delete e[l],_.exports=Object.fromEntries(Object.entries(e).sort(([e],[t])=>e===`./package.json`?1:t===`./package.json`?-1:e.localeCompare(t)))}if(d.includes(`cli`)?_.pi={extensions:[`./dist/extensions/pi.mjs`]}:delete _.pi,d.includes(`server`)){let e=r.default.existsSync(i.default.join(n,s))?{contracts:{entry:`./${s}`,dist:`./dist/${o}.mjs`}}:{};_.doompiServer={entry:`./${t.GENERATED_DIR}/server.ts`,dist:`./dist/extensions/server.mjs`,scopes:f(a),...e}}else delete _.doompiServer;return d.includes(`web`)?_.doompiWeb={pluginId:g,channels:m(a,_),client:u,scopes:p(a)}:delete _.doompiWeb,_}function _(e,t){let n={...e};return t?n.doompiMcp={entry:d,dist:`./dist/extensions/mcp.mjs`,scopes:[`session`]}:delete n.doompiMcp,n}function v(e){let t=i.default.join(e.packageDir,`package.json`),n=`${JSON.stringify(g(e),null,2)}\n`;return r.default.readFileSync(t,`utf8`)!==n&&(r.default.writeFileSync(t,n),!0)}function y(e,t){let n=i.default.join(e,`package.json`),a=JSON.parse(r.default.readFileSync(n,`utf8`)),o=`${JSON.stringify(_(a,t),null,2)}\n`;return r.default.readFileSync(n,`utf8`)!==o&&(r.default.writeFileSync(n,o),!0)}exports.syncManifest=g,exports.syncMcpManifest=_,exports.writeManifest=v,exports.writeMcpManifest=y;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["GENERATED_DIR","toSnake","fs","path"],"sources":["../../../src/services/syncManifest/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { GENERATED_DIR } from '../../constants/layout';\nimport type { ExtensionGraph, ExtensionScope } from '../../types/extensionGraph';\nimport { toSnake } from '../identity';\nimport type { BuildTarget } from '../resolveTarget/type';\nimport type { ManifestSync } from './type';\n\nconst SCOPE_ORDER: readonly ExtensionScope[] = ['global', 'workspace', 'session'];\n\nconst API_CONTRACTS_ENTRY = 'apiContracts';\nconst API_CONTRACTS_SOURCE = `src/exports/${API_CONTRACTS_ENTRY}.ts`;\n\nconst PI_EXPORT = './extensions/pi';\nconst PI_TYPES = './dist/extensions/pi.d.mts';\nconst PI_IMPORT = './dist/extensions/pi.mjs';\nconst PI_REQUIRE = './dist/extensions/pi.cjs';\nconst WEB_EXPORT = './extensions/web';\nconst WEB_DIST = './dist/extensions/web.mjs';\nconst MCP_DIST = './dist/extensions/mcp.mjs';\nconst MCP_ENTRY = `./${GENERATED_DIR}/mcp.ts`;\n\n/**\n * Which scopes a server facet declares.\n *\n * The generated facet cascades: a contribution declared at global registers at\n * global, workspace and session alike, because the server reads exactly one\n * scope declaration and nothing above it. So the declared set runs from the\n * shallowest contribution downward.\n */\nfunction serverScopes(graph: ExtensionGraph): ExtensionScope[] {\n const declared = graph.entries.filter((entry) => entry.side === 'backend');\n if (declared.length === 0) return [];\n const shallowest = Math.min(...declared.map((entry) => SCOPE_ORDER.indexOf(entry.scope)));\n return SCOPE_ORDER.slice(shallowest);\n}\n\n/**\n * Which scopes a cockpit plugin declares.\n *\n * The cockpit does not cascade the way the server does. It merges the scopes\n * it is given, so a plugin declares exactly the ones it contributes to.\n */\nfunction webScopes(graph: ExtensionGraph): ExtensionScope[] {\n const declared = new Set(graph.entries.filter((entry) => entry.side === 'frontend').map((entry) => entry.scope));\n return SCOPE_ORDER.filter((scope) => declared.has(scope));\n}\n\n/** The frame types the browser half claims, which the cockpit reads before loading it. */\nfunction channels(graph: ExtensionGraph, _manifest: Record<string, unknown>): string[] {\n return [\n ...new Set(\n graph.entries\n .filter((entry) => entry.side === 'frontend' && entry.surface === 'channel')\n .map((entry) => toSnake(entry.name)),\n ),\n ].sort();\n}\n\n/**\n * Restores the `types` condition tsdown's exports generation omits.\n *\n * tsdown derives the map from the built chunks, which is what makes it follow\n * the folder tree. But its `genSubExport` only ever emits `import`, `require`\n * or `default`: a declaration chunk contributes nothing to a subpath, so only\n * the root gets a top-level `types`. TypeScript still resolves a sibling\n * `.d.mts` under node16 and bundler resolution, but publint and attw read the\n * condition, so it belongs in the map.\n */\nfunction withTypeConditions(packageDir: string, exportsMap: Record<string, unknown>): Record<string, unknown> {\n const next: Record<string, unknown> = {};\n for (const [subpath, target] of Object.entries(exportsMap)) {\n if (subpath === './package.json' || target === null || typeof target !== 'object') {\n next[subpath] = target;\n continue;\n }\n const conditions = target as Record<string, string>;\n const esm = conditions.import ?? conditions.default;\n const types = esm?.endsWith('.mjs') === true ? `${esm.slice(0, -'.mjs'.length)}.d.mts` : undefined;\n // Ahead of import and require: a resolver takes the first condition it\n // matches, so a types-aware one must not fall through to the runtime file.\n next[subpath] =\n conditions.types === undefined && types !== undefined && fs.existsSync(path.join(packageDir, types))\n ? { types, ...conditions }\n : conditions;\n }\n return next;\n}\n\n/**\n * Everything the manifest can say about a package that its tree already says.\n *\n * The three host blocks are pure restatements of the folder tree: which hosts\n * the package contributes to, at which scopes, and which frame types the\n * browser claims. Leaving them hand-written is what made a package declare the\n * same thing in six places, so the build writes them.\n *\n * Returns the manifest rather than writing it, so a caller can diff it and a\n * test can read it without a filesystem.\n */\nexport function syncManifest(input: ManifestSync): Record<string, unknown> {\n const { packageDir, graph, targets, pluginId } = input;\n const manifest = { ...input.manifest };\n\n if (manifest.exports !== undefined && typeof manifest.exports === 'object') {\n const exportsMap = withTypeConditions(packageDir, manifest.exports as Record<string, unknown>);\n // The browser bundle is a real entry point now that it is built rather\n // than shipped as source, so it is exported like the other two. Import\n // only: its own config emits ESM and no declarations, because nothing\n // imports it as a typed module.\n //\n // Added from the target rather than from the file existing, because this\n // runs on the node build's build:done and the browser build follows it.\n if (targets.includes('cli')) {\n exportsMap[PI_EXPORT] = { types: PI_TYPES, import: PI_IMPORT, require: PI_REQUIRE };\n } else delete exportsMap[PI_EXPORT];\n if (targets.includes('web')) exportsMap[WEB_EXPORT] = { import: WEB_DIST };\n else delete exportsMap[WEB_EXPORT];\n manifest.exports = Object.fromEntries(\n Object.entries(exportsMap).sort(([left], [right]) =>\n left === './package.json' ? 1 : right === './package.json' ? -1 : left.localeCompare(right),\n ),\n );\n }\n\n if (targets.includes('cli')) manifest.pi = { extensions: ['./dist/extensions/pi.mjs'] };\n else delete manifest.pi;\n\n if (targets.includes('server')) {\n const contracts = fs.existsSync(path.join(packageDir, API_CONTRACTS_SOURCE))\n ? {\n contracts: {\n entry: `./${API_CONTRACTS_SOURCE}`,\n dist: `./dist/${API_CONTRACTS_ENTRY}.mjs`,\n },\n }\n : {};\n manifest.doompiServer = {\n entry: `./${GENERATED_DIR}/server.ts`,\n dist: './dist/extensions/server.mjs',\n scopes: serverScopes(graph),\n ...contracts,\n };\n } else delete manifest.doompiServer;\n\n if (targets.includes('web')) {\n manifest.doompiWeb = {\n pluginId,\n channels: channels(graph, manifest),\n // The built browser bundle, not the source. Its own tsdown config\n // targets a browser and leaves every bare specifier external, so the\n // cockpit resolves one React rather than bundling a copy per plugin.\n client: WEB_DIST,\n scopes: webScopes(graph),\n };\n } else delete manifest.doompiWeb;\n\n return manifest;\n}\n\n/** Updates only the MCP-owned manifest block, leaving normal build metadata untouched. */\nexport function syncMcpManifest(manifest: Record<string, unknown>, enabled: boolean): Record<string, unknown> {\n const next = { ...manifest };\n if (enabled) next.doompiMcp = { entry: MCP_ENTRY, dist: MCP_DIST, scopes: ['session'] };\n else delete next.doompiMcp;\n return next;\n}\n\n/** Writes the synced manifest when it differs, so a second build is a no-op. */\nexport function writeManifest(input: ManifestSync & { readonly targets: readonly BuildTarget[] }): boolean {\n const manifestPath = path.join(input.packageDir, 'package.json');\n const next = `${JSON.stringify(syncManifest(input), null, 2)}\\n`;\n if (fs.readFileSync(manifestPath, 'utf8') === next) return false;\n fs.writeFileSync(manifestPath, next);\n return true;\n}\n\n/** Writes only MCP metadata after an isolated build. */\nexport function writeMcpManifest(packageDir: string, enabled: boolean): boolean {\n const manifestPath = path.join(packageDir, 'package.json');\n const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;\n const next = `${JSON.stringify(syncMcpManifest(manifest, enabled), null, 2)}\\n`;\n if (fs.readFileSync(manifestPath, 'utf8') === next) return false;\n fs.writeFileSync(manifestPath, next);\n return true;\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { BuildTarget } from "../resolveTarget/type.mjs";
2
+ import { ManifestSync } from "./type.mjs";
3
+ //#region src/services/syncManifest/index.d.ts
4
+ /**
5
+ * Everything the manifest can say about a package that its tree already says.
6
+ *
7
+ * The three host blocks are pure restatements of the folder tree: which hosts
8
+ * the package contributes to, at which scopes, and which frame types the
9
+ * browser claims. Leaving them hand-written is what made a package declare the
10
+ * same thing in six places, so the build writes them.
11
+ *
12
+ * Returns the manifest rather than writing it, so a caller can diff it and a
13
+ * test can read it without a filesystem.
14
+ */
15
+ export declare function syncManifest(input: ManifestSync): Record<string, unknown>;
16
+ /** Writes the synced manifest when it differs, so a second build is a no-op. */
17
+ export declare function writeManifest(input: ManifestSync & {
18
+ readonly targets: readonly BuildTarget[];
19
+ }): boolean;
20
+ //#endregion
21
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/syncManifest/index.ts"],"mappings":";;;;;;;;;;;;;;wBAqGgB,aAAa,OAAO,eAAe;;wBAqEnC,cAAc,OAAO;WAA0B,kBAAkB"}
@@ -0,0 +1,2 @@
1
+ import{GENERATED_DIR as e}from"../../constants/layout.mjs";import{toSnake as t}from"../identity/index.mjs";import n from"node:fs";import r from"node:path";const i=[`global`,`workspace`,`session`],a=`apiContracts`,o=`src/exports/${a}.ts`,s=`./extensions/pi`,c=`./extensions/web`,l=`./dist/extensions/web.mjs`,u=`./${e}/mcp.ts`;function d(e){let t=e.entries.filter(e=>e.side===`backend`);if(t.length===0)return[];let n=Math.min(...t.map(e=>i.indexOf(e.scope)));return i.slice(n)}function f(e){let t=new Set(e.entries.filter(e=>e.side===`frontend`).map(e=>e.scope));return i.filter(e=>t.has(e))}function p(e,n){return[...new Set(e.entries.filter(e=>e.side===`frontend`&&e.surface===`channel`).map(e=>t(e.name)))].sort()}function m(e,t){let i={};for(let[a,o]of Object.entries(t)){if(a===`./package.json`||typeof o!=`object`||!o){i[a]=o;continue}let t=o,s=t.import??t.default,c=s?.endsWith(`.mjs`)===!0?`${s.slice(0,-4)}.d.mts`:void 0;i[a]=t.types===void 0&&c!==void 0&&n.existsSync(r.join(e,c))?{types:c,...t}:t}return i}function h(t){let{packageDir:i,graph:u,targets:h,pluginId:g}=t,_={...t.manifest};if(_.exports!==void 0&&typeof _.exports==`object`){let e=m(i,_.exports);h.includes(`cli`)?e[s]={types:`./dist/extensions/pi.d.mts`,import:`./dist/extensions/pi.mjs`,require:`./dist/extensions/pi.cjs`}:delete e[s],h.includes(`web`)?e[c]={import:l}:delete e[c],_.exports=Object.fromEntries(Object.entries(e).sort(([e],[t])=>e===`./package.json`?1:t===`./package.json`?-1:e.localeCompare(t)))}if(h.includes(`cli`)?_.pi={extensions:[`./dist/extensions/pi.mjs`]}:delete _.pi,h.includes(`server`)){let t=n.existsSync(r.join(i,o))?{contracts:{entry:`./${o}`,dist:`./dist/${a}.mjs`}}:{};_.doompiServer={entry:`./${e}/server.ts`,dist:`./dist/extensions/server.mjs`,scopes:d(u),...t}}else delete _.doompiServer;return h.includes(`web`)?_.doompiWeb={pluginId:g,channels:p(u,_),client:l,scopes:f(u)}:delete _.doompiWeb,_}function g(e,t){let n={...e};return t?n.doompiMcp={entry:u,dist:`./dist/extensions/mcp.mjs`,scopes:[`session`]}:delete n.doompiMcp,n}function _(e){let t=r.join(e.packageDir,`package.json`),i=`${JSON.stringify(h(e),null,2)}\n`;return n.readFileSync(t,`utf8`)!==i&&(n.writeFileSync(t,i),!0)}function v(e,t){let i=r.join(e,`package.json`),a=JSON.parse(n.readFileSync(i,`utf8`)),o=`${JSON.stringify(g(a,t),null,2)}\n`;return n.readFileSync(i,`utf8`)!==o&&(n.writeFileSync(i,o),!0)}export{h as syncManifest,g as syncMcpManifest,_ as writeManifest,v as writeMcpManifest};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/syncManifest/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { GENERATED_DIR } from '../../constants/layout';\nimport type { ExtensionGraph, ExtensionScope } from '../../types/extensionGraph';\nimport { toSnake } from '../identity';\nimport type { BuildTarget } from '../resolveTarget/type';\nimport type { ManifestSync } from './type';\n\nconst SCOPE_ORDER: readonly ExtensionScope[] = ['global', 'workspace', 'session'];\n\nconst API_CONTRACTS_ENTRY = 'apiContracts';\nconst API_CONTRACTS_SOURCE = `src/exports/${API_CONTRACTS_ENTRY}.ts`;\n\nconst PI_EXPORT = './extensions/pi';\nconst PI_TYPES = './dist/extensions/pi.d.mts';\nconst PI_IMPORT = './dist/extensions/pi.mjs';\nconst PI_REQUIRE = './dist/extensions/pi.cjs';\nconst WEB_EXPORT = './extensions/web';\nconst WEB_DIST = './dist/extensions/web.mjs';\nconst MCP_DIST = './dist/extensions/mcp.mjs';\nconst MCP_ENTRY = `./${GENERATED_DIR}/mcp.ts`;\n\n/**\n * Which scopes a server facet declares.\n *\n * The generated facet cascades: a contribution declared at global registers at\n * global, workspace and session alike, because the server reads exactly one\n * scope declaration and nothing above it. So the declared set runs from the\n * shallowest contribution downward.\n */\nfunction serverScopes(graph: ExtensionGraph): ExtensionScope[] {\n const declared = graph.entries.filter((entry) => entry.side === 'backend');\n if (declared.length === 0) return [];\n const shallowest = Math.min(...declared.map((entry) => SCOPE_ORDER.indexOf(entry.scope)));\n return SCOPE_ORDER.slice(shallowest);\n}\n\n/**\n * Which scopes a cockpit plugin declares.\n *\n * The cockpit does not cascade the way the server does. It merges the scopes\n * it is given, so a plugin declares exactly the ones it contributes to.\n */\nfunction webScopes(graph: ExtensionGraph): ExtensionScope[] {\n const declared = new Set(graph.entries.filter((entry) => entry.side === 'frontend').map((entry) => entry.scope));\n return SCOPE_ORDER.filter((scope) => declared.has(scope));\n}\n\n/** The frame types the browser half claims, which the cockpit reads before loading it. */\nfunction channels(graph: ExtensionGraph, _manifest: Record<string, unknown>): string[] {\n return [\n ...new Set(\n graph.entries\n .filter((entry) => entry.side === 'frontend' && entry.surface === 'channel')\n .map((entry) => toSnake(entry.name)),\n ),\n ].sort();\n}\n\n/**\n * Restores the `types` condition tsdown's exports generation omits.\n *\n * tsdown derives the map from the built chunks, which is what makes it follow\n * the folder tree. But its `genSubExport` only ever emits `import`, `require`\n * or `default`: a declaration chunk contributes nothing to a subpath, so only\n * the root gets a top-level `types`. TypeScript still resolves a sibling\n * `.d.mts` under node16 and bundler resolution, but publint and attw read the\n * condition, so it belongs in the map.\n */\nfunction withTypeConditions(packageDir: string, exportsMap: Record<string, unknown>): Record<string, unknown> {\n const next: Record<string, unknown> = {};\n for (const [subpath, target] of Object.entries(exportsMap)) {\n if (subpath === './package.json' || target === null || typeof target !== 'object') {\n next[subpath] = target;\n continue;\n }\n const conditions = target as Record<string, string>;\n const esm = conditions.import ?? conditions.default;\n const types = esm?.endsWith('.mjs') === true ? `${esm.slice(0, -'.mjs'.length)}.d.mts` : undefined;\n // Ahead of import and require: a resolver takes the first condition it\n // matches, so a types-aware one must not fall through to the runtime file.\n next[subpath] =\n conditions.types === undefined && types !== undefined && fs.existsSync(path.join(packageDir, types))\n ? { types, ...conditions }\n : conditions;\n }\n return next;\n}\n\n/**\n * Everything the manifest can say about a package that its tree already says.\n *\n * The three host blocks are pure restatements of the folder tree: which hosts\n * the package contributes to, at which scopes, and which frame types the\n * browser claims. Leaving them hand-written is what made a package declare the\n * same thing in six places, so the build writes them.\n *\n * Returns the manifest rather than writing it, so a caller can diff it and a\n * test can read it without a filesystem.\n */\nexport function syncManifest(input: ManifestSync): Record<string, unknown> {\n const { packageDir, graph, targets, pluginId } = input;\n const manifest = { ...input.manifest };\n\n if (manifest.exports !== undefined && typeof manifest.exports === 'object') {\n const exportsMap = withTypeConditions(packageDir, manifest.exports as Record<string, unknown>);\n // The browser bundle is a real entry point now that it is built rather\n // than shipped as source, so it is exported like the other two. Import\n // only: its own config emits ESM and no declarations, because nothing\n // imports it as a typed module.\n //\n // Added from the target rather than from the file existing, because this\n // runs on the node build's build:done and the browser build follows it.\n if (targets.includes('cli')) {\n exportsMap[PI_EXPORT] = { types: PI_TYPES, import: PI_IMPORT, require: PI_REQUIRE };\n } else delete exportsMap[PI_EXPORT];\n if (targets.includes('web')) exportsMap[WEB_EXPORT] = { import: WEB_DIST };\n else delete exportsMap[WEB_EXPORT];\n manifest.exports = Object.fromEntries(\n Object.entries(exportsMap).sort(([left], [right]) =>\n left === './package.json' ? 1 : right === './package.json' ? -1 : left.localeCompare(right),\n ),\n );\n }\n\n if (targets.includes('cli')) manifest.pi = { extensions: ['./dist/extensions/pi.mjs'] };\n else delete manifest.pi;\n\n if (targets.includes('server')) {\n const contracts = fs.existsSync(path.join(packageDir, API_CONTRACTS_SOURCE))\n ? {\n contracts: {\n entry: `./${API_CONTRACTS_SOURCE}`,\n dist: `./dist/${API_CONTRACTS_ENTRY}.mjs`,\n },\n }\n : {};\n manifest.doompiServer = {\n entry: `./${GENERATED_DIR}/server.ts`,\n dist: './dist/extensions/server.mjs',\n scopes: serverScopes(graph),\n ...contracts,\n };\n } else delete manifest.doompiServer;\n\n if (targets.includes('web')) {\n manifest.doompiWeb = {\n pluginId,\n channels: channels(graph, manifest),\n // The built browser bundle, not the source. Its own tsdown config\n // targets a browser and leaves every bare specifier external, so the\n // cockpit resolves one React rather than bundling a copy per plugin.\n client: WEB_DIST,\n scopes: webScopes(graph),\n };\n } else delete manifest.doompiWeb;\n\n return manifest;\n}\n\n/** Updates only the MCP-owned manifest block, leaving normal build metadata untouched. */\nexport function syncMcpManifest(manifest: Record<string, unknown>, enabled: boolean): Record<string, unknown> {\n const next = { ...manifest };\n if (enabled) next.doompiMcp = { entry: MCP_ENTRY, dist: MCP_DIST, scopes: ['session'] };\n else delete next.doompiMcp;\n return next;\n}\n\n/** Writes the synced manifest when it differs, so a second build is a no-op. */\nexport function writeManifest(input: ManifestSync & { readonly targets: readonly BuildTarget[] }): boolean {\n const manifestPath = path.join(input.packageDir, 'package.json');\n const next = `${JSON.stringify(syncManifest(input), null, 2)}\\n`;\n if (fs.readFileSync(manifestPath, 'utf8') === next) return false;\n fs.writeFileSync(manifestPath, next);\n return true;\n}\n\n/** Writes only MCP metadata after an isolated build. */\nexport function writeMcpManifest(packageDir: string, enabled: boolean): boolean {\n const manifestPath = path.join(packageDir, 'package.json');\n const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;\n const next = `${JSON.stringify(syncMcpManifest(manifest, enabled), null, 2)}\\n`;\n if (fs.readFileSync(manifestPath, 'utf8') === next) return false;\n fs.writeFileSync(manifestPath, next);\n return true;\n}\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { ExtensionGraph } from "../../types/extensionGraph.mjs";
2
+ import { BuildTarget } from "../resolveTarget/type.mjs";
3
+ //#region src/services/syncManifest/type.d.ts
4
+ /** Everything deriving a manifest needs, so nothing has to rescan. */
5
+ export interface ManifestSync {
6
+ readonly packageDir: string;
7
+ /** The manifest as read, which the sync returns a copy of. */
8
+ readonly manifest: Record<string, unknown>;
9
+ readonly graph: ExtensionGraph;
10
+ /** Hosts this package contributes to, as the generator resolved them. */
11
+ readonly targets: readonly BuildTarget[];
12
+ readonly pluginId: string;
13
+ }
14
+ //#endregion
15
+ //# sourceMappingURL=type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type.d.mts","names":[],"sources":["../../../src/services/syncManifest/type.ts"],"mappings":";;;;iBAIiB;WACN;;WAEA,UAAU;WACV,OAAO;;WAEP,kBAAkB;WAClB"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../constants/layout.cjs"),n=require("../generate/index.cjs"),r=require("../syncManifest/index.cjs");let i=require("node:fs");i=e.__toESM(i,1);let a=require("node:path");a=e.__toESM(a,1);const o=`apiContracts`;function s(e,t){let n=a.default.join(e,t);if(!i.default.existsSync(n))return{};let r={};for(let e of i.default.readdirSync(n)){if(!e.endsWith(`.ts`)||e.endsWith(`.d.ts`))continue;let n=e.slice(0,-3);r[n]=`${t}/${e}`}return r}function c(e,t,n){let r=a.default.join(e,t);if(!i.default.existsSync(r))return{};let o={};for(let e of i.default.readdirSync(r,{encoding:`utf8`,recursive:!0})){if(!n(e))continue;let r=e.split(a.default.sep).join(`/`);o[`./${t}/${r}`]=`./${t}/${r}`}return o}function l(e){let t=JSON.parse(i.default.readFileSync(a.default.join(e,`package.json`),`utf8`)),n=t.exports!==null&&typeof t.exports==`object`&&!Array.isArray(t.exports)?Object.fromEntries(Object.entries(t.exports).filter(([,e])=>typeof e==`string`)):{};return{...c(e,`skills`,e=>a.default.basename(e)===`SKILL.md`),...c(e,`themes`,e=>e.endsWith(`.json`)),...n}}function u(){return{name:`doompi-query-assets`,resolveId(e,t){let n=e.indexOf(`?`);if(n<0||t===void 0||!e.startsWith(`.`))return;let r=t.split(`?`)[0];return`${a.default.resolve(a.default.dirname(r),e.slice(0,n))}${e.slice(n)}`},load(e){let t=e.indexOf(`?`);if(t<0)return;let n=e.slice(0,t),r=new URLSearchParams(e.slice(t+1));if(!r.has(`url`))return;let o=r.has(`worker`);return`export default import.meta.ROLLUP_FILE_URL_${this.emitFile(o?{type:`chunk`,id:n,name:a.default.basename(n,a.default.extname(n))}:{type:`asset`,name:a.default.basename(n),source:i.default.readFileSync(n)})};`}}}function d(){return{entry:{"extensions/web":`${t.GENERATED_DIR}/web.ts`},clean:!1,dts:!1,exports:!1,format:[`esm`],platform:`browser`,fixedExtension:!0,external:e=>!e.startsWith(`.`)&&!a.default.isAbsolute(e),plugins:[u()],sourcemap:!0,unbundle:!1,tsconfig:`tsconfig.web.json`}}function f(e){let t=a.default.join(e,`dist`);if(i.default.existsSync(t))for(let e of i.default.readdirSync(t,{recursive:!0,encoding:`utf8`})){let n=e.split(a.default.sep).join(`/`);if(!/^(?:extensions\/mcp|.+\.mcp)\.(?:mjs|cjs|d\.mts|d\.cts)(?:\.map)?$/u.test(n))continue;let r=a.default.join(t,e);i.default.lstatSync(r).isFile()&&i.default.rmSync(r)}}function p(e={packageDir:process.cwd()}){let c=e.packageDir??process.cwd(),u=n.generateExtension({...e,packageDir:c,check:e.check??!1});for(let e of u.notices)process.stderr.write(`[doompi-build] ${e.path}: ${e.message}\n`);let p=e.target===`mcp`;p&&!e.check&&f(c);let m=p&&!u.targets.includes(`mcp`),h=`\0doompi-empty-mcp`;m&&!e.check&&r.writeMcpManifest(c,!1);let g=p?u.targets.includes(`mcp`)?{"extensions/mcp":`${t.GENERATED_DIR}/mcp.ts`}:{"extensions/mcp":h}:{...s(c,e.exportsDir??`src/exports`),...u.targets.includes(`cli`)?{"extensions/pi":`${t.GENERATED_DIR}/pi.ts`}:{},...u.targets.includes(`server`)?{"extensions/server":`${t.GENERATED_DIR}/server.ts`}:{},...e.entry},_={clean:!1,dts:!m&&{incremental:!0,parallel:!1,eager:!0},...m?{write:!1,plugins:[{name:`doompi-empty-mcp`,resolveId:e=>e===h?h:void 0,load:e=>e===h?`export {};`:void 0}]}:{},exports:!p&&{customExports:e=>({...l(c),...e})},format:{esm:{},cjs:{dts:!1}},platform:`node`,outExtensions:({format:e})=>({js:e===`es`?`.mjs`:`.cjs`,dts:e===`es`?`.d.mts`:`.d.cts`}),sourcemap:!0},v=(e,t)=>({..._,entry:e,unbundle:!1,hooks:{"build:done":()=>{if(p){r.writeMcpManifest(c,u.targets.includes(`mcp`));return}r.writeManifest({packageDir:c,manifest:JSON.parse(i.default.readFileSync(a.default.join(c,`package.json`),`utf8`)),graph:u.graph,targets:u.targets,pluginId:u.pluginId})}}}),y=!p&&o in g?[((e,t)=>({..._,entry:e,unbundle:t}))({[o]:g[o]},!1),v(Object.fromEntries(Object.entries(g).filter(([e])=>e!==o)),!0)]:[v(g,!p)];return!p&&u.targets.includes(`web`)?[...y,d()]:y.length===1?y[0]:y}exports.doompiExtension=p;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["path","fs","GENERATED_DIR","generateExtension"],"sources":["../../../src/services/tsdownPreset/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { GENERATED_DIR } from '../../constants/layout';\nimport { generateExtension } from '../generate';\nimport type { GenerateOptions } from '../generate/type';\nimport { writeManifest, writeMcpManifest } from '../syncManifest';\n\ninterface EmittedFile {\n readonly type: 'asset' | 'chunk';\n readonly id?: string;\n readonly name: string;\n readonly source?: Buffer;\n}\n\ninterface QueryAssetPlugin {\n readonly name: string;\n resolveId(source: string, importer: string | undefined): string | undefined;\n load(this: { emitFile(file: EmittedFile): string }, id: string): string | undefined;\n}\n\n/** The shape a tsdown config needs from this preset, without importing tsdown's types. */\ninterface BrowserPresetConfig {\n entry: Record<string, string>;\n clean: boolean;\n dts: false;\n exports: boolean;\n format: 'esm'[];\n platform: 'browser';\n fixedExtension: boolean;\n external: (id: string) => boolean;\n plugins: QueryAssetPlugin[];\n sourcemap: boolean;\n unbundle: boolean;\n tsconfig: string;\n}\n\ninterface GeneratedExports {\n customExports: (exports: Record<string, unknown>) => Record<string, unknown>;\n}\n\ninterface BasePresetConfig {\n entry: Record<string, string>;\n clean: boolean;\n dts: false | { incremental: boolean; parallel: boolean; eager: boolean };\n write?: false;\n plugins?: QueryAssetPlugin[];\n exports: boolean | GeneratedExports;\n format: { esm: Record<string, never>; cjs: { dts: false } };\n platform: 'node';\n sourcemap: boolean;\n unbundle: boolean;\n outExtensions: ({ format }: { format: string }) => {\n js: string;\n dts: string;\n };\n}\n\ninterface PresetConfig extends BasePresetConfig {\n hooks?: { 'build:done': () => void };\n}\n\nconst DEFAULT_EXPORTS_DIR = 'src/exports';\nconst API_CONTRACTS_ENTRY = 'apiContracts';\n\n/** `src/exports/apiContracts.ts` becomes the `apiContracts` entry; index keeps its name. */\nfunction exportEntries(packageDir: string, exportsDir: string): Record<string, string> {\n const absolute = path.join(packageDir, exportsDir);\n if (!fs.existsSync(absolute)) return {};\n const entries: Record<string, string> = {};\n for (const file of fs.readdirSync(absolute)) {\n if (!file.endsWith('.ts') || file.endsWith('.d.ts')) continue;\n const stem = file.slice(0, -'.ts'.length);\n entries[stem] = `${exportsDir}/${file}`;\n }\n return entries;\n}\n\n/** Static resources receive package export entries without becoming tsdown inputs. */\nfunction resourceEntries(\n packageDir: string,\n directory: string,\n matches: (relative: string) => boolean,\n): Record<string, string> {\n const root = path.join(packageDir, directory);\n if (!fs.existsSync(root)) return {};\n const entries: Record<string, string> = {};\n for (const relative of fs.readdirSync(root, { encoding: 'utf8', recursive: true })) {\n if (!matches(relative)) continue;\n const normalized = relative.split(path.sep).join('/');\n entries[`./${directory}/${normalized}`] = `./${directory}/${normalized}`;\n }\n return entries;\n}\n\n/** Preserves declared resources and discovers default skill and theme resource exports. */\nfunction staticExports(packageDir: string): Record<string, unknown> {\n const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >;\n const declared =\n manifest.exports !== null && typeof manifest.exports === 'object' && !Array.isArray(manifest.exports)\n ? Object.fromEntries(Object.entries(manifest.exports).filter(([, target]) => typeof target === 'string'))\n : {};\n return {\n ...resourceEntries(packageDir, 'skills', (relative) => path.basename(relative) === 'SKILL.md'),\n ...resourceEntries(packageDir, 'themes', (relative) => relative.endsWith('.json')),\n ...declared,\n };\n}\n/**\n * One tsdown config for a folder-routed extension.\n *\n * Entries come from the tree rather than a hand-written list, and the manifest\n * blocks that restate the tree are written after the build rather than kept in\n * step by hand. Browser contributions are emitted through a second bundled\n * config because their routed source is not published.\n */\nexport interface ExtensionPresetOptions extends GenerateOptions {\n /** Extra tsdown entries beyond the derived ones. */\n readonly entry?: Record<string, string>;\n /** Public export modules, package-relative. Defaults to `src/exports`. */\n readonly exportsDir?: string;\n}\n\n/**\n * Implements the Vite query imports extension browser code already uses.\n *\n * `?url` copies an opaque asset and `?worker&url` emits a separately bundled\n * worker chunk. Rolldown then replaces its file reference with an URL relative\n * to the extension bundle, which remains valid when the cockpit composes it.\n */\nfunction queryAssetPlugin(): QueryAssetPlugin {\n return {\n name: 'doompi-query-assets',\n resolveId(source, importer) {\n const queryAt = source.indexOf('?');\n if (queryAt < 0 || importer === undefined || !source.startsWith('.')) return undefined;\n const importerFile = importer.split('?')[0] as string;\n return `${path.resolve(path.dirname(importerFile), source.slice(0, queryAt))}${source.slice(queryAt)}`;\n },\n load(id) {\n const queryAt = id.indexOf('?');\n if (queryAt < 0) return undefined;\n const file = id.slice(0, queryAt);\n const query = new URLSearchParams(id.slice(queryAt + 1));\n if (!query.has('url')) return undefined;\n const worker = query.has('worker');\n const reference = this.emitFile(\n worker\n ? { type: 'chunk', id: file, name: path.basename(file, path.extname(file)) }\n : { type: 'asset', name: path.basename(file), source: fs.readFileSync(file) },\n );\n return `export default import.meta.ROLLUP_FILE_URL_${reference};`;\n },\n };\n}\n\n/** The separately bundled browser half of a routed extension. */\nfunction browserConfig(): BrowserPresetConfig {\n return {\n entry: { 'extensions/web': `${GENERATED_DIR}/web.ts` },\n clean: false,\n // No declarations. Nothing imports this as a typed module: it has no\n // export subpath, and the cockpit consumes it as a bundle. The browser\n // half is still typechecked, by tsconfig.web.json in the typecheck script.\n dts: false,\n exports: false,\n format: ['esm'],\n platform: 'browser',\n // platform browser turns tsdown's fixedExtension off, which would emit\n // .js. The manifest names .mjs like the other two entries, so ask for it.\n fixedExtension: true,\n // Every bare specifier stays external and the cockpit resolves it, which\n // is what keeps one React and one store in the page no matter how many\n // plugins are installed. An allowlist would have to be kept in step with\n // the cockpit's; this cannot drift.\n //\n // A predicate rather than a pattern: rolldown tests `external` against the\n // resolved id, so /^[^.]/ also matches the absolute path of this package's\n // own routed file and would leave a dangling import to unbuilt .tsx.\n external: (id: string) => !id.startsWith('.') && !path.isAbsolute(id),\n plugins: [queryAssetPlugin()],\n sourcemap: true,\n // Bundled, unlike the node half. Unbundling would emit an import to the\n // routed source file, which is not published and is .tsx besides. One\n // module with only bare specifiers external is what the cockpit wants.\n unbundle: false,\n // Browser routes are excluded from the node tsconfig. Every browser-capable\n // extension owns this focused config for its matching typecheck.\n tsconfig: 'tsconfig.web.json',\n };\n}\n\n/** Removes only filenames owned by the isolated MCP build, including obsolete declarations. */\nfunction cleanMcpOutput(packageDir: string): void {\n const dist = path.join(packageDir, 'dist');\n if (!fs.existsSync(dist)) return;\n for (const relative of fs.readdirSync(dist, { recursive: true, encoding: 'utf8' })) {\n const normalized = relative.split(path.sep).join('/');\n if (!/^(?:extensions\\/mcp|.+\\.mcp)\\.(?:mjs|cjs|d\\.mts|d\\.cts)(?:\\.map)?$/u.test(normalized)) continue;\n const absolute = path.join(dist, relative);\n if (fs.lstatSync(absolute).isFile()) fs.rmSync(absolute);\n }\n}\n\nexport function doompiExtension(\n options: ExtensionPresetOptions = { packageDir: process.cwd() },\n): PresetConfig | (PresetConfig | BrowserPresetConfig)[] {\n const packageDir = options.packageDir ?? process.cwd();\n const result = generateExtension({ ...options, packageDir, check: options.check ?? false });\n\n for (const notice of result.notices) process.stderr.write(`[doompi-build] ${notice.path}: ${notice.message}\\n`);\n\n const mcpOnly = options.target === 'mcp';\n if (mcpOnly && !options.check) cleanMcpOutput(packageDir);\n const emptyMcp = mcpOnly && !result.targets.includes('mcp');\n const emptyEntry = '\\0doompi-empty-mcp';\n if (emptyMcp && !options.check) writeMcpManifest(packageDir, false);\n const entry: Record<string, string> = mcpOnly\n ? result.targets.includes('mcp')\n ? { 'extensions/mcp': `${GENERATED_DIR}/mcp.ts` }\n : { 'extensions/mcp': emptyEntry }\n : {\n ...exportEntries(packageDir, options.exportsDir ?? DEFAULT_EXPORTS_DIR),\n ...(result.targets.includes('cli') ? { 'extensions/pi': `${GENERATED_DIR}/pi.ts` } : {}),\n ...(result.targets.includes('server') ? { 'extensions/server': `${GENERATED_DIR}/server.ts` } : {}),\n ...options.entry,\n };\n\n const baseNode: Omit<BasePresetConfig, 'entry' | 'unbundle'> = {\n // MCP shares dist/ with the normal build, so neither build may erase the other.\n clean: false,\n dts: emptyMcp ? false : { incremental: true, parallel: false, eager: true },\n // tsdown rejects both an empty entry and an empty config array. A virtual\n // input completes its normal lifecycle without publishing an empty facet.\n ...(emptyMcp\n ? {\n write: false as const,\n plugins: [\n {\n name: 'doompi-empty-mcp',\n resolveId: (source: string) => (source === emptyEntry ? emptyEntry : undefined),\n load: (id: string) => (id === emptyEntry ? 'export {};' : undefined),\n },\n ],\n }\n : {}),\n exports: mcpOnly\n ? false\n : {\n customExports: (generated) => ({ ...staticExports(packageDir), ...generated }),\n },\n // CommonJS keeps its JavaScript but emits no declarations. Every export map\n // resolves types through .d.mts, and a CJS declaration build would rerun\n // the same whole-program tsgo emit a second time.\n format: { esm: {}, cjs: { dts: false } },\n // No minify. This is a library build, and a mangled stack trace inside a\n // published extension is far more expensive than the bytes it saves.\n // Node output uses fixed extensions, so pi.extensions and doompiServer.dist\n // both land on the .mjs filenames their hosts require.\n platform: 'node',\n outExtensions: ({ format }) => ({\n js: format === 'es' ? '.mjs' : '.cjs',\n dts: format === 'es' ? '.d.mts' : '.d.cts',\n }),\n sourcemap: true,\n };\n\n // Contract bundles are published as a self-contained graph. Normal extension\n // entries remain unbundled for routed runtime resources, while MCP remains\n // isolated from the shared dist directory.\n const node = (entries: Record<string, string>, _unbundle: boolean): PresetConfig => ({\n ...baseNode,\n entry: entries,\n // Public entry facades must remain bundled. Otherwise their relative\n // imports target unbundled implementation chunks, which Vite treats as\n // source files when resolving a workspace package.\n unbundle: false,\n hooks: {\n 'build:done': () => {\n if (mcpOnly) {\n writeMcpManifest(packageDir, result.targets.includes('mcp'));\n return;\n }\n writeManifest({\n packageDir,\n manifest: JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >,\n graph: result.graph,\n targets: result.targets,\n pluginId: result.pluginId,\n });\n },\n },\n });\n const unsyncedNode = (entries: Record<string, string>, unbundle: boolean): PresetConfig => ({\n ...baseNode,\n entry: entries,\n unbundle,\n });\n const configs =\n !mcpOnly && API_CONTRACTS_ENTRY in entry\n ? [\n unsyncedNode({ [API_CONTRACTS_ENTRY]: entry[API_CONTRACTS_ENTRY] }, false),\n node(Object.fromEntries(Object.entries(entry).filter(([name]) => name !== API_CONTRACTS_ENTRY)), true),\n ]\n : [node(entry, !mcpOnly)];\n if (!mcpOnly && result.targets.includes('web')) return [...configs, browserConfig()];\n return configs.length === 1 ? configs[0]! : configs;\n}\n"],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ import { GenerateOptions } from "../generate/type.mjs";
2
+ //#region src/services/tsdownPreset/index.d.ts
3
+ interface EmittedFile {
4
+ readonly type: 'asset' | 'chunk';
5
+ readonly id?: string;
6
+ readonly name: string;
7
+ readonly source?: Buffer;
8
+ }
9
+ interface QueryAssetPlugin {
10
+ readonly name: string;
11
+ resolveId(source: string, importer: string | undefined): string | undefined;
12
+ load(this: {
13
+ emitFile(file: EmittedFile): string;
14
+ }, id: string): string | undefined;
15
+ }
16
+ /** The shape a tsdown config needs from this preset, without importing tsdown's types. */
17
+ interface BrowserPresetConfig {
18
+ entry: Record<string, string>;
19
+ clean: boolean;
20
+ dts: false;
21
+ exports: boolean;
22
+ format: 'esm'[];
23
+ platform: 'browser';
24
+ fixedExtension: boolean;
25
+ external: (id: string) => boolean;
26
+ plugins: QueryAssetPlugin[];
27
+ sourcemap: boolean;
28
+ unbundle: boolean;
29
+ tsconfig: string;
30
+ }
31
+ interface GeneratedExports {
32
+ customExports: (exports: Record<string, unknown>) => Record<string, unknown>;
33
+ }
34
+ interface BasePresetConfig {
35
+ entry: Record<string, string>;
36
+ clean: boolean;
37
+ dts: false | {
38
+ incremental: boolean;
39
+ parallel: boolean;
40
+ eager: boolean;
41
+ };
42
+ write?: false;
43
+ plugins?: QueryAssetPlugin[];
44
+ exports: boolean | GeneratedExports;
45
+ format: {
46
+ esm: Record<string, never>;
47
+ cjs: {
48
+ dts: false;
49
+ };
50
+ };
51
+ platform: 'node';
52
+ sourcemap: boolean;
53
+ unbundle: boolean;
54
+ outExtensions: ({ format }: {
55
+ format: string;
56
+ }) => {
57
+ js: string;
58
+ dts: string;
59
+ };
60
+ }
61
+ interface PresetConfig extends BasePresetConfig {
62
+ hooks?: {
63
+ 'build:done': () => void;
64
+ };
65
+ }
66
+ /**
67
+ * One tsdown config for a folder-routed extension.
68
+ *
69
+ * Entries come from the tree rather than a hand-written list, and the manifest
70
+ * blocks that restate the tree are written after the build rather than kept in
71
+ * step by hand. Browser contributions are emitted through a second bundled
72
+ * config because their routed source is not published.
73
+ */
74
+ export interface ExtensionPresetOptions extends GenerateOptions {
75
+ /** Extra tsdown entries beyond the derived ones. */
76
+ readonly entry?: Record<string, string>;
77
+ /** Public export modules, package-relative. Defaults to `src/exports`. */
78
+ readonly exportsDir?: string;
79
+ }
80
+ export declare function doompiExtension(options?: ExtensionPresetOptions): PresetConfig | (PresetConfig | BrowserPresetConfig)[];
81
+ //#endregion
82
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/tsdownPreset/index.ts"],"mappings":";;UAQU;WACC;WACA;WACA;WACA,SAAS;;UAGV;WACC;EACT,UAAU,gBAAgB;EAC1B,KAAK;IAAQ,SAAS,MAAM;KAAwB;;;UAI5C;EACR,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;EACX,SAAS;EACT;EACA;EACA;;UAGQ;EACR,gBAAgB,SAAS,4BAA4B;;UAG7C;EACR,OAAO;EACP;EACA;IAAe;IAAsB;IAAmB;;EACxD;EACA,UAAU;EACV,mBAAmB;EACnB;IAAU,KAAK;IAAuB;MAAO;;;EAC7C;EACA;EACA;EACA,kBAAkB;IAAY;;IAC5B;IACA;;;UAIM,qBAAqB;EAC7B;IAAU;;;;;;;;;;;iBA4DK,+BAA+B;;WAErC,QAAQ;;WAER;;wBAoFK,gBACd,UAAS,yBACR,gBAAgB,eAAe"}
@@ -0,0 +1,2 @@
1
+ import{GENERATED_DIR as e}from"../../constants/layout.mjs";import{generateExtension as t}from"../generate/index.mjs";import{writeManifest as n,writeMcpManifest as r}from"../syncManifest/index.mjs";import i from"node:fs";import a from"node:path";const o=`apiContracts`;function s(e,t){let n=a.join(e,t);if(!i.existsSync(n))return{};let r={};for(let e of i.readdirSync(n)){if(!e.endsWith(`.ts`)||e.endsWith(`.d.ts`))continue;let n=e.slice(0,-3);r[n]=`${t}/${e}`}return r}function c(e,t,n){let r=a.join(e,t);if(!i.existsSync(r))return{};let o={};for(let e of i.readdirSync(r,{encoding:`utf8`,recursive:!0})){if(!n(e))continue;let r=e.split(a.sep).join(`/`);o[`./${t}/${r}`]=`./${t}/${r}`}return o}function l(e){let t=JSON.parse(i.readFileSync(a.join(e,`package.json`),`utf8`)),n=t.exports!==null&&typeof t.exports==`object`&&!Array.isArray(t.exports)?Object.fromEntries(Object.entries(t.exports).filter(([,e])=>typeof e==`string`)):{};return{...c(e,`skills`,e=>a.basename(e)===`SKILL.md`),...c(e,`themes`,e=>e.endsWith(`.json`)),...n}}function u(){return{name:`doompi-query-assets`,resolveId(e,t){let n=e.indexOf(`?`);if(n<0||t===void 0||!e.startsWith(`.`))return;let r=t.split(`?`)[0];return`${a.resolve(a.dirname(r),e.slice(0,n))}${e.slice(n)}`},load(e){let t=e.indexOf(`?`);if(t<0)return;let n=e.slice(0,t),r=new URLSearchParams(e.slice(t+1));if(!r.has(`url`))return;let o=r.has(`worker`);return`export default import.meta.ROLLUP_FILE_URL_${this.emitFile(o?{type:`chunk`,id:n,name:a.basename(n,a.extname(n))}:{type:`asset`,name:a.basename(n),source:i.readFileSync(n)})};`}}}function d(){return{entry:{"extensions/web":`${e}/web.ts`},clean:!1,dts:!1,exports:!1,format:[`esm`],platform:`browser`,fixedExtension:!0,external:e=>!e.startsWith(`.`)&&!a.isAbsolute(e),plugins:[u()],sourcemap:!0,unbundle:!1,tsconfig:`tsconfig.web.json`}}function f(e){let t=a.join(e,`dist`);if(i.existsSync(t))for(let e of i.readdirSync(t,{recursive:!0,encoding:`utf8`})){let n=e.split(a.sep).join(`/`);if(!/^(?:extensions\/mcp|.+\.mcp)\.(?:mjs|cjs|d\.mts|d\.cts)(?:\.map)?$/u.test(n))continue;let r=a.join(t,e);i.lstatSync(r).isFile()&&i.rmSync(r)}}function p(c={packageDir:process.cwd()}){let u=c.packageDir??process.cwd(),p=t({...c,packageDir:u,check:c.check??!1});for(let e of p.notices)process.stderr.write(`[doompi-build] ${e.path}: ${e.message}\n`);let m=c.target===`mcp`;m&&!c.check&&f(u);let h=m&&!p.targets.includes(`mcp`),g=`\0doompi-empty-mcp`;h&&!c.check&&r(u,!1);let _=m?p.targets.includes(`mcp`)?{"extensions/mcp":`${e}/mcp.ts`}:{"extensions/mcp":g}:{...s(u,c.exportsDir??`src/exports`),...p.targets.includes(`cli`)?{"extensions/pi":`${e}/pi.ts`}:{},...p.targets.includes(`server`)?{"extensions/server":`${e}/server.ts`}:{},...c.entry},v={clean:!1,dts:!h&&{incremental:!0,parallel:!1,eager:!0},...h?{write:!1,plugins:[{name:`doompi-empty-mcp`,resolveId:e=>e===g?g:void 0,load:e=>e===g?`export {};`:void 0}]}:{},exports:!m&&{customExports:e=>({...l(u),...e})},format:{esm:{},cjs:{dts:!1}},platform:`node`,outExtensions:({format:e})=>({js:e===`es`?`.mjs`:`.cjs`,dts:e===`es`?`.d.mts`:`.d.cts`}),sourcemap:!0},y=(e,t)=>({...v,entry:e,unbundle:!1,hooks:{"build:done":()=>{if(m){r(u,p.targets.includes(`mcp`));return}n({packageDir:u,manifest:JSON.parse(i.readFileSync(a.join(u,`package.json`),`utf8`)),graph:p.graph,targets:p.targets,pluginId:p.pluginId})}}}),b=!m&&o in _?[((e,t)=>({...v,entry:e,unbundle:t}))({[o]:_[o]},!1),y(Object.fromEntries(Object.entries(_).filter(([e])=>e!==o)),!0)]:[y(_,!m)];return!m&&p.targets.includes(`web`)?[...b,d()]:b.length===1?b[0]:b}export{p as doompiExtension};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/tsdownPreset/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { GENERATED_DIR } from '../../constants/layout';\nimport { generateExtension } from '../generate';\nimport type { GenerateOptions } from '../generate/type';\nimport { writeManifest, writeMcpManifest } from '../syncManifest';\n\ninterface EmittedFile {\n readonly type: 'asset' | 'chunk';\n readonly id?: string;\n readonly name: string;\n readonly source?: Buffer;\n}\n\ninterface QueryAssetPlugin {\n readonly name: string;\n resolveId(source: string, importer: string | undefined): string | undefined;\n load(this: { emitFile(file: EmittedFile): string }, id: string): string | undefined;\n}\n\n/** The shape a tsdown config needs from this preset, without importing tsdown's types. */\ninterface BrowserPresetConfig {\n entry: Record<string, string>;\n clean: boolean;\n dts: false;\n exports: boolean;\n format: 'esm'[];\n platform: 'browser';\n fixedExtension: boolean;\n external: (id: string) => boolean;\n plugins: QueryAssetPlugin[];\n sourcemap: boolean;\n unbundle: boolean;\n tsconfig: string;\n}\n\ninterface GeneratedExports {\n customExports: (exports: Record<string, unknown>) => Record<string, unknown>;\n}\n\ninterface BasePresetConfig {\n entry: Record<string, string>;\n clean: boolean;\n dts: false | { incremental: boolean; parallel: boolean; eager: boolean };\n write?: false;\n plugins?: QueryAssetPlugin[];\n exports: boolean | GeneratedExports;\n format: { esm: Record<string, never>; cjs: { dts: false } };\n platform: 'node';\n sourcemap: boolean;\n unbundle: boolean;\n outExtensions: ({ format }: { format: string }) => {\n js: string;\n dts: string;\n };\n}\n\ninterface PresetConfig extends BasePresetConfig {\n hooks?: { 'build:done': () => void };\n}\n\nconst DEFAULT_EXPORTS_DIR = 'src/exports';\nconst API_CONTRACTS_ENTRY = 'apiContracts';\n\n/** `src/exports/apiContracts.ts` becomes the `apiContracts` entry; index keeps its name. */\nfunction exportEntries(packageDir: string, exportsDir: string): Record<string, string> {\n const absolute = path.join(packageDir, exportsDir);\n if (!fs.existsSync(absolute)) return {};\n const entries: Record<string, string> = {};\n for (const file of fs.readdirSync(absolute)) {\n if (!file.endsWith('.ts') || file.endsWith('.d.ts')) continue;\n const stem = file.slice(0, -'.ts'.length);\n entries[stem] = `${exportsDir}/${file}`;\n }\n return entries;\n}\n\n/** Static resources receive package export entries without becoming tsdown inputs. */\nfunction resourceEntries(\n packageDir: string,\n directory: string,\n matches: (relative: string) => boolean,\n): Record<string, string> {\n const root = path.join(packageDir, directory);\n if (!fs.existsSync(root)) return {};\n const entries: Record<string, string> = {};\n for (const relative of fs.readdirSync(root, { encoding: 'utf8', recursive: true })) {\n if (!matches(relative)) continue;\n const normalized = relative.split(path.sep).join('/');\n entries[`./${directory}/${normalized}`] = `./${directory}/${normalized}`;\n }\n return entries;\n}\n\n/** Preserves declared resources and discovers default skill and theme resource exports. */\nfunction staticExports(packageDir: string): Record<string, unknown> {\n const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >;\n const declared =\n manifest.exports !== null && typeof manifest.exports === 'object' && !Array.isArray(manifest.exports)\n ? Object.fromEntries(Object.entries(manifest.exports).filter(([, target]) => typeof target === 'string'))\n : {};\n return {\n ...resourceEntries(packageDir, 'skills', (relative) => path.basename(relative) === 'SKILL.md'),\n ...resourceEntries(packageDir, 'themes', (relative) => relative.endsWith('.json')),\n ...declared,\n };\n}\n/**\n * One tsdown config for a folder-routed extension.\n *\n * Entries come from the tree rather than a hand-written list, and the manifest\n * blocks that restate the tree are written after the build rather than kept in\n * step by hand. Browser contributions are emitted through a second bundled\n * config because their routed source is not published.\n */\nexport interface ExtensionPresetOptions extends GenerateOptions {\n /** Extra tsdown entries beyond the derived ones. */\n readonly entry?: Record<string, string>;\n /** Public export modules, package-relative. Defaults to `src/exports`. */\n readonly exportsDir?: string;\n}\n\n/**\n * Implements the Vite query imports extension browser code already uses.\n *\n * `?url` copies an opaque asset and `?worker&url` emits a separately bundled\n * worker chunk. Rolldown then replaces its file reference with an URL relative\n * to the extension bundle, which remains valid when the cockpit composes it.\n */\nfunction queryAssetPlugin(): QueryAssetPlugin {\n return {\n name: 'doompi-query-assets',\n resolveId(source, importer) {\n const queryAt = source.indexOf('?');\n if (queryAt < 0 || importer === undefined || !source.startsWith('.')) return undefined;\n const importerFile = importer.split('?')[0] as string;\n return `${path.resolve(path.dirname(importerFile), source.slice(0, queryAt))}${source.slice(queryAt)}`;\n },\n load(id) {\n const queryAt = id.indexOf('?');\n if (queryAt < 0) return undefined;\n const file = id.slice(0, queryAt);\n const query = new URLSearchParams(id.slice(queryAt + 1));\n if (!query.has('url')) return undefined;\n const worker = query.has('worker');\n const reference = this.emitFile(\n worker\n ? { type: 'chunk', id: file, name: path.basename(file, path.extname(file)) }\n : { type: 'asset', name: path.basename(file), source: fs.readFileSync(file) },\n );\n return `export default import.meta.ROLLUP_FILE_URL_${reference};`;\n },\n };\n}\n\n/** The separately bundled browser half of a routed extension. */\nfunction browserConfig(): BrowserPresetConfig {\n return {\n entry: { 'extensions/web': `${GENERATED_DIR}/web.ts` },\n clean: false,\n // No declarations. Nothing imports this as a typed module: it has no\n // export subpath, and the cockpit consumes it as a bundle. The browser\n // half is still typechecked, by tsconfig.web.json in the typecheck script.\n dts: false,\n exports: false,\n format: ['esm'],\n platform: 'browser',\n // platform browser turns tsdown's fixedExtension off, which would emit\n // .js. The manifest names .mjs like the other two entries, so ask for it.\n fixedExtension: true,\n // Every bare specifier stays external and the cockpit resolves it, which\n // is what keeps one React and one store in the page no matter how many\n // plugins are installed. An allowlist would have to be kept in step with\n // the cockpit's; this cannot drift.\n //\n // A predicate rather than a pattern: rolldown tests `external` against the\n // resolved id, so /^[^.]/ also matches the absolute path of this package's\n // own routed file and would leave a dangling import to unbuilt .tsx.\n external: (id: string) => !id.startsWith('.') && !path.isAbsolute(id),\n plugins: [queryAssetPlugin()],\n sourcemap: true,\n // Bundled, unlike the node half. Unbundling would emit an import to the\n // routed source file, which is not published and is .tsx besides. One\n // module with only bare specifiers external is what the cockpit wants.\n unbundle: false,\n // Browser routes are excluded from the node tsconfig. Every browser-capable\n // extension owns this focused config for its matching typecheck.\n tsconfig: 'tsconfig.web.json',\n };\n}\n\n/** Removes only filenames owned by the isolated MCP build, including obsolete declarations. */\nfunction cleanMcpOutput(packageDir: string): void {\n const dist = path.join(packageDir, 'dist');\n if (!fs.existsSync(dist)) return;\n for (const relative of fs.readdirSync(dist, { recursive: true, encoding: 'utf8' })) {\n const normalized = relative.split(path.sep).join('/');\n if (!/^(?:extensions\\/mcp|.+\\.mcp)\\.(?:mjs|cjs|d\\.mts|d\\.cts)(?:\\.map)?$/u.test(normalized)) continue;\n const absolute = path.join(dist, relative);\n if (fs.lstatSync(absolute).isFile()) fs.rmSync(absolute);\n }\n}\n\nexport function doompiExtension(\n options: ExtensionPresetOptions = { packageDir: process.cwd() },\n): PresetConfig | (PresetConfig | BrowserPresetConfig)[] {\n const packageDir = options.packageDir ?? process.cwd();\n const result = generateExtension({ ...options, packageDir, check: options.check ?? false });\n\n for (const notice of result.notices) process.stderr.write(`[doompi-build] ${notice.path}: ${notice.message}\\n`);\n\n const mcpOnly = options.target === 'mcp';\n if (mcpOnly && !options.check) cleanMcpOutput(packageDir);\n const emptyMcp = mcpOnly && !result.targets.includes('mcp');\n const emptyEntry = '\\0doompi-empty-mcp';\n if (emptyMcp && !options.check) writeMcpManifest(packageDir, false);\n const entry: Record<string, string> = mcpOnly\n ? result.targets.includes('mcp')\n ? { 'extensions/mcp': `${GENERATED_DIR}/mcp.ts` }\n : { 'extensions/mcp': emptyEntry }\n : {\n ...exportEntries(packageDir, options.exportsDir ?? DEFAULT_EXPORTS_DIR),\n ...(result.targets.includes('cli') ? { 'extensions/pi': `${GENERATED_DIR}/pi.ts` } : {}),\n ...(result.targets.includes('server') ? { 'extensions/server': `${GENERATED_DIR}/server.ts` } : {}),\n ...options.entry,\n };\n\n const baseNode: Omit<BasePresetConfig, 'entry' | 'unbundle'> = {\n // MCP shares dist/ with the normal build, so neither build may erase the other.\n clean: false,\n dts: emptyMcp ? false : { incremental: true, parallel: false, eager: true },\n // tsdown rejects both an empty entry and an empty config array. A virtual\n // input completes its normal lifecycle without publishing an empty facet.\n ...(emptyMcp\n ? {\n write: false as const,\n plugins: [\n {\n name: 'doompi-empty-mcp',\n resolveId: (source: string) => (source === emptyEntry ? emptyEntry : undefined),\n load: (id: string) => (id === emptyEntry ? 'export {};' : undefined),\n },\n ],\n }\n : {}),\n exports: mcpOnly\n ? false\n : {\n customExports: (generated) => ({ ...staticExports(packageDir), ...generated }),\n },\n // CommonJS keeps its JavaScript but emits no declarations. Every export map\n // resolves types through .d.mts, and a CJS declaration build would rerun\n // the same whole-program tsgo emit a second time.\n format: { esm: {}, cjs: { dts: false } },\n // No minify. This is a library build, and a mangled stack trace inside a\n // published extension is far more expensive than the bytes it saves.\n // Node output uses fixed extensions, so pi.extensions and doompiServer.dist\n // both land on the .mjs filenames their hosts require.\n platform: 'node',\n outExtensions: ({ format }) => ({\n js: format === 'es' ? '.mjs' : '.cjs',\n dts: format === 'es' ? '.d.mts' : '.d.cts',\n }),\n sourcemap: true,\n };\n\n // Contract bundles are published as a self-contained graph. Normal extension\n // entries remain unbundled for routed runtime resources, while MCP remains\n // isolated from the shared dist directory.\n const node = (entries: Record<string, string>, _unbundle: boolean): PresetConfig => ({\n ...baseNode,\n entry: entries,\n // Public entry facades must remain bundled. Otherwise their relative\n // imports target unbundled implementation chunks, which Vite treats as\n // source files when resolving a workspace package.\n unbundle: false,\n hooks: {\n 'build:done': () => {\n if (mcpOnly) {\n writeMcpManifest(packageDir, result.targets.includes('mcp'));\n return;\n }\n writeManifest({\n packageDir,\n manifest: JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >,\n graph: result.graph,\n targets: result.targets,\n pluginId: result.pluginId,\n });\n },\n },\n });\n const unsyncedNode = (entries: Record<string, string>, unbundle: boolean): PresetConfig => ({\n ...baseNode,\n entry: entries,\n unbundle,\n });\n const configs =\n !mcpOnly && API_CONTRACTS_ENTRY in entry\n ? [\n unsyncedNode({ [API_CONTRACTS_ENTRY]: entry[API_CONTRACTS_ENTRY] }, false),\n node(Object.fromEntries(Object.entries(entry).filter(([name]) => name !== API_CONTRACTS_ENTRY)), true),\n ]\n : [node(entry, !mcpOnly)];\n if (!mcpOnly && result.targets.includes('web')) return [...configs, browserConfig()];\n return configs.length === 1 ? configs[0]! : configs;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);var r=class extends Error{stale;constructor(e){super(`Stale generated files: ${e.join(`, `)}. Run the package's build to regenerate them.`),this.stale=e,this.name=`StaleGeneratedError`}};function i(e,i,a=!1,o=[]){let s=[];for(let[r,o]of i){let i=n.default.join(e,r);(t.default.existsSync(i)?t.default.readFileSync(i,`utf8`):void 0)!==o&&(s.push(r),!a&&(t.default.mkdirSync(n.default.dirname(i),{recursive:!0}),t.default.writeFileSync(i,o)))}for(let r of o){if(i.has(r))continue;let o=n.default.join(e,r);t.default.existsSync(o)&&(s.push(r),a||t.default.rmSync(o))}if(a&&s.length>0)throw new r(s);return{changed:s}}exports.StaleGeneratedError=r,exports.writeGenerated=i;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["path","fs"],"sources":["../../../src/services/writeGenerated/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\n/** What a sync pass did, or would have done. */\nexport interface WriteResult {\n /** Package-relative paths whose contents changed. */\n readonly changed: readonly string[];\n}\n\nexport class StaleGeneratedError extends Error {\n constructor(readonly stale: readonly string[]) {\n super(`Stale generated files: ${stale.join(', ')}. Run the package's build to regenerate them.`);\n this.name = 'StaleGeneratedError';\n }\n}\n\n/**\n * Write-if-changed locally, throw on stale under an explicit `check`.\n *\n * Generated entries are ignored build inputs, so normal and CI builds create\n * them. Explicit check mode remains available to callers that need to compare\n * an existing generated tree without changing it.\n *\n * Writing only on a real change matters for watch mode: an unconditional\n * write would retrigger the watcher that called it.\n */\nexport function writeGenerated(\n packageDir: string,\n files: ReadonlyMap<string, string>,\n check = false,\n managed: readonly string[] = [],\n): WriteResult {\n const changed: string[] = [];\n\n for (const [relative, contents] of files) {\n const absolute = path.join(packageDir, relative);\n const existing = fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf8') : undefined;\n if (existing === contents) continue;\n changed.push(relative);\n if (check) continue;\n fs.mkdirSync(path.dirname(absolute), { recursive: true });\n fs.writeFileSync(absolute, contents);\n }\n\n for (const relative of managed) {\n if (files.has(relative)) continue;\n const absolute = path.join(packageDir, relative);\n if (!fs.existsSync(absolute)) continue;\n changed.push(relative);\n if (!check) fs.rmSync(absolute);\n }\n\n if (check && changed.length > 0) throw new StaleGeneratedError(changed);\n return { changed };\n}\n"],"mappings":"8IASA,IAAa,EAAb,cAAyC,KAAM,CACxB,MAArB,YAAY,EAAmC,CAC7C,MAAM,0BAA0B,EAAM,KAAK,IAAI,EAAE,8CAA8C,EAD5E,KAAA,MAAA,EAEnB,KAAK,KAAO,qBACd,CACF,EAYA,SAAgB,EACd,EACA,EACA,EAAQ,GACR,EAA6B,CAAC,EACjB,CACb,IAAM,EAAoB,CAAC,EAE3B,IAAK,GAAM,CAAC,EAAU,KAAa,EAAO,CACxC,IAAM,EAAWA,EAAAA,QAAK,KAAK,EAAY,CAAQ,GAC9BC,EAAAA,QAAG,WAAW,CAAQ,EAAIA,EAAAA,QAAG,aAAa,EAAU,MAAM,EAAI,IAAA,MAC9D,IACjB,EAAQ,KAAK,CAAQ,EACjB,KACJ,EAAA,QAAG,UAAUD,EAAAA,QAAK,QAAQ,CAAQ,EAAG,CAAE,UAAW,EAAK,CAAC,EACxD,EAAA,QAAG,cAAc,EAAU,CAAQ,GACrC,CAEA,IAAK,IAAM,KAAY,EAAS,CAC9B,GAAI,EAAM,IAAI,CAAQ,EAAG,SACzB,IAAM,EAAWA,EAAAA,QAAK,KAAK,EAAY,CAAQ,EAC1CC,EAAAA,QAAG,WAAW,CAAQ,IAC3B,EAAQ,KAAK,CAAQ,EAChB,GAAO,EAAA,QAAG,OAAO,CAAQ,EAChC,CAEA,GAAI,GAAS,EAAQ,OAAS,EAAG,MAAM,IAAI,EAAoB,CAAO,EACtE,MAAO,CAAE,SAAQ,CACnB"}
@@ -0,0 +1,23 @@
1
+ //#region src/services/writeGenerated/index.d.ts
2
+ /** What a sync pass did, or would have done. */
3
+ export interface WriteResult {
4
+ /** Package-relative paths whose contents changed. */
5
+ readonly changed: readonly string[];
6
+ }
7
+ export declare class StaleGeneratedError extends Error {
8
+ readonly stale: readonly string[];
9
+ constructor(stale: readonly string[]);
10
+ }
11
+ /**
12
+ * Write-if-changed locally, throw on stale under an explicit `check`.
13
+ *
14
+ * Generated entries are ignored build inputs, so normal and CI builds create
15
+ * them. Explicit check mode remains available to callers that need to compare
16
+ * an existing generated tree without changing it.
17
+ *
18
+ * Writing only on a real change matters for watch mode: an unconditional
19
+ * write would retrigger the watcher that called it.
20
+ */
21
+ export declare function writeGenerated(packageDir: string, files: ReadonlyMap<string, string>, check?: boolean, managed?: readonly string[]): WriteResult;
22
+ //#endregion
23
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/writeGenerated/index.ts"],"mappings":";;iBAIiB;;WAEN;;qBAGE,4BAA4B;WAClB;EAArB,YAAqB;;;;;;;;;;;;wBAgBP,eACd,oBACA,OAAO,6BACP,iBACA,8BACC"}
@@ -0,0 +1,2 @@
1
+ import e from"node:fs";import t from"node:path";var n=class extends Error{stale;constructor(e){super(`Stale generated files: ${e.join(`, `)}. Run the package's build to regenerate them.`),this.stale=e,this.name=`StaleGeneratedError`}};function r(r,i,a=!1,o=[]){let s=[];for(let[n,o]of i){let i=t.join(r,n);(e.existsSync(i)?e.readFileSync(i,`utf8`):void 0)!==o&&(s.push(n),!a&&(e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,o)))}for(let n of o){if(i.has(n))continue;let o=t.join(r,n);e.existsSync(o)&&(s.push(n),a||e.rmSync(o))}if(a&&s.length>0)throw new n(s);return{changed:s}}export{n as StaleGeneratedError,r as writeGenerated};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/writeGenerated/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\n/** What a sync pass did, or would have done. */\nexport interface WriteResult {\n /** Package-relative paths whose contents changed. */\n readonly changed: readonly string[];\n}\n\nexport class StaleGeneratedError extends Error {\n constructor(readonly stale: readonly string[]) {\n super(`Stale generated files: ${stale.join(', ')}. Run the package's build to regenerate them.`);\n this.name = 'StaleGeneratedError';\n }\n}\n\n/**\n * Write-if-changed locally, throw on stale under an explicit `check`.\n *\n * Generated entries are ignored build inputs, so normal and CI builds create\n * them. Explicit check mode remains available to callers that need to compare\n * an existing generated tree without changing it.\n *\n * Writing only on a real change matters for watch mode: an unconditional\n * write would retrigger the watcher that called it.\n */\nexport function writeGenerated(\n packageDir: string,\n files: ReadonlyMap<string, string>,\n check = false,\n managed: readonly string[] = [],\n): WriteResult {\n const changed: string[] = [];\n\n for (const [relative, contents] of files) {\n const absolute = path.join(packageDir, relative);\n const existing = fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf8') : undefined;\n if (existing === contents) continue;\n changed.push(relative);\n if (check) continue;\n fs.mkdirSync(path.dirname(absolute), { recursive: true });\n fs.writeFileSync(absolute, contents);\n }\n\n for (const relative of managed) {\n if (files.has(relative)) continue;\n const absolute = path.join(packageDir, relative);\n if (!fs.existsSync(absolute)) continue;\n changed.push(relative);\n if (!check) fs.rmSync(absolute);\n }\n\n if (check && changed.length > 0) throw new StaleGeneratedError(changed);\n return { changed };\n}\n"],"mappings":"gDASA,IAAa,EAAb,cAAyC,KAAM,CACxB,MAArB,YAAY,EAAmC,CAC7C,MAAM,0BAA0B,EAAM,KAAK,IAAI,EAAE,8CAA8C,EAD5E,KAAA,MAAA,EAEnB,KAAK,KAAO,qBACd,CACF,EAYA,SAAgB,EACd,EACA,EACA,EAAQ,GACR,EAA6B,CAAC,EACjB,CACb,IAAM,EAAoB,CAAC,EAE3B,IAAK,GAAM,CAAC,EAAU,KAAa,EAAO,CACxC,IAAM,EAAW,EAAK,KAAK,EAAY,CAAQ,GAC9B,EAAG,WAAW,CAAQ,EAAI,EAAG,aAAa,EAAU,MAAM,EAAI,IAAA,MAC9D,IACjB,EAAQ,KAAK,CAAQ,EACjB,KACJ,EAAG,UAAU,EAAK,QAAQ,CAAQ,EAAG,CAAE,UAAW,EAAK,CAAC,EACxD,EAAG,cAAc,EAAU,CAAQ,GACrC,CAEA,IAAK,IAAM,KAAY,EAAS,CAC9B,GAAI,EAAM,IAAI,CAAQ,EAAG,SACzB,IAAM,EAAW,EAAK,KAAK,EAAY,CAAQ,EAC1C,EAAG,WAAW,CAAQ,IAC3B,EAAQ,KAAK,CAAQ,EAChB,GAAO,EAAG,OAAO,CAAQ,EAChC,CAEA,GAAI,GAAS,EAAQ,OAAS,EAAG,MAAM,IAAI,EAAoB,CAAO,EACtE,MAAO,CAAE,SAAQ,CACnB"}