@frontera-sdk/cli 1.53.23 → 1.53.24
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 +4 -4
- package/src/api/knowledge-base-api.ts +230 -0
- package/src/commands/knowledge/index-commands.ts +6 -6
- package/src/commands/knowledge-base/index-commands.ts +328 -0
- package/src/commands/local/check.ts +12 -0
- package/src/commands/local/dev.ts +64 -2
- package/src/commands/local/index-commands.ts +34 -10
- package/src/commands/local/init.ts +11 -2
- package/src/commands/local/schema.ts +1 -1
- package/src/commands/local/sync-request.ts +135 -29
- package/src/commands/local/sync.ts +40 -25
- package/src/commands/registry.ts +4 -1
- package/src/commands/types.ts +6 -0
- package/src/flag-help.ts +10 -4
- package/src/forge/touches.ts +29 -0
- package/src/local/local-applied.ts +28 -0
- package/src/local/local-build-summary.ts +1 -0
- package/src/local/local-component-health.ts +7 -2
- package/src/local/local-dev-references.ts +6 -2
- package/src/local/local-dev-sync.ts +43 -4
- package/src/local/local-docker.ts +16 -1
- package/src/local/local-env.ts +19 -0
- package/src/local/local-file-checks.ts +56 -8
- package/src/local/local-knowledge-base-apply.ts +714 -0
- package/src/local/local-knowledge-base-files.ts +195 -0
- package/src/local/local-knowledge-base-pull.ts +458 -0
- package/src/local/local-knowledge-base-sources-off.ts +67 -0
- package/src/local/local-person-steps.ts +2 -1
- package/src/local/local-personas.ts +64 -7
- package/src/local/local-portability.ts +27 -0
- package/src/local/local-ports.ts +4 -2
- package/src/local/local-project-secrets.ts +30 -5
- package/src/local/local-scaffold.ts +2 -0
- package/src/local/local-schema-check.ts +3 -1
- package/src/local/local-schemas.ts +11 -1
- package/src/local/local-search-index.ts +92 -0
- package/src/local/local-secrets.ts +7 -2
- package/src/local/local-seed.ts +17 -0
- package/src/local/local-state.ts +7 -5
- package/src/local/local-studio-server.ts +3 -3
- package/src/local/local-supervisor.ts +45 -8
- package/src/local/local-sync-link.ts +16 -0
- package/src/local/local-sync-names.ts +10 -1
- package/src/local/local-sync-plan.ts +44 -5
- package/src/local/local-sync-push.ts +165 -34
- package/src/local/local-sync-refs.ts +3 -1
- package/src/local/local-sync-remote.ts +44 -12
- package/src/local/local-sync-snapshot.ts +156 -12
- package/src/main.ts +13 -0
- package/src/mcp/mcp-server.ts +3 -0
- package/src/mcp/tools/docs-tools.ts +1 -1
- package/src/mcp/tools/knowledge-base-tools.ts +85 -0
|
@@ -10,7 +10,7 @@ import { projectBlueprintOutline } from './local-blueprint-outline'
|
|
|
10
10
|
import { currentBuildRows, currentBuildSummary } from './local-build-inputs'
|
|
11
11
|
import { appStatusRow, itemBuild } from './local-build-summary'
|
|
12
12
|
import { KnowledgeDocumentError, KNOWLEDGE_UPLOAD_EXTENSIONS, MAX_KNOWLEDGE_UPLOAD_BYTES, checkUpload, documentInFolder, knowledgeFolderOf, projectDocuments, removeKnowledgeDocument, removeRuntimeCopies, runtimeDocuments, saveKnowledgeDocument, type ServiceCall } from './local-knowledge-uploads'
|
|
13
|
-
import { isAlive, logPath, readApplied, readState, type ComponentName } from './local-state'
|
|
13
|
+
import { COMPONENTS, isAlive, logPath, readApplied, readState, type ComponentName } from './local-state'
|
|
14
14
|
import { appendTestCase, decisionOfApprovals, MIXED_DECISIONS_LINE, noteRedactedVariables, redactedVariablesLine, redactedVariablesOfTimeline, renderTestCase, variablesOfTimeline } from './local-test-cases'
|
|
15
15
|
import { findCaseFiles } from './local-test-runner'
|
|
16
16
|
import { workflowTestListing } from './local-workflow-tests'
|
|
@@ -91,7 +91,7 @@ function cookieToken(req: Request): string | null {
|
|
|
91
91
|
return null
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const LOG_COMPONENTS: ComponentName[] =
|
|
94
|
+
const LOG_COMPONENTS: readonly ComponentName[] = COMPONENTS
|
|
95
95
|
|
|
96
96
|
function tail(path: string, lines: number): string[] {
|
|
97
97
|
if (!existsSync(path)) return []
|
|
@@ -193,7 +193,7 @@ export function startStudioServer(opts: StudioServerOptions) {
|
|
|
193
193
|
const run = testRuns.get(runMatch[1]!)
|
|
194
194
|
return run ? json(run) : json({ error: 'no such test run' }, 404)
|
|
195
195
|
}
|
|
196
|
-
const logMatch = /^\/__local\/logs\/([a-z]+)$/.exec(url.pathname)
|
|
196
|
+
const logMatch = /^\/__local\/logs\/([a-z-]+)$/.exec(url.pathname)
|
|
197
197
|
if (logMatch) {
|
|
198
198
|
const component = logMatch[1] as ComponentName
|
|
199
199
|
if (!LOG_COMPONENTS.includes(component)) return json({ error: 'unknown component' }, 404)
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
containerState,
|
|
7
7
|
copyFromContainer,
|
|
8
8
|
dbVolumeName,
|
|
9
|
+
dockerMemoryBytes,
|
|
9
10
|
execInContainer,
|
|
10
11
|
followLogs,
|
|
11
12
|
imagePresent,
|
|
@@ -49,8 +50,10 @@ import { recordRuntime } from './local-runtime-history'
|
|
|
49
50
|
import { recordExit } from '../telemetry/cli-telemetry'
|
|
50
51
|
import { buildDocs, checkoutDocsStale, copyDocs, customerDocsBundle, IMAGE_DOCS_PATH, writeDocsBuildMarker } from './local-docs'
|
|
51
52
|
import { keepSchemas, type SchemaSource } from './local-schemas'
|
|
52
|
-
import { applyModelKeys, ensureBlueprint, ensureOrgKey, ensureProviderModels, retireLocalModel, seedLocalRuntime, signIn } from './local-seed'
|
|
53
|
+
import { applyModelKeys, ensureBlueprint, ensureKnowledgeSteward, ensureOrgKey, ensureProviderModels, retireLocalModel, seedLocalRuntime, signIn } from './local-seed'
|
|
54
|
+
import { SEARCH_INDEX_IMAGE, searchIndexChoice, searchIndexRunSpec, waitForSearchIndex } from './local-search-index'
|
|
53
55
|
import {
|
|
56
|
+
COMPONENTS,
|
|
54
57
|
logPath,
|
|
55
58
|
readState,
|
|
56
59
|
updateComponent,
|
|
@@ -93,7 +96,8 @@ export const containerName = (project: LocalProject, component: ComponentName) =
|
|
|
93
96
|
export const volumeName = (project: LocalProject) => dbVolumeName(project.id)
|
|
94
97
|
|
|
95
98
|
interface Running {
|
|
96
|
-
|
|
99
|
+
// The search index is a container, never a child process.
|
|
100
|
+
children: Map<Exclude<ComponentName, 'search-index'>, ChildProcess>
|
|
97
101
|
containers: Set<string>
|
|
98
102
|
studio?: ReturnType<typeof startStudioServer>
|
|
99
103
|
embeddings?: ReturnType<typeof startLocalEmbeddings>
|
|
@@ -105,7 +109,8 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
|
|
|
105
109
|
if (!config) throw new Error('no local runtime config; run `frontera local start`')
|
|
106
110
|
const secrets = loadOrCreateSecrets(project)
|
|
107
111
|
const previous = readState(project)
|
|
108
|
-
const
|
|
112
|
+
const searchIndex = searchIndexChoice({ root: project.root, with: config.with ?? [], exclude: config.exclude, dockerMemoryBytes: dockerMemoryBytes() })
|
|
113
|
+
const enabled = (c: ComponentName) => (c === 'search-index' ? searchIndex.on : !config.exclude.includes(c))
|
|
109
114
|
|
|
110
115
|
let state: LocalState = {
|
|
111
116
|
version: 1,
|
|
@@ -118,13 +123,13 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
|
|
|
118
123
|
startedAt: new Date().toISOString(),
|
|
119
124
|
updatedAt: new Date().toISOString(),
|
|
120
125
|
ports,
|
|
121
|
-
components:
|
|
126
|
+
components: COMPONENTS.map(
|
|
122
127
|
(name): ComponentState => ({
|
|
123
128
|
name,
|
|
124
129
|
status: enabled(name) ? 'starting' : 'off',
|
|
125
|
-
kind: name === 'studio' ? 'supervisor' : name === '
|
|
130
|
+
kind: name === 'studio' ? 'supervisor' : name === 'service' || name === 'runner' ? (config.mode === 'source' ? 'process' : 'container') : 'container',
|
|
126
131
|
port: ports[name],
|
|
127
|
-
detail: enabled(name) ? undefined : 'excluded',
|
|
132
|
+
detail: enabled(name) ? undefined : name === 'search-index' && !searchIndex.on ? searchIndex.why : 'excluded',
|
|
128
133
|
}),
|
|
129
134
|
),
|
|
130
135
|
credentials: previous?.credentials,
|
|
@@ -177,6 +182,11 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
|
|
|
177
182
|
await startPostgres(project, ports, secrets, running, set)
|
|
178
183
|
// Object storage before anything that uploads to it.
|
|
179
184
|
if (enabled('storage')) await startStorage(project, ports, secrets, running, set)
|
|
185
|
+
// The search index takes about a minute on one CPU, and the service reaches it only when Knowledge is used:
|
|
186
|
+
// it comes up while the rest starts, and the runtime is ready once it answers. It says its own failure.
|
|
187
|
+
const searchIndexFailed = enabled('search-index')
|
|
188
|
+
? startSearchIndex(project, ports, running, set).then(() => null, (err: Error) => err.message)
|
|
189
|
+
: Promise.resolve(null)
|
|
180
190
|
// 2. Job engine (before the service, so the service's registration lands)
|
|
181
191
|
if (enabled('jobs')) await startJobs(project, ports, running, set)
|
|
182
192
|
// Keyless embeddings answer before the service needs them.
|
|
@@ -203,7 +213,7 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
|
|
|
203
213
|
},
|
|
204
214
|
})
|
|
205
215
|
// 3. Service
|
|
206
|
-
await startService(project, ports, secrets, config, running, set, enabled('runner'), enabled('jobs'), enabled('storage'))
|
|
216
|
+
await startService(project, ports, secrets, config, running, set, enabled('runner'), enabled('jobs'), enabled('storage'), enabled('search-index'))
|
|
207
217
|
// The service has migrated; the catalog it reads is seeded per environment.
|
|
208
218
|
seedPluginCatalog(project)
|
|
209
219
|
// 4. Function runner
|
|
@@ -269,10 +279,18 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
|
|
|
269
279
|
organizationId: state.credentials!.organizationId,
|
|
270
280
|
logFile: logPath(project, 'service'),
|
|
271
281
|
})
|
|
282
|
+
if (enabled('search-index')) {
|
|
283
|
+
await ensureKnowledgeSteward({ serviceUrl, password: secrets.ownerPassword, organizationId: state.credentials!.organizationId, logFile: logPath(project, 'service') })
|
|
284
|
+
}
|
|
272
285
|
state = { ...state, credentials: { ...state.credentials!, session, modelProviders } }
|
|
273
286
|
save()
|
|
274
287
|
// The formats of the project's files, for the release the service runs.
|
|
275
288
|
await keepFileSchemas(project, { serviceUrl, token: state.credentials!.workspaceKey ?? session }, logPath(project, 'service'))
|
|
289
|
+
const searchIndexProblem = await searchIndexFailed
|
|
290
|
+
if (searchIndexProblem) {
|
|
291
|
+
await fail('search-index', searchIndexProblem)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
276
294
|
// 6. Studio. From a checkout, rebuild it first when its sources changed
|
|
277
295
|
// since the last build, so a pulled change is never served stale.
|
|
278
296
|
if (studioBuildStale(config)) {
|
|
@@ -294,7 +312,8 @@ export async function runSupervisor(project: LocalProject, ports: PortBlock, ses
|
|
|
294
312
|
save()
|
|
295
313
|
recordRuntime(project, { event: 'started', at: new Date().toISOString(), since: state.startedAt })
|
|
296
314
|
} catch (err) {
|
|
297
|
-
|
|
315
|
+
// The search index starts beside the others and says its own failure above.
|
|
316
|
+
const failing = state.components.find((c) => c.status === 'starting' && c.name !== 'search-index')?.name ?? 'service'
|
|
298
317
|
await fail(failing, (err as Error).message)
|
|
299
318
|
return
|
|
300
319
|
}
|
|
@@ -491,6 +510,22 @@ async function startStorage(project: LocalProject, ports: PortBlock, secrets: Lo
|
|
|
491
510
|
throw new Error('object storage did not answer within 45 s')
|
|
492
511
|
}
|
|
493
512
|
|
|
513
|
+
async function startSearchIndex(project: LocalProject, ports: PortBlock, running: Running, set: SetFn) {
|
|
514
|
+
const name = containerName(project, 'search-index')
|
|
515
|
+
await ensureImage(SEARCH_INDEX_IMAGE, project, 'search-index')
|
|
516
|
+
const current = containerState(name)
|
|
517
|
+
if (current === 'exited') startContainer(name)
|
|
518
|
+
else if (current !== 'running') {
|
|
519
|
+
if (current !== 'missing') removeContainer(name)
|
|
520
|
+
runContainer(searchIndexRunSpec({ name, projectId: project.id, port: ports['search-index'] }))
|
|
521
|
+
}
|
|
522
|
+
running.containers.add(name)
|
|
523
|
+
followLogs(name, logPath(project, 'search-index'))
|
|
524
|
+
const why = await waitForSearchIndex(ports['search-index'], 180_000)
|
|
525
|
+
if (why) throw new Error(`the search index did not come up within 180 s (${why}); see \`frontera local logs search-index\`, or start without it: \`frontera local start --exclude search-index\``)
|
|
526
|
+
set('search-index', { status: 'running', container: name, startedAt: new Date().toISOString() })
|
|
527
|
+
}
|
|
528
|
+
|
|
494
529
|
async function startJobs(project: LocalProject, ports: PortBlock, running: Running, set: SetFn) {
|
|
495
530
|
const name = containerName(project, 'jobs')
|
|
496
531
|
await ensureImage(IMAGES.jobs, project, 'jobs')
|
|
@@ -527,6 +562,7 @@ async function startService(
|
|
|
527
562
|
runnerEnabled: boolean,
|
|
528
563
|
jobsEnabled: boolean,
|
|
529
564
|
storageEnabled: boolean,
|
|
565
|
+
searchIndexEnabled: boolean,
|
|
530
566
|
) {
|
|
531
567
|
const env = serviceEnv({
|
|
532
568
|
secrets,
|
|
@@ -535,6 +571,7 @@ async function startService(
|
|
|
535
571
|
runnerEnabled,
|
|
536
572
|
jobsEnabled,
|
|
537
573
|
storageEnabled,
|
|
574
|
+
searchIndexEnabled,
|
|
538
575
|
keylessEmbeddings: localEmbeddingsReachable(),
|
|
539
576
|
documentationBundle: config.mode === 'source' ? customerDocsBundle(config.platformSource) : null,
|
|
540
577
|
})
|
|
@@ -12,6 +12,7 @@ import { configDir } from '../paths'
|
|
|
12
12
|
import { CliError } from '../errors'
|
|
13
13
|
import { deleteProfile, fingerprint, getProfile, putProfile } from '../profiles'
|
|
14
14
|
import { syncClient, type SyncClient } from './local-sync-client'
|
|
15
|
+
import type { PersonClients } from './local-sync-remote'
|
|
15
16
|
import { PROJECT_FILES, type LocalProject } from './local-project'
|
|
16
17
|
import { normalizeOrigin } from './local-sync-client'
|
|
17
18
|
|
|
@@ -360,6 +361,21 @@ export async function organizationReader(owner: KeyOwner, link: SyncLink): Promi
|
|
|
360
361
|
return { client: byKey, notSteward: byKey ? null : (person.email ?? 'the person') }
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
/** One session's two places: the linked workspace, and its organization (where a steward or an administrator reads more). */
|
|
365
|
+
export function personClients(platform: string, token: string, orgId: string, workspaceId: string): PersonClients {
|
|
366
|
+
return {
|
|
367
|
+
workspace: syncClient(platform, token, { 'x-org-id': orgId, 'x-workspace-id': workspaceId }),
|
|
368
|
+
organization: syncClient(platform, token, { 'x-org-id': orgId }),
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The person signed in for the link or a named target, as the Knowledge app reads them; null with no session. */
|
|
373
|
+
export async function personReader(owner: KeyOwner, link: SyncLink): Promise<PersonClients | null> {
|
|
374
|
+
const person = await targetSession(owner, link)
|
|
375
|
+
const orgId = person ? await targetOrganization(owner, link) : null
|
|
376
|
+
return person && orgId ? personClients(link.platform, person.token, orgId, link.workspace) : null
|
|
377
|
+
}
|
|
378
|
+
|
|
363
379
|
const organizationReads = new Map<string, Promise<boolean>>()
|
|
364
380
|
|
|
365
381
|
/** Whether the person may read the organization's Blueprint, asked once per process: a refusal there is a refusal of every organization read. */
|
|
@@ -12,7 +12,8 @@ const KIND_WORD: Record<string, string> = {
|
|
|
12
12
|
workflow: 'Workflow',
|
|
13
13
|
function: 'Function',
|
|
14
14
|
skill: 'Skill',
|
|
15
|
-
knowledge: '
|
|
15
|
+
knowledge: 'Agent knowledge',
|
|
16
|
+
'knowledge-base': 'Knowledge base',
|
|
16
17
|
plugin: 'Plugin',
|
|
17
18
|
evaluation: 'Evaluation dataset',
|
|
18
19
|
dataset: 'Dataset',
|
|
@@ -44,6 +45,8 @@ export function readableName(slug: string): string {
|
|
|
44
45
|
export function kindWord(ref: string): string {
|
|
45
46
|
const [kind, sub] = ref.split('/')
|
|
46
47
|
if (kind === 'blueprint') return BLUEPRINT_WORD[sub ?? ''] ?? 'Blueprint'
|
|
48
|
+
// The organization's Knowledge settings live in the knowledge-base folder but are no base.
|
|
49
|
+
if (ref === 'knowledge-base/settings') return 'Knowledge settings'
|
|
47
50
|
return KIND_WORD[kind ?? ''] ?? readableName(kind ?? 'item')
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -114,6 +117,7 @@ export function namingPaths(kind: string, name: string): string[] {
|
|
|
114
117
|
if (kind === 'chat-app') return [`chat-apps/${name}.json`]
|
|
115
118
|
if (kind === 'app') return [`apps/${name}.json`]
|
|
116
119
|
if (kind === 'knowledge') return [`knowledge/${name}/knowledge.json`]
|
|
120
|
+
if (kind === 'knowledge-base') return name === 'settings' ? [] : [`knowledge-base/${name}/knowledge-base.json`]
|
|
117
121
|
if (kind === 'dataset') return [`datasets/${name}.json`]
|
|
118
122
|
return []
|
|
119
123
|
}
|
|
@@ -160,6 +164,11 @@ export function itemTitle(kind: string, name: string, files: Record<string, stri
|
|
|
160
164
|
const own = declared(parseJson(files[`knowledge/${name}/knowledge.json`]), 'displayName', 'name')
|
|
161
165
|
if (own) return own
|
|
162
166
|
}
|
|
167
|
+
if (kind === 'knowledge-base') {
|
|
168
|
+
if (name === 'settings') return 'Knowledge settings'
|
|
169
|
+
const own = declared(parseJson(files[`knowledge-base/${name}/knowledge-base.json`]), 'name')
|
|
170
|
+
if (own) return own
|
|
171
|
+
}
|
|
163
172
|
// An Agent's lessons are named by their Agent: "Lessons for Agent Claims desk".
|
|
164
173
|
if (kind === 'lessons') {
|
|
165
174
|
const own = declared((parseJson(files[`agents/${name}/agent.json`]) as { config?: unknown } | null)?.config, 'name')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { parse as parseYaml } from 'yaml'
|
|
6
6
|
|
|
@@ -28,11 +28,16 @@ import { comparablePortable, toLocal } from './local-sync-portable'
|
|
|
28
28
|
* pull produces a `dataset/` item. It stays a kind so `--kinds datasets`, a
|
|
29
29
|
* lock written with one and the change-request kinds keep reading as before.
|
|
30
30
|
*
|
|
31
|
+
* `knowledge-base` is the Knowledge app's (`knowledge-base/<folder>/`, and
|
|
32
|
+
* the organization's `knowledge-base/settings.json`), apart from Agent
|
|
33
|
+
* knowledge (`knowledge`). A base's documents are not in its item: they come
|
|
34
|
+
* only when asked for (`pull --documents knowledge-base/<folder>`).
|
|
35
|
+
*
|
|
31
36
|
* `lessons` is an Agent's `lessons.yaml` as a change request carries it
|
|
32
37
|
* (`lessons/<slug>`): brought by `pull --lessons`, never listed by a fetch or
|
|
33
38
|
* kept in the lock, and proposed back as rules for the platform's reviewers.
|
|
34
39
|
*/
|
|
35
|
-
export const SYNC_KINDS = ['blueprint', 'dataset', 'skill', 'function', 'plugin', 'knowledge', 'workflow', 'agent', 'evaluation', 'theme', 'chat-app', 'app', 'brand', 'library', 'lessons'] as const
|
|
40
|
+
export const SYNC_KINDS = ['blueprint', 'dataset', 'skill', 'function', 'plugin', 'knowledge', 'knowledge-base', 'workflow', 'agent', 'evaluation', 'theme', 'chat-app', 'app', 'brand', 'library', 'lessons'] as const
|
|
36
41
|
export type SyncKind = (typeof SYNC_KINDS)[number]
|
|
37
42
|
|
|
38
43
|
export const KIND_LABEL: Record<SyncKind, string> = {
|
|
@@ -42,6 +47,7 @@ export const KIND_LABEL: Record<SyncKind, string> = {
|
|
|
42
47
|
function: 'Functions',
|
|
43
48
|
plugin: 'Plugins',
|
|
44
49
|
knowledge: 'Knowledge',
|
|
50
|
+
'knowledge-base': 'Knowledge bases',
|
|
45
51
|
workflow: 'Workflows',
|
|
46
52
|
agent: 'Agents',
|
|
47
53
|
evaluation: 'Evaluations',
|
|
@@ -93,6 +99,18 @@ export interface RemoteItem {
|
|
|
93
99
|
* for the original source.
|
|
94
100
|
*/
|
|
95
101
|
recovered?: string[]
|
|
102
|
+
/**
|
|
103
|
+
* What the platform keeps of it that its files leave out: a knowledge
|
|
104
|
+
* base's sources the read could not file, by name. A change request carries
|
|
105
|
+
* every source a base has, so a push never proposes it from these files.
|
|
106
|
+
*/
|
|
107
|
+
keptOnPlatform?: string[]
|
|
108
|
+
/**
|
|
109
|
+
* Its name in the workspace's sync index, when the project files it under
|
|
110
|
+
* another (a knowledge base keeps the folder it was pulled into): what a
|
|
111
|
+
* change request names it by.
|
|
112
|
+
*/
|
|
113
|
+
platformName?: string
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
export interface ItemAsset {
|
|
@@ -538,11 +556,31 @@ export function entryFor(item: RemoteItem, now: string): LockEntry {
|
|
|
538
556
|
|
|
539
557
|
export const WORKSPACE_SUFFIX = '.workspace'
|
|
540
558
|
|
|
559
|
+
type CaseFs = { existsSync(path: string): boolean; readdirSync(path: string): string[]; renameSync(from: string, to: string): void }
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Where the file system ignores case (a Mac's, by default), writing
|
|
563
|
+
* `Newsroom.json` over `newsroom.json` keeps the old name, and a push matches a
|
|
564
|
+
* knowledge base's sources by file name: the file there is renamed to the
|
|
565
|
+
* workspace's name first. Where case counts, they are two files, and both stay.
|
|
566
|
+
* True when it renamed one.
|
|
567
|
+
*/
|
|
568
|
+
export function matchCase(full: string, fs: CaseFs = { existsSync, readdirSync, renameSync }): boolean {
|
|
569
|
+
if (!fs.existsSync(full)) return false
|
|
570
|
+
const name = basename(full)
|
|
571
|
+
const entries = fs.readdirSync(dirname(full))
|
|
572
|
+
const held = entries.includes(name) ? undefined : entries.find((e) => e.toLowerCase() === name.toLowerCase())
|
|
573
|
+
if (!held) return false
|
|
574
|
+
fs.renameSync(join(dirname(full), held), full)
|
|
575
|
+
return true
|
|
576
|
+
}
|
|
577
|
+
|
|
541
578
|
/** Write or delete a working file; true when it changed. */
|
|
542
579
|
function put(root: string, path: string, content: string | null): boolean {
|
|
543
580
|
const full = safePath(root, path)
|
|
581
|
+
const renamed = content !== null && matchCase(full)
|
|
544
582
|
const now = readLocal(root, path)
|
|
545
|
-
if (now === content) return
|
|
583
|
+
if (now === content) return renamed
|
|
546
584
|
if (content === null) rmSync(full, { force: true })
|
|
547
585
|
else {
|
|
548
586
|
mkdirSync(dirname(full), { recursive: true })
|
|
@@ -628,11 +666,12 @@ export function applyItems(root: string, workspace: string, items: RemoteItem[],
|
|
|
628
666
|
mkdirSync(dirname(full), { recursive: true })
|
|
629
667
|
// A Plugin's secret values set here stay: they are this computer's configuration. So do a mapped model and
|
|
630
668
|
// the multi-turn evaluation cases (`local-sync-portable.ts`).
|
|
669
|
+
const renamed = matchCase(full)
|
|
631
670
|
const content = isPluginFile(path) ? withLocalSecrets(remote, readLocal(root, path)) : toLocal(path, remote, readLocal(root, path), projectModels(root), opts.base?.(path) ?? null)
|
|
632
671
|
if (readLocal(root, path) !== content) {
|
|
633
672
|
writeFileSync(full, content)
|
|
634
673
|
written.push(path)
|
|
635
|
-
}
|
|
674
|
+
} else if (renamed) written.push(path)
|
|
636
675
|
}
|
|
637
676
|
for (const [path, wroteHash] of Object.entries(previous?.files ?? {})) {
|
|
638
677
|
if (path in item.files) continue
|