@frontera-sdk/cli 1.52.39 → 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
@@ -8,7 +8,8 @@ import { slugify } from '../../local/local-project'
8
8
  import { readProviderSetup } from '../../local/local-providers'
9
9
  import { readLink } from '../../local/local-sync-link'
10
10
  import { generateSyntheticData, addMissingBackings, loadScenario, SCENARIO_TEMPLATE } from '../../local/local-synthetic-data'
11
- import { writeSolutionScaffold } from '../../local/local-scaffold'
11
+ import { isoDay, writeSampleOptions } from '../../local/local-sample-remake'
12
+ import { migrateMcpConfigs, writeSolutionScaffold } from '../../local/local-scaffold'
12
13
  import { flagBool, flagString, type Command, type CommandContext } from '../types'
13
14
  import { localSync } from './sync'
14
15
 
@@ -37,6 +38,7 @@ export const localInit: Command = {
37
38
  'org-key': 'string',
38
39
  kinds: 'string',
39
40
  'no-sample-data': 'boolean',
41
+ 'no-app-code': 'boolean',
40
42
  rows: 'string',
41
43
  seed: 'string',
42
44
  },
@@ -48,7 +50,6 @@ export const localInit: Command = {
48
50
  'frontera local init acme --from-workspace --platform $URL --workspace-key -',
49
51
  ],
50
52
  offline: true,
51
- internal: true,
52
53
  },
53
54
 
54
55
  async run(ctx) {
@@ -73,6 +74,12 @@ export const localInit: Command = {
73
74
  // The starter Agent takes the model the FDE chose for new Agents (`frontera local model-default`);
74
75
  // a project started from a workspace gets the workspace's Agents instead of the examples.
75
76
  const scaffold = writeSolutionScaffold(target, name, force, { agentModel: readProviderSetup().defaults.agentModel, samples: !fromWorkspace })
77
+ // A config kept (not --force) that still launches `frontera local docs-mcp` launches `frontera mcp`.
78
+ for (const file of migrateMcpConfigs(target)) {
79
+ if (!scaffold.written.includes(file)) scaffold.written.push(file)
80
+ const at = scaffold.skipped.indexOf(file)
81
+ if (at >= 0) scaffold.skipped.splice(at, 1)
82
+ }
76
83
  const listed = (files: string[]) => files.filter((f) => !f.startsWith('vendor/'))
77
84
  const written = [...new Set(listed([...scaffold.written, ...harness.written]))]
78
85
  const skipped = listed([...scaffold.skipped, ...harness.skipped]).filter((f) => !written.includes(f))
@@ -131,14 +138,16 @@ async function startFromWorkspace(ctx: CommandContext, target: string): Promise<
131
138
  } else if (!readLink(target)) {
132
139
  throw new UsageError('no workspace to start from', 'pass --platform <url> --workspace-key - with the key on stdin')
133
140
  }
134
- const kinds = flagString(ctx, 'kinds') ?? 'blueprint,skills,functions,plugins,knowledge,workflows,agents,evaluations'
141
+ // Everything a solution is made of: Apps, Chat Apps and slide themes too, so a pulled project has what the workspace lists.
142
+ const kinds = flagString(ctx, 'kinds') ?? 'blueprint,skills,functions,plugins,knowledge,workflows,agents,evaluations,themes,chat-apps,apps'
135
143
  // `none` links the project and pulls nothing, for someone who picks items next (`local sync pull <kind/name>…`).
136
144
  if (kinds === 'none') {
137
145
  const link = readLink(target)
138
146
  return { workspace: link?.workspaceName ?? link?.workspace ?? 'the workspace', pulled: 0, sampleRows: 0 }
139
147
  }
140
148
  ctx.output.note(`Pulling ${kinds.split(',').join(', ')}`)
141
- const pull = (await localSync.run({ ...sub, positional: ['pull'], flags: { kinds } })).data as { pulled: string[]; warnings: string[] }
149
+ // Custom Apps come with their code (apps/<slug>/), unless --no-app-code.
150
+ const pull = (await localSync.run({ ...sub, positional: ['pull'], flags: { kinds, ...(flagBool(ctx, 'no-app-code') ? { 'no-app-code': true } : {}) } })).data as { pulled: string[]; warnings: string[] }
142
151
  for (const w of pull.warnings) ctx.output.note(` could not read ${w}`)
143
152
  let sampleRows = 0
144
153
  const wantsData = !flagBool(ctx, 'no-sample-data') && existsSync(join(target, 'blueprint', 'object-types'))
@@ -152,7 +161,11 @@ async function startFromWorkspace(ctx: CommandContext, target: string): Promise<
152
161
  }
153
162
  addMissingBackings(target)
154
163
  const scenario = await loadScenario(target, seed)
155
- sampleRows = generateSyntheticData(target, { rows: Number(flagString(ctx, 'rows') ?? 200), seed, scenario: scenario ?? undefined }).reduce((n, d) => n + d.rows, 0)
164
+ const rows = Number(flagString(ctx, 'rows') ?? 200)
165
+ // Today, kept with the seed and rows: a pull that brings Blueprint items makes the same rows again.
166
+ const today = isoDay(new Date())
167
+ sampleRows = generateSyntheticData(target, { rows, seed, today: new Date(`${today}T00:00:00Z`), scenario: scenario ?? undefined }).reduce((n, d) => n + (d.kept ? 0 : d.rows), 0)
168
+ writeSampleOptions(target, { seed, rows, today, madeBy: 'pull' })
156
169
  }
157
170
  const link = readLink(target)
158
171
  return { workspace: link?.workspaceName ?? link?.workspace ?? 'the workspace', pulled: pull.pulled.length, sampleRows }
@@ -38,7 +38,6 @@ export const localKeySet: Command = {
38
38
  summary: 'Store your model-provider key for the local runtime (OS keychain)',
39
39
  examples: ['frontera local key-set anthropic', 'printf %s "$KEY" | frontera local key-set openrouter --stdin'],
40
40
  offline: true,
41
- internal: true,
42
41
  },
43
42
  async run(ctx) {
44
43
  const provider = providerArg(ctx.positional[0])
@@ -75,7 +74,6 @@ export const localKeyList: Command = {
75
74
  summary: 'List which providers have a key for the local runtime',
76
75
  examples: ['frontera local key-list'],
77
76
  offline: true,
78
- internal: true,
79
77
  },
80
78
  async run() {
81
79
  const configured = new Set(configuredProviders())
@@ -96,7 +94,6 @@ export const localKeyRemove: Command = {
96
94
  summary: 'Remove a stored model-provider key',
97
95
  examples: ['frontera local key-remove openai'],
98
96
  offline: true,
99
- internal: true,
100
97
  },
101
98
  async run(ctx) {
102
99
  const provider = providerArg(ctx.positional[0])
@@ -64,7 +64,6 @@ export const localProviders: Command = {
64
64
  summary: 'List the model providers, models and default models of your local runtimes',
65
65
  examples: ['frontera local providers', 'frontera local providers --json'],
66
66
  offline: true,
67
- internal: true,
68
67
  },
69
68
  async run(ctx) {
70
69
  const setup = readProviderSetup()
@@ -102,7 +101,6 @@ export const localProviderAdd: Command = {
102
101
  summary: 'Add an OpenAI-compatible model provider (a gateway, Azure, a model served on this machine)',
103
102
  examples: ['frontera local provider-add ollama --base-url http://127.0.0.1:11434/v1', 'frontera local provider-add acme --base-url https://acme.example/v1 --stdin'],
104
103
  offline: true,
105
- internal: true,
106
104
  },
107
105
  async run(ctx) {
108
106
  const slug = ctx.positional[0] ?? ''
@@ -129,7 +127,6 @@ export const localProviderRemove: Command = {
129
127
  summary: 'Remove an OpenAI-compatible provider, its key and its models',
130
128
  examples: ['frontera local provider-remove acme-gateway'],
131
129
  offline: true,
132
- internal: true,
133
130
  },
134
131
  async run(ctx) {
135
132
  const slug = ctx.positional[0] ?? ''
@@ -158,7 +155,6 @@ export const localModelsAvailable: Command = {
158
155
  summary: "List the models a provider offers (needs the runtime running and the provider's key)",
159
156
  examples: ['frontera local models-available openrouter', 'frontera local models-available custom:acme-gateway --json'],
160
157
  offline: true,
161
- internal: true,
162
158
  },
163
159
  async run(ctx) {
164
160
  const provider = ctx.positional[0] ?? ''
@@ -179,7 +175,6 @@ export const localModelAdd: Command = {
179
175
  summary: 'Make a model available to Agents in your local runtimes',
180
176
  examples: ['frontera local model-add openrouter openai/gpt-6-luna --name "GPT-6 Luna"', 'frontera local model-add custom:acme-gateway llama-4-70b'],
181
177
  offline: true,
182
- internal: true,
183
178
  },
184
179
  async run(ctx) {
185
180
  const [provider = '', modelId = ''] = ctx.positional
@@ -202,7 +197,6 @@ export const localModelRemove: Command = {
202
197
  summary: 'Stop offering a model to Agents in your local runtimes',
203
198
  examples: ['frontera local model-remove gpt-6-luna'],
204
199
  offline: true,
205
- internal: true,
206
200
  },
207
201
  async run(ctx) {
208
202
  const slug = ctx.positional[0] ?? ''
@@ -226,7 +220,6 @@ export const localModelDefault: Command = {
226
220
  summary: 'Choose the model new Agents start with and the one that grades tests',
227
221
  examples: ['frontera local model-default claude-sonnet-5-5', 'frontera local model-default gpt-6-luna --for tests'],
228
222
  offline: true,
229
- internal: true,
230
223
  },
231
224
  async run(ctx) {
232
225
  const slug = ctx.positional[0] ?? ''
@@ -0,0 +1,116 @@
1
+ import { existsSync, readdirSync, rmSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { credentialStore } from '../../credential-store'
5
+ import { CliError } from '../../errors'
6
+ import { deleteProfile, getProfile } from '../../profiles'
7
+ import { canPrompt, promptConfirm } from '../../prompt'
8
+ import { destroyRuntimeData, localProfileName, stopRuntime } from '../../local/local-control'
9
+ import { dockerAvailable } from '../../local/local-docker'
10
+ import { portBlockOf, releasePorts } from '../../local/local-ports'
11
+ import { localProject, type LocalProject } from '../../local/local-project'
12
+ import { isAlive, readState } from '../../local/local-state'
13
+ import { stopDevServers } from '../../mcp/mcp-apps'
14
+ import { flagBool, type Command } from '../types'
15
+
16
+ /**
17
+ * `frontera local remove`: delete this project's local runtime permanently.
18
+ *
19
+ * `stop --no-backup` and `reset` wipe the data of a runtime the project goes
20
+ * on using; this is for a project that is done with its runtime. It stops it,
21
+ * deletes its containers and volumes (the database and stored files), frees
22
+ * its port block for another project, forgets its CLI profiles and their
23
+ * keys, and clears the runtime's records under `.frontera/local`. The
24
+ * project's own files are untouched, and `frontera local start` makes a new,
25
+ * empty runtime.
26
+ */
27
+
28
+ /** Left in place: written by Forge, which may still be attached to the project. */
29
+ const KEPT_STATE_FILES = new Set(['forge.json'])
30
+
31
+ function clearRuntimeFiles(project: LocalProject): void {
32
+ if (!existsSync(project.stateDir)) return
33
+ for (const name of readdirSync(project.stateDir)) {
34
+ if (KEPT_STATE_FILES.has(name)) continue
35
+ rmSync(join(project.stateDir, name), { recursive: true, force: true })
36
+ }
37
+ }
38
+
39
+ export const localRemove: Command = {
40
+ meta: {
41
+ noun: 'local',
42
+ verb: 'remove',
43
+ args: [],
44
+ flags: { force: 'boolean' },
45
+ summary: 'Delete the local runtime of this project and all its local data: database, stored files, keys, port block and CLI profiles',
46
+ details: [
47
+ 'The local data cannot be recovered afterwards; the project\'s own files are kept. '
48
+ + 'Asks first unless --yes. A running runtime is refused unless --force, which stops it first. '
49
+ + '`frontera local start` afterwards makes a new, empty runtime.',
50
+ ],
51
+ examples: ['frontera local remove', 'frontera local remove --yes', 'frontera local remove --force --yes'],
52
+ offline: true,
53
+ },
54
+
55
+ async run(ctx) {
56
+ const project = localProject(ctx.cwd)
57
+ const state = readState(project)
58
+ const profile = localProfileName(project)
59
+ const profiles = [profile, `${profile}-org`.slice(0, 64)].filter((name) => getProfile(name))
60
+ const block = portBlockOf(project.id)
61
+ if (!state && !block && !profiles.length) {
62
+ return { data: { removed: false }, text: 'No local runtime here.' }
63
+ }
64
+
65
+ const running = Boolean(state?.supervisorPid && isAlive(state.supervisorPid))
66
+ if (running && !flagBool(ctx, 'force')) {
67
+ throw new CliError('the local runtime is running', {
68
+ code: 'LOCAL_RUNNING',
69
+ hint: 'stop it with `frontera local stop` first, or pass --force to stop and remove it',
70
+ })
71
+ }
72
+
73
+ if (!flagBool(ctx, 'yes')) {
74
+ const confirmed = canPrompt()
75
+ ? await promptConfirm(`Delete the local runtime of ${project.root}, with its database, stored files and keys? This cannot be undone.`)
76
+ : false
77
+ if (!confirmed) {
78
+ throw new CliError('removing the local runtime deletes its local data', {
79
+ code: 'USAGE',
80
+ hint: 're-run with --yes to confirm — its database, stored files and keys cannot be recovered afterwards',
81
+ })
82
+ }
83
+ }
84
+
85
+ const docker = dockerAvailable()
86
+ if (!docker.ok) {
87
+ throw new CliError(`cannot reach Docker to delete the runtime's containers and volumes: ${docker.reason}`, {
88
+ code: 'LOCAL_DOCKER',
89
+ hint: 'start Docker Desktop or OrbStack, then run `frontera local remove` again',
90
+ })
91
+ }
92
+
93
+ // Its App dev servers first, while their records (under .frontera/local) are still there to find them by.
94
+ const apps = stopDevServers(project)
95
+ await ctx.output.task('Stopping the local runtime', () => stopRuntime(project))
96
+ destroyRuntimeData(project)
97
+ clearRuntimeFiles(project)
98
+ releasePorts(project.id)
99
+ const store = credentialStore(process.env, { allowPlaintext: true })
100
+ for (const name of profiles) {
101
+ await store.delete(name)
102
+ deleteProfile(name)
103
+ }
104
+
105
+ return {
106
+ data: { removed: true, runtime: project.id, root: project.root, portBlockFreed: Boolean(block), profilesRemoved: profiles, ...(apps.length ? { appDevServersStopped: apps.map((a) => ({ app: a.slug, port: a.port, pid: a.pid })) } : {}) },
107
+ text: [
108
+ `Removed the local runtime of ${project.root}: its database, stored files and keys are deleted.`,
109
+ block ? ` Freed its ports (${block.studio}–${block.studio + 9}).` : null,
110
+ profiles.length ? ` Forgot the CLI profiles ${profiles.join(', ')}.` : null,
111
+ apps.length ? ` Stopped its App dev server${apps.length === 1 ? '' : 's'}: ${apps.map((a) => `${a.slug} (port ${a.port})`).join(', ')}.` : null,
112
+ ' `frontera local start` makes a new, empty runtime.',
113
+ ].filter(Boolean).join('\n'),
114
+ }
115
+ },
116
+ }
@@ -16,10 +16,9 @@ export const localSchema: Command = {
16
16
  verb: 'schema',
17
17
  args: [{ name: 'kind', required: false, description: SCHEMA_KINDS.join(' | ') }],
18
18
  flags: {},
19
- summary: 'Print the JSON Schema of a project file (operating, agent, workflow, evals, dataset) for the runtime\'s release',
20
- examples: ['frontera local schema', 'frontera local schema operating', "frontera local schema workflow | jq '.\"$defs\" | keys'"],
19
+ summary: 'Print the JSON Schema of a project file (Agent, its bindings, workflow, tests, dataset, slide theme, Blueprint) for the runtime\'s release',
20
+ examples: ['frontera local schema', 'frontera local schema operating', 'frontera local schema workflow-tests', 'frontera local schema blueprint-action', 'frontera local schema bindings', "frontera local schema workflow | jq '.\"$defs\" | keys'"],
21
21
  offline: true,
22
- internal: true,
23
22
  },
24
23
  async run(ctx) {
25
24
  const kind = ctx.positional[0]
@@ -29,7 +28,7 @@ export const localSchema: Command = {
29
28
  data: kinds,
30
29
  text: [
31
30
  'Project file formats, as JSON Schema: `frontera local schema <kind>`.',
32
- ...kinds.map((k) => ` ${k.kind.padEnd(10)} ${k.file}`),
31
+ ...kinds.map((k) => ` ${k.kind.padEnd(24)} ${k.file}`),
33
32
  '`frontera local start` keeps them in .frontera/local/schemas/<kind>.schema.json.',
34
33
  ].join('\n'),
35
34
  }
@@ -0,0 +1,59 @@
1
+ import { UsageError } from '../../errors'
2
+ import { localProject } from '../../local/local-project'
3
+ import { ensureProjectBunfig } from '../../local/local-scaffold'
4
+ import { refreshVendoredSdk, vendoredSdkState } from '../../local/local-vendored-sdk'
5
+ import vendored from '../../vendor/sdk-sources.json'
6
+ import { flagBool, type Command } from '../types'
7
+
8
+ /**
9
+ * `frontera local sdk-refresh`: bring the project's vendored Functions SDK
10
+ * (`vendor/frontera/`, what its Function tests and typecheck use) to this
11
+ * CLI's copy. The files the CLI wrote and nobody changed are replaced (and
12
+ * `local dev` does that by itself); a file edited since is kept unless
13
+ * `--force`. A folder with no record of what wrote it has each differing
14
+ * file replaced and a copy kept. A copy known or likely to be newer than this
15
+ * CLI's (a newer CLI's manifest, or a pull from a newer platform release) is
16
+ * not replaced without `--force`.
17
+ */
18
+ export const localSdkRefresh: Command = {
19
+ meta: {
20
+ noun: 'local',
21
+ verb: 'sdk-refresh',
22
+ args: [],
23
+ flags: { force: 'boolean' },
24
+ summary: "Bring the project's vendored Functions SDK (vendor/frontera/) to this CLI's copy",
25
+ details: [
26
+ "The files the CLI wrote and nobody changed are replaced; `frontera local dev` does that on its own. A file edited since the CLI wrote it is kept unless --force. A vendor/frontera/ with no vendor/frontera/.frontera-sdk.json can't tell an edit apart: each file that differs is replaced. Every edited file replaced is copied to .frontera/local/sdk-backup/<time>/ first.",
27
+ "It refuses to go back to an older SDK without --force: when vendor/frontera/.frontera-sdk.json says a newer CLI wrote the copy, or, with no such file, when the project was pulled from a platform release newer than this CLI. Upgrade the CLI instead; --force replaces the copy anyway.",
28
+ ],
29
+ examples: ['frontera local sdk-refresh', 'frontera local sdk-refresh --force'],
30
+ offline: true,
31
+ },
32
+ async run(ctx) {
33
+ const project = localProject(ctx.cwd)
34
+ const before = vendoredSdkState(project.root)
35
+ if (!before.vendored) throw new UsageError('this project keeps no vendored SDK (vendor/frontera/)', '`frontera local init` writes one in a new project')
36
+ // A copy known or likely to be newer than this CLI's is not replaced by an older one unasked.
37
+ if (before.newer && !flagBool(ctx, 'force')) {
38
+ const why = before.newer.by === 'manifest'
39
+ ? `vendor/frontera/ was written by a newer CLI (SDK ${before.newer.version}) than this one (${vendored.sdkVersion})`
40
+ : `the project was pulled from platform ${before.newer.version}, newer than this CLI's SDK (${vendored.sdkVersion}), and nothing records which SDK wrote vendor/frontera/`
41
+ throw new UsageError(`${why}: refreshing would replace it with an older copy, so nothing was changed`, `upgrade the Frontera CLI to ${before.newer.version} or later, then run this again; \`frontera local sdk-refresh --force\` replaces it with this CLI's copy anyway (a copy of each replaced file is kept under .frontera/local/sdk-backup/)`)
42
+ }
43
+ const done = refreshVendoredSdk(project.root, { force: flagBool(ctx, 'force'), backupAll: true })
44
+ // Written only when the project has none: `bun test` at its root runs the Functions' tests alone.
45
+ const bunfig = ensureProjectBunfig(project.root)
46
+ const changed = [...done.replaced, ...done.added]
47
+ return {
48
+ data: { ...done, version: vendored.sdkVersion, ...(bunfig ? { bunfig: 'bunfig.toml' } : {}) },
49
+ text: [
50
+ changed.length ? `Brought vendor/frontera/ to this CLI's SDK: ${changed.length} file${changed.length === 1 ? '' : 's'}` : 'vendor/frontera/ already matches this CLI\'s SDK',
51
+ ...done.replaced.map((f) => ` ~ ${f}`),
52
+ ...done.added.map((f) => ` + ${f}`),
53
+ ...done.kept.map((f) => ` · ${f} (edited since the CLI wrote it; --force replaces it)`),
54
+ ...(done.backup ? [` the files replaced are copied to ${done.backup}/`] : []),
55
+ ...(bunfig ? ["bunfig.toml: written, so `bun test` here runs only the Functions' unit tests (functions/)"] : []),
56
+ ].join('\n'),
57
+ }
58
+ },
59
+ }
@@ -0,0 +1,100 @@
1
+ import { CliError, UsageError } from '../../errors'
2
+ import { appsLostWords } from '../../local/local-app-restore'
3
+ import { BASELINE_DB, readBaseline, restoreBaseline, saveBaseline, secretsWords } from '../../local/local-db-snapshot'
4
+ import { localIngestApi, settleKnowledge } from '../../local/local-knowledge-ingestion'
5
+ import { localProject } from '../../local/local-project'
6
+ import { projectSecretLines } from '../../local/local-project-secrets'
7
+ import { waitForDatabaseReady } from '../../local/local-runtime-health'
8
+ import { localSecretPresence } from '../../local/local-secrets'
9
+ import { isAlive, readState } from '../../local/local-state'
10
+ import { flagBool, type Command } from '../types'
11
+
12
+ /**
13
+ * `frontera local snapshot save|restore`: the runtime's database as a
14
+ * baseline, and back. `local check` does both around its tests; this is the
15
+ * same, by hand: save after setting up data to come back to, restore to throw
16
+ * away what was written since. The catalog of managed datasets, object
17
+ * storage and local traces are not part of it. Workspace secrets are
18
+ * configuration, not data: a restore keeps them as they are now, unless
19
+ * `--exact` puts the baseline's back too.
20
+ */
21
+
22
+ const ACTIONS = ['save', 'restore'] as const
23
+
24
+ export const localSnapshot: Command = {
25
+ meta: {
26
+ noun: 'local',
27
+ verb: 'snapshot',
28
+ args: [{ name: 'action', required: true, description: ACTIONS.join(' | ') }],
29
+ flags: { exact: 'boolean' },
30
+ summary: 'Save the local database as a baseline, or restore it, throwing away what was written since (local check does this around its tests)',
31
+ details: [
32
+ 'save keeps the database as it is now as the baseline, replacing the one before. restore puts the database back as the baseline holds it; the service, job engine and runner keep running and reconnect, and the command returns once the service reaches the database again (it waits up to 30 seconds).',
33
+ 'Workspace secrets are configuration, not data: restore keeps them as they are now (set, changed or deleted since the baseline), and `local check` keeps them across its rollback too. `--exact` puts the baseline\'s secrets back as well.',
34
+ 'Items applied after the baseline count as not applied once it is restored: the next `frontera local dev` applies them again (a running one does so at once).',
35
+ 'Knowledge documents are read by a background job. save waits (up to two minutes) until no document is still being read, so the baseline never holds one half-read; restore queues any document the baseline holds unread to be read again, and waits for it.',
36
+ ],
37
+ examples: ['frontera local snapshot save', 'frontera local snapshot restore', 'frontera local snapshot restore --exact'],
38
+ offline: true,
39
+ },
40
+ async run(ctx) {
41
+ const action = ctx.positional[0]
42
+ if (!action || !(ACTIONS as readonly string[]).includes(action)) {
43
+ throw new UsageError(action ? `\`${action}\` is not a snapshot action` : 'say what to do', `use one of: ${ACTIONS.join(', ')} (\`frontera local snapshot save\`)`)
44
+ }
45
+ const project = localProject(ctx.cwd)
46
+ const state = readState(project)
47
+ if (!state?.credentials || state.phase !== 'ready' || !state.supervisorPid || !isAlive(state.supervisorPid)) {
48
+ throw new CliError('the local runtime is not running', { code: 'LOCAL_NOT_RUNNING', hint: 'run `frontera local start` first' })
49
+ }
50
+ const organizationId = state.credentials.organizationId
51
+ if (action === 'save' && flagBool(ctx, 'exact')) throw new UsageError('`--exact` is for restore', 'frontera local snapshot restore --exact')
52
+ const serviceUrl = `http://127.0.0.1:${state.ports.service}`
53
+ const ingest = localIngestApi(serviceUrl, state.credentials)
54
+ if (action === 'save') {
55
+ // A document mid-read would be kept unread, its reading lost on a restore: the save waits (bounded) for it.
56
+ const settled = ingest ? await settleKnowledge(ingest, state.credentials.workspaceId, { restoredAt: readBaseline(project, organizationId)?.restoredAt }) : null
57
+ const saved = await ctx.output.task('Saving the database as the baseline', () => saveBaseline(project, organizationId, { serviceAnswers: () => waitForDatabaseReady(`http://127.0.0.1:${state.ports.service}`) }, true))
58
+ if (!saved.ok) throw new CliError(saved.note, { code: 'LOCAL_SNAPSHOT_FAILED', hint: 'see `frontera local logs postgres`' })
59
+ const knowledge = settled?.lines ?? []
60
+ return {
61
+ data: { action, baseline: BASELINE_DB, ms: saved.ms, ...(knowledge.length ? { knowledge: knowledge.map((l) => l.replace(/^warning:\s*/, '')) } : {}) },
62
+ text: [`Saved the database as the baseline (${saved.ms} ms). \`frontera local snapshot restore\` puts it back.`, ...knowledge.map((l) => ` ${l}`)].join('\n'),
63
+ }
64
+ }
65
+ const exact = flagBool(ctx, 'exact')
66
+ const restored = await ctx.output.task('Restoring the database from the baseline', () =>
67
+ restoreBaseline(project, organizationId, { serviceAnswers: () => waitForDatabaseReady(`http://127.0.0.1:${state.ports.service}`) }, { secrets: exact ? 'baseline' : 'keep' }),
68
+ )
69
+ if (!restored.ok) throw new CliError(restored.note, { code: 'LOCAL_SNAPSHOT_FAILED', hint: 'see `frontera local logs postgres` and `frontera local logs service`' })
70
+ // The documents the baseline holds unread are read again: their reading went with the database the restore replaced.
71
+ const knowledge = ingest ? (await settleKnowledge(ingest, state.credentials.workspaceId, { afterRestore: true })).lines : []
72
+ // A secret the project needs that has no value since the restore: said now, not when its trigger refuses.
73
+ const secrets = projectSecretLines(project.root, await localSecretPresence(`http://127.0.0.1:${state.ports.service}`, state.credentials))
74
+ .map((l) => l.trim().replace(/^warning:\s*/, ''))
75
+ .filter((l) => /^Needs a value for /.test(l))
76
+ const lost = restored.appsLost ?? []
77
+ const items = (restored.reapply ?? 0) - lost.length
78
+ return {
79
+ data: {
80
+ action,
81
+ baseline: BASELINE_DB,
82
+ ms: restored.ms,
83
+ reapply: restored.reapply ?? 0,
84
+ // Custom Apps deployed after the baseline: not on the runtime now, deployed again by the next `local dev`.
85
+ ...(lost.length ? { appsLost: lost.map((a) => ({ app: a.name, dir: a.dir, deployment: a.lost, baselineDeployment: a.back })) } : {}),
86
+ // Names and counts only: what became of the workspace secrets.
87
+ workspaceSecrets: restored.secrets?.mode === 'keep' ? { kept: restored.secrets.ok, count: restored.secrets.kept, setAgain: restored.secrets.notKept, ...(restored.secrets.problem ? { problem: restored.secrets.problem } : {}) } : { kept: false, exact: exact },
88
+ ...(secrets.length ? { secrets } : {}),
89
+ ...(knowledge.length ? { knowledge: knowledge.map((l) => l.replace(/^warning:\s*/, '')) } : {}),
90
+ },
91
+ text: [
92
+ `Restored the database from the baseline: what was written since is gone (${restored.ms} ms).${items ? ` ${items} item${items === 1 ? '' : 's'} applied after the baseline will apply again on the next \`frontera local dev\`.` : ''}`,
93
+ ...(lost.length ? [` ${appsLostWords(lost).replace(/^./, (c) => c.toUpperCase())}.`] : []),
94
+ ...(restored.secrets ? [` ${secretsWords(restored.secrets).replace(/^./, (c) => c.toUpperCase())}${restored.secrets.mode === 'keep' && restored.secrets.ok && !restored.secrets.notKept.length ? ': secrets are configuration, not test data (`--exact` restores the baseline’s).' : '.'}`] : []),
95
+ ...secrets.map((l) => ` warning: ${l}`),
96
+ ...knowledge.map((l) => ` ${l}`),
97
+ ].join('\n'),
98
+ }
99
+ },
100
+ }