@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
package/src/api/workflow-api.ts
CHANGED
|
@@ -23,7 +23,10 @@ export interface WorkflowSummary {
|
|
|
23
23
|
liveVersion: number | null
|
|
24
24
|
latestDraftNumber: number | null
|
|
25
25
|
versionCount: number
|
|
26
|
+
/** Live top-level runs only: test runs and a workflow's child runs are not counted. */
|
|
26
27
|
runCount: number
|
|
28
|
+
/** Test runs (a draft run from `local test`, Studio or `workflow_run`); absent from a service that does not count them. */
|
|
29
|
+
testRunCount?: number
|
|
27
30
|
lastRunAt: string | null
|
|
28
31
|
lastRunStatus: string | null
|
|
29
32
|
triggerKinds: string[]
|
|
@@ -67,6 +70,8 @@ export interface PushResult {
|
|
|
67
70
|
version?: { id: string; number: number; status: string }
|
|
68
71
|
grants?: string[]
|
|
69
72
|
lineage?: LineageEdge[]
|
|
73
|
+
/** What the service accepted but says, one sentence each (a step that may not be able to run here). */
|
|
74
|
+
warnings?: string[]
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export interface LineageEdge {
|
|
@@ -263,8 +268,8 @@ export class WorkflowApi {
|
|
|
263
268
|
* carried here, because no verb pages yet and a parameter no caller passes
|
|
264
269
|
* reads as a capability the CLI has. `function runs` is capped the same way.
|
|
265
270
|
*/
|
|
266
|
-
runs(slug: string): Promise<RunSummary[]> {
|
|
267
|
-
return this.call<RunSummary[]>(`/v1/workflows/${encodeURIComponent(slug)}/runs`)
|
|
271
|
+
runs(slug: string, source?: 'live' | 'test'): Promise<RunSummary[]> {
|
|
272
|
+
return this.call<RunSummary[]>(`/v1/workflows/${encodeURIComponent(slug)}/runs${source ? `?source=${source}` : ''}`)
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
/** One run and, with `depth: 'all'`, the child runs beneath it. */
|
|
@@ -272,9 +277,35 @@ export class WorkflowApi {
|
|
|
272
277
|
return this.call<RunTree>(`/v1/workflows/runs/${encodeURIComponent(id)}?depth=${depth}`)
|
|
273
278
|
}
|
|
274
279
|
|
|
280
|
+
/** Take a workflow out of service, every version and run kept: nothing starts it, and it takes no new version until restored. */
|
|
281
|
+
archive(slug: string): Promise<{ id: string; slug: string; archivedAt: string | null }> {
|
|
282
|
+
return this.call(`/v1/workflows/${encodeURIComponent(slug)}/archive`, { method: 'POST' })
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Put an archived workflow back as it was. */
|
|
286
|
+
restore(slug: string): Promise<{ id: string; slug: string; archivedAt: string | null }> {
|
|
287
|
+
return this.call(`/v1/workflows/${encodeURIComponent(slug)}/restore`, { method: 'POST' })
|
|
288
|
+
}
|
|
289
|
+
|
|
275
290
|
cancel(id: string): Promise<{ status: string }> {
|
|
276
291
|
return this.call(`/v1/workflows/runs/${encodeURIComponent(id)}/cancel`, { method: 'POST' })
|
|
277
292
|
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Arm the test door of a workflow's draft for one delivery, as Studio's
|
|
296
|
+
* Listen does: the latest draft unless `version` names one. Answers each
|
|
297
|
+
* webhook door's test URL and when the Listen ends.
|
|
298
|
+
*/
|
|
299
|
+
listen(slug: string, version?: number): Promise<WebhookListen> {
|
|
300
|
+
return this.call(`/v1/workflows/${encodeURIComponent(slug)}/listen`, { method: 'POST', body: version === undefined ? {} : { version } })
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** `POST /v1/workflows/:slug/listen`. */
|
|
305
|
+
export interface WebhookListen {
|
|
306
|
+
version: number
|
|
307
|
+
until: string
|
|
308
|
+
doors: Array<{ path: string; methods: string[]; test: string }>
|
|
278
309
|
}
|
|
279
310
|
|
|
280
311
|
/** `details.errors` from a 400, when the failure was the definition itself. */
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_BLUEPRINT_TYPES_OUTPUT } from './blueprint-types'
|
|
5
|
+
import { collectSourceFiles } from './packaging'
|
|
6
|
+
import { APP_SDK_PACKAGES } from './template'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What a Custom App calls on the platform, read from its source at deploy
|
|
10
|
+
* (design 2026-10-18 §6, the Blueprint contract): the object types it reads
|
|
11
|
+
* with the properties its generated types give them, the governed Actions it
|
|
12
|
+
* submits and the Functions it starts. The platform checks it against the
|
|
13
|
+
* workspace a deployment would go live in. Best effort by construction: a
|
|
14
|
+
* name built at run time is not seen, and nothing here is authority.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface AppContract {
|
|
18
|
+
/** The Blueprint schema digest the App's generated types came from. */
|
|
19
|
+
digest?: string
|
|
20
|
+
objectTypes: Record<string, string[]>
|
|
21
|
+
actions: string[]
|
|
22
|
+
functions: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CODE = /\.(tsx?|jsx?|mjs|cjs)$/
|
|
26
|
+
|
|
27
|
+
const OBJECT_READ = [
|
|
28
|
+
/\buse(?:Objects|ObjectInstance)\s*(?:<[^>]*>)?\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g,
|
|
29
|
+
/\bobjectType\s*:\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g,
|
|
30
|
+
/\/v1\/blueprint\/object-types\/([A-Za-z_][A-Za-z0-9_]*)\//g,
|
|
31
|
+
]
|
|
32
|
+
const ACTION = [/\buseAction\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g, /governed-actions\/actions\/([A-Za-z_][A-Za-z0-9_]*)\/requests/g]
|
|
33
|
+
const FUNCTION = [/\/v1\/(?:functions|automations)\/([a-z0-9][a-z0-9-]*)\/app-run/g, /\b(?:runFunction|startFunction)\s*\(\s*['"]([a-z0-9][a-z0-9-]*)['"]/g]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The SDK copy a pulled App carries under src/frontera/ (`APP_SDK_PACKAGES`):
|
|
37
|
+
* the platform's code, not the App's. It names no object type of the App's
|
|
38
|
+
* except in its examples, so it is not read.
|
|
39
|
+
*/
|
|
40
|
+
const SDK_COPY_DIRS = Object.values(APP_SDK_PACKAGES).map((p) => `${p.dir}/`)
|
|
41
|
+
const isSdkCopy = (rel: string) => SDK_COPY_DIRS.some((dir) => rel.startsWith(dir))
|
|
42
|
+
|
|
43
|
+
/** Before a token, a `/` starts a regular expression rather than a division. */
|
|
44
|
+
const REGEX_BEFORE = /(?:^|[(,=:[!&|?{};+\-*%~^]|\b(?:return|typeof|case|do|else|in|of|new|delete|void|throw|yield|await))\s*$/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The code with its comments blanked out (the same length, line breaks kept):
|
|
48
|
+
* a doc comment's example, such as `useObjects('SupportTicket')` in the SDK's
|
|
49
|
+
* own docs, is not a call. Strings, template literals and regular
|
|
50
|
+
* expressions are read as such, so a `//` in a URL is not a comment.
|
|
51
|
+
*/
|
|
52
|
+
export function withoutComments(text: string): string {
|
|
53
|
+
let out = ''
|
|
54
|
+
let i = 0
|
|
55
|
+
// Template literals nest through `${…}`: each open one, and the brace depth its expression is at.
|
|
56
|
+
const templates: number[] = []
|
|
57
|
+
let depth = 0
|
|
58
|
+
const blank = (s: string) => s.replace(/[^\n]/g, ' ')
|
|
59
|
+
while (i < text.length) {
|
|
60
|
+
const c = text[i]!
|
|
61
|
+
const next = text[i + 1]
|
|
62
|
+
if (c === '/' && next === '/') {
|
|
63
|
+
const end = text.indexOf('\n', i)
|
|
64
|
+
const stop = end < 0 ? text.length : end
|
|
65
|
+
out += blank(text.slice(i, stop))
|
|
66
|
+
i = stop
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (c === '/' && next === '*') {
|
|
70
|
+
const end = text.indexOf('*/', i + 2)
|
|
71
|
+
const stop = end < 0 ? text.length : end + 2
|
|
72
|
+
out += blank(text.slice(i, stop))
|
|
73
|
+
i = stop
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (c === '/' && REGEX_BEFORE.test(out.slice(-40))) {
|
|
77
|
+
let j = i + 1
|
|
78
|
+
let inClass = false
|
|
79
|
+
while (j < text.length && text[j] !== '\n') {
|
|
80
|
+
const d = text[j]!
|
|
81
|
+
if (d === '\\') j += 1
|
|
82
|
+
else if (d === '[') inClass = true
|
|
83
|
+
else if (d === ']') inClass = false
|
|
84
|
+
else if (d === '/' && !inClass) break
|
|
85
|
+
j += 1
|
|
86
|
+
}
|
|
87
|
+
out += text.slice(i, j + 1)
|
|
88
|
+
i = j + 1
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
if (c === "'" || c === '"') {
|
|
92
|
+
let j = i + 1
|
|
93
|
+
while (j < text.length && text[j] !== c && text[j] !== '\n') j += text[j] === '\\' ? 2 : 1
|
|
94
|
+
out += text.slice(i, j + 1)
|
|
95
|
+
i = j + 1
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if (c === '`' || (c === '}' && templates.length && templates.at(-1) === depth)) {
|
|
99
|
+
// A template literal's text, from its start or the end of a `${…}` up to its end or the next `${`.
|
|
100
|
+
if (c === '}') templates.pop()
|
|
101
|
+
let j = i + 1
|
|
102
|
+
while (j < text.length && text[j] !== '`' && !(text[j] === '$' && text[j + 1] === '{')) j += text[j] === '\\' ? 2 : 1
|
|
103
|
+
if (text[j] === '$') {
|
|
104
|
+
templates.push(depth)
|
|
105
|
+
out += text.slice(i, j + 2)
|
|
106
|
+
i = j + 2
|
|
107
|
+
} else {
|
|
108
|
+
out += text.slice(i, j + 1)
|
|
109
|
+
i = j + 1
|
|
110
|
+
}
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (c === '{') depth += 1
|
|
114
|
+
else if (c === '}') depth -= 1
|
|
115
|
+
out += c
|
|
116
|
+
i += 1
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function matches(text: string, patterns: RegExp[], into: Set<string>): void {
|
|
122
|
+
for (const pattern of patterns) for (const m of text.matchAll(pattern)) if (m[1]) into.add(m[1])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Each object type's properties as the App's generated Blueprint types declare them. */
|
|
126
|
+
export function generatedProperties(text: string): { digest?: string; types: Map<string, string[]> } {
|
|
127
|
+
const digest = text.match(/Blueprint schema digest: (sha256:[0-9a-f]{64})/)?.[1]
|
|
128
|
+
const types = new Map<string, string[]>()
|
|
129
|
+
for (const block of text.matchAll(/export interface ([A-Za-z_][A-Za-z0-9_]*) \{\n([\s\S]*?)\n\}/g)) {
|
|
130
|
+
const props = [...block[2]!.matchAll(/^ {2}([A-Za-z_][A-Za-z0-9_]*)\??:/gm)].map((m) => m[1]!)
|
|
131
|
+
types.set(block[1]!, props)
|
|
132
|
+
}
|
|
133
|
+
return { ...(digest ? { digest } : {}), types }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function collectAppContract(root: string): AppContract {
|
|
137
|
+
const objects = new Set<string>()
|
|
138
|
+
const actions = new Set<string>()
|
|
139
|
+
const functions = new Set<string>()
|
|
140
|
+
const generated = join(root, DEFAULT_BLUEPRINT_TYPES_OUTPUT)
|
|
141
|
+
for (const file of collectSourceFiles(root)) {
|
|
142
|
+
if (!CODE.test(file.rel) || file.rel.startsWith('.frontera/') || isSdkCopy(file.rel) || join(root, file.rel) === generated || /(^|\/)(__tests__|tests?)\//.test(file.rel) || /\.(test|spec)\.[jt]sx?$/.test(file.rel)) continue
|
|
143
|
+
// What the code calls, not what its comments show.
|
|
144
|
+
const text = withoutComments(new TextDecoder().decode(file.bytes))
|
|
145
|
+
matches(text, OBJECT_READ, objects)
|
|
146
|
+
matches(text, ACTION, actions)
|
|
147
|
+
matches(text, FUNCTION, functions)
|
|
148
|
+
}
|
|
149
|
+
const types = existsSync(generated) ? generatedProperties(readFileSync(generated, 'utf8')) : { types: new Map<string, string[]>() }
|
|
150
|
+
return {
|
|
151
|
+
...('digest' in types && types.digest ? { digest: types.digest } : {}),
|
|
152
|
+
objectTypes: Object.fromEntries([...objects].sort().map((name) => [name, types.types.get(name) ?? []])),
|
|
153
|
+
actions: [...actions].sort(),
|
|
154
|
+
functions: [...functions].sort(),
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/app-lock.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `@frontera-sdk/*` versions an App's own lockfile resolved from the
|
|
6
|
+
* registry: what its code was written and checked against. A pulled App's
|
|
7
|
+
* `bun.lock` (or `package-lock.json`) is the best record of that, better than
|
|
8
|
+
* whatever SDK this CLI carries, so a repair goes back to it instead of
|
|
9
|
+
* swapping in another SDK the code was never built with.
|
|
10
|
+
*
|
|
11
|
+
* Only registry resolutions count (`1.50.61`): a `file:`, `link:` or
|
|
12
|
+
* `workspace:` entry records a copy that may not exist anywhere else.
|
|
13
|
+
*/
|
|
14
|
+
export interface LockedSdk {
|
|
15
|
+
/** The version the lockfile installed, e.g. `1.50.61`. */
|
|
16
|
+
version: string
|
|
17
|
+
/** The range package.json asked for when the lockfile was written (`^1.43.10`), when it was a registry range. */
|
|
18
|
+
range?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const REGISTRY_VERSION = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/
|
|
22
|
+
const REGISTRY_RANGE = /^[\^~]?\d+(?:\.\d+){0,2}(?:[-+][\w.-]+)?$/
|
|
23
|
+
|
|
24
|
+
/** Whether a package.json spec is a registry version or range (`^1.50.61`, `1.50.61`), not a `file:`, `link:` or `workspace:` one. */
|
|
25
|
+
export const isRegistryRange = (spec: string | undefined): boolean => !!spec && REGISTRY_RANGE.test(spec)
|
|
26
|
+
|
|
27
|
+
export function lockedSdkVersions(appRoot: string): Map<string, LockedSdk> {
|
|
28
|
+
const out = new Map<string, LockedSdk>()
|
|
29
|
+
let bun: string | null = null
|
|
30
|
+
try {
|
|
31
|
+
bun = readFileSync(join(appRoot, 'bun.lock'), 'utf8')
|
|
32
|
+
} catch {
|
|
33
|
+
bun = null
|
|
34
|
+
}
|
|
35
|
+
if (bun) {
|
|
36
|
+
// `"@frontera-sdk/blueprint": ["@frontera-sdk/blueprint@1.50.61", …]` under `packages`.
|
|
37
|
+
for (const m of bun.matchAll(/"(@frontera-sdk\/[a-z0-9-]+)":\s*\[\s*"(@frontera-sdk\/[a-z0-9-]+)@([^"]+)"/g)) {
|
|
38
|
+
if (m[1] !== m[2] || !REGISTRY_VERSION.test(m[3]!) || out.has(m[1]!)) continue
|
|
39
|
+
out.set(m[1]!, { version: m[3]! })
|
|
40
|
+
}
|
|
41
|
+
// `"@frontera-sdk/blueprint": "^1.43.10"` under the root workspace's dependencies.
|
|
42
|
+
for (const m of bun.matchAll(/"(@frontera-sdk\/[a-z0-9-]+)":\s*"([^"]+)"/g)) {
|
|
43
|
+
const locked = out.get(m[1]!)
|
|
44
|
+
if (locked && !locked.range && REGISTRY_RANGE.test(m[2]!)) locked.range = m[2]!
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const npm = JSON.parse(readFileSync(join(appRoot, 'package-lock.json'), 'utf8')) as {
|
|
50
|
+
packages?: Record<string, { version?: string; resolved?: string; dependencies?: Record<string, string> }>
|
|
51
|
+
}
|
|
52
|
+
const root = npm.packages?.[''] ?? {}
|
|
53
|
+
for (const [key, entry] of Object.entries(npm.packages ?? {})) {
|
|
54
|
+
const m = /^node_modules\/(@frontera-sdk\/[a-z0-9-]+)$/.exec(key)
|
|
55
|
+
if (!m || !entry.version || !REGISTRY_VERSION.test(entry.version) || (entry.resolved && !/^https?:/.test(entry.resolved))) continue
|
|
56
|
+
const range = root.dependencies?.[m[1]!]
|
|
57
|
+
out.set(m[1]!, { version: entry.version, ...(range && REGISTRY_RANGE.test(range) ? { range } : {}) })
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// No lockfile: nothing recorded.
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The spec that brings the locked version back: a caret on the release the
|
|
67
|
+
* lockfile installed (`^1.50.61`), never the older range the lockfile was
|
|
68
|
+
* written for (`^1.43.10`), so package.json names the SDK the code was
|
|
69
|
+
* checked against even where an install runs without the lockfile.
|
|
70
|
+
*/
|
|
71
|
+
export function lockedSpec(locked: LockedSdk): string {
|
|
72
|
+
return `^${locked.version}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Record in the App's lockfile that package.json now asks for `specs`, so a
|
|
77
|
+
* frozen install (`bun install --frozen-lockfile`, CI, `app deploy`) accepts
|
|
78
|
+
* the restored package.json: only the root workspace's own dependency ranges
|
|
79
|
+
* change, never a resolution, and each spec still matches the release the
|
|
80
|
+
* lockfile installed. Returns whether the lockfile changed.
|
|
81
|
+
*/
|
|
82
|
+
export function recordLockedRanges(appRoot: string, specs: Record<string, string>): boolean {
|
|
83
|
+
const names = Object.keys(specs)
|
|
84
|
+
if (!names.length) return false
|
|
85
|
+
const bunPath = join(appRoot, 'bun.lock')
|
|
86
|
+
let bun: string | null = null
|
|
87
|
+
try {
|
|
88
|
+
bun = readFileSync(bunPath, 'utf8')
|
|
89
|
+
} catch {
|
|
90
|
+
bun = null
|
|
91
|
+
}
|
|
92
|
+
if (bun !== null) {
|
|
93
|
+
// The root workspace's ranges sit between `"workspaces": {` and the top-level `"packages": {`.
|
|
94
|
+
const start = bun.indexOf('"workspaces"')
|
|
95
|
+
const end = start < 0 ? -1 : bun.indexOf('\n "packages"', start)
|
|
96
|
+
if (start < 0 || end < 0) return false
|
|
97
|
+
let section = bun.slice(start, end)
|
|
98
|
+
for (const pkg of names) {
|
|
99
|
+
const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')
|
|
100
|
+
section = section.replace(new RegExp(`("${escaped}":\\s*)"[^"]*"`, 'g'), (_m, head: string) => `${head}"${specs[pkg]}"`)
|
|
101
|
+
}
|
|
102
|
+
const next = bun.slice(0, start) + section + bun.slice(end)
|
|
103
|
+
if (next === bun) return false
|
|
104
|
+
writeFileSync(bunPath, next)
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
const npmPath = join(appRoot, 'package-lock.json')
|
|
108
|
+
try {
|
|
109
|
+
const text = readFileSync(npmPath, 'utf8')
|
|
110
|
+
const npm = JSON.parse(text) as { packages?: Record<string, { dependencies?: Record<string, string> }> }
|
|
111
|
+
const root = npm.packages?.['']
|
|
112
|
+
if (!root?.dependencies) return false
|
|
113
|
+
let changed = false
|
|
114
|
+
for (const pkg of names) {
|
|
115
|
+
if (pkg in root.dependencies && root.dependencies[pkg] !== specs[pkg]) {
|
|
116
|
+
root.dependencies[pkg] = specs[pkg]!
|
|
117
|
+
changed = true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (changed) writeFileSync(npmPath, `${JSON.stringify(npm, null, /^(\s+)"/m.exec(text)?.[1] ?? 2)}\n`)
|
|
121
|
+
return changed
|
|
122
|
+
} catch {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/args.ts
CHANGED
|
@@ -6,6 +6,12 @@ export type FlagSpec = Readonly<Record<string, FlagKind>>
|
|
|
6
6
|
export interface ParsedArgs {
|
|
7
7
|
positional: string[]
|
|
8
8
|
flags: Record<string, string | boolean>
|
|
9
|
+
/**
|
|
10
|
+
* Every value of a string flag given more than once, in order. `flags`
|
|
11
|
+
* keeps the last; a command that ANDs repeated values (`--where`) reads
|
|
12
|
+
* them all here.
|
|
13
|
+
*/
|
|
14
|
+
repeated: Record<string, string[]>
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
/**
|
|
@@ -28,6 +34,11 @@ export function parseArgs(
|
|
|
28
34
|
): ParsedArgs {
|
|
29
35
|
const positional: string[] = []
|
|
30
36
|
const flags: Record<string, string | boolean> = {}
|
|
37
|
+
const repeated: Record<string, string[]> = {}
|
|
38
|
+
const setString = (name: string, value: string) => {
|
|
39
|
+
if (typeof flags[name] === 'string') (repeated[name] ??= [flags[name] as string]).push(value)
|
|
40
|
+
flags[name] = value
|
|
41
|
+
}
|
|
31
42
|
|
|
32
43
|
for (let i = 0; i < argv.length; i += 1) {
|
|
33
44
|
let token = argv[i]!
|
|
@@ -86,7 +97,7 @@ export function parseArgs(
|
|
|
86
97
|
|
|
87
98
|
if (eq !== -1) {
|
|
88
99
|
// `--dir=` is a deliberate empty string, not a missing value.
|
|
89
|
-
|
|
100
|
+
setString(name, token.slice(eq + 1))
|
|
90
101
|
continue
|
|
91
102
|
}
|
|
92
103
|
|
|
@@ -100,9 +111,9 @@ export function parseArgs(
|
|
|
100
111
|
`pass it as --${name} <value> or --${name}=<value>`,
|
|
101
112
|
)
|
|
102
113
|
}
|
|
103
|
-
|
|
114
|
+
setString(name, value)
|
|
104
115
|
i += 1
|
|
105
116
|
}
|
|
106
117
|
|
|
107
|
-
return { positional, flags }
|
|
118
|
+
return { positional, flags, repeated }
|
|
108
119
|
}
|
package/src/auth-verify.ts
CHANGED
|
@@ -29,6 +29,16 @@ export function classifyKey(token: string): CredentialKind {
|
|
|
29
29
|
)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Whether a token is a key rather than a person's session, by its prefix:
|
|
34
|
+
* workspace (`sk-ws-`), organization (`sk-org-`) and every other `sk-` key
|
|
35
|
+
* the platform issues, and an Agent key (`sak_`), which a profile does not
|
|
36
|
+
* store but `FRONTERA_TOKEN` can carry. A person's session token has neither.
|
|
37
|
+
*/
|
|
38
|
+
export function isKeyToken(token: string): boolean {
|
|
39
|
+
return token.startsWith('sk-') || token.startsWith('sak_')
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
/**
|
|
33
43
|
* Prove the key works BEFORE it is stored.
|
|
34
44
|
*
|
|
@@ -38,12 +38,20 @@ export const AUTOMATION_SDK_FILES = [
|
|
|
38
38
|
// `testing.ts` imports the file-handle helpers from it; without it every
|
|
39
39
|
// scaffolded project failed to build with `Could not resolve: "./runtime-context"`.
|
|
40
40
|
'frontera/automation/runtime-context.ts',
|
|
41
|
+
// `FronteraCallError`, which every `ctx.*` refusal throws and `index.ts`,
|
|
42
|
+
// `testing.ts` and `runtime-context.ts` import.
|
|
43
|
+
'frontera/automation/errors.ts',
|
|
41
44
|
// `inputs.ts` is authoring surface too: `validateInputValue` is what a
|
|
42
45
|
// handler's own tests and the Console call to check a value against the
|
|
43
46
|
// manifest's `inputs` schema, and `index.ts` re-exports it — so omitting it
|
|
44
47
|
// breaks a scaffolded project's typecheck the same way skipping `testing.ts`
|
|
45
48
|
// would.
|
|
46
49
|
'frontera/automation/inputs.ts',
|
|
50
|
+
// `subjectOf` (an Action's subject and version from a query row) is exported
|
|
51
|
+
// by `index.ts`; `test-check.ts` is how `define.ts` checks a handler the way
|
|
52
|
+
// the platform will when `testing.ts` runs it.
|
|
53
|
+
'frontera/automation/subject.ts',
|
|
54
|
+
'frontera/automation/test-check.ts',
|
|
47
55
|
'frontera/blueprint/types.ts',
|
|
48
56
|
] as const
|
|
49
57
|
|
|
@@ -187,8 +195,10 @@ import automation, { OBJECT_TYPE } from './index'
|
|
|
187
195
|
// A handler is just a function, so it can be tested without deploying. The
|
|
188
196
|
// context refuses what the platform refuses — a grant the manifest does not
|
|
189
197
|
// declare, a step name used twice — so a passing test means something.
|
|
198
|
+
// \`replay: true\` runs it as the platform does, again from the top after each
|
|
199
|
+
// step, so a read outside a step that a later step changes fails here too.
|
|
190
200
|
test('handles an empty result', async () => {
|
|
191
|
-
const { ctx, steps } = createTestContext({ grants: ['blueprint:read'] })
|
|
201
|
+
const { ctx, steps } = createTestContext({ replay: true, grants: ['blueprint:read'] })
|
|
192
202
|
|
|
193
203
|
const result = await automation.handler(ctx)
|
|
194
204
|
|
|
@@ -199,6 +209,7 @@ test('handles an empty result', async () => {
|
|
|
199
209
|
|
|
200
210
|
test('handles rows', async () => {
|
|
201
211
|
const { ctx, steps } = createTestContext({
|
|
212
|
+
replay: true,
|
|
202
213
|
grants: ['blueprint:read'],
|
|
203
214
|
// Stub only what this test is about; an object type you do not stub returns
|
|
204
215
|
// no rows, which is a real answer.
|