@frontera-sdk/cli 1.52.39 → 1.52.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,9 +1,23 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { parse as parseYaml } from 'yaml'
|
|
5
|
+
|
|
6
|
+
import { BUILD_LABEL, buildCounts, buildSummaryLine, buildView, combineBuilds, failureFix, loadBuild, loadSettingsDifferences, NEEDS_VALUE, refOfTarget, withSetupNeeded, type ItemBuild, type ItemBuildState, type SolutionItem } from './local-applied'
|
|
7
|
+
import { agentActionGapLine, agentActionGaps } from './local-agent-action-grants'
|
|
8
|
+
import { agentLeanPlugins, agentLeanPluginsLine } from './local-agent-lean-plugins'
|
|
9
|
+
import { agentMissingTools, isMissingToolAdaptation, missingToolsLine } from './local-agent-missing-tools'
|
|
10
|
+
import { blueprintFile } from './local-blueprint-errors'
|
|
11
|
+
import { emptyWorkflowLine, emptyWorkflows, workspaceWorkflows } from './local-empty-workflows'
|
|
5
12
|
import { blueprintBuild, localOrganizationId, objectStatus, projectBlueprintOutline } from './local-blueprint-outline'
|
|
13
|
+
import { liveActionRuns, type LiveAction } from './local-actions'
|
|
14
|
+
import type { LiveApp } from './local-app-runtime'
|
|
15
|
+
import { appCodeStates, appPath, customAppFolder, type AppCodeState, type CustomAppFolder } from './local-apps'
|
|
6
16
|
import { allTargets } from './local-dev-sync'
|
|
17
|
+
import { functionSecretNames, hasSecretValue, type SecretPresence, workflowSecretNames } from './local-secrets'
|
|
18
|
+
import { settingEffect } from './local-org-settings'
|
|
19
|
+
import { readLock } from './local-sync-plan'
|
|
20
|
+
import { readSnapshot } from './local-sync-snapshot'
|
|
7
21
|
|
|
8
22
|
/**
|
|
9
23
|
* How the last `local dev` left the project, counted one way for every
|
|
@@ -30,6 +44,11 @@ const projectOf = (root: string, id = '') => ({ root, id, stateDir: join(root, '
|
|
|
30
44
|
export function projectItemBuilds(root: string, id = ''): Record<string, ItemBuild> | null {
|
|
31
45
|
const org = localOrganizationId(root)
|
|
32
46
|
if (!org) return null
|
|
47
|
+
return itemBuildsFor(root, id, org)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Each item's build by reference; with no organization (never started), every item is not applied yet. */
|
|
51
|
+
function itemBuildsFor(root: string, id: string, org: string | null): Record<string, ItemBuild> {
|
|
33
52
|
const view = buildView(projectOf(root, id), org)
|
|
34
53
|
const byRef = new Map<string, ItemBuild[]>()
|
|
35
54
|
for (const target of allTargets(root)) {
|
|
@@ -38,7 +57,39 @@ export function projectItemBuilds(root: string, id = ''): Record<string, ItemBui
|
|
|
38
57
|
const ref = refOfTarget(target)
|
|
39
58
|
byRef.set(ref, [...(byRef.get(ref) ?? []), view.get(target.key)!])
|
|
40
59
|
}
|
|
41
|
-
|
|
60
|
+
// An Agent whose instructions name Actions its grants never offer: said on its row, its state unchanged.
|
|
61
|
+
const warned = new Map<string, string[]>()
|
|
62
|
+
for (const gap of agentActionGaps(root)) warned.set(`agent/${gap.agent}`, [...(warned.get(`agent/${gap.agent}`) ?? []), agentActionGapLine(gap)])
|
|
63
|
+
// An Agent whose Plugins the lean profile leaves out (behavior, no computer): said on its row, its state unchanged.
|
|
64
|
+
for (const g of agentLeanPlugins(root)) warned.set(`agent/${g.agent}`, [...(warned.get(`agent/${g.agent}`) ?? []), agentLeanPluginsLine(g)])
|
|
65
|
+
// A workflow with no steps applies, but does nothing: said on its row, its state unchanged.
|
|
66
|
+
const empty = emptyWorkflows(root)
|
|
67
|
+
const inWorkspace = empty.length ? workspaceWorkflows(root) : null
|
|
68
|
+
for (const slug of empty) warned.set(`workflow/${slug}`, [...(warned.get(`workflow/${slug}`) ?? []), emptyWorkflowLine(slug, inWorkspace)])
|
|
69
|
+
const combined = Object.fromEntries([...byRef].map(([ref, list]) => [ref, combineBuilds(list)] as const))
|
|
70
|
+
// An Agent whose bound Functions or workflows failed to apply runs without them: a warning on its row, not an adaptation.
|
|
71
|
+
const agents = [...byRef.keys()].filter((r) => r.startsWith('agent/')).map((r) => r.slice('agent/'.length))
|
|
72
|
+
const missing = new Set<string>()
|
|
73
|
+
for (const m of agentMissingTools(root, agents, combined)) {
|
|
74
|
+
const ref = `agent/${m.agent}`
|
|
75
|
+
missing.add(ref)
|
|
76
|
+
warned.set(ref, [missingToolsLine(m), ...(warned.get(ref) ?? [])])
|
|
77
|
+
}
|
|
78
|
+
return Object.fromEntries(Object.entries(combined).map(([ref, built]) => {
|
|
79
|
+
const own = missing.has(ref) ? withoutMissingTools(built) : built
|
|
80
|
+
const warnings = warned.get(ref)
|
|
81
|
+
return [ref, warnings ? { ...own, warnings } : own]
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** An Agent's build without the adaptations its missing tools recorded: they are its warning now. Adapted by nothing else, it is built. */
|
|
86
|
+
function withoutMissingTools(b: ItemBuild): ItemBuild {
|
|
87
|
+
const adaptations = b.adaptations.filter((a) => !isMissingToolAdaptation(a))
|
|
88
|
+
if (adaptations.length === b.adaptations.length) return b
|
|
89
|
+
if (b.state !== 'adapted') return { ...b, adaptations }
|
|
90
|
+
// A reason that was the adaptations, said together, follows them; one of its own stays.
|
|
91
|
+
const reason = b.reason === b.adaptations.join('; ') ? adaptations.join('; ') || null : b.reason
|
|
92
|
+
return adaptations.length ? { ...b, adaptations, reason } : { ...b, state: 'built', label: BUILD_LABEL.built, reason: null, adaptations }
|
|
42
93
|
}
|
|
43
94
|
|
|
44
95
|
/** One item's build, with each adaptation in words: what an Agent's page says about how it runs here. */
|
|
@@ -51,22 +102,64 @@ export interface ProjectBuildSummary {
|
|
|
51
102
|
line: string
|
|
52
103
|
/** Short: "2 problems", or "All 12 built" — what to say first. */
|
|
53
104
|
headline: string
|
|
54
|
-
/** Blocked and
|
|
105
|
+
/** Blocked, failed and needing setup together: what does not run, or fails when used. */
|
|
55
106
|
problems: number
|
|
107
|
+
/** Every row but the Custom Apps not deployed here, by state: what `local dev` applies. */
|
|
56
108
|
counts: Record<ItemBuildState, number>
|
|
109
|
+
/** The Custom Apps not deployed here, by their own status (`appStatus`): counted apart, as `local dev` doesn't deploy them. */
|
|
110
|
+
apps: Partial<Record<AppStatus, number>>
|
|
57
111
|
/** Where the runtime's organization settings differ from the workspace's, one line each. */
|
|
58
112
|
settings: string[]
|
|
113
|
+
/** Each of `settings`, with what it changes when testing here and whether it can be aligned (`settingEffect`). */
|
|
114
|
+
settingDetails: Array<{ line: string; effect: string | null; align: string | null }>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One item the build is counted over: what `status` lists, and what the summary counts, the same rows. */
|
|
118
|
+
export interface BuildRow extends ItemBuild {
|
|
119
|
+
/** `agent/desk`, `workflow/claim-intake`, `objectType/Ticket`. */
|
|
120
|
+
ref: string
|
|
121
|
+
/** A Custom App's code (`app/<name>`): its folder, its last deployment here and its dev server. */
|
|
122
|
+
app?: CustomAppFolder
|
|
123
|
+
/** A Custom App's one status (`app/<name>`): deployed to this runtime, built, not built, no code, or code not pulled. */
|
|
124
|
+
appStatus?: AppStatus
|
|
125
|
+
/** A saved object set (`objectSet/<ref>`): its id on the runtime, what an App's code keeps. */
|
|
126
|
+
id?: string
|
|
127
|
+
/**
|
|
128
|
+
* A Custom App the folder says is deployed that the runtime does not have
|
|
129
|
+
* (a database restore, a reset): why, in a line. Its status is then
|
|
130
|
+
* `built` (or `not-built`), not `deployed`.
|
|
131
|
+
*/
|
|
132
|
+
notOnRuntime?: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Every item the project's build is said over, once: each item the runtime
|
|
137
|
+
* applies (Agents, workflows, Functions, skills, knowledge, Plugins, Chat
|
|
138
|
+
* Apps…), an Agent, workflow or Function whose folder is there but which was
|
|
139
|
+
* never applied, and each Blueprint object type (`objectType/<ApiName>`), as
|
|
140
|
+
* `objectStatus` judges it, `stubbed` included. An object type left off by
|
|
141
|
+
* design (it reads the platform's media) is not a row: it is not a problem to
|
|
142
|
+
* fix. Null before the runtime first started.
|
|
143
|
+
*/
|
|
144
|
+
export function projectBuildRows(root: string, id = '', live: ReadonlyMap<string, LiveAction> | null = null, objectSetIds: Readonly<Record<string, string>> | null = null, secrets: SecretPresence | null = null, apps: ReadonlyMap<string, LiveApp> | null = null): BuildRow[] | null {
|
|
145
|
+
const built = projectItemBuilds(root, id)
|
|
146
|
+
if (!built) return null
|
|
147
|
+
return buildRowsFrom(root, id, built, live, objectSetIds, secrets, apps)
|
|
59
148
|
}
|
|
60
149
|
|
|
61
150
|
/**
|
|
62
|
-
* The
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* to fix. Null before the runtime first started, or when there is nothing.
|
|
151
|
+
* The rows the first apply will be counted over, before the runtime ever
|
|
152
|
+
* started: the same rows `projectBuildRows` gives once it has, so "start the
|
|
153
|
+
* runtime to apply 290 items" and the count after it say one number.
|
|
66
154
|
*/
|
|
67
|
-
export function
|
|
68
|
-
|
|
69
|
-
|
|
155
|
+
export function projectRowsBeforeApply(root: string): BuildRow[] {
|
|
156
|
+
return buildRowsFrom(root, '', itemBuildsFor(root, '', null), null, null, null, null)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildRowsFrom(root: string, id: string, built: Record<string, ItemBuild>, live: ReadonlyMap<string, LiveAction> | null, objectSetIds: Readonly<Record<string, string>> | null, secrets: SecretPresence | null, liveApps: ReadonlyMap<string, LiveApp> | null): BuildRow[] {
|
|
160
|
+
// With the runtime's secrets read (`secrets`), a "Needs a value" follows the secret now, not the last apply.
|
|
161
|
+
const needs = secrets ? secretsByRef(root) : null
|
|
162
|
+
const builds = secrets && needs ? Object.fromEntries(Object.entries(built).map(([ref, b]) => [ref, withSecretsNow(b, needs.get(ref) ?? [], secrets)])) : built
|
|
70
163
|
const agents = entries(join(root, 'agents')).filter((slug) => existsSync(join(root, 'agents', slug, 'agent.json')))
|
|
71
164
|
const workflows = entries(join(root, 'workflows')).filter((f) => /\.ya?ml$/.test(f)).map((f) => f.replace(/\.ya?ml$/, ''))
|
|
72
165
|
const functions = entries(join(root, 'functions')).flatMap((f) => {
|
|
@@ -74,33 +167,407 @@ export function projectBuildSummary(root: string, id = ''): ProjectBuildSummary
|
|
|
74
167
|
if (/\.ts$/.test(f) && !/\.(test|d)\.ts$/.test(f) && statSync(at).isFile()) return [f.replace(/\.ts$/, '')]
|
|
75
168
|
return existsSync(join(at, 'package.json')) ? [f] : []
|
|
76
169
|
})
|
|
170
|
+
const rows: BuildRow[] = []
|
|
171
|
+
const seen = new Set<string>()
|
|
172
|
+
// Evaluations are run, not applied.
|
|
173
|
+
for (const [ref, b] of Object.entries(builds)) {
|
|
174
|
+
if (ref.startsWith('evaluation/')) continue
|
|
175
|
+
rows.push({ ref, ...b })
|
|
176
|
+
seen.add(ref)
|
|
177
|
+
}
|
|
178
|
+
for (const ref of [...agents.map((a) => `agent/${a}`), ...workflows.map((w) => `workflow/${w}`), ...functions.map((f) => `function/${f}`)]) {
|
|
179
|
+
if (seen.has(ref)) continue
|
|
180
|
+
rows.push({ ref, state: 'not-applied', label: 'Not applied yet', reason: null, adaptations: [] })
|
|
181
|
+
seen.add(ref)
|
|
182
|
+
}
|
|
183
|
+
// The datasets, as their own applies left them.
|
|
184
|
+
const org = localOrganizationId(root)
|
|
185
|
+
const view = buildView(projectOf(root, id), org)
|
|
186
|
+
for (const target of allTargets(root)) {
|
|
187
|
+
if (target.kind !== 'dataset') continue
|
|
188
|
+
const b = view.get(target.key)
|
|
189
|
+
if (b) rows.push({ ref: `dataset/${target.apiName}`, ...b })
|
|
190
|
+
}
|
|
77
191
|
const blueprint = blueprintBuild(root)
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
192
|
+
for (const t of projectBlueprintOutline(root, blueprint.actions)) {
|
|
193
|
+
const status = objectStatus(t, blueprint)
|
|
194
|
+
if (status.kind === 'platform-only') continue
|
|
195
|
+
const state = status.kind as ItemBuildState
|
|
196
|
+
rows.push({
|
|
197
|
+
ref: `objectType/${t.apiName}`,
|
|
198
|
+
state,
|
|
199
|
+
label: status.label,
|
|
200
|
+
reason: status.reason ?? null,
|
|
201
|
+
adaptations: state === 'adapted' || state === 'stubbed' ? [status.reason ?? status.label] : [],
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
// The rest of the Blueprint goes as the Blueprint went: each link type, metric, semantic
|
|
205
|
+
// type, shared field and saved object set is built when it applied, and fails with it.
|
|
206
|
+
const whole: Omit<ItemBuild, 'adaptations'> = blueprint.failed
|
|
207
|
+
? { state: 'failed', label: 'Failed', reason: blueprint.failed }
|
|
208
|
+
: blueprint.applied
|
|
209
|
+
? { state: 'built', label: 'Built', reason: null }
|
|
210
|
+
: { state: 'not-applied', label: 'Not applied yet', reason: null }
|
|
211
|
+
// A saved set's id: read from the runtime when given, else as the last Blueprint apply saved it.
|
|
212
|
+
const setIds = objectSetIds ?? loadBuild(projectOf(root, id), org).blueprint?.objectSetIds ?? {}
|
|
213
|
+
for (const [folder, kind] of BLUEPRINT_PARTS) {
|
|
214
|
+
for (const name of blueprintNames(root, folder)) {
|
|
215
|
+
const setId = kind === 'objectSet' ? setIds[name] : undefined
|
|
216
|
+
// Left off this runtime: refused (a failure, with the runtime's words) or naming a type left off.
|
|
217
|
+
const part = blueprint.parts?.[`${kind}/${name}`]
|
|
218
|
+
if (part) {
|
|
219
|
+
const reason = `Left off here: ${part.reason}`
|
|
220
|
+
rows.push(part.refused
|
|
221
|
+
? { ref: `${kind}/${name}`, state: 'failed', label: 'Left off: the runtime refused it', reason: part.reason.replace(/^the local runtime refused it: /, ''), adaptations: [], fix: failureFix({}, blueprintFile(root, folder, name)) }
|
|
222
|
+
: { ref: `${kind}/${name}`, state: 'adapted', label: 'Left off here', reason, adaptations: [reason] })
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
rows.push({ ref: `${kind}/${name}`, ...whole, adaptations: [], ...(setId ? { id: setId } : {}) })
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Each governed Action, as it was deployed here: running, switched off, or not deployed.
|
|
229
|
+
// With the runtime read (`live`, what `frontera action list` reads), its deployment decides, not the last build.
|
|
230
|
+
for (const name of blueprintNames(root, 'actions')) {
|
|
231
|
+
// Refused at the last apply, and a Blueprint file changed since: still a failure (unless the runtime runs it now),
|
|
232
|
+
// so the count doesn't drop while the change waits for `local dev`, in status and Studio alike.
|
|
233
|
+
const before = blueprint.refusedBefore?.[name]
|
|
234
|
+
const row = before && !liveActionRuns(live?.get(name))
|
|
235
|
+
? changedSinceFailure(actionRow(before, whole))
|
|
236
|
+
: live ? liveActionRow(live.get(name), blueprint.actions?.[name], whole) : actionRow(blueprint.actions?.[name], whole)
|
|
237
|
+
// A refused or undeployed Action is fixed in its own file, as any failed item is.
|
|
238
|
+
// One deployed as the workspace runs it says what its owner fixes there, its state unchanged.
|
|
239
|
+
const inherited = row.state === 'failed' ? [] : blueprint.actions?.[name]?.inherited ?? []
|
|
240
|
+
rows.push({ ref: `action/${name}`, ...row, ...(row.state === 'failed' && !row.fix && blueprint.actions?.[name] ? { fix: failureFix({}, blueprintFile(root, 'actions', name)) } : {}), ...(inherited.length ? { warnings: inherited.map((w) => `action/${name}: ${w}`) } : {}) })
|
|
241
|
+
}
|
|
242
|
+
// Each Custom App, with one status: its code deployed to this runtime, built and not deployed, not built, not in the
|
|
243
|
+
// project, or none at all. `local dev` saves only what the App is (its file, and the Functions it may start): its
|
|
244
|
+
// code is deployed on its own, so that save is not the App's status. A save that failed is, until it is fixed.
|
|
245
|
+
for (const code of appCodeStates(root)) {
|
|
246
|
+
const ref = `app/${code.slug}`
|
|
247
|
+
const at = rows.findIndex((r) => r.ref === ref)
|
|
248
|
+
const saved = at >= 0 ? rows[at]! : null
|
|
249
|
+
const own = appStatusRow(root, code, liveApps)
|
|
250
|
+
const failed = saved && (saved.state === 'failed' || saved.state === 'blocked' || saved.state === 'needs-setup')
|
|
251
|
+
const row: BuildRow = failed ? { ...saved, appStatus: own.appStatus, ...(own.app ? { app: own.app } : {}) } : { ref, ...own, ...(saved?.warnings?.length ? { warnings: saved.warnings } : {}) }
|
|
252
|
+
if (at >= 0) rows[at] = row
|
|
253
|
+
else rows.push(row)
|
|
254
|
+
}
|
|
255
|
+
return rows
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The secrets each workflow and Function names in its file, by reference (`workflow/intake`). */
|
|
259
|
+
export function secretsByRef(root: string): Map<string, string[]> {
|
|
260
|
+
const out = new Map<string, string[]>()
|
|
261
|
+
for (const target of allTargets(root)) {
|
|
262
|
+
if (target.kind !== 'workflow' && target.kind !== 'function') continue
|
|
263
|
+
try {
|
|
264
|
+
const text = readFileSync(join(root, target.file), 'utf8')
|
|
265
|
+
const names = target.kind === 'function' ? functionSecretNames(text) : workflowSecretNames(parseYaml(text))
|
|
266
|
+
const ref = refOfTarget(target)
|
|
267
|
+
out.set(ref, [...new Set([...(out.get(ref) ?? []), ...names])])
|
|
268
|
+
} catch {
|
|
269
|
+
// Unreadable: it names nothing here.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* An item's build with its secrets as the runtime holds them now: a "Needs a
|
|
277
|
+
* value for X" the last apply recorded goes once X has a value, and one is
|
|
278
|
+
* added for a secret the item names that has none now (a database restore can
|
|
279
|
+
* leave a secret listed without its value). An item with nothing else
|
|
280
|
+
* adapted is built again. Only built and adapted items change: what failed or
|
|
281
|
+
* is blocked says its own reason.
|
|
282
|
+
*/
|
|
283
|
+
export function withSecretsNow(build: ItemBuild, needs: readonly string[], secrets: SecretPresence): ItemBuild {
|
|
284
|
+
if (build.state !== 'built' && build.state !== 'adapted' && build.state !== 'needs-setup') return build
|
|
285
|
+
const kept = build.adaptations.filter((a) => {
|
|
286
|
+
const name = NEEDS_VALUE.exec(a)?.[1]
|
|
287
|
+
return !name || !hasSecretValue(secrets, name)
|
|
81
288
|
})
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
]
|
|
89
|
-
|
|
289
|
+
const said = new Set(kept.flatMap((a) => NEEDS_VALUE.exec(a)?.[1] ?? []))
|
|
290
|
+
const added = needs
|
|
291
|
+
.filter((n) => !said.has(n) && !hasSecretValue(secrets, n))
|
|
292
|
+
.map((n) => `Needs a value for ${n}${secrets.names.has(n) ? ': listed, but this runtime has no value for it' : ''}: set it with \`frontera secret set ${n}\` in the project folder`)
|
|
293
|
+
if (kept.length === build.adaptations.length && !added.length) return build
|
|
294
|
+
const adaptations = [...kept, ...added]
|
|
295
|
+
if (!adaptations.length) return { state: 'built', label: 'Built', reason: null, adaptations: [] }
|
|
296
|
+
// A secret still without a value: it needs setup; the rest is adapted.
|
|
297
|
+
const setup = withSetupNeeded('adapted', adaptations)
|
|
298
|
+
const state = setup?.state ?? 'adapted'
|
|
299
|
+
return { state, label: BUILD_LABEL[state], reason: setup?.reason ?? adaptations.join('; '), adaptations }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The project's files against the runtime, for `local status`: `local dev`
|
|
304
|
+
* is the next step only for files changed since their last apply. A file
|
|
305
|
+
* whose apply failed as it is now, or that waits for another item, is
|
|
306
|
+
* counted with a pointer to its Build row, which says its own reason and fix:
|
|
307
|
+
* applying the same files again changes nothing.
|
|
308
|
+
*/
|
|
309
|
+
export function projectFilesLines(files: SolutionItem[]): string[] {
|
|
310
|
+
// A Blueprint that applied with refused items is on the runtime: the Build lines below name what it refused.
|
|
311
|
+
const changed = files.filter((f) => (f.state === 'new' || f.state === 'changed') && !f.waiting)
|
|
312
|
+
const failed = files.filter((f) => f.state === 'failed')
|
|
313
|
+
const waiting = files.filter((f) => f.waiting)
|
|
314
|
+
const behind = changed.length + failed.length + waiting.length
|
|
315
|
+
if (!behind) return [` Project all ${files.length} files applied`]
|
|
316
|
+
const names = (list: SolutionItem[]) => `${list.slice(0, 6).map((f) => f.name).join(', ')}${list.length > 6 ? ` and ${list.length - 6} more` : ''}`
|
|
317
|
+
const lines = [` Project ${behind} of ${files.length} files not on the runtime`]
|
|
318
|
+
if (changed.length) lines.push(` ${changed.length} changed since the last apply (${names(changed)}) — run \`frontera local dev\``)
|
|
319
|
+
// A failure that is the runtime's (no embedding model that runs), not the file's, applies again once that is fixed.
|
|
320
|
+
const notTheFile = failed.filter((f) => f.fix?.startsWith('not its file'))
|
|
321
|
+
if (failed.length) lines.push(` ${failed.length} didn’t apply as ${failed.length === 1 ? 'it is' : 'they are'} (${names(failed)}): each one’s reason and fix are under Build; \`frontera local dev\` changes nothing until ${failed.length === 1 ? 'its file changes' : 'their files change'}${notTheFile.length ? `, except ${names(notTheFile)}: ${notTheFile.length === 1 ? 'it applies' : 'they apply'} once the runtime is fixed as ${notTheFile.length === 1 ? 'its' : 'their'} fix says` : ''}`)
|
|
322
|
+
if (waiting.length) lines.push(` ${waiting.length} wait${waiting.length === 1 ? 's' : ''} for another item (${names(waiting)}): ${waiting.length === 1 ? 'it applies' : 'they apply'} once that one does (under Build)`)
|
|
323
|
+
return lines
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A Custom App's one status, said alike by `frontera local status`, Forge
|
|
328
|
+
* and Studio: its code deployed to this runtime; built (its `out/`) and not
|
|
329
|
+
* deployed; not built; not in the project (pulled without it); or none at
|
|
330
|
+
* all (it was never deployed in the workspace). Each but the first says the
|
|
331
|
+
* command that moves it on.
|
|
332
|
+
*/
|
|
333
|
+
export type AppStatus = 'deployed' | 'built' | 'not-built' | 'no-code' | 'not-pulled'
|
|
334
|
+
|
|
335
|
+
export const APP_STATUS_LABEL: Record<AppStatus, string> = {
|
|
336
|
+
deployed: 'Deployed to this runtime',
|
|
337
|
+
built: 'Built, not deployed',
|
|
338
|
+
'not-built': 'Not built',
|
|
339
|
+
'no-code': 'No code',
|
|
340
|
+
'not-pulled': 'Code not pulled',
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
type AppStatusRow = ItemBuild & { appStatus: AppStatus; app?: CustomAppFolder; notOnRuntime?: string }
|
|
344
|
+
|
|
345
|
+
/** Why the runtime doesn't have the deployment the folder names, or null when it has it (or wasn't asked). */
|
|
346
|
+
function notOnRuntimeReason(app: CustomAppFolder, live: LiveApp | undefined): string | null {
|
|
347
|
+
if (app.away) return `its deployment ${app.away.deployment} went with the database restore (${app.away.restoredAt.slice(0, 16).replace('T', ' ')})${app.deployment ? `; the runtime has ${app.deployment.id}` : ''}`
|
|
348
|
+
if (!app.deployment || !live) return null
|
|
349
|
+
if (!live.app) return `the runtime has no App by this folder's id, so deployment ${app.deployment.id} isn't there (a restore of its database, or a reset?)`
|
|
350
|
+
if (live.deployment === false) return `the runtime's App has no deployment ${app.deployment.id} (a restore of its database?)`
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** A dev server that could not open the browser's session: said with what fixes it. */
|
|
355
|
+
function devSessionWords(app: CustomAppFolder): string {
|
|
356
|
+
return app.devSessionError ? `; its dev server at ${app.devServer} couldn't open a session (${app.devSessionError}): reload the page, and if it still fails, restart it (\`app_dev\` stop, then start)` : ''
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** A Custom App with its code in the project: deployed here (and whether its code moved since), built, or not built. */
|
|
360
|
+
export function customAppRow(app: CustomAppFolder, live?: LiveApp): AppStatusRow {
|
|
361
|
+
const dev = app.devServer ? `; its dev server runs at ${app.devServer}${devSessionWords(app)}` : ''
|
|
362
|
+
const gone = notOnRuntimeReason(app, live)
|
|
363
|
+
if (gone) {
|
|
364
|
+
const fix = `\`frontera app deploy\` in ${app.dir} deploys it again (\`frontera local dev\` does after a restore)`
|
|
365
|
+
return { state: 'not-applied', appStatus: app.built ? 'built' : 'not-built', label: 'Not on the runtime', reason: `Not on the runtime: ${gone}${dev}`, adaptations: [], fix, notOnRuntime: gone }
|
|
366
|
+
}
|
|
367
|
+
if (app.deployment) {
|
|
368
|
+
const moved = app.changedSinceDeploy
|
|
369
|
+
return { state: 'built', appStatus: 'deployed', label: APP_STATUS_LABEL.deployed, reason: `Deployment ${app.deployment.id}${moved ? ', and its code changed since' : ''}${dev}`, adaptations: [], ...(moved ? { fix: `\`frontera app deploy\` in ${app.dir} deploys the change` } : {}) }
|
|
370
|
+
}
|
|
371
|
+
const pulled = app.pulled ? `Pulled at version ${app.pulled.version}; ` : ''
|
|
372
|
+
if (app.built) return { state: 'not-applied', appStatus: 'built', label: APP_STATUS_LABEL.built, reason: `${pulled}${pulled ? 'its' : 'Its'} build is in ${app.dir}/out${dev}`, adaptations: [], fix: `\`frontera app deploy\` in ${app.dir}` }
|
|
373
|
+
return { state: 'not-applied', appStatus: 'not-built', label: APP_STATUS_LABEL['not-built'], reason: `${pulled}${pulled ? 'its' : 'Its'} code is in ${app.dir}${dev || ': `frontera app dev` there runs it against this runtime'}`, adaptations: [], fix: `\`frontera app deploy\` in ${app.dir} builds and deploys it` }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Any Custom App the project names, by where its code is: in its folder, not pulled, or none. */
|
|
377
|
+
export function appStatusRow(root: string, code: AppCodeState, live: ReadonlyMap<string, LiveApp> | null = null): AppStatusRow {
|
|
378
|
+
if (code.code === 'folder') {
|
|
379
|
+
const app = customAppFolder(root, code.dir)
|
|
380
|
+
return { ...customAppRow(app, live?.get(app.name)), app }
|
|
381
|
+
}
|
|
382
|
+
if (code.code === 'not-pulled') {
|
|
383
|
+
return { state: 'not-applied', appStatus: 'not-pulled', label: APP_STATUS_LABEL['not-pulled'], reason: `Its code (version ${code.version}) isn’t in the project; ${appPath(code.slug)} is what the App is`, adaptations: [], fix: `\`frontera local sync pull app/${code.slug}\` brings it into apps/${code.slug}/` }
|
|
384
|
+
}
|
|
385
|
+
return { state: 'not-applied', appStatus: 'no-code', label: APP_STATUS_LABEL['no-code'], reason: `${appPath(code.slug)} describes it only: it was never deployed in the workspace, so it has no code`, adaptations: [], fix: '`frontera app init` in apps/ starts its code' }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** A Custom App counted by its own status, apart from what `local dev` applies: one whose code isn't deployed here. */
|
|
389
|
+
export const appNotDeployed = (r: Pick<BuildRow, 'appStatus' | 'state'>): boolean => !!r.appStatus && r.appStatus !== 'deployed' && r.state === 'not-applied'
|
|
390
|
+
|
|
391
|
+
/** "2 Custom Apps not deployed here (1 built, not deployed, 1 no code)": null when every App is deployed, or there is none. */
|
|
392
|
+
export function appsNotDeployedText(apps: Partial<Record<AppStatus, number>>): string | null {
|
|
393
|
+
const parts = (['built', 'not-built', 'not-pulled', 'no-code'] as const).filter((s) => apps[s]).map((s) => `${apps[s]} ${APP_STATUS_LABEL[s].charAt(0).toLowerCase()}${APP_STATUS_LABEL[s].slice(1)}`)
|
|
394
|
+
const n = (['built', 'not-built', 'not-pulled', 'no-code'] as const).reduce((sum, s) => sum + (apps[s] ?? 0), 0)
|
|
395
|
+
return n ? `${n} Custom App${n === 1 ? '' : 's'} not deployed here (${parts.join(', ')})` : null
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** The Custom Apps in a line, each with its status and what moves it on; null when the project has none. */
|
|
399
|
+
export function appsStatusText(rows: BuildRow[]): string | null {
|
|
400
|
+
const apps = rows.filter((r) => r.appStatus)
|
|
401
|
+
if (!apps.length) return null
|
|
402
|
+
return apps
|
|
403
|
+
.map((r) => {
|
|
404
|
+
const name = r.ref.slice('app/'.length)
|
|
405
|
+
// A save of what the App is that failed here is its status until fixed, with its reason.
|
|
406
|
+
if (r.state !== 'not-applied' && r.state !== 'built') return `${name} ${r.label.toLowerCase()}${r.reason ? ` (${r.reason})` : ''}${r.fix ? `: ${r.fix}` : ''}`
|
|
407
|
+
if (r.notOnRuntime) return `${name} not on the runtime (${r.notOnRuntime})${r.app?.devSessionError ? `, its dev server can't open a session: restart it` : ''}${r.fix ? `: ${r.fix}` : ''}`
|
|
408
|
+
const status = APP_STATUS_LABEL[r.appStatus!].toLowerCase()
|
|
409
|
+
const d = r.app?.deployment
|
|
410
|
+
const deployed = d ? ` (${d.id}${d.at ? `, ${d.at.slice(0, 16).replace('T', ' ')}` : ''})${r.app!.changedSinceDeploy ? ', code changed since' : ''}` : ''
|
|
411
|
+
const pulled = r.app?.pulled && !d ? `, pulled at version ${r.app.pulled.version}` : ''
|
|
412
|
+
return `${name} ${status}${deployed}${pulled}${r.app?.devServer ? `, dev server ${r.app.devServer}${r.app.devSessionError ? ` (it couldn't open a session: reload the page, else restart it)` : ''}` : ''}${r.fix ? `: ${r.fix}` : ''}`
|
|
413
|
+
})
|
|
414
|
+
.join('; ')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The Blueprint's folders whose files are rows of their own, and the reference each is named by. */
|
|
418
|
+
const BLUEPRINT_PARTS: Array<[string, string]> = [
|
|
419
|
+
['link-types', 'linkType'],
|
|
420
|
+
['metrics', 'metric'],
|
|
421
|
+
['semantic-types', 'semanticType'],
|
|
422
|
+
['shared-fields', 'sharedField'],
|
|
423
|
+
['object-sets', 'objectSet'],
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
/** The names of a Blueprint folder's files: each file's apiName (an object set's file name). */
|
|
427
|
+
function blueprintNames(root: string, folder: string): string[] {
|
|
428
|
+
return entries(join(root, 'blueprint', folder))
|
|
429
|
+
.filter((f) => /\.(ya?ml|json)$/.test(f))
|
|
430
|
+
.map((f) => f.replace(/\.(ya?ml|json)$/, ''))
|
|
431
|
+
.sort()
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** One governed Action's row, from how the last Blueprint build deployed it. */
|
|
435
|
+
function actionRow(build: { status: string; reason?: string; refused?: boolean } | undefined, whole: Omit<ItemBuild, 'adaptations'>): ItemBuild {
|
|
436
|
+
if (!build) return { ...whole, ...(whole.state === 'built' ? { state: 'not-applied' as const, label: 'Not deployed yet' } : {}), adaptations: [] }
|
|
437
|
+
if (build.status === 'runs') return { state: 'built', label: 'Runs here', reason: null, adaptations: [] }
|
|
438
|
+
if (build.status === 'switched-off') {
|
|
439
|
+
const reason = `Switched off here: ${build.reason ?? 'it changes a system outside the local runtime'}`
|
|
440
|
+
return { state: 'stubbed', label: 'Switched off here', reason, adaptations: [reason] }
|
|
441
|
+
}
|
|
442
|
+
// The runtime refused the Action's own file: a failure to fix, with the runtime's words.
|
|
443
|
+
if (build.status === 'left-off' && build.refused) return { state: 'failed', label: 'Left off: the runtime refused it', reason: (build.reason ?? 'the local runtime refused it').replace(/^the local runtime refused it: /, ''), adaptations: [] }
|
|
444
|
+
if (build.status === 'left-off') {
|
|
445
|
+
const reason = `Left off here: ${build.reason ?? 'its object type is left off'}`
|
|
446
|
+
return { state: 'adapted', label: 'Left off here', reason, adaptations: [reason] }
|
|
447
|
+
}
|
|
448
|
+
return { state: 'failed', label: 'Didn’t deploy', reason: build.reason ?? null, adaptations: [] }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** A failure from the last apply whose files changed since: still failed, said as at its last apply. */
|
|
452
|
+
function changedSinceFailure(row: ItemBuild): ItemBuild {
|
|
453
|
+
return { ...row, reason: `${row.reason ?? 'The runtime refused it'} (at its last apply; its files changed since, and \`frontera local dev\` applies it again)` }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* One governed Action's row from its deployment on the runtime. One the last
|
|
458
|
+
* build switched off or left off by design keeps that row when the runtime
|
|
459
|
+
* agrees; anything else not running is a failure, said with the runtime's state.
|
|
460
|
+
*/
|
|
461
|
+
export function liveActionRow(live: LiveAction | undefined, build: { status: string; reason?: string; refused?: boolean } | undefined, whole: Omit<ItemBuild, 'adaptations'>): ItemBuild {
|
|
462
|
+
if (build && (build.status === 'switched-off' || build.status === 'left-off') && (!live || live.deploymentStatus !== 'active')) return actionRow(build, whole)
|
|
463
|
+
if (!live) return whole.state === 'built' ? { state: 'not-applied', label: 'Not published here', reason: null, adaptations: [] } : { ...whole, adaptations: [] }
|
|
464
|
+
if (live.deploymentStatus === 'active' && (!live.availability || live.availability === 'available')) return { state: 'built', label: 'Runs here', reason: null, adaptations: [] }
|
|
465
|
+
const why = `Its deployment here is ${live.deploymentStatus}${live.availability && live.availability !== live.deploymentStatus ? ` (${live.availability})` : ''}${live.reason ? `: ${live.reason}` : ''}; \`frontera local dev\` deploys it again`
|
|
466
|
+
return { state: 'failed', label: 'Doesn’t run here', reason: build?.status === 'failed' && build.reason ? `${why} (last apply: ${build.reason})` : why, adaptations: [] }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** The rows by kind, in a line: "3 Agents, 6 object types, 6 datasets, 6 link types…". */
|
|
470
|
+
export function itemKindsText(rows: BuildRow[]): string {
|
|
471
|
+
const WORDS: Record<string, [string, string]> = {
|
|
472
|
+
agent: ['Agent', 'Agents'], workflow: ['workflow', 'workflows'], function: ['Function', 'Functions'], dataset: ['dataset', 'datasets'],
|
|
473
|
+
objectType: ['object type', 'object types'], linkType: ['link type', 'link types'], metric: ['metric', 'metrics'], semanticType: ['semantic type', 'semantic types'],
|
|
474
|
+
sharedField: ['shared field', 'shared fields'], objectSet: ['saved object set', 'saved object sets'], action: ['governed Action', 'governed Actions'],
|
|
475
|
+
knowledge: ['knowledge base', 'knowledge bases'], skill: ['skill', 'skills'], plugin: ['Plugin', 'Plugins'], theme: ['slide theme', 'slide themes'],
|
|
476
|
+
'chat-app': ['Chat App', 'Chat Apps'], app: ['Custom App', 'Custom Apps'],
|
|
477
|
+
}
|
|
478
|
+
const counts = new Map<string, number>()
|
|
479
|
+
for (const r of rows) {
|
|
480
|
+
const kind = r.ref.includes('/') ? r.ref.slice(0, r.ref.indexOf('/')) : r.ref
|
|
481
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1)
|
|
482
|
+
}
|
|
483
|
+
return `${rows.length} in all: ${[...counts].map(([k, n]) => `${n} ${(WORDS[k] ?? [k, k])[n === 1 ? 0 : 1]}`).join(', ')}`
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** The saved object sets in a line, each with its id when known: "open-work-orders 48536fc6-…"; null when the project has none. */
|
|
487
|
+
export function objectSetsStatusText(rows: BuildRow[]): string | null {
|
|
488
|
+
const sets = rows.filter((r) => r.ref.startsWith('objectSet/'))
|
|
489
|
+
if (!sets.length) return null
|
|
490
|
+
return sets.map((r) => `${r.ref.slice('objectSet/'.length)} ${r.id ?? `(${r.label.toLowerCase()}, no id yet)`}`).join('; ')
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The governed Actions in a line: how many, and how each was deployed here;
|
|
495
|
+
* `none` when the project has none, which the status says outright.
|
|
496
|
+
*/
|
|
497
|
+
export function actionsStatusText(rows: BuildRow[]): string {
|
|
498
|
+
const actions = rows.filter((r) => r.ref.startsWith('action/'))
|
|
499
|
+
if (!actions.length) return 'none (blueprint/actions/ is empty: nothing writes the Blueprint’s records)'
|
|
500
|
+
const by = (state: ItemBuildState) => actions.filter((a) => a.state === state).map((a) => a.ref.slice('action/'.length))
|
|
501
|
+
const parts = [
|
|
502
|
+
by('built').length ? `${by('built').length} run here` : '',
|
|
503
|
+
by('stubbed').length ? `${by('stubbed').length} switched off (${by('stubbed').join(', ')})` : '',
|
|
504
|
+
by('adapted').length ? `${by('adapted').length} left off (${by('adapted').join(', ')})` : '',
|
|
505
|
+
by('failed').length ? `${by('failed').length} didn’t deploy (${by('failed').join(', ')})` : '',
|
|
506
|
+
by('blocked').length ? `${by('blocked').length} blocked (${by('blocked').join(', ')})` : '',
|
|
507
|
+
by('not-applied').length ? `${by('not-applied').length} not deployed yet (${by('not-applied').join(', ')})` : '',
|
|
508
|
+
].filter(Boolean)
|
|
509
|
+
return `${actions.length}: ${parts.join(', ')}`
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The project's build in a few numbers, counted over exactly the rows
|
|
514
|
+
* `projectBuildRows` gives, so a count always matches the list beside it.
|
|
515
|
+
* Null before the runtime first started, or when there is nothing.
|
|
516
|
+
*/
|
|
517
|
+
export function projectBuildSummary(root: string, id = '', live: ReadonlyMap<string, LiveAction> | null = null, secrets: SecretPresence | null = null, liveApps: ReadonlyMap<string, LiveApp> | null = null): ProjectBuildSummary | null {
|
|
518
|
+
const rows = projectBuildRows(root, id, live, null, secrets, liveApps)
|
|
519
|
+
if (!rows?.length) return null
|
|
520
|
+
// A Custom App whose code isn't deployed here is counted by its own status: `local dev` doesn't deploy it.
|
|
521
|
+
const states = rows.filter((r) => !appNotDeployed(r)).map((r) => r.state)
|
|
90
522
|
const counts = buildCounts(states)
|
|
523
|
+
const apps: Partial<Record<AppStatus, number>> = {}
|
|
524
|
+
for (const r of rows) if (appNotDeployed(r)) apps[r.appStatus!] = (apps[r.appStatus!] ?? 0) + 1
|
|
91
525
|
const org = localOrganizationId(root)
|
|
92
526
|
const settings = org ? loadSettingsDifferences(projectOf(root, id), org) : []
|
|
93
|
-
const problems = counts.blocked + counts.failed
|
|
94
|
-
const
|
|
527
|
+
const problems = counts.blocked + counts.failed + counts['needs-setup']
|
|
528
|
+
const appsLine = appsNotDeployedText(apps)
|
|
529
|
+
const line = [buildSummaryLine(counts), appsLine ?? '', settings.length ? `${settings.length} setting${settings.length === 1 ? ' differs' : 's differ'} from the workspace` : ''].filter(Boolean).join(' · ')
|
|
95
530
|
const headline = problems ? `${problems} problem${problems === 1 ? '' : 's'}` : counts['not-applied'] ? `${counts['not-applied']} not applied yet` : `All ${states.length} built`
|
|
96
|
-
|
|
531
|
+
const settingDetails = settings.map((l) => {
|
|
532
|
+
const e = settingEffect(l)
|
|
533
|
+
return { line: l, effect: e?.effect ?? null, align: e?.align ?? null }
|
|
534
|
+
})
|
|
535
|
+
return { line, headline, problems, counts, apps, settings, settingDetails }
|
|
97
536
|
}
|
|
98
537
|
|
|
99
|
-
/** The CLI's line for the build: the problems first, then the rest, short ("
|
|
538
|
+
/** The CLI's line for the build: the problems first, then the rest, short ("3 problems: 1 failed, 1 blocked, 1 needs setup · 7 built, 3 adapted"). */
|
|
100
539
|
export function buildStatusText(summary: ProjectBuildSummary): string {
|
|
101
540
|
const c = summary.counts
|
|
102
|
-
const problems = [c.failed ? `${c.failed} failed` : '', c.blocked ? `${c.blocked} blocked` : ''].filter(Boolean).join(', ')
|
|
541
|
+
const problems = [c.failed ? `${c.failed} failed` : '', c.blocked ? `${c.blocked} blocked` : '', c['needs-setup'] ? `${c['needs-setup']} need${c['needs-setup'] === 1 ? 's' : ''} setup` : ''].filter(Boolean).join(', ')
|
|
103
542
|
const rest = [c.built ? `${c.built} built` : '', c.adapted ? `${c.adapted} adapted` : '', c.stubbed ? `${c.stubbed} stubbed` : '', c['not-applied'] ? `${c['not-applied']} not applied yet` : ''].filter(Boolean).join(', ')
|
|
104
543
|
const settings = summary.settings.length ? `${summary.settings.length} setting${summary.settings.length === 1 ? ' differs' : 's differ'} from the workspace` : ''
|
|
105
|
-
|
|
544
|
+
const apps = appsNotDeployedText(summary.apps ?? {}) ?? ''
|
|
545
|
+
return [summary.problems ? `${summary.headline}: ${problems}` : rest, summary.problems ? rest : '', apps, settings].filter(Boolean).join(' · ')
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* What the linked workspace lists that the project never pulled, by kind,
|
|
550
|
+
* with the command that brings it: an App the pull index names but no
|
|
551
|
+
* `apps/<slug>.json` holds is said, not silent. Datasets (the workspace's
|
|
552
|
+
* records) never come. Empty for a project with no fetched workspace.
|
|
553
|
+
*/
|
|
554
|
+
export function notPulledLines(root: string): string[] {
|
|
555
|
+
const snap = readSnapshot(root)
|
|
556
|
+
if (!snap) return []
|
|
557
|
+
const held = new Set(Object.keys(readLock(root)?.items ?? {}))
|
|
558
|
+
const missing = Object.keys(snap.items).filter((ref) => !held.has(ref) && !ref.startsWith('dataset/') && !ref.startsWith('lessons/') && !ref.startsWith('blueprint/'))
|
|
559
|
+
if (!missing.length) return []
|
|
560
|
+
const byKind = new Map<string, string[]>()
|
|
561
|
+
for (const ref of missing.sort()) {
|
|
562
|
+
const kind = ref.slice(0, ref.indexOf('/'))
|
|
563
|
+
byKind.set(kind, [...(byKind.get(kind) ?? []), ref])
|
|
564
|
+
}
|
|
565
|
+
// `--kinds` takes the plural, but knowledge is its own.
|
|
566
|
+
const flagWord = (kind: string) => (kind === 'knowledge' ? kind : `${kind}s`)
|
|
567
|
+
return [...byKind].map(([kind, refs]) => `${refs.length} ${refs.length === 1 ? kind : flagWord(kind)} (${refs.slice(0, 4).join(', ')}${refs.length > 4 ? ', …' : ''}): \`frontera local sync pull ${refs.length > 4 ? `--kinds ${flagWord(kind)}` : refs.join(' ')}\``)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** The knowledge bases with no documents here, each with the command that brings them (`emptyKnowledgeNote`). */
|
|
571
|
+
export function emptyKnowledgeLines(rows: BuildRow[]): string[] {
|
|
572
|
+
return rows.filter((r) => r.ref.startsWith('knowledge/') && /has no documents/.test(r.reason ?? '')).map((r) => r.reason!)
|
|
106
573
|
}
|