@frontera-sdk/cli 1.52.38 → 1.52.40

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 (225) 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 +48 -4
  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/index-commands.ts +69 -7
  33. package/src/commands/agent/resolve.ts +2 -0
  34. package/src/commands/app/checks.ts +44 -0
  35. package/src/commands/app/deploy.ts +243 -60
  36. package/src/commands/app/deployments.ts +97 -0
  37. package/src/commands/app/dev.ts +103 -11
  38. package/src/commands/app/init.ts +36 -3
  39. package/src/commands/app/promote.ts +45 -19
  40. package/src/commands/app/pull.ts +3 -1
  41. package/src/commands/app/rollback.ts +46 -0
  42. package/src/commands/app/sdk.ts +54 -8
  43. package/src/commands/app/unpin.ts +27 -0
  44. package/src/commands/app/versions.ts +1 -1
  45. package/src/commands/automation/bindings.ts +4 -2
  46. package/src/commands/automation/build-entry.ts +4 -1
  47. package/src/commands/automation/index-commands.ts +3 -1
  48. package/src/commands/automation/pull.ts +6 -1
  49. package/src/commands/automation/run.ts +34 -9
  50. package/src/commands/blueprint/bind.ts +2 -1
  51. package/src/commands/blueprint/declarative.ts +331 -31
  52. package/src/commands/blueprint/get.ts +6 -2
  53. package/src/commands/blueprint/grants.ts +64 -10
  54. package/src/commands/blueprint/metric.ts +108 -0
  55. package/src/commands/blueprint/object-sets.ts +71 -0
  56. package/src/commands/blueprint/query.ts +155 -31
  57. package/src/commands/dataset/index-commands.ts +156 -7
  58. package/src/commands/knowledge/index-commands.ts +122 -20
  59. package/src/commands/local/check.ts +657 -44
  60. package/src/commands/local/data.ts +33 -12
  61. package/src/commands/local/dev.ts +875 -88
  62. package/src/commands/local/docs-mcp.ts +10 -18
  63. package/src/commands/local/index-commands.ts +300 -63
  64. package/src/commands/local/init.ts +18 -5
  65. package/src/commands/local/key.ts +0 -3
  66. package/src/commands/local/providers.ts +0 -7
  67. package/src/commands/local/remove.ts +116 -0
  68. package/src/commands/local/schema.ts +3 -4
  69. package/src/commands/local/sdk-refresh.ts +59 -0
  70. package/src/commands/local/snapshot.ts +100 -0
  71. package/src/commands/local/sync-request.ts +320 -13
  72. package/src/commands/local/sync.ts +102 -5
  73. package/src/commands/mcp/serve.ts +34 -0
  74. package/src/commands/registry.ts +52 -8
  75. package/src/commands/types.ts +29 -0
  76. package/src/commands/workflow/archive.ts +46 -0
  77. package/src/commands/workflow/get.ts +2 -1
  78. package/src/commands/workflow/index-commands.ts +5 -0
  79. package/src/commands/workflow/push.ts +49 -2
  80. package/src/commands/workflow/run.ts +65 -7
  81. package/src/commands/workflow/runs.ts +9 -6
  82. package/src/commands/workflow/webhook-test.ts +166 -0
  83. package/src/config.ts +43 -6
  84. package/src/dev-broker.ts +52 -0
  85. package/src/exit.ts +103 -4
  86. package/src/flag-help.ts +77 -17
  87. package/src/forge/touches.ts +15 -12
  88. package/src/function-outside-imports.ts +89 -0
  89. package/src/harness.ts +42 -24
  90. package/src/heal.ts +112 -1
  91. package/src/local/fix-words.ts +12 -0
  92. package/src/local/function-bundle-recovery.ts +775 -0
  93. package/src/local/local-actions.ts +103 -10
  94. package/src/local/local-agent-action-grants.ts +210 -0
  95. package/src/local/local-agent-checks.ts +232 -0
  96. package/src/local/local-agent-grants.ts +29 -7
  97. package/src/local/local-agent-lean-plugins.ts +68 -0
  98. package/src/local/local-agent-missing-tools.ts +68 -0
  99. package/src/local/local-agent-name.ts +27 -0
  100. package/src/local/local-app-code.ts +191 -0
  101. package/src/local/local-app-dev-files.ts +186 -0
  102. package/src/local/local-app-dev-session.ts +88 -0
  103. package/src/local/local-app-lock-install.ts +114 -0
  104. package/src/local/local-app-pulled.ts +167 -0
  105. package/src/local/local-app-restore.ts +111 -0
  106. package/src/local/local-app-runtime.ts +68 -0
  107. package/src/local/local-app-sdk-paths.ts +149 -0
  108. package/src/local/local-app-sdk.ts +509 -0
  109. package/src/local/local-applied.ts +211 -21
  110. package/src/local/local-apply-history.ts +84 -0
  111. package/src/local/local-apply-summary.ts +86 -0
  112. package/src/local/local-apply-watchdog.ts +95 -0
  113. package/src/local/local-apps.ts +180 -7
  114. package/src/local/local-blueprint-errors.ts +198 -0
  115. package/src/local/local-blueprint-outline.ts +31 -12
  116. package/src/local/local-blueprint-schemas.ts +582 -0
  117. package/src/local/local-build-inputs.ts +48 -0
  118. package/src/local/local-build-summary.ts +495 -28
  119. package/src/local/local-case-evidence.ts +120 -0
  120. package/src/local/local-case-not-run.ts +153 -0
  121. package/src/local/local-chat-apps.ts +93 -16
  122. package/src/local/local-check-inherited.ts +83 -0
  123. package/src/local/local-cities.ts +89 -0
  124. package/src/local/local-component-health.ts +119 -0
  125. package/src/local/local-control.ts +34 -7
  126. package/src/local/local-db-snapshot.ts +348 -0
  127. package/src/local/local-dev-references.ts +346 -35
  128. package/src/local/local-dev-sync.ts +136 -7
  129. package/src/local/local-docker.ts +35 -1
  130. package/src/local/local-docs-search.ts +1 -1
  131. package/src/local/local-docs.ts +281 -2
  132. package/src/local/local-empty-workflows.ts +92 -0
  133. package/src/local/local-env.ts +16 -1
  134. package/src/local/local-file-checks.ts +104 -0
  135. package/src/local/local-function-ids.ts +141 -0
  136. package/src/local/local-function-recovery.ts +141 -0
  137. package/src/local/local-function-state.ts +87 -0
  138. package/src/local/local-keys.ts +7 -5
  139. package/src/local/local-knowledge-ingestion.ts +225 -0
  140. package/src/local/local-knowledge-uploads.ts +217 -0
  141. package/src/local/local-media-placeholders.ts +182 -0
  142. package/src/local/local-media-sets.ts +90 -10
  143. package/src/local/local-model-parameters.ts +89 -0
  144. package/src/local/local-object-sets.ts +18 -6
  145. package/src/local/local-operating.ts +35 -1
  146. package/src/local/local-org-settings.ts +205 -7
  147. package/src/local/local-orphans.ts +321 -0
  148. package/src/local/local-personas.ts +296 -0
  149. package/src/local/local-portability.ts +141 -16
  150. package/src/local/local-ports.ts +99 -7
  151. package/src/local/local-project-notices.ts +234 -0
  152. package/src/local/local-project-secrets.ts +133 -0
  153. package/src/local/local-project.ts +12 -0
  154. package/src/local/local-provider-refusal.ts +66 -0
  155. package/src/local/local-request-evidence.ts +112 -0
  156. package/src/local/local-runtime-health.ts +123 -0
  157. package/src/local/local-runtime-history.ts +50 -0
  158. package/src/local/local-runtime-target.ts +42 -0
  159. package/src/local/local-sample-data.ts +141 -0
  160. package/src/local/local-sample-remake.ts +138 -0
  161. package/src/local/local-scaffold.ts +336 -53
  162. package/src/local/local-schema-check.ts +148 -0
  163. package/src/local/local-schemas.ts +742 -25
  164. package/src/local/local-secret-carry.ts +107 -0
  165. package/src/local/local-secrets.ts +38 -8
  166. package/src/local/local-seed.ts +102 -26
  167. package/src/local/local-stand-ins.ts +1 -1
  168. package/src/local/local-state.ts +43 -2
  169. package/src/local/local-studio-server.ts +170 -7
  170. package/src/local/local-supervisor.ts +44 -6
  171. package/src/local/local-sync-names.ts +2 -0
  172. package/src/local/local-sync-plan.ts +11 -0
  173. package/src/local/local-sync-push.ts +10 -7
  174. package/src/local/local-sync-refs.ts +49 -3
  175. package/src/local/local-sync-remote.ts +343 -32
  176. package/src/local/local-sync-snapshot.ts +15 -10
  177. package/src/local/local-synthetic-data.ts +2116 -99
  178. package/src/local/local-test-baseline.ts +113 -0
  179. package/src/local/local-test-cases.ts +76 -1
  180. package/src/local/local-test-runner.ts +582 -91
  181. package/src/local/local-test-runs.ts +13 -5
  182. package/src/local/local-vendored-sdk.ts +183 -0
  183. package/src/local/local-workflow-state.ts +16 -3
  184. package/src/local/local-workflow-tests.ts +482 -0
  185. package/src/local/local-workspace-grants.ts +81 -0
  186. package/src/main.ts +50 -9
  187. package/src/mcp/a11y-scan.ts +55 -0
  188. package/src/mcp/browser/ax-snapshot.ts +181 -0
  189. package/src/mcp/browser/credentials.ts +78 -0
  190. package/src/mcp/browser/frames.ts +336 -0
  191. package/src/mcp/browser/headless-session.ts +413 -0
  192. package/src/mcp/browser/headless-tab.ts +575 -0
  193. package/src/mcp/browser/keys.ts +70 -0
  194. package/src/mcp/browser/page-scripts.ts +129 -0
  195. package/src/mcp/browser/wait.ts +73 -0
  196. package/src/mcp/forge-link.ts +126 -0
  197. package/src/mcp/headless.ts +199 -0
  198. package/src/mcp/mcp-apps.ts +435 -0
  199. package/src/mcp/mcp-chat.ts +226 -0
  200. package/src/mcp/mcp-cli.ts +53 -0
  201. package/src/mcp/mcp-protocol.ts +122 -0
  202. package/src/mcp/mcp-runtime.ts +113 -0
  203. package/src/mcp/mcp-server.ts +108 -0
  204. package/src/mcp/mcp-traces.ts +409 -0
  205. package/src/mcp/page-observation.ts +193 -0
  206. package/src/mcp/tools/agent-tools.ts +700 -0
  207. package/src/mcp/tools/app-tools.ts +241 -0
  208. package/src/mcp/tools/browser-tools.ts +310 -0
  209. package/src/mcp/tools/docs-tools.ts +32 -0
  210. package/src/mcp/tools/project-tools.ts +205 -0
  211. package/src/mcp/tools/test-tools.ts +339 -0
  212. package/src/mcp/tools/workflow-tools.ts +611 -0
  213. package/src/output.ts +1 -1
  214. package/src/pack.ts +8 -3
  215. package/src/packaging.ts +1 -1
  216. package/src/project-bootstrap.ts +107 -4
  217. package/src/project-context.ts +18 -1
  218. package/src/project.ts +27 -2
  219. package/src/provenance.ts +25 -0
  220. package/src/render-evidence.ts +65 -7
  221. package/src/template.ts +14 -8
  222. package/src/templates/next-app-files.ts +35 -6
  223. package/src/templates/next-skills.ts +61 -27
  224. package/src/vendor/kit-assets.json +6 -6
  225. package/src/vendor/sdk-sources.json +24 -21
