@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
@@ -5,6 +5,7 @@ import { join } from 'node:path'
5
5
  import {
6
6
  containerState,
7
7
  copyFromContainer,
8
+ dbVolumeName,
8
9
  execInContainer,
9
10
  followLogs,
10
11
  imagePresent,
@@ -13,6 +14,7 @@ import {
13
14
  runContainer,
14
15
  startContainer,
15
16
  stopContainer,
17
+ storageVolumeName,
16
18
  } from './local-docker'
17
19
  import {
18
20
  CATALOG_DB,
@@ -30,14 +32,15 @@ import {
30
32
  type LocalConfig,
31
33
  type LocalSecrets,
32
34
  } from './local-env'
33
- import { keylessEmbeddings, providerEnv } from './local-keys'
35
+ import { localEmbeddingsReachable, providerEnv } from './local-keys'
34
36
  import { applyProviderSetup, readProviderSetup, rememberSlugs, writeProviderSetup } from './local-providers'
35
37
  import type { PortBlock } from './local-ports'
36
38
  import { startProcess, stopProcess, waitForHttp } from './local-process'
37
39
  import type { LocalProject } from './local-project'
38
40
  import { startLocalEmbeddings } from './local-embeddings'
39
41
  import { LOCAL_TRACE_KEYS, startLocalTraces } from './local-traces'
40
- import { copyDocs, customerDocsBundle, IMAGE_DOCS_PATH } from './local-docs'
42
+ import { recordRuntime } from './local-runtime-history'
43
+ import { buildDocs, checkoutDocsStale, copyDocs, customerDocsBundle, IMAGE_DOCS_PATH, writeDocsBuildMarker } from './local-docs'
41
44
  import { keepSchemas, type SchemaSource } from './local-schemas'
42
45
  import { applyModelKeys, ensureBlueprint, ensureOrgKey, ensureProviderModels, retireLocalModel, seedLocalRuntime, signIn } from './local-seed'
43
46
  import {
@@ -80,7 +83,7 @@ export const IMAGES = {
80
83
  export const containerName = (project: LocalProject, component: ComponentName) =>
81
84
  `frontera-${project.id}-${component}`
82
85
 
83
- export const volumeName = (project: LocalProject) => `frontera-${project.id}-db`
86
+ export const volumeName = (project: LocalProject) => dbVolumeName(project.id)
84
87
 
85
88
  interface Running {
86
89
  children: Map<ComponentName, ChildProcess>
@@ -144,6 +147,8 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
144
147
  }
145
148
  for (const name of [...running.containers].reverse()) stopContainer(name)
146
149
  for (const c of state.components) if (c.status === 'running' || c.status === 'starting') set(c.name, { status: 'stopped' })
150
+ // A failed start says so where it failed (`fail`); this is the stop of a runtime that was up.
151
+ if (code === 0) recordRuntime(project, { event: 'stopped', at: new Date().toISOString() })
147
152
  state = { ...state, phase: 'stopped', supervisorPid: undefined }
148
153
  save()
149
154
  process.exit(code)
@@ -155,6 +160,7 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
155
160
  set(name, { status: 'failed', detail })
156
161
  state = { ...state, phase: 'failed', error: `${name}: ${detail}` }
157
162
  save()
163
+ recordRuntime(project, { event: 'failed', at: new Date().toISOString(), since: state.startedAt, detail: `${name}: ${detail}` })
158
164
  await shutdown(1)
159
165
  }
160
166
 
@@ -278,6 +284,7 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
278
284
  set('studio', { status: 'running', startedAt: new Date().toISOString(), pid: process.pid })
279
285
  state = { ...state, phase: 'ready', error: undefined }
280
286
  save()
287
+ recordRuntime(project, { event: 'started', at: new Date().toISOString(), since: state.startedAt })
281
288
  } catch (err) {
282
289
  const failing = state.components.find((c) => c.status === 'starting')?.name ?? 'service'
283
290
  await fail(failing, (err as Error).message)
@@ -393,7 +400,7 @@ async function startStorage(project: LocalProject, ports: PortBlock, secrets: Lo
393
400
  projectId: project.id,
394
401
  ports: [{ host: ports.storage, container: 9000 }],
395
402
  env: { RUSTFS_ACCESS_KEY: secrets.storageAccessKey, RUSTFS_SECRET_KEY: secrets.storageSecretKey },
396
- volumes: [{ name: `frontera-${project.id}-storage`, path: '/data' }],
403
+ volumes: [{ name: storageVolumeName(project.id), path: '/data' }],
397
404
  memory: '256m',
398
405
  cpus: '0.5',
399
406
  args: ['server', '/data'],
@@ -462,7 +469,7 @@ async function startService(
462
469
  runnerEnabled,
463
470
  jobsEnabled,
464
471
  storageEnabled,
465
- keylessEmbeddings: keylessEmbeddings(),
472
+ keylessEmbeddings: localEmbeddingsReachable(),
466
473
  documentationBundle: config.mode === 'source' ? customerDocsBundle(config.platformSource) : null,
467
474
  })
468
475
  const log = logPath(project, 'service')
@@ -479,6 +486,7 @@ async function startService(
479
486
  if (!ok) throw new Error('the service did not become healthy; see `frontera local logs service`')
480
487
  set('service', { status: 'running', pid: child.pid, startedAt: new Date().toISOString() })
481
488
  keepDocs(project, { bundle: customerDocsBundle(config.platformSource) }, log)
489
+ refreshCheckoutDocs(project, config.platformSource, log)
482
490
  return
483
491
  }
484
492
  const name = containerName(project, 'service')
@@ -504,7 +512,7 @@ async function startService(
504
512
  keepDocs(project, { fromContainer: (dest) => copyFromContainer(name, IMAGE_DOCS_PATH, dest) }, log)
505
513
  }
506
514
 
507
- /** The release's documentation, for the solution's coding agents (`local docs-mcp`). */
515
+ /** The release's documentation, for the solution's coding agents (`frontera mcp`). */
508
516
  function keepDocs(project: LocalProject, from: { bundle?: string | null; fromContainer?: (dest: string) => boolean }, log: string): void {
509
517
  if (!from.bundle && !from.fromContainer) {
510
518
  appendFileSync(log, '[docs] no customer documentation in the platform checkout; run `bun run docs:build` there\n')
@@ -514,6 +522,36 @@ function keepDocs(project: LocalProject, from: { bundle?: string | null; fromCon
514
522
  if (problem) appendFileSync(log, `[docs] the documentation for coding agents was not copied: ${problem}\n`)
515
523
  }
516
524
 
525
+ /**
526
+ * From a checkout whose documentation build is behind it (no build, guides
527
+ * changed since, another version): build it in the background, at low
528
+ * priority, and refresh the copy when it is done. Start never waits for it:
529
+ * the copy from the previous build serves until then, and the service rereads
530
+ * the checkout's publication when it changes on disk.
531
+ */
532
+ function refreshCheckoutDocs(project: LocalProject, platformSource: string | undefined, log: string): void {
533
+ if (!platformSource) return
534
+ const why = checkoutDocsStale(platformSource)
535
+ if (!why) {
536
+ // Current: a failure an earlier start recorded no longer applies.
537
+ writeDocsBuildMarker(project, null)
538
+ return
539
+ }
540
+ appendFileSync(log, `[docs] ${why}; building it in the background (nice -n 10 bun run docs:build); the previous copy serves until it is done\n`)
541
+ // What `local status` and Forge's Docs say while it builds ("rebuilding"), and if it fails.
542
+ writeDocsBuildMarker(project, { state: 'building', at: new Date().toISOString(), pid: process.pid, why })
543
+ void buildDocs(platformSource, log).then((built) => {
544
+ if (!built) {
545
+ writeDocsBuildMarker(project, { state: 'failed', at: new Date().toISOString(), why })
546
+ appendFileSync(log, '[docs] the documentation build did not finish; coding agents keep the previous copy (`bun run docs:build` in the checkout shows why)\n')
547
+ return
548
+ }
549
+ keepDocs(project, { bundle: customerDocsBundle(platformSource) }, log)
550
+ writeDocsBuildMarker(project, null)
551
+ appendFileSync(log, '[docs] built the documentation and refreshed the copy for coding agents\n')
552
+ })
553
+ }
554
+
517
555
  /** The formats of the project's files, for its coding agents and editors (`local schema`). Logged, never a reason to fail start. */
518
556
  async function keepFileSchemas(project: LocalProject, source: SchemaSource, log: string): Promise<void> {
519
557
  for (const problem of await keepSchemas(project, source)) appendFileSync(log, `[schemas] not kept: ${problem}\n`)
@@ -131,6 +131,8 @@ export function itemTitle(kind: string, name: string, files: Record<string, stri
131
131
  const own = declared(doc?.config, 'name') ?? declared(doc, 'name')
132
132
  if (own) return own
133
133
  }
134
+ // The workspace's own grants beyond read (`blueprint/workspace-grants.yaml`): one file, no display name.
135
+ if (kind === 'blueprint' && name === 'workspace-grants') return 'Workspace grants'
134
136
  if (kind === 'blueprint') {
135
137
  const [dir, apiName] = name.split('/')
136
138
  const file = Object.entries(files).find(([p]) => p.startsWith(`blueprint/${dir}/${apiName}.`))?.[1]
@@ -84,6 +84,13 @@ export interface RemoteItem {
84
84
  * is being read; `from`, the snapshot's copy, once fetched.
85
85
  */
86
86
  assets?: ItemAsset[]
87
+ /**
88
+ * Files the pull recovered from the item's deployed bundle because its
89
+ * source lacks them (a Function's imports from beside its folder), by
90
+ * project path. Pulled like any other file; listed so it is never mistaken
91
+ * for the original source.
92
+ */
93
+ recovered?: string[]
87
94
  }
88
95
 
89
96
  export interface ItemAsset {
@@ -193,6 +200,8 @@ export interface LockEntry {
193
200
  pulledAt: string
194
201
  /** Pulled with conflicts not yet resolved; the base above is still the last clean pull. */
195
202
  conflict?: LockConflict
203
+ /** Its files the pull recovered from the deployed bundle, not its source (`RemoteItem.recovered`). */
204
+ recovered?: string[]
196
205
  }
197
206
 
198
207
  /**
@@ -245,6 +254,7 @@ function sortEntry(entry: LockEntry): LockEntry {
245
254
  ...(entry.state ? { state: entry.state } : {}),
246
255
  pulledAt: entry.pulledAt,
247
256
  ...(entry.conflict ? { conflict: sortConflict(entry.conflict) } : {}),
257
+ ...(entry.recovered?.length ? { recovered: [...entry.recovered].sort() } : {}),
248
258
  }
249
259
  }
250
260
 
@@ -475,6 +485,7 @@ function entryFor(item: RemoteItem, now: string): LockEntry {
475
485
  ...(item.version ? { version: item.version } : {}),
476
486
  ...(item.state ? { state: item.state } : {}),
477
487
  pulledAt: now,
488
+ ...(item.recovered?.length ? { recovered: [...item.recovered].sort() } : {}),
478
489
  }
479
490
  }
480
491
 
@@ -1,13 +1,15 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { appendFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
3
  import { dirname, join, posix } from 'node:path'
4
4
 
5
+ import { ignoreInFronteraDir } from '../project-context'
5
6
  import matter from 'gray-matter'
6
7
  import { parse as parseYaml } from 'yaml'
7
8
 
8
9
  import { mimeFromPath } from '../commands/skill/bundle-commands'
9
10
  import { writeFileAtomic } from '../fs-atomic'
10
11
  import { CliError } from '../errors'
12
+ import { readWorkspaceGrants, WORKSPACE_GRANTS_FILE, WORKSPACE_GRANTS_NAME, WORKSPACE_GRANTS_REF } from './local-workspace-grants'
11
13
  import { documentChanges, localDocuments, splitDocumentPath, type PulledDocument } from './local-knowledge-documents'
12
14
  import { QUALITY_CHECKS_FILE, qualityChecksPath, readQualityChecks } from './local-quality-checks'
13
15
  import { lessonsPath, lessonsProposal } from './local-lessons'
@@ -266,6 +268,8 @@ export function projectRefs(root: string): Map<string, { kind: SyncKind; name: s
266
268
  for (const n of dirs('skills')) if (existsSync(join(root, 'skills', n, 'SKILL.md'))) add('skill', n)
267
269
  for (const n of dirs('knowledge')) if (existsSync(join(root, 'knowledge', n, 'knowledge.json'))) add('knowledge', n)
268
270
  for (const dir of BLUEPRINT_DIRS) for (const f of dirs(`blueprint/${dir}`)) if (/\.(ya?ml|json)$/.test(f)) add('blueprint', `${dir}/${f.replace(/\.(ya?ml|json)$/, '')}`)
271
+ // The workspace's own grants beyond read: one Blueprint item, `blueprint/workspace-grants`.
272
+ if (existsSync(join(root, WORKSPACE_GRANTS_FILE))) add('blueprint', WORKSPACE_GRANTS_NAME)
269
273
  for (const f of dirs('mcp')) if (f.endsWith('.plugin.json') && f !== '.plugin.json') add('plugin', f.slice(0, -'.plugin.json'.length))
270
274
  for (const f of dirs('chat-apps')) if (f.endsWith('.json') && f !== '.json') add('chat-app', f.slice(0, -'.json'.length))
271
275
  for (const slug of dirs('agents')) if (existsSync(join(root, lessonsPath(slug)))) add('lessons', slug)
@@ -441,12 +445,15 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
441
445
  else if (status === 'conflict') block('moved', BLOCK_REASON.moved)
442
446
  else if (status === 'unresolved') block('conflicts', BLOCK_REASON.conflicts)
443
447
  else if (deleted && NOT_DELETED[kind]) block('shape', NOT_DELETED[kind]!)
448
+ // Never proposed as a deletion: the platform refuses one, since deleting the file changes no grant.
449
+ else if (deleted && ref === WORKSPACE_GRANTS_REF) block('shape', `Deleting ${WORKSPACE_GRANTS_FILE} changes no grant: to take read_sensitive away, put the file back and list the type with \`permissions: [read]\``)
444
450
  else if (status === 'deleted-remote') block('workspace-deleted', BLOCK_REASON['workspace-deleted'])
445
451
  else if (deleted) {
446
452
  // Proposable: Apply deletes it there, after everything that names it is updated.
447
453
  } else if (problem) block('shape', problem)
448
454
  else if (!Object.keys(files).length) block('shape', 'No files to propose')
449
455
  else if (kind === 'blueprint' && Object.keys(files).length > 1) block('shape', `${Object.keys(files).join(' and ')}: keep one file per Blueprint artifact`)
456
+ else if (ref === WORKSPACE_GRANTS_REF && readWorkspaceGrants(root)?.problems.length) block('shape', readWorkspaceGrants(root)!.problems.join('; '))
450
457
  else if (assetState?.changed && entry?.revision && theirs?.revision && entry.revision !== theirs.revision && textSame) {
451
458
  // A pull never writes over an asset the folder has, so one the workspace changed since reads as a change here.
452
459
  block('moved', 'Its assets changed in the workspace since your last pull: delete the copies here to bring the workspace’s, then propose')
@@ -577,12 +584,8 @@ const REQUESTS_DIR = join(SYNC_DIR, 'requests')
577
584
 
578
585
  /** `.frontera/sync/` stays out of git, the requests with it. */
579
586
  function ignoreSync(root: string): void {
580
- const ignore = join(root, '.frontera', '.gitignore')
581
- mkdirSync(dirname(ignore), { recursive: true })
582
- const text = existsSync(ignore) ? readFileSync(ignore, 'utf8') : ''
583
- if (text.split(/\r?\n/).some((l) => l.trim() === 'sync/' || l.trim() === 'sync')) return
584
- if (text) appendFileSync(ignore, `${text.endsWith('\n') ? '' : '\n'}sync/\n`)
585
- else writeFileSync(ignore, 'local/\nsync/\n')
587
+ // A pulled or linked project: its sync state, and this machine's profile binding, stay out of git.
588
+ ignoreInFronteraDir(root, ['local/', 'sync/'])
586
589
  }
587
590
 
588
591
  /** The requests file for this workspace; another workspace's (a relinked project) reads as none. */
@@ -15,6 +15,8 @@
15
15
  * directory knows, is rewritten.
16
16
  */
17
17
 
18
+ import { slugify } from './local-project'
19
+
18
20
  /**
19
21
  * `object` is an object definition an Agent's object tools work on
20
22
  * (`object/claim_ticket`), by slug; its fields travel in the Agent's file.
@@ -24,7 +26,50 @@ export type RefKind = (typeof REF_KINDS)[number]
24
26
 
25
27
  /** Names as resources are named: slugs, API names, install names. */
26
28
  const NAME = '[A-Za-z0-9][A-Za-z0-9._-]*'
27
- const REF = new RegExp(`^(${REF_KINDS.join('|')})/(${NAME})$`)
29
+ /**
30
+ * Kinds people name in words: a saved object set ("Open work orders") and a
31
+ * knowledge base. A reference to one may hold the name as people say it,
32
+ * spaces and all (`objectSet/Open work orders`), or its name as a reference
33
+ * holds it (`objectSet/open-work-orders`, `refName`); both resolve.
34
+ */
35
+ export const WORDED_KINDS: readonly RefKind[] = ['objectSet', 'knowledge']
36
+ /** A worded name: no leading or trailing space, no slash, no line break. */
37
+ const WORDS = '[A-Za-z0-9](?:[A-Za-z0-9 ._()&\'’,-]*[A-Za-z0-9._)])?'
38
+ const PLAIN_KINDS = REF_KINDS.filter((k) => !WORDED_KINDS.includes(k))
39
+ const REF = new RegExp(`^(?:(${PLAIN_KINDS.join('|')})/(${NAME})|(${WORDED_KINDS.join('|')})/(${WORDS}))$`)
40
+
41
+ /**
42
+ * The pattern a whole-value reference of these kinds matches (`^(function|workflow)/<name>$`),
43
+ * the same one `parseRef` reads: what `local schema` tells an editor a project file may write
44
+ * where the runtime wants an id.
45
+ */
46
+ export function refPattern(kinds: readonly RefKind[] = REF_KINDS): string {
47
+ const plain = kinds.filter((k) => !WORDED_KINDS.includes(k))
48
+ const worded = kinds.filter((k) => WORDED_KINDS.includes(k))
49
+ const parts = [...(plain.length ? [`(${plain.join('|')})/${NAME}`] : []), ...(worded.length ? [`(${worded.join('|')})/${WORDS}`] : [])]
50
+ return parts.length === 1 ? `^${parts[0]}$` : `^(?:${parts.join('|')})$`
51
+ }
52
+
53
+ /**
54
+ * A resource's name as a reference holds it: as it is when it is already a
55
+ * slug or API name, else its words made a slug (`Open work orders` →
56
+ * `open-work-orders`, as `slugify` makes one, at most 24 characters).
57
+ */
58
+ export function refName(name: string): string {
59
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) ? name : slugify(name) || 'unnamed'
60
+ }
61
+
62
+ /**
63
+ * A name's id in one kind's map: as written, or for a kind named in words,
64
+ * by its name as a reference holds it (`objectSet/Open work orders` finds the
65
+ * set the directory lists as `open-work-orders`).
66
+ */
67
+ export function lookupRef(names: ReadonlyMap<string, string> | undefined, kind: RefKind, name: string): string | undefined {
68
+ if (!names) return undefined
69
+ const exact = names.get(name)
70
+ if (exact !== undefined || !WORDED_KINDS.includes(kind)) return exact
71
+ return names.get(refName(name))
72
+ }
28
73
  /** `[[function:<id or name>]]` in guidance text, the platform's link form. */
29
74
  const LINK = new RegExp(`\\[\\[(${REF_KINDS.join('|')}):([^\\]\\s]+)\\]\\]`, 'g')
30
75
  /** `function:<id or name>` as a whole value: a procedure's tool list names its tools this way. */
@@ -37,7 +82,8 @@ export function isUuid(value: string): boolean {
37
82
 
38
83
  export function parseRef(value: string): { kind: RefKind; name: string } | null {
39
84
  const m = REF.exec(value)
40
- return m ? { kind: m[1] as RefKind, name: m[2]! } : null
85
+ if (!m) return null
86
+ return m[1] ? { kind: m[1] as RefKind, name: m[2]! } : { kind: m[3] as RefKind, name: m[4]! }
41
87
  }
42
88
 
43
89
  export function formatRef(kind: RefKind, name: string): string {
@@ -147,7 +193,7 @@ export function toIds<T>(doc: T, directory: Directory): { doc: T; unresolved: Un
147
193
  const out = walk(doc, [], (s, path) => {
148
194
  const ref = parseRef(s)
149
195
  if (ref) {
150
- const id = directory[ref.kind]?.get(ref.name)
196
+ const id = lookupRef(directory[ref.kind], ref.kind, ref.name)
151
197
  if (id) return id
152
198
  unresolved.push({ path: path.join('.'), value: s, kind: ref.kind })
153
199
  return s