@foundation0/api 1.0.0
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/agents.ts +492 -0
- package/dist/git.js +4 -0
- package/git.ts +31 -0
- package/mcp/cli.mjs +37 -0
- package/mcp/cli.ts +43 -0
- package/mcp/client.ts +149 -0
- package/mcp/index.ts +15 -0
- package/mcp/server.ts +461 -0
- package/package.json +40 -0
- package/projects.ts +2690 -0
- package/taskgraph-parser.ts +217 -0
package/agents.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { spawnSync } from 'child_process'
|
|
4
|
+
import { pathToFileURL } from 'url'
|
|
5
|
+
|
|
6
|
+
export interface VersionedFileSpec {
|
|
7
|
+
parts: string[]
|
|
8
|
+
base: string
|
|
9
|
+
version: string | null
|
|
10
|
+
ext: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ActiveFileResult {
|
|
14
|
+
agentName: string
|
|
15
|
+
sourceFile: string
|
|
16
|
+
activeFile: string
|
|
17
|
+
matchedFile: string
|
|
18
|
+
mode: 'symlink'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AgentLoadResult {
|
|
22
|
+
agentName: string
|
|
23
|
+
loaded: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ActiveConfigInput {
|
|
27
|
+
path: string
|
|
28
|
+
required: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CLI_NAME = 'f0'
|
|
32
|
+
const VERSION_RE = 'v?\\d+(?:\\.\\d+){2,}'
|
|
33
|
+
const VERSION_RE_CORE = `(?:${VERSION_RE})`
|
|
34
|
+
const VERSION_WITH_EXT_RE = new RegExp(`^(.+)\\.(${VERSION_RE})\\.([A-Za-z][A-Za-z0-9_-]*)$`)
|
|
35
|
+
const VERSION_ONLY_RE = new RegExp(`^(.+)\\.(${VERSION_RE})$`)
|
|
36
|
+
const PRIORITIZED_EXTS = ['.md', '.json', '.yaml', '.yml', '.ts', '.js', '.txt']
|
|
37
|
+
|
|
38
|
+
export function usage(): string {
|
|
39
|
+
return `Usage:\n ${CLI_NAME} agents <agent-name> <file-path> --set-active\n ${CLI_NAME} agents --list\n ${CLI_NAME} agents <agent-name> load\n\n` +
|
|
40
|
+
`Examples:\n ${CLI_NAME} agents coder /system/boot.v0.0.1 --set-active\n ${CLI_NAME} agents coder /system/boot --set-active --latest\n ${CLI_NAME} agents coder /skills/coding-standards.v0.0.1 --set-active\n ${CLI_NAME} agents --list\n` +
|
|
41
|
+
` ${CLI_NAME} agents coder load\n` +
|
|
42
|
+
`\n` +
|
|
43
|
+
`file-path is relative to the agent root (leading slash required).\n` +
|
|
44
|
+
`Use --latest to resolve /file-name to the latest version.\n` +
|
|
45
|
+
`The active file created is [file].active.<ext>.\n`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveAgentsRoot(processRoot: string = process.cwd()): string {
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(processRoot, 'agents'),
|
|
51
|
+
path.join(processRoot, 'wireborn', 'agents'),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
for (const candidatesPath of candidates) {
|
|
55
|
+
if (existsDir(candidatesPath)) {
|
|
56
|
+
return candidatesPath
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const checked = candidates.map((candidate) => `\n checked path: ${candidate}`).join('')
|
|
61
|
+
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Could not find agents directory relative to working directory.\n cwd: ${processRoot}${checked}`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const resolveAgentsRootFrom = resolveAgentsRoot
|
|
68
|
+
|
|
69
|
+
export function listAgents(processRoot: string = process.cwd()): string[] {
|
|
70
|
+
const agentsPath = resolveAgentsRoot(processRoot)
|
|
71
|
+
|
|
72
|
+
return fs
|
|
73
|
+
.readdirSync(agentsPath, { withFileTypes: true })
|
|
74
|
+
.filter((entry) => entry.isDirectory())
|
|
75
|
+
.map((entry) => entry.name)
|
|
76
|
+
.sort((a, b) => a.localeCompare(b))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseTargetSpec(spec: string): VersionedFileSpec {
|
|
80
|
+
if (!spec || typeof spec !== 'string') {
|
|
81
|
+
throw new Error('file-path is required.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const normalized = spec.replace(/\\/g, '/').trim()
|
|
85
|
+
|
|
86
|
+
if (!normalized.startsWith('/')) {
|
|
87
|
+
throw new Error('file-path must start with a leading slash, e.g. /system/boot.v0.0.1')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const body = normalized.slice(1)
|
|
91
|
+
if (!body) {
|
|
92
|
+
throw new Error('file-path is empty.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parts = body.split('/').filter(Boolean)
|
|
96
|
+
if (parts.length < 2) {
|
|
97
|
+
throw new Error('file-path must include a file name, for example /system/boot.v0.0.1')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const file = parts.pop()
|
|
101
|
+
if (!file) {
|
|
102
|
+
throw new Error('file-path must include a file name, for example /system/boot.v0.0.1')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const withExt = file.match(VERSION_WITH_EXT_RE)
|
|
106
|
+
const withoutExt = file.match(VERSION_ONLY_RE)
|
|
107
|
+
if (withExt) {
|
|
108
|
+
return {
|
|
109
|
+
parts,
|
|
110
|
+
base: withExt[1],
|
|
111
|
+
version: withExt[2],
|
|
112
|
+
ext: withExt[3],
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (withoutExt) {
|
|
117
|
+
return {
|
|
118
|
+
parts,
|
|
119
|
+
base: withoutExt[1],
|
|
120
|
+
version: withoutExt[2],
|
|
121
|
+
ext: null,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (parts.length === 0) {
|
|
126
|
+
throw new Error('file-path must include a file name, for example /system/boot or /system/boot.v0.0.1.')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
parts,
|
|
131
|
+
base: file,
|
|
132
|
+
version: null,
|
|
133
|
+
ext: null,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseVersion(version: string): number[] {
|
|
138
|
+
const clean = version.replace(/^v/, '')
|
|
139
|
+
return clean.split('.').filter(Boolean).map((part) => Number.parseInt(part, 10))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function escapeRegExp(value: string): string {
|
|
143
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function compareVersions(left: string, right: string): number {
|
|
147
|
+
const a = parseVersion(left)
|
|
148
|
+
const b = parseVersion(right)
|
|
149
|
+
const size = Math.max(a.length, b.length)
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < size; i += 1) {
|
|
152
|
+
const leftValue = a[i] ?? 0
|
|
153
|
+
const rightValue = b[i] ?? 0
|
|
154
|
+
if (leftValue > rightValue) return 1
|
|
155
|
+
if (leftValue < rightValue) return -1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return 0
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function findLatestVersionedFile(targetDir: string, base: string, ext: string | null): string | null {
|
|
162
|
+
const fileEntries = fs
|
|
163
|
+
.readdirSync(targetDir, { withFileTypes: true })
|
|
164
|
+
.filter((entry) => entry.isFile())
|
|
165
|
+
.map((entry) => entry.name)
|
|
166
|
+
|
|
167
|
+
const safeBase = escapeRegExp(base)
|
|
168
|
+
const regex = new RegExp(`^${safeBase}\\.(${VERSION_RE_CORE})\\.([^.]+)$`)
|
|
169
|
+
const matches = fileEntries
|
|
170
|
+
.map((entry) => {
|
|
171
|
+
const match = entry.match(regex)
|
|
172
|
+
if (!match) return null
|
|
173
|
+
|
|
174
|
+
const version = match[1]
|
|
175
|
+
const detectedExt = `.${match[2]}`
|
|
176
|
+
if (ext && detectedExt !== `.${ext}`) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { entry, version, detectedExt }
|
|
181
|
+
})
|
|
182
|
+
.filter((value): value is { entry: string; version: string; detectedExt: string } => value !== null)
|
|
183
|
+
|
|
184
|
+
if (matches.length === 0) return null
|
|
185
|
+
|
|
186
|
+
matches.sort((a, b) => compareVersions(a.version, b.version))
|
|
187
|
+
const selected = matches[matches.length - 1]
|
|
188
|
+
|
|
189
|
+
const version = selected.version
|
|
190
|
+
const sameVersion = matches
|
|
191
|
+
.filter((item) => compareVersions(item.version, version) === 0)
|
|
192
|
+
.map((item) => item.entry)
|
|
193
|
+
.sort()
|
|
194
|
+
|
|
195
|
+
if (sameVersion.length <= 1) {
|
|
196
|
+
return sameVersion[0]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const extName of PRIORITIZED_EXTS) {
|
|
200
|
+
const preferred = sameVersion.find((entryName) => entryName.endsWith(extName))
|
|
201
|
+
if (preferred) {
|
|
202
|
+
return preferred
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return sameVersion.sort()[0]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resolveTargetFile(targetDir: string, spec: VersionedFileSpec, options: { latest?: boolean } = {}): string | null {
|
|
210
|
+
const entries = fs
|
|
211
|
+
.readdirSync(targetDir, { withFileTypes: true })
|
|
212
|
+
.filter((entry) => entry.isFile())
|
|
213
|
+
.map((entry) => entry.name)
|
|
214
|
+
|
|
215
|
+
if (!spec.version) {
|
|
216
|
+
if (!options.latest) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
return findLatestVersionedFile(targetDir, spec.base, null)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (spec.ext) {
|
|
223
|
+
const exact = `${spec.base}.${spec.version}.${spec.ext}`
|
|
224
|
+
if (entries.includes(exact)) {
|
|
225
|
+
return exact
|
|
226
|
+
}
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const prefix = `${spec.base}.${spec.version}.`
|
|
231
|
+
const candidates = entries.filter((entry) => entry.startsWith(prefix))
|
|
232
|
+
if (candidates.length === 0) {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const ext of PRIORITIZED_EXTS) {
|
|
237
|
+
const match = candidates.find((entry) => entry.endsWith(ext))
|
|
238
|
+
if (match) {
|
|
239
|
+
return match
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return candidates.sort()[0]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function setActiveLink(sourceFile: string, activeFile: string): 'symlink' {
|
|
247
|
+
if (!existsFile(sourceFile)) {
|
|
248
|
+
throw new Error(`Versioned file not found: ${sourceFile}`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fs.mkdirSync(path.dirname(activeFile), { recursive: true })
|
|
252
|
+
|
|
253
|
+
if (fs.existsSync(activeFile)) {
|
|
254
|
+
fs.rmSync(activeFile, { force: true })
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const linkTarget = path.relative(path.dirname(activeFile), sourceFile)
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
fs.symlinkSync(linkTarget, activeFile, 'file')
|
|
261
|
+
return 'symlink'
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (process.platform === 'win32') {
|
|
264
|
+
const ok = createWindowsSymlink(activeFile, sourceFile)
|
|
265
|
+
if (ok) {
|
|
266
|
+
return 'symlink'
|
|
267
|
+
}
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Failed to create symlink on Windows. Run shell as Administrator or enable Developer Mode.\n${String(error?.message ?? error)}`
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
throw error
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function createWindowsSymlink(activeFile: string, sourceFile: string): boolean {
|
|
278
|
+
const quote = (value: string) => `"${value.replace(/\"/g, '""')}"`
|
|
279
|
+
const command = `mklink ${quote(path.resolve(activeFile))} ${quote(path.resolve(sourceFile))}`
|
|
280
|
+
const result = spawnSync('cmd', ['/c', command], {
|
|
281
|
+
shell: false,
|
|
282
|
+
encoding: 'utf8',
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
return result.status === 0
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function setActive(
|
|
289
|
+
agentName: string,
|
|
290
|
+
target: string,
|
|
291
|
+
processRoot: string = process.cwd(),
|
|
292
|
+
options: { latest?: boolean } = {}
|
|
293
|
+
): ActiveFileResult {
|
|
294
|
+
const spec = parseTargetSpec(target)
|
|
295
|
+
const agentsRoot = resolveAgentsRootFrom(processRoot)
|
|
296
|
+
const targetDir = path.join(agentsRoot, agentName, ...spec.parts)
|
|
297
|
+
const agentDir = path.join(agentsRoot, agentName)
|
|
298
|
+
|
|
299
|
+
if (!existsDir(agentDir)) {
|
|
300
|
+
throw new Error(`Agent folder not found: ${agentDir}`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!existsDir(targetDir)) {
|
|
304
|
+
throw new Error(`Target folder not found: ${targetDir}`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const matchedFile = resolveTargetFile(targetDir, spec, options)
|
|
308
|
+
if (!matchedFile && !spec.version && options.latest) {
|
|
309
|
+
throw new Error(`No files found for /${spec.parts.join('/')}${spec.parts.length > 0 ? '/' : ''}${spec.base}`)
|
|
310
|
+
}
|
|
311
|
+
if (!matchedFile) {
|
|
312
|
+
if (!spec.version && !options.latest) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Version missing for /${spec.parts.join('/')}/${spec.base}. Use --latest to select the latest version.`
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
const extensionHint = spec.ext ? `.${spec.ext}` : ' with any extension'
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Versioned file not found for /${spec.parts.join('/')}/${spec.base}.${spec.version}${extensionHint}`
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const sourceFile = path.join(targetDir, matchedFile)
|
|
324
|
+
const activeFile = path.join(targetDir, `${spec.base}.active${path.extname(matchedFile)}`)
|
|
325
|
+
const mode = setActiveLink(sourceFile, activeFile)
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
agentName,
|
|
329
|
+
sourceFile,
|
|
330
|
+
activeFile,
|
|
331
|
+
matchedFile,
|
|
332
|
+
mode,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function loadAgent(agentName: string, processRoot: string = process.cwd()): Promise<AgentLoadResult> {
|
|
337
|
+
const agentsRoot = resolveAgentsRoot(processRoot)
|
|
338
|
+
const agentDir = path.join(agentsRoot, agentName)
|
|
339
|
+
|
|
340
|
+
if (!existsDir(agentDir)) {
|
|
341
|
+
throw new Error(`Agent folder not found: ${agentDir}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const loadFile = path.join(agentDir, 'load.ts')
|
|
345
|
+
if (!existsFile(loadFile)) {
|
|
346
|
+
throw new Error(`Load file not found for agent '${agentName}': ${loadFile}`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
validateActiveConfigInputs(agentDir)
|
|
350
|
+
|
|
351
|
+
const moduleUrl = pathToFileURL(loadFile).href
|
|
352
|
+
const module = await import(moduleUrl)
|
|
353
|
+
const loadFn = module.load
|
|
354
|
+
|
|
355
|
+
if (typeof loadFn !== 'function') {
|
|
356
|
+
throw new Error(`Load module for agent '${agentName}' must export a 'load' function.`)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
agentName,
|
|
361
|
+
loaded: await loadFn(),
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function validateActiveConfigInputs(agentDir: string): void {
|
|
366
|
+
const issues: string[] = []
|
|
367
|
+
|
|
368
|
+
const requiredInputs = collectActiveConfigInputs(agentDir)
|
|
369
|
+
|
|
370
|
+
for (const input of requiredInputs) {
|
|
371
|
+
if (!existsFile(input.path)) {
|
|
372
|
+
if (input.required) {
|
|
373
|
+
issues.push(`missing: ${path.relative(agentDir, input.path)}`)
|
|
374
|
+
}
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!isSymbolicLink(input.path)) {
|
|
379
|
+
issues.push(`not a symlink: ${path.relative(agentDir, input.path)}`)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (issues.length > 0) {
|
|
384
|
+
throw new Error(`Config input check failed: ${issues.join(', ')}`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function collectActiveConfigInputs(agentDir: string): ActiveConfigInput[] {
|
|
389
|
+
const systemDir = path.join(agentDir, 'system')
|
|
390
|
+
const skillsDir = path.join(agentDir, 'skills')
|
|
391
|
+
|
|
392
|
+
const required: ActiveConfigInput[] = [
|
|
393
|
+
{ path: path.join(systemDir, 'boot.active.md'), required: true },
|
|
394
|
+
{ path: path.join(systemDir, 'workflow.active.md'), required: true },
|
|
395
|
+
{ path: path.join(systemDir, 'tools.active.json'), required: true },
|
|
396
|
+
{ path: path.join(systemDir, 'model.active.json'), required: true },
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
const enabledSkillsPath = path.join(skillsDir, 'enabled-skills.md')
|
|
400
|
+
if (!existsFile(enabledSkillsPath)) {
|
|
401
|
+
return required
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const lines = fs.readFileSync(enabledSkillsPath, 'utf8')
|
|
405
|
+
.split(/\r?\n/)
|
|
406
|
+
.map((line) => line.trim())
|
|
407
|
+
.filter(Boolean)
|
|
408
|
+
|
|
409
|
+
for (const line of lines) {
|
|
410
|
+
required.push({ path: path.join(skillsDir, line), required: true })
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return required
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isSymbolicLink(filePath: string): boolean {
|
|
417
|
+
try {
|
|
418
|
+
return fs.lstatSync(filePath).isSymbolicLink()
|
|
419
|
+
} catch {
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export async function main(argv: string[], processRoot: string = process.cwd()): Promise<string> {
|
|
425
|
+
if (
|
|
426
|
+
argv.length === 0 ||
|
|
427
|
+
argv.includes('--help') ||
|
|
428
|
+
argv.includes('-h')
|
|
429
|
+
) {
|
|
430
|
+
return usage()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const [scope, maybeAgentOrFlag, maybeTarget, ...rest] = argv
|
|
434
|
+
const listMode = scope === 'agents' && maybeAgentOrFlag === '--list'
|
|
435
|
+
|
|
436
|
+
if (scope !== 'agents') {
|
|
437
|
+
throw new Error('Expected command `agents` as first positional argument.')
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (listMode) {
|
|
441
|
+
const unknownFlags = rest.filter(Boolean)
|
|
442
|
+
if (unknownFlags.length > 0) {
|
|
443
|
+
throw new Error(`Unknown flags for list mode: ${unknownFlags.join(', ')}`)
|
|
444
|
+
}
|
|
445
|
+
return listAgents(processRoot).join('\n')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const agentName = maybeAgentOrFlag
|
|
449
|
+
const target = maybeTarget
|
|
450
|
+
const setActive = rest.includes('--set-active')
|
|
451
|
+
const useLatest = rest.includes('--latest')
|
|
452
|
+
|
|
453
|
+
if (target === 'load') {
|
|
454
|
+
if (rest.length > 0) {
|
|
455
|
+
throw new Error(`Unknown flags for load mode: ${rest.join(', ')}`)
|
|
456
|
+
}
|
|
457
|
+
const result = await loadAgent(agentName, processRoot)
|
|
458
|
+
return JSON.stringify(result.loaded, null, 2)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!agentName || !target) {
|
|
462
|
+
throw new Error('Missing required arguments: <agent-name> and <file-path>.')
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!setActive) {
|
|
466
|
+
throw new Error('`--set-active` is required for this operation.')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const unknownFlags = rest.filter((arg) => arg !== '--set-active' && arg !== '--latest')
|
|
470
|
+
if (unknownFlags.length > 0) {
|
|
471
|
+
throw new Error(`Unknown flags: ${unknownFlags.join(', ')}`)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const result = setActive(agentName, target, processRoot, { latest: useLatest })
|
|
475
|
+
return `[${CLI_NAME}] set active: ${result.matchedFile} -> ${path.basename(result.activeFile)} (${result.mode})`
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function existsDir(p: string): boolean {
|
|
479
|
+
try {
|
|
480
|
+
return fs.statSync(p).isDirectory()
|
|
481
|
+
} catch {
|
|
482
|
+
return false
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function existsFile(p: string): boolean {
|
|
487
|
+
try {
|
|
488
|
+
return fs.statSync(p).isFile()
|
|
489
|
+
} catch {
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
}
|