@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/exit.ts
CHANGED
|
@@ -16,7 +16,12 @@ export const EXIT = {
|
|
|
16
16
|
FAILURE: 1,
|
|
17
17
|
/** Bad invocation or bad input. Fix the command. */
|
|
18
18
|
USAGE: 2,
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Someone else changed it first, or the state of the record or the request
|
|
21
|
+
* refuses it (a governed Action's submission criteria, a request whose
|
|
22
|
+
* outcome is unknown). Re-read, then change the input or the record; the
|
|
23
|
+
* same request is refused again, so never retry it as it is.
|
|
24
|
+
*/
|
|
20
25
|
CONFLICT: 3,
|
|
21
26
|
/** Credentials or permission. Neither retry nor rewording helps. */
|
|
22
27
|
AUTH: 4,
|
|
@@ -58,8 +63,75 @@ const USAGE_CODES = new Set([
|
|
|
58
63
|
// Authoring kit. Both are resolved by running a named command, not by retry.
|
|
59
64
|
'KIT_VERSION_MISMATCH',
|
|
60
65
|
'GENERATED_FILE_CONFLICT',
|
|
66
|
+
// The Idempotency-Key was used for a different request: a new key fixes it,
|
|
67
|
+
// re-reading does not. (An effect another request already made is a
|
|
68
|
+
// conflict: see CONFLICT_REASONS.)
|
|
69
|
+
'ACTION_IDEMPOTENCY_CONFLICT',
|
|
70
|
+
// An Action that isn't there to submit, or a submit without the key it needs.
|
|
71
|
+
'ACTION_NOT_FOUND',
|
|
72
|
+
'ACTION_IDEMPOTENCY_KEY_REQUIRED',
|
|
61
73
|
])
|
|
62
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Someone changed it first: re-read, then decide or submit again. Each of
|
|
76
|
+
* these is fixed by re-reading the record or the request, which a retry of
|
|
77
|
+
* the same body never does.
|
|
78
|
+
*/
|
|
79
|
+
const CONFLICT_CODES = new Set([
|
|
80
|
+
'DRAFT_CONFLICT',
|
|
81
|
+
'CONFLICT',
|
|
82
|
+
'VALIDATION_STALE',
|
|
83
|
+
// The record moved past `expectedVersion`: read it again, submit with the new one.
|
|
84
|
+
'ACTION_SOURCE_VERSION_STALE',
|
|
85
|
+
// The request changed under its approvals: read the request again.
|
|
86
|
+
'ACTION_APPROVAL_STALE',
|
|
87
|
+
// The record already has an open request of this Action (`openRequests: one_per_subject`):
|
|
88
|
+
// read that request (`details.openRequest`) instead of submitting another.
|
|
89
|
+
'ACTION_REQUEST_ALREADY_OPEN',
|
|
90
|
+
// Refused by the state of the record, the Action or the request: the same
|
|
91
|
+
// request is refused every time, so exit 1 ("retry") would send a caller
|
|
92
|
+
// round a loop. Each is fixed by re-reading and changing something first.
|
|
93
|
+
// The record or the input fails the Action's submission criteria.
|
|
94
|
+
'ACTION_CRITERIA_FAILED',
|
|
95
|
+
// The Action is switched off, has no binding, its binding is revoked or
|
|
96
|
+
// suspended, or it has no deployment (`ACTION_NOT_DEPLOYED`, the CLI's word
|
|
97
|
+
// for that 409): nothing changes until someone turns it back on or deploys it.
|
|
98
|
+
'ACTION_DISABLED',
|
|
99
|
+
'ACTION_UNBOUND',
|
|
100
|
+
'ACTION_BINDING_REVOKED',
|
|
101
|
+
'ACTION_BINDING_SUSPENDED',
|
|
102
|
+
'ACTION_NOT_DEPLOYED',
|
|
103
|
+
// It needs an approval first, or affects more records than the Action allows.
|
|
104
|
+
'ACTION_APPROVAL_REQUIRED',
|
|
105
|
+
'ACTION_EFFECT_LIMIT_EXCEEDED',
|
|
106
|
+
// The outcome is settled or unknown: read the request (`action requests`),
|
|
107
|
+
// never submit again — a second submit could make the change twice.
|
|
108
|
+
'ACTION_CONFIRMED_NOT_APPLIED',
|
|
109
|
+
'ACTION_OUTCOME_UNKNOWN',
|
|
110
|
+
'ACTION_RESOLUTION_REQUIRED',
|
|
111
|
+
'AUTOMATION_PUBLISH_UNCERTAIN',
|
|
112
|
+
// The dataset's rows are written somewhere else now.
|
|
113
|
+
'DATASET_AUTHORITY_MOVED',
|
|
114
|
+
])
|
|
115
|
+
/**
|
|
116
|
+
* Refusals the service sends under another code with a `reason` that is a
|
|
117
|
+
* conflict: the approval step moved on, or another request already made the
|
|
118
|
+
* effect. They exit 3 whatever their code.
|
|
119
|
+
*/
|
|
120
|
+
const CONFLICT_REASONS = new Set([
|
|
121
|
+
'ACTION_APPROVAL_STEP_CHANGED',
|
|
122
|
+
'ACTION_LOGICAL_EFFECT_CONFLICT',
|
|
123
|
+
'ACTION_SOURCE_VERSION_STALE',
|
|
124
|
+
'ACTION_REQUEST_ALREADY_OPEN',
|
|
125
|
+
])
|
|
126
|
+
|
|
127
|
+
/** A refusal's `reason`, at the top of its details or nested one level under `details`. */
|
|
128
|
+
export function reasonOf(details: unknown): string | undefined {
|
|
129
|
+
if (typeof details !== 'object' || details === null) return undefined
|
|
130
|
+
const body = details as { reason?: unknown; details?: unknown }
|
|
131
|
+
if (typeof body.reason === 'string') return body.reason
|
|
132
|
+
const nested = body.details
|
|
133
|
+
return typeof nested === 'object' && nested !== null && typeof (nested as { reason?: unknown }).reason === 'string' ? (nested as { reason: string }).reason : undefined
|
|
134
|
+
}
|
|
63
135
|
const AUTH_CODES = new Set([
|
|
64
136
|
'UNAUTHORIZED',
|
|
65
137
|
'FORBIDDEN',
|
|
@@ -78,7 +150,9 @@ const AUTH_CODES = new Set([
|
|
|
78
150
|
* an SDK release — which means this function must treat "never seen it" as
|
|
79
151
|
* ordinary, not exceptional.
|
|
80
152
|
*/
|
|
81
|
-
export function exitCodeFor(code: string): ExitCode {
|
|
153
|
+
export function exitCodeFor(code: string, details?: unknown): ExitCode {
|
|
154
|
+
const reason = reasonOf(details)
|
|
155
|
+
if (reason && CONFLICT_REASONS.has(reason)) return EXIT.CONFLICT
|
|
82
156
|
if (USAGE_CODES.has(code)) return EXIT.USAGE
|
|
83
157
|
if (CONFLICT_CODES.has(code)) return EXIT.CONFLICT
|
|
84
158
|
if (AUTH_CODES.has(code)) return EXIT.AUTH
|
|
@@ -98,6 +172,10 @@ const SERVICE_HINTS: Record<string, string> = {
|
|
|
98
172
|
// which case they are in throw a CliError with a specific hint instead.
|
|
99
173
|
CONFLICT: 'the message above names what conflicted — resolve that, then retry',
|
|
100
174
|
VALIDATION_STALE: 're-fetch the resource and re-run validation against the current revision',
|
|
175
|
+
ACTION_SOURCE_VERSION_STALE: 'the record changed first — read it again (`frontera blueprint query`), then submit with its current version as expectedVersion',
|
|
176
|
+
ACTION_APPROVAL_STALE: 'the request changed under its approvals — read it again (`frontera action requests`), then decide',
|
|
177
|
+
ACTION_IDEMPOTENCY_CONFLICT: 'that Idempotency-Key belongs to another request — submit with a new key',
|
|
178
|
+
ACTION_REQUEST_ALREADY_OPEN: 'this record already has an open request of this Action — follow it (`frontera action requests <requestId>`, the id in the details) instead of submitting another',
|
|
101
179
|
// Names neither kind. Both are valid here — a workspace key for agents,
|
|
102
180
|
// skills, plugins and knowledge; an organization key for Blueprint authoring,
|
|
103
181
|
// datasets and Sources — and naming only one sent holders of the other
|
|
@@ -223,6 +301,19 @@ function hintFor(code: string, details: unknown): string | undefined {
|
|
|
223
301
|
return SERVICE_HINTS[code]
|
|
224
302
|
}
|
|
225
303
|
|
|
304
|
+
/**
|
|
305
|
+
* A refusal that names what it refused only in its details (`invalid_plugins`
|
|
306
|
+
* with `{ invalid: ['decision'] }`), said with the names: the message alone
|
|
307
|
+
* leaves nobody able to tell which entry to take out.
|
|
308
|
+
*/
|
|
309
|
+
export function withRefusedNames(message: string, details: unknown): string {
|
|
310
|
+
if (!/^invalid_[a-z_]+$/.test(message) || typeof details !== 'object' || details === null) return message
|
|
311
|
+
const body = details as { invalid?: unknown; details?: { invalid?: unknown } }
|
|
312
|
+
const invalid = Array.isArray(body.invalid) ? body.invalid : Array.isArray(body.details?.invalid) ? body.details.invalid : null
|
|
313
|
+
const names = invalid?.filter((n): n is string => typeof n === 'string') ?? []
|
|
314
|
+
return names.length ? `${message}: ${names.join(', ')}` : message
|
|
315
|
+
}
|
|
316
|
+
|
|
226
317
|
function isFronteraError(
|
|
227
318
|
err: unknown,
|
|
228
319
|
): err is { code: string; message: string; details?: unknown } {
|
|
@@ -246,8 +337,16 @@ export function toEnvelope(err: unknown): ErrorEnvelope {
|
|
|
246
337
|
}
|
|
247
338
|
|
|
248
339
|
if (isFronteraError(err)) {
|
|
340
|
+
// Invoking an Action that has no deployment, through any client: said as that.
|
|
341
|
+
const details = err.details as { details?: { reason?: unknown; readinessReason?: unknown; deploymentStatus?: unknown } ; reason?: unknown; readinessReason?: unknown; deploymentStatus?: unknown } | undefined
|
|
342
|
+
const nd = details?.reason === 'ACTION_NOT_DEPLOYED' ? details : details?.details?.reason === 'ACTION_NOT_DEPLOYED' ? details.details : null
|
|
343
|
+
if (nd) {
|
|
344
|
+
const why = typeof nd.readinessReason === 'string' ? nd.readinessReason : typeof nd.deploymentStatus === 'string' ? `its deployment is ${nd.deploymentStatus}` : 'it has no active deployment'
|
|
345
|
+
return { error: true, code: 'ACTION_NOT_DEPLOYED', message: `${err.message.replace(/[.]$/, '')}: the Action isn't deployed: ${why}`, hint: '`frontera action list` shows why each Action cannot run; `frontera action deploy <name>` builds its write path' }
|
|
346
|
+
}
|
|
249
347
|
const hint = hintFor(err.code, err.details)
|
|
250
|
-
|
|
348
|
+
const reason = reasonOf(err.details)
|
|
349
|
+
return { error: true, code: err.code, message: withRefusedNames(err.message, err.details), ...(hint ? { hint } : {}), ...(reason ? { details: { reason } } : {}) }
|
|
251
350
|
}
|
|
252
351
|
|
|
253
352
|
return {
|
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
|
}
|
|
@@ -59,12 +59,17 @@ async function probe(cwd: string, claudePath: string, log?: Logger): Promise<For
|
|
|
59
59
|
const session = query({
|
|
60
60
|
// Never yields: the initialization response is all this call is for, and
|
|
61
61
|
// a prompt that resolved would send a turn to the model.
|
|
62
|
-
prompt:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
prompt: {
|
|
63
|
+
[Symbol.asyncIterator]() {
|
|
64
|
+
return {
|
|
65
|
+
next: () => new Promise<IteratorResult<never>>((resolve) => {
|
|
66
|
+
const done = () => resolve({ done: true, value: undefined })
|
|
67
|
+
if (abort.signal.aborted) done()
|
|
68
|
+
else abort.signal.addEventListener('abort', done, { once: true })
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
},
|
|
68
73
|
options: {
|
|
69
74
|
cwd,
|
|
70
75
|
pathToClaudeCodeExecutable: claudePath,
|
|
@@ -58,7 +58,7 @@ function readSkillDir(dir: string): Array<{ dirName: string; path: string; conte
|
|
|
58
58
|
return []
|
|
59
59
|
}
|
|
60
60
|
const out: Array<{ dirName: string; path: string; contents: string }> = []
|
|
61
|
-
for (const entry of [...entries].sort()) {
|
|
61
|
+
for (const entry of [...entries].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
|
|
62
62
|
const path = join(dir, entry, 'SKILL.md')
|
|
63
63
|
try {
|
|
64
64
|
out.push({ dirName: entry, path, contents: readFileSync(path, 'utf8') })
|
|
@@ -92,14 +92,14 @@ function pluginSkillRoots(configDir: string): Array<{ plugin: string; dir: strin
|
|
|
92
92
|
} catch {
|
|
93
93
|
return []
|
|
94
94
|
}
|
|
95
|
-
for (const marketplace of marketplaces.sort()) {
|
|
95
|
+
for (const marketplace of marketplaces.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
|
|
96
96
|
let plugins: string[]
|
|
97
97
|
try {
|
|
98
98
|
plugins = readdirSync(join(cache, marketplace))
|
|
99
99
|
} catch {
|
|
100
100
|
continue
|
|
101
101
|
}
|
|
102
|
-
for (const plugin of plugins.sort()) {
|
|
102
|
+
for (const plugin of plugins.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
|
|
103
103
|
let versions: string[]
|
|
104
104
|
try {
|
|
105
105
|
versions = readdirSync(join(cache, marketplace, plugin))
|
|
@@ -107,7 +107,7 @@ function pluginSkillRoots(configDir: string): Array<{ plugin: string; dir: strin
|
|
|
107
107
|
continue
|
|
108
108
|
}
|
|
109
109
|
// Newest version wins when a cache holds more than one.
|
|
110
|
-
for (const version of versions.sort().reverse()) {
|
|
110
|
+
for (const version of versions.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).reverse()) {
|
|
111
111
|
const dir = join(cache, marketplace, plugin, version, 'skills')
|
|
112
112
|
try {
|
|
113
113
|
if (statSync(dir).isDirectory()) {
|
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
|
+
}
|