@frontera-sdk/cli 1.52.39 → 1.52.41

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 (235) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +49 -5
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/draft-changes.ts +1 -1
  33. package/src/commands/agent/index-commands.ts +69 -7
  34. package/src/commands/agent/resolve.ts +2 -0
  35. package/src/commands/app/checks.ts +44 -0
  36. package/src/commands/app/deploy.ts +243 -60
  37. package/src/commands/app/deployments.ts +97 -0
  38. package/src/commands/app/dev.ts +103 -11
  39. package/src/commands/app/init.ts +36 -3
  40. package/src/commands/app/promote.ts +45 -19
  41. package/src/commands/app/pull.ts +3 -1
  42. package/src/commands/app/rollback.ts +46 -0
  43. package/src/commands/app/sdk.ts +54 -8
  44. package/src/commands/app/unpin.ts +27 -0
  45. package/src/commands/app/versions.ts +1 -1
  46. package/src/commands/automation/bindings.ts +4 -2
  47. package/src/commands/automation/build-entry.ts +4 -1
  48. package/src/commands/automation/index-commands.ts +3 -1
  49. package/src/commands/automation/pull.ts +6 -1
  50. package/src/commands/automation/run.ts +34 -9
  51. package/src/commands/blueprint/bind.ts +2 -1
  52. package/src/commands/blueprint/declarative.ts +332 -32
  53. package/src/commands/blueprint/editable.ts +3 -3
  54. package/src/commands/blueprint/get.ts +6 -2
  55. package/src/commands/blueprint/grants.ts +64 -10
  56. package/src/commands/blueprint/metric.ts +108 -0
  57. package/src/commands/blueprint/object-sets.ts +71 -0
  58. package/src/commands/blueprint/query.ts +155 -31
  59. package/src/commands/dataset/index-commands.ts +156 -7
  60. package/src/commands/knowledge/index-commands.ts +122 -20
  61. package/src/commands/local/check.ts +657 -44
  62. package/src/commands/local/data.ts +33 -12
  63. package/src/commands/local/dev.ts +876 -89
  64. package/src/commands/local/docs-mcp.ts +10 -18
  65. package/src/commands/local/index-commands.ts +300 -63
  66. package/src/commands/local/init.ts +18 -5
  67. package/src/commands/local/key.ts +0 -3
  68. package/src/commands/local/providers.ts +0 -7
  69. package/src/commands/local/remove.ts +116 -0
  70. package/src/commands/local/schema.ts +3 -4
  71. package/src/commands/local/sdk-refresh.ts +59 -0
  72. package/src/commands/local/snapshot.ts +100 -0
  73. package/src/commands/local/sync-request.ts +321 -14
  74. package/src/commands/local/sync.ts +103 -6
  75. package/src/commands/mcp/serve.ts +34 -0
  76. package/src/commands/registry.ts +54 -10
  77. package/src/commands/types.ts +29 -0
  78. package/src/commands/workflow/archive.ts +46 -0
  79. package/src/commands/workflow/get.ts +2 -1
  80. package/src/commands/workflow/index-commands.ts +5 -0
  81. package/src/commands/workflow/push.ts +49 -2
  82. package/src/commands/workflow/run.ts +65 -7
  83. package/src/commands/workflow/runs.ts +9 -6
  84. package/src/commands/workflow/webhook-test.ts +166 -0
  85. package/src/config.ts +43 -6
  86. package/src/dev-broker.ts +52 -0
  87. package/src/exit.ts +103 -4
  88. package/src/flag-help.ts +77 -17
  89. package/src/forge/claude-commands.ts +11 -6
  90. package/src/forge/claude-skills.ts +4 -4
  91. package/src/forge/touches.ts +15 -12
  92. package/src/function-outside-imports.ts +89 -0
  93. package/src/harness.ts +42 -24
  94. package/src/heal.ts +113 -2
  95. package/src/kit.ts +1 -1
  96. package/src/local/fix-words.ts +12 -0
  97. package/src/local/function-bundle-recovery.ts +775 -0
  98. package/src/local/local-actions.ts +103 -10
  99. package/src/local/local-agent-action-grants.ts +210 -0
  100. package/src/local/local-agent-checks.ts +232 -0
  101. package/src/local/local-agent-grants.ts +29 -7
  102. package/src/local/local-agent-lean-plugins.ts +68 -0
  103. package/src/local/local-agent-missing-tools.ts +68 -0
  104. package/src/local/local-agent-name.ts +27 -0
  105. package/src/local/local-app-code.ts +191 -0
  106. package/src/local/local-app-dev-files.ts +186 -0
  107. package/src/local/local-app-dev-session.ts +88 -0
  108. package/src/local/local-app-lock-install.ts +114 -0
  109. package/src/local/local-app-pulled.ts +167 -0
  110. package/src/local/local-app-restore.ts +111 -0
  111. package/src/local/local-app-runtime.ts +68 -0
  112. package/src/local/local-app-sdk-paths.ts +149 -0
  113. package/src/local/local-app-sdk.ts +509 -0
  114. package/src/local/local-applied.ts +212 -22
  115. package/src/local/local-apply-history.ts +84 -0
  116. package/src/local/local-apply-summary.ts +86 -0
  117. package/src/local/local-apply-watchdog.ts +95 -0
  118. package/src/local/local-apps.ts +181 -8
  119. package/src/local/local-blueprint-errors.ts +198 -0
  120. package/src/local/local-blueprint-outline.ts +31 -12
  121. package/src/local/local-blueprint-schemas.ts +582 -0
  122. package/src/local/local-build-inputs.ts +48 -0
  123. package/src/local/local-build-summary.ts +495 -28
  124. package/src/local/local-case-evidence.ts +120 -0
  125. package/src/local/local-case-not-run.ts +153 -0
  126. package/src/local/local-chat-apps.ts +93 -16
  127. package/src/local/local-check-inherited.ts +83 -0
  128. package/src/local/local-cities.ts +89 -0
  129. package/src/local/local-component-health.ts +119 -0
  130. package/src/local/local-control.ts +34 -7
  131. package/src/local/local-db-snapshot.ts +348 -0
  132. package/src/local/local-dev-references.ts +347 -36
  133. package/src/local/local-dev-sync.ts +141 -12
  134. package/src/local/local-docker.ts +35 -1
  135. package/src/local/local-docs-search.ts +1 -1
  136. package/src/local/local-docs.ts +281 -2
  137. package/src/local/local-empty-workflows.ts +92 -0
  138. package/src/local/local-env.ts +16 -1
  139. package/src/local/local-file-checks.ts +104 -0
  140. package/src/local/local-function-ids.ts +141 -0
  141. package/src/local/local-function-recovery.ts +141 -0
  142. package/src/local/local-function-state.ts +87 -0
  143. package/src/local/local-keys.ts +7 -5
  144. package/src/local/local-knowledge-documents.ts +1 -1
  145. package/src/local/local-knowledge-ingestion.ts +225 -0
  146. package/src/local/local-knowledge-uploads.ts +217 -0
  147. package/src/local/local-lessons.ts +1 -1
  148. package/src/local/local-library.ts +1 -1
  149. package/src/local/local-media-placeholders.ts +182 -0
  150. package/src/local/local-media-sets.ts +90 -10
  151. package/src/local/local-model-parameters.ts +89 -0
  152. package/src/local/local-object-sets.ts +18 -6
  153. package/src/local/local-operating.ts +35 -1
  154. package/src/local/local-org-settings.ts +206 -8
  155. package/src/local/local-orphans.ts +321 -0
  156. package/src/local/local-personas.ts +296 -0
  157. package/src/local/local-portability.ts +143 -18
  158. package/src/local/local-ports.ts +99 -7
  159. package/src/local/local-project-notices.ts +234 -0
  160. package/src/local/local-project-secrets.ts +133 -0
  161. package/src/local/local-project.ts +12 -0
  162. package/src/local/local-provider-refusal.ts +66 -0
  163. package/src/local/local-request-evidence.ts +112 -0
  164. package/src/local/local-runtime-health.ts +123 -0
  165. package/src/local/local-runtime-history.ts +50 -0
  166. package/src/local/local-runtime-target.ts +42 -0
  167. package/src/local/local-sample-data.ts +141 -0
  168. package/src/local/local-sample-remake.ts +138 -0
  169. package/src/local/local-scaffold.ts +336 -53
  170. package/src/local/local-schema-check.ts +148 -0
  171. package/src/local/local-schemas.ts +742 -25
  172. package/src/local/local-secret-carry.ts +107 -0
  173. package/src/local/local-secrets.ts +41 -11
  174. package/src/local/local-seed.ts +102 -26
  175. package/src/local/local-stand-ins.ts +3 -3
  176. package/src/local/local-state.ts +43 -2
  177. package/src/local/local-studio-server.ts +170 -7
  178. package/src/local/local-supervisor.ts +44 -6
  179. package/src/local/local-sync-merge.ts +1 -1
  180. package/src/local/local-sync-names.ts +2 -0
  181. package/src/local/local-sync-plan.ts +19 -8
  182. package/src/local/local-sync-push.ts +15 -12
  183. package/src/local/local-sync-refs.ts +49 -3
  184. package/src/local/local-sync-remote.ts +349 -38
  185. package/src/local/local-sync-snapshot.ts +18 -13
  186. package/src/local/local-synthetic-data.ts +2116 -99
  187. package/src/local/local-test-baseline.ts +113 -0
  188. package/src/local/local-test-cases.ts +76 -1
  189. package/src/local/local-test-runner.ts +582 -91
  190. package/src/local/local-test-runs.ts +13 -5
  191. package/src/local/local-vendored-sdk.ts +183 -0
  192. package/src/local/local-workflow-state.ts +16 -3
  193. package/src/local/local-workflow-tests.ts +482 -0
  194. package/src/local/local-workspace-grants.ts +81 -0
  195. package/src/main.ts +50 -9
  196. package/src/mcp/a11y-scan.ts +55 -0
  197. package/src/mcp/browser/ax-snapshot.ts +181 -0
  198. package/src/mcp/browser/credentials.ts +78 -0
  199. package/src/mcp/browser/frames.ts +336 -0
  200. package/src/mcp/browser/headless-session.ts +413 -0
  201. package/src/mcp/browser/headless-tab.ts +575 -0
  202. package/src/mcp/browser/keys.ts +70 -0
  203. package/src/mcp/browser/page-scripts.ts +129 -0
  204. package/src/mcp/browser/wait.ts +73 -0
  205. package/src/mcp/forge-link.ts +126 -0
  206. package/src/mcp/headless.ts +199 -0
  207. package/src/mcp/mcp-apps.ts +435 -0
  208. package/src/mcp/mcp-chat.ts +226 -0
  209. package/src/mcp/mcp-cli.ts +53 -0
  210. package/src/mcp/mcp-protocol.ts +122 -0
  211. package/src/mcp/mcp-runtime.ts +113 -0
  212. package/src/mcp/mcp-server.ts +108 -0
  213. package/src/mcp/mcp-traces.ts +409 -0
  214. package/src/mcp/page-observation.ts +193 -0
  215. package/src/mcp/tools/agent-tools.ts +700 -0
  216. package/src/mcp/tools/app-tools.ts +241 -0
  217. package/src/mcp/tools/browser-tools.ts +310 -0
  218. package/src/mcp/tools/docs-tools.ts +32 -0
  219. package/src/mcp/tools/project-tools.ts +205 -0
  220. package/src/mcp/tools/test-tools.ts +339 -0
  221. package/src/mcp/tools/workflow-tools.ts +611 -0
  222. package/src/output.ts +1 -1
  223. package/src/pack.ts +8 -3
  224. package/src/packaging.ts +1 -1
  225. package/src/project-bootstrap.ts +107 -4
  226. package/src/project-context.ts +18 -1
  227. package/src/project.ts +27 -2
  228. package/src/provenance.ts +25 -0
  229. package/src/render-evidence.ts +65 -7
  230. package/src/scopes.ts +1 -1
  231. package/src/template.ts +14 -8
  232. package/src/templates/next-app-files.ts +35 -6
  233. package/src/templates/next-skills.ts +61 -27
  234. package/src/vendor/kit-assets.json +6 -6
  235. package/src/vendor/sdk-sources.json +17 -14
package/src/harness.ts CHANGED
@@ -25,6 +25,16 @@ description: Use when reading or changing anything on the Frontera platform from
25
25
  never out of date — read it instead of guessing, and instead of trusting this
26
26
  file for syntax.
27
27
 
28
+ ## In a project with the Frontera MCP server
29
+
30
+ A project made by \`frontera local\` registers the \`frontera\` MCP server
31
+ (\`.mcp.json\`). When its tools are offered (\`mcp__frontera__*\` in Claude
32
+ Code), use them for the local loop instead of the shell: \`status\`,
33
+ \`apply\`, \`test_run\`, \`logs\`, \`read_file_schema\` and the documentation
34
+ tools. The person approves the server once, where every shell command can
35
+ ask again. Run \`frontera local …\` only when the tools aren't offered, or
36
+ for a step that has no tool (\`frontera local start\`, \`frontera local check\`).
37
+
28
38
  ## Preflight: know which customer you are about to change
29
39
 
30
40
  \`\`\`bash
@@ -63,7 +73,10 @@ Two fields decide whether a call can work at all:
63
73
  key: set \`FRONTERA_TOKEN\` to the session token and \`FRONTERA_API_URL\` (or
64
74
  pass \`--api-url\`). The CLI refuses an \`sk-ws-\`/\`sk-org-\` key on those
65
75
  before sending anything; \`frontera login\` stores keys only, so it cannot
66
- supply the session.
76
+ supply the session. On a project's local runtime, never fetch a person's
77
+ session: \`--as <persona>\` runs \`action available\`, \`submit\`,
78
+ \`approve\` and \`reject\` as a local persona (\`--as submitter\`,
79
+ \`--as <approver>\`; \`frontera local status\` lists them).
67
80
  - \`available\` — \`false\` means the verb is named but unbuilt, and
68
81
  \`unavailableReason\` says why. Pick another route rather than retrying.
69
82
 
@@ -126,7 +139,7 @@ Pass \`--json\` for machine-readable output. The contract:
126
139
  | 0 | success | continue |
127
140
  | 1 | transient or remote failure | retry, then report |
128
141
  | 2 | usage, input, or profile selection | fix the command |
129
- | 3 | conflict — someone changed it first | re-fetch, reapply, retry |
142
+ | 3 | conflict or refusal — someone changed it first, or the record's state refuses it | re-read, change the input or the record, then send it again; never retry it unchanged |
130
143
  | 4 | auth or permission | different credential, or ask an admin |
131
144
 
132
145
  Errors carry a \`hint\` naming the next command. Read it.
@@ -192,8 +205,9 @@ frontera blueprint instance <apiName> <pk>
192
205
  app or agent sees at runtime. Anything absent here is absent for them too.
193
206
 
194
207
  \`--where\` takes \`<property>:<value>\` or \`<property>:<op>:<value>\` for one
195
- condition; pass a JSON where-node for anything with and/or in it, or
196
- \`--file\` a whole query document for unions and link traversals.
208
+ condition, and \`<property>:in:<a>,<b>\` (also \`notIn\`, \`between\`) for a list;
209
+ repeat it for conditions that must all hold. Pass a JSON where-node for an
210
+ or/not, or \`--file\` a whole query document for unions and link traversals.
197
211
 
198
212
  ## Configuring an agent
199
213
 
@@ -239,14 +253,13 @@ means someone else changed it: re-read and retry, never force.
239
253
  ## Building an app
240
254
 
241
255
  \`\`\`bash
242
- frontera app init <name> # scaffold — installs with public npm only
243
- bun install
256
+ frontera app init <name> # Next.js static export — installs with public npm only
257
+ cd <name>
244
258
  frontera blueprint list # what data this workspace actually has
245
- frontera app add <component> # registry source into the project
246
- bun run typecheck # vite build does NOT type-check
247
- bun run build # the deploy gate — must succeed
248
- frontera app deploy --no-promote
249
- frontera app promote <version>
259
+ frontera app dev # the App against real data, with a short-lived token
260
+ bun run check # typecheck, tests and build: the gate
261
+ frontera app deploy --build # build, then a preview deployment with its link
262
+ frontera app promote <deployment>
250
263
  \`\`\`
251
264
 
252
265
  Read \`frontera blueprint list\` and \`frontera blueprint get <apiName>\` before
@@ -254,19 +267,24 @@ designing against data. What they return is the slice this workspace is granted,
254
267
  which is exactly what the app will be able to query at runtime — so anything
255
268
  absent there will be absent for the app too.
256
269
 
257
- \`src/frontera/\` in a scaffolded project is the SDK, copied in as source so the
258
- project installs anywhere. Read it; never edit it, never rewrite an
259
- \`@frontera-sdk/…\` import to a relative path, never add \`@frontera-sdk/*\` to
260
- \`package.json\`. The project's own \`using-frontera-sdk\` skill has the detail.
261
-
262
- \`bun run dev\` serves the app, but opening \`/\` directly renders "This app runs
263
- inside Frontera" instead of mounting: the handshake refuses an unknown parent,
264
- which is correct, not a broken build.
265
-
266
- Open **\`/dev-host.html\`** instead. It frames the app and plays the host side of
267
- the bridge, so the app mounts and reads real data. That is the URL to screenshot
268
- or click through when verifying your work — a screenshot of \`/\` is a screenshot
269
- of the refusal panel.
270
+ The SDK depends on how the App was made:
271
+
272
+ - **Next.js (the default).** \`@frontera-sdk/core\` and \`@frontera-sdk/blueprint\`
273
+ (and \`@frontera-sdk/agents\` when used) are public npm packages in
274
+ \`package.json\`, kept at the release \`frontera app init\` wrote and updated
275
+ with the package manager. Never copy SDK source into \`src/\`. Open the App
276
+ through \`frontera app dev\`: it serves on \`http://127.0.0.1:<port>\` and
277
+ hands the page its session; never write that session into \`.env\`.
278
+ - **Vite (legacy, \`--framework react\`).** \`src/frontera/\` is the SDK, copied
279
+ in as source. Read it; never edit it, never rewrite an \`@frontera-sdk/…\`
280
+ import to a relative path, and never add \`@frontera-sdk/*\` to
281
+ \`package.json\` there. \`frontera app sdk sync\` refreshes the copy. Opening
282
+ \`/\` renders "This app runs inside Frontera" (the handshake refuses an
283
+ unknown parent); open **\`/dev-host.html\`** instead, which frames the app and
284
+ plays the host side of the bridge.
285
+
286
+ The project's own skills (\`.agents/skills/\`) have the detail for its
287
+ framework.
270
288
  `
271
289
 
272
290
  export const AGENTS_MD = `# Frontera
package/src/heal.ts CHANGED
@@ -45,9 +45,10 @@
45
45
  * CLI (`src/vendor/sdk-sources.json`).
46
46
  */
47
47
 
48
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
48
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
49
49
  import { dirname, join } from 'node:path'
50
50
 
51
+ import { lockedSdkVersions, lockedSpec, recordLockedRanges } from './app-lock'
51
52
  import {
52
53
  APP_SDK_PACKAGES,
53
54
  LEGACY_SDK_ALIASES,
@@ -102,6 +103,10 @@ export function healProject(dir: string): HealResult {
102
103
 
103
104
  const name = typeof manifest.name === 'string' && manifest.name ? manifest.name : 'app'
104
105
  let manifestChanged = false
106
+ // What the App's own lockfile installed from the registry: the SDK its code was written against.
107
+ const locked = lockedSdkVersions(dir)
108
+ // The specs restored to a locked release, recorded in the lockfile too so the two agree.
109
+ const restored: Record<string, string> = {}
105
110
 
106
111
  // Packages a legacy tree needs declared under their CURRENT name as well, so
107
112
  // the vendored copy resolves the siblings it imports internally.
@@ -131,6 +136,18 @@ export function healProject(dir: string): HealResult {
131
136
  if (typeof spec !== 'string' || !UNRESOLVABLE.test(spec)) continue
132
137
 
133
138
  const pkg = currentSdkPackage(dep)
139
+ // The lockfile installed a published version: that is the SDK the code was checked against, so
140
+ // the App goes back to it rather than to a copy of another SDK (a pulled App's typecheck depends on it).
141
+ const lockedAt = pkg && dep === pkg ? locked.get(pkg) : undefined
142
+ if (lockedAt) {
143
+ deps[dep] = lockedSpec(lockedAt)
144
+ restored[dep] = deps[dep]
145
+ repairs.push(`restored ${dep} to ${deps[dep]}, the published release its bun.lock installed (was ${truncate(spec)})`)
146
+ manifestChanged = true
147
+ // A copy left under src/frontera/ would still win through tsconfig paths: it goes.
148
+ if (unvendorSdkPackage(dir, pkg!)) repairs.push(`removed the vendored copy in ${APP_SDK_PACKAGES[pkg!].dir} and its tsconfig path`)
149
+ continue
150
+ }
134
151
  if (pkg) {
135
152
  // Already pointing at the copy in this tree — that is the shape this
136
153
  // function produces, so re-pulling must not rewrite it back over
@@ -188,6 +205,10 @@ export function healProject(dir: string): HealResult {
188
205
  // still carry a stale copy, which is the ordinary state of every app
189
206
  // scaffolded before the SDK grew a module.
190
207
  if (vendorCurrent(dir, pkg) && (declares(manifest, specifier) || specifier === pkg)) continue
208
+ // Declared with a published range (`^1.43.10`): the package manager installs it, and the code was written against
209
+ // that release. Vendoring this CLI's copy over it swaps the SDK under the code (a pulled Next.js App's case).
210
+ const spec = manifest.dependencies?.[specifier] ?? manifest.devDependencies?.[specifier]
211
+ if (typeof spec === 'string' && !UNRESOLVABLE.test(spec)) continue
191
212
  vendorSdkPackage(dir, pkg)
192
213
  manifest.dependencies ??= {}
193
214
  manifest.dependencies[specifier] = `file:./${APP_SDK_PACKAGES[pkg].dir}`
@@ -225,6 +246,8 @@ export function healProject(dir: string): HealResult {
225
246
  }
226
247
 
227
248
  if (manifestChanged) writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
249
+ // The lockfile's record of what package.json asks for follows, so a frozen install takes the restored package.json.
250
+ if (recordLockedRanges(dir, restored)) repairs.push(`recorded ${Object.entries(restored).map(([d, s]) => `${d} ${s}`).join(' and ')} in the lockfile, which already installed that release`)
228
251
 
229
252
  // Point the editor at the vendored copy directly. Without this TypeScript
230
253
  // reaches it through the `file:` link instead and reports every diagnostic
@@ -246,6 +269,94 @@ export function healProject(dir: string): HealResult {
246
269
  return { repairs }
247
270
  }
248
271
 
272
+ /**
273
+ * Take a vendored SDK package out of an App that installs the published one:
274
+ * its directory under src/frontera/ and the tsconfig `paths` entries that
275
+ * point at it (which would otherwise resolve imports to the copy instead of
276
+ * node_modules). True when anything was removed. A Vite App whose config
277
+ * aliases the copy keeps it: the alias is how it resolves the SDK.
278
+ */
279
+ /** Does the App's Vite config alias the vendored copy (a scaffolded Vite App resolves the SDK that way)? */
280
+ export function viteAliasesSdk(dir: string, pkg: AppSdkPackage): boolean {
281
+ const rel = APP_SDK_PACKAGES[pkg].dir
282
+ return ['vite.config.ts', 'vite.config.js', 'vite.config.mts'].some((f) => {
283
+ try {
284
+ return readFileSync(join(dir, f), 'utf8').includes(rel)
285
+ } catch {
286
+ return false
287
+ }
288
+ })
289
+ }
290
+
291
+ export function unvendorSdkPackage(dir: string, pkg: AppSdkPackage): boolean {
292
+ const removed = unvendorSdkPackageDetail(dir, pkg)
293
+ return !!removed && (removed.files.length > 0 || removed.tsconfigPaths.length > 0)
294
+ }
295
+
296
+ /** What taking a vendored SDK package out of an App removed: the copy's files, and the tsconfig paths to it. */
297
+ export interface Unvendored {
298
+ /** The copy's directory, relative to the App (`src/frontera/blueprint`). */
299
+ dir: string
300
+ /** Each file deleted with it, relative to that directory, sorted. */
301
+ files: string[]
302
+ /** The directory above it, when it was left empty and deleted too (`src/frontera`). */
303
+ parentRemoved?: string
304
+ /** Each `compilerOptions.paths` key removed from tsconfig.json because it pointed at the copy. */
305
+ tsconfigPaths: string[]
306
+ }
307
+
308
+ /** `unvendorSdkPackage`, saying what it removed; null when the App's Vite config aliases the copy, so it is kept. */
309
+ export function unvendorSdkPackageDetail(dir: string, pkg: AppSdkPackage): Unvendored | null {
310
+ const rel = APP_SDK_PACKAGES[pkg].dir
311
+ if (viteAliasesSdk(dir, pkg)) return null
312
+ const out: Unvendored = { dir: rel, files: [], tsconfigPaths: [] }
313
+ if (existsSync(join(dir, rel))) {
314
+ out.files = filesUnder(join(dir, rel))
315
+ rmSync(join(dir, rel), { recursive: true, force: true })
316
+ const parent = dirname(join(dir, rel))
317
+ try {
318
+ if (readdirSync(parent).length === 0) {
319
+ rmSync(parent, { recursive: true })
320
+ out.parentRemoved = dirname(rel)
321
+ }
322
+ } catch {
323
+ // Gone already.
324
+ }
325
+ }
326
+ const tsconfig = join(dir, 'tsconfig.json')
327
+ try {
328
+ const config = JSON.parse(readFileSync(tsconfig, 'utf8')) as { compilerOptions?: { paths?: Record<string, string[]> } }
329
+ const paths = config.compilerOptions?.paths
330
+ if (paths) {
331
+ for (const [key, targets] of Object.entries(paths)) {
332
+ if (Array.isArray(targets) && targets.some((t) => t.replace(/^\.\//, '').startsWith(`${rel}/`) || t.replace(/^\.\//, '') === rel)) {
333
+ delete paths[key]
334
+ out.tsconfigPaths.push(key)
335
+ }
336
+ }
337
+ if (out.tsconfigPaths.length) writeFileSync(tsconfig, `${JSON.stringify(config, null, 2)}\n`)
338
+ }
339
+ } catch {
340
+ // No tsconfig, or JSON with comments: left as it is.
341
+ }
342
+ return out
343
+ }
344
+
345
+ /** Every file under a directory, by its path relative to it, sorted. */
346
+ function filesUnder(root: string): string[] {
347
+ const out: string[] = []
348
+ const walk = (at: string, rel: string) => {
349
+ for (const name of readdirSync(at).sort()) {
350
+ const path = join(at, name)
351
+ const r = rel ? `${rel}/${name}` : name
352
+ if (statSync(path).isDirectory()) walk(path, r)
353
+ else out.push(r)
354
+ }
355
+ }
356
+ walk(root, '')
357
+ return out
358
+ }
359
+
249
360
  function isSdkPackage(dep: string): dep is AppSdkPackage {
250
361
  return (SDK_PACKAGE_NAMES as string[]).includes(dep)
251
362
  }
@@ -380,7 +491,7 @@ function vendorSdkPackage(dir: string, pkg: AppSdkPackage): void {
380
491
  */
381
492
  export function vendoredManifest(pkg: AppSdkPackage, files: string[]): Record<string, unknown> {
382
493
  const exports: Record<string, unknown> = { './package.json': './package.json' }
383
- for (const rel of files.sort()) {
494
+ for (const rel of files.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
384
495
  if (!/\.tsx?$/.test(rel)) continue
385
496
  exports[`./${rel.replace(/\.tsx?$/, '')}`] = { types: `./${rel}`, import: `./${rel}` }
386
497
  }
package/src/kit.ts CHANGED
@@ -135,7 +135,7 @@ export interface RenderedProject {
135
135
  /** One source, two native projections — Codex reads neither host's other tree. */
136
136
  export function renderKit(kit: KitAssets = KIT): RenderedProject {
137
137
  const generated: Record<string, string> = {}
138
- for (const rel of Object.keys(kit.assets).sort()) {
138
+ for (const rel of Object.keys(kit.assets).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
139
139
  const content = kit.assets[rel]!
140
140
  if (kit.hosts.codex) generated[join('.agents', 'skills', rel)] = content
141
141
  if (kit.hosts.claudeCode) generated[join('.claude', 'skills', rel)] = content
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A message and what fixes it, said as one sentence pair: "… the type
3
+ * allows. Fix: change …". The message keeps its own end mark when it has
4
+ * one (a runtime's error often ends with a period), so the join never says
5
+ * "allows.. Fix:"; a trailing comma, colon or semicolon gives way to the
6
+ * period. Without a fix, the message as it is.
7
+ */
8
+ export function withFix(message: string, fix: string | null | undefined): string {
9
+ const said = message.trim().replace(/[\s,;:]+$/, '')
10
+ if (!fix?.trim()) return said
11
+ return `${said}${/[.!?…]$/.test(said) ? '' : '.'} Fix: ${fix.trim()}`
12
+ }