package/src/flag-help.ts CHANGED
@@ -40,7 +40,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
40
40
  remove: 'comma-separated property API names to drop from the current set',
41
41
  // Blueprint authoring (organization API key). `notes` is described further down
42
42
  // — this map is keyed by flag name across every command, so one entry serves both.
43
- label: 'release label recorded on the published or rolled-back release',
43
+ label: 'release label recorded on the published or rolled-back release; for an App deployment, unique per App',
44
44
  report: 'validation report id to act on — the release publishes against it, the review is pinned to it',
45
45
  instruction: 'migration instruction applied to every change a rollback discards',
46
46
  // Global
@@ -71,7 +71,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
71
71
 
72
72
  // Writes
73
73
  file: 'read the document from this path, or `-` for stdin',
74
- force: 'proceed past a refusal — overwrite existing files, or a dirty tree',
74
+ force: 'proceed past a refusal — overwrite existing files, a dirty tree, an App\'s or a change request\'s required checks (with --reason), a running local runtime',
75
75
  'expect-revision':
76
76
  'refuse the write unless the resource is still at this revision; exit 3 if it moved',
77
77
  notes: 'note recorded against the published version',
@@ -91,6 +91,19 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
91
91
  registry: 'the registry host and namespace image mode pulls the platform images from, such as registry.example.com/frontera',
