@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/flag-help.ts
CHANGED
|
@@ -40,7 +40,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
40
40
|
remove: 'comma-separated property API names to drop from the current set',
|
|
41
41
|
// Blueprint authoring (organization API key). `notes` is described further down
|
|
42
42
|
// — this map is keyed by flag name across every command, so one entry serves both.
|
|
43
|
-
label: 'release label recorded on the published or rolled-back release',
|
|
43
|
+
label: 'release label recorded on the published or rolled-back release; for an App deployment, unique per App',
|
|
44
44
|
report: 'validation report id to act on — the release publishes against it, the review is pinned to it',
|
|
45
45
|
instruction: 'migration instruction applied to every change a rollback discards',
|
|
46
46
|
// Global
|
|
@@ -71,7 +71,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
71
71
|
|
|
72
72
|
// Writes
|
|
73
73
|
file: 'read the document from this path, or `-` for stdin',
|
|
74
|
-
force: 'proceed past a refusal — overwrite existing files,
|
|
74
|
+
force: 'proceed past a refusal — overwrite existing files, a dirty tree, an App\'s or a change request\'s required checks (with --reason), a running local runtime',
|
|
75
75
|
'expect-revision':
|
|
76
76
|
'refuse the write unless the resource is still at this revision; exit 3 if it moved',
|
|
77
77
|
notes: 'note recorded against the published version',
|
|
@@ -91,6 +91,19 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
91
91
|
registry: 'the registry host and namespace image mode pulls the platform images from, such as registry.example.com/frontera',
|
|
92
92
|
reapply: 'apply every file again, including those unchanged since they were last applied',
|
|
93
93
|
stop: 'stop the local runtime when the check ends',
|
|
94
|
+
'keep-state': 'keep what the Agent and workflow tests wrote, instead of restoring the database as the apply left it',
|
|
95
|
+
exact: 'restore: put the baseline’s workspace secrets back too, instead of keeping the ones set now',
|
|
96
|
+
workflow: 'run only this workflow’s tests (workflows/tests/<slug>.yaml)',
|
|
97
|
+
skip: 'comma-separated check steps to leave out: typecheck, unit, apps, agents, workflows',
|
|
98
|
+
'object-type': 'the object type a new action acts on; its primary key names the key input',
|
|
99
|
+
body: 'the request body to send: a file, or - for standard input',
|
|
100
|
+
path: 'which webhook of the workflow, by its trigger path; needed when it has several',
|
|
101
|
+
method: 'the HTTP method to send; POST when the webhook takes it',
|
|
102
|
+
header: 'one header the sender would send, `Name: value` (the webhook’s own auth)',
|
|
103
|
+
query: 'a query string to send with the delivery, such as source=portal&urgent=1',
|
|
104
|
+
'fail-fast': 'stop the check at the first step that fails, instead of running and reporting every step',
|
|
105
|
+
strict: 'fail on inherited failures too: items and files unchanged since the pull, which otherwise are listed and do not fail the check',
|
|
106
|
+
concurrency: 'how many Agent test cases run at once (1–16, default 2); cases that change records for real still run one at a time. Lower it when the model provider refuses for requests in flight',
|
|
94
107
|
|
|
95
108
|
// Apps
|
|
96
109
|
framework: 'App scaffold to create: next (default) or react (legacy Vite)',
|
|
@@ -104,7 +117,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
104
117
|
// choose which version to RUN, one chooses which to publish. The old text
|
|
105
118
|
// described only the third, so `workflow run --help` and `function run
|
|
106
119
|
// --help` both advertised package.json.
|
|
107
|
-
version: 'the version to act on — the one to run, or on a deploy the one to publish',
|
|
120
|
+
version: 'the version to act on — the one to run (`workflow run` also takes draft: the newest version not retired), or on a deploy the one to publish',
|
|
108
121
|
input: "JSON object of the declared inputs — inline ('{\"key\": 1}') or @file.json",
|
|
109
122
|
// The primary key alone. A subject-bound workflow takes exactly one, so the
|
|
110
123
|
// flag spends no characters on the `{"pk": …}` the route models it as.
|
|
@@ -112,8 +125,11 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
112
125
|
// Action requests.
|
|
113
126
|
'subject-version': 'record version of --subject the request expects, so a stale read is refused',
|
|
114
127
|
'idempotency-key': 'reuse a key to retry a submit and get the same request back; generated if omitted',
|
|
115
|
-
'no-promote': 'publish the version without moving the live pointer',
|
|
116
|
-
|
|
128
|
+
'no-promote': 'publish the version without moving the live pointer; with app deploy --prod, stage it for production',
|
|
129
|
+
build: 'run bun run build first, so the deployment is built from the source as it is now',
|
|
130
|
+
'skip-build-check': 'deploy even when the build output is older than the source, and skip bun run check',
|
|
131
|
+
prod: 'go live once the required checks pass (a deploy is otherwise a preview)',
|
|
132
|
+
undo: 'undo the rollback: production serves what it did before, and is unpinned',
|
|
117
133
|
'no-wait': 'return as soon as the run is queued instead of waiting for it to finish',
|
|
118
134
|
// Named for what it RUNS, not where it runs: an author reads this to decide
|
|
119
135
|
// between their working copy and what is deployed.
|
|
@@ -129,8 +145,8 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
129
145
|
|
|
130
146
|
// Blueprint scaffolding
|
|
131
147
|
template:
|
|
132
|
-
'starting
|
|
133
|
-
+ '
|
|
148
|
+
'starting shape: a semantic type\'s rule (non-negative, percentage, status-enum, '
|
|
149
|
+
+ 'code-length, the default), or an action\'s (edit, the default, or create)',
|
|
134
150
|
|
|
135
151
|
// Packs
|
|
136
152
|
'remove-apps': 'also uninstall the apps the pack installed',
|
|
@@ -146,8 +162,9 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
146
162
|
// Blueprint reads
|
|
147
163
|
select: 'comma-separated property API names to return (default: whatever the service returns)',
|
|
148
164
|
where:
|
|
149
|
-
'filter as <property>:<value>, <property>:<op>:<value>,
|
|
150
|
-
+ '
|
|
165
|
+
'filter as <property>:<value>, <property>:<op>:<value>, <property>:in:<a>,<b> (also notIn and between; '
|
|
166
|
+
+ 'a backslash before a comma keeps it in a value, or give a JSON array), or a JSON where-node for an '
|
|
167
|
+
+ 'or/not; repeat it for conditions that must all hold',
|
|
151
168
|
order: 'sort as <property>:asc or <property>:desc, comma-separated for more than one',
|
|
152
169
|
// No number here on purpose. This map is keyed by flag NAME across every
|
|
153
170
|
// command, and the three surfaces that take `--limit` cap a page at 1000,
|
|
@@ -155,6 +172,22 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
155
172
|
// command's own refusal names its real ceiling.
|
|
156
173
|
limit: 'maximum rows to return in one page; each surface has its own ceiling',
|
|
157
174
|
'page-token': 'cursor from the previous response’s nextPageToken; omit for the first page',
|
|
175
|
+
as:
|
|
176
|
+
'on the local runtime only: act as a local persona (`submitter`, or an approval step such as '
|
|
177
|
+
+ '`dispatch_lead`), as `frontera local status` lists them; refused anywhere else',
|
|
178
|
+
// A metric's query (`blueprint metric query`).
|
|
179
|
+
measure: 'comma-separated measures of the metric to return; all of them when absent',
|
|
180
|
+
'group-by': 'comma-separated dimensions of the metric to break the numbers down by',
|
|
181
|
+
grain: 'a series by hour, day, week, month, quarter or year over the metric’s timeseries dimension (added to the group-by)',
|
|
182
|
+
range: 'a time window as an ISO-8601 duration (P7D, PT12H, P3M), back from the newest value of the metric’s timeseries in the data, not from now',
|
|
183
|
+
start: 'the window’s start, an ISO date or timestamp (inclusive; a date is its midnight UTC)',
|
|
184
|
+
end: 'the window’s end, an ISO date or timestamp (exclusive; a date is its midnight UTC)',
|
|
185
|
+
compare: 'add the window before the one given: <measure>_prev, <measure>_delta and <measure>_pct',
|
|
186
|
+
only: 'live: only live runs; test: only test runs (from `local test`, Studio or a draft run)',
|
|
187
|
+
npm: 'with use-local: switch the App back to the published @frontera-sdk packages from npm, deleting vendor/frontera-sdk/ and any SDK copy under src/frontera/ (the output lists each file)',
|
|
188
|
+
'object-set':
|
|
189
|
+
'read a saved object set’s rows: its name, its reference (objectSet/<ref>) or its id, '
|
|
190
|
+
+ 'from `frontera blueprint object-sets`',
|
|
158
191
|
// Named for the field the response carries, which differs by surface —
|
|
159
192
|
// Blueprint answers `nextPageToken`, data-integration answers `nextCursor`.
|
|
160
193
|
// Matching each flag to its own response beats one name that is wrong half
|
|
@@ -184,8 +217,9 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
184
217
|
// permissive and 0 matches nothing at all. Described the wrong way round
|
|
185
218
|
// first, which made `--threshold 0` look like "return everything".
|
|
186
219
|
threshold:
|
|
187
|
-
'maximum cosine distance a chunk may have; higher is more permissive (default
|
|
188
|
-
+ 'Not the
|
|
220
|
+
'maximum cosine distance a chunk may have, 0 to 2; higher is more permissive (default: the '
|
|
221
|
+
+ 'base’s own match threshold, as an Agent’s retrieval uses it). Not the score shown in the '
|
|
222
|
+
+ 'output: a score is 1 − distance / 2, so 0.5 keeps scores above 0.75',
|
|
189
223
|
strategy:
|
|
190
224
|
'text extraction to use per file: auto (default), text, or ocr; ocr needs an OCR engine on the base',
|
|
191
225
|
|
|
@@ -214,7 +248,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
214
248
|
'org-key': '`-` to read an organization key (sk-org-…) from stdin, after the workspace key; widens the Blueprint to the whole model',
|
|
215
249
|
blueprint: 'workspace (the slice granted to the workspace, default) or organization (the whole model; needs an organization key)',
|
|
216
250
|
relink: 'point a project already linked to another workspace at this one',
|
|
217
|
-
preview: 'list what a pull would bring, or a push would propose, file by file, and change nothing',
|
|
251
|
+
preview: 'local sync: list what a pull would bring, or a push would propose, file by file, and change nothing; app deploy: make a preview deployment (the default), said explicitly as a pipeline must',
|
|
218
252
|
kinds: 'comma-separated: blueprint, skills, functions, plugins, knowledge, workflows, agents, evaluations',
|
|
219
253
|
drafts: 'comma-separated items (agent/<slug>, workflow/<slug>, function/<slug>, blueprint) to read as their draft instead of the live version; the Blueprint’s draft takes an organization key',
|
|
220
254
|
'all-changed': 'pull every item the workspace changed since the last pull, and every clean merge; never one that conflicts or replaces local edits',
|
|
@@ -230,14 +264,25 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
230
264
|
edited: 'take the files as you edited them; the workspace’s copies beside them go',
|
|
231
265
|
title: 'the change request’s title; a push that opens a new one needs it',
|
|
232
266
|
migration: 'for a Blueprint change that is not additive: what happens to existing records',
|
|
233
|
-
documents: 'comma-separated knowledge bases whose documents a pull brings, or a push proposes (`*` for every one)
|
|
267
|
+
documents: 'comma-separated knowledge bases whose documents a pull brings (a base pulled now brings its own anyway), or a push proposes (`*` for every one)',
|
|
268
|
+
'no-documents': 'pull knowledge bases without their documents (by default a pulled base brings them)',
|
|
269
|
+
'no-app-code': 'pull Custom Apps without their code (by default a pulled App brings its deployed version into apps/<slug>/, unless that folder is there)',
|
|
270
|
+
release: 'Custom Apps the change request releases, comma-separated slug=deployment (the newest deployment when only the slug is given); Apply promotes each one last',
|
|
271
|
+
evals: 'run each Agent’s gated cases (gate: true) against the local runtime and attach the results, compared with its last applied run',
|
|
272
|
+
renders: 'attach each released App’s last render (its screenshot and console summary)',
|
|
273
|
+
screenshots: 'attach the newest screenshots a Forge session saved (its id; this session’s when run inside Forge)',
|
|
274
|
+
trace: 'attach a trace link (an http(s) address) to the change request',
|
|
234
275
|
lessons: 'bring each Agent’s reviewed rules from customer feedback into agents/<slug>/lessons.yaml (never the feedback itself); off by default',
|
|
235
276
|
conflicts: 'the conflicts the picks were chosen against, as `diff --json` names them; refused if they moved',
|
|
236
277
|
'from-workspace': 'start the project from a platform workspace instead of the examples',
|
|
237
278
|
'no-sample-data': 'do not generate sample rows for the pulled Blueprint',
|
|
279
|
+
'keep-sample-data': 'leave the sample datasets as they are; by default they are made again (same seed, rows and day) after a pull that brings Blueprint items, or when an older generator made them',
|
|
280
|
+
'remake-sample-data': 'make the sample datasets again with the newest sample rules (same seed, rows and day), even the ones a pull wrote, which local dev otherwise keeps as pulled',
|
|
238
281
|
synthetic: 'generate sample rows for the Blueprint: values fit each property, links stay consistent',
|
|
239
282
|
rows: 'rows per object type (default 200; types others link to get half)',
|
|
240
|
-
seed: 'the same seed writes the same rows (default 7)',
|
|
283
|
+
seed: 'the same seed writes the same rows on the same day; with --today, on any day (default 7)',
|
|
284
|
+
today: 'the day sample due dates are spread around, YYYY-MM-DD (default: today, UTC)',
|
|
285
|
+
region: 'where sample geopoints fall: a country code (ID, US, GB…) or `<lat>,<lon>[,<radius km>]` (default: the project’s `locale` in frontera.yaml, else the workspace’s timezone, else US)',
|
|
241
286
|
}
|
|
242
287
|
|
|
243
288
|
/**
|
|
@@ -259,6 +304,8 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
|
259
304
|
// take an integer version number, and this map is keyed by flag name for all
|
|
260
305
|
// three. `<version>` is the only placeholder true of every consumer.
|
|
261
306
|
version: 'version',
|
|
307
|
+
body: 'file',
|
|
308
|
+
header: 'name: value',
|
|
262
309
|
framework: 'next|react',
|
|
263
310
|
port: 'number',
|
|
264
311
|
origin: 'url',
|
|
@@ -279,6 +326,15 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
|
279
326
|
order: 'prop:asc',
|
|
280
327
|
limit: 'n',
|
|
281
328
|
'page-token': 'cursor',
|
|
329
|
+
'object-set': 'name|ref|id',
|
|
330
|
+
measure: 'a,b',
|
|
331
|
+
'group-by': 'a,b',
|
|
332
|
+
grain: 'hour|day|week|month|quarter|year',
|
|
333
|
+
range: 'P7D',
|
|
334
|
+
start: 'iso',
|
|
335
|
+
end: 'iso',
|
|
336
|
+
only: 'live|test',
|
|
337
|
+
as: 'persona',
|
|
282
338
|
cursor: 'cursor',
|
|
283
339
|
lifecycle: 'state',
|
|
284
340
|
actions: 'allow_all|read_only|custom',
|
|
@@ -288,7 +344,7 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
|
288
344
|
markings: 'a,b',
|
|
289
345
|
'max-distance': '0-2',
|
|
290
346
|
permissions: 'read,write',
|
|
291
|
-
template: 'non-negative|percentage|status-enum|code-length',
|
|
347
|
+
template: 'non-negative|percentage|status-enum|code-length|edit|create',
|
|
292
348
|
platform: 'url',
|
|
293
349
|
'workspace-key': '-',
|
|
294
350
|
'org-key': '-',
|
|
@@ -299,6 +355,9 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
|
299
355
|
'delete-here': 'kind/name,…',
|
|
300
356
|
'keep-local': 'kind/name,…',
|
|
301
357
|
documents: 'name,…',
|
|
358
|
+
release: 'slug=deployment,…',
|
|
359
|
+
screenshots: 'session',
|
|
360
|
+
trace: 'url',
|
|
302
361
|
keep: 'workspace|mine',
|
|
303
362
|
picks: 'n=side,…',
|
|
304
363
|
rows: 'n',
|
|
@@ -315,6 +374,7 @@ export function flagUsage(name: string, type: 'string' | 'boolean', alias?: stri
|
|
|
315
374
|
return `${short}--${name} <${PLACEHOLDER[name] ?? 'value'}>`
|
|
316
375
|
}
|
|
317
376
|
|
|
318
|
-
|
|
319
|
-
|
|
377
|
+
/** A command's own wording for a flag (`meta.flagHelp`) wins: the one place a name means something else there. */
|
|
378
|
+
export function describeFlag(name: string, own?: Readonly<Record<string, string>>): string {
|
|
379
|
+
return own?.[name] ?? FLAG_HELP[name] ?? ''
|
|
320
380
|
}
|
package/src/forge/touches.ts
CHANGED
|
@@ -44,29 +44,31 @@ export const FORGE_ARTIFACT_KINDS = [
|
|
|
44
44
|
* `search`, `runs`, `bindings` — is a read and is deliberately absent.
|
|
45
45
|
*/
|
|
46
46
|
export const TOUCHING_VERBS: Record<ForgeArtifactKind, readonly string[]> = {
|
|
47
|
-
agent: ['apply', 'create', 'publish', 'revert', 'discard', 'skill-attach', 'skill-detach'],
|
|
47
|
+
agent: ['apply', 'create', 'publish', 'revert', 'discard', 'skill-attach', 'skill-detach', 'archive', 'restore'],
|
|
48
48
|
automation: ['deploy', 'promote', 'enable', 'disable', 'init', 'bind', 'unbind', 'run'],
|
|
49
|
-
app: ['deploy', 'promote', 'save', 'add'],
|
|
49
|
+
app: ['deploy', 'promote', 'rollback', 'unpin', 'save', 'add'],
|
|
50
50
|
blueprint: ['apply', 'publish', 'create', 'update', 'delete', 'rename', 'rollback', 'bind', 'grant', 'adopt', 'editable'],
|
|
51
51
|
action: ['prepare', 'deploy', 'grant', 'review'],
|
|
52
52
|
skill: ['push', 'delete'],
|
|
53
53
|
source: ['create', 'update', 'revise', 'disable'],
|
|
54
|
-
|
|
54
|
+
// `revise` cuts the dataset's next revision from its CSV.
|
|
55
|
+
dataset: ['create', 'upload', 'revise', 'test-source'],
|
|
55
56
|
knowledge: ['create', 'attach', 'detach', 'upload', 'retry', 'delete'],
|
|
57
|
+
// `webhook-test` starts a run on the draft through its webhook, as `run` does.
|
|
56
58
|
// `get`, `lineage`, `list`, `runs` and `validate` are reads — `validate` is
|
|
57
59
|
// `push --dry-run` and writes nothing. `cancel` is deliberately absent: it
|
|
58
60
|
// stops one in-flight run and leaves the definition, the versions and the
|
|
59
61
|
// live pointer exactly as they were, and its positional is a RUN id, not a
|
|
60
62
|
// slug, so it could not name the workflow page anyway. Same call
|
|
61
63
|
// `automation` makes, which lists `run` and not `cancel`.
|
|
62
|
-
workflow: ['push', 'promote', 'run'],
|
|
64
|
+
workflow: ['push', 'promote', 'run', 'webhook-test', 'archive', 'restore'],
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/** Verbs after which the artifact is LIVE, not merely changed. */
|
|
66
68
|
const LIVE_VERBS: Partial<Record<ForgeArtifactKind, readonly string[]>> = {
|
|
67
69
|
agent: ['publish'],
|
|
68
70
|
automation: ['promote', 'enable'],
|
|
69
|
-
app: ['promote'],
|
|
71
|
+
app: ['promote', 'rollback'],
|
|
70
72
|
blueprint: ['publish'],
|
|
71
73
|
workflow: ['promote'],
|
|
72
74
|
}
|
|
@@ -97,9 +99,9 @@ const FILE_POSITIONAL_VERBS: Partial<Record<ForgeArtifactKind, readonly string[]
|
|
|
97
99
|
automation: ['deploy', 'init'],
|
|
98
100
|
skill: ['push'],
|
|
99
101
|
blueprint: ['create'],
|
|
100
|
-
// `app deploy` names nothing (the folder is the app); `app promote`
|
|
101
|
-
// a
|
|
102
|
-
app: ['deploy', 'promote', 'save', 'add'],
|
|
102
|
+
// `app deploy` names nothing (the folder is the app); `app promote` and
|
|
103
|
+
// `app rollback` name a DEPLOYMENT. The app itself is the project's, which the pane knows.
|
|
104
|
+
app: ['deploy', 'promote', 'rollback', 'unpin', 'save', 'add'],
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/**
|
|
@@ -122,7 +124,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
122
124
|
'accept-contract-change', 'accept-orphaned-knowledge', 'dry-run', 'end-user', 'no-activate',
|
|
123
125
|
'no-components', 'no-git', 'no-input', 'no-install', 'no-open', 'no-promote', 'no-wait',
|
|
124
126
|
'remove-apps', 'token-stdin', 'prune', 'dev', 'revoke', 'draft', 'check',
|
|
125
|
-
'skip-build-check',
|
|
127
|
+
'skip-build-check', 'prod', 'preview', 'undo', 'build',
|
|
126
128
|
])
|
|
127
129
|
|
|
128
130
|
/**
|
|
@@ -401,9 +403,10 @@ export function parseFronteraCommands(line: string): ParsedFronteraCommand[] {
|
|
|
401
403
|
: (rest[0] ?? null)
|
|
402
404
|
const live =
|
|
403
405
|
(LIVE_VERBS[kind] ?? []).includes(verb)
|
|
404
|
-
// `
|
|
405
|
-
// unless
|
|
406
|
-
|| (
|
|
406
|
+
// `function deploy` builds and promotes in one act unless told not to;
|
|
407
|
+
// `app deploy` makes a preview unless `--prod` (and `--prod --no-promote` stages it).
|
|
408
|
+
|| (kind === 'automation' && verb === 'deploy' && !flags.has('no-promote'))
|
|
409
|
+
|| (kind === 'app' && verb === 'deploy' && flags.has('prod') && !flags.has('no-promote'))
|
|
407
410
|
out.push({ kind, verb, ref, live })
|
|
408
411
|
}
|
|
409
412
|
return out
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, posix, relative, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { isExcluded, type SourceFile } from './packaging'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The files a Function imports from outside its own project folder.
|
|
8
|
+
*
|
|
9
|
+
* A Function deployed from its folder (`functions/<slug>/`) may import a
|
|
10
|
+
* helper beside it (`../../shared/eligibility` from `src/index.ts`). The
|
|
11
|
+
* bundle holds that code, but its source archive is the folder alone, so a
|
|
12
|
+
* pull would bring a Function that no longer builds. Each such file travels
|
|
13
|
+
* in the archive under `frontera-imports/`, by its path from the folder with
|
|
14
|
+
* every `..` as `__up__` (`frontera-imports/__up__/shared/eligibility.ts`),
|
|
15
|
+
* and a pull writes it back where the import finds it.
|
|
16
|
+
*
|
|
17
|
+
* Only files the code imports, by relative path, and never above the
|
|
18
|
+
* solution (the nearest folder with `frontera.yaml`, else two folders above
|
|
19
|
+
* the Function's): the bundle already carries their code.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const OUTSIDE_DIR = 'frontera-imports'
|
|
23
|
+
const UP = '__up__'
|
|
24
|
+
|
|
25
|
+
const RELATIVE_IMPORT = /(?:from|import)\s*\(?\s*['"](\.{1,2}\/[^'"]+)['"]/g
|
|
26
|
+
const CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs|json)$/
|
|
27
|
+
const EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.js', '.mjs', '.json', '/index.ts', '/index.js']
|
|
28
|
+
|
|
29
|
+
/** `../shared/x.ts` (from the folder) → `frontera-imports/__up__/shared/x.ts`. */
|
|
30
|
+
export function outsidePath(fromFolder: string): string {
|
|
31
|
+
return `${OUTSIDE_DIR}/${posix.normalize(fromFolder).split('/').map((s) => (s === '..' ? UP : s)).join('/')}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `frontera-imports/__up__/shared/x.ts` → `../shared/x.ts`; null for any other archive path. */
|
|
35
|
+
export function fromOutsidePath(archivePath: string): string | null {
|
|
36
|
+
if (!archivePath.startsWith(`${OUTSIDE_DIR}/`)) return null
|
|
37
|
+
const rel = archivePath.slice(OUTSIDE_DIR.length + 1).split('/').map((s) => (s === UP ? '..' : s)).join('/')
|
|
38
|
+
return rel.startsWith('../') ? rel : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function solutionRoot(projectRoot: string): string {
|
|
42
|
+
for (let dir = projectRoot; ; dir = dirname(dir)) {
|
|
43
|
+
if (existsSync(join(dir, 'frontera.yaml'))) return dir
|
|
44
|
+
if (dirname(dir) === dir) break
|
|
45
|
+
}
|
|
46
|
+
return dirname(dirname(projectRoot))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const within = (root: string, path: string) => path === root || path.startsWith(`${root}${sep}`)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The files outside `projectRoot` that `entry` imports, followed through the
|
|
53
|
+
* files it imports inside it too, as archive entries under `frontera-imports/`.
|
|
54
|
+
*/
|
|
55
|
+
export function outsideImports(projectRoot: string, entry: string): SourceFile[] {
|
|
56
|
+
const root = resolve(projectRoot)
|
|
57
|
+
const bound = resolve(solutionRoot(root))
|
|
58
|
+
const seen = new Set<string>()
|
|
59
|
+
const out: SourceFile[] = []
|
|
60
|
+
const read = (path: string): string | null => {
|
|
61
|
+
try {
|
|
62
|
+
return existsSync(path) && lstatSync(path).isFile() ? readFileSync(path, 'utf8') : null
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const visit = (file: string) => {
|
|
68
|
+
if (seen.has(file)) return
|
|
69
|
+
const text = read(file)
|
|
70
|
+
if (text === null) return
|
|
71
|
+
seen.add(file)
|
|
72
|
+
if (!within(root, file)) {
|
|
73
|
+
const rel = relative(root, file).split(sep).join('/')
|
|
74
|
+
if (CODE.test(file) && !isExcluded(rel.replace(/^(\.\.\/)+/, ''))) out.push({ rel: outsidePath(rel), bytes: new TextEncoder().encode(text) })
|
|
75
|
+
}
|
|
76
|
+
for (const m of text.matchAll(RELATIVE_IMPORT)) {
|
|
77
|
+
const base = resolve(dirname(file), m[1]!)
|
|
78
|
+
if (!within(bound, base) || base.split(sep).includes('node_modules')) continue
|
|
79
|
+
const found = EXTENSIONS.map((ext) => `${base}${ext}`).find((c) => read(c) !== null)
|
|
80
|
+
if (!found) continue
|
|
81
|
+
visit(found)
|
|
82
|
+
// A JavaScript file's types beside it travel with it (a module a pull recovered from a
|
|
83
|
+
// deployed bundle is a `.js` typed by its `.d.ts`), so the next pull still typechecks.
|
|
84
|
+
if (found.endsWith('.js')) visit(`${found.slice(0, -'.js'.length)}.d.ts`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
visit(resolve(entry))
|
|
88
|
+
return out
|
|
89
|
+
}
|
package/src/harness.ts
CHANGED
|
@@ -25,6 +25,16 @@ description: Use when reading or changing anything on the Frontera platform from
|
|
|
25
25
|
never out of date — read it instead of guessing, and instead of trusting this
|
|
26
26
|
file for syntax.
|
|
27
27
|
|
|
28
|
+
## In a project with the Frontera MCP server
|
|
29
|
+
|
|
30
|
+
A project made by \`frontera local\` registers the \`frontera\` MCP server
|
|
31
|
+
(\`.mcp.json\`). When its tools are offered (\`mcp__frontera__*\` in Claude
|
|
32
|
+
Code), use them for the local loop instead of the shell: \`status\`,
|
|
33
|
+
\`apply\`, \`test_run\`, \`logs\`, \`read_file_schema\` and the documentation
|
|
34
|
+
tools. The person approves the server once, where every shell command can
|
|
35
|
+
ask again. Run \`frontera local …\` only when the tools aren't offered, or
|
|
36
|
+
for a step that has no tool (\`frontera local start\`, \`frontera local check\`).
|
|
37
|
+
|
|
28
38
|
## Preflight: know which customer you are about to change
|
|
29
39
|
|
|
30
40
|
\`\`\`bash
|
|
@@ -63,7 +73,10 @@ Two fields decide whether a call can work at all:
|
|
|
63
73
|
key: set \`FRONTERA_TOKEN\` to the session token and \`FRONTERA_API_URL\` (or
|
|
64
74
|
pass \`--api-url\`). The CLI refuses an \`sk-ws-\`/\`sk-org-\` key on those
|
|
65
75
|
before sending anything; \`frontera login\` stores keys only, so it cannot
|
|
66
|
-
supply the session.
|
|
76
|
+
supply the session. On a project's local runtime, never fetch a person's
|
|
77
|
+
session: \`--as <persona>\` runs \`action available\`, \`submit\`,
|
|
78
|
+
\`approve\` and \`reject\` as a local persona (\`--as submitter\`,
|
|
79
|
+
\`--as <approver>\`; \`frontera local status\` lists them).
|
|
67
80
|
- \`available\` — \`false\` means the verb is named but unbuilt, and
|
|
68
81
|
\`unavailableReason\` says why. Pick another route rather than retrying.
|
|
69
82
|
|
|
@@ -126,7 +139,7 @@ Pass \`--json\` for machine-readable output. The contract:
|
|
|
126
139
|
| 0 | success | continue |
|
|
127
140
|
| 1 | transient or remote failure | retry, then report |
|
|
128
141
|
| 2 | usage, input, or profile selection | fix the command |
|
|
129
|
-
| 3 | conflict — someone changed it first | re-
|
|
142
|
+
| 3 | conflict or refusal — someone changed it first, or the record's state refuses it | re-read, change the input or the record, then send it again; never retry it unchanged |
|
|
130
143
|
| 4 | auth or permission | different credential, or ask an admin |
|
|
131
144
|
|
|
132
145
|
Errors carry a \`hint\` naming the next command. Read it.
|
|
@@ -192,8 +205,9 @@ frontera blueprint instance <apiName> <pk>
|
|
|
192
205
|
app or agent sees at runtime. Anything absent here is absent for them too.
|
|
193
206
|
|
|
194
207
|
\`--where\` takes \`<property>:<value>\` or \`<property>:<op>:<value>\` for one
|
|
195
|
-
condition
|
|
196
|
-
|
|
208
|
+
condition, and \`<property>:in:<a>,<b>\` (also \`notIn\`, \`between\`) for a list;
|
|
209
|
+
repeat it for conditions that must all hold. Pass a JSON where-node for an
|
|
210
|
+
or/not, or \`--file\` a whole query document for unions and link traversals.
|
|
197
211
|
|
|
198
212
|
## Configuring an agent
|
|
199
213
|
|
|
@@ -239,14 +253,13 @@ means someone else changed it: re-read and retry, never force.
|
|
|
239
253
|
## Building an app
|
|
240
254
|
|
|
241
255
|
\`\`\`bash
|
|
242
|
-
frontera app init <name> #
|
|
243
|
-
|
|
256
|
+
frontera app init <name> # Next.js static export — installs with public npm only
|
|
257
|
+
cd <name>
|
|
244
258
|
frontera blueprint list # what data this workspace actually has
|
|
245
|
-
frontera app
|
|
246
|
-
bun run
|
|
247
|
-
|
|
248
|
-
frontera app
|
|
249
|
-
frontera app promote <version>
|
|
259
|
+
frontera app dev # the App against real data, with a short-lived token
|
|
260
|
+
bun run check # typecheck, tests and build: the gate
|
|
261
|
+
frontera app deploy --build # build, then a preview deployment with its link
|
|
262
|
+
frontera app promote <deployment>
|
|
250
263
|
\`\`\`
|
|
251
264
|
|
|
252
265
|
Read \`frontera blueprint list\` and \`frontera blueprint get <apiName>\` before
|
|
@@ -254,19 +267,24 @@ designing against data. What they return is the slice this workspace is granted,
|
|
|
254
267
|
which is exactly what the app will be able to query at runtime — so anything
|
|
255
268
|
absent there will be absent for the app too.
|
|
256
269
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
The SDK depends on how the App was made:
|
|
271
|
+
|
|
272
|
+
- **Next.js (the default).** \`@frontera-sdk/core\` and \`@frontera-sdk/blueprint\`
|
|
273
|
+
(and \`@frontera-sdk/agents\` when used) are public npm packages in
|
|
274
|
+
\`package.json\`, kept at the release \`frontera app init\` wrote and updated
|
|
275
|
+
with the package manager. Never copy SDK source into \`src/\`. Open the App
|
|
276
|
+
through \`frontera app dev\`: it serves on \`http://127.0.0.1:<port>\` and
|
|
277
|
+
hands the page its session; never write that session into \`.env\`.
|
|
278
|
+
- **Vite (legacy, \`--framework react\`).** \`src/frontera/\` is the SDK, copied
|
|
279
|
+
in as source. Read it; never edit it, never rewrite an \`@frontera-sdk/…\`
|
|
280
|
+
import to a relative path, and never add \`@frontera-sdk/*\` to
|
|
281
|
+
\`package.json\` there. \`frontera app sdk sync\` refreshes the copy. Opening
|
|
282
|
+
\`/\` renders "This app runs inside Frontera" (the handshake refuses an
|
|
283
|
+
unknown parent); open **\`/dev-host.html\`** instead, which frames the app and
|
|
284
|
+
plays the host side of the bridge.
|
|
285
|
+
|
|
286
|
+
The project's own skills (\`.agents/skills/\`) have the detail for its
|
|
287
|
+
framework.
|
|
270
288
|
`
|
|
271
289
|
|
|
272
290
|
export const AGENTS_MD = `# Frontera
|
package/src/heal.ts
CHANGED
|
@@ -45,9 +45,10 @@
|
|
|
45
45
|
* CLI (`src/vendor/sdk-sources.json`).
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
48
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
49
49
|
import { dirname, join } from 'node:path'
|
|
50
50
|
|
|
51
|
+
import { lockedSdkVersions, lockedSpec, recordLockedRanges } from './app-lock'
|
|
51
52
|
import {
|
|
52
53
|
APP_SDK_PACKAGES,
|
|
53
54
|
LEGACY_SDK_ALIASES,
|
|
@@ -102,6 +103,10 @@ export function healProject(dir: string): HealResult {
|
|
|
102
103
|
|
|
103
104
|
const name = typeof manifest.name === 'string' && manifest.name ? manifest.name : 'app'
|
|
104
105
|
let manifestChanged = false
|
|
106
|
+
// What the App's own lockfile installed from the registry: the SDK its code was written against.
|
|
107
|
+
const locked = lockedSdkVersions(dir)
|
|
108
|
+
// The specs restored to a locked release, recorded in the lockfile too so the two agree.
|
|
109
|
+
const restored: Record<string, string> = {}
|
|
105
110
|
|
|
106
111
|
// Packages a legacy tree needs declared under their CURRENT name as well, so
|
|
107
112
|
// the vendored copy resolves the siblings it imports internally.
|
|
@@ -131,6 +136,18 @@ export function healProject(dir: string): HealResult {
|
|
|
131
136
|
if (typeof spec !== 'string' || !UNRESOLVABLE.test(spec)) continue
|
|
132
137
|
|
|
133
138
|
const pkg = currentSdkPackage(dep)
|
|
139
|
+
// The lockfile installed a published version: that is the SDK the code was checked against, so
|
|
140
|
+
// the App goes back to it rather than to a copy of another SDK (a pulled App's typecheck depends on it).
|
|
141
|
+
const lockedAt = pkg && dep === pkg ? locked.get(pkg) : undefined
|
|
142
|
+
if (lockedAt) {
|
|
143
|
+
deps[dep] = lockedSpec(lockedAt)
|
|
144
|
+
restored[dep] = deps[dep]
|
|
145
|
+
repairs.push(`restored ${dep} to ${deps[dep]}, the published release its bun.lock installed (was ${truncate(spec)})`)
|
|
146
|
+
manifestChanged = true
|
|
147
|
+
// A copy left under src/frontera/ would still win through tsconfig paths: it goes.
|
|
148
|
+
if (unvendorSdkPackage(dir, pkg!)) repairs.push(`removed the vendored copy in ${APP_SDK_PACKAGES[pkg!].dir} and its tsconfig path`)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
134
151
|
if (pkg) {
|
|
135
152
|
// Already pointing at the copy in this tree — that is the shape this
|
|
136
153
|
// function produces, so re-pulling must not rewrite it back over
|
|
@@ -188,6 +205,10 @@ export function healProject(dir: string): HealResult {
|
|
|
188
205
|
// still carry a stale copy, which is the ordinary state of every app
|
|
189
206
|
// scaffolded before the SDK grew a module.
|
|
190
207
|
if (vendorCurrent(dir, pkg) && (declares(manifest, specifier) || specifier === pkg)) continue
|
|
208
|
+
// Declared with a published range (`^1.43.10`): the package manager installs it, and the code was written against
|
|
209
|
+
// that release. Vendoring this CLI's copy over it swaps the SDK under the code (a pulled Next.js App's case).
|
|
210
|
+
const spec = manifest.dependencies?.[specifier] ?? manifest.devDependencies?.[specifier]
|
|
211
|
+
if (typeof spec === 'string' && !UNRESOLVABLE.test(spec)) continue
|
|
191
212
|
vendorSdkPackage(dir, pkg)
|
|
192
213
|
manifest.dependencies ??= {}
|
|
193
214
|
manifest.dependencies[specifier] = `file:./${APP_SDK_PACKAGES[pkg].dir}`
|
|
@@ -225,6 +246,8 @@ export function healProject(dir: string): HealResult {
|
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
if (manifestChanged) writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
249
|
+
// The lockfile's record of what package.json asks for follows, so a frozen install takes the restored package.json.
|
|
250
|
+
if (recordLockedRanges(dir, restored)) repairs.push(`recorded ${Object.entries(restored).map(([d, s]) => `${d} ${s}`).join(' and ')} in the lockfile, which already installed that release`)
|
|
228
251
|
|
|
229
252
|
// Point the editor at the vendored copy directly. Without this TypeScript
|
|
230
253
|
// reaches it through the `file:` link instead and reports every diagnostic
|
|
@@ -246,6 +269,94 @@ export function healProject(dir: string): HealResult {
|
|
|
246
269
|
return { repairs }
|
|
247
270
|
}
|
|
248
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Take a vendored SDK package out of an App that installs the published one:
|
|
274
|
+
* its directory under src/frontera/ and the tsconfig `paths` entries that
|
|
275
|
+
* point at it (which would otherwise resolve imports to the copy instead of
|
|
276
|
+
* node_modules). True when anything was removed. A Vite App whose config
|
|
277
|
+
* aliases the copy keeps it: the alias is how it resolves the SDK.
|
|
278
|
+
*/
|
|
279
|
+
/** Does the App's Vite config alias the vendored copy (a scaffolded Vite App resolves the SDK that way)? */
|
|
280
|
+
export function viteAliasesSdk(dir: string, pkg: AppSdkPackage): boolean {
|
|
281
|
+
const rel = APP_SDK_PACKAGES[pkg].dir
|
|
282
|
+
return ['vite.config.ts', 'vite.config.js', 'vite.config.mts'].some((f) => {
|
|
283
|
+
try {
|
|
284
|
+
return readFileSync(join(dir, f), 'utf8').includes(rel)
|
|
285
|
+
} catch {
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function unvendorSdkPackage(dir: string, pkg: AppSdkPackage): boolean {
|
|
292
|
+
const removed = unvendorSdkPackageDetail(dir, pkg)
|
|
293
|
+
return !!removed && (removed.files.length > 0 || removed.tsconfigPaths.length > 0)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** What taking a vendored SDK package out of an App removed: the copy's files, and the tsconfig paths to it. */
|
|
297
|
+
export interface Unvendored {
|
|
298
|
+
/** The copy's directory, relative to the App (`src/frontera/blueprint`). */
|
|
299
|
+
dir: string
|
|
300
|
+
/** Each file deleted with it, relative to that directory, sorted. */
|
|
301
|
+
files: string[]
|
|
302
|
+
/** The directory above it, when it was left empty and deleted too (`src/frontera`). */
|
|
303
|
+
parentRemoved?: string
|
|
304
|
+
/** Each `compilerOptions.paths` key removed from tsconfig.json because it pointed at the copy. */
|
|
305
|
+
tsconfigPaths: string[]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** `unvendorSdkPackage`, saying what it removed; null when the App's Vite config aliases the copy, so it is kept. */
|
|
309
|
+
export function unvendorSdkPackageDetail(dir: string, pkg: AppSdkPackage): Unvendored | null {
|
|
310
|
+
const rel = APP_SDK_PACKAGES[pkg].dir
|
|
311
|
+
if (viteAliasesSdk(dir, pkg)) return null
|
|
312
|
+
const out: Unvendored = { dir: rel, files: [], tsconfigPaths: [] }
|
|
313
|
+
if (existsSync(join(dir, rel))) {
|
|
314
|
+
out.files = filesUnder(join(dir, rel))
|
|
315
|
+
rmSync(join(dir, rel), { recursive: true, force: true })
|
|
316
|
+
const parent = dirname(join(dir, rel))
|
|
317
|
+
try {
|
|
318
|
+
if (readdirSync(parent).length === 0) {
|
|
319
|
+
rmSync(parent, { recursive: true })
|
|
320
|
+
out.parentRemoved = dirname(rel)
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
// Gone already.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const tsconfig = join(dir, 'tsconfig.json')
|
|
327
|
+
try {
|
|
328
|
+
const config = JSON.parse(readFileSync(tsconfig, 'utf8')) as { compilerOptions?: { paths?: Record<string, string[]> } }
|
|
329
|
+
const paths = config.compilerOptions?.paths
|
|
330
|
+
if (paths) {
|
|
331
|
+
for (const [key, targets] of Object.entries(paths)) {
|
|
332
|
+
if (Array.isArray(targets) && targets.some((t) => t.replace(/^\.\//, '').startsWith(`${rel}/`) || t.replace(/^\.\//, '') === rel)) {
|
|
333
|
+
delete paths[key]
|
|
334
|
+
out.tsconfigPaths.push(key)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (out.tsconfigPaths.length) writeFileSync(tsconfig, `${JSON.stringify(config, null, 2)}\n`)
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// No tsconfig, or JSON with comments: left as it is.
|
|
341
|
+
}
|
|
342
|
+
return out
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Every file under a directory, by its path relative to it, sorted. */
|
|
346
|
+
function filesUnder(root: string): string[] {
|
|
347
|
+
const out: string[] = []
|
|
348
|
+
const walk = (at: string, rel: string) => {
|
|
349
|
+
for (const name of readdirSync(at).sort()) {
|
|
350
|
+
const path = join(at, name)
|
|
351
|
+
const r = rel ? `${rel}/${name}` : name
|
|
352
|
+
if (statSync(path).isDirectory()) walk(path, r)
|
|
353
|
+
else out.push(r)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
walk(root, '')
|
|
357
|
+
return out
|
|
358
|
+
}
|
|
359
|
+
|
|
249
360
|
function isSdkPackage(dep: string): dep is AppSdkPackage {
|
|
250
361
|
return (SDK_PACKAGE_NAMES as string[]).includes(dep)
|
|
251
362
|
}
|