@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
5
5
  "type": "module",
6
6
  "bin": {
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
- y reconstruye el indice FTS5 con --rebuild
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
- if (await exists(target)) continue
230
- await writeFile(target, await readFile(join(ROOT, 'templates', t)))
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()}`)
@@ -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 -->
@@ -91,3 +91,7 @@ openspec/
91
91
  - `npm test` → Run tests
92
92
  - `npm run build` → Production build
93
93
  - `npm run lint` → Linter
94
+
95
+ <!-- LOCKED: test-block -->
96
+ Contexto gestionado por @ancleto/spec — no editar: se re-aplica en cada actualizacion.
97
+ <!-- /LOCKED: test-block -->