92
92
  reapply: 'apply every file again, including those unchanged since they were last applied',
93
93
  stop: 'stop the local runtime when the check ends',
94
+ 'keep-state': 'keep what the Agent and workflow tests wrote, instead of restoring the database as the apply left it',
95
+ exact: 'restore: put the baseline’s workspace secrets back too, instead of keeping the ones set now',
96
+ workflow: 'run only this workflow’s tests (workflows/tests/<slug>.yaml)',
97
+ skip: 'comma-separated check steps to leave out: typecheck, unit, apps, agents, workflows',
98
+ 'object-type': 'the object type a new action acts on; its primary key names the key input',
99
+ body: 'the request body to send: a file, or - for standard input',
100
+ path: 'which webhook of the workflow, by its trigger path; needed when it has several',
101
+ method: 'the HTTP method to send; POST when the webhook takes it',
102
+ header: 'one header the sender would send, `Name: value` (the webhook’s own auth)',
103
+ query: 'a query string to send with the delivery, such as source=portal&urgent=1',
104
+ 'fail-fast': 'stop the check at the first step that fails, instead of running and reporting every step',
105
+ strict: 'fail on inherited failures too: items and files unchanged since the pull, which otherwise are listed and do not fail the check',
106
+ concurrency: 'how many Agent test cases run at once (1–16, default 2); cases that change records for real still run one at a time. Lower it when the model provider refuses for requests in flight',
94
107
 
