@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
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
@@ -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
- y reconstruye el indice FTS5 con --rebuild
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
- if (await exists(target)) continue
230
- await writeFile(target, await readFile(join(ROOT, 'templates', t)))
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
- const mcpMap = withMcp ? buildDefaultMcp() : {}
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()}`)
@@ -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 -->