@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
@@ -23,7 +23,10 @@ export interface WorkflowSummary {
23
23
  liveVersion: number | null
24
24
  latestDraftNumber: number | null
25
25
  versionCount: number
26
+ /** Live top-level runs only: test runs and a workflow's child runs are not counted. */
26
27
  runCount: number
28
+ /** Test runs (a draft run from `local test`, Studio or `workflow_run`); absent from a service that does not count them. */
29
+ testRunCount?: number
27
30
  lastRunAt: string | null
28
31
  lastRunStatus: string | null
29
32
  triggerKinds: string[]
@@ -67,6 +70,8 @@ export interface PushResult {
67
70
  version?: { id: string; number: number; status: string }
68
71
  grants?: string[]
69
72
  lineage?: LineageEdge[]
73
+ /** What the service accepted but says, one sentence each (a step that may not be able to run here). */
74
+ warnings?: string[]
70
75
  }
71
76
 
72
77
  export interface LineageEdge {
@@ -263,8 +268,8 @@ export class WorkflowApi {
263
268
  * carried here, because no verb pages yet and a parameter no caller passes
264
269
  * reads as a capability the CLI has. `function runs` is capped the same way.
265
270
  */
266
- runs(slug: string): Promise<RunSummary[]> {
267
- return this.call<RunSummary[]>(`/v1/workflows/${encodeURIComponent(slug)}/runs`)
271
+ runs(slug: string, source?: 'live' | 'test'): Promise<RunSummary[]> {
272
+ return this.call<RunSummary[]>(`/v1/workflows/${encodeURIComponent(slug)}/runs${source ? `?source=${source}` : ''}`)
268
273
  }
269
274
 
270
275
  /** One run and, with `depth: 'all'`, the child runs beneath it. */
@@ -272,9 +277,35 @@ export class WorkflowApi {
272
277
  return this.call<RunTree>(`/v1/workflows/runs/${encodeURIComponent(id)}?depth=${depth}`)
273
278
  }
274
279
 
280
+ /** Take a workflow out of service, every version and run kept: nothing starts it, and it takes no new version until restored. */
281
+ archive(slug: string): Promise<{ id: string; slug: string; archivedAt: string | null }> {
282
+ return this.call(`/v1/workflows/${encodeURIComponent(slug)}/archive`, { method: 'POST' })
283
+ }
284
+
285
+ /** Put an archived workflow back as it was. */
286
+ restore(slug: string): Promise<{ id: string; slug: string; archivedAt: string | null }> {
287
+ return this.call(`/v1/workflows/${encodeURIComponent(slug)}/restore`, { method: 'POST' })
288
+ }
289
+
275
290
  cancel(id: string): Promise<{ status: string }> {
276
291
  return this.call(`/v1/workflows/runs/${encodeURIComponent(id)}/cancel`, { method: 'POST' })
277
292
  }
293
+
294
+ /**
295
+ * Arm the test door of a workflow's draft for one delivery, as Studio's
296
+ * Listen does: the latest draft unless `version` names one. Answers each
297
+ * webhook door's test URL and when the Listen ends.
298
+ */
299
+ listen(slug: string, version?: number): Promise<WebhookListen> {
300
+ return this.call(`/v1/workflows/${encodeURIComponent(slug)}/listen`, { method: 'POST', body: version === undefined ? {} : { version } })
301
+ }
302
+ }
303
+
304
+ /** `POST /v1/workflows/:slug/listen`. */
305
+ export interface WebhookListen {
306
+ version: number
307
+ until: string
308
+ doors: Array<{ path: string; methods: string[]; test: string }>
278
309
  }
279
310
 
280
311
  /** `details.errors` from a 400, when the failure was the definition itself. */
@@ -0,0 +1,156 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { DEFAULT_BLUEPRINT_TYPES_OUTPUT } from './blueprint-types'
5
+ import { collectSourceFiles } from './packaging'
6
+ import { APP_SDK_PACKAGES } from './template'
7
+
8
+ /**
9
+ * What a Custom App calls on the platform, read from its source at deploy
10
+ * (design 2026-10-18 §6, the Blueprint contract): the object types it reads
11
+ * with the properties its generated types give them, the governed Actions it
12
+ * submits and the Functions it starts. The platform checks it against the
13
+ * workspace a deployment would go live in. Best effort by construction: a
14
+ * name built at run time is not seen, and nothing here is authority.
15
+ */
16
+
17
+ export interface AppContract {
18
+ /** The Blueprint schema digest the App's generated types came from. */
19
+ digest?: string
20
+ objectTypes: Record<string, string[]>
21
+ actions: string[]
22
+ functions: string[]
23
+ }
24
+
25
+ const CODE = /\.(tsx?|jsx?|mjs|cjs)$/
26
+
27
+ const OBJECT_READ = [
28
+ /\buse(?:Objects|ObjectInstance)\s*(?:<[^>]*>)?\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g,
29
+ /\bobjectType\s*:\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g,
30
+ /\/v1\/blueprint\/object-types\/([A-Za-z_][A-Za-z0-9_]*)\//g,
31
+ ]
32
+ const ACTION = [/\buseAction\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g, /governed-actions\/actions\/([A-Za-z_][A-Za-z0-9_]*)\/requests/g]
33
+ const FUNCTION = [/\/v1\/(?:functions|automations)\/([a-z0-9][a-z0-9-]*)\/app-run/g, /\b(?:runFunction|startFunction)\s*\(\s*['"]([a-z0-9][a-z0-9-]*)['"]/g]
34
+
35
+ /**
36
+ * The SDK copy a pulled App carries under src/frontera/ (`APP_SDK_PACKAGES`):
37
+ * the platform's code, not the App's. It names no object type of the App's
38
+ * except in its examples, so it is not read.
39
+ */
40
+ const SDK_COPY_DIRS = Object.values(APP_SDK_PACKAGES).map((p) => `${p.dir}/`)
41
+ const isSdkCopy = (rel: string) => SDK_COPY_DIRS.some((dir) => rel.startsWith(dir))
42
+
43
+ /** Before a token, a `/` starts a regular expression rather than a division. */
44
+ const REGEX_BEFORE = /(?:^|[(,=:[!&|?{};+\-*%~^]|\b(?:return|typeof|case|do|else|in|of|new|delete|void|throw|yield|await))\s*$/
45
+
46
+ /**
47
+ * The code with its comments blanked out (the same length, line breaks kept):
48
+ * a doc comment's example, such as `useObjects('SupportTicket')` in the SDK's
49
+ * own docs, is not a call. Strings, template literals and regular
50
+ * expressions are read as such, so a `//` in a URL is not a comment.
51
+ */
52
+ export function withoutComments(text: string): string {
53
+ let out = ''
54
+ let i = 0
55
+ // Template literals nest through `${…}`: each open one, and the brace depth its expression is at.
56
+ const templates: number[] = []
57
+ let depth = 0
58
+ const blank = (s: string) => s.replace(/[^\n]/g, ' ')
59
+ while (i < text.length) {
60
+ const c = text[i]!
61
+ const next = text[i + 1]
62
+ if (c === '/' && next === '/') {
63
+ const end = text.indexOf('\n', i)
64
+ const stop = end < 0 ? text.length : end
65
+ out += blank(text.slice(i, stop))
66
+ i = stop
67
+ continue
68
+ }
69
+ if (c === '/' && next === '*') {
70
+ const end = text.indexOf('*/', i + 2)
71
+ const stop = end < 0 ? text.length : end + 2
72
+ out += blank(text.slice(i, stop))
73
+ i = stop
74
+ continue
75
+ }
76
+ if (c === '/' && REGEX_BEFORE.test(out.slice(-40))) {
77
+ let j = i + 1
78
+ let inClass = false
79
+ while (j < text.length && text[j] !== '\n') {
80
+ const d = text[j]!
81
+ if (d === '\\') j += 1
82
+ else if (d === '[') inClass = true
83
+ else if (d === ']') inClass = false
84
+ else if (d === '/' && !inClass) break
85
+ j += 1
86
+ }
87
+ out += text.slice(i, j + 1)
88
+ i = j + 1
89
+ continue
90
+ }
91
+ if (c === "'" || c === '"') {
92
+ let j = i + 1
93
+ while (j < text.length && text[j] !== c && text[j] !== '\n') j += text[j] === '\\' ? 2 : 1
94
+ out += text.slice(i, j + 1)
95
+ i = j + 1
96
+ continue
97
+ }
98
+ if (c === '`' || (c === '}' && templates.length && templates.at(-1) === depth)) {
99
+ // A template literal's text, from its start or the end of a `${…}` up to its end or the next `${`.
100
+ if (c === '}') templates.pop()
101
+ let j = i + 1
102
+ while (j < text.length && text[j] !== '`' && !(text[j] === '$' && text[j + 1] === '{')) j += text[j] === '\\' ? 2 : 1
103
+ if (text[j] === '$') {
104
+ templates.push(depth)
105
+ out += text.slice(i, j + 2)
106
+ i = j + 2
107
+ } else {
108
+ out += text.slice(i, j + 1)
109
+ i = j + 1
110
+ }
111
+ continue
112
+ }
113
+ if (c === '{') depth += 1
114
+ else if (c === '}') depth -= 1
115
+ out += c
116
+ i += 1
117
+ }
118
+ return out
119
+ }
120
+
121
+ function matches(text: string, patterns: RegExp[], into: Set<string>): void {
122
+ for (const pattern of patterns) for (const m of text.matchAll(pattern)) if (m[1]) into.add(m[1])
123
+ }
124
+
125
+ /** Each object type's properties as the App's generated Blueprint types declare them. */
126
+ export function generatedProperties(text: string): { digest?: string; types: Map<string, string[]> } {
127
+ const digest = text.match(/Blueprint schema digest: (sha256:[0-9a-f]{64})/)?.[1]
128
+ const types = new Map<string, string[]>()
129
+ for (const block of text.matchAll(/export interface ([A-Za-z_][A-Za-z0-9_]*) \{\n([\s\S]*?)\n\}/g)) {
130
+ const props = [...block[2]!.matchAll(/^ {2}([A-Za-z_][A-Za-z0-9_]*)\??:/gm)].map((m) => m[1]!)
131
+ types.set(block[1]!, props)
132
+ }
133
+ return { ...(digest ? { digest } : {}), types }
134
+ }
135
+
136
+ export function collectAppContract(root: string): AppContract {
137
+ const objects = new Set<string>()
138
+ const actions = new Set<string>()
139
+ const functions = new Set<string>()
140
+ const generated = join(root, DEFAULT_BLUEPRINT_TYPES_OUTPUT)
141
+ for (const file of collectSourceFiles(root)) {
142
+ if (!CODE.test(file.rel) || file.rel.startsWith('.frontera/') || isSdkCopy(file.rel) || join(root, file.rel) === generated || /(^|\/)(__tests__|tests?)\//.test(file.rel) || /\.(test|spec)\.[jt]sx?$/.test(file.rel)) continue
143
+ // What the code calls, not what its comments show.
144
+ const text = withoutComments(new TextDecoder().decode(file.bytes))
145
+ matches(text, OBJECT_READ, objects)
146
+ matches(text, ACTION, actions)
147
+ matches(text, FUNCTION, functions)
148
+ }
149
+ const types = existsSync(generated) ? generatedProperties(readFileSync(generated, 'utf8')) : { types: new Map<string, string[]>() }
150
+ return {
151
+ ...('digest' in types && types.digest ? { digest: types.digest } : {}),
152
+ objectTypes: Object.fromEntries([...objects].sort().map((name) => [name, types.types.get(name) ?? []])),
153
+ actions: [...actions].sort(),
154
+ functions: [...functions].sort(),
155
+ }
156
+ }
@@ -0,0 +1,125 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ /**
5
+ * The `@frontera-sdk/*` versions an App's own lockfile resolved from the
6
+ * registry: what its code was written and checked against. A pulled App's
7
+ * `bun.lock` (or `package-lock.json`) is the best record of that, better than
8
+ * whatever SDK this CLI carries, so a repair goes back to it instead of
9
+ * swapping in another SDK the code was never built with.
10
+ *
11
+ * Only registry resolutions count (`1.50.61`): a `file:`, `link:` or
12
+ * `workspace:` entry records a copy that may not exist anywhere else.
13
+ */
14
+ export interface LockedSdk {
15
+ /** The version the lockfile installed, e.g. `1.50.61`. */
16
+ version: string
17
+ /** The range package.json asked for when the lockfile was written (`^1.43.10`), when it was a registry range. */
18
+ range?: string
19
+ }
20
+
21
+ const REGISTRY_VERSION = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/
22
+ const REGISTRY_RANGE = /^[\^~]?\d+(?:\.\d+){0,2}(?:[-+][\w.-]+)?$/
23
+
24
+ /** Whether a package.json spec is a registry version or range (`^1.50.61`, `1.50.61`), not a `file:`, `link:` or `workspace:` one. */
25
+ export const isRegistryRange = (spec: string | undefined): boolean => !!spec && REGISTRY_RANGE.test(spec)
26
+
27
+ export function lockedSdkVersions(appRoot: string): Map<string, LockedSdk> {
28
+ const out = new Map<string, LockedSdk>()
29
+ let bun: string | null = null
30
+ try {
31
+ bun = readFileSync(join(appRoot, 'bun.lock'), 'utf8')
32
+ } catch {
33
+ bun = null
34
+ }
35
+ if (bun) {
36
+ // `"@frontera-sdk/blueprint": ["@frontera-sdk/blueprint@1.50.61", …]` under `packages`.
37
+ for (const m of bun.matchAll(/"(@frontera-sdk\/[a-z0-9-]+)":\s*\[\s*"(@frontera-sdk\/[a-z0-9-]+)@([^"]+)"/g)) {
38
+ if (m[1] !== m[2] || !REGISTRY_VERSION.test(m[3]!) || out.has(m[1]!)) continue
39
+ out.set(m[1]!, { version: m[3]! })
40
+ }
41
+ // `"@frontera-sdk/blueprint": "^1.43.10"` under the root workspace's dependencies.
42
+ for (const m of bun.matchAll(/"(@frontera-sdk\/[a-z0-9-]+)":\s*"([^"]+)"/g)) {
43
+ const locked = out.get(m[1]!)
44
+ if (locked && !locked.range && REGISTRY_RANGE.test(m[2]!)) locked.range = m[2]!
45
+ }
46
+ return out
47
+ }
48
+ try {
49
+ const npm = JSON.parse(readFileSync(join(appRoot, 'package-lock.json'), 'utf8')) as {
50
+ packages?: Record<string, { version?: string; resolved?: string; dependencies?: Record<string, string> }>
51
+ }
52
+ const root = npm.packages?.[''] ?? {}
53
+ for (const [key, entry] of Object.entries(npm.packages ?? {})) {
54
+ const m = /^node_modules\/(@frontera-sdk\/[a-z0-9-]+)$/.exec(key)
55
+ if (!m || !entry.version || !REGISTRY_VERSION.test(entry.version) || (entry.resolved && !/^https?:/.test(entry.resolved))) continue
56
+ const range = root.dependencies?.[m[1]!]
57
+ out.set(m[1]!, { version: entry.version, ...(range && REGISTRY_RANGE.test(range) ? { range } : {}) })
58
+ }
59
+ } catch {
60
+ // No lockfile: nothing recorded.
61
+ }
62
+ return out
63
+ }
64
+
65
+ /**
66
+ * The spec that brings the locked version back: a caret on the release the
67
+ * lockfile installed (`^1.50.61`), never the older range the lockfile was
68
+ * written for (`^1.43.10`), so package.json names the SDK the code was
69
+ * checked against even where an install runs without the lockfile.
70
+ */
71
+ export function lockedSpec(locked: LockedSdk): string {
72
+ return `^${locked.version}`
73
+ }
74
+
75
+ /**
76
+ * Record in the App's lockfile that package.json now asks for `specs`, so a
77
+ * frozen install (`bun install --frozen-lockfile`, CI, `app deploy`) accepts
78
+ * the restored package.json: only the root workspace's own dependency ranges
79
+ * change, never a resolution, and each spec still matches the release the
80
+ * lockfile installed. Returns whether the lockfile changed.
81
+ */
82
+ export function recordLockedRanges(appRoot: string, specs: Record<string, string>): boolean {
83
+ const names = Object.keys(specs)
84
+ if (!names.length) return false
85
+ const bunPath = join(appRoot, 'bun.lock')
86
+ let bun: string | null = null
87
+ try {
88
+ bun = readFileSync(bunPath, 'utf8')
89
+ } catch {
90
+ bun = null
91
+ }
92
+ if (bun !== null) {
93
+ // The root workspace's ranges sit between `"workspaces": {` and the top-level `"packages": {`.
94
+ const start = bun.indexOf('"workspaces"')
95
+ const end = start < 0 ? -1 : bun.indexOf('\n "packages"', start)
96
+ if (start < 0 || end < 0) return false
97
+ let section = bun.slice(start, end)
98
+ for (const pkg of names) {
99
+ const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')
100
+ section = section.replace(new RegExp(`("${escaped}":\\s*)"[^"]*"`, 'g'), (_m, head: string) => `${head}"${specs[pkg]}"`)
101
+ }
102
+ const next = bun.slice(0, start) + section + bun.slice(end)
103
+ if (next === bun) return false
104
+ writeFileSync(bunPath, next)
105
+ return true
106
+ }
107
+ const npmPath = join(appRoot, 'package-lock.json')
108
+ try {
109
+ const text = readFileSync(npmPath, 'utf8')
110
+ const npm = JSON.parse(text) as { packages?: Record<string, { dependencies?: Record<string, string> }> }
111
+ const root = npm.packages?.['']
112
+ if (!root?.dependencies) return false
113
+ let changed = false
114
+ for (const pkg of names) {
115
+ if (pkg in root.dependencies && root.dependencies[pkg] !== specs[pkg]) {
116
+ root.dependencies[pkg] = specs[pkg]!
117
+ changed = true
118
+ }
119
+ }
120
+ if (changed) writeFileSync(npmPath, `${JSON.stringify(npm, null, /^(\s+)"/m.exec(text)?.[1] ?? 2)}\n`)
121
+ return changed
122
+ } catch {
123
+ return false
124
+ }
125
+ }
package/src/args.ts CHANGED
@@ -6,6 +6,12 @@ export type FlagSpec = Readonly<Record<string, FlagKind>>
6
6
  export interface ParsedArgs {
7
7
  positional: string[]
8
8
  flags: Record<string, string | boolean>
9
+ /**
10
+ * Every value of a string flag given more than once, in order. `flags`
11
+ * keeps the last; a command that ANDs repeated values (`--where`) reads
12
+ * them all here.
13
+ */
14
+ repeated: Record<string, string[]>
9
15
  }
10
16
 
11
17
  /**
@@ -28,6 +34,11 @@ export function parseArgs(
28
34
  ): ParsedArgs {
29
35
  const positional: string[] = []
30
36
  const flags: Record<string, string | boolean> = {}
37
+ const repeated: Record<string, string[]> = {}
38
+ const setString = (name: string, value: string) => {
39
+ if (typeof flags[name] === 'string') (repeated[name] ??= [flags[name] as string]).push(value)
40
+ flags[name] = value
41
+ }
31
42
 
32
43
  for (let i = 0; i < argv.length; i += 1) {
33
44
  let token = argv[i]!
@@ -86,7 +97,7 @@ export function parseArgs(
86
97
 
87
98
  if (eq !== -1) {
88
99
  // `--dir=` is a deliberate empty string, not a missing value.
89
- flags[name] = token.slice(eq + 1)
100
+ setString(name, token.slice(eq + 1))
90
101
  continue
91
102
  }
92
103
 
@@ -100,9 +111,9 @@ export function parseArgs(
100
111
  `pass it as --${name} <value> or --${name}=<value>`,
101
112
  )
102
113
  }
103
- flags[name] = value
114
+ setString(name, value)
104
115
  i += 1
105
116
  }
106
117
 
107
- return { positional, flags }
118
+ return { positional, flags, repeated }
108
119
  }
@@ -29,6 +29,16 @@ export function classifyKey(token: string): CredentialKind {
29
29
  )
30
30
  }
31
31
 
32
+ /**
33
+ * Whether a token is a key rather than a person's session, by its prefix:
34
+ * workspace (`sk-ws-`), organization (`sk-org-`) and every other `sk-` key
35
+ * the platform issues, and an Agent key (`sak_`), which a profile does not
36
+ * store but `FRONTERA_TOKEN` can carry. A person's session token has neither.
37
+ */
38
+ export function isKeyToken(token: string): boolean {
39
+ return token.startsWith('sk-') || token.startsWith('sak_')
40
+ }
41
+
32
42
  /**
33
43
  * Prove the key works BEFORE it is stored.
34
44
  *
@@ -38,12 +38,20 @@ export const AUTOMATION_SDK_FILES = [
38
38
  // `testing.ts` imports the file-handle helpers from it; without it every
39
39
  // scaffolded project failed to build with `Could not resolve: "./runtime-context"`.
40
40
  'frontera/automation/runtime-context.ts',
41
+ // `FronteraCallError`, which every `ctx.*` refusal throws and `index.ts`,
42
+ // `testing.ts` and `runtime-context.ts` import.
43
+ 'frontera/automation/errors.ts',
41
44
  // `inputs.ts` is authoring surface too: `validateInputValue` is what a
42
45
  // handler's own tests and the Console call to check a value against the
43
46
  // manifest's `inputs` schema, and `index.ts` re-exports it — so omitting it
44
47
  // breaks a scaffolded project's typecheck the same way skipping `testing.ts`
45
48
  // would.
46
49
  'frontera/automation/inputs.ts',
50
+ // `subjectOf` (an Action's subject and version from a query row) is exported
51
+ // by `index.ts`; `test-check.ts` is how `define.ts` checks a handler the way
52
+ // the platform will when `testing.ts` runs it.
53
+ 'frontera/automation/subject.ts',
54
+ 'frontera/automation/test-check.ts',
47
55
  'frontera/blueprint/types.ts',
48
56
  ] as const
49
57
 
@@ -187,8 +195,10 @@ import automation, { OBJECT_TYPE } from './index'
187
195
  // A handler is just a function, so it can be tested without deploying. The
188
196
  // context refuses what the platform refuses — a grant the manifest does not
189
197
  // declare, a step name used twice — so a passing test means something.
198
+ // \`replay: true\` runs it as the platform does, again from the top after each
199
+ // step, so a read outside a step that a later step changes fails here too.
190
200
  test('handles an empty result', async () => {
191
- const { ctx, steps } = createTestContext({ grants: ['blueprint:read'] })
201
+ const { ctx, steps } = createTestContext({ replay: true, grants: ['blueprint:read'] })
192
202
 
193
203
  const result = await automation.handler(ctx)
194
204
 
@@ -199,6 +209,7 @@ test('handles an empty result', async () => {
199
209
 
200
210
  test('handles rows', async () => {
201
211
  const { ctx, steps } = createTestContext({
212
+ replay: true,
202
213
  grants: ['blueprint:read'],
203
214
  // Stub only what this test is about; an object type you do not stub returns
204
215
  // no rows, which is a real answer.