@frontera-sdk/cli 1.52.39 → 1.52.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +49 -5
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/draft-changes.ts +1 -1
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +332 -32
- package/src/commands/blueprint/editable.ts +3 -3
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +876 -89
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +321 -14
- package/src/commands/local/sync.ts +103 -6
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +54 -10
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/claude-commands.ts +11 -6
- package/src/forge/claude-skills.ts +4 -4
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +113 -2
- package/src/kit.ts +1 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +212 -22
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +181 -8
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +347 -36
- package/src/local/local-dev-sync.ts +141 -12
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-documents.ts +1 -1
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-lessons.ts +1 -1
- package/src/local/local-library.ts +1 -1
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +206 -8
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +143 -18
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +41 -11
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +3 -3
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-merge.ts +1 -1
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +19 -8
- package/src/local/local-sync-push.ts +15 -12
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +349 -38
- package/src/local/local-sync-snapshot.ts +18 -13
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/scopes.ts +1 -1
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +17 -14
package/src/local/local-apps.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, join, relative } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import type { RemoteItem } from './local-sync-plan'
|
|
5
|
+
import { readDevSessionState } from './local-app-dev-session'
|
|
6
|
+
import { isAlive } from './local-state'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Custom Apps: the workspace's code Apps (a Chat App is `chat-apps/`). A
|
|
@@ -11,9 +13,10 @@ import type { RemoteItem } from './local-sync-plan'
|
|
|
11
13
|
* with its switch. `local dev` makes the App on the local runtime and binds
|
|
12
14
|
* those Functions, so the App's calls to them are allowed here as there.
|
|
13
15
|
*
|
|
14
|
-
* Its code is its own project
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
16
|
+
* Its code is its own project: a pull brings the deployed version's source
|
|
17
|
+
* into `apps/<slug>/` (`local-app-code.ts`) unless `--no-app-code` or the
|
|
18
|
+
* folder is there, and `local dev` deploys it here on its own; its
|
|
19
|
+
* portability says so (Adapted). Who may open it, its
|
|
17
20
|
* sign-in (audiences, roles, keys) and its addresses are the platform's:
|
|
18
21
|
* people and deployment (sync v2 design §8). Never proposed back.
|
|
19
22
|
*/
|
|
@@ -45,7 +48,7 @@ export interface AppFile {
|
|
|
45
48
|
export const appPath = (slug: string) => `apps/${slug}.json`
|
|
46
49
|
|
|
47
50
|
/** Why an App is Adapted here, as the build and the Pull dialog say it. */
|
|
48
|
-
export const APP_WITHOUT_CODE = 'Saved here with the Functions it may start
|
|
51
|
+
export const APP_WITHOUT_CODE = 'Saved here with the Functions it may start; its code is its own project in apps/<slug>/, which a pull brings unless --no-app-code, where `frontera app dev` runs it against this runtime and `frontera app deploy` deploys it here'
|
|
49
52
|
|
|
50
53
|
const strings = (v: unknown): string[] | undefined => (Array.isArray(v) && v.every((x) => typeof x === 'string') && v.length ? (v as string[]) : undefined)
|
|
51
54
|
|
|
@@ -71,7 +74,7 @@ export function readApps(rows: AppRow[], wanted: (ref: string) => boolean): Remo
|
|
|
71
74
|
if (!wanted(ref)) continue
|
|
72
75
|
const doc = appDocument(row)
|
|
73
76
|
const notes = [
|
|
74
|
-
row.version ? `Its code (version ${row.version}) is its own project
|
|
77
|
+
row.version ? `Its code (version ${row.version}) is its own project: a pull brings it into apps/${row.slug}/ unless --no-app-code or that folder is there; this file is what the App is and the Functions it may start` : 'Never deployed in the workspace; this file is what the App is and the Functions it may start',
|
|
75
78
|
row.hosting === 'external' ? 'Hosted on its own domain: its sign-in, roles and keys stay on the platform' : 'Who may open it and its addresses stay on the platform',
|
|
76
79
|
]
|
|
77
80
|
out.push({ ref, kind: 'app', name: row.slug, title: row.name, files: { [appPath(row.slug)]: `${JSON.stringify(doc, null, 2)}\n` }, unresolved: [], notes, requires: doc.functions.map((f) => f.function), id: row.id, ...(row.version ? { version: `v${row.version}` } : {}) })
|
|
@@ -151,6 +154,176 @@ export async function applyApp(root: string, slug: string, call: Call, provides:
|
|
|
151
154
|
const gone = await call('DELETE', `/v1/functions/${encodeURIComponent(fn)}/app-bindings/${encodeURIComponent(slug)}`)
|
|
152
155
|
if (gone.status >= 400 && gone.status !== 404) lines.push(` warning: Function ${fn} could not be unbound from it (${errorOf(gone)})`)
|
|
153
156
|
}
|
|
154
|
-
lines.unshift(` saved App ${name}${named.size ? ` (may start ${[...named].sort().join(', ')})` : ''}`)
|
|
157
|
+
lines.unshift(` saved App ${name}${named.size ? ` (may start ${[...named].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).join(', ')})` : ''}`)
|
|
155
158
|
return { ok: true, output: lines.join('\n') }
|
|
156
159
|
}
|
|
160
|
+
|
|
161
|
+
// ── A Custom App's code, in the project ─────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/** Folders an App's code is never in: dependencies, build output, the project's state and its other kinds. */
|
|
164
|
+
const NOT_APP_DIRS = new Set(['node_modules', '.git', '.frontera', '.next', 'dist', 'out', 'build', 'vendor', 'fixtures', 'datasets', 'blueprint', 'agents', 'workflows', 'functions', 'knowledge', 'skills', 'themes', 'mcp', 'chat-apps'])
|
|
165
|
+
|
|
166
|
+
export function readPackage(dir: string): { scripts?: Record<string, string>; frontera?: unknown } | null {
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { scripts?: Record<string, string>; frontera?: unknown }
|
|
169
|
+
} catch {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A folder that is a Custom App's code: its manifest (`frontera.config.json`, or `frontera` in package.json) beside a package.json. */
|
|
175
|
+
function isAppFolder(dir: string): boolean {
|
|
176
|
+
if (!existsSync(join(dir, 'package.json'))) return false
|
|
177
|
+
if (existsSync(join(dir, 'frontera.config.json'))) return true
|
|
178
|
+
return !!readPackage(dir)?.frontera
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Each Custom App's code folder, relative to the project: apps/<name>/ with a
|
|
183
|
+
* package.json, and any other folder an App was made in (`frontera app init
|
|
184
|
+
* <name>` at the project's root makes ./<name>/), found by its manifest. The
|
|
185
|
+
* project's own root is not one.
|
|
186
|
+
*/
|
|
187
|
+
export function customAppFolders(root: string): string[] {
|
|
188
|
+
const out: string[] = []
|
|
189
|
+
const walk = (dir: string, depth: number) => {
|
|
190
|
+
let names: string[]
|
|
191
|
+
try {
|
|
192
|
+
names = readdirSync(dir).sort()
|
|
193
|
+
} catch {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
for (const name of names) {
|
|
197
|
+
if (name.startsWith('.') || NOT_APP_DIRS.has(name)) continue
|
|
198
|
+
const path = join(dir, name)
|
|
199
|
+
try {
|
|
200
|
+
if (!lstatSync(path).isDirectory()) continue
|
|
201
|
+
} catch {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
// apps/<name>/ with a package.json is an App's code, manifest or not, as it always was.
|
|
205
|
+
if (isAppFolder(path) || (dir === join(root, 'apps') && existsSync(join(path, 'package.json')))) {
|
|
206
|
+
out.push(relative(root, path))
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (depth < 2) walk(path, depth + 1)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
walk(root, 0)
|
|
213
|
+
return out
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Where each Custom App's code is, as Studio says how to work on it: `folder`
|
|
218
|
+
* (its code folder, where `frontera app dev` runs it), `not-pulled` (the
|
|
219
|
+
* workspace deployed it, but its code isn't in the project: a pull brings
|
|
220
|
+
* it), or `none` (its file only describes it: it was never deployed, so no
|
|
221
|
+
* code exists, and `frontera app init` starts one). By the App's slug, for
|
|
222
|
+
* every apps/<slug>.json and every code folder.
|
|
223
|
+
*/
|
|
224
|
+
export type AppCodeState = { slug: string; code: 'folder'; dir: string } | { slug: string; code: 'not-pulled'; version: string } | { slug: string; code: 'none' }
|
|
225
|
+
|
|
226
|
+
export function appCodeStates(root: string): AppCodeState[] {
|
|
227
|
+
const folders = new Map(customAppFolders(root).map((dir) => [basename(dir), dir] as const))
|
|
228
|
+
const described = existsSync(join(root, 'apps')) ? readdirSync(join(root, 'apps')).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -'.json'.length)) : []
|
|
229
|
+
const slugs = [...new Set([...described, ...folders.keys()])].sort()
|
|
230
|
+
return slugs.map((slug): AppCodeState => {
|
|
231
|
+
const dir = folders.get(slug)
|
|
232
|
+
if (dir) return { slug, code: 'folder', dir }
|
|
233
|
+
let version: unknown
|
|
234
|
+
try {
|
|
235
|
+
version = (JSON.parse(readFileSync(join(root, appPath(slug)), 'utf8')) as { version?: unknown }).version
|
|
236
|
+
} catch {
|
|
237
|
+
version = undefined
|
|
238
|
+
}
|
|
239
|
+
return typeof version === 'string' && version ? { slug, code: 'not-pulled', version } : { slug, code: 'none' }
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** What the project knows about one Custom App's code: its last deployment here and its dev server. */
|
|
244
|
+
export interface CustomAppFolder {
|
|
245
|
+
/** Its folder's name: the row's name (`app/<name>`). */
|
|
246
|
+
name: string
|
|
247
|
+
/** Its folder, relative to the project. */
|
|
248
|
+
dir: string
|
|
249
|
+
/** The last `frontera app deploy` from this folder, when it made one: its id and when. */
|
|
250
|
+
deployment: { id: string; at: string | null } | null
|
|
251
|
+
/** The version its code was pulled at (a pull of the App, or `frontera app pull`), when it was: not a deployment here. */
|
|
252
|
+
pulled: { version: string; at: string | null } | null
|
|
253
|
+
/** Its code changed after that deployment. */
|
|
254
|
+
changedSinceDeploy: boolean
|
|
255
|
+
/** Its build is in the folder (`out/`, what `frontera app deploy` uploads), deployed or not. */
|
|
256
|
+
built: boolean
|
|
257
|
+
/** The dev server `frontera app dev` (or `app_dev`) runs for it, while it runs. */
|
|
258
|
+
devServer: string | null
|
|
259
|
+
/**
|
|
260
|
+
* A deployment from here that went with a database restore (`frontera local
|
|
261
|
+
* snapshot restore`, a `local check` rollback): the restored database does
|
|
262
|
+
* not have it, and the next `frontera local dev` deploys the App again.
|
|
263
|
+
*/
|
|
264
|
+
away?: { deployment: string; restoredAt: string }
|
|
265
|
+
/** The running dev server could not mint the browser's session the last time it tried: its words. */
|
|
266
|
+
devSessionError?: string
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function newestSource(dir: string): number {
|
|
270
|
+
let newest = 0
|
|
271
|
+
const walk = (at: string, depth: number) => {
|
|
272
|
+
let names: string[]
|
|
273
|
+
try {
|
|
274
|
+
names = readdirSync(at)
|
|
275
|
+
} catch {
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
for (const n of names) {
|
|
279
|
+
if (n.startsWith('.') || ['node_modules', 'out', 'dist', '.next', 'build'].includes(n)) continue
|
|
280
|
+
const path = join(at, n)
|
|
281
|
+
try {
|
|
282
|
+
const st = statSync(path)
|
|
283
|
+
if (st.isDirectory()) {
|
|
284
|
+
if (depth < 8) walk(path, depth + 1)
|
|
285
|
+
} else if (st.mtimeMs > newest && !/\.tsbuildinfo$/.test(n)) newest = st.mtimeMs
|
|
286
|
+
} catch {
|
|
287
|
+
// Gone while reading.
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
walk(dir, 0)
|
|
292
|
+
return newest
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function customAppFolder(root: string, dir: string): CustomAppFolder {
|
|
296
|
+
const name = basename(dir)
|
|
297
|
+
const statePath = join(root, dir, '.frontera', 'state.json')
|
|
298
|
+
let deployment: CustomAppFolder['deployment'] = null
|
|
299
|
+
let pulled: CustomAppFolder['pulled'] = null
|
|
300
|
+
let changedSinceDeploy = false
|
|
301
|
+
let away: CustomAppFolder['away']
|
|
302
|
+
try {
|
|
303
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8')) as { parentVersion?: unknown; pulledVersion?: unknown; restoredAway?: { deployment?: unknown; restoredAt?: unknown } }
|
|
304
|
+
const at = statSync(statePath).mtimeMs
|
|
305
|
+
const r = state.restoredAway
|
|
306
|
+
if (r && typeof r.deployment === 'string' && typeof r.restoredAt === 'string') away = { deployment: r.deployment, restoredAt: r.restoredAt }
|
|
307
|
+
if (typeof state.parentVersion === 'string' && state.parentVersion) {
|
|
308
|
+
deployment = { id: state.parentVersion, at: new Date(at).toISOString() }
|
|
309
|
+
changedSinceDeploy = newestSource(join(root, dir)) > at + 1000
|
|
310
|
+
}
|
|
311
|
+
if (typeof state.pulledVersion === 'string' && state.pulledVersion) pulled = { version: state.pulledVersion, at: new Date(at).toISOString() }
|
|
312
|
+
} catch {
|
|
313
|
+
// Never deployed from here.
|
|
314
|
+
}
|
|
315
|
+
let devServer: string | null = null
|
|
316
|
+
let devSessionError: string | undefined
|
|
317
|
+
try {
|
|
318
|
+
const dev = JSON.parse(readFileSync(join(root, '.frontera', 'local', 'app-dev', `${name}.json`), 'utf8')) as { pid?: unknown; url?: unknown; startedAt?: unknown }
|
|
319
|
+
if (typeof dev.url === 'string' && typeof dev.pid === 'number' && isAlive(dev.pid)) {
|
|
320
|
+
devServer = dev.url
|
|
321
|
+
// This server's own last session mint, not one an earlier server recorded.
|
|
322
|
+
const session = readDevSessionState(join(root, '.frontera', 'local'), name)
|
|
323
|
+
if (session && !session.ok && (typeof dev.startedAt !== 'string' || session.at >= dev.startedAt)) devSessionError = session.error ?? 'its session request failed'
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// Not running.
|
|
327
|
+
}
|
|
328
|
+
return { name, dir, deployment, pulled, changedSinceDeploy, built: existsSync(join(root, dir, 'out')), devServer, ...(away ? { away } : {}), ...(devSessionError ? { devSessionError } : {}) }
|
|
329
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { ActionBuild } from './local-actions'
|
|
5
|
+
import { ACTIVE_CATALOG } from './local-blueprint-schemas'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A Blueprint failure, said against the project's files.
|
|
9
|
+
*
|
|
10
|
+
* The runtime names a problem by where it sits in the draft definition it
|
|
11
|
+
* holds (`actions.1.concurrency.strategy`), which says nothing to whoever
|
|
12
|
+
* edits `blueprint/actions/resetLine.yaml`; and `blueprint apply` prints the
|
|
13
|
+
* same issue in its message and again in its hint. Here each indexed path
|
|
14
|
+
* becomes the file it came from and the field inside it, and a line that says
|
|
15
|
+
* what another already says is dropped.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The draft definition's arrays, and the `blueprint/` folder each one's files live in. */
|
|
19
|
+
const COLLECTIONS: Record<string, string> = {
|
|
20
|
+
actions: 'actions',
|
|
21
|
+
objects: 'object-types',
|
|
22
|
+
objectTypes: 'object-types',
|
|
23
|
+
relationships: 'link-types',
|
|
24
|
+
linkTypes: 'link-types',
|
|
25
|
+
metrics: 'metrics',
|
|
26
|
+
sharedProperties: 'shared-fields',
|
|
27
|
+
semanticTypes: 'semantic-types',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COLLECTION = Object.keys(COLLECTIONS).join('|')
|
|
31
|
+
|
|
32
|
+
/** `actions.1.x`, `actions[1].x`, `/actions/1/x`: a collection, an index, then the field path. */
|
|
33
|
+
const INDEXED = new RegExp(`(?<![A-Za-z0-9_])/?(${COLLECTION})(?:\\.|\\[|/)(\\d+)\\]?((?:(?:\\.|/)[A-Za-z0-9_]+|\\[\\d+\\])*)`, 'g')
|
|
34
|
+
|
|
35
|
+
/** The file a Blueprint artifact lives in, as the project holds it: `blueprint/actions/resetLine.yaml`. */
|
|
36
|
+
export function blueprintFile(root: string, folder: string, apiName: string): string {
|
|
37
|
+
for (const ext of ['yaml', 'yml', 'json']) {
|
|
38
|
+
const rel = join('blueprint', folder, `${apiName}.${ext}`)
|
|
39
|
+
if (existsSync(join(root, rel))) return rel
|
|
40
|
+
}
|
|
41
|
+
return join('blueprint', folder, `${apiName}.yaml`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `/concurrency/strategy` or `.inputs[0].valueType` → `concurrency.strategy`, `inputs.0.valueType`. */
|
|
45
|
+
function fieldPath(rest: string): string {
|
|
46
|
+
return rest
|
|
47
|
+
.replace(/\[(\d+)\]/g, '.$1')
|
|
48
|
+
.split(/[./]/)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join('.')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every indexed path in the text, named by the file it came from: the
|
|
55
|
+
* definition is the runtime's draft (`GET /v1/blueprint/lifecycle`), whose
|
|
56
|
+
* arrays the indices count into. A path whose index the definition does not
|
|
57
|
+
* have is left as it is.
|
|
58
|
+
*/
|
|
59
|
+
export function nameIndexedPaths(text: string, definition: unknown, root: string): string {
|
|
60
|
+
const def = (definition && typeof definition === 'object' ? definition : {}) as Record<string, unknown>
|
|
61
|
+
return text.replace(INDEXED, (whole, collection: string, index: string, rest: string) => {
|
|
62
|
+
const list = def[collection]
|
|
63
|
+
const entry = Array.isArray(list) ? (list[Number(index)] as { apiName?: unknown } | undefined) : undefined
|
|
64
|
+
const apiName = typeof entry?.apiName === 'string' ? entry.apiName : null
|
|
65
|
+
if (!apiName) return whole
|
|
66
|
+
const field = fieldPath(rest)
|
|
67
|
+
const file = blueprintFile(root, COLLECTIONS[collection]!, apiName)
|
|
68
|
+
return field ? `${file} (${field})` : file
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether the text names a definition array by index, so it is worth reading the draft to name it. */
|
|
73
|
+
export function hasIndexedPaths(text: string): boolean {
|
|
74
|
+
INDEXED.lastIndex = 0
|
|
75
|
+
const found = INDEXED.test(text)
|
|
76
|
+
INDEXED.lastIndex = 0
|
|
77
|
+
return found
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The words of a line, for telling two lines that say the same thing apart from two that do not. */
|
|
81
|
+
function gist(line: string): string {
|
|
82
|
+
return line
|
|
83
|
+
.replace(/^\s*(error|hint|warning)(\s*\[[^\]]*\])?:\s*/i, '')
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
86
|
+
.trim()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Lines with each thing said once: a line whose words another line already
|
|
91
|
+
* holds is dropped (the hint that repeats the message), and of two that
|
|
92
|
+
* overlap, the longer one stays where the first one was.
|
|
93
|
+
*/
|
|
94
|
+
export function dedupeLines(lines: string[]): string[] {
|
|
95
|
+
const kept: Array<{ line: string; gist: string }> = []
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const g = gist(line)
|
|
98
|
+
if (!g) continue
|
|
99
|
+
if (kept.some((k) => k.gist.includes(g))) continue
|
|
100
|
+
const shorter = kept.findIndex((k) => g.includes(k.gist))
|
|
101
|
+
if (shorter >= 0) {
|
|
102
|
+
kept[shorter] = { line, gist: g }
|
|
103
|
+
// A third line the new one also covers.
|
|
104
|
+
for (let i = kept.length - 1; i > shorter; i--) if (g.includes(kept[i]!.gist)) kept.splice(i, 1)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
kept.push({ line, gist: g })
|
|
108
|
+
}
|
|
109
|
+
return kept.map((k) => k.line)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The governed Actions that did not deploy, as the failure `local dev` reports:
|
|
114
|
+
* one line per Action, naming its file, then each failed check under it.
|
|
115
|
+
* Empty when every Action that runs here deployed.
|
|
116
|
+
*/
|
|
117
|
+
export function actionFailureLines(actions: Record<string, ActionBuild>, root: string): string[] {
|
|
118
|
+
const out: string[] = []
|
|
119
|
+
for (const [name, a] of Object.entries(actions)) {
|
|
120
|
+
if (a.status !== 'failed') continue
|
|
121
|
+
out.push(`governed Action ${name} (${blueprintFile(root, 'actions', name)}) didn’t deploy: ${a.reason ?? 'no reason given'}`)
|
|
122
|
+
for (const f of a.findings ?? []) out.push(` ✕ ${f}`)
|
|
123
|
+
}
|
|
124
|
+
return out
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The service's refusal of an Action's source system by a name the draft lacks. */
|
|
128
|
+
const UNKNOWN_SOURCE_SYSTEM = /authoritySourceSystem names a source system "([^"]*)", which is not on the draft/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* What to write instead when an Action names a source system the draft does
|
|
132
|
+
* not have: source systems have no file or command, so the one an Action that
|
|
133
|
+
* writes Blueprint records can name is `active-catalog`, which the service
|
|
134
|
+
* adds with the first object type. Null when the text is about something else.
|
|
135
|
+
*/
|
|
136
|
+
export function sourceSystemHint(text: string): string | null {
|
|
137
|
+
const m = UNKNOWN_SOURCE_SYSTEM.exec(text)
|
|
138
|
+
if (!m || m[1] === ACTIVE_CATALOG) {
|
|
139
|
+
return m ? `\`${ACTIVE_CATALOG}\` is added to the draft with its first object type: apply an object type before (or with) the Action.` : null
|
|
140
|
+
}
|
|
141
|
+
return `An Action that writes the Blueprint's own records names \`effect.authoritySourceSystem: ${ACTIVE_CATALOG}\`, or leaves the key out (source systems have no file or command of their own); "${m[1]}" is not one.`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** `blueprint apply`'s line naming the step the runtime refused: `Stopped at: create object-type Receipt`. */
|
|
145
|
+
const STOPPED_AT = /^\s*Stopped at: (create|update|delete) (semantic-type|shared-field|object-type|link-type|metric|action) ([A-Za-z][A-Za-z0-9_]*)\s*$/m
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The Blueprint artifact a failed `blueprint apply` stopped at, as
|
|
149
|
+
* `kind:apiName` (`object-type:Receipt`), with the runtime's reason: the
|
|
150
|
+
* line after the step's. Null when the output names no step (a failure
|
|
151
|
+
* before any write, or a CLI that does not say it), or when the draft moved
|
|
152
|
+
* under the apply (a conflict is not the file's fault).
|
|
153
|
+
*/
|
|
154
|
+
export function refusedArtifact(output: string): { key: string; reason: string } | null {
|
|
155
|
+
const m = STOPPED_AT.exec(output)
|
|
156
|
+
if (!m) return null
|
|
157
|
+
// Someone else changed the draft first (another apply, a person in Blueprint): the file is not
|
|
158
|
+
// what was refused, so nothing is left off for it.
|
|
159
|
+
if (/error \[(CONFLICT|DRAFT_CONFLICT|VALIDATION_STALE)\]/.test(output)) return null
|
|
160
|
+
const after = output.slice(m.index + m[0].length).split('\n').map((l) => l.trim()).filter(Boolean)
|
|
161
|
+
const at = after.findIndex((l) => !/^(hint|warning):/i.test(l) && !/^frontera\s/.test(l))
|
|
162
|
+
const reason = at >= 0 ? after[at]!.replace(/^error(\s*\[[^\]]*\])?:\s*/i, '') : 'the runtime refused it'
|
|
163
|
+
// The specifics travel in the error's hint (`blueprint apply` prints the service's details there:
|
|
164
|
+
// `businessOutcomes.0.expectedChanges.4.value — An optional input …`): said with the reason, so
|
|
165
|
+
// `local dev` and `status` never stop at "validation failed". A command to run is not a reason.
|
|
166
|
+
const specifics = at >= 0
|
|
167
|
+
? after.slice(at + 1)
|
|
168
|
+
.map((l) => l.replace(/^hint:\s*/i, ''))
|
|
169
|
+
.filter((l) => l && !/^frontera\s/.test(l) && !/^`?frontera\s/.test(l) && !/^warning:/i.test(l) && !/^(Applied before failing|Nothing was applied)/.test(l))
|
|
170
|
+
: []
|
|
171
|
+
const said = dedupeLines([reason, ...specifics])
|
|
172
|
+
// A detail the reason already carries (a service that names the issue in its message too) is said once.
|
|
173
|
+
const gistOf = (l: string) => (l.includes(' — ') ? l.slice(l.lastIndexOf(' — ') + 3) : l).replace(/[.]$/, '').trim()
|
|
174
|
+
const head = said[0] ?? reason
|
|
175
|
+
const fresh = said.slice(1).filter((l) => !head.includes(gistOf(l)))
|
|
176
|
+
const joined = fresh.length ? `${head.replace(/[.:]\s*$/, '')}: ${fresh.map((l) => l.replace(/[.]$/, '')).join('; ')}` : head
|
|
177
|
+
return { key: `${m[2]}:${m[3]}`, reason: joined }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A `✕ <folder>/<name>.yaml: <why>` line `blueprint plan`/`apply` prints for a rule an Action or metric file breaks. */
|
|
181
|
+
const RULE_LINE = /^\s*✕\s+(?:\S*\/)?(actions|metrics)\/([A-Za-z][A-Za-z0-9_]*)\.(?:ya?ml|json):\s*(.+?)\s*$/gm
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The Action and metric files `blueprint apply` refused before its first write
|
|
185
|
+
* (a broken rule: `actions/x.yaml: … optional with no default …`), each as
|
|
186
|
+
* `kind:apiName` with its reasons: `local dev` leaves each off and applies the
|
|
187
|
+
* rest, as it does for a step the runtime refused. Empty when the output is
|
|
188
|
+
* not such a refusal.
|
|
189
|
+
*/
|
|
190
|
+
export function refusedByRules(output: string): Array<{ key: string; reason: string }> {
|
|
191
|
+
if (!/error \[VALIDATION_ERROR\]/.test(output) || !/so nothing was (planned or )?applied/.test(output)) return []
|
|
192
|
+
const reasons = new Map<string, string[]>()
|
|
193
|
+
for (const m of output.matchAll(RULE_LINE)) {
|
|
194
|
+
const key = `${m[1] === 'actions' ? 'action' : 'metric'}:${m[2]}`
|
|
195
|
+
reasons.set(key, [...(reasons.get(key) ?? []), m[3]!])
|
|
196
|
+
}
|
|
197
|
+
return [...reasons].map(([key, why]) => ({ key, reason: why.join('; ') }))
|
|
198
|
+
}
|
|
@@ -199,12 +199,20 @@ const ACTION_LABEL: Record<ActionLocalStatus, string> = {
|
|
|
199
199
|
'not-deployed': 'Not deployed yet',
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** What the runtime said when it refused an Action's file, without the build record's lead-in. */
|
|
203
|
+
export function refusedReason(reason: string | undefined): string {
|
|
204
|
+
const words = (reason ?? 'the local runtime refused it').replace(/^the local runtime refused it: /, '')
|
|
205
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
206
|
+
}
|
|
207
|
+
|
|
202
208
|
/**
|
|
203
209
|
* How the local runtime holds one Action: what the last Blueprint build
|
|
204
210
|
* recorded for the files as they are, or, before one, what it will do.
|
|
205
211
|
*/
|
|
206
212
|
export function actionLocal(doc: Doc, built: BuildEntry['actions'] | null): OutlineAction['local'] {
|
|
207
213
|
const recorded = built?.[doc.apiName as string]
|
|
214
|
+
// Refused by the runtime (its own file is wrong), not left off by design: a failure to fix, with the runtime's words.
|
|
215
|
+
if (recorded?.status === 'left-off' && recorded.refused) return { status: 'failed', label: 'Refused by the runtime', reason: refusedReason(recorded.reason) }
|
|
208
216
|
if (recorded) return { status: recorded.status, label: ACTION_LABEL[recorded.status], reason: recorded.reason ?? null }
|
|
209
217
|
const fit = actionFit(doc)
|
|
210
218
|
return fit.runsLocally ? { status: 'not-deployed', label: ACTION_LABEL['not-deployed'], reason: null } : { status: 'switched-off', label: ACTION_LABEL['switched-off'], reason: fit.reason ?? null }
|
|
@@ -403,29 +411,34 @@ export interface BlueprintBuildState {
|
|
|
403
411
|
applied: boolean
|
|
404
412
|
/** The last Blueprint build failed for these files: its error. */
|
|
405
413
|
failed?: string | null
|
|
414
|
+
/** Each object type as the last build left it, when it was for these files: a type the runtime refused says so. */
|
|
415
|
+
objects?: BuildEntry['objects'] | null
|
|
406
416
|
}
|
|
407
417
|
|
|
408
418
|
/**
|
|
409
419
|
* The one thing worth saying about an object type on this runtime, most
|
|
410
420
|
* important first: platform only > the Blueprint failed to build > not
|
|
411
|
-
* applied yet > adapted (an Action
|
|
412
|
-
* off, every row read) > built.
|
|
421
|
+
* applied yet > adapted (an Action switched off here, a calculated field
|
|
422
|
+
* off, every row read) > built. An Action that didn't deploy is its own
|
|
423
|
+
* row, not its type's. `state` (or `true`) says the runtime holds the
|
|
413
424
|
* project's current Blueprint.
|
|
414
425
|
*/
|
|
415
|
-
export function objectStatus(o: Pick<ObjectOutline, 'fields' | 'actions' | 'local'
|
|
426
|
+
export function objectStatus(o: Pick<ObjectOutline, 'fields' | 'actions' | 'local'> & { apiName?: string }, state: boolean | BlueprintBuildState): ObjectStatus {
|
|
416
427
|
const s: BlueprintBuildState = typeof state === 'boolean' ? { applied: state } : state
|
|
417
428
|
if (o.local.leftOff) return { kind: 'platform-only', label: 'Platform only' }
|
|
418
429
|
if (s.failed) return { kind: 'failed', label: 'Failed', reason: s.failed }
|
|
430
|
+
// The runtime refused this type alone: it is left off, and the rest of the Blueprint built.
|
|
431
|
+
const refused = o.apiName ? s.objects?.[o.apiName]?.refused : undefined
|
|
432
|
+
if (refused) return { kind: 'failed', label: 'Left off: the runtime refused it', reason: refused }
|
|
419
433
|
if (!s.applied) return { kind: 'not-applied', label: 'Not applied yet' }
|
|
420
|
-
|
|
434
|
+
// A governed Action that didn't deploy is a failure of its own (`action/<name>`, a row of the
|
|
435
|
+
// build), so the type it acts on is not counted as failed a second time.
|
|
436
|
+
const idle = o.actions.filter((a) => a.local.status === 'switched-off')
|
|
421
437
|
if (idle.length) {
|
|
422
|
-
const
|
|
423
|
-
const label = failed.length
|
|
424
|
-
? failed.length === 1 ? 'An action didn’t deploy' : `${failed.length} actions didn’t deploy`
|
|
425
|
-
: idle.length === 1 ? '1 action switched off here' : `${idle.length} actions switched off here`
|
|
438
|
+
const label = idle.length === 1 ? '1 action switched off here' : `${idle.length} actions switched off here`
|
|
426
439
|
return { kind: 'adapted', label, reason: idle.map((a) => `${a.displayName}: ${a.local.reason ?? a.local.label}`).join('; ') }
|
|
427
440
|
}
|
|
428
|
-
if (o.local.mediaStub) return { kind: 'stubbed', label: 'Stubbed:
|
|
441
|
+
if (o.local.mediaStub) return { kind: 'stubbed', label: 'Stubbed: placeholder files', reason: `Its files stay on the platform: a stand-in Media Set, ${o.local.mediaStub}, holds placeholder files for them here` }
|
|
429
442
|
if (o.local.calculated.length) {
|
|
430
443
|
const names = o.local.calculated.map((api) => o.fields.find((f) => f.apiName === api)?.displayName ?? api)
|
|
431
444
|
return { kind: 'adapted', label: names.length === 1 ? `Adapted: ${inline(names[0]!)} off` : `Adapted: ${names.length} fields off` }
|
|
@@ -445,14 +458,20 @@ export function localOrganizationId(root: string): string | null {
|
|
|
445
458
|
}
|
|
446
459
|
|
|
447
460
|
/** The Blueprint's build for the files as they are: applied or not, failed or not, and its Actions. */
|
|
448
|
-
export function blueprintBuild(root: string): BlueprintBuildState & { actions: BuildEntry['actions'] | null } {
|
|
461
|
+
export function blueprintBuild(root: string): BlueprintBuildState & { actions: BuildEntry['actions'] | null; parts: BuildEntry['parts'] | null; refusedBefore: BuildEntry['actions'] | null } {
|
|
449
462
|
const project = { root, id: '', stateDir: join(root, '.frontera', 'local') }
|
|
450
463
|
const org = localOrganizationId(root)
|
|
451
464
|
const entry = loadBuild(project, org).blueprint
|
|
452
465
|
// The record speaks only for the files it was made from.
|
|
453
466
|
const current = !!entry && entry.hash === contentHash(project, { kind: 'blueprint', key: 'blueprint' })
|
|
454
|
-
|
|
455
|
-
|
|
467
|
+
// Published, with a governed Action that didn't deploy: the types are on the runtime, and
|
|
468
|
+
// that Action's object type is what failed (`objectStatus`), not every type.
|
|
469
|
+
const published = current && entry.status === 'failed' && !!entry.actions && Object.values(entry.actions).some((a) => a.status === 'failed')
|
|
470
|
+
const failed = current && entry.status === 'failed' && !published ? (entry.error ?? 'The Blueprint didn’t apply') : null
|
|
471
|
+
// The Actions the last build couldn't deploy, kept when a Blueprint file changed since: a refusal stays a
|
|
472
|
+
// failure until an apply says otherwise, as any item's does (`buildView`), not "not applied" while unapplied.
|
|
473
|
+
const refusedBefore = entry && !current && entry.actions ? Object.fromEntries(Object.entries(entry.actions).filter(([, a]) => a.status === 'failed' || (a.status === 'left-off' && a.refused))) : null
|
|
474
|
+
return { applied: published || blueprintApplyState(project, org) === 'applied', failed, actions: current && !failed ? (entry.actions ?? null) : null, objects: current && !failed ? (entry.objects ?? null) : null, parts: current && !failed ? (entry.parts ?? null) : null, refusedBefore: refusedBefore && Object.keys(refusedBefore).length ? refusedBefore : null }
|
|
456
475
|
}
|
|
457
476
|
|
|
458
477
|
/** The outline of a project folder: its files, its local datasets and what names its Actions. */
|