@frontera-sdk/cli 1.52.28 → 1.52.30
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/package.json +5 -4
- package/src/commands/local/data.ts +60 -0
- package/src/commands/local/dev.ts +361 -18
- package/src/commands/local/index-commands.ts +11 -3
- package/src/commands/local/init.ts +88 -7
- package/src/commands/local/sync-request.ts +750 -0
- package/src/commands/local/sync.ts +941 -0
- package/src/commands/skill/bundle-commands.ts +9 -6
- package/src/errors.ts +4 -1
- package/src/exit.ts +3 -0
- package/src/flag-help.ts +48 -0
- package/src/local/local-actions.ts +306 -0
- package/src/local/local-adapt.ts +216 -0
- package/src/local/local-agent-files.ts +9 -2
- package/src/local/local-agent-grants.ts +125 -0
- package/src/local/local-agent-name.ts +71 -0
- package/src/local/local-applied.ts +334 -3
- package/src/local/local-apps.ts +156 -0
- package/src/local/local-blueprint-outline.ts +465 -0
- package/src/local/local-brand.ts +123 -0
- package/src/local/local-build-summary.ts +106 -0
- package/src/local/local-chat-apps.ts +170 -0
- package/src/local/local-dev-references.ts +1132 -0
- package/src/local/local-dev-sync.ts +171 -7
- package/src/local/local-docs.ts +53 -5
- package/src/local/local-env.ts +2 -0
- package/src/local/local-function-draft.ts +33 -0
- package/src/local/local-function-shared.ts +65 -0
- package/src/local/local-function-state.ts +34 -0
- package/src/local/local-knowledge-documents.ts +302 -0
- package/src/local/local-lessons.ts +278 -0
- package/src/local/local-library.ts +124 -0
- package/src/local/local-media-sets.ts +31 -0
- package/src/local/local-object-sets.ts +181 -0
- package/src/local/local-objects.ts +159 -0
- package/src/local/local-operating.ts +9 -5
- package/src/local/local-org-settings.ts +118 -0
- package/src/local/local-portability.ts +346 -0
- package/src/local/local-project.ts +6 -2
- package/src/local/local-quality-checks.ts +171 -0
- package/src/local/local-scaffold.ts +14 -11
- package/src/local/local-schemas.ts +33 -2
- package/src/local/local-secrets.ts +306 -0
- package/src/local/local-stand-ins.ts +442 -0
- package/src/local/local-studio-server.ts +23 -2
- package/src/local/local-sync-base.ts +237 -0
- package/src/local/local-sync-client.ts +93 -0
- package/src/local/local-sync-link.ts +122 -0
- package/src/local/local-sync-merge.ts +395 -0
- package/src/local/local-sync-names.ts +169 -0
- package/src/local/local-sync-plan.ts +717 -0
- package/src/local/local-sync-push.ts +986 -0
- package/src/local/local-sync-refs.ts +221 -0
- package/src/local/local-sync-remote.ts +1482 -0
- package/src/local/local-sync-snapshot.ts +526 -0
- package/src/local/local-synthetic-data.ts +374 -0
- package/src/local/local-test-runner.ts +172 -14
- package/src/local/local-themes.ts +201 -0
- 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
|
+
}
|