@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.
Files changed (225) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +48 -4
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/index-commands.ts +69 -7
  33. package/src/commands/agent/resolve.ts +2 -0
  34. package/src/commands/app/checks.ts +44 -0
  35. package/src/commands/app/deploy.ts +243 -60
  36. package/src/commands/app/deployments.ts +97 -0
  37. package/src/commands/app/dev.ts +103 -11
  38. package/src/commands/app/init.ts +36 -3
  39. package/src/commands/app/promote.ts +45 -19
  40. package/src/commands/app/pull.ts +3 -1
  41. package/src/commands/app/rollback.ts +46 -0
  42. package/src/commands/app/sdk.ts +54 -8
  43. package/src/commands/app/unpin.ts +27 -0
  44. package/src/commands/app/versions.ts +1 -1
  45. package/src/commands/automation/bindings.ts +4 -2
  46. package/src/commands/automation/build-entry.ts +4 -1
  47. package/src/commands/automation/index-commands.ts +3 -1
  48. package/src/commands/automation/pull.ts +6 -1
  49. package/src/commands/automation/run.ts +34 -9
  50. package/src/commands/blueprint/bind.ts +2 -1
  51. package/src/commands/blueprint/declarative.ts +331 -31
  52. package/src/commands/blueprint/get.ts +6 -2
  53. package/src/commands/blueprint/grants.ts +64 -10
  54. package/src/commands/blueprint/metric.ts +108 -0
  55. package/src/commands/blueprint/object-sets.ts +71 -0
  56. package/src/commands/blueprint/query.ts +155 -31
  57. package/src/commands/dataset/index-commands.ts +156 -7
  58. package/src/commands/knowledge/index-commands.ts +122 -20
  59. package/src/commands/local/check.ts +657 -44
  60. package/src/commands/local/data.ts +33 -12
  61. package/src/commands/local/dev.ts +875 -88
  62. package/src/commands/local/docs-mcp.ts +10 -18
  63. package/src/commands/local/index-commands.ts +300 -63
  64. package/src/commands/local/init.ts +18 -5
  65. package/src/commands/local/key.ts +0 -3
  66. package/src/commands/local/providers.ts +0 -7
  67. package/src/commands/local/remove.ts +116 -0
  68. package/src/commands/local/schema.ts +3 -4
  69. package/src/commands/local/sdk-refresh.ts +59 -0
  70. package/src/commands/local/snapshot.ts +100 -0
  71. package/src/commands/local/sync-request.ts +320 -13
  72. package/src/commands/local/sync.ts +102 -5
  73. package/src/commands/mcp/serve.ts +34 -0
  74. package/src/commands/registry.ts +52 -8
  75. package/src/commands/types.ts +29 -0
  76. package/src/commands/workflow/archive.ts +46 -0
  77. package/src/commands/workflow/get.ts +2 -1
  78. package/src/commands/workflow/index-commands.ts +5 -0
  79. package/src/commands/workflow/push.ts +49 -2
  80. package/src/commands/workflow/run.ts +65 -7
  81. package/src/commands/workflow/runs.ts +9 -6
  82. package/src/commands/workflow/webhook-test.ts +166 -0
  83. package/src/config.ts +43 -6
  84. package/src/dev-broker.ts +52 -0
  85. package/src/exit.ts +103 -4
  86. package/src/flag-help.ts +77 -17
  87. package/src/forge/touches.ts +15 -12
  88. package/src/function-outside-imports.ts +89 -0
  89. package/src/harness.ts +42 -24
  90. package/src/heal.ts +112 -1
  91. package/src/local/fix-words.ts +12 -0
  92. package/src/local/function-bundle-recovery.ts +775 -0
  93. package/src/local/local-actions.ts +103 -10
  94. package/src/local/local-agent-action-grants.ts +210 -0
  95. package/src/local/local-agent-checks.ts +232 -0
  96. package/src/local/local-agent-grants.ts +29 -7
  97. package/src/local/local-agent-lean-plugins.ts +68 -0
  98. package/src/local/local-agent-missing-tools.ts +68 -0
  99. package/src/local/local-agent-name.ts +27 -0
  100. package/src/local/local-app-code.ts +191 -0
  101. package/src/local/local-app-dev-files.ts +186 -0
  102. package/src/local/local-app-dev-session.ts +88 -0
  103. package/src/local/local-app-lock-install.ts +114 -0
  104. package/src/local/local-app-pulled.ts +167 -0
  105. package/src/local/local-app-restore.ts +111 -0
  106. package/src/local/local-app-runtime.ts +68 -0
  107. package/src/local/local-app-sdk-paths.ts +149 -0
  108. package/src/local/local-app-sdk.ts +509 -0
  109. package/src/local/local-applied.ts +211 -21
  110. package/src/local/local-apply-history.ts +84 -0
  111. package/src/local/local-apply-summary.ts +86 -0
  112. package/src/local/local-apply-watchdog.ts +95 -0
  113. package/src/local/local-apps.ts +180 -7
  114. package/src/local/local-blueprint-errors.ts +198 -0
  115. package/src/local/local-blueprint-outline.ts +31 -12
  116. package/src/local/local-blueprint-schemas.ts +582 -0
  117. package/src/local/local-build-inputs.ts +48 -0
  118. package/src/local/local-build-summary.ts +495 -28
  119. package/src/local/local-case-evidence.ts +120 -0
  120. package/src/local/local-case-not-run.ts +153 -0
  121. package/src/local/local-chat-apps.ts +93 -16
  122. package/src/local/local-check-inherited.ts +83 -0
  123. package/src/local/local-cities.ts +89 -0
  124. package/src/local/local-component-health.ts +119 -0
  125. package/src/local/local-control.ts +34 -7
  126. package/src/local/local-db-snapshot.ts +348 -0
  127. package/src/local/local-dev-references.ts +346 -35
  128. package/src/local/local-dev-sync.ts +136 -7
  129. package/src/local/local-docker.ts +35 -1
  130. package/src/local/local-docs-search.ts +1 -1
  131. package/src/local/local-docs.ts +281 -2
  132. package/src/local/local-empty-workflows.ts +92 -0
  133. package/src/local/local-env.ts +16 -1
  134. package/src/local/local-file-checks.ts +104 -0
  135. package/src/local/local-function-ids.ts +141 -0
  136. package/src/local/local-function-recovery.ts +141 -0
  137. package/src/local/local-function-state.ts +87 -0
  138. package/src/local/local-keys.ts +7 -5
  139. package/src/local/local-knowledge-ingestion.ts +225 -0
  140. package/src/local/local-knowledge-uploads.ts +217 -0
  141. package/src/local/local-media-placeholders.ts +182 -0
  142. package/src/local/local-media-sets.ts +90 -10
  143. package/src/local/local-model-parameters.ts +89 -0
  144. package/src/local/local-object-sets.ts +18 -6
  145. package/src/local/local-operating.ts +35 -1
  146. package/src/local/local-org-settings.ts +205 -7
  147. package/src/local/local-orphans.ts +321 -0
  148. package/src/local/local-personas.ts +296 -0
  149. package/src/local/local-portability.ts +141 -16
  150. package/src/local/local-ports.ts +99 -7
  151. package/src/local/local-project-notices.ts +234 -0
  152. package/src/local/local-project-secrets.ts +133 -0
  153. package/src/local/local-project.ts +12 -0
  154. package/src/local/local-provider-refusal.ts +66 -0
  155. package/src/local/local-request-evidence.ts +112 -0
  156. package/src/local/local-runtime-health.ts +123 -0
  157. package/src/local/local-runtime-history.ts +50 -0
  158. package/src/local/local-runtime-target.ts +42 -0
  159. package/src/local/local-sample-data.ts +141 -0
  160. package/src/local/local-sample-remake.ts +138 -0
  161. package/src/local/local-scaffold.ts +336 -53
  162. package/src/local/local-schema-check.ts +148 -0
  163. package/src/local/local-schemas.ts +742 -25
  164. package/src/local/local-secret-carry.ts +107 -0
  165. package/src/local/local-secrets.ts +38 -8
  166. package/src/local/local-seed.ts +102 -26
  167. package/src/local/local-stand-ins.ts +1 -1
  168. package/src/local/local-state.ts +43 -2
  169. package/src/local/local-studio-server.ts +170 -7
  170. package/src/local/local-supervisor.ts +44 -6
  171. package/src/local/local-sync-names.ts +2 -0
  172. package/src/local/local-sync-plan.ts +11 -0
  173. package/src/local/local-sync-push.ts +10 -7
  174. package/src/local/local-sync-refs.ts +49 -3
  175. package/src/local/local-sync-remote.ts +343 -32
  176. package/src/local/local-sync-snapshot.ts +15 -10
  177. package/src/local/local-synthetic-data.ts +2116 -99
  178. package/src/local/local-test-baseline.ts +113 -0
  179. package/src/local/local-test-cases.ts +76 -1
  180. package/src/local/local-test-runner.ts +582 -91
  181. package/src/local/local-test-runs.ts +13 -5
  182. package/src/local/local-vendored-sdk.ts +183 -0
  183. package/src/local/local-workflow-state.ts +16 -3
  184. package/src/local/local-workflow-tests.ts +482 -0
  185. package/src/local/local-workspace-grants.ts +81 -0
  186. package/src/main.ts +50 -9
  187. package/src/mcp/a11y-scan.ts +55 -0
  188. package/src/mcp/browser/ax-snapshot.ts +181 -0
  189. package/src/mcp/browser/credentials.ts +78 -0
  190. package/src/mcp/browser/frames.ts +336 -0
  191. package/src/mcp/browser/headless-session.ts +413 -0
  192. package/src/mcp/browser/headless-tab.ts +575 -0
  193. package/src/mcp/browser/keys.ts +70 -0
  194. package/src/mcp/browser/page-scripts.ts +129 -0
  195. package/src/mcp/browser/wait.ts +73 -0
  196. package/src/mcp/forge-link.ts +126 -0
  197. package/src/mcp/headless.ts +199 -0
  198. package/src/mcp/mcp-apps.ts +435 -0
  199. package/src/mcp/mcp-chat.ts +226 -0
  200. package/src/mcp/mcp-cli.ts +53 -0
  201. package/src/mcp/mcp-protocol.ts +122 -0
  202. package/src/mcp/mcp-runtime.ts +113 -0
  203. package/src/mcp/mcp-server.ts +108 -0
  204. package/src/mcp/mcp-traces.ts +409 -0
  205. package/src/mcp/page-observation.ts +193 -0
  206. package/src/mcp/tools/agent-tools.ts +700 -0
  207. package/src/mcp/tools/app-tools.ts +241 -0
  208. package/src/mcp/tools/browser-tools.ts +310 -0
  209. package/src/mcp/tools/docs-tools.ts +32 -0
  210. package/src/mcp/tools/project-tools.ts +205 -0
  211. package/src/mcp/tools/test-tools.ts +339 -0
  212. package/src/mcp/tools/workflow-tools.ts +611 -0
  213. package/src/output.ts +1 -1
  214. package/src/pack.ts +8 -3
  215. package/src/packaging.ts +1 -1
  216. package/src/project-bootstrap.ts +107 -4
  217. package/src/project-context.ts +18 -1
  218. package/src/project.ts +27 -2
  219. package/src/provenance.ts +25 -0
  220. package/src/render-evidence.ts +65 -7
  221. package/src/template.ts +14 -8
  222. package/src/templates/next-app-files.ts +35 -6
  223. package/src/templates/next-skills.ts +61 -27
  224. package/src/vendor/kit-assets.json +6 -6
  225. package/src/vendor/sdk-sources.json +24 -21