95
108
  // Apps
96
109
  framework: 'App scaffold to create: next (default) or react (legacy Vite)',
@@ -104,7 +117,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
104
117
  // choose which version to RUN, one chooses which to publish. The old text
105
118
  // described only the third, so `workflow run --help` and `function run
106
119
  // --help` both advertised package.json.
107
- version: 'the version to act on — the one to run, or on a deploy the one to publish',
120
+ version: 'the version to act on — the one to run (`workflow run` also takes draft: the newest version not retired), or on a deploy the one to publish',
108
121
  input: "JSON object of the declared inputs — inline ('{\"key\": 1}') or @file.json",
109
122
  // The primary key alone. A subject-bound workflow takes exactly one, so the
110
123
  // flag spends no characters on the `{"pk": …}` the route models it as.
@@ -112,8 +125,11 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
112
125
  // Action requests.
113
126
  'subject-version': 'record version of --subject the request expects, so a stale read is refused',
114
127
  'idempotency-key': 'reuse a key to retry a submit and get the same request back; generated if omitted',
115
- 'no-promote': 'publish the version without moving the live pointer',
116
- 'skip-build-check': 'deploy even when the build output is older than the source',
128
+ 'no-promote': 'publish the version without moving the live pointer; with app deploy --prod, stage it for production',
129
+ build: 'run bun run build first, so the deployment is built from the source as it is now',
130
+ 'skip-build-check': 'deploy even when the build output is older than the source, and skip bun run check',
131
+ prod: 'go live once the required checks pass (a deploy is otherwise a preview)',
132
+ undo: 'undo the rollback: production serves what it did before, and is unpinned',
117
133
  'no-wait': 'return as soon as the run is queued instead of waiting for it to finish',
118
134
  // Named for what it RUNS, not where it runs: an author reads this to decide
119
135
  // between their working copy and what is deployed.
@@ -129,8 +145,8 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
129
145
 
130
146
  // Blueprint scaffolding
131
147
  template:
132
- 'starting rule for `blueprint new semantic-type`: non-negative, percentage, status-enum '
133
- + 'or code-length (default)',
148
+ 'starting shape: a semantic type\'s rule (non-negative, percentage, status-enum, '
149
+ + 'code-length, the default), or an action\'s (edit, the default, or create)',
134
150
 
135
151
  // Packs
136
152
  'remove-apps': 'also uninstall the apps the pack installed',
@@ -146,8 +162,9 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
146
162
  // Blueprint reads
147
163
  select: 'comma-separated property API names to return (default: whatever the service returns)',
148
164
  where:
149
- 'filter as <property>:<value>, <property>:<op>:<value>, or a JSON where-node for anything '
150
- + 'with and/or in it',
165
+ 'filter as <property>:<value>, <property>:<op>:<value>, <property>:in:<a>,<b> (also notIn and between; '
166
+ + 'a backslash before a comma keeps it in a value, or give a JSON array), or a JSON where-node for an '
167
+ + 'or/not; repeat it for conditions that must all hold',
151
168
  order: 'sort as <property>:asc or <property>:desc, comma-separated for more than one',
152
169
  // No number here on purpose. This map is keyed by flag NAME across every
153
170
  // command, and the three surfaces that take `--limit` cap a page at 1000,
@@ -155,6 +172,22 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
155
172
  // command's own refusal names its real ceiling.
156
173
  limit: 'maximum rows to return in one page; each surface has its own ceiling',
157
174
  'page-token': 'cursor from the previous response’s nextPageToken; omit for the first page',
