@frontera-sdk/cli 1.52.39 → 1.52.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +48 -4
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +331 -31
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +875 -88
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +320 -13
- package/src/commands/local/sync.ts +102 -5
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +52 -8
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +112 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +211 -21
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +180 -7
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +346 -35
- package/src/local/local-dev-sync.ts +136 -7
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +205 -7
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +141 -16
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +38 -8
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +1 -1
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +11 -0
- package/src/local/local-sync-push.ts +10 -7
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +343 -32
- package/src/local/local-sync-snapshot.ts +15 -10
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +24 -21
package/src/commands/app/dev.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
1
4
|
import { AppsApi } from '../../api/apps-api'
|
|
2
5
|
import { PlatformApi } from '../../api/platform-api'
|
|
3
|
-
import { createDevSessionBroker } from '../../dev-broker'
|
|
6
|
+
import { createDevSessionBroker, freeLoopbackPort, isLoopbackPortFree } from '../../dev-broker'
|
|
4
7
|
import { CliError, UsageError } from '../../errors'
|
|
8
|
+
import { DEV_OWNER_ENV, enclosingSolution, forgetDevServer, registerDevServer, watchOwner } from '../../mcp/mcp-apps'
|
|
9
|
+
import { ensureAppPackages } from '../../project-bootstrap'
|
|
5
10
|
import { detectAppFramework } from '../../provenance'
|
|
6
11
|
import { writeAppId } from '../../project'
|
|
12
|
+
import { mintDevSession, writeDevSessionState } from '../../local/local-app-dev-session'
|
|
13
|
+
import { keepDevWrittenFiles, leftLine, putBackLine } from '../../local/local-app-dev-files'
|
|
14
|
+
import { setAsideCopyPaths } from '../../local/local-app-sdk-paths'
|
|
7
15
|
import { requireProjectFrom } from './shared'
|
|
8
16
|
import { flagBool, flagString, type Command } from '../types'
|
|
9
17
|
|
|
10
18
|
const DEFAULT_PORT = 3000
|
|
11
19
|
const READY_TIMEOUT_MS = 30_000
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
/** The port `--port` asks for; 0 is any free one, picked when the server starts. */
|
|
22
|
+
export function appPort(raw: string | undefined): number {
|
|
14
23
|
if (raw === undefined) return DEFAULT_PORT
|
|
15
24
|
const port = Number(raw)
|
|
16
|
-
if (!Number.isInteger(port) || port <
|
|
17
|
-
throw new UsageError('--port must be an integer from
|
|
25
|
+
if (raw.trim() === '' || !Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
26
|
+
throw new UsageError('--port must be an integer from 0 to 65535 (0: any free port)', 'for example: frontera app dev --port 3210, or --port 0')
|
|
18
27
|
}
|
|
19
28
|
return port
|
|
20
29
|
}
|
|
@@ -55,6 +64,16 @@ function openBrowser(url: string): void {
|
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
/** How often a dev server an MCP session or Forge started checks that its starter is still there. */
|
|
68
|
+
const OWNER_POLL_MS = 5_000
|
|
69
|
+
/** How long a stopping dev server is given to exit before what it wrote is put back anyway. */
|
|
70
|
+
const STOP_WAIT_MS = 10_000
|
|
71
|
+
|
|
72
|
+
function withoutOwner(env: NodeJS.ProcessEnv): Record<string, string> {
|
|
73
|
+
const { [DEV_OWNER_ENV]: _owner, ...rest } = env
|
|
74
|
+
return rest as Record<string, string>
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
export const appDev: Command = {
|
|
59
78
|
meta: {
|
|
60
79
|
noun: 'app',
|
|
@@ -62,18 +81,40 @@ export const appDev: Command = {
|
|
|
62
81
|
args: [],
|
|
63
82
|
flags: { port: 'string', 'no-open': 'boolean' },
|
|
64
83
|
summary: 'Run an App locally with short-lived authenticated Blueprint access',
|
|
65
|
-
examples: ['frontera app dev', 'frontera app dev --port 3210 --no-open'],
|
|
84
|
+
examples: ['frontera app dev', 'frontera app dev --port 3210 --no-open', 'frontera app dev --port 0'],
|
|
66
85
|
needsProject: true,
|
|
67
86
|
},
|
|
68
87
|
|
|
69
88
|
async run(ctx) {
|
|
70
89
|
const project = requireProjectFrom(ctx)
|
|
71
|
-
const
|
|
90
|
+
const asked = appPort(flagString(ctx, 'port'))
|
|
91
|
+
// `--port 0`: one free on every loopback, said with the URL below.
|
|
92
|
+
const port = asked === 0 ? await freeLoopbackPort() : asked
|
|
93
|
+
// The App binds 127.0.0.1; a process holding the port on ::1 or a wildcard would answer `localhost` instead.
|
|
94
|
+
if (asked !== 0 && !(await isLoopbackPortFree(port))) {
|
|
95
|
+
throw new CliError(`port ${port} is already in use`, {
|
|
96
|
+
code: 'VALIDATION_ERROR',
|
|
97
|
+
hint: `something listens on ${port} (on 127.0.0.1, ::1 or every address); stop it, or choose another: frontera app dev --port ${port + 1}`,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
72
100
|
const appOrigin = `http://127.0.0.1:${port}`
|
|
73
101
|
const framework = detectAppFramework(project.root)
|
|
102
|
+
// A pulled App comes without its packages: installed first, and said, or its dev command isn't there (`next: command not found`).
|
|
103
|
+
if (!existsSync(join(project.root, 'node_modules'))) ctx.output.note('[dev] no node_modules here: installing the App\'s packages first (bun install)')
|
|
104
|
+
const installed = await ensureAppPackages(project.root)
|
|
105
|
+
if (installed?.lockMismatch) throw new CliError(`the App's packages weren't installed: ${installed.summary}`, { code: 'FAILURE', hint: 'bun.lock was left as it is; the message above says which file to change' })
|
|
106
|
+
if (installed && !installed.installed) {
|
|
107
|
+
// From its own SDK copy, the install that keeps package.json and bun.lock as pulled is the one to retry.
|
|
108
|
+
throw new CliError(`the App's packages could not be installed: ${installed.summary}`, { code: 'FAILURE', hint: installed.fromCopy ? 'run `bun install --no-save` in the App folder (it writes neither package.json nor bun.lock) and read its error' : 'run `bun install` in the App folder and read its error' })
|
|
109
|
+
}
|
|
110
|
+
if (installed) ctx.output.note(`[dev] ${installed.summary}`)
|
|
74
111
|
const apps = new AppsApi(ctx.apiUrl, ctx.token)
|
|
75
112
|
const app = await apps.ensureApp(project.slug, project.displayName, project.appId)
|
|
76
113
|
writeAppId(project.root, app.id)
|
|
114
|
+
// The solution this App is in, if any: where status reads how this server's sessions fare.
|
|
115
|
+
const solution = enclosingSolution(project.root)
|
|
116
|
+
// The id sessions are minted for: a restore to a baseline from before the App existed takes it away.
|
|
117
|
+
let appId = app.id
|
|
77
118
|
|
|
78
119
|
let broker: ReturnType<typeof createDevSessionBroker> | null = null
|
|
79
120
|
if (framework === 'next') {
|
|
@@ -87,13 +128,26 @@ export const appDev: Command = {
|
|
|
87
128
|
broker = createDevSessionBroker({
|
|
88
129
|
appOrigin,
|
|
89
130
|
loadSession: async () => {
|
|
90
|
-
const
|
|
131
|
+
const minted = await mintDevSession({
|
|
132
|
+
appId,
|
|
133
|
+
mint: (id) => apps.appToken(id),
|
|
134
|
+
// Gone from the runtime (`local snapshot restore`): registered again by its slug, as `app dev` started.
|
|
135
|
+
reRegister: async () => (await apps.ensureApp(project.slug, project.displayName, null)).id,
|
|
136
|
+
onReRegistered: (id) => {
|
|
137
|
+
if (id === appId) return
|
|
138
|
+
ctx.output.note(`[dev] the runtime no longer had ${app.slug} (a restore of its database?): registered it again as ${id}; \`frontera app deploy\` puts its code back`)
|
|
139
|
+
appId = id
|
|
140
|
+
writeAppId(project.root, id)
|
|
141
|
+
},
|
|
142
|
+
record: solution ? (state) => writeDevSessionState(solution.stateDir, project.slug, state) : undefined,
|
|
143
|
+
})
|
|
144
|
+
const session = minted.session
|
|
91
145
|
return {
|
|
92
146
|
init: {
|
|
93
147
|
type: 'frontera:init',
|
|
94
148
|
token: session.token,
|
|
95
149
|
apiBaseUrl: ctx.apiUrl.replace(/\/+$/, ''),
|
|
96
|
-
appId:
|
|
150
|
+
appId: minted.appId,
|
|
97
151
|
version: 'dev',
|
|
98
152
|
orgId: identity.orgId,
|
|
99
153
|
workspaceId: identity.workspaceId,
|
|
@@ -116,6 +170,13 @@ export const appDev: Command = {
|
|
|
116
170
|
|
|
117
171
|
const targetUrl = framework === 'vite' ? `${appOrigin}/dev-host.html` : appOrigin
|
|
118
172
|
ctx.output.note(`[dev] ${app.slug} at ${targetUrl}`)
|
|
173
|
+
// Installed as its bun.lock records while tsconfig.json still sends the SDK to the older copy under src/frontera/:
|
|
174
|
+
// the server runs with those paths set aside, so it serves the code against the release it was written for.
|
|
175
|
+
const aside = setAsideCopyPaths(project.root)
|
|
176
|
+
if (aside) ctx.output.note(`[dev] ${aside.line}`)
|
|
177
|
+
// The tracked files the dev server writes (`next dev` rewrites next-env.d.ts when it starts) go back as they
|
|
178
|
+
// were once it is ready, and again when it stops: a preview is not a change. Never a change of the person's.
|
|
179
|
+
const kept = keepDevWrittenFiles(project.root)
|
|
119
180
|
|
|
120
181
|
let child: Bun.Subprocess
|
|
121
182
|
try {
|
|
@@ -125,9 +186,10 @@ export const appDev: Command = {
|
|
|
125
186
|
: ['bun', 'run', 'dev', '--', '--hostname', '127.0.0.1', '--port', String(port)],
|
|
126
187
|
{
|
|
127
188
|
cwd: project.root,
|
|
189
|
+
// The owner is this command's to watch, not the framework's: nothing under it inherits the lifetime.
|
|
128
190
|
env: broker
|
|
129
|
-
? { ...process.env, NEXT_PUBLIC_FRONTERA_DEV_SESSION_ENDPOINT: broker.endpoint }
|
|
130
|
-
: process.env,
|
|
191
|
+
? { ...withoutOwner(process.env), NEXT_PUBLIC_FRONTERA_DEV_SESSION_ENDPOINT: broker.endpoint }
|
|
192
|
+
: withoutOwner(process.env),
|
|
131
193
|
stdin: 'inherit',
|
|
132
194
|
stdout: 'inherit',
|
|
133
195
|
stderr: 'inherit',
|
|
@@ -135,6 +197,7 @@ export const appDev: Command = {
|
|
|
135
197
|
)
|
|
136
198
|
} catch (error) {
|
|
137
199
|
broker?.server.stop(true)
|
|
200
|
+
aside?.restore()
|
|
138
201
|
throw error
|
|
139
202
|
}
|
|
140
203
|
|
|
@@ -148,10 +211,32 @@ export const appDev: Command = {
|
|
|
148
211
|
const onSignal = () => stop()
|
|
149
212
|
process.on('SIGINT', onSignal)
|
|
150
213
|
process.on('SIGTERM', onSignal)
|
|
214
|
+
// Started by an MCP session (`app_dev`, `app_render`) or Forge (its Start): it stops when that process ends,
|
|
215
|
+
// however it ends, so a server an agent or Forge started never outlives it. The starter recorded the server itself.
|
|
216
|
+
const owner = Number(process.env[DEV_OWNER_ENV]) || null
|
|
217
|
+
const unwatch = owner
|
|
218
|
+
? watchOwner(
|
|
219
|
+
owner,
|
|
220
|
+
() => {
|
|
221
|
+
ctx.output.note(`[dev] the process that started this server (pid ${owner}) ended, so the server stops`)
|
|
222
|
+
// The whole group this server leads (it was started detached): the framework's server under `bun run dev` too.
|
|
223
|
+
try {
|
|
224
|
+
process.kill(-process.pid, 'SIGTERM')
|
|
225
|
+
} catch {
|
|
226
|
+
stop()
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
OWNER_POLL_MS,
|
|
230
|
+
)
|
|
231
|
+
: null
|
|
151
232
|
|
|
152
233
|
try {
|
|
153
234
|
await waitForApp(targetUrl, child)
|
|
235
|
+
const back = putBackLine(kept.afterReady(), 'started')
|
|
236
|
+
if (back) ctx.output.note(`[dev] ${back}`)
|
|
154
237
|
ctx.output.note(`[dev] ready: ${targetUrl}`)
|
|
238
|
+
// In a solution, say so where its agents look, so they render this server instead of starting another.
|
|
239
|
+
if (solution && !owner) registerDevServer(solution, { slug: project.slug, pid: process.pid, port, url: targetUrl, startedAt: new Date().toISOString(), log: 'the terminal running frontera app dev' })
|
|
155
240
|
if (!flagBool(ctx, 'no-open')) openBrowser(targetUrl)
|
|
156
241
|
const exitCode = await child.exited
|
|
157
242
|
if (!stopped && exitCode !== 0) {
|
|
@@ -161,13 +246,20 @@ export const appDev: Command = {
|
|
|
161
246
|
})
|
|
162
247
|
}
|
|
163
248
|
return {
|
|
164
|
-
data: { appId
|
|
249
|
+
data: { appId, origin: targetUrl, exitCode },
|
|
165
250
|
text: `Stopped ${app.slug} local development server.`,
|
|
166
251
|
}
|
|
167
252
|
} finally {
|
|
253
|
+
unwatch?.()
|
|
168
254
|
process.off('SIGINT', onSignal)
|
|
169
255
|
process.off('SIGTERM', onSignal)
|
|
256
|
+
if (solution) forgetDevServer(solution, project.slug, process.pid)
|
|
170
257
|
stop()
|
|
258
|
+
// Once the framework's server has exited (or after a while, whatever it does), what it wrote goes back.
|
|
259
|
+
await Promise.race([child.exited, Bun.sleep(STOP_WAIT_MS)])
|
|
260
|
+
const { putBack, left } = kept.atStop()
|
|
261
|
+
for (const line of [putBackLine(putBack, 'stopped'), leftLine(left)]) if (line) ctx.output.note(`[dev] ${line}`)
|
|
262
|
+
if (aside) ctx.output.note(`[dev] ${aside.restore()}`)
|
|
171
263
|
}
|
|
172
264
|
},
|
|
173
265
|
}
|
package/src/commands/app/init.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { BASELINE_COMPONENTS, addShadcnComponents, shadcnAddCommand } from '../.
|
|
|
13
13
|
import { scaffold, scaffoldFiles, type AppFramework } from '../../template'
|
|
14
14
|
import { writeHarnessFiles } from '../../harness'
|
|
15
15
|
import { DEV_ENV_FILE, writeDevEnv } from '../../dev-env'
|
|
16
|
+
import { checkoutSdkFor, LOCAL_SDK_DIR, useCheckoutSdk } from '../../local/local-app-sdk'
|
|
17
|
+
import { findSolutionRoot, PROJECT_FILES } from '../../local/local-project'
|
|
16
18
|
import { flagBool, flagString, type Command, type CommandContext, type CommandResult } from '../types'
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -122,6 +124,21 @@ async function adoptExistingProject(ctx: CommandContext, root: string): Promise<
|
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Where `frontera app init <name>` puts a new App: inside a Frontera
|
|
129
|
+
* solution (a folder with `frontera.yaml` at or above it), its `apps/`
|
|
130
|
+
* folder, where `local dev`, `local check` and the MCP tools find Apps; any
|
|
131
|
+
* other folder, beside where it runs. `--dir` always wins, and an existing
|
|
132
|
+
* project named here is adopted where it is.
|
|
133
|
+
*/
|
|
134
|
+
export function appParentDir(cwd: string, name: string, dir: string | undefined): { base: string; inSolution: string | null } {
|
|
135
|
+
if (dir) return { base: dir, inSolution: null }
|
|
136
|
+
if (existsSync(join(cwd, name, 'package.json'))) return { base: cwd, inSolution: null }
|
|
137
|
+
const root = findSolutionRoot(cwd)
|
|
138
|
+
if (!PROJECT_FILES.some((f) => existsSync(join(root, f)))) return { base: cwd, inSolution: null }
|
|
139
|
+
return { base: join(root, 'apps'), inSolution: root }
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
export const appInit: Command = {
|
|
126
143
|
meta: {
|
|
127
144
|
noun: 'app',
|
|
@@ -154,7 +171,8 @@ export const appInit: Command = {
|
|
|
154
171
|
|
|
155
172
|
async run(ctx) {
|
|
156
173
|
const name = ctx.positional[0]
|
|
157
|
-
const
|
|
174
|
+
const placed = name ? appParentDir(ctx.cwd, name, flagString(ctx, 'dir')) : { base: flagString(ctx, 'dir') ?? ctx.cwd, inSolution: null }
|
|
175
|
+
const base = placed.base
|
|
158
176
|
|
|
159
177
|
/**
|
|
160
178
|
* Which of the two jobs this is, decided the way `shadcn init` decides it:
|
|
@@ -179,7 +197,8 @@ export const appInit: Command = {
|
|
|
179
197
|
throw new UsageError('unsupported framework', 'use --framework next or --framework react')
|
|
180
198
|
}
|
|
181
199
|
const framework: AppFramework = (requested as AppFramework | undefined) ?? 'next'
|
|
182
|
-
const target = resolve(join(
|
|
200
|
+
const target = resolve(join(base, name))
|
|
201
|
+
if (placed.inSolution) ctx.output.note(` in the solution at ${placed.inSolution}: the App goes under apps/`)
|
|
183
202
|
|
|
184
203
|
// Both checks precede the first write. A scaffold that fails halfway is
|
|
185
204
|
// worse than one that never started, because the caller cannot tell which
|
|
@@ -236,6 +255,20 @@ export const appInit: Command = {
|
|
|
236
255
|
ctx.output.note('')
|
|
237
256
|
}
|
|
238
257
|
|
|
258
|
+
// A local runtime that runs from a platform checkout has that checkout's SDK, which can be ahead of
|
|
259
|
+
// every npm release: the App installs the checkout's packages (`frontera app sdk use-local`).
|
|
260
|
+
const checkout = framework === 'next' ? checkoutSdkFor(target) : null
|
|
261
|
+
if (checkout) {
|
|
262
|
+
try {
|
|
263
|
+
const used = useCheckoutSdk(target, checkout)
|
|
264
|
+
ctx.output.note(` SDK source the platform checkout the local runtime runs from (${checkout.version}): ${used.packages.map((p) => p.pkg).join(', ')} packed into ${LOCAL_SDK_DIR}/`)
|
|
265
|
+
ctx.output.note(' `frontera app sdk use-local --npm` switches to the published packages')
|
|
266
|
+
} catch (err) {
|
|
267
|
+
ctx.output.note(` SDK source npm: the checkout's SDK could not be packed (${(err as Error).message}); \`frontera app sdk use-local\` tries again`)
|
|
268
|
+
}
|
|
269
|
+
ctx.output.note('')
|
|
270
|
+
}
|
|
271
|
+
|
|
239
272
|
let installed = false
|
|
240
273
|
if (!flagBool(ctx, 'no-install')) {
|
|
241
274
|
const result = await ctx.output.task('Installing dependencies with bun', () =>
|
|
@@ -289,7 +322,7 @@ export const appInit: Command = {
|
|
|
289
322
|
...(framework === 'react'
|
|
290
323
|
? [' bun run dev', ' bun run typecheck', ' bun run build']
|
|
291
324
|
: [' frontera app dev', ' bun run check']),
|
|
292
|
-
' frontera app deploy
|
|
325
|
+
' frontera app deploy',
|
|
293
326
|
]
|
|
294
327
|
|
|
295
328
|
return {
|
|
@@ -1,36 +1,62 @@
|
|
|
1
1
|
import { AppsApi } from '../../api/apps-api'
|
|
2
|
-
import { UsageError } from '../../errors'
|
|
2
|
+
import { CliError, UsageError } from '../../errors'
|
|
3
3
|
import { projectApp, requireProjectFrom } from './shared'
|
|
4
|
-
import type
|
|
4
|
+
import { flagBool, flagString, type Command } from '../types'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Point production at an existing deployment. No rebuild: the reviewed
|
|
8
|
+
* artifact is what goes live. Required checks hold it; `--force` skips them
|
|
9
|
+
* with a reason, audited. A pinned production refuses until the rollback is
|
|
10
|
+
* undone or the pin lifted.
|
|
11
|
+
*/
|
|
6
12
|
export const appPromote: Command = {
|
|
7
13
|
meta: {
|
|
8
14
|
noun: 'app',
|
|
9
15
|
verb: 'promote',
|
|
10
|
-
args: [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
args: [
|
|
17
|
+
{
|
|
18
|
+
name: 'deployment',
|
|
19
|
+
required: true,
|
|
20
|
+
description: 'the deployment id or release label, from `frontera app deployments`',
|
|
21
|
+
producer: 'frontera app deployments — then `frontera app promote <deployment>`',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
flags: { force: 'boolean', reason: 'string' },
|
|
25
|
+
summary: 'Point production at an existing deployment, once its required checks pass',
|
|
26
|
+
examples: ['frontera app promote dpl_01j…', 'frontera app promote 1.4.0', 'frontera app promote 1.4.0 --force --reason "hotfix for the outage"'],
|
|
19
27
|
needsProject: true,
|
|
20
28
|
},
|
|
21
29
|
|
|
22
30
|
async run(ctx) {
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
25
|
-
|
|
31
|
+
const deployment = ctx.positional[0]
|
|
32
|
+
if (!deployment) throw new UsageError('missing <deployment>', 'frontera app promote <deployment>')
|
|
33
|
+
const force = flagBool(ctx, 'force')
|
|
34
|
+
const reason = flagString(ctx, 'reason')
|
|
35
|
+
if (force && !reason?.trim()) throw new UsageError('--force needs --reason', 'frontera app promote <deployment> --force --reason "why the checks can wait"')
|
|
26
36
|
const project = requireProjectFrom(ctx)
|
|
27
37
|
const client = new AppsApi(ctx.apiUrl, ctx.token)
|
|
28
38
|
const app = await projectApp(client, project)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
try {
|
|
40
|
+
const res = await client.promote(app.id, { deployment, ...(force ? { force: true, reason } : {}) })
|
|
41
|
+
const name = res.label ?? res.promoted
|
|
42
|
+
return {
|
|
43
|
+
data: { app: app.slug, appId: app.id, ...res },
|
|
44
|
+
text: `Promoted ${app.slug}@${name}${res.forced.length ? ` (skipped required checks: ${res.forced.join(', ')})` : ''}${res.liveUrl ? `\n\nLive at ${res.liveUrl}` : ''}`,
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const details = (err as { details?: { code?: string; checks?: Array<{ label: string; detail: string; required: boolean; status: string }> } }).details
|
|
48
|
+
if (details?.code === 'CHECKS_FAILED') {
|
|
49
|
+
const blocking = (details.checks ?? []).filter((c) => c.required && c.status !== 'passed')
|
|
50
|
+
throw new CliError(`${deployment} did not go live: required checks did not pass`, {
|
|
51
|
+
code: 'CONFLICT',
|
|
52
|
+
hint: `${blocking.map((c) => `${c.label} — ${c.detail}`).join('; ')}. Fix them and deploy again, or --force --reason "…"`,
|
|
53
|
+
details,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
if (details?.code === 'PRODUCTION_PINNED') {
|
|
57
|
+
throw new CliError((err as Error).message, { code: 'CONFLICT', hint: 'frontera app rollback --undo returns to what served before the rollback; unpin on the App\'s Deployments keeps what serves now' })
|
|
58
|
+
}
|
|
59
|
+
throw err
|
|
34
60
|
}
|
|
35
61
|
},
|
|
36
62
|
}
|
package/src/commands/app/pull.ts
CHANGED
|
@@ -94,7 +94,9 @@ export const appPull: Command = {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const count = unpackTo(dir, bytes)
|
|
97
|
-
|
|
97
|
+
// What was pulled, apart from a deployment from here: the folder is not read as deployed until `app deploy`,
|
|
98
|
+
// whose parent is this version (`readProject`). A deployment recorded before this pull no longer describes the tree.
|
|
99
|
+
writeState(dir, { appId: app.id, parentVersion: null, pulledFrom: app.id, pulledVersion: parentVersion })
|
|
98
100
|
|
|
99
101
|
// Pull's contract is "give me a working copy", and a copy that cannot
|
|
100
102
|
// `bun install` is not one. Apps stored by an older toolchain carry
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AppsApi } from '../../api/apps-api'
|
|
2
|
+
import { UsageError } from '../../errors'
|
|
3
|
+
import { projectApp, requireProjectFrom } from './shared'
|
|
4
|
+
import { flagBool, flagString, type Command } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Point production at an earlier production deployment (the previous one
|
|
8
|
+
* when none is named) and pin it: nothing promotes until `--undo` or an
|
|
9
|
+
* audited Unpin, so the next routine deploy cannot replace the rollback.
|
|
10
|
+
*/
|
|
11
|
+
export const appRollback: Command = {
|
|
12
|
+
meta: {
|
|
13
|
+
noun: 'app',
|
|
14
|
+
verb: 'rollback',
|
|
15
|
+
args: [{ name: 'deployment', required: false, description: 'an earlier production deployment (id or label); the previous one by default' }],
|
|
16
|
+
flags: { reason: 'string', undo: 'boolean' },
|
|
17
|
+
summary: 'Roll production back and pin it; --undo restores what served before',
|
|
18
|
+
examples: ['frontera app rollback --reason "checkout broke"', 'frontera app rollback 1.3.0 --reason "back to the last good release"', 'frontera app rollback --undo'],
|
|
19
|
+
needsProject: true,
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async run(ctx) {
|
|
23
|
+
const undo = flagBool(ctx, 'undo')
|
|
24
|
+
const reason = flagString(ctx, 'reason')
|
|
25
|
+
if (!undo && !reason?.trim()) throw new UsageError('a rollback needs --reason', 'frontera app rollback --reason "what went wrong"')
|
|
26
|
+
const project = requireProjectFrom(ctx)
|
|
27
|
+
const client = new AppsApi(ctx.apiUrl, ctx.token)
|
|
28
|
+
const app = await projectApp(client, project)
|
|
29
|
+
if (undo) {
|
|
30
|
+
const res = await client.undoRollback(app.id)
|
|
31
|
+
return {
|
|
32
|
+
data: { app: app.slug, appId: app.id, ...res },
|
|
33
|
+
// A platform from before the undo refused a missing origin answers with nothing restored: say what happened.
|
|
34
|
+
text: res.restored
|
|
35
|
+
? `Undid the rollback of ${app.slug}: production serves ${res.label ?? res.restored} again and is no longer pinned`
|
|
36
|
+
: `Lifted the pin on ${app.slug}, but nothing was restored: the deployment production served before the rollback is gone, so production still serves what it does now`,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const deployment = ctx.positional[0]
|
|
40
|
+
const res = await client.rollback(app.id, { ...(deployment ? { deployment } : {}), reason: reason! })
|
|
41
|
+
return {
|
|
42
|
+
data: { app: app.slug, appId: app.id, ...res },
|
|
43
|
+
text: `Rolled ${app.slug} back to ${res.label ?? res.to}${res.from ? ` from ${res.fromLabel ?? res.from}` : ''}. Production is pinned: new deployments won't go live until \`frontera app rollback --undo\` or an Unpin.`,
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
}
|
package/src/commands/app/sdk.ts
CHANGED
|
@@ -1,26 +1,72 @@
|
|
|
1
|
-
import { UsageError } from '../../errors'
|
|
1
|
+
import { CliError, UsageError } from '../../errors'
|
|
2
|
+
import { checkoutSdkFor, LOCAL_SDK_DIR, sdkSwitchChangeLines, useCheckoutSdk, useNpmSdk } from '../../local/local-app-sdk'
|
|
3
|
+
import { installDependencies } from '../../project-bootstrap'
|
|
2
4
|
import { syncVendoredAppSdk } from '../../sdk-sync'
|
|
3
5
|
import { requireProjectFrom } from './shared'
|
|
4
|
-
import type
|
|
6
|
+
import { flagBool, type Command } from '../types'
|
|
5
7
|
|
|
6
8
|
export const appSdk: Command = {
|
|
7
9
|
meta: {
|
|
8
10
|
noun: 'app',
|
|
9
11
|
verb: 'sdk',
|
|
10
|
-
args: [{ name: 'action', required: true, description: 'sync', producer: 'frontera app sdk
|
|
11
|
-
flags: {},
|
|
12
|
-
summary: '
|
|
13
|
-
|
|
12
|
+
args: [{ name: 'action', required: true, description: 'sync | use-local', producer: 'frontera app sdk use-local' }],
|
|
13
|
+
flags: { npm: 'boolean' },
|
|
14
|
+
summary: 'Use the SDK of the platform checkout the local runtime runs from (use-local), or refresh a legacy vendored SDK (sync)',
|
|
15
|
+
details: [
|
|
16
|
+
`\`use-local\`: when the local runtime runs in source mode (a platform checkout), the App's @frontera-sdk packages are packed from that checkout into ${LOCAL_SDK_DIR}/ and package.json depends on them (\`file:./${LOCAL_SDK_DIR}/<tarball>\`, a relative path), then \`bun install\` runs. It writes package.json and a tarball per package in ${LOCAL_SDK_DIR}/, deletes an older tarball of the same package, and the install writes node_modules and updates bun.lock. Commit package.json, bun.lock and ${LOCAL_SDK_DIR}/: a clone, CI and \`app deploy\` build the same SDK. Run it again after the checkout's SDK changes.`,
|
|
17
|
+
`\`use-local --npm\`: back to the published packages, then \`bun install\` runs. Each @frontera-sdk dependency on a checkout tarball or on the SDK copy under src/frontera/ is set to the release the App's lockfile installed (\`^<that version>\`, and the lockfile's own record of the range is rewritten to match), else to the CLI's \`^<version>\`. It deletes ${LOCAL_SDK_DIR}/ and each SDK copy under src/frontera/ (src/frontera/core/, src/frontera/blueprint/, and src/frontera/ when that leaves it empty) with the tsconfig.json paths that point at them. A Vite App whose config aliases its copy keeps it. The output lists every file it changed or deleted: review them before you commit.`,
|
|
18
|
+
'`sync`: refreshes the SDK tree a legacy (Vite) App vendors under src/frontera/.',
|
|
19
|
+
],
|
|
20
|
+
examples: ['frontera app sdk use-local', 'frontera app sdk use-local --npm', 'frontera app sdk sync'],
|
|
14
21
|
needsProject: true,
|
|
22
|
+
// Local files and a local install: no credential.
|
|
23
|
+
offline: true,
|
|
15
24
|
},
|
|
16
25
|
async run(ctx) {
|
|
17
|
-
|
|
26
|
+
const action = ctx.positional[0]
|
|
18
27
|
const project = requireProjectFrom(ctx)
|
|
28
|
+
if (action === 'use-local') {
|
|
29
|
+
const npm = flagBool(ctx, 'npm')
|
|
30
|
+
const checkout = npm ? null : checkoutSdkFor(project.root)
|
|
31
|
+
if (!npm && !checkout) {
|
|
32
|
+
throw new UsageError(
|
|
33
|
+
'the local runtime does not run from a platform checkout here, so there is no local SDK to use',
|
|
34
|
+
'source mode is set by `frontera local start --source <checkout>`; in image mode the App uses the published packages (`frontera app sdk use-local --npm`)',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
let switched
|
|
38
|
+
try {
|
|
39
|
+
switched = npm ? useNpmSdk(project.root) : useCheckoutSdk(project.root, checkout!)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new CliError((err as Error).message, { code: 'FAILURE', hint: 'check that bun is on PATH and the checkout has packages/sdk-core and packages/sdk-blueprint' })
|
|
42
|
+
}
|
|
43
|
+
const install = await installDependencies(project.root)
|
|
44
|
+
if (!install.installed) throw new CliError(`package.json now names the ${npm ? 'published' : 'checkout'} SDK, but bun install failed: ${install.summary}`, { code: 'FAILURE', hint: 'run `bun install` in the App' })
|
|
45
|
+
// What package.json now says, and where a range comes from: the release its lockfile installed, or this CLI's SDK.
|
|
46
|
+
const lines = switched.packages.map((p) => ` ${p.pkg.padEnd(26)} ${p.spec}${npm ? (p.locked ? ` (the release its lockfile installed, ${p.locked}, recorded there with the same range)` : ` (this CLI's SDK range: no lockfile recorded a published release)`) : ''}`)
|
|
47
|
+
// Every file the switch wrote or deleted, by name, then what the install changed.
|
|
48
|
+
const changes = sdkSwitchChangeLines(switched.changes)
|
|
49
|
+
return {
|
|
50
|
+
data: { source: npm ? 'npm' : 'checkout', ...(checkout ? { checkout: checkout.platformSource, version: checkout.version } : {}), packages: switched.packages, changes: switched.changes, installed: install.installed },
|
|
51
|
+
text: [
|
|
52
|
+
switched.packages.length
|
|
53
|
+
? npm ? 'The App uses the published SDK again. package.json now names:' : `The App uses the SDK of the platform checkout (${checkout!.version}):`
|
|
54
|
+
: 'The App already used the published SDK.',
|
|
55
|
+
...lines,
|
|
56
|
+
...(changes.length ? ['Changed in the App:', ...changes.map((c) => ` ${c}`)] : []),
|
|
57
|
+
` bun install: ${install.summary} (node_modules, and bun.lock where what it resolves changed)`,
|
|
58
|
+
npm
|
|
59
|
+
? changes.length ? 'Review these changes, then commit them.' : ''
|
|
60
|
+
: `Commit package.json, bun.lock and ${LOCAL_SDK_DIR}/. Run this again after the checkout's SDK changes; \`--npm\` switches back.`,
|
|
61
|
+
].filter(Boolean).join('\n'),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (action !== 'sync') throw new UsageError('expected use-local or sync', 'frontera app sdk use-local')
|
|
19
65
|
const result = syncVendoredAppSdk(project.root)
|
|
20
66
|
if (result.kind === 'published') {
|
|
21
67
|
return {
|
|
22
68
|
data: result,
|
|
23
|
-
text: 'This App uses published @frontera-sdk packages; update them with the package manager.',
|
|
69
|
+
text: 'This App uses published @frontera-sdk packages; update them with the package manager, or `frontera app sdk use-local` for the local runtime\'s checkout.',
|
|
24
70
|
}
|
|
25
71
|
}
|
|
26
72
|
return {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AppsApi } from '../../api/apps-api'
|
|
2
|
+
import { UsageError } from '../../errors'
|
|
3
|
+
import { projectApp, requireProjectFrom } from './shared'
|
|
4
|
+
import { flagString, type Command } from '../types'
|
|
5
|
+
|
|
6
|
+
/** Lift the pin a rollback left, keeping what serves: new deployments may go live again. Audited. */
|
|
7
|
+
export const appUnpin: Command = {
|
|
8
|
+
meta: {
|
|
9
|
+
noun: 'app',
|
|
10
|
+
verb: 'unpin',
|
|
11
|
+
args: [],
|
|
12
|
+
flags: { reason: 'string' },
|
|
13
|
+
summary: 'Unpin production after a rollback, keeping what serves now',
|
|
14
|
+
examples: ['frontera app unpin --reason "fixed forward in 1.4.1"'],
|
|
15
|
+
needsProject: true,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async run(ctx) {
|
|
19
|
+
const reason = flagString(ctx, 'reason')
|
|
20
|
+
if (!reason?.trim()) throw new UsageError('unpinning needs --reason', 'frontera app unpin --reason "why production may move again"')
|
|
21
|
+
const project = requireProjectFrom(ctx)
|
|
22
|
+
const client = new AppsApi(ctx.apiUrl, ctx.token)
|
|
23
|
+
const app = await projectApp(client, project)
|
|
24
|
+
const res = await client.unpin(app.id, reason)
|
|
25
|
+
return { data: { app: app.slug, appId: app.id, ...res }, text: `Unpinned ${app.slug}: production keeps serving ${res.productionLabel ?? res.production ?? 'what it serves'}, and promotion works again` }
|
|
26
|
+
},
|
|
27
|
+
}
|
|
@@ -9,7 +9,7 @@ export const appVersions: Command = {
|
|
|
9
9
|
verb: 'versions',
|
|
10
10
|
args: [],
|
|
11
11
|
flags: {},
|
|
12
|
-
summary: 'List
|
|
12
|
+
summary: 'List deployments, newest first; * marks production (see app deployments)',
|
|
13
13
|
examples: ['frontera app versions', 'frontera app versions --json'],
|
|
14
14
|
needsProject: true,
|
|
15
15
|
},
|
|
@@ -161,7 +161,8 @@ const bindings: Command = {
|
|
|
161
161
|
// still has a row, and a list that showed only the row would
|
|
162
162
|
// read as a working permission.
|
|
163
163
|
b.projects ? 'yes' : 'no',
|
|
164
|
-
|
|
164
|
+
// `requiresApproval: false` is not "never asks": in an attended chat the person's approval mode still asks.
|
|
165
|
+
b.requiresApproval ? 'required' : 'unattended (no card)',
|
|
165
166
|
`${b.waitSeconds}s`,
|
|
166
167
|
b.inertReason ?? '',
|
|
167
168
|
]),
|
|
@@ -181,7 +182,8 @@ const bindings: Command = {
|
|
|
181
182
|
b.agentSlug,
|
|
182
183
|
b.agentName,
|
|
183
184
|
String(b.enabled),
|
|
184
|
-
|
|
185
|
+
// `requiresApproval: false` is not "never asks": in an attended chat the person's approval mode still asks.
|
|
186
|
+
b.requiresApproval ? 'required' : 'unattended (no card)',
|
|
185
187
|
`${b.waitSeconds}s`,
|
|
186
188
|
b.createdBy,
|
|
187
189
|
]),
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'
|
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { UsageError } from '../../errors'
|
|
6
|
+
import { bundleIdMapFromEnv, mapBundleIds } from '../../local/local-function-ids'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Building an automation entry file, extracted from `index-commands`.
|
|
@@ -51,7 +52,9 @@ export async function buildAndExtract(file: string): Promise<{
|
|
|
51
52
|
)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
// `local dev` deploying a pulled Function to the local runtime: the workspace's object type ids
|
|
56
|
+
// its code hard-codes become this runtime's, in the bundle only (`local-function-ids.ts`).
|
|
57
|
+
const code = mapBundleIds(await built.outputs[0]!.text(), bundleIdMapFromEnv())
|
|
55
58
|
|
|
56
59
|
const dir = mkdtempSync(join(tmpdir(), 'frontera-automation-'))
|
|
57
60
|
try {
|
|
@@ -6,6 +6,7 @@ import { validateManifest } from '@frontera-sdk/functions'
|
|
|
6
6
|
|
|
7
7
|
import { AutomationApi } from '../../api/automation-api'
|
|
8
8
|
import { UsageError } from '../../errors'
|
|
9
|
+
import { outsideImports } from '../../function-outside-imports'
|
|
9
10
|
import { packDirectory } from '../../pack'
|
|
10
11
|
import { automationInit } from './init'
|
|
11
12
|
import { automationPull } from './pull'
|
|
@@ -118,7 +119,8 @@ const deploy: Command = {
|
|
|
118
119
|
const projectRoot = findAutomationProjectRoot(resolve(file))
|
|
119
120
|
let source: Uint8Array | undefined
|
|
120
121
|
if (projectRoot) {
|
|
121
|
-
|
|
122
|
+
// With the files it imports from outside its folder, so a pull brings a Function that builds.
|
|
123
|
+
source = packDirectory(projectRoot, ctx.output, 'source', outsideImports(projectRoot, resolve(file))).tgz
|
|
122
124
|
} else {
|
|
123
125
|
// Degrade honestly rather than refuse. Authoring inside this monorepo is
|
|
124
126
|
// still supported and simply cannot retain source — there is no
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { AutomationApi } from '../../api/automation-api'
|
|
5
5
|
import { UsageError } from '../../errors'
|
|
6
|
+
import { OUTSIDE_DIR } from '../../function-outside-imports'
|
|
6
7
|
import { unpackTo } from '../../untar'
|
|
7
8
|
import { flagBool, type Command } from '../types'
|
|
8
9
|
|
|
@@ -109,11 +110,15 @@ export const automationPull: Command = {
|
|
|
109
110
|
const gz = await client.downloadSource(slug, resolved)
|
|
110
111
|
mkdirSync(target, { recursive: true })
|
|
111
112
|
const count = unpackTo(target, gz)
|
|
113
|
+
// Files it imports from beside its folder travel under `frontera-imports/` (`function-outside-imports.ts`):
|
|
114
|
+
// they are said, since this folder alone has nowhere beside it to put them.
|
|
115
|
+
const beside = existsSync(join(target, OUTSIDE_DIR))
|
|
112
116
|
|
|
113
117
|
return {
|
|
114
118
|
data: { slug, version: resolved, path: target, files: count },
|
|
115
119
|
text: [
|
|
116
120
|
`Pulled ${slug} v${resolved} into ${target} (${count} files)`,
|
|
121
|
+
...(beside ? [` ${OUTSIDE_DIR}/ holds the files it imports from beside its folder, by their path from it (each __up__ is ..): put them there before deploying, or pull the project with \`frontera local sync pull\`, which does`] : []),
|
|
117
122
|
'',
|
|
118
123
|
` cd ${target}`,
|
|
119
124
|
' bun install',
|