@frontera-sdk/cli 1.52.27 → 1.52.29

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 (59) hide show
  1. package/package.json +5 -4
  2. package/src/commands/local/data.ts +60 -0
  3. package/src/commands/local/dev.ts +361 -18
  4. package/src/commands/local/index-commands.ts +11 -3
  5. package/src/commands/local/init.ts +88 -7
  6. package/src/commands/local/sync-request.ts +750 -0
  7. package/src/commands/local/sync.ts +941 -0
  8. package/src/commands/skill/bundle-commands.ts +9 -6
  9. package/src/errors.ts +4 -1
  10. package/src/exit.ts +3 -0
  11. package/src/flag-help.ts +48 -0
  12. package/src/local/local-actions.ts +306 -0
  13. package/src/local/local-adapt.ts +216 -0
  14. package/src/local/local-agent-files.ts +9 -2
  15. package/src/local/local-agent-grants.ts +125 -0
  16. package/src/local/local-agent-name.ts +71 -0
  17. package/src/local/local-applied.ts +334 -3
  18. package/src/local/local-apps.ts +156 -0
  19. package/src/local/local-blueprint-outline.ts +465 -0
  20. package/src/local/local-brand.ts +123 -0
  21. package/src/local/local-build-summary.ts +106 -0
  22. package/src/local/local-chat-apps.ts +170 -0
  23. package/src/local/local-dev-references.ts +1132 -0
  24. package/src/local/local-dev-sync.ts +171 -7
  25. package/src/local/local-docs.ts +53 -5
  26. package/src/local/local-env.ts +2 -0
  27. package/src/local/local-function-draft.ts +33 -0
  28. package/src/local/local-function-shared.ts +65 -0
  29. package/src/local/local-function-state.ts +34 -0
  30. package/src/local/local-knowledge-documents.ts +302 -0
  31. package/src/local/local-lessons.ts +278 -0
  32. package/src/local/local-library.ts +124 -0
  33. package/src/local/local-media-sets.ts +31 -0
  34. package/src/local/local-object-sets.ts +181 -0
  35. package/src/local/local-objects.ts +159 -0
  36. package/src/local/local-operating.ts +9 -5
  37. package/src/local/local-org-settings.ts +118 -0
  38. package/src/local/local-portability.ts +346 -0
  39. package/src/local/local-project.ts +6 -2
  40. package/src/local/local-quality-checks.ts +171 -0
  41. package/src/local/local-scaffold.ts +14 -11
  42. package/src/local/local-schemas.ts +33 -2
  43. package/src/local/local-secrets.ts +306 -0
  44. package/src/local/local-stand-ins.ts +442 -0
  45. package/src/local/local-studio-server.ts +23 -2
  46. package/src/local/local-sync-base.ts +237 -0
  47. package/src/local/local-sync-client.ts +93 -0
  48. package/src/local/local-sync-link.ts +122 -0
  49. package/src/local/local-sync-merge.ts +395 -0
  50. package/src/local/local-sync-names.ts +169 -0
  51. package/src/local/local-sync-plan.ts +717 -0
  52. package/src/local/local-sync-push.ts +986 -0
  53. package/src/local/local-sync-refs.ts +221 -0
  54. package/src/local/local-sync-remote.ts +1482 -0
  55. package/src/local/local-sync-snapshot.ts +526 -0
  56. package/src/local/local-synthetic-data.ts +374 -0
  57. package/src/local/local-test-runner.ts +172 -14
  58. package/src/local/local-themes.ts +201 -0
  59. package/src/local/local-workflow-state.ts +131 -0
@@ -0,0 +1,106 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { buildCounts, buildSummaryLine, buildView, combineBuilds, loadSettingsDifferences, refOfTarget, type ItemBuild, type ItemBuildState } from './local-applied'
5
+ import { blueprintBuild, localOrganizationId, objectStatus, projectBlueprintOutline } from './local-blueprint-outline'
6
+ import { allTargets } from './local-dev-sync'
7
+
8
+ /**
9
+ * How the last `local dev` left the project, counted one way for every
10
+ * surface that says it: `local sync status`, Forge's project home and the
11
+ * Agent page in Studio. Read from the project alone (`.frontera/local/`
12
+ * against the files now), so it answers whether or not the runtime runs.
13
+ */
14
+
15
+ function entries(dir: string): string[] {
16
+ try {
17
+ return readdirSync(dir).filter((name) => !name.startsWith('.'))
18
+ } catch {
19
+ return []
20
+ }
21
+ }
22
+
23
+ const projectOf = (root: string, id = '') => ({ root, id, stateDir: join(root, '.frontera', 'local') })
24
+
25
+ /**
26
+ * How each Agent (its file, behavior settings, tools and lessons together),
27
+ * workflow and Function was built, by the reference other files name it by
28
+ * (`agent/desk`). Null before the project's runtime first started.
29
+ */
30
+ export function projectItemBuilds(root: string, id = ''): Record<string, ItemBuild> | null {
31
+ const org = localOrganizationId(root)
32
+ if (!org) return null
33
+ const view = buildView(projectOf(root, id), org)
34
+ const byRef = new Map<string, ItemBuild[]>()
35
+ for (const target of allTargets(root)) {
36
+ // Every kind the project home lists rows of; datasets and the Blueprint as a whole are not rows.
37
+ if (target.kind === 'dataset' || target.kind === 'blueprint') continue
38
+ const ref = refOfTarget(target)
39
+ byRef.set(ref, [...(byRef.get(ref) ?? []), view.get(target.key)!])
40
+ }
41
+ return Object.fromEntries([...byRef].map(([ref, list]) => [ref, combineBuilds(list)]))
42
+ }
43
+
44
+ /** One item's build, with each adaptation in words: what an Agent's page says about how it runs here. */
45
+ export function itemBuild(root: string, ref: string, id = ''): ItemBuild | null {
46
+ return projectItemBuilds(root, id)?.[ref] ?? null
47
+ }
48
+
49
+ export interface ProjectBuildSummary {
50
+ /** "49 built · 2 adapted · 3 blocked", with the settings that differ from the workspace. */
51
+ line: string
52
+ /** Short: "2 problems", or "All 12 built" — what to say first. */
53
+ headline: string
54
+ /** Blocked and failed together: what does not run. */
55
+ problems: number
56
+ counts: Record<ItemBuildState, number>
57
+ /** Where the runtime's organization settings differ from the workspace's, one line each. */
58
+ settings: string[]
59
+ }
60
+
61
+ /**
62
+ * The project's build in a few numbers, over every Agent, workflow, Function
63
+ * and Blueprint object type the project holds. An object type left off by
64
+ * design (it reads the platform's media) is not counted: it is not a problem
65
+ * to fix. Null before the runtime first started, or when there is nothing.
66
+ */
67
+ export function projectBuildSummary(root: string, id = ''): ProjectBuildSummary | null {
68
+ const builds = projectItemBuilds(root, id)
69
+ if (!builds) return null
70
+ const agents = entries(join(root, 'agents')).filter((slug) => existsSync(join(root, 'agents', slug, 'agent.json')))
71
+ const workflows = entries(join(root, 'workflows')).filter((f) => /\.ya?ml$/.test(f)).map((f) => f.replace(/\.ya?ml$/, ''))
72
+ const functions = entries(join(root, 'functions')).flatMap((f) => {
73
+ const at = join(root, 'functions', f)
74
+ if (/\.ts$/.test(f) && !/\.(test|d)\.ts$/.test(f) && statSync(at).isFile()) return [f.replace(/\.ts$/, '')]
75
+ return existsSync(join(at, 'package.json')) ? [f] : []
76
+ })
77
+ const blueprint = blueprintBuild(root)
78
+ const objects = projectBlueprintOutline(root, blueprint.actions).flatMap((t): ItemBuildState[] => {
79
+ const kind = objectStatus(t, blueprint).kind
80
+ return kind === 'platform-only' ? [] : [kind as ItemBuildState]
81
+ })
82
+ const listed = new Set([...agents.map((a) => `agent/${a}`), ...workflows.map((w) => `workflow/${w}`), ...functions.map((f) => `function/${f}`)])
83
+ const states: ItemBuildState[] = [
84
+ ...[...listed].map((ref) => builds[ref]?.state ?? 'not-applied'),
85
+ // The other kinds the runtime applies (skills, knowledge, Plugins, Chat Apps…); evaluations are run, not applied.
86
+ ...Object.entries(builds).filter(([ref]) => !listed.has(ref) && !ref.startsWith('evaluation/')).map(([, b]) => b.state),
87
+ ...objects,
88
+ ]
89
+ if (!states.length) return null
90
+ const counts = buildCounts(states)
91
+ const org = localOrganizationId(root)
92
+ const settings = org ? loadSettingsDifferences(projectOf(root, id), org) : []
93
+ const problems = counts.blocked + counts.failed
94
+ const line = settings.length ? `${buildSummaryLine(counts)} · ${settings.length} setting${settings.length === 1 ? ' differs' : 's differ'} from the workspace` : buildSummaryLine(counts)
95
+ const headline = problems ? `${problems} problem${problems === 1 ? '' : 's'}` : counts['not-applied'] ? `${counts['not-applied']} not applied yet` : `All ${states.length} built`
96
+ return { line, headline, problems, counts, settings }
97
+ }
98
+
99
+ /** The CLI's line for the build: the problems first, then the rest, short ("2 problems: 1 failed, 1 blocked · 7 built, 3 adapted"). */
100
+ export function buildStatusText(summary: ProjectBuildSummary): string {
101
+ const c = summary.counts
102
+ const problems = [c.failed ? `${c.failed} failed` : '', c.blocked ? `${c.blocked} blocked` : ''].filter(Boolean).join(', ')
103
+ const rest = [c.built ? `${c.built} built` : '', c.adapted ? `${c.adapted} adapted` : '', c.stubbed ? `${c.stubbed} stubbed` : '', c['not-applied'] ? `${c['not-applied']} not applied yet` : ''].filter(Boolean).join(', ')
104
+ const settings = summary.settings.length ? `${summary.settings.length} setting${summary.settings.length === 1 ? ' differs' : 's differ'} from the workspace` : ''
105
+ return [summary.problems ? `${summary.headline}: ${problems}` : rest, summary.problems ? rest : '', settings].filter(Boolean).join(' · ')
106
+ }
@@ -0,0 +1,170 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { RemoteItem } from './local-sync-plan'
5
+ import { itemRef, referencesIn, toIds, toNames, type Directory } from './local-sync-refs'
6
+
7
+ /**
8
+ * Chat Apps: the experience an Agent is used in on the platform. A Chat App
9
+ * holds what people see first (welcome, suggested prompts, the context it
10
+ * gives its Agents, an accent), its roster of Agents, which features it
11
+ * offers (file upload, web browsing, voice, sharing…, each a limit on what
12
+ * its Agents may do there), and its conversation and memory behaviour. A
13
+ * pull writes each published App as `chat-apps/<slug>.json`, its Agents
14
+ * named (`agent/support-concierge`); `local dev` saves it on the local
15
+ * runtime, and a change request proposes it back.
16
+ *
17
+ * Who may open an App and its endpoints (its address, the widget) are the
18
+ * platform's: people and deployment (design §8). The Agent versions a
19
+ * published App pins are the platform's too: locally every Agent is a draft,
20
+ * so the App is saved as the runtime owner's draft: its experience is on the
21
+ * runtime, and it opens once its Agents are published.
22
+ */
23
+
24
+ /** A Chat App as `/v1/sync/chat-apps` lists it. */
25
+ export interface ChatAppRow {
26
+ id: string
27
+ slug: string
28
+ name: string
29
+ description?: string | null
30
+ version?: string | null
31
+ configuration?: Record<string, unknown> | null
32
+ }
33
+
34
+ /** The configuration's sections a project file keeps, in this order. */
35
+ const SECTIONS = ['presentation', 'roster', 'features', 'conversation', 'memory'] as const
36
+
37
+ export const chatAppPath = (slug: string) => `chat-apps/${slug}.json`
38
+
39
+ /** Why a Chat App stays a draft locally, as the build and the Pull dialog say it. */
40
+ export const CHAT_APP_DRAFT = 'Saved as the owner’s draft here and opened as its preview, its Agents running their drafts: a Chat App publishes only published Agents, and the local runtime’s Agents are drafts'
41
+
42
+ /**
43
+ * The project's file for an App's live configuration: its Agents by name,
44
+ * and without the Agent versions the platform pinned when it was published
45
+ * (a version number means nothing on another runtime).
46
+ */
47
+ export function chatAppDocument(row: ChatAppRow, directory: Directory): { doc: Record<string, unknown>; unresolved: RemoteItem['unresolved'] } {
48
+ const config = (row.configuration ?? {}) as Record<string, unknown>
49
+ const roster = { ...((config.roster ?? {}) as Record<string, unknown>) }
50
+ if (Array.isArray(roster.agents)) {
51
+ roster.agents = (roster.agents as Array<Record<string, unknown>>).map(({ agentVersionNumber: _pinned, ...entry }) => entry)
52
+ }
53
+ const doc: Record<string, unknown> = {
54
+ name: row.name,
55
+ ...(row.description ? { description: row.description } : {}),
56
+ schemaVersion: config.schemaVersion ?? 1,
57
+ }
58
+ for (const key of SECTIONS) if (config[key] !== undefined) doc[key] = key === 'roster' ? roster : config[key]
59
+ const named = toNames(doc, directory)
60
+ return { doc: named.doc, unresolved: named.unresolved }
61
+ }
62
+
63
+ /** The workspace's published Chat Apps as project items; one never published is counted in the notes. */
64
+ export function readChatApps(rows: ChatAppRow[], directory: Directory, wanted: (ref: string) => boolean, notes: string[]): RemoteItem[] {
65
+ const out: RemoteItem[] = []
66
+ let unpublished = 0
67
+ for (const row of rows) {
68
+ if (!row.version || !row.configuration) {
69
+ unpublished += 1
70
+ continue
71
+ }
72
+ const ref = `chat-app/${row.slug}`
73
+ if (!wanted(ref)) continue
74
+ const { doc, unresolved } = chatAppDocument(row, directory)
75
+ const requires = referencesIn(doc).map((r) => itemRef(r.kind, r.name))
76
+ const itemNotes = ['Who may open it and its addresses stay on the platform']
77
+ if (unresolved.length) itemNotes.push(`${unresolved.length === 1 ? 'An Agent on its roster is' : `${unresolved.length} Agents on its roster are`} not in this workspace’s list, so ${unresolved.length === 1 ? 'it keeps its' : 'they keep their'} platform id`)
78
+ out.push({ ref, kind: 'chat-app', name: row.slug, title: row.name, files: { [chatAppPath(row.slug)]: `${JSON.stringify(doc, null, 2)}\n` }, unresolved, notes: itemNotes, requires, id: row.id, version: `v${row.version}` })
79
+ }
80
+ if (unpublished) notes.push(`${unpublished} Chat App${unpublished === 1 ? ' was' : 's were'} never published, so ${unpublished === 1 ? 'it stays' : 'they stay'} on the platform: a draft is its author’s own`)
81
+ return out
82
+ }
83
+
84
+ type Call = (method: string, path: string, body?: unknown) => Promise<{ status: number; body: unknown }>
85
+
86
+ const dataOf = (body: unknown) => ((body as { data?: unknown } | null)?.data ?? body) as Record<string, unknown> | null
87
+
88
+ const errorOf = (a: { status: number; body: unknown }) => {
89
+ const b = a.body as { message?: unknown } | null
90
+ return `${a.status} ${String(typeof b?.message === 'string' ? b.message : JSON.stringify(b)).replace(/\s+/g, ' ').slice(0, 200)}`
91
+ }
92
+
93
+ export interface ChatAppApplied {
94
+ ok: boolean
95
+ output: string
96
+ /** Not applied because it names these, which the project holds but the runtime does not have yet. */
97
+ missing?: string[]
98
+ }
99
+
100
+ /**
101
+ * Save a project's Chat App on the runtime: create it (or find it by slug),
102
+ * set its name and description, and save its configuration as the owner's
103
+ * draft, with the roster's Agents as this runtime's ids. Then publish it
104
+ * when the runtime allows: a Chat App publishes only published Agents, and
105
+ * locally every Agent is a draft, so it stays the owner's draft, which the
106
+ * build says. An Agent the project does not
107
+ * hold is left off the roster, and said; one it holds but the runtime does
108
+ * not have yet holds the App back until it does.
109
+ */
110
+ export async function applyChatApp(root: string, slug: string, call: Call, directory: () => Promise<Directory>, provides: (ref: string) => boolean): Promise<ChatAppApplied> {
111
+ const path = join(root, chatAppPath(slug))
112
+ if (!existsSync(path)) return { ok: false, output: `error [CHAT_APP]: ${chatAppPath(slug)} is missing` }
113
+ let doc: Record<string, unknown>
114
+ try {
115
+ doc = JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>
116
+ } catch (err) {
117
+ return { ok: false, output: `error [USAGE]: ${chatAppPath(slug)} is not valid JSON: ${(err as Error).message}` }
118
+ }
119
+ const lines: string[] = []
120
+ const dir = await directory()
121
+ const roster = { ...((doc.roster ?? {}) as Record<string, unknown>) }
122
+ const entries = Array.isArray(roster.agents) ? (roster.agents as Array<Record<string, unknown>>) : []
123
+ const waiting: string[] = []
124
+ const kept = entries.filter((e) => {
125
+ const ref = typeof e.agentConfigId === 'string' ? e.agentConfigId : ''
126
+ if (!toIds(ref, dir).unresolved.length) return true
127
+ if (provides(ref)) waiting.push(ref)
128
+ else lines.push(` warning: Its roster names ${ref.replace('/', ' ')}, which isn’t in this project: the App opens without it here`)
129
+ return false
130
+ })
131
+ if (waiting.length) {
132
+ return { ok: false, missing: waiting, output: [`error [UNRESOLVED_REFERENCE]: ${chatAppPath(slug)} names what this runtime does not have`, ...waiting.map((w) => ` ${w}: in the project, not applied yet`)].join('\n') }
133
+ }
134
+ // The live version when it is published (null), as the platform's editor drafts it.
135
+ roster.agents = kept.map((e) => ({ ...e, agentVersionNumber: null }))
136
+ const ids = new Set(kept.map((e) => e.agentConfigId))
137
+ if (typeof roster.defaultAgentConfigId === 'string' && !ids.has(roster.defaultAgentConfigId)) roster.defaultAgentConfigId = kept[0]?.agentConfigId ?? null
138
+ const { name: _name, description: _description, ...configuration } = { ...doc, roster } as Record<string, unknown>
139
+ const resolved = toIds(configuration, dir).doc as Record<string, unknown>
140
+ const name = typeof doc.name === 'string' && doc.name.trim() ? doc.name.trim() : slug
141
+ const description = typeof doc.description === 'string' ? doc.description : null
142
+ const accent = ((doc.presentation ?? {}) as { accent?: unknown }).accent
143
+
144
+ const ensured = await call('POST', '/v1/platform-apps/ensure', { slug, displayName: name, kind: 'chat', ...(description ? { description } : {}), ...(typeof accent === 'string' ? { accent } : {}) })
145
+ if (ensured.status >= 400) return { ok: false, output: `error [CHAT_APP]: ${errorOf(ensured)}` }
146
+ const appId = String(dataOf(ensured.body)?.id ?? '')
147
+ if (!appId) return { ok: false, output: 'error [CHAT_APP]: the runtime did not return the App' }
148
+ if (!dataOf(ensured.body)?.created) {
149
+ const renamed = await call('PATCH', `/v1/platform-apps/${encodeURIComponent(appId)}`, { displayName: name, description })
150
+ if (renamed.status >= 400) lines.push(` warning: Its name and description could not be set here (${errorOf(renamed)})`)
151
+ }
152
+ const draft = await call('PUT', `/v1/chat-apps/${encodeURIComponent(appId)}/draft`, { configuration: resolved })
153
+ if (draft.status >= 400) return { ok: false, output: `error [CHAT_APP]: ${errorOf(draft)}` }
154
+ const published = await call('POST', `/v1/chat-apps/${encodeURIComponent(appId)}/publish`, { configuration: resolved })
155
+ if (published.status >= 400) {
156
+ const findings = ((dataOf(published.body)?.details ?? (published.body as { details?: unknown } | null)?.details) ?? []) as Array<{ code?: string; message?: string }>
157
+ const list = Array.isArray(findings) ? findings : []
158
+ if (list.length && list.every((f) => f.code === 'agent.unpublished' || f.code === 'agent.unavailable' || f.code === 'roster.empty')) {
159
+ lines.push(
160
+ list.some((f) => f.code === 'roster.empty')
161
+ ? ' warning: It has no Agent on its roster here, so it stays a draft'
162
+ : ` warning: ${CHAT_APP_DRAFT}`,
163
+ )
164
+ } else {
165
+ lines.push(` warning: It stays a draft here: ${list.map((f) => f.message).filter(Boolean).join('; ') || errorOf(published)}`)
166
+ }
167
+ }
168
+ lines.unshift(` saved Chat App ${name}`)
169
+ return { ok: true, output: lines.join('\n') }
170
+ }