@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.
- package/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +49 -5
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/draft-changes.ts +1 -1
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +332 -32
- package/src/commands/blueprint/editable.ts +3 -3
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +876 -89
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +321 -14
- package/src/commands/local/sync.ts +103 -6
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +54 -10
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/claude-commands.ts +11 -6
- package/src/forge/claude-skills.ts +4 -4
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +113 -2
- package/src/kit.ts +1 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +212 -22
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +181 -8
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +347 -36
- package/src/local/local-dev-sync.ts +141 -12
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-documents.ts +1 -1
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-lessons.ts +1 -1
- package/src/local/local-library.ts +1 -1
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +206 -8
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +143 -18
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +41 -11
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +3 -3
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-merge.ts +1 -1
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +19 -8
- package/src/local/local-sync-push.ts +15 -12
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +349 -38
- package/src/local/local-sync-snapshot.ts +18 -13
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/scopes.ts +1 -1
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +17 -14
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
+
}
|