@@ -1,11 +1,13 @@
1
1
  import type { LocalProject } from './local-project'
2
2
  import { readState } from './local-state'
3
- import { findCaseFiles, runCases, type CaseResult } from './local-test-runner'
3
+ import { recordRun } from './local-test-baseline'
4
+ import { caseCount, runCases, selectCaseFiles, type CaseFile, type CaseResult } from './local-test-runner'
4
5
 
5
6
  /**
6
7
  * Test runs started from Studio: the same runner as `frontera local test`,
7
8
  * run in the supervisor so the page can follow progress. Kept in memory,
8
- * newest first; the case files in the solution are the record that matters.
9
+ * newest first; each case's outcome is also kept as its last run, as
10
+ * `frontera local test` keeps it.
9
11
  */
10
12
 
11
13
  export interface TestRun {
@@ -35,13 +37,13 @@ export function createTestRuns(project: LocalProject, serviceUrl: string) {
35
37
  if (runs.some((r) => r.status === 'running')) return { error: 'a test run is already running', status: 409 }
36
38
  const creds = readState(project)?.credentials
37
39
  if (!creds?.session) return { error: 'the runtime is not seeded yet', status: 409 }
38
- let files: ReturnType<typeof findCaseFiles>
40
+ let files: CaseFile[]
39
41
  try {
40
- files = findCaseFiles(project.root, args.agent)
42
+ files = selectCaseFiles(project.root, { agent: args.agent })
41
43
  } catch (err) {
42
44
  return { error: `a case file could not be read: ${(err as Error).message}`, status: 422 }
43
45
  }
44
- const total = files.reduce((n, f) => n + f.cases.length, 0)
46
+ const total = caseCount(files)
45
47
  if (total === 0) return { error: 'no test cases to run', status: 400 }
46
48
 
47
49
  const run: TestRun = {
@@ -61,6 +63,12 @@ export function createTestRuns(project: LocalProject, serviceUrl: string) {
61
63
  void runCases({ serviceUrl, creds, files, live: run.live, onResult: (r) => run.results.push(r) })
62
64
  .then((results) => {
63
65
  run.status = results.every((r) => r.passed) ? 'passed' : 'failed'
66
+ // Each case's last run, as `frontera local test` keeps it: Forge's home marks the evaluations from it.
67
+ try {
68
+ recordRun(project, results)
69
+ } catch (err) {
70
+ console.error(`[local-test-runs] could not keep the results: ${(err as Error).message}`)
71
+ }
64
72
  })
65
73
  .catch((err: Error) => {
66
74
  run.status = 'error'
@@ -0,0 +1,183 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
+
5
+ import { AUTOMATION_SDK_FILES } from '../automation-template'
6
+ import vendored from '../vendor/sdk-sources.json'
7
+ import { compareVersions } from './local-app-sdk'
8
+ import { readSnapshot } from './local-sync-snapshot'
9
+
10
+ /**
11
+ * The Functions SDK a project keeps in `vendor/frontera/` (what its Function
12
+ * tests and typecheck resolve `@frontera-sdk/functions` to), against this
13
+ * CLI's copy.
14
+ *
15
+ * The CLI writes those files (`local init`, a pull), so they are the CLI's to
16
+ * keep current — but a person may have edited one, and an edit is never
17
+ * overwritten unasked. So the CLI records what it wrote: the manifest
18
+ * `vendor/frontera/.frontera-sdk.json` holds each file's hash as written.
19
+ * A file still at that hash is the CLI's copy, unmodified, and `local dev`
20
+ * replaces it with this CLI's when they differ. A file that differs from the
21
+ * hash was edited and is kept; `frontera local sdk-refresh --force` replaces
22
+ * it. A project from a CLI that wrote no manifest cannot tell the two apart,
23
+ * so it is only told; `frontera local sdk-refresh` replaces the files that
24
+ * differ and keeps a copy of each one it replaces.
25
+ *
26
+ * Which way a difference points is said only when something records it: the
27
+ * manifest's version (a newer CLI wrote the copy), or, with no manifest, the
28
+ * platform release the project was pulled from (`.frontera/sync/remote.json`
29
+ * `platformVersion`) when it is newer than this CLI: its Functions were
30
+ * written against a newer SDK, so this CLI's copy would be a step back.
31
+ * Neither replaces a newer copy without `--force`; the advice is to update
32
+ * the CLI. With nothing to go by, the copy only "differs".
33
+ */
34
+
35
+ export const SDK_MANIFEST = 'vendor/frontera/.frontera-sdk.json'
36
+
37
+ interface SdkManifest {
38
+ version: string
39
+ files: Record<string, string>
40
+ }
41
+
42
+ const sha = (text: string) => createHash('sha256').update(text).digest('hex')
43
+
44
+ function cliFiles(): Record<string, string> {
45
+ const files = vendored.files as Record<string, string>
46
+ return Object.fromEntries(AUTOMATION_SDK_FILES.flatMap((rel) => (files[rel] === undefined ? [] : [[`vendor/${rel}`, files[rel]!]])))
47
+ }
48
+
49
+ function readManifest(root: string): SdkManifest | null {
50
+ try {
51
+ const doc = JSON.parse(readFileSync(join(root, SDK_MANIFEST), 'utf8')) as Partial<SdkManifest>
52
+ return typeof doc.version === 'string' && doc.files && typeof doc.files === 'object' ? (doc as SdkManifest) : null
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ /** The manifest for the files as this CLI writes them: what `writeSolutionScaffold` and a refresh leave beside them. */
59
+ export function sdkManifestText(files: Record<string, string> = cliFiles()): string {
60
+ return `${JSON.stringify({ version: vendored.sdkVersion, files: Object.fromEntries(Object.entries(files).map(([rel, text]) => [rel, sha(text)])) }, null, 2)}\n`
61
+ }
62
+
63
+ export interface VendoredSdkState {
64
+ /** The project keeps the SDK in vendor/frontera/. */
65
+ vendored: boolean
66
+ /** The SDK version the manifest says, or null without one. */
67
+ version: string | null
68
+ /** Files that differ from this CLI's copy and are the CLI's unmodified copy: safe to replace. */
69
+ outdated: string[]
70
+ /** Files that differ from this CLI's copy and were edited since the CLI wrote them (or, with no manifest, can't be told). */
71
+ edited: string[]
72
+ /** Files this CLI vendors that the project lacks. */
73
+ missing: string[]
74
+ /** No manifest: nothing records which SDK wrote the folder (a CLI from before manifests, or a hand-made copy). */
75
+ unknown: boolean
76
+ /**
77
+ * The copy is known or likely to be newer than this CLI's, so replacing it would go back:
78
+ * `manifest`, a newer CLI wrote it; `pull`, no manifest, and the project was pulled from a
79
+ * newer platform release. Null when nothing says so.
80
+ */
81
+ newer: { by: 'manifest' | 'pull'; version: string } | null
82
+ }
83
+
84
+ /** This CLI's SDK version (`1.52.28`). */
85
+ export const CLI_SDK = vendored.sdkVersion
86
+
87
+ /** The platform release the project was last pulled from, when a pull recorded it. */
88
+ function pulledFromVersion(root: string): string | null {
89
+ const v = readSnapshot(root)?.platformVersion
90
+ return typeof v === 'string' && /^\d+\.\d+/.test(v) ? v : null
91
+ }
92
+
93
+ /** How the project's vendored SDK compares with this CLI's. */
94
+ export function vendoredSdkState(root: string): VendoredSdkState {
95
+ const mine = cliFiles()
96
+ const vendoredHere = existsSync(join(root, 'vendor', 'frontera'))
97
+ const manifest = readManifest(root)
98
+ const state: VendoredSdkState = { vendored: vendoredHere, version: manifest?.version ?? null, outdated: [], edited: [], missing: [], unknown: vendoredHere && !manifest, newer: null }
99
+ if (!vendoredHere) return state
100
+ for (const [rel, text] of Object.entries(mine)) {
101
+ const path = join(root, rel)
102
+ if (!existsSync(path)) {
103
+ state.missing.push(rel)
104
+ continue
105
+ }
106
+ const here = readFileSync(path, 'utf8')
107
+ if (here === text) continue
108
+ const written = manifest?.files[rel]
109
+ if (written && sha(here) === written) state.outdated.push(rel)
110
+ else state.edited.push(rel)
111
+ }
112
+ if (state.outdated.length || state.edited.length) {
113
+ if (manifest && compareVersions(manifest.version, CLI_SDK) > 0) state.newer = { by: 'manifest', version: manifest.version }
114
+ else if (!manifest) {
115
+ const pulled = pulledFromVersion(root)
116
+ if (pulled && compareVersions(pulled, CLI_SDK) > 0) state.newer = { by: 'pull', version: pulled }
117
+ }
118
+ }
119
+ return state
120
+ }
121
+
122
+ /**
123
+ * Bring the vendored SDK to this CLI's copy: the files the CLI wrote and
124
+ * nobody changed, and the missing ones; with `force`, the edited ones too.
125
+ * Without a manifest every differing file counts as replaceable (nothing
126
+ * tells an edit apart) and a copy of each is kept under
127
+ * `.frontera/local/sdk-backup/`. Writes the manifest. Returns what changed.
128
+ */
129
+ export function refreshVendoredSdk(root: string, opts: { force?: boolean; backupAll?: boolean; now?: Date } = {}): { replaced: string[]; added: string[]; kept: string[]; backup: string | null } {
130
+ const state = vendoredSdkState(root)
131
+ const mine = cliFiles()
132
+ const replaceEdited = opts.force === true || (state.unknown && opts.backupAll === true)
133
+ const replace = [...state.outdated, ...(replaceEdited ? state.edited : [])]
134
+ const kept = replaceEdited ? [] : state.edited
135
+ let backup: string | null = null
136
+ const toBackUp = replaceEdited ? state.edited : []
137
+ if (toBackUp.length) {
138
+ backup = join('.frontera', 'local', 'sdk-backup', (opts.now ?? new Date()).toISOString().replace(/[:.]/g, '-'))
139
+ for (const rel of toBackUp) {
140
+ const to = join(root, backup, rel)
141
+ mkdirSync(dirname(to), { recursive: true })
142
+ writeFileSync(to, readFileSync(join(root, rel)))
143
+ }
144
+ }
145
+ for (const rel of [...replace, ...state.missing]) {
146
+ mkdirSync(dirname(join(root, rel)), { recursive: true })
147
+ writeFileSync(join(root, rel), mine[rel]!)
148
+ }
149
+ // The manifest records what the CLI wrote; a kept edit keeps the hash it was written at, so it stays "edited".
150
+ const previous = readManifest(root)?.files ?? {}
151
+ const recorded = Object.fromEntries(Object.entries(mine).map(([rel, text]) => [rel, kept.includes(rel) && previous[rel] ? previous[rel]! : sha(text)]))
152
+ mkdirSync(dirname(join(root, SDK_MANIFEST)), { recursive: true })
153
+ writeFileSync(join(root, SDK_MANIFEST), `${JSON.stringify({ version: vendored.sdkVersion, files: recorded }, null, 2)}\n`)
154
+ return { replaced: replace, added: state.missing, kept, backup }
155
+ }
156
+
157
+ /**
158
+ * What `local dev` and `local check` say when the vendored SDK is not this
159
+ * CLI's, with the command that refreshes it; null when it is.
160
+ */
161
+ export function vendoredSdkLine(state: VendoredSdkState): string | null {
162
+ if (!state.vendored) return null
163
+ const names = (list: string[]) => `${list.slice(0, 3).map((f) => f.replace(/^vendor\//, '')).join(', ')}${list.length > 3 ? ', …' : ''}`
164
+ const differs = (list: string[]) => `vendor/frontera/ (the Functions SDK the project's tests and typecheck use) differs from this CLI's copy (${CLI_SDK}) in ${list.length} file${list.length === 1 ? '' : 's'} (${names(list)})`
165
+ const changed = [...state.outdated, ...state.edited]
166
+ if (state.newer?.by === 'manifest') {
167
+ return `${differs(changed)}: a newer CLI wrote it (SDK ${state.newer.version}), so this CLI's copy would be a step back. It is kept: upgrade the Frontera CLI to ${state.newer.version} or later rather than refreshing (\`frontera local sdk-refresh --force\` replaces it with this CLI's older copy, keeping a copy of each file)`
168
+ }
169
+ if (state.unknown && state.newer?.by === 'pull' && state.edited.length) {
170
+ return `${differs(state.edited)}. Nothing records which SDK wrote it (no vendor/frontera/.frontera-sdk.json), and the project was pulled from platform ${state.newer.version}, newer than this CLI: refreshing would replace what may be a newer copy with an older one. Upgrade the Frontera CLI to ${state.newer.version} or later, then \`frontera local sdk-refresh\` (\`--force\` refreshes with this CLI's copy anyway, keeping a copy of each file under .frontera/local/sdk-backup/)`
171
+ }
172
+ if (state.unknown && state.edited.length) {
173
+ return `${differs(state.edited)}. Nothing records which SDK wrote it (no vendor/frontera/.frontera-sdk.json), so it may be older or newer than this CLI's. If the project's tests or typecheck fail inside vendor/frontera/, \`frontera local sdk-refresh\` replaces those files with this CLI's copy and keeps a copy of each under .frontera/local/sdk-backup/`
174
+ }
175
+ if (state.edited.length) {
176
+ return `vendor/frontera/ has ${state.edited.length} file${state.edited.length === 1 ? '' : 's'} edited since the CLI wrote ${state.edited.length === 1 ? 'it' : 'them'} (${names(state.edited)}), which differ from this CLI's SDK (${vendored.sdkVersion}): kept. \`frontera local sdk-refresh --force\` replaces them and keeps a copy`
177
+ }
178
+ if (state.missing.length || state.outdated.length) {
179
+ const behind = state.version && compareVersions(state.version, CLI_SDK) < 0
180
+ return `vendor/frontera/ ${behind ? `is behind this CLI's SDK (it was written for ${state.version}; this CLI's is ${CLI_SDK})` : `differs from this CLI's SDK (${CLI_SDK})`}: \`frontera local sdk-refresh\` brings it to this CLI's copy`
181
+ }
182
+ return null
183
+ }
@@ -107,11 +107,11 @@ async function setEnabled(serviceUrl: string, creds: LocalCredentials, slug: str
107
107
  }
108
108
 
109
109
  /**
110
- * After a push: promote and switch triggers as in the workspace, for a
110
+ * After a push (as `version`): promote and switch triggers as in the workspace, for a
111
111
  * workflow the lock says is live there. Returns lines for the apply's output;
112
112
  * a `warning:` line is an adaptation in the build.
113
113
  */
114
- export async function mirrorWorkflowState(project: LocalProject, slug: string, file: string, serviceUrl: string, creds: LocalCredentials): Promise<string[]> {
114
+ export async function mirrorWorkflowState(project: LocalProject, slug: string, file: string, serviceUrl: string, creds: LocalCredentials, version?: number): Promise<string[]> {
115
115
  const state = readLock(project.root)?.items[`workflow/${slug}`]?.state
116
116
  if (!state?.live || !creds.workspaceKey) return []
117
117
  let doc: unknown = null
@@ -121,7 +121,7 @@ export async function mirrorWorkflowState(project: LocalProject, slug: string, f
121
121
  return []
122
122
  }
123
123
  const lines: string[] = []
124
- const promoted = await promoteWorkflow(serviceUrl, creds, slug)
124
+ const promoted = await promoteWorkflow(serviceUrl, creds, slug, version)
125
125
  if (promoted) lines.push(` promoted workflow ${slug} v${promoted}: it is live in the workspace`)
126
126
  const want = localTriggersOn(doc, state.enabled)
127
127
  const failed = await setEnabled(serviceUrl, creds, slug, want.on)
@@ -129,3 +129,16 @@ export async function mirrorWorkflowState(project: LocalProject, slug: string, f
129
129
  else if (want.why) lines.push(` warning: ${want.why}`)
130
130
  return lines
131
131
  }
132
+
133
+ /**
134
+ * After a push (as `version`) of a workflow that already has a live version
135
+ * here (an Agent's binding promoted it, or it is live in the workspace): the
136
+ * pushed version is promoted too. An Agent, a webhook and a schedule start the
137
+ * live version, and on this runtime that has to be what the file says, not
138
+ * the push before it. A workflow that is only drafts stays drafts.
139
+ */
140
+ export async function keepLiveCurrent(serviceUrl: string, creds: LocalCredentials, slug: string, version: number | undefined): Promise<string[]> {
141
+ if (!version) return []
142
+ const promoted = await promoteWorkflow(serviceUrl, creds, slug, version, { onlyIfLive: true })
143
+ return promoted ? [` promoted workflow ${slug} v${promoted}: it has a live version here, which Agents, webhooks and schedules start, and that now matches the file`] : []
144
+ }