@ancleto/spec 0.4.0 → 0.4.2
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 +158 -3
- package/templates/AGENTS.md +4 -0
- package/templates/PRODUCT.md +4 -0
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -32,7 +32,9 @@ Uso:
|
|
|
32
32
|
ancleto memory context [--scope X] [--out file]
|
|
33
33
|
Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
|
|
34
34
|
ancleto memory doctor [--rebuild] Diagnostica .ancleto/memory.db (integridad, FTS5, unicidad)
|
|
35
|
-
|
|
35
|
+
y reconstruye el indice FTS5 con --rebuild
|
|
36
|
+
ancleto check Verifica integridad de archivos instalados vs manifiesto
|
|
37
|
+
ancleto doctor Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
36
38
|
ancleto --help Esta ayuda
|
|
37
39
|
ancleto --version Version del paquete
|
|
38
40
|
`
|
|
@@ -221,13 +223,65 @@ async function copyAssets(dest) {
|
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
const DEFAULT_OPENSPEC_CONFIG = `# OpenSpec project configuration
|
|
227
|
+
# Generado por @ancleto/spec (G5) — editalo libremente, no se sobrescribe en reinstalaciones.
|
|
228
|
+
schema: spec-driven-development
|
|
229
|
+
`
|
|
230
|
+
|
|
231
|
+
async function scaffoldOpenSpec(projectDir) {
|
|
232
|
+
const changesDir = join(projectDir, 'openspec', 'changes')
|
|
233
|
+
await mkdir(changesDir, { recursive: true })
|
|
234
|
+
const configPath = join(projectDir, 'openspec', 'config.yaml')
|
|
235
|
+
if (!(await exists(configPath))) {
|
|
236
|
+
await writeFile(configPath, DEFAULT_OPENSPEC_CONFIG)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractLockedBlocks(content) {
|
|
241
|
+
const blocks = new Map()
|
|
242
|
+
const re = /<!--\s*LOCKED:\s*([\w-]+)\s*-->([\s\S]*?)<!--\s*\/LOCKED:\s*\1\s*-->/g
|
|
243
|
+
let m
|
|
244
|
+
while ((m = re.exec(content)) !== null) {
|
|
245
|
+
blocks.set(m[1], m[0])
|
|
246
|
+
}
|
|
247
|
+
return blocks
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function replaceLockedBlock(local, name, sourceBlock) {
|
|
251
|
+
const re = new RegExp(`<!--\\s*LOCKED:\\s*${escapeRe(name)}\\s*-->[\\s\\S]*?<!--\\s*\\/LOCKED:\\s*${escapeRe(name)}\\s*-->`)
|
|
252
|
+
if (!re.test(local)) return null
|
|
253
|
+
return local.replace(re, sourceBlock)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function mergeLocked(source, local, filename) {
|
|
257
|
+
const blocks = extractLockedBlocks(source)
|
|
258
|
+
let result = local
|
|
259
|
+
for (const [name, sourceBlock] of blocks) {
|
|
260
|
+
const replaced = replaceLockedBlock(result, name, sourceBlock)
|
|
261
|
+
if (replaced === null) {
|
|
262
|
+
console.warn(`ancleto: no se pudo actualizar el bloque LOCKED "${name}" en ${filename} (tags ausentes o mal formados)`)
|
|
263
|
+
} else {
|
|
264
|
+
result = replaced
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result
|
|
268
|
+
}
|
|
269
|
+
|
|
224
270
|
async function copyTemplates(projectDir) {
|
|
225
271
|
const dest = join(projectDir, '.opencode')
|
|
226
272
|
await copyAssets(dest)
|
|
227
273
|
for (const t of TEMPLATES) {
|
|
228
274
|
const target = join(projectDir, t)
|
|
229
|
-
|
|
230
|
-
|
|
275
|
+
const source = await readFile(join(ROOT, 'templates', t), 'utf8')
|
|
276
|
+
if (!(await exists(target))) {
|
|
277
|
+
await writeFile(target, source)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
const local = await readFile(target, 'utf8')
|
|
281
|
+
const merged = mergeLocked(source, local, t)
|
|
282
|
+
if (merged !== local) {
|
|
283
|
+
await writeFile(target, merged)
|
|
284
|
+
}
|
|
231
285
|
}
|
|
232
286
|
}
|
|
233
287
|
|
|
@@ -256,6 +310,7 @@ async function install(args) {
|
|
|
256
310
|
|
|
257
311
|
if (project) {
|
|
258
312
|
await copyTemplates(resolve(project))
|
|
313
|
+
await scaffoldOpenSpec(resolve(project))
|
|
259
314
|
await writeManifest(resolve(project), {
|
|
260
315
|
installedPaths: {
|
|
261
316
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
@@ -296,6 +351,7 @@ async function initProject(args) {
|
|
|
296
351
|
if (withAzure) azure.enabled = true
|
|
297
352
|
const discovery = existing?.discovery ?? { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
298
353
|
const manifest = await writeManifest(projectDir, { azure, discovery })
|
|
354
|
+
await scaffoldOpenSpec(projectDir)
|
|
299
355
|
console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'}`)
|
|
300
356
|
}
|
|
301
357
|
|
|
@@ -556,6 +612,99 @@ async function memoryCmd(args) {
|
|
|
556
612
|
process.exit(1)
|
|
557
613
|
}
|
|
558
614
|
|
|
615
|
+
async function checkCommand() {
|
|
616
|
+
const cwd = process.cwd()
|
|
617
|
+
const rc = await readAncletorc(cwd)
|
|
618
|
+
if (!rc || !rc.installedPaths) {
|
|
619
|
+
console.error('ancleto: no hay .ancletorc con installedPaths (corre ancleto init y ancleto install --project)')
|
|
620
|
+
process.exit(1)
|
|
621
|
+
}
|
|
622
|
+
const ip = rc.installedPaths
|
|
623
|
+
let missing = 0
|
|
624
|
+
let orphans = 0
|
|
625
|
+
|
|
626
|
+
for (const t of ip.templates || []) {
|
|
627
|
+
if (await exists(join(cwd, t))) {
|
|
628
|
+
console.log(` ✔ ${t}`)
|
|
629
|
+
} else {
|
|
630
|
+
console.log(` ✖ ${t} (faltante)`)
|
|
631
|
+
missing++
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
for (const cat of ['agents', 'commands', 'skills']) {
|
|
636
|
+
for (const dirRel of ip[cat] || []) {
|
|
637
|
+
const destDir = join(cwd, dirRel)
|
|
638
|
+
if (!(await exists(destDir))) {
|
|
639
|
+
console.log(` ✖ ${dirRel} (directorio faltante)`)
|
|
640
|
+
missing++
|
|
641
|
+
continue
|
|
642
|
+
}
|
|
643
|
+
const expected = (await readdir(join(ROOT, cat))).sort()
|
|
644
|
+
const actual = (await readdir(destDir)).sort()
|
|
645
|
+
const missingFiles = expected.filter((f) => !actual.includes(f))
|
|
646
|
+
const orphanFiles = actual.filter((f) => !expected.includes(f))
|
|
647
|
+
for (const f of missingFiles) {
|
|
648
|
+
console.log(` ✖ ${dirRel}/${f} (faltante)`)
|
|
649
|
+
missing++
|
|
650
|
+
}
|
|
651
|
+
for (const f of orphanFiles) {
|
|
652
|
+
console.log(` ⚠ ${dirRel}/${f} (huerfano)`)
|
|
653
|
+
orphans++
|
|
654
|
+
}
|
|
655
|
+
if (missingFiles.length === 0 && orphanFiles.length === 0) {
|
|
656
|
+
console.log(` ✔ ${dirRel} (${actual.length} archivos)`)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
console.log(`ancleto: check -> ${missing} faltantes, ${orphans} huerfanos`)
|
|
662
|
+
process.exit(missing > 0 ? 1 : 0)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function doctorCommand() {
|
|
666
|
+
let fatal = false
|
|
667
|
+
|
|
668
|
+
const nodeVersion = process.versions.node
|
|
669
|
+
const nodeMajor = Number(nodeVersion.split('.')[0])
|
|
670
|
+
if (nodeMajor >= 24) {
|
|
671
|
+
console.log(` ✔ Node.js ${nodeVersion} (>=24)`)
|
|
672
|
+
} else {
|
|
673
|
+
console.log(` ✖ Node.js ${nodeVersion} (requiere >=24 para node:sqlite)`)
|
|
674
|
+
fatal = true
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
await import('node:sqlite')
|
|
679
|
+
console.log(' ✔ node:sqlite importable')
|
|
680
|
+
} catch (err) {
|
|
681
|
+
console.log(` ✖ node:sqlite no importable: ${err.message}`)
|
|
682
|
+
fatal = true
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const configDir = globalConfigDir()
|
|
686
|
+
let cfgFile = null
|
|
687
|
+
for (const c of ['opencode.json', 'opencode.jsonc']) {
|
|
688
|
+
const p = join(configDir, c)
|
|
689
|
+
if (await exists(p)) {
|
|
690
|
+
cfgFile = p
|
|
691
|
+
break
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (!cfgFile) {
|
|
695
|
+
console.log(' ⚠ opencode.json no encontrado (config MCP)')
|
|
696
|
+
} else {
|
|
697
|
+
try {
|
|
698
|
+
JSON.parse((await readFile(cfgFile, 'utf8')).replace(/^\uFEFF/, ''))
|
|
699
|
+
console.log(` ✔ ${basename(cfgFile)} valido`)
|
|
700
|
+
} catch {
|
|
701
|
+
console.log(` ✖ ${basename(cfgFile)} JSON invalido`)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
process.exit(fatal ? 1 : 0)
|
|
706
|
+
}
|
|
707
|
+
|
|
559
708
|
const [cmd, ...rest] = process.argv.slice(2)
|
|
560
709
|
|
|
561
710
|
switch (cmd) {
|
|
@@ -572,6 +721,12 @@ switch (cmd) {
|
|
|
572
721
|
case 'memory':
|
|
573
722
|
await memoryCmd(rest)
|
|
574
723
|
break
|
|
724
|
+
case 'check':
|
|
725
|
+
await checkCommand()
|
|
726
|
+
break
|
|
727
|
+
case 'doctor':
|
|
728
|
+
await doctorCommand()
|
|
729
|
+
break
|
|
575
730
|
case '--version':
|
|
576
731
|
case '-v':
|
|
577
732
|
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