175
+ as:
176
+ 'on the local runtime only: act as a local persona (`submitter`, or an approval step such as '
177
+ + '`dispatch_lead`), as `frontera local status` lists them; refused anywhere else',
178
+ // A metric's query (`blueprint metric query`).
179
+ measure: 'comma-separated measures of the metric to return; all of them when absent',
180
+ 'group-by': 'comma-separated dimensions of the metric to break the numbers down by',
181
+ grain: 'a series by hour, day, week, month, quarter or year over the metric’s timeseries dimension (added to the group-by)',
182
+ range: 'a time window as an ISO-8601 duration (P7D, PT12H, P3M), back from the newest value of the metric’s timeseries in the data, not from now',
183
+ start: 'the window’s start, an ISO date or timestamp (inclusive; a date is its midnight UTC)',
184
+ end: 'the window’s end, an ISO date or timestamp (exclusive; a date is its midnight UTC)',
185
+ compare: 'add the window before the one given: <measure>_prev, <measure>_delta and <measure>_pct',
186
+ only: 'live: only live runs; test: only test runs (from `local test`, Studio or a draft run)',
187
+ npm: 'with use-local: switch the App back to the published @frontera-sdk packages from npm, deleting vendor/frontera-sdk/ and any SDK copy under src/frontera/ (the output lists each file)',
188
+ 'object-set':
189
+ 'read a saved object set’s rows: its name, its reference (objectSet/<ref>) or its id, '
190
+ + 'from `frontera blueprint object-sets`',
158
191
  // Named for the field the response carries, which differs by surface —
159
192
  // Blueprint answers `nextPageToken`, data-integration answers `nextCursor`.
160
193
  // Matching each flag to its own response beats one name that is wrong half
@@ -184,8 +217,9 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
184
217
  // permissive and 0 matches nothing at all. Described the wrong way round
185
218
  // first, which made `--threshold 0` look like "return everything".
186
219
  threshold:
187
- 'maximum cosine distance a chunk may have; higher is more permissive (default 0.9). '
188
- + 'Not the similarity score shown in the output — 0 matches nothing',
220
+ 'maximum cosine distance a chunk may have, 0 to 2; higher is more permissive (default: the '
221
+ + 'base’s own match threshold, as an Agent’s retrieval uses it). Not the score shown in the '
222
+ + 'output: a score is 1 − distance / 2, so 0.5 keeps scores above 0.75',
189
223
  strategy:
190
224
  'text extraction to use per file: auto (default), text, or ocr; ocr needs an OCR engine on the base',
191
225
 
@@ -214,7 +248,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
214
248
  'org-key': '`-` to read an organization key (sk-org-…) from stdin, after the workspace key; widens the Blueprint to the whole model',
215
249
  blueprint: 'workspace (the slice granted to the workspace, default) or organization (the whole model; needs an organization key)',
216
250
  relink: 'point a project already linked to another workspace at this one',
217
- preview: 'list what a pull would bring, or a push would propose, file by file, and change nothing',
251
+ preview: 'local sync: list what a pull would bring, or a push would propose, file by file, and change nothing; app deploy: make a preview deployment (the default), said explicitly as a pipeline must',
218
252
  kinds: 'comma-separated: blueprint, skills, functions, plugins, knowledge, workflows, agents, evaluations',
219
253
  drafts: 'comma-separated items (agent/<slug>, workflow/<slug>, function/<slug>, blueprint) to read as their draft instead of the live version; the Blueprint’s draft takes an organization key',
220
254
  'all-changed': 'pull every item the workspace changed since the last pull, and every clean merge; never one that conflicts or replaces local edits',
@@ -230,14 +264,25 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
230
264
  edited: 'take the files as you edited them; the workspace’s copies beside them go',
231
265
  title: 'the change request’s title; a push that opens a new one needs it',
232
266
  migration: 'for a Blueprint change that is not additive: what happens to existing records',
233
- documents: 'comma-separated knowledge bases whose documents a pull brings, or a push proposes (`*` for every one); without it they stay as they are',
267
+ documents: 'comma-separated knowledge bases whose documents a pull brings (a base pulled now brings its own anyway), or a push proposes (`*` for every one)',
268
+ 'no-documents': 'pull knowledge bases without their documents (by default a pulled base brings them)',
269
+ 'no-app-code': 'pull Custom Apps without their code (by default a pulled App brings its deployed version into apps/<slug>/, unless that folder is there)',
270
+ release: 'Custom Apps the change request releases, comma-separated slug=deployment (the newest deployment when only the slug is given); Apply promotes each one last',
271
+ evals: 'run each Agent’s gated cases (gate: true) against the local runtime and attach the results, compared with its last applied run',
272
+ renders: 'attach each released App’s last render (its screenshot and console summary)',
273
+ screenshots: 'attach the newest screenshots a Forge session saved (its id; this session’s when run inside Forge)',
274
+ trace: 'attach a trace link (an http(s) address) to the change request',
234
275
  lessons: 'bring each Agent’s reviewed rules from customer feedback into agents/<slug>/lessons.yaml (never the feedback itself); off by default',
235
276
  conflicts: 'the conflicts the picks were chosen against, as `diff --json` names them; refused if they moved',
236
277
  'from-workspace': 'start the project from a platform workspace instead of the examples',
237
278
  'no-sample-data': 'do not generate sample rows for the pulled Blueprint',
279
+ 'keep-sample-data': 'leave the sample datasets as they are; by default they are made again (same seed, rows and day) after a pull that brings Blueprint items, or when an older generator made them',
280
+ 'remake-sample-data': 'make the sample datasets again with the newest sample rules (same seed, rows and day), even the ones a pull wrote, which local dev otherwise keeps as pulled',
238
281
  synthetic: 'generate sample rows for the Blueprint: values fit each property, links stay consistent',
239
282
  rows: 'rows per object type (default 200; types others link to get half)',
240
- seed: 'the same seed writes the same rows (default 7)',
283
+ seed: 'the same seed writes the same rows on the same day; with --today, on any day (default 7)',
284
+ today: 'the day sample due dates are spread around, YYYY-MM-DD (default: today, UTC)',
285
+ region: 'where sample geopoints fall: a country code (ID, US, GB…) or `<lat>,<lon>[,<radius km>]` (default: the project’s `locale` in frontera.yaml, else the workspace’s timezone, else US)',
241
286
  }
242
287
 
243
288
  /**
@@ -259,6 +304,8 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
259
304
  // take an integer version number, and this map is keyed by flag name for all
260
305
  // three. `<version>` is the only placeholder true of every consumer.
261
306
  version: 'version',
307
+ body: 'file',
308
+ header: 'name: value',
262
309
  framework: 'next|react',
263
310
  port: 'number',
264
311
  origin: 'url',
@@ -279,6 +326,15 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
279
326
  order: 'prop:asc',
280
327
  limit: 'n',
281
328
  'page-token': 'cursor',
329
+ 'object-set': 'name|ref|id',
330
+ measure: 'a,b',
331
+ 'group-by': 'a,b',
332
+ grain: 'hour|day|week|month|quarter|year',
333
+ range: 'P7D',
334
+ start: 'iso',
335
+ end: 'iso',
336
+ only: 'live|test',
337
+ as: 'persona',
282
338
  cursor: 'cursor',
283
339
  lifecycle: 'state',
284
340
  actions: 'allow_all|read_only|custom',
@@ -288,7 +344,7 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
288
344
  markings: 'a,b',
289
345
  'max-distance': '0-2',
290
346
  permissions: 'read,write',
291
- template: 'non-negative|percentage|status-enum|code-length',
347
+ template: 'non-negative|percentage|status-enum|code-length|edit|create',
292
348
  platform: 'url',
293
349
  'workspace-key': '-',
294
350
  'org-key': '-',
@@ -299,6 +355,9 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
299
355
  'delete-here': 'kind/name,…',
300
356
  'keep-local': 'kind/name,…',
301
357
  documents: 'name,…',
358
+ release: 'slug=deployment,…',
359
+ screenshots: 'session',
360
+ trace: 'url',
302
361
  keep: 'workspace|mine',
303
362
  picks: 'n=side,…',
304
363
  rows: 'n',
@@ -315,6 +374,7 @@ export function flagUsage(name: string, type: 'string' | 'boolean', alias?: stri
315
374
  return `${short}--${name} <${PLACEHOLDER[name] ?? 'value'}>`
316
375
  }
317
376
 
318
- export function describeFlag(name: string): string {
319
- return FLAG_HELP[name] ?? ''
377
+ /** A command's own wording for a flag (`meta.flagHelp`) wins: the one place a name means something else there. */
378
+ export function describeFlag(name: string, own?: Readonly<Record<string, string>>): string {
379
+ return own?.[name] ?? FLAG_HELP[name] ?? ''
320
380
  }
@@ -44,29 +44,31 @@ export const FORGE_ARTIFACT_KINDS = [
44
44
  * `search`, `runs`, `bindings` — is a read and is deliberately absent.
45
45
  */
46
46
  export const TOUCHING_VERBS: Record<ForgeArtifactKind, readonly string[]> = {
47
- agent: ['apply', 'create', 'publish', 'revert', 'discard', 'skill-attach', 'skill-detach'],
47
+ agent: ['apply', 'create', 'publish', 'revert', 'discard', 'skill-attach', 'skill-detach', 'archive', 'restore'],
48
48
  automation: ['deploy', 'promote', 'enable', 'disable', 'init', 'bind', 'unbind', 'run'],
49
- app: ['deploy', 'promote', 'save', 'add'],
49
+ app: ['deploy', 'promote', 'rollback', 'unpin', 'save', 'add'],
50
50
  blueprint: ['apply', 'publish', 'create', 'update', 'delete', 'rename', 'rollback', 'bind', 'grant', 'adopt', 'editable'],
51
51
  action: ['prepare', 'deploy', 'grant', 'review'],
52
52
  skill: ['push', 'delete'],
53
53
  source: ['create', 'update', 'revise', 'disable'],
54
- dataset: ['create', 'upload', 'test-source'],
54
+ // `revise` cuts the dataset's next revision from its CSV.
55
+ dataset: ['create', 'upload', 'revise', 'test-source'],
55
56
  knowledge: ['create', 'attach', 'detach', 'upload', 'retry', 'delete'],
57
+ // `webhook-test` starts a run on the draft through its webhook, as `run` does.
56
58
  // `get`, `lineage`, `list`, `runs` and `validate` are reads — `validate` is
57
59
  // `push --dry-run` and writes nothing. `cancel` is deliberately absent: it
58
60
  // stops one in-flight run and leaves the definition, the versions and the
59
61
  // live pointer exactly as they were, and its positional is a RUN id, not a
60
62
  // slug, so it could not name the workflow page anyway. Same call
61
63
  // `automation` makes, which lists `run` and not `cancel`.
62
- workflow: ['push', 'promote', 'run'],
64
+ workflow: ['push', 'promote', 'run', 'webhook-test', 'archive', 'restore'],
63
65
  }
64
66
 
65
67
  /** Verbs after which the artifact is LIVE, not merely changed. */
66
68
  const LIVE_VERBS: Partial<Record<ForgeArtifactKind, readonly string[]>> = {
67
69
  agent: ['publish'],
68
70
  automation: ['promote', 'enable'],
69
- app: ['promote'],
71
+ app: ['promote', 'rollback'],
70
72
  blueprint: ['publish'],
71
73
  workflow: ['promote'],
72
74
  }
@@ -97,9 +99,9 @@ const FILE_POSITIONAL_VERBS: Partial<Record<ForgeArtifactKind, readonly string[]
97
99
  automation: ['deploy', 'init'],
98
100
  skill: ['push'],
99
101
  blueprint: ['create'],
100
- // `app deploy` names nothing (the folder is the app); `app promote` names
101
- // a VERSION. The app itself is the project's, which the pane knows.
102
- app: ['deploy', 'promote', 'save', 'add'],
102
+ // `app deploy` names nothing (the folder is the app); `app promote` and
103
+ // `app rollback` name a DEPLOYMENT. The app itself is the project's, which the pane knows.
104
+ app: ['deploy', 'promote', 'rollback', 'unpin', 'save', 'add'],
103
105
  }
104
106
 
105
107
  /**
@@ -122,7 +124,7 @@ export const BOOLEAN_FLAGS = new Set([
122
124
  'accept-contract-change', 'accept-orphaned-knowledge', 'dry-run', 'end-user', 'no-activate',
123
125
  'no-components', 'no-git', 'no-input', 'no-install', 'no-open', 'no-promote', 'no-wait',
124
126
  'remove-apps', 'token-stdin', 'prune', 'dev', 'revoke', 'draft', 'check',
125
- 'skip-build-check',
127
+ 'skip-build-check', 'prod', 'preview', 'undo', 'build',
126
128
  ])
127
129
 
128
130
  /**
@@ -401,9 +403,10 @@ export function parseFronteraCommands(line: string): ParsedFronteraCommand[] {
401
403
  : (rest[0] ?? null)
402
404
  const live =
403
405
  (LIVE_VERBS[kind] ?? []).includes(verb)
404
- // `app deploy` and `function deploy` build and promote in one act
405
- // unless told not to.
406
- || ((kind === 'app' || kind === 'automation') && verb === 'deploy' && !flags.has('no-promote'))
406
+ // `function deploy` builds and promotes in one act unless told not to;
407
+ // `app deploy` makes a preview unless `--prod` (and `--prod --no-promote` stages it).
408
+ || (kind === 'automation' && verb === 'deploy' && !flags.has('no-promote'))
409
+ || (kind === 'app' && verb === 'deploy' && flags.has('prod') && !flags.has('no-promote'))
407
410
  out.push({ kind, verb, ref, live })
408
411
  }
409
412
  return out
@@ -0,0 +1,89 @@
1
+ import { existsSync, lstatSync, readFileSync } from 'node:fs'
2
+ import { dirname, join, posix, relative, resolve, sep } from 'node:path'
3
+
4
+ import { isExcluded, type SourceFile } from './packaging'
5
+
6
+ /**
7
+ * The files a Function imports from outside its own project folder.
8
+ *
9
+ * A Function deployed from its folder (`functions/<slug>/`) may import a
10
+ * helper beside it (`../../shared/eligibility` from `src/index.ts`). The
11
+ * bundle holds that code, but its source archive is the folder alone, so a
12
+ * pull would bring a Function that no longer builds. Each such file travels
13
+ * in the archive under `frontera-imports/`, by its path from the folder with
14
+ * every `..` as `__up__` (`frontera-imports/__up__/shared/eligibility.ts`),
15
+ * and a pull writes it back where the import finds it.
16
+ *
17
+ * Only files the code imports, by relative path, and never above the
18
+ * solution (the nearest folder with `frontera.yaml`, else two folders above
19
+ * the Function's): the bundle already carries their code.
20
+ */
21
+
22
+ export const OUTSIDE_DIR = 'frontera-imports'
23
+ const UP = '__up__'
24
+
25
+ const RELATIVE_IMPORT = /(?:from|import)\s*\(?\s*['"](\.{1,2}\/[^'"]+)['"]/g
26
+ const CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs|json)$/
27
+ const EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.js', '.mjs', '.json', '/index.ts', '/index.js']
28
+
29
+ /** `../shared/x.ts` (from the folder) → `frontera-imports/__up__/shared/x.ts`. */
30
+ export function outsidePath(fromFolder: string): string {
31
+ return `${OUTSIDE_DIR}/${posix.normalize(fromFolder).split('/').map((s) => (s === '..' ? UP : s)).join('/')}`
32
+ }
33
+
34
+ /** `frontera-imports/__up__/shared/x.ts` → `../shared/x.ts`; null for any other archive path. */
35
+ export function fromOutsidePath(archivePath: string): string | null {
36
+ if (!archivePath.startsWith(`${OUTSIDE_DIR}/`)) return null
37
+ const rel = archivePath.slice(OUTSIDE_DIR.length + 1).split('/').map((s) => (s === UP ? '..' : s)).join('/')
38
+ return rel.startsWith('../') ? rel : null
39
+ }
40
+
41
+ function solutionRoot(projectRoot: string): string {
42
+ for (let dir = projectRoot; ; dir = dirname(dir)) {
43
+ if (existsSync(join(dir, 'frontera.yaml'))) return dir
44
+ if (dirname(dir) === dir) break
45
+ }
46
+ return dirname(dirname(projectRoot))
47
+ }
48
+
49
+ const within = (root: string, path: string) => path === root || path.startsWith(`${root}${sep}`)
50
+
51
+ /**
52
+ * The files outside `projectRoot` that `entry` imports, followed through the
53
+ * files it imports inside it too, as archive entries under `frontera-imports/`.
54
+ */
55
+ export function outsideImports(projectRoot: string, entry: string): SourceFile[] {
56
+ const root = resolve(projectRoot)
57
+ const bound = resolve(solutionRoot(root))
58
+ const seen = new Set<string>()
59
+ const out: SourceFile[] = []
60
+ const read = (path: string): string | null => {
61
+ try {
62
+ return existsSync(path) && lstatSync(path).isFile() ? readFileSync(path, 'utf8') : null
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+ const visit = (file: string) => {
68
+ if (seen.has(file)) return
69
+ const text = read(file)
70
+ if (text === null) return
71
+ seen.add(file)
72
+ if (!within(root, file)) {
73
+ const rel = relative(root, file).split(sep).join('/')
74
+ if (CODE.test(file) && !isExcluded(rel.replace(/^(\.\.\/)+/, ''))) out.push({ rel: outsidePath(rel), bytes: new TextEncoder().encode(text) })
75
+ }
76
+ for (const m of text.matchAll(RELATIVE_IMPORT)) {
77
+ const base = resolve(dirname(file), m[1]!)
78
+ if (!within(bound, base) || base.split(sep).includes('node_modules')) continue
79
+ const found = EXTENSIONS.map((ext) => `${base}${ext}`).find((c) => read(c) !== null)
80
+ if (!found) continue
81
+ visit(found)
82
+ // A JavaScript file's types beside it travel with it (a module a pull recovered from a
83
+ // deployed bundle is a `.js` typed by its `.d.ts`), so the next pull still typechecks.
84
+ if (found.endsWith('.js')) visit(`${found.slice(0, -'.js'.length)}.d.ts`)
85
+ }
86
+ }
87
+ visit(resolve(entry))
88
+ return out
89
+ }
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
  }