@ancleto/spec 0.4.0 → 0.4.3
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 +1 -1
- package/src/cli/index.js +171 -4
- package/templates/AGENTS.md +4 -0
- package/templates/PRODUCT.md +4 -0
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
13
13
|
const ROOT = join(__dirname, '..', '..')
|
|
14
14
|
const ASSETS = ['agents', 'commands', 'skills']
|
|
15
15
|
const TEMPLATES = ['AGENTS.md', 'PRODUCT.md']
|
|
16
|
+
const AZURE_MCP_NOTICE = 'ancleto: MCP azure-devops habilitado — usa las variables de entorno AZURE_DEVOPS_ORG_URL y AZURE_DEVOPS_PAT'
|
|
16
17
|
|
|
17
18
|
const HELP = `ancleto - orquestador SDD liviano con subagentes optimizados para costo/tokens
|
|
18
19
|
(alias: aspec)
|
|
@@ -32,7 +33,9 @@ Uso:
|
|
|
32
33
|
ancleto memory context [--scope X] [--out file]
|
|
33
34
|
Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
|
|
34
35
|
ancleto memory doctor [--rebuild] Diagnostica .ancleto/memory.db (integridad, FTS5, unicidad)
|
|
35
|
-
|
|
36
|
+
y reconstruye el indice FTS5 con --rebuild
|
|
37
|
+
ancleto check Verifica integridad de archivos instalados vs manifiesto
|
|
38
|
+
ancleto doctor Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
36
39
|
ancleto --help Esta ayuda
|
|
37
40
|
ancleto --version Version del paquete
|
|
38
41
|
`
|
|
@@ -221,13 +224,65 @@ async function copyAssets(dest) {
|
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
const DEFAULT_OPENSPEC_CONFIG = `# OpenSpec project configuration
|
|
228
|
+
# Generado por @ancleto/spec (G5) — editalo libremente, no se sobrescribe en reinstalaciones.
|
|
229
|
+
schema: spec-driven-development
|
|
230
|
+
`
|
|
231
|
+
|
|
232
|
+
async function scaffoldOpenSpec(projectDir) {
|
|
233
|
+
const changesDir = join(projectDir, 'openspec', 'changes')
|
|
234
|
+
await mkdir(changesDir, { recursive: true })
|
|
235
|
+
const configPath = join(projectDir, 'openspec', 'config.yaml')
|
|
236
|
+
if (!(await exists(configPath))) {
|
|
237
|
+
await writeFile(configPath, DEFAULT_OPENSPEC_CONFIG)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function extractLockedBlocks(content) {
|
|
242
|
+
const blocks = new Map()
|
|
243
|
+
const re = /<!--\s*LOCKED:\s*([\w-]+)\s*-->([\s\S]*?)<!--\s*\/LOCKED:\s*\1\s*-->/g
|
|
244
|
+
let m
|
|
245
|
+
while ((m = re.exec(content)) !== null) {
|
|
246
|
+
blocks.set(m[1], m[0])
|
|
247
|
+
}
|
|
248
|
+
return blocks
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function replaceLockedBlock(local, name, sourceBlock) {
|
|
252
|
+
const re = new RegExp(`<!--\\s*LOCKED:\\s*${escapeRe(name)}\\s*-->[\\s\\S]*?<!--\\s*\\/LOCKED:\\s*${escapeRe(name)}\\s*-->`)
|
|
253
|
+
if (!re.test(local)) return null
|
|
254
|
+
return local.replace(re, sourceBlock)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function mergeLocked(source, local, filename) {
|
|
258
|
+
const blocks = extractLockedBlocks(source)
|
|
259
|
+
let result = local
|
|
260
|
+
for (const [name, sourceBlock] of blocks) {
|
|
261
|
+
const replaced = replaceLockedBlock(result, name, sourceBlock)
|
|
262
|
+
if (replaced === null) {
|
|
263
|
+
console.warn(`ancleto: no se pudo actualizar el bloque LOCKED "${name}" en ${filename} (tags ausentes o mal formados)`)
|
|
264
|
+
} else {
|
|
265
|
+
result = replaced
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return result
|
|
269
|
+
}
|
|
270
|
+
|
|
224
271
|
async function copyTemplates(projectDir) {
|
|
225
272
|
const dest = join(projectDir, '.opencode')
|
|
226
273
|
await copyAssets(dest)
|
|
227
274
|
for (const t of TEMPLATES) {
|
|
228
275
|
const target = join(projectDir, t)
|
|
229
|
-
|
|
230
|
-
|
|
276
|
+
const source = await readFile(join(ROOT, 'templates', t), 'utf8')
|
|
277
|
+
if (!(await exists(target))) {
|
|
278
|
+
await writeFile(target, source)
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
const local = await readFile(target, 'utf8')
|
|
282
|
+
const merged = mergeLocked(source, local, t)
|
|
283
|
+
if (merged !== local) {
|
|
284
|
+
await writeFile(target, merged)
|
|
285
|
+
}
|
|
231
286
|
}
|
|
232
287
|
}
|
|
233
288
|
|
|
@@ -235,7 +290,7 @@ async function install(args) {
|
|
|
235
290
|
const pi = args.indexOf('--project')
|
|
236
291
|
const project = pi >= 0 ? args[pi + 1] : null
|
|
237
292
|
const withMcp = !args.includes('--no-mcp')
|
|
238
|
-
|
|
293
|
+
let mcpMap = withMcp ? buildDefaultMcp() : {}
|
|
239
294
|
|
|
240
295
|
const ti = args.indexOf('--tier')
|
|
241
296
|
let tier = ti >= 0 ? args[ti + 1] : null
|
|
@@ -256,6 +311,7 @@ async function install(args) {
|
|
|
256
311
|
|
|
257
312
|
if (project) {
|
|
258
313
|
await copyTemplates(resolve(project))
|
|
314
|
+
await scaffoldOpenSpec(resolve(project))
|
|
259
315
|
await writeManifest(resolve(project), {
|
|
260
316
|
installedPaths: {
|
|
261
317
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
@@ -277,6 +333,15 @@ async function install(args) {
|
|
|
277
333
|
await applyTier(join(target, 'agents'), tier)
|
|
278
334
|
await writeFile(tierStatePath(target), tier + '\n')
|
|
279
335
|
|
|
336
|
+
let azureMcp = false
|
|
337
|
+
if (project && withMcp) {
|
|
338
|
+
const rc = await readAncletorc(resolve(project))
|
|
339
|
+
if (rc?.azure?.enabled) {
|
|
340
|
+
mcpMap['azure-devops'] = { type: 'local', enabled: true, command: ['npx', '-y', '@davstack/mcp-azure-devops'] }
|
|
341
|
+
azureMcp = true
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
280
345
|
const res = await mergeMcp(target, mcpMap)
|
|
281
346
|
const loc = project
|
|
282
347
|
? `${resolve(project)} (.opencode/ + templates en la raiz)`
|
|
@@ -286,6 +351,7 @@ async function install(args) {
|
|
|
286
351
|
if (res.added.length) {
|
|
287
352
|
console.log(`ancleto: MCP configurados: ${res.added.join(', ')} en ${res.file}`)
|
|
288
353
|
}
|
|
354
|
+
if (azureMcp) console.log(AZURE_MCP_NOTICE)
|
|
289
355
|
}
|
|
290
356
|
|
|
291
357
|
async function initProject(args) {
|
|
@@ -296,6 +362,8 @@ async function initProject(args) {
|
|
|
296
362
|
if (withAzure) azure.enabled = true
|
|
297
363
|
const discovery = existing?.discovery ?? { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
298
364
|
const manifest = await writeManifest(projectDir, { azure, discovery })
|
|
365
|
+
await scaffoldOpenSpec(projectDir)
|
|
366
|
+
if (azure.enabled) console.log(AZURE_MCP_NOTICE)
|
|
299
367
|
console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'}`)
|
|
300
368
|
}
|
|
301
369
|
|
|
@@ -556,6 +624,99 @@ async function memoryCmd(args) {
|
|
|
556
624
|
process.exit(1)
|
|
557
625
|
}
|
|
558
626
|
|
|
627
|
+
async function checkCommand() {
|
|
628
|
+
const cwd = process.cwd()
|
|
629
|
+
const rc = await readAncletorc(cwd)
|
|
630
|
+
if (!rc || !rc.installedPaths) {
|
|
631
|
+
console.error('ancleto: no hay .ancletorc con installedPaths (corre ancleto init y ancleto install --project)')
|
|
632
|
+
process.exit(1)
|
|
633
|
+
}
|
|
634
|
+
const ip = rc.installedPaths
|
|
635
|
+
let missing = 0
|
|
636
|
+
let orphans = 0
|
|
637
|
+
|
|
638
|
+
for (const t of ip.templates || []) {
|
|
639
|
+
if (await exists(join(cwd, t))) {
|
|
640
|
+
console.log(` ✔ ${t}`)
|
|
641
|
+
} else {
|
|
642
|
+
console.log(` ✖ ${t} (faltante)`)
|
|
643
|
+
missing++
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
for (const cat of ['agents', 'commands', 'skills']) {
|
|
648
|
+
for (const dirRel of ip[cat] || []) {
|
|
649
|
+
const destDir = join(cwd, dirRel)
|
|
650
|
+
if (!(await exists(destDir))) {
|
|
651
|
+
console.log(` ✖ ${dirRel} (directorio faltante)`)
|
|
652
|
+
missing++
|
|
653
|
+
continue
|
|
654
|
+
}
|
|
655
|
+
const expected = (await readdir(join(ROOT, cat))).sort()
|
|
656
|
+
const actual = (await readdir(destDir)).sort()
|
|
657
|
+
const missingFiles = expected.filter((f) => !actual.includes(f))
|
|
658
|
+
const orphanFiles = actual.filter((f) => !expected.includes(f))
|
|
659
|
+
for (const f of missingFiles) {
|
|
660
|
+
console.log(` ✖ ${dirRel}/${f} (faltante)`)
|
|
661
|
+
missing++
|
|
662
|
+
}
|
|
663
|
+
for (const f of orphanFiles) {
|
|
664
|
+
console.log(` ⚠ ${dirRel}/${f} (huerfano)`)
|
|
665
|
+
orphans++
|
|
666
|
+
}
|
|
667
|
+
if (missingFiles.length === 0 && orphanFiles.length === 0) {
|
|
668
|
+
console.log(` ✔ ${dirRel} (${actual.length} archivos)`)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
console.log(`ancleto: check -> ${missing} faltantes, ${orphans} huerfanos`)
|
|
674
|
+
process.exit(missing > 0 ? 1 : 0)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function doctorCommand() {
|
|
678
|
+
let fatal = false
|
|
679
|
+
|
|
680
|
+
const nodeVersion = process.versions.node
|
|
681
|
+
const nodeMajor = Number(nodeVersion.split('.')[0])
|
|
682
|
+
if (nodeMajor >= 24) {
|
|
683
|
+
console.log(` ✔ Node.js ${nodeVersion} (>=24)`)
|
|
684
|
+
} else {
|
|
685
|
+
console.log(` ✖ Node.js ${nodeVersion} (requiere >=24 para node:sqlite)`)
|
|
686
|
+
fatal = true
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
await import('node:sqlite')
|
|
691
|
+
console.log(' ✔ node:sqlite importable')
|
|
692
|
+
} catch (err) {
|
|
693
|
+
console.log(` ✖ node:sqlite no importable: ${err.message}`)
|
|
694
|
+
fatal = true
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const configDir = globalConfigDir()
|
|
698
|
+
let cfgFile = null
|
|
699
|
+
for (const c of ['opencode.json', 'opencode.jsonc']) {
|
|
700
|
+
const p = join(configDir, c)
|
|
701
|
+
if (await exists(p)) {
|
|
702
|
+
cfgFile = p
|
|
703
|
+
break
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (!cfgFile) {
|
|
707
|
+
console.log(' ⚠ opencode.json no encontrado (config MCP)')
|
|
708
|
+
} else {
|
|
709
|
+
try {
|
|
710
|
+
JSON.parse((await readFile(cfgFile, 'utf8')).replace(/^\uFEFF/, ''))
|
|
711
|
+
console.log(` ✔ ${basename(cfgFile)} valido`)
|
|
712
|
+
} catch {
|
|
713
|
+
console.log(` ✖ ${basename(cfgFile)} JSON invalido`)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
process.exit(fatal ? 1 : 0)
|
|
718
|
+
}
|
|
719
|
+
|
|
559
720
|
const [cmd, ...rest] = process.argv.slice(2)
|
|
560
721
|
|
|
561
722
|
switch (cmd) {
|
|
@@ -572,6 +733,12 @@ switch (cmd) {
|
|
|
572
733
|
case 'memory':
|
|
573
734
|
await memoryCmd(rest)
|
|
574
735
|
break
|
|
736
|
+
case 'check':
|
|
737
|
+
await checkCommand()
|
|
738
|
+
break
|
|
739
|
+
case 'doctor':
|
|
740
|
+
await doctorCommand()
|
|
741
|
+
break
|
|
575
742
|
case '--version':
|
|
576
743
|
case '-v':
|
|
577
744
|
console.log(`ancleto ${await packageVersion()}`)
|
package/templates/AGENTS.md
CHANGED
|
@@ -60,3 +60,7 @@ conflicto, prevalece el diseño vigente.
|
|
|
60
60
|
|
|
61
61
|
- `ancleto`: Descubrimiento técnico e inicialización.
|
|
62
62
|
- `openspec`: Gestión del ciclo de vida del cambio (proposal, specs, design, tasks, archive).
|
|
63
|
+
|
|
64
|
+
<!-- LOCKED: test-block -->
|
|
65
|
+
Contexto gestionado por @ancleto/spec — no editar: se re-aplica en cada actualizacion.
|
|
66
|
+
<!-- /LOCKED: test-block -->
|
package/templates/PRODUCT.md
CHANGED