@frontera-sdk/cli 1.52.38 → 1.52.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +48 -4
- 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/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 +331 -31
- 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 +875 -88
- 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 +320 -13
- package/src/commands/local/sync.ts +102 -5
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +52 -8
- 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/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +112 -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 +211 -21
- 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 +180 -7
- 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 +346 -35
- package/src/local/local-dev-sync.ts +136 -7
- 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-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- 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 +205 -7
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +141 -16
- 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 +38 -8
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +1 -1
- 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-names.ts +2 -0
- package/src/local/local-sync-plan.ts +11 -0
- package/src/local/local-sync-push.ts +10 -7
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +343 -32
- package/src/local/local-sync-snapshot.ts +15 -10
- 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/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 +24 -21
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { docsCopyPath, serveDocsMcp } from '../../local/local-docs'
|
|
2
1
|
import { localProject } from '../../local/local-project'
|
|
3
|
-
import {
|
|
2
|
+
import { serveMcp } from '../../mcp/mcp-protocol'
|
|
3
|
+
import { MCP_INFO, mcpTools } from '../../mcp/mcp-server'
|
|
4
4
|
import type { Command } from '../types'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the formats of the project's files that start kept, so it needs no key and
|
|
12
|
-
* keeps working while the runtime is stopped. Nothing but protocol goes to
|
|
13
|
-
* stdout.
|
|
7
|
+
* The earlier name of `frontera mcp`, kept for one release so projects
|
|
8
|
+
* registered with `frontera-docs` keep their documentation tools. It serves
|
|
9
|
+
* the same server; Forge's Set up agents and `local init` now register
|
|
10
|
+
* `frontera mcp` instead.
|
|
14
11
|
*/
|
|
15
12
|
export const localDocsMcp: Command = {
|
|
16
13
|
meta: {
|
|
@@ -18,19 +15,14 @@ export const localDocsMcp: Command = {
|
|
|
18
15
|
verb: 'docs-mcp',
|
|
19
16
|
args: [],
|
|
20
17
|
flags: {},
|
|
21
|
-
summary:
|
|
22
|
-
examples: ['frontera
|
|
18
|
+
summary: 'Deprecated: the same server as `frontera mcp`',
|
|
19
|
+
examples: ['frontera mcp'],
|
|
23
20
|
offline: true,
|
|
24
21
|
internal: true,
|
|
25
22
|
},
|
|
26
23
|
async run(ctx) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await serveDocsMcp({
|
|
30
|
-
docsPath: docsCopyPath(project),
|
|
31
|
-
schemaFor: (kind) => cliSchema(kind) ?? readKeptSchema(project, kind),
|
|
32
|
-
})
|
|
33
|
-
// The stream is the protocol: exit here, before the CLI renders a result onto it.
|
|
24
|
+
process.stderr.write('frontera local docs-mcp is deprecated: register `frontera mcp` instead (Forge: Set up agents).\n')
|
|
25
|
+
await serveMcp({ info: MCP_INFO, tools: mcpTools(localProject(ctx.cwd)) })
|
|
34
26
|
process.exit(0)
|
|
35
27
|
},
|
|
36
28
|
}
|
|
@@ -3,12 +3,23 @@ import { existsSync, readFileSync, rmSync } from 'node:fs'
|
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { CliError, UsageError } from '../../errors'
|
|
6
|
+
import { EXIT } from '../../exit'
|
|
6
7
|
import { writeFileAtomic } from '../../fs-atomic'
|
|
7
8
|
import { solutionStatus } from '../../local/local-applied'
|
|
9
|
+
import type { LiveAction } from '../../local/local-actions'
|
|
10
|
+
import type { LiveApp } from '../../local/local-app-runtime'
|
|
11
|
+
import { runtimeBuildInputs } from '../../local/local-build-inputs'
|
|
12
|
+
import { listPersonas, type LocalPersona } from '../../local/local-personas'
|
|
13
|
+
import type { SecretPresence } from '../../local/local-secrets'
|
|
14
|
+
import { findObjectSet, normalizeObjectSets, projectObjectSetNames } from '../../blueprint/object-sets'
|
|
15
|
+
import { actionsStatusText, appsStatusText, buildStatusText, emptyKnowledgeLines, itemKindsText, notPulledLines, objectSetsStatusText, projectBuildRows, projectBuildSummary, projectFilesLines } from '../../local/local-build-summary'
|
|
16
|
+
import { inheritedActionWarnings } from '../../local/local-check-inherited'
|
|
17
|
+
import { checkoutVersion, docsStatus, versionAtStart } from '../../local/local-docs'
|
|
8
18
|
import { dockerAvailable } from '../../local/local-docker'
|
|
19
|
+
import { downPhase, downToRestart, liveComponents, type DownComponent } from '../../local/local-component-health'
|
|
9
20
|
import { readConfig, writeConfig, type LocalConfig } from '../../local/local-env'
|
|
10
21
|
import { configuredProviders } from '../../local/local-keys'
|
|
11
|
-
import { claimPorts, isPortFree, portHolder, releasePorts } from '../../local/local-ports'
|
|
22
|
+
import { claimPortBlock, claimPorts, isPortFree, portHolder, reclaimedNote, releasePorts } from '../../local/local-ports'
|
|
12
23
|
import { localProject, type LocalProject } from '../../local/local-project'
|
|
13
24
|
import {
|
|
14
25
|
destroyRuntimeData,
|
|
@@ -22,21 +33,35 @@ import {
|
|
|
22
33
|
} from '../../local/local-control'
|
|
23
34
|
import { clearState, ensureStateDir, isAlive, logPath, readState, type ComponentName, type LocalState } from '../../local/local-state'
|
|
24
35
|
import { newSessionToken } from '../../local/local-studio-server'
|
|
36
|
+
import { migrateMcpConfigs } from '../../local/local-scaffold'
|
|
25
37
|
import { DEFAULT_REGISTRY, runSupervisor } from '../../local/local-supervisor'
|
|
26
|
-
import {
|
|
38
|
+
import { compareWithLastRun, readLastRun, recordRun } from '../../local/local-test-baseline'
|
|
39
|
+
import { agentFilesProblem, caseConcurrency, caseCount, runCases, selectCaseFiles, waitedWords, type CaseFile, type CaseResult } from '../../local/local-test-runner'
|
|
40
|
+
import { devServerLine, listDevServers, stopDevServers } from '../../mcp/mcp-apps'
|
|
41
|
+
import { caseTraces, evidenceLine, failingCaseEvidence } from '../../local/local-case-evidence'
|
|
42
|
+
import { notRunGroups, notRunWords } from '../../local/local-case-not-run'
|
|
43
|
+
import { findWorkflowTestFiles, recordWorkflowTestRun, runWorkflowTests, workflowFileState, type WorkflowTestFile, type WorkflowTestResult } from '../../local/local-workflow-tests'
|
|
27
44
|
import { flagBool, flagString, type Command, type CommandContext } from '../types'
|
|
28
45
|
import { localCheck } from './check'
|
|
46
|
+
import { projectNotices } from '../../local/local-project-notices'
|
|
47
|
+
import { readBaseline } from '../../local/local-db-snapshot'
|
|
48
|
+
import { stuckKnowledge } from '../../local/local-knowledge-ingestion'
|
|
49
|
+
import { orphanLine, projectOrphans } from '../../local/local-orphans'
|
|
29
50
|
import { localData } from './data'
|
|
51
|
+
import { localSdkRefresh } from './sdk-refresh'
|
|
30
52
|
import { localDev } from './dev'
|
|
31
53
|
import { localDocsMcp } from './docs-mcp'
|
|
32
54
|
import { localInit } from './init'
|
|
33
55
|
import { localKeyList, localKeyRemove, localKeySet } from './key'
|
|
34
56
|
import { localModelAdd, localModelDefault, localModelRemove, localModelsAvailable, localProviderAdd, localProviderRemove, localProviders } from './providers'
|
|
57
|
+
import { localRemove } from './remove'
|
|
35
58
|
import { localSchema } from './schema'
|
|
36
59
|
import { localSync } from './sync'
|
|
60
|
+
import { keylessEmbeddingsStatus, KEYLESS_RETRIEVAL_NOTE } from '../../local/local-org-settings'
|
|
61
|
+
import { withFix } from '../../local/fix-words'
|
|
37
62
|
|
|
38
63
|
/**
|
|
39
|
-
* `frontera local …`: Frontera's runtimes on this machine, for
|
|
64
|
+
* `frontera local …`: Frontera's runtimes on this machine, for FDEs and the coding agents working with them.
|
|
40
65
|
*
|
|
41
66
|
* Modelled on `supabase start`: one command brings up only the runtimes a
|
|
42
67
|
* solution needs (no product UI, no kernel), seeds an owner, organization,
|
|
@@ -49,7 +74,7 @@ const OPTIONAL: ComponentName[] = ['jobs', 'runner', 'storage']
|
|
|
49
74
|
|
|
50
75
|
const sessionFile = (project: LocalProject) => join(project.stateDir, 'studio-session')
|
|
51
76
|
|
|
52
|
-
function readSession(project: LocalProject): string | null {
|
|
77
|
+
export function readSession(project: LocalProject): string | null {
|
|
53
78
|
const path = sessionFile(project)
|
|
54
79
|
return existsSync(path) ? readFileSync(path, 'utf8').trim() : null
|
|
55
80
|
}
|
|
@@ -68,13 +93,15 @@ function studioUrl(state: LocalState, project: LocalProject): string {
|
|
|
68
93
|
return `http://127.0.0.1:${state.ports.studio}/${token ? `?token=${token}` : ''}`
|
|
69
94
|
}
|
|
70
95
|
|
|
71
|
-
const DOT: Record<string, string> = { running: '●', starting: '◐', off: '○', stopped: '○', failed: '✕' }
|
|
96
|
+
const DOT: Record<string, string> = { running: '●', starting: '◐', off: '○', stopped: '○', failed: '✕', down: '✕' }
|
|
72
97
|
|
|
73
|
-
function renderStatus(state: LocalState, project: LocalProject, profile?: { profile: string; bound: boolean }): string {
|
|
98
|
+
function renderStatus(state: LocalState, project: LocalProject, profile?: { profile: string; bound: boolean }, live: ReadonlyMap<string, LiveAction> | null = null, setIds: Record<string, string> | null = null, personas: LocalPersona[] | null = null, secrets: SecretPresence | null = null, liveApps: ReadonlyMap<string, LiveApp> | null = null, down: DownComponent[] = []): string {
|
|
74
99
|
const lines: string[] = []
|
|
75
|
-
|
|
100
|
+
// What is down now (`liveComponents`), not what the state file said at start: a container can exit under the supervisor.
|
|
101
|
+
const phase = state.supervisorPid && !isAlive(state.supervisorPid) && state.phase !== 'stopped' ? 'stopped (supervisor exited)' : down.length ? downPhase(down) : state.phase
|
|
76
102
|
lines.push(`Frontera local runtime · ${project.root.split('/').pop()} · ${phase}`)
|
|
77
103
|
if (state.error) lines.push(` ${state.error}`)
|
|
104
|
+
if (down.length) lines.push(' Reads and writes fail until it is back: `frontera local start` restarts what is down and keeps the data')
|
|
78
105
|
lines.push('')
|
|
79
106
|
if (state.phase === 'ready') {
|
|
80
107
|
lines.push(` Studio ${studioUrl(state, project)}`)
|
|
@@ -88,8 +115,11 @@ function renderStatus(state: LocalState, project: LocalProject, profile?: { prof
|
|
|
88
115
|
}
|
|
89
116
|
for (const c of state.components) {
|
|
90
117
|
const where = c.kind === 'container' ? 'container' : c.kind === 'process' ? 'process' : 'supervisor'
|
|
91
|
-
|
|
118
|
+
const status = down.some((d) => d.name === c.name) ? 'down' : c.status
|
|
119
|
+
lines.push(` ${DOT[status] ?? '·'} ${c.name.padEnd(9)} ${status.padEnd(9)} ${c.status === 'off' ? (c.detail ?? '') : where}${c.status === 'failed' && c.detail ? ` — ${c.detail}` : ''}`)
|
|
92
120
|
}
|
|
121
|
+
const cache = down.find((d) => d.name === 'cache')
|
|
122
|
+
if (cache) lines.push(` ✕ ${'cache'.padEnd(9)} ${'down'.padEnd(9)} container — ${cache.why}; \`frontera local start\` brings it back`)
|
|
93
123
|
if (state.credentials) {
|
|
94
124
|
lines.push('')
|
|
95
125
|
lines.push(` Signed in as ${state.credentials.email}`)
|
|
@@ -99,21 +129,64 @@ function renderStatus(state: LocalState, project: LocalProject, profile?: { prof
|
|
|
99
129
|
? ` CLI profile ${name} — this directory is bound to another profile; pass \`--profile ${name}\``
|
|
100
130
|
: ` CLI profile ${name} (this directory uses it)`,
|
|
101
131
|
)
|
|
102
|
-
lines.push(
|
|
132
|
+
lines.push(
|
|
133
|
+
profile?.bound === false
|
|
134
|
+
? ` Org profile ${name}-org — pass \`--profile ${name}-org\` to Blueprint and Action commands`
|
|
135
|
+
: ` Org profile ${name}-org (Blueprint and Action commands here use it)`,
|
|
136
|
+
)
|
|
103
137
|
const files = solutionStatus(project, state.credentials.organizationId)
|
|
104
|
-
if (files.length)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
138
|
+
if (files.length) lines.push(...projectFilesLines(files))
|
|
139
|
+
// Counted over the same rows it lists: the problems by name, under the line.
|
|
140
|
+
const build = projectBuildSummary(project.root, project.id, live, secrets, liveApps)
|
|
141
|
+
if (build) {
|
|
142
|
+
lines.push(` Build ${buildStatusText(build)}`)
|
|
143
|
+
// Each setting that differs from the workspace: what it changes when testing here, and whether it can be aligned.
|
|
144
|
+
for (const d of build.settingDetails) lines.push(` Setting ${d.line}${d.effect ? `: ${d.effect}` : ''}${d.align ? ` (${d.align.charAt(0).toLowerCase()}${d.align.slice(1)})` : ''}`)
|
|
145
|
+
const rows = projectBuildRows(project.root, project.id, live, setIds, secrets, liveApps) ?? []
|
|
146
|
+
for (const row of rows.filter((r) => r.state === 'failed' || r.state === 'blocked').slice(0, 12)) {
|
|
147
|
+
lines.push(` ✕ ${row.ref} ${row.state}${row.reason ? `: ${row.reason}` : ''}`)
|
|
148
|
+
if (row.fix) lines.push(` fix: ${row.fix}`)
|
|
149
|
+
}
|
|
150
|
+
// Applied, but naming a secret this runtime has no value for: it fails when used.
|
|
151
|
+
for (const row of rows.filter((r) => r.state === 'needs-setup').slice(0, 12)) lines.push(` ! ${row.ref} needs setup${row.reason ? `: ${row.reason}` : ''}`)
|
|
152
|
+
// Each kind the build counts, so the line above adds up: `--json` lists every row.
|
|
153
|
+
lines.push(` Items ${itemKindsText(rows)}`)
|
|
154
|
+
lines.push(` Actions ${actionsStatusText(rows)}`)
|
|
155
|
+
const sets = objectSetsStatusText(rows)
|
|
156
|
+
if (sets) lines.push(` Object sets ${sets}`)
|
|
157
|
+
if (personas?.length) lines.push(` Personas ${personas.map((p) => `${p.key} (${p.displayName})`).join(', ')}: act as one with \`--as <key>\` on action available, submit, approve and reject`)
|
|
158
|
+
const apps = appsStatusText(rows)
|
|
159
|
+
if (apps) lines.push(` Apps ${apps}`)
|
|
160
|
+
// A knowledge base an Agent finds nothing in, and what the workspace has that was never pulled: each with its command.
|
|
161
|
+
for (const k of emptyKnowledgeLines(rows)) lines.push(` Knowledge ${k}`)
|
|
162
|
+
for (const n of notPulledLines(project.root)) lines.push(` Not pulled ${n}`)
|
|
163
|
+
// What the pull couldn't bring and why, App code on the platform, older sample data or SDK: each with its command.
|
|
164
|
+
const notices = projectNotices(project.root)
|
|
165
|
+
for (const n of [...notices.pull, ...(notices.agentNotes ?? [])]) lines.push(` Pull note ${n}`)
|
|
166
|
+
for (const n of notices.apps) lines.push(` App code ${n}`)
|
|
167
|
+
if (notices.sampleData) lines.push(` Sample data ${notices.sampleData}`)
|
|
168
|
+
if (notices.sdk) lines.push(` SDK ${notices.sdk}`)
|
|
169
|
+
for (const n of notices.agents) lines.push(` Agent tools ${n}`)
|
|
170
|
+
// An Agent whose bound Functions or workflows failed to apply: it runs without them here.
|
|
171
|
+
for (const row of rows) for (const w of row.warnings ?? []) if (/^agent\/[\w-]+: missing tools:/.test(w)) lines.push(` Agent tools ${w}`)
|
|
172
|
+
// A governed Action deployed as the workspace runs it, with what its owner fixes there.
|
|
173
|
+
for (const w of inheritedActionWarnings(rows)) lines.push(` Action ${w}`)
|
|
174
|
+
for (const n of notices.workflows) lines.push(` Workflow ${n}`)
|
|
111
175
|
}
|
|
112
176
|
}
|
|
177
|
+
const docs = docsStatus(project, { mode: state.mode, platformVersion: state.platformVersion, platformSource: readConfig(project)?.platformSource })
|
|
178
|
+
lines.push(` Docs ${docs.present ? `${docs.version ?? 'unknown version'}${docs.builtAt ? `, built ${docs.builtAt.slice(0, 10)}` : ''}${docs.note ? ` — ${docs.note}` : ''}` : docs.note}`)
|
|
113
179
|
const providers = configuredProviders()
|
|
114
180
|
lines.push(` Model keys ${providers.length ? providers.join(', ') : 'none — run `frontera local key-set <provider>`'}`)
|
|
115
181
|
if (state.credentials?.modelProviders?.includes('local-embeddings')) {
|
|
116
|
-
|
|
182
|
+
// With an OpenRouter key stored, the local embeddings mean the provider refused it for embeddings at start.
|
|
183
|
+
lines.push(
|
|
184
|
+
providers.includes('openrouter')
|
|
185
|
+
? ' Embeddings local: words hashed into vectors, because OpenRouter refused this computer’s key for its embedding model at start (`frontera local logs service` shows its answer after `[embedding] preflight failed`); `frontera local key-set openrouter` with a key it takes replaces them'
|
|
186
|
+
: ' Embeddings local: words hashed into vectors, no key (an OpenAI or OpenRouter key replaces it)',
|
|
187
|
+
)
|
|
188
|
+
// What that does to knowledge search, and what gives the platform's: said where an agent reads the runtime first.
|
|
189
|
+
lines.push(` Knowledge ${KEYLESS_RETRIEVAL_NOTE.charAt(0).toUpperCase()}${KEYLESS_RETRIEVAL_NOTE.slice(1)}`)
|
|
117
190
|
}
|
|
118
191
|
return lines.join('\n')
|
|
119
192
|
}
|
|
@@ -141,11 +214,26 @@ function parseList(raw: string | undefined): ComponentName[] {
|
|
|
141
214
|
|
|
142
215
|
async function start(ctx: CommandContext): Promise<{ data: unknown; text: string }> {
|
|
143
216
|
const project = localProject(ctx.cwd)
|
|
217
|
+
// Coding-agent configs from before `frontera mcp` launched the documentation server alone.
|
|
218
|
+
for (const file of migrateMcpConfigs(project.root)) ctx.output.note(` ${file}: now launches \`frontera mcp\` (was \`frontera local docs-mcp\`)`)
|
|
144
219
|
const existing = readState(project)
|
|
145
|
-
|
|
220
|
+
// Up, or partly up: what is down (a container that exited under it, a process that crashed) is brought back.
|
|
221
|
+
const restart = existing ? await downToRestart(existing) : null
|
|
222
|
+
if (existing && !restart && existing.supervisorPid && isAlive(existing.supervisorPid) && existing.phase !== 'stopped') {
|
|
146
223
|
const profile = existing.credentials ? await registerLocalProfile(project, existing) : undefined
|
|
147
224
|
return { data: existing, text: `Already running.\n\n${renderStatus(existing, project, profile)}` }
|
|
148
225
|
}
|
|
226
|
+
if (restart?.length) {
|
|
227
|
+
ctx.output.note(` ${restart.map((d) => `${d.name} is down (${d.why})`).join('; ')}: restarting the runtime; its data is kept`)
|
|
228
|
+
await ctx.output.task('Stopping what is left of it', () => stopRuntime(project))
|
|
229
|
+
} else if (existing?.supervisorPid && !isAlive(existing.supervisorPid) && existing.phase !== 'stopped') {
|
|
230
|
+
// The supervisor went away and left its containers up: they are stopped (not removed), and started again below.
|
|
231
|
+
try {
|
|
232
|
+
await stopRuntime(project)
|
|
233
|
+
} catch {
|
|
234
|
+
// No container runtime: the check below says so.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
149
237
|
|
|
150
238
|
const docker = dockerAvailable()
|
|
151
239
|
if (!docker.ok) {
|
|
@@ -156,24 +244,28 @@ async function start(ctx: CommandContext): Promise<{ data: unknown; text: string
|
|
|
156
244
|
}
|
|
157
245
|
|
|
158
246
|
const sourceFlag = flagString(ctx, 'source')
|
|
159
|
-
const
|
|
160
|
-
|
|
247
|
+
const previous = readConfig(project)
|
|
248
|
+
// A restart of what went down, with no flags, runs as it ran: the same release image or checkout.
|
|
249
|
+
const keep = !!restart?.length && !sourceFlag && !flagBool(ctx, 'image') && !!previous
|
|
250
|
+
const useImage = flagBool(ctx, 'image') || (keep && previous!.mode === 'image')
|
|
251
|
+
const source = sourceFlag ? resolve(ctx.cwd, sourceFlag) : useImage ? null : ((keep ? previous!.platformSource : undefined) ?? process.env.FRONTERA_PLATFORM_SOURCE ?? detectPlatformSource())
|
|
161
252
|
if (sourceFlag && !isPlatformSource(source!)) {
|
|
162
253
|
throw new UsageError(`${source} is not a Frontera platform checkout`, 'point --source at the repository root that holds packages/service')
|
|
163
254
|
}
|
|
164
|
-
const exclude = parseList(flagString(ctx, 'exclude'))
|
|
165
|
-
const previous = readConfig(project)
|
|
255
|
+
const exclude = keep && !flagString(ctx, 'exclude') ? previous!.exclude : parseList(flagString(ctx, 'exclude'))
|
|
166
256
|
const config: LocalConfig = {
|
|
167
257
|
mode: source ? 'source' : 'image',
|
|
168
258
|
platformSource: source ?? undefined,
|
|
169
|
-
platformVersion: flagString(ctx, 'version')
|
|
259
|
+
platformVersion: versionAtStart({ checkout: source ? checkoutVersion(source) : null, flag: flagString(ctx, 'version'), previous, cli: cliVersion() }),
|
|
170
260
|
registry: normalizeRegistry(flagString(ctx, 'registry') ?? process.env.FRONTERA_LOCAL_REGISTRY ?? previous?.registry),
|
|
171
261
|
exclude,
|
|
172
262
|
}
|
|
173
263
|
ensureStateDir(project)
|
|
174
264
|
writeConfig(project, config)
|
|
175
265
|
|
|
176
|
-
const ports =
|
|
266
|
+
const { ports, reclaimed } = claimPortBlock(project.id, project.root)
|
|
267
|
+
const freed = reclaimedNote(reclaimed)
|
|
268
|
+
if (freed) ctx.output.note(freed)
|
|
177
269
|
for (const name of [...COMPONENTS, 'cache', 'embeddings', 'traces'] as const) {
|
|
178
270
|
if (exclude.includes(name as ComponentName)) continue
|
|
179
271
|
if (!(await isPortFree(ports[name]))) {
|
|
@@ -223,7 +315,6 @@ const startMeta = {
|
|
|
223
315
|
summary: 'Start Frontera\'s runtimes on this machine and seed an owner, workspace and keys',
|
|
224
316
|
examples: ['frontera local start', 'frontera local start --exclude runner', 'frontera local start --image --version 1.52.19', 'frontera local start --image --registry registry.example.com/frontera'],
|
|
225
317
|
offline: true,
|
|
226
|
-
internal: true,
|
|
227
318
|
} as const
|
|
228
319
|
|
|
229
320
|
export const localStart: Command = { meta: { noun: 'local', verb: 'start', ...startMeta }, run: start }
|
|
@@ -231,16 +322,20 @@ export const localStart: Command = { meta: { noun: 'local', verb: 'start', ...st
|
|
|
231
322
|
async function stop(ctx: CommandContext) {
|
|
232
323
|
const project = localProject(ctx.cwd)
|
|
233
324
|
const state = readState(project)
|
|
234
|
-
|
|
325
|
+
// The project's App dev servers point at this runtime: they stop with it (only this project's records are read).
|
|
326
|
+
const apps = stopDevServers(project)
|
|
327
|
+
const appsLine = apps.length ? ` Stopped ${apps.length} App dev server${apps.length === 1 ? '' : 's'}: ${apps.map((a) => `${a.slug} (port ${a.port})`).join(', ')}.` : ''
|
|
328
|
+
const appsField = apps.length ? { appDevServersStopped: apps.map((a) => ({ app: a.slug, port: a.port, pid: a.pid })) } : {}
|
|
329
|
+
if (!state) return { data: { stopped: false, ...appsField }, text: `No local runtime here.${appsLine}` }
|
|
235
330
|
await ctx.output.task('Stopping the local runtime', () => stopRuntime(project))
|
|
236
331
|
if (flagBool(ctx, 'no-backup')) {
|
|
237
332
|
destroyRuntimeData(project)
|
|
238
333
|
clearState(project)
|
|
239
334
|
rmSync(join(project.stateDir, 'secrets.json'), { force: true })
|
|
240
335
|
releasePorts(project.id)
|
|
241
|
-
return { data: { stopped: true, dataRemoved: true }, text:
|
|
336
|
+
return { data: { stopped: true, dataRemoved: true, ...appsField }, text: `Stopped, and removed the database and local secrets.${appsLine}` }
|
|
242
337
|
}
|
|
243
|
-
return { data: { stopped: true }, text:
|
|
338
|
+
return { data: { stopped: true, ...appsField }, text: `Stopped. Data is kept; \`frontera local start\` brings it back.${appsLine}` }
|
|
244
339
|
}
|
|
245
340
|
|
|
246
341
|
export const localStop: Command = {
|
|
@@ -249,14 +344,36 @@ export const localStop: Command = {
|
|
|
249
344
|
verb: 'stop',
|
|
250
345
|
args: [],
|
|
251
346
|
flags: { 'no-backup': 'boolean' },
|
|
252
|
-
summary: 'Stop the local runtime (keeps its data unless --no-backup)',
|
|
347
|
+
summary: 'Stop the local runtime and the project\'s App dev servers (keeps its data unless --no-backup)',
|
|
253
348
|
examples: ['frontera local stop', 'frontera local stop --no-backup'],
|
|
254
349
|
offline: true,
|
|
255
|
-
internal: true,
|
|
256
350
|
},
|
|
257
351
|
run: stop,
|
|
258
352
|
}
|
|
259
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Each project object set's id on the runtime, by its file's reference, read
|
|
356
|
+
* where `frontera blueprint object-sets` reads them; null when the runtime is
|
|
357
|
+
* stopped or does not answer (the last apply's record is shown instead).
|
|
358
|
+
*/
|
|
359
|
+
async function runtimeObjectSetIds(state: LocalState, project: LocalProject): Promise<Record<string, string> | null> {
|
|
360
|
+
const files = projectObjectSetNames(project.root)
|
|
361
|
+
if (!files.size || state.phase !== 'ready' || !isAlive(state.supervisorPid) || !state.credentials?.workspaceKey) return null
|
|
362
|
+
try {
|
|
363
|
+
const res = await fetch(`http://127.0.0.1:${state.ports.service}/v1/sync/object-sets`, { headers: { authorization: `Bearer ${state.credentials.workspaceKey}` } })
|
|
364
|
+
if (!res.ok) return null
|
|
365
|
+
const sets = normalizeObjectSets(await res.json(), files)
|
|
366
|
+
const out: Record<string, string> = {}
|
|
367
|
+
for (const ref of files.keys()) {
|
|
368
|
+
const found = findObjectSet(sets, ref, files)
|
|
369
|
+
if ('set' in found) out[ref] = found.set.id
|
|
370
|
+
}
|
|
371
|
+
return out
|
|
372
|
+
} catch {
|
|
373
|
+
return null
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
260
377
|
async function status(ctx: CommandContext) {
|
|
261
378
|
const project = localProject(ctx.cwd)
|
|
262
379
|
const state = readState(project)
|
|
@@ -279,10 +396,48 @@ async function status(ctx: CommandContext) {
|
|
|
279
396
|
]
|
|
280
397
|
return { data: Object.fromEntries(lines.map((l) => l.split(/=(.*)/s).slice(0, 2))), text: lines.join('\n') }
|
|
281
398
|
}
|
|
282
|
-
|
|
399
|
+
// Each component as it is now: a container that exited under the supervisor is down, whatever the state file says.
|
|
400
|
+
const { components, down } = await liveComponents(state)
|
|
401
|
+
const { credentials, ...rest } = { ...state, components }
|
|
402
|
+
// Each governed Action as the runtime deploys it, and its secrets now (names, and whether each has a value):
|
|
403
|
+
// read as Studio's build summary reads them, so the two count the same failures.
|
|
404
|
+
const { live, secrets, apps: liveApps } = await runtimeBuildInputs(state, project.root)
|
|
405
|
+
const setIds = await runtimeObjectSetIds(state, project)
|
|
406
|
+
// The local personas, by key and name: never a session.
|
|
407
|
+
const personas = state.phase === 'ready' && isAlive(state.supervisorPid) && state.credentials ? await listPersonas(`http://127.0.0.1:${state.ports.service}`, state.credentials) : null
|
|
408
|
+
const summary = projectBuildSummary(project.root, project.id, live, secrets, liveApps ?? null)
|
|
409
|
+
const devServers = listDevServers(project)
|
|
410
|
+
// A document left unread by a rollback reads as built here otherwise: said with its fix.
|
|
411
|
+
const stuck = (state.phase === 'ready' && isAlive(state.supervisorPid) && state.credentials ? await stuckKnowledge(`http://127.0.0.1:${state.ports.service}`, state.credentials, readBaseline(project, state.credentials.organizationId)?.restoredAt) : null) ?? []
|
|
412
|
+
// What the runtime holds with no file in the project (an Agent whose folder was deleted): said with its fix.
|
|
413
|
+
const orphans = (state.phase === 'ready' && isAlive(state.supervisorPid) && state.credentials ? await projectOrphans(project, `http://127.0.0.1:${state.ports.service}`, state.credentials).catch(() => null) : null) ?? []
|
|
283
414
|
return {
|
|
284
|
-
data: {
|
|
285
|
-
|
|
415
|
+
data: {
|
|
416
|
+
...rest,
|
|
417
|
+
credentials: credentials ? { email: credentials.email, organizationId: credentials.organizationId, workspaceId: credentials.workspaceId } : null,
|
|
418
|
+
// Down now (`components` says `failed` for each), with why; `frontera local start` brings them back.
|
|
419
|
+
...(down.length ? { down } : {}),
|
|
420
|
+
profiles: { workspace: localProfileName(project), organization: `${localProfileName(project)}-org`.slice(0, 64) },
|
|
421
|
+
// `counts` is over exactly `items`.
|
|
422
|
+
build: summary ? { line: summary.line, counts: summary.counts, items: projectBuildRows(project.root, project.id, live, setIds, secrets, liveApps ?? null) ?? [] } : null,
|
|
423
|
+
personas: personas ?? [],
|
|
424
|
+
// Knowledge search on the keyless local embeddings: approximate, with what gives the platform's.
|
|
425
|
+
...(state.credentials?.modelProviders?.includes('local-embeddings') ? { embeddings: keylessEmbeddingsStatus() } : {}),
|
|
426
|
+
docs: docsStatus(project, { mode: state.mode, platformVersion: state.platformVersion, platformSource: readConfig(project)?.platformSource }),
|
|
427
|
+
notices: projectNotices(project.root),
|
|
428
|
+
// The App dev servers running against this runtime, with their ports: `frontera local stop` stops them.
|
|
429
|
+
appDevServers: devServers.map((s) => ({ app: s.slug, port: s.port, url: s.url, pid: s.pid, startedAt: s.startedAt, startedBy: s.owner?.kind ?? 'hand', ...(s.owner ? { ownerPid: s.owner.pid } : {}) })),
|
|
430
|
+
// Knowledge documents pending or processing with no ingestion behind them, each with its fix.
|
|
431
|
+
...(stuck.length ? { knowledgeStuck: stuck } : {}),
|
|
432
|
+
// On the runtime, not in the files: each with what fixes it (`local dev` archives an Agent or workflow whose file was deleted).
|
|
433
|
+
...(orphans.length ? { notInProject: orphans } : {}),
|
|
434
|
+
},
|
|
435
|
+
text: [
|
|
436
|
+
renderStatus({ ...state, components }, project, undefined, live, setIds, personas, secrets, liveApps ?? null, down),
|
|
437
|
+
...(stuck.length ? ['', 'Knowledge', ...stuck.map((k) => ` warning: ${k.problem}`)] : []),
|
|
438
|
+
...(orphans.length ? ['', 'On the runtime, not in the project', ...orphans.map((o) => ` warning: ${orphanLine(o)}`)] : []),
|
|
439
|
+
...(devServers.length ? ['', 'App dev servers', ...devServers.map((s) => ` ${devServerLine(s)}`)] : []),
|
|
440
|
+
].join('\n'),
|
|
286
441
|
}
|
|
287
442
|
}
|
|
288
443
|
|
|
@@ -296,7 +451,6 @@ export const localStatus: Command = {
|
|
|
296
451
|
summary: 'Show the local runtime: components, URLs and credentials (-o env for scripts)',
|
|
297
452
|
examples: ['frontera local status', 'eval "$(frontera local status -o env)"'],
|
|
298
453
|
offline: true,
|
|
299
|
-
internal: true,
|
|
300
454
|
},
|
|
301
455
|
run: status,
|
|
302
456
|
}
|
|
@@ -330,7 +484,6 @@ export const localLogs: Command = {
|
|
|
330
484
|
summary: 'Print a component\'s log (service by default)',
|
|
331
485
|
examples: ['frontera local logs', 'frontera local logs jobs -f'],
|
|
332
486
|
offline: true,
|
|
333
|
-
internal: true,
|
|
334
487
|
},
|
|
335
488
|
run: logs,
|
|
336
489
|
}
|
|
@@ -379,45 +532,123 @@ export const localUi: Command = {
|
|
|
379
532
|
summary: 'Open Frontera Studio for the running local runtime',
|
|
380
533
|
examples: ['frontera local ui', 'frontera local ui --no-open'],
|
|
381
534
|
offline: true,
|
|
382
|
-
internal: true,
|
|
383
535
|
},
|
|
384
536
|
run: ui,
|
|
385
537
|
}
|
|
386
538
|
|
|
539
|
+
/**
|
|
540
|
+
* The case files `frontera local test` runs. `agent` takes one Agent's cases,
|
|
541
|
+
* `workflow` one workflow's; neither runs both kinds. `regression`: only the
|
|
542
|
+
* Agents' regression datasets, as the workspace chose them. `file`: one case
|
|
543
|
+
* file, an Agent's (agents/<slug>/evals/*.yaml) or a workflow's
|
|
544
|
+
* (workflows/tests/*.yaml), chosen as MCP `test_run` chooses it.
|
|
545
|
+
*/
|
|
546
|
+
export function localTestFiles(root: string, scope: { agent?: string; workflow?: string; file?: string; regression?: boolean }): { files: CaseFile[]; workflowFiles: WorkflowTestFile[] } {
|
|
547
|
+
const file = scope.file?.replace(/^\.\//, '')
|
|
548
|
+
const files = scope.workflow ? [] : selectCaseFiles(root, { agent: scope.agent, file, regression: scope.regression })
|
|
549
|
+
const workflowFiles = scope.agent || scope.regression ? [] : findWorkflowTestFiles(root, scope.workflow).filter((f) => !file || f.file === file)
|
|
550
|
+
return { files, workflowFiles }
|
|
551
|
+
}
|
|
552
|
+
|
|
387
553
|
async function test(ctx: CommandContext) {
|
|
388
554
|
const project = localProject(ctx.cwd)
|
|
389
555
|
const state = readState(project)
|
|
390
556
|
if (!state || state.phase !== 'ready' || !state.credentials?.session) {
|
|
391
557
|
throw new CliError('the local runtime is not running', { code: 'LOCAL_NOT_RUNNING', hint: 'run `frontera local start` first' })
|
|
392
558
|
}
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
559
|
+
const agent = flagString(ctx, 'agent')
|
|
560
|
+
const workflow = flagString(ctx, 'workflow')
|
|
561
|
+
const regression = flagBool(ctx, 'regression')
|
|
562
|
+
const file = flagString(ctx, 'file')?.replace(/^\.\//, '')
|
|
563
|
+
const { files, workflowFiles } = localTestFiles(project.root, { agent, workflow, file, regression })
|
|
564
|
+
const total = caseCount(files)
|
|
565
|
+
const workflowTotal = workflowFiles.reduce((n, f) => n + Math.max(f.cases.length, f.problem ? 1 : 0), 0)
|
|
566
|
+
const empty = { passed: 0, failed: 0, results: [], workflows: [] }
|
|
567
|
+
if (total + workflowTotal === 0 && regression) {
|
|
568
|
+
return { data: empty, text: 'No regression datasets. A pull marks the dataset each Agent is tested on before publishing in the workspace (`regression: true`).' }
|
|
569
|
+
}
|
|
570
|
+
if (total + workflowTotal === 0 && file) {
|
|
571
|
+
throw new UsageError(`no test case file ${file}${agent ? ` for Agent ${agent}` : workflow ? ` for workflow ${workflow}` : ''}`, 'give its path from the project root, e.g. agents/<slug>/evals/<name>.yaml or workflows/tests/<slug>.yaml')
|
|
398
572
|
}
|
|
399
|
-
if (total === 0) {
|
|
573
|
+
if (total + workflowTotal === 0) {
|
|
400
574
|
return {
|
|
401
|
-
data:
|
|
402
|
-
text:
|
|
575
|
+
data: empty,
|
|
576
|
+
text: workflow
|
|
577
|
+
? `No test cases for workflow ${workflow}. Add workflows/tests/${workflow}.yaml (\`frontera local schema workflow-tests\` is its format).`
|
|
578
|
+
: 'No test cases. Save one from Studio (Agents → Save as test case), add agents/<slug>/evals/*.yaml, or add workflows/tests/<slug>.yaml.',
|
|
403
579
|
}
|
|
404
580
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
581
|
+
const serviceUrl = `http://127.0.0.1:${state.ports.service}`
|
|
582
|
+
let concurrency: number | undefined
|
|
583
|
+
try {
|
|
584
|
+
concurrency = caseConcurrency(flagString(ctx, 'concurrency'))
|
|
585
|
+
} catch (err) {
|
|
586
|
+
throw new UsageError((err as Error).message, 'frontera local test --concurrency 1')
|
|
587
|
+
}
|
|
588
|
+
const results: CaseResult[] = total
|
|
589
|
+
? await runCases({
|
|
590
|
+
serviceUrl,
|
|
591
|
+
creds: state.credentials,
|
|
592
|
+
files,
|
|
593
|
+
live: flagBool(ctx, 'live'),
|
|
594
|
+
// The draft is what the files say, or the cases say what to run first.
|
|
595
|
+
staleDraft: (slug) => agentFilesProblem(project, state.credentials!.organizationId, slug),
|
|
596
|
+
concurrency,
|
|
597
|
+
onResult: (r) => {
|
|
598
|
+
// A case never sent after the run stopped is said once, below, not once each.
|
|
599
|
+
if (r.notRun?.skipped) return
|
|
600
|
+
ctx.output.note(` ${r.passed ? '✓' : r.notRun ? '–' : '✕'} ${r.agent} · ${r.name}${r.passed ? '' : r.notRun ? ` — couldn’t run: ${r.notRun.what}` : ` — ${r.reason}`}${r.passed && r.retried ? ' (run twice: the provider refused the first try for its limits)' : ''}${r.waited?.length ? ` (${waitedWords(r.waited)})` : ''} (${r.ms} ms)`)
|
|
601
|
+
},
|
|
602
|
+
})
|
|
603
|
+
: []
|
|
604
|
+
// What kept cases from running, once each, with its fix: never a verdict on the Agent.
|
|
605
|
+
const notRun = notRunGroups(results)
|
|
606
|
+
for (const g of notRun) {
|
|
607
|
+
if (g.skipped) ctx.output.note(` – ${g.skipped} more case${g.skipped === 1 ? '' : 's'} not sent: ${g.what}`)
|
|
608
|
+
ctx.output.note(` couldn’t run ${g.cases.length} case${g.cases.length === 1 ? '' : 's'}: ${withFix(g.what, g.fix)}`)
|
|
419
609
|
}
|
|
420
|
-
|
|
610
|
+
const workflows: WorkflowTestResult[] = workflowTotal
|
|
611
|
+
? await runWorkflowTests({
|
|
612
|
+
serviceUrl,
|
|
613
|
+
creds: state.credentials,
|
|
614
|
+
files: workflowFiles,
|
|
615
|
+
// Exactly the version `local dev` pushed each file as, never a stale draft.
|
|
616
|
+
fileState: (slug) => workflowFileState(project, state.credentials!.organizationId, slug),
|
|
617
|
+
onResult: (r) => ctx.output.note(` ${r.passed ? '✓' : '✕'} workflow ${r.workflow} · ${r.name}${r.passed ? '' : ` — ${r.reason}`} (${r.ms} ms)`),
|
|
618
|
+
})
|
|
619
|
+
: []
|
|
620
|
+
// Each failing Agent case's conversation, last turn's trace and reply: what to read to tell a regression from a bad case.
|
|
621
|
+
const evidence = await failingCaseEvidence(project, results)
|
|
622
|
+
for (const [i, e] of evidence) ctx.output.note(` ${results[i]!.agent} · ${results[i]!.name}: ${evidenceLine(e)}`)
|
|
623
|
+
// Every case's last-turn trace beside its conversation (`--json`), passing ones included; a failing one's came with its evidence.
|
|
624
|
+
const traces = await caseTraces(project, results, { known: evidence })
|
|
625
|
+
const comparison = compareWithLastRun(results, readLastRun(project))
|
|
626
|
+
if (results.length) recordRun(project, results)
|
|
627
|
+
// Each workflow case's last run, for Studio's Evaluations and Forge's home.
|
|
628
|
+
recordWorkflowTestRun(project, workflows)
|
|
629
|
+
const failed = [...results.filter((r) => !r.passed && !r.notRun).map((f) => `${f.agent} · ${f.name}`), ...workflows.filter((r) => !r.passed).map((f) => `workflow ${f.workflow} · ${f.name}`)]
|
|
630
|
+
const notRunCount = notRun.reduce((n, g) => n + g.cases.length, 0)
|
|
631
|
+
const passed = results.length + workflows.length - failed.length - notRunCount
|
|
632
|
+
const newly = comparison.newlyFailing.length ? `, ${comparison.newlyFailing.length} newly failing since the last run` : ''
|
|
633
|
+
const summary = `${passed} passed, ${failed.length} failed${newly}${notRun.length ? `, ${notRunWords(notRun)}` : ''}`
|
|
634
|
+
const data = { passed, failed: failed.length, ...(notRun.length ? { notRun: notRunCount, couldNotRun: notRun.map((g) => ({ what: `couldn’t run: ${g.what}`, fix: g.fix, cases: g.cases, ...(g.skipped ? { notSent: g.skipped } : {}) })) } : {}), fixed: comparison.fixed.length, results: results.map((r, i) => ({ ...r, ...traces.get(i), ...evidence.get(i) })), workflows }
|
|
635
|
+
if (failed.length || notRun.length) {
|
|
636
|
+
// Only cases that couldn't run: the run did not pass, and the cause is the provider's or the runtime's, not the Agent's.
|
|
637
|
+
const onlyNotRun = !failed.length
|
|
638
|
+
const code = onlyNotRun ? 'LOCAL_TESTS_NOT_RUN' : 'LOCAL_TESTS_FAILED'
|
|
639
|
+
const message = onlyNotRun ? summary : `${summary}: ${failed.join('; ')}`
|
|
640
|
+
const caseHint = workflows.some((r) => !r.passed)
|
|
641
|
+
? 'open the run in Studio (`frontera local ui` → Workflows → Runs) or the Agent\'s conversation, then fix the workflow, the Agent or the case'
|
|
642
|
+
: 'open the Agent in Studio (`frontera local ui`) to see the conversation, then fix the Agent or the case'
|
|
643
|
+
const hint = [onlyNotRun ? null : caseHint, ...notRun.map((g) => g.fix)].filter(Boolean).join('; ')
|
|
644
|
+
// `--json`: the per-case results ARE the data, failed or not, so they go to stdout (exit 1 says a case failed), as `local check` does.
|
|
645
|
+
if (flagBool(ctx, 'json')) {
|
|
646
|
+
ctx.output.note(`error [${code}]: ${message}`)
|
|
647
|
+
return { data: { ...data, error: { code, message, hint } }, text: '', exitCode: EXIT.FAILURE }
|
|
648
|
+
}
|
|
649
|
+
throw new CliError(message, { code, hint, details: data })
|
|
650
|
+
}
|
|
651
|
+
return { data, text: summary }
|
|
421
652
|
}
|
|
422
653
|
|
|
423
654
|
export const localTest: Command = {
|
|
@@ -425,11 +656,15 @@ export const localTest: Command = {
|
|
|
425
656
|
noun: 'local',
|
|
426
657
|
verb: 'test',
|
|
427
658
|
args: [],
|
|
428
|
-
flags: { agent: 'string', live: 'boolean', regression: 'boolean' },
|
|
429
|
-
|
|
430
|
-
|
|
659
|
+
flags: { agent: 'string', workflow: 'string', file: 'string', live: 'boolean', regression: 'boolean', concurrency: 'string' },
|
|
660
|
+
flagHelp: { file: 'run only this case file, by its path from the project root: agents/<slug>/evals/<name>.yaml or workflows/tests/<slug>.yaml' },
|
|
661
|
+
summary: 'Run the project\'s Agent test cases and workflow tests against the local runtime',
|
|
662
|
+
details: [
|
|
663
|
+
'`--file` runs one case file, by its path from the project root: an Agent\'s (agents/<slug>/evals/<name>.yaml) or a workflow\'s (workflows/tests/<slug>.yaml), the same file MCP `test_run` takes. A path that names no case file is refused.',
|
|
664
|
+
'A workflow test runs the workflow for real on the local runtime: a governed Action one of its steps submits (an action step, or a Function step\'s code) is applied and changes the runtime\'s records when the Action runs here; one whose effect would reach another system is switched off here. The writes stay; `frontera local check` rolls back its own tests\' writes, and `frontera local snapshot save` / `restore` do it by hand. The workspace is never touched.',
|
|
665
|
+
],
|
|
666
|
+
examples: ['frontera local test', 'frontera local test --agent claims-triage', 'frontera local test --workflow claim-intake', 'frontera local test --file agents/claims-triage/evals/refunds.yaml', 'frontera local test --live', 'frontera local test --regression', 'frontera local test --concurrency 1'],
|
|
431
667
|
offline: true,
|
|
432
|
-
internal: true,
|
|
433
668
|
},
|
|
434
669
|
run: test,
|
|
435
670
|
}
|
|
@@ -463,6 +698,7 @@ export const localCommands: readonly Command[] = [
|
|
|
463
698
|
localStatus,
|
|
464
699
|
localLogs,
|
|
465
700
|
localReset,
|
|
701
|
+
localRemove,
|
|
466
702
|
localUi,
|
|
467
703
|
localDev,
|
|
468
704
|
localTest,
|
|
@@ -480,6 +716,7 @@ export const localCommands: readonly Command[] = [
|
|
|
480
716
|
localDocsMcp,
|
|
481
717
|
localSchema,
|
|
482
718
|
localData,
|
|
719
|
+
localSdkRefresh,
|
|
483
720
|
localSync,
|
|
484
721
|
localDaemon,
|
|
485
722
|
]
|