@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/pack.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { collectSourceFiles, formatBytes } from './packaging'
|
|
1
|
+
import { collectSourceFiles, formatBytes, type SourceFile } from './packaging'
|
|
2
2
|
import { createTarGz } from './tar'
|
|
3
3
|
import type { Output } from './output'
|
|
4
|
+
import { tsconfigAsKept } from './local/local-app-sdk-paths'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Package a directory for upload, reporting its size.
|
|
@@ -10,8 +11,12 @@ import type { Output } from './output'
|
|
|
10
11
|
* bill — but it moves to stderr via `note`. It is commentary about the work,
|
|
11
12
|
* not the work's result, and mixing it into stdout broke piping.
|
|
12
13
|
*/
|
|
13
|
-
export function packDirectory(dir: string, output: Output, label: string) {
|
|
14
|
-
|
|
14
|
+
export function packDirectory(dir: string, output: Output, label: string, extra: SourceFile[] = []) {
|
|
15
|
+
// `extra`: files kept beside the directory's own (a Function's imports from outside its folder).
|
|
16
|
+
// A tsconfig.json whose SDK-copy paths a run still going (the App's dev server) set aside is packaged as it is the App's.
|
|
17
|
+
const kept = tsconfigAsKept(dir)
|
|
18
|
+
const own = collectSourceFiles(dir).map((f) => (kept && f.rel === 'tsconfig.json' ? { rel: f.rel, bytes: new Uint8Array(kept) } : f))
|
|
19
|
+
const files = [...own, ...extra]
|
|
15
20
|
const totalBytes = files.reduce((n, f) => n + f.bytes.byteLength, 0)
|
|
16
21
|
output.note(` ${label} ${files.length} files, ${formatBytes(totalBytes)}`)
|
|
17
22
|
return { tgz: createTarGz(files), fileCount: files.length, totalBytes }
|
package/src/packaging.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function isExcluded(rel: string): boolean {
|
|
|
61
61
|
* IGNORED for anything in the hard set, so a permissive `.gitignore` can never
|
|
62
62
|
* widen packaging past §6.2.
|
|
63
63
|
*/
|
|
64
|
-
function gitignoreMatcher(root: string): (rel: string) => boolean {
|
|
64
|
+
export function gitignoreMatcher(root: string): (rel: string) => boolean {
|
|
65
65
|
const path = join(root, '.gitignore')
|
|
66
66
|
if (!existsSync(path)) return () => false
|
|
67
67
|
|
package/src/project-bootstrap.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { UsageError } from './errors'
|
|
5
|
+
import { copyInstallLine, repullRefFor, type SdkShadow } from './local/local-app-code'
|
|
6
|
+
import { installFromLock, lockInstallLine } from './local/local-app-lock-install'
|
|
7
|
+
import { copyInstallShadows, lockMismatchAdvice, vendoredShadows } from './local/local-app-sdk'
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* The steps between "files written" and "a project someone can work in".
|
|
@@ -74,6 +77,53 @@ export interface InstallResult {
|
|
|
74
77
|
installed: boolean
|
|
75
78
|
/** The last line bun printed — its own package count and timing. */
|
|
76
79
|
summary: string
|
|
80
|
+
/** Installed from the lockfile as it is (`--frozen-lockfile`), and it disagreed with package.json: nothing was written. */
|
|
81
|
+
lockMismatch?: boolean
|
|
82
|
+
/**
|
|
83
|
+
* The install went as package.json says, the SDK from the App's own copy
|
|
84
|
+
* under src/frontera/ that shadows the release its bun.lock records,
|
|
85
|
+
* without writing either file (`bun install --no-save`; `installed` says
|
|
86
|
+
* whether it worked): those packages, with the release bun.lock records.
|
|
87
|
+
*/
|
|
88
|
+
fromCopy?: SdkShadow[]
|
|
89
|
+
/**
|
|
90
|
+
* Installed as bun.lock records it, from npm, where package.json names the
|
|
91
|
+
* App's own SDK copy (`installFromLock`): those packages, with the release
|
|
92
|
+
* installed. Neither file was written.
|
|
93
|
+
*/
|
|
94
|
+
fromLock?: SdkShadow[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface InstallOptions {
|
|
98
|
+
/** Install exactly what the lockfile says, never rewriting it (`bun install --frozen-lockfile`). */
|
|
99
|
+
frozenLockfile?: boolean
|
|
100
|
+
/** Install as package.json says without writing package.json or the lockfile (`bun install --no-save`). */
|
|
101
|
+
noSave?: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The files an install must leave as they are, as they are now. */
|
|
105
|
+
const TRACKED_INSTALL_FILES = ['package.json', 'bun.lock', 'bun.lockb']
|
|
106
|
+
function snapshotFiles(dir: string): Map<string, Buffer> {
|
|
107
|
+
const out = new Map<string, Buffer>()
|
|
108
|
+
for (const name of TRACKED_INSTALL_FILES) if (existsSync(join(dir, name))) out.set(name, readFileSync(join(dir, name)))
|
|
109
|
+
return out
|
|
110
|
+
}
|
|
111
|
+
/** Put each back that changed (removing one that appeared); the names put back. */
|
|
112
|
+
function restoreFiles(dir: string, before: Map<string, Buffer>): string[] {
|
|
113
|
+
const restored: string[] = []
|
|
114
|
+
for (const name of TRACKED_INSTALL_FILES) {
|
|
115
|
+
const path = join(dir, name)
|
|
116
|
+
const was = before.get(name)
|
|
117
|
+
if (!was) {
|
|
118
|
+
if (!existsSync(path)) continue
|
|
119
|
+
rmSync(path, { force: true })
|
|
120
|
+
restored.push(name)
|
|
121
|
+
} else if (!existsSync(path) || !readFileSync(path).equals(was)) {
|
|
122
|
+
writeFileSync(path, was)
|
|
123
|
+
restored.push(name)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return restored
|
|
77
127
|
}
|
|
78
128
|
|
|
79
129
|
/**
|
|
@@ -85,9 +135,61 @@ export interface InstallResult {
|
|
|
85
135
|
* rather than inherited because stdout carries command data and nothing else;
|
|
86
136
|
* everything here is progress, which belongs on stderr.
|
|
87
137
|
*/
|
|
88
|
-
|
|
138
|
+
/**
|
|
139
|
+
* An App's packages, installed when it has none yet (a pulled App comes
|
|
140
|
+
* without node_modules): `bun install` in its folder. Null when it already
|
|
141
|
+
* has them; otherwise what the install did, for the caller to say.
|
|
142
|
+
*
|
|
143
|
+
* With a lockfile, exactly what it records (`--frozen-lockfile`): a pulled
|
|
144
|
+
* App's bun.lock is the record of the SDK its code was written against, and
|
|
145
|
+
* an install that rewrote it to match a changed package.json would lose that
|
|
146
|
+
* record and change a tracked file behind the person's back. When the two
|
|
147
|
+
* disagree nothing is installed or written, and `summary` says why and what
|
|
148
|
+
* fixes it (`lockMismatchAdvice`).
|
|
149
|
+
*
|
|
150
|
+
* Except when they disagree because package.json names the App's own SDK
|
|
151
|
+
* copy under src/frontera/ where bun.lock installed a published release (an
|
|
152
|
+
* older pull's repair). Then what bun.lock records is installed from npm,
|
|
153
|
+
* the deployed App's actual dependencies, in a temporary folder whose copy
|
|
154
|
+
* of package.json names those releases (`installFromLock`); `fromLock` says
|
|
155
|
+
* so. Offline, or refused, and the copy is there (`copyInstallShadows`): the
|
|
156
|
+
* packages are installed as package.json says, the SDK from that copy, with
|
|
157
|
+
* `bun install --no-save`, so the App previews and checks while package.json
|
|
158
|
+
* and bun.lock stay as pulled (either put back if the install changed it).
|
|
159
|
+
* `fromCopy` names those packages and `summary` says so, with what brings the
|
|
160
|
+
* deployed source back.
|
|
161
|
+
*/
|
|
162
|
+
export async function ensureAppPackages(
|
|
163
|
+
appRoot: string,
|
|
164
|
+
install: (dir: string, options?: InstallOptions) => Promise<InstallResult> = installDependencies,
|
|
165
|
+
): Promise<InstallResult | null> {
|
|
166
|
+
if (!existsSync(join(appRoot, 'package.json')) || existsSync(join(appRoot, 'node_modules'))) return null
|
|
167
|
+
if (!['bun.lock', 'bun.lockb'].some((f) => existsSync(join(appRoot, f)))) return install(appRoot)
|
|
168
|
+
// package.json names the SDK copy where bun.lock installed a release: what bun.lock records comes first, from npm.
|
|
169
|
+
const shadowed = vendoredShadows(appRoot)
|
|
170
|
+
const fromLock = shadowed.length ? await installFromLock(appRoot, shadowed, install) : null
|
|
171
|
+
if (fromLock?.installed) return { ...fromLock, summary: lockInstallLine(fromLock.fromLock, fromLock.summary) }
|
|
172
|
+
// Offline, or the registry refused it: the App's own SDK copy, as before.
|
|
173
|
+
const notFromLock = fromLock ? `the install of what bun.lock records from npm failed (${fromLock.summary}), so ` : ''
|
|
174
|
+
const fromCopy = copyInstallShadows(appRoot)
|
|
175
|
+
if (fromCopy.length) {
|
|
176
|
+
// Read before the install: whether the folder is still as an earlier pull left it.
|
|
177
|
+
const repull = repullRefFor(appRoot)
|
|
178
|
+
const before = snapshotFiles(appRoot)
|
|
179
|
+
const result = await install(appRoot, { noSave: true })
|
|
180
|
+
const putBack = restoreFiles(appRoot, before)
|
|
181
|
+
const restored = putBack.length ? `; the install changed ${putBack.join(' and ')}, put back as pulled` : ''
|
|
182
|
+
if (!result.installed) return { ...result, fromCopy, summary: `${notFromLock}\`bun install --no-save\` (from the App’s own SDK copy under src/frontera/, keeping bun.lock as pulled) failed: ${result.summary}${restored}` }
|
|
183
|
+
return { ...result, fromCopy, summary: `${notFromLock}${copyInstallLine(fromCopy, result.summary, { repull })}${restored}` }
|
|
184
|
+
}
|
|
185
|
+
const result = await install(appRoot, { frozenLockfile: true })
|
|
186
|
+
if (result.installed || !result.lockMismatch) return result
|
|
187
|
+
return { ...result, summary: lockMismatchAdvice(appRoot) }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function installDependencies(target: string, options: InstallOptions = {}): Promise<InstallResult> {
|
|
89
191
|
try {
|
|
90
|
-
const child = Bun.spawn(['bun', 'install'], {
|
|
192
|
+
const child = Bun.spawn(['bun', 'install', ...(options.frozenLockfile ? ['--frozen-lockfile'] : []), ...(options.noSave ? ['--no-save'] : [])], {
|
|
91
193
|
cwd: target,
|
|
92
194
|
stdin: 'ignore',
|
|
93
195
|
stdout: 'pipe',
|
|
@@ -104,7 +206,8 @@ export async function installDependencies(target: string): Promise<InstallResult
|
|
|
104
206
|
.filter((line) => line.length > 0)
|
|
105
207
|
|
|
106
208
|
if (exitCode !== 0) {
|
|
107
|
-
|
|
209
|
+
const lockMismatch = !!options.frozenLockfile && lines.some((line) => /lockfile had changes|lockfile is frozen|frozen.?lockfile/i.test(line))
|
|
210
|
+
return { installed: false, summary: lines.at(-1) ?? `bun install exited with ${exitCode}`, ...(lockMismatch ? { lockMismatch } : {}) }
|
|
108
211
|
}
|
|
109
212
|
const installed = lines.filter((line) => /packages installed|Checked \d+ install/.test(line))
|
|
110
213
|
return { installed: true, summary: installed.at(-1) ?? lines.at(-1) ?? 'dependencies installed' }
|
package/src/project-context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { dirname, join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { isTrustedBinding, recordBinding } from './binding-registry'
|
|
@@ -197,3 +197,20 @@ export function ensureGitignored(root: string): 'added' | 'present' | 'created'
|
|
|
197
197
|
)
|
|
198
198
|
return 'added'
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Lines `.frontera/.gitignore` keeps out of git, each added once, the file's
|
|
203
|
+
* other lines left as they are. `context.json` is always among them: which
|
|
204
|
+
* profile a directory is bound to is this machine's choice, and a copy of the
|
|
205
|
+
* project (a clone, a pull, a folder copied) must not carry it to another
|
|
206
|
+
* machine or another directory, where it would name someone else's runtime.
|
|
207
|
+
*/
|
|
208
|
+
export function ignoreInFronteraDir(root: string, entries: readonly string[]): void {
|
|
209
|
+
const path = join(root, CONTEXT_DIR, '.gitignore')
|
|
210
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
211
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : ''
|
|
212
|
+
const have = new Set(text.split(/\r?\n/).map((l) => l.trim().replace(/^\//, '').replace(/\/$/, '')))
|
|
213
|
+
const missing = [...new Set([...entries, CONTEXT_FILE])].filter((e) => !have.has(e.replace(/^\//, '').replace(/\/$/, '')))
|
|
214
|
+
if (!missing.length) return
|
|
215
|
+
writeFileAtomic(path, `${text}${text && !text.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`, 0o644)
|
|
216
|
+
}
|
package/src/project.ts
CHANGED
|
@@ -11,7 +11,10 @@ export interface ProjectConfig {
|
|
|
11
11
|
slug: string
|
|
12
12
|
displayName: string
|
|
13
13
|
description?: string
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The version a deploy names as its parent: the last deployment from this
|
|
16
|
+
* folder, else the version the working tree was pulled from.
|
|
17
|
+
*/
|
|
15
18
|
parentVersion?: string | null
|
|
16
19
|
/** Origins the app may reach at runtime; become the served CSP. */
|
|
17
20
|
connectDomains: string[]
|
|
@@ -84,7 +87,8 @@ export function readProject(dir: string): ProjectConfig {
|
|
|
84
87
|
slug,
|
|
85
88
|
displayName: config.displayName ?? pkg.frontera?.displayName ?? slug,
|
|
86
89
|
description: config.description ?? pkg.frontera?.description,
|
|
87
|
-
|
|
90
|
+
// A folder never deployed from keeps its lineage: the version it was pulled from is the parent.
|
|
91
|
+
parentVersion: state.parentVersion ?? state.pulledVersion ?? null,
|
|
88
92
|
connectDomains: stringList(config.connectDomains ?? pkg.frontera?.connectDomains, 'connectDomains'),
|
|
89
93
|
resourceDomains: stringList(config.resourceDomains ?? pkg.frontera?.resourceDomains, 'resourceDomains'),
|
|
90
94
|
outputDirectory: safeOutputDirectory(
|
|
@@ -137,8 +141,29 @@ export function readPackageVersion(dir: string): string {
|
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
interface ProjectState {
|
|
144
|
+
/** The last deployment made from this folder (`app deploy`): what it was deployed as. */
|
|
140
145
|
parentVersion?: string | null
|
|
141
146
|
appId?: string
|
|
147
|
+
/**
|
|
148
|
+
* The version the working tree was pulled from (`app pull`, or a local
|
|
149
|
+
* project's pull of an App's code), kept apart from `parentVersion` so a
|
|
150
|
+
* pulled folder is not read as deployed from here.
|
|
151
|
+
*/
|
|
152
|
+
pulledVersion?: string | null
|
|
153
|
+
/** The App it was pulled from, by its id where it was pulled. */
|
|
154
|
+
pulledFrom?: string | null
|
|
155
|
+
/**
|
|
156
|
+
* Each source file as the pull left it, by path in the folder, with its hash
|
|
157
|
+
* (`local-app-pulled.ts`): `local check` counts an App still like this as
|
|
158
|
+
* inherited from the workspace.
|
|
159
|
+
*/
|
|
160
|
+
pulledFiles?: Record<string, string>
|
|
161
|
+
/**
|
|
162
|
+
* A deployment from here that a database restore took away
|
|
163
|
+
* (`local-app-restore.ts`): the next `frontera local dev` deploys again,
|
|
164
|
+
* and the deploy clears it.
|
|
165
|
+
*/
|
|
166
|
+
restoredAway?: { deployment: string; restoredAt: string }
|
|
142
167
|
}
|
|
143
168
|
|
|
144
169
|
export function readState(dir: string): ProjectState {
|
package/src/provenance.ts
CHANGED
|
@@ -13,6 +13,16 @@ export interface AppBuildProvenance {
|
|
|
13
13
|
lockfilePath?: string
|
|
14
14
|
lockfileDigest?: string
|
|
15
15
|
packagedAt: string
|
|
16
|
+
/** The Git branch the working copy is on, read locally. Informational, never authorization. */
|
|
17
|
+
branch?: string
|
|
18
|
+
/** The commit's author, `Name <email>`, read locally. */
|
|
19
|
+
commitAuthor?: string
|
|
20
|
+
/** The Forge session (and its harness) the deploy ran in, when it ran in one. */
|
|
21
|
+
session?: { id: string; harness: string | null }
|
|
22
|
+
/** What the CLI checked before uploading (design 2026-10-18 §6). */
|
|
23
|
+
checks?: { build: { status: 'passed' | 'failed' | 'skipped'; detail: string } }
|
|
24
|
+
/** What the App calls, for the platform's contract check. */
|
|
25
|
+
contract?: { digest?: string; objectTypes: Record<string, string[]>; actions: string[]; functions: string[] }
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
const LOCKFILES = ['bun.lock', 'bun.lockb', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'] as const
|
|
@@ -85,9 +95,24 @@ export function collectBuildProvenance(root: string): AppBuildProvenance {
|
|
|
85
95
|
encoding: 'utf8',
|
|
86
96
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
87
97
|
}).trim().length > 0
|
|
98
|
+
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
99
|
+
// A detached HEAD has no branch; its commit still identifies the source.
|
|
100
|
+
if (branch && branch !== 'HEAD') provenance.branch = branch
|
|
101
|
+
const author = execFileSync('git', ['log', '-1', '--format=%an <%ae>'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
102
|
+
if (author) provenance.commitAuthor = author
|
|
88
103
|
} catch {
|
|
89
104
|
// Source provenance is optional for projects outside Git.
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
const session = forgeSessionOf(process.env)
|
|
108
|
+
if (session) provenance.session = session
|
|
109
|
+
|
|
92
110
|
return provenance
|
|
93
111
|
}
|
|
112
|
+
|
|
113
|
+
/** The Forge session a command runs in: Forge puts `FORGE_SESSION` (and `FORGE_HARNESS`) in every session's environment. */
|
|
114
|
+
export function forgeSessionOf(env: NodeJS.ProcessEnv): { id: string; harness: string | null } | null {
|
|
115
|
+
const id = env.FORGE_SESSION?.trim()
|
|
116
|
+
if (!id) return null
|
|
117
|
+
return { id: id.slice(0, 120), harness: env.FORGE_HARNESS?.trim() || null }
|
|
118
|
+
}
|
package/src/render-evidence.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
2
2
|
import { join, relative, sep } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { isExcluded } from './packaging'
|
|
4
|
+
import { gitignoreMatcher, isExcluded } from './packaging'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Has anybody looked at this app before it goes live?
|
|
@@ -34,6 +34,12 @@ import { isExcluded } from './packaging'
|
|
|
34
34
|
* tree; neither error matters here, because nothing is being enforced. The
|
|
35
35
|
* cheap answer to a reported question beats the expensive answer to a blocked
|
|
36
36
|
* one.
|
|
37
|
+
*
|
|
38
|
+
* The tooling's own temporary changes are not source changes, so they must
|
|
39
|
+
* not move an mtime: what `app dev` and `local check` rewrite for a run (the
|
|
40
|
+
* SDK copy's `paths` in `tsconfig.json`, what the dev server or a build
|
|
41
|
+
* writes) goes back byte for byte with the time it had (`putBackAsWas`). A
|
|
42
|
+
* put-back that left a new mtime made every render before it stale.
|
|
37
43
|
*/
|
|
38
44
|
|
|
39
45
|
/** Written by `render_app`; see RENDER_EVIDENCE_RELATIVE_PATH in the service. */
|
|
@@ -59,6 +65,18 @@ export interface RenderEvidence {
|
|
|
59
65
|
path?: string
|
|
60
66
|
/** What the browser console said at render time, if the renderer recorded it. */
|
|
61
67
|
consoleErrors?: number
|
|
68
|
+
/** Requests that failed at render time. */
|
|
69
|
+
failedRequests?: number
|
|
70
|
+
/** The basic accessibility scan, kept only for a fresh render: serious and critical findings are `violations`. */
|
|
71
|
+
a11y?: { violations: number; findings?: unknown[] }
|
|
72
|
+
/** What was rendered: the App's dev server or one of its previews, and the address. Absent on a render recorded before this was kept. */
|
|
73
|
+
of?: 'dev-server' | 'preview'
|
|
74
|
+
url?: string
|
|
75
|
+
/**
|
|
76
|
+
* The last render was not of this App (another address, or another App's
|
|
77
|
+
* page): it is not attached, and this deployment counts as not rendered.
|
|
78
|
+
*/
|
|
79
|
+
notThisApp?: { url: string; why: string }
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
interface RenderRecord {
|
|
@@ -66,17 +84,31 @@ interface RenderRecord {
|
|
|
66
84
|
renderedAtMs?: number
|
|
67
85
|
path?: string
|
|
68
86
|
consoleErrors?: number
|
|
87
|
+
failedRequests?: number
|
|
88
|
+
a11y?: { violations?: unknown; findings?: unknown[] }
|
|
89
|
+
target?: { of?: unknown; url?: unknown; app?: unknown }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Files a framework rewrites on every run, which nobody authors. `next dev`
|
|
94
|
+
* and `next build` each write `next-env.d.ts` with their own types folder, so
|
|
95
|
+
* building after a render made every render stale.
|
|
96
|
+
*/
|
|
97
|
+
export function isGeneratedFile(rel: string): boolean {
|
|
98
|
+
return rel === 'next-env.d.ts' || rel.endsWith('.tsbuildinfo')
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
/**
|
|
72
102
|
* Newest mtime across the files that would be packaged.
|
|
73
103
|
*
|
|
74
|
-
* Same exclusion set as packaging
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* everyone would stop
|
|
104
|
+
* Same exclusion set as packaging (the hard set and the project's
|
|
105
|
+
* `.gitignore`), so `node_modules` churn and build output cannot make a tree
|
|
106
|
+
* look edited — the ONE failure mode that would make this report worthless,
|
|
107
|
+
* because it would report `stale` every single time and everyone would stop
|
|
108
|
+
* reading it.
|
|
78
109
|
*/
|
|
79
110
|
export function newestSourceChangeMs(root: string): number {
|
|
111
|
+
const ignored = gitignoreMatcher(root)
|
|
80
112
|
let newest = 0
|
|
81
113
|
const walk = (dir: string) => {
|
|
82
114
|
let entries: string[]
|
|
@@ -102,6 +134,7 @@ export function newestSourceChangeMs(root: string): number {
|
|
|
102
134
|
walk(full)
|
|
103
135
|
continue
|
|
104
136
|
}
|
|
137
|
+
if (ignored(rel) || isGeneratedFile(rel)) continue
|
|
105
138
|
newest = Math.max(newest, stat.mtimeMs)
|
|
106
139
|
}
|
|
107
140
|
}
|
|
@@ -109,7 +142,13 @@ export function newestSourceChangeMs(root: string): number {
|
|
|
109
142
|
return newest
|
|
110
143
|
}
|
|
111
144
|
|
|
112
|
-
|
|
145
|
+
/**
|
|
146
|
+
* The last render, as evidence of this App's current source. With `app` (the
|
|
147
|
+
* App's slug), a render of an address that is not its dev server or one of
|
|
148
|
+
* its previews, or a render made for another App, is not evidence of it: it
|
|
149
|
+
* comes back unrendered, saying what it was of.
|
|
150
|
+
*/
|
|
151
|
+
export function readRenderEvidence(root: string, app?: string): RenderEvidence {
|
|
113
152
|
const path = join(root, RENDER_EVIDENCE_PATH)
|
|
114
153
|
if (!existsSync(path)) return { rendered: false, reason: 'none' }
|
|
115
154
|
|
|
@@ -126,13 +165,29 @@ export function readRenderEvidence(root: string): RenderEvidence {
|
|
|
126
165
|
const renderedAtMs = record.renderedAtMs ?? (record.renderedAt ? Date.parse(record.renderedAt) : NaN)
|
|
127
166
|
if (!Number.isFinite(renderedAtMs)) return { rendered: false, reason: 'none' }
|
|
128
167
|
|
|
168
|
+
const target = record.target
|
|
169
|
+
const url = typeof target?.url === 'string' ? target.url : undefined
|
|
170
|
+
if (app && target && url) {
|
|
171
|
+
const why = target.app !== undefined && target.app !== app
|
|
172
|
+
? `it was rendered for ${String(target.app)}, not ${app}`
|
|
173
|
+
: target.of === 'elsewhere'
|
|
174
|
+
? `it is not ${app}'s dev server or one of its previews`
|
|
175
|
+
: null
|
|
176
|
+
if (why) return { rendered: false, reason: 'none', notThisApp: { url, why } }
|
|
177
|
+
}
|
|
178
|
+
const of = target?.of === 'dev-server' || target?.of === 'preview' ? target.of : undefined
|
|
129
179
|
const rendered = renderedAtMs >= newestSourceChangeMs(root)
|
|
130
180
|
return {
|
|
131
181
|
rendered,
|
|
182
|
+
...(of ? { of } : {}),
|
|
183
|
+
...(url ? { url } : {}),
|
|
132
184
|
reason: rendered ? 'fresh' : 'stale',
|
|
133
185
|
...(record.renderedAt ? { renderedAt: record.renderedAt } : {}),
|
|
134
186
|
...(record.path ? { path: record.path } : {}),
|
|
135
187
|
...(typeof record.consoleErrors === 'number' ? { consoleErrors: record.consoleErrors } : {}),
|
|
188
|
+
...(typeof record.failedRequests === 'number' ? { failedRequests: record.failedRequests } : {}),
|
|
189
|
+
// A scan of an earlier tree says nothing about this one.
|
|
190
|
+
...(rendered && typeof record.a11y?.violations === 'number' ? { a11y: { violations: record.a11y.violations, findings: (record.a11y.findings ?? []).slice(0, 20) } } : {}),
|
|
136
191
|
}
|
|
137
192
|
}
|
|
138
193
|
|
|
@@ -167,9 +222,12 @@ export function readRenderThumbnail(root: string): Uint8Array | null {
|
|
|
167
222
|
* moves on.
|
|
168
223
|
*/
|
|
169
224
|
export function describeRenderEvidence(evidence: RenderEvidence): string {
|
|
225
|
+
if (evidence.notThisApp) {
|
|
226
|
+
return ` not attaching the last render, of ${evidence.notThisApp.url}: ${evidence.notThisApp.why}; render the App's dev server (app_render with no url) and deploy again`
|
|
227
|
+
}
|
|
170
228
|
switch (evidence.reason) {
|
|
171
229
|
case 'fresh':
|
|
172
|
-
return ` rendered ${evidence.path ?? '/'}${
|
|
230
|
+
return ` rendered ${evidence.url ? `${evidence.url}${evidence.of === 'preview' ? ' (a preview)' : ''}` : evidence.path ?? '/'}${
|
|
173
231
|
evidence.consoleErrors ? ` (${evidence.consoleErrors} console error${evidence.consoleErrors === 1 ? '' : 's'} at render time)` : ''
|
|
174
232
|
}`
|
|
175
233
|
case 'stale':
|
package/src/scopes.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function commandScope(meta: {
|
|
|
54
54
|
|
|
55
55
|
/** For prose: the organization-scoped nouns that actually reach the service. */
|
|
56
56
|
export function orgScopedServiceNouns(): string[] {
|
|
57
|
-
return [...ORG_SCOPED_NOUNS].filter((n) => n !== 'auth' && n !== 'kit').sort()
|
|
57
|
+
return [...ORG_SCOPED_NOUNS].filter((n) => n !== 'auth' && n !== 'kit').sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
package/src/template.ts
CHANGED
|
@@ -574,14 +574,20 @@ const instance = useObjectInstance('SupportTicket', selectedId)
|
|
|
574
574
|
const version = recordVersionOf(instance.data)
|
|
575
575
|
\`\`\`
|
|
576
576
|
|
|
577
|
-
**A
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
577
|
+
**A row from \`useObjects\` does not carry the version.** It arrives as
|
|
578
|
+
\`_meta.recordVersion\`, only for an editable type: always from the INSTANCE
|
|
579
|
+
route, and on a query's rows only when the query asks for it (\`recordVersions\`,
|
|
580
|
+
which Functions, workflows and the CLI ask for). The App SDK's queries do not
|
|
581
|
+
ask, so a control that edits a row in a table fetches that row's instance first:
|
|
582
|
+
selecting a row and submitting straight from \`useObjects\` data sends no
|
|
583
|
+
version at all.
|
|
584
|
+
|
|
585
|
+
An edit of an existing record always compares versions, so a missing version
|
|
586
|
+
is refused, not skipped: the submit fails and nothing changes. The platform
|
|
587
|
+
fills the key input and the \`expectedVersion\` input from the subject (the
|
|
588
|
+
record and the version you submit against), and refuses the submit when the
|
|
589
|
+
inputs you pass disagree with it. Use \`recordVersionOf\` rather than reaching
|
|
590
|
+
into \`_meta\` yourself.
|
|
585
591
|
|
|
586
592
|
A stale value IS refused, and that refusal is the feature — report it as
|
|
587
593
|
"someone else changed this, reload" rather than retrying with a fresher version
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
* build to be green.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import vendored from '../vendor/sdk-sources.json'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The `@frontera-sdk/*` range a Next App depends on: the release of the SDK
|
|
27
|
+
* this CLI carries, so a new App starts on the SDK the CLI was tested with
|
|
28
|
+
* rather than on a floor several releases old. The packages are public npm
|
|
29
|
+
* packages; only the legacy Vite scaffold vendors their source.
|
|
30
|
+
*/
|
|
31
|
+
export const SDK_RANGE = `^${vendored.sdkVersion}`
|
|
32
|
+
|
|
23
33
|
interface ScaffoldInput {
|
|
24
34
|
/** Project name — the package name, the display name and the page title. */
|
|
25
35
|
name: string
|
|
@@ -62,8 +72,8 @@ function projectFiles(name: string): Record<string, string> {
|
|
|
62
72
|
deploy: 'bun run check && frontera app deploy',
|
|
63
73
|
},
|
|
64
74
|
dependencies: {
|
|
65
|
-
'@frontera-sdk/blueprint':
|
|
66
|
-
'@frontera-sdk/core':
|
|
75
|
+
'@frontera-sdk/blueprint': SDK_RANGE,
|
|
76
|
+
'@frontera-sdk/core': SDK_RANGE,
|
|
67
77
|
'@tanstack/react-query': '^5.90.21',
|
|
68
78
|
// Everything from here to `tailwind-merge` is what a shadcn
|
|
69
79
|
// component imports. Every item in that registry declares NO
|
|
@@ -120,6 +130,11 @@ function projectFiles(name: string): Record<string, string> {
|
|
|
120
130
|
|
|
121
131
|
'next.config.ts': `import type { NextConfig } from 'next'
|
|
122
132
|
|
|
133
|
+
// This folder is the App's project, also when it sits inside a Frontera
|
|
134
|
+
// solution that has a lockfile of its own: without this, Next picks the
|
|
135
|
+
// outermost lockfile as the workspace root and warns on every run.
|
|
136
|
+
const root = typeof __dirname === 'string' ? __dirname : process.cwd()
|
|
137
|
+
|
|
123
138
|
const nextConfig: NextConfig = {
|
|
124
139
|
output: 'export',
|
|
125
140
|
trailingSlash: true,
|
|
@@ -127,6 +142,11 @@ const nextConfig: NextConfig = {
|
|
|
127
142
|
// Current Frontera SDK packages publish TypeScript source. This is Next's
|
|
128
143
|
// supported boundary for compiling those dependencies inside the App build.
|
|
129
144
|
transpilePackages: ['@frontera-sdk/core', '@frontera-sdk/blueprint', '@frontera-sdk/agents'],
|
|
145
|
+
// AGENTS.md is this project's own. Left on, \`next dev\` rewrites it on every
|
|
146
|
+
// run, the tree is never clean, and a deploy refuses the build as stale.
|
|
147
|
+
agentRules: false,
|
|
148
|
+
turbopack: { root },
|
|
149
|
+
outputFileTracingRoot: root,
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
export default nextConfig
|
|
@@ -213,6 +233,9 @@ export default nextConfig
|
|
|
213
233
|
'.gitignore': `node_modules/
|
|
214
234
|
.next/
|
|
215
235
|
out/
|
|
236
|
+
# Written by \`next dev\` and \`next build\`, each with its own types folder.
|
|
237
|
+
next-env.d.ts
|
|
238
|
+
*.tsbuildinfo
|
|
216
239
|
.frontera/
|
|
217
240
|
.env
|
|
218
241
|
.env.*
|
|
@@ -1032,16 +1055,22 @@ bun run check
|
|
|
1032
1055
|
\`\`\`
|
|
1033
1056
|
|
|
1034
1057
|
\`\`\`bash
|
|
1035
|
-
frontera app deploy
|
|
1058
|
+
frontera app deploy
|
|
1036
1059
|
\`\`\`
|
|
1037
1060
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1061
|
+
A deploy uploads an immutable preview deployment and returns its link, plus
|
|
1062
|
+
the links of this branch's and this session's previews. It never changes what
|
|
1063
|
+
users see. Promote it once it looks right (it needs the promote permission,
|
|
1064
|
+
and the workspace's required checks):
|
|
1040
1065
|
|
|
1041
1066
|
\`\`\`bash
|
|
1042
|
-
frontera app promote <
|
|
1067
|
+
frontera app promote <deployment>
|
|
1043
1068
|
\`\`\`
|
|
1044
1069
|
|
|
1070
|
+
\`frontera app deployments\` lists them; \`frontera app rollback --reason "…"\`
|
|
1071
|
+
returns production to the previous one and pins it until
|
|
1072
|
+
\`frontera app rollback --undo\`.
|
|
1073
|
+
|
|
1045
1074
|
## Layout
|
|
1046
1075
|
|
|
1047
1076
|
\`\`\`text
|