@ancleto/spec 0.4.6 → 0.5.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.6",
3
+ "version": "0.5.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
@@ -8,6 +8,8 @@ import { fileURLToPath } from 'node:url'
8
8
  import { homedir, tmpdir } from 'node:os'
9
9
  import { createMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.js'
10
10
  import { memoryDoctor } from '../core/memory/doctor.js'
11
+ import { writeDiscoveryMap } from '../core/discovery.js'
12
+ import { readProjectTier, buildRepomixArgs, tierTokenBudget } from '../core/repomix-tier.js'
11
13
 
12
14
  const __dirname = dirname(fileURLToPath(import.meta.url))
13
15
  const ROOT = join(__dirname, '..', '..')
@@ -26,6 +28,7 @@ Uso:
26
28
  ancleto install --tier <nivel> normal | minimo | gratis (pregunta en la 1ra config)
27
29
  ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (pregunta si no esta guardado)
28
30
  ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
31
+ ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
29
32
  ancleto init [--with-azure] [--agent <nombre>]
30
33
  Crea .ancletorc en el repositorio actual
31
34
  (Azure desactivado por defecto, agente: opencode)
@@ -417,6 +420,31 @@ async function install(args) {
417
420
  if (azureMcp) console.log(AZURE_MCP_NOTICE)
418
421
  }
419
422
 
423
+ async function upgradeCmd(args) {
424
+ const projectDir = process.cwd()
425
+ const rc = await readAncletorc(projectDir)
426
+ if (!rc) {
427
+ console.error("Error: No se encontro .ancletorc. Ejecuta 'ancleto init' primero.")
428
+ process.exit(1)
429
+ }
430
+ const agent = await resolveAgent(args, rc.agent)
431
+ await copyTemplates(projectDir)
432
+ const agentSkillsDir = await installAgentSkills(projectDir, agent)
433
+ const manifest = await writeManifest(projectDir, {
434
+ agent,
435
+ installedPaths: {
436
+ templates: ['AGENTS.md', 'PRODUCT.md'],
437
+ agents: ['.opencode/agents'],
438
+ commands: ['.opencode/commands'],
439
+ skills: [agentSkillsDir]
440
+ }
441
+ })
442
+ console.log(`ancleto: upgrade completo (v${manifest.version}, agente: ${agent})`)
443
+ console.log(' ✔ templates re-aplicados (bloques LOCKED actualizados, EXTENSIBLE intacto)')
444
+ console.log(` ✔ skills actualizadas en ${agentSkillsDir} (${OPENSPEC_SKILLS.length} skills)`)
445
+ console.log(' ✔ manifiesto .ancletorc actualizado')
446
+ }
447
+
420
448
  async function initProject(args) {
421
449
  const withAzure = args.includes('--with-azure')
422
450
  const projectDir = process.cwd()
@@ -571,16 +599,11 @@ async function checkDiscovery() {
571
599
 
572
600
  async function runRepomix(flags, tmpFile) {
573
601
  const local = await resolveBin('repomix')
602
+ const tier = readProjectTier(process.cwd())
603
+ const { exclude } = await loadDiscoveryConfig()
574
604
  const args = []
575
605
  if (!local) args.push('-y', 'repomix@1.18.0')
576
- args.push('--output', tmpFile)
577
- const inc = flagValue(flags, '--include')
578
- if (inc) args.push('--include', inc)
579
- const extraIgnore = flagValue(flags, '--ignore')
580
- const { exclude } = await loadDiscoveryConfig()
581
- const ignore = [...exclude, ...(extraIgnore ? extraIgnore.split(',') : [])].filter(Boolean)
582
- if (ignore.length) args.push('--ignore', ignore.join(','))
583
- if (flags.includes('--compress')) args.push('--compress')
606
+ args.push('--output', tmpFile, ...buildRepomixArgs(flags, tier, exclude))
584
607
  const cmd = local || 'npx'
585
608
  const r = spawnSync(cmd, args, { encoding: 'utf8', cwd: process.cwd(), shell: true })
586
609
  if (r.error) {
@@ -602,8 +625,9 @@ async function packDiscovery(flags) {
602
625
  let content = ''
603
626
  try { content = await readFile(tmpFile, 'utf8') } catch {}
604
627
  const tokens = Math.round(content.length / 4)
605
- const budget = flagValue(flags, '--token-budget')
606
- if (budget && tokens > Number(budget)) {
628
+ const explicit = flagValue(flags, '--token-budget')
629
+ const budget = explicit ? Number(explicit) : tierTokenBudget(readProjectTier(process.cwd()))
630
+ if (budget && tokens > budget) {
607
631
  console.error(`ancleto: el pack supera el token-budget (${tokens} > ${budget})`)
608
632
  process.exit(1)
609
633
  }
@@ -623,6 +647,7 @@ async function packDiscovery(flags) {
623
647
  }
624
648
 
625
649
  async function discovery(flags) {
650
+ try { writeDiscoveryMap(process.cwd()) } catch {}
626
651
  if (flags.includes('--check')) {
627
652
  await checkDiscovery()
628
653
  return
@@ -788,6 +813,9 @@ switch (cmd) {
788
813
  case 'update':
789
814
  await install(rest)
790
815
  break
816
+ case 'upgrade':
817
+ await upgradeCmd(rest)
818
+ break
791
819
  case 'init':
792
820
  await initProject(rest)
793
821
  break
@@ -0,0 +1,62 @@
1
+ import { readdirSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export const DISCOVERY_MAP_FILE = '.discovery-map.json'
5
+ export const TOPOLOGY_IGNORED_DIRS = ['node_modules', '.git', '.ancleto', 'dist', 'build', 'coverage']
6
+
7
+ function countFilesRecursive(dir, ignored) {
8
+ let n = 0
9
+ let entries
10
+ try {
11
+ entries = readdirSync(dir, { withFileTypes: true })
12
+ } catch {
13
+ return 0
14
+ }
15
+ for (const e of entries) {
16
+ const p = join(dir, e.name)
17
+ if (e.isDirectory()) {
18
+ if (ignored.has(e.name)) continue
19
+ n += countFilesRecursive(p, ignored)
20
+ } else if (e.isFile()) {
21
+ n += 1
22
+ }
23
+ }
24
+ return n
25
+ }
26
+
27
+ export function buildTopologyMap(rootDir) {
28
+ const ignored = new Set(TOPOLOGY_IGNORED_DIRS)
29
+ const tree_summary = {}
30
+ const root_files = []
31
+ let total = 0
32
+ let entries
33
+ try {
34
+ entries = readdirSync(rootDir, { withFileTypes: true })
35
+ } catch {
36
+ entries = []
37
+ }
38
+ for (const e of entries) {
39
+ if (ignored.has(e.name)) continue
40
+ if (e.isDirectory()) {
41
+ const n = countFilesRecursive(join(rootDir, e.name), ignored)
42
+ tree_summary[e.name] = n
43
+ total += n
44
+ } else if (e.isFile()) {
45
+ root_files.push(e.name)
46
+ total += 1
47
+ }
48
+ }
49
+ root_files.sort()
50
+ return {
51
+ last_updated: new Date().toISOString(),
52
+ total_files: total,
53
+ tree_summary,
54
+ root_files
55
+ }
56
+ }
57
+
58
+ export function writeDiscoveryMap(rootDir) {
59
+ const map = buildTopologyMap(rootDir)
60
+ writeFileSync(join(rootDir, DISCOVERY_MAP_FILE), JSON.stringify(map, null, 2) + '\n')
61
+ return map
62
+ }
@@ -0,0 +1,38 @@
1
+ import { readFileSync, existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export const TIER_PACK_CONFIG = {
5
+ normal: { extraIgnore: [], compress: false, tokenBudget: null },
6
+ minimo: { extraIgnore: ['test/**', 'docs/**', '**/*.md'], compress: true, tokenBudget: null },
7
+ gratis: { extraIgnore: ['test/**', 'docs/**', '**/*.md'], compress: true, tokenBudget: 50000 }
8
+ }
9
+
10
+ export function readProjectTier(cwd) {
11
+ for (const p of [join(cwd, '.ancleto-tier'), join(cwd, '.opencode', '.ancleto-tier')]) {
12
+ if (!existsSync(p)) continue
13
+ const v = readFileSync(p, 'utf8').trim()
14
+ if (v) return TIER_PACK_CONFIG[v] ? v : 'gratis'
15
+ }
16
+ return 'gratis'
17
+ }
18
+
19
+ export function tierTokenBudget(tier) {
20
+ return (TIER_PACK_CONFIG[tier] || TIER_PACK_CONFIG.gratis).tokenBudget
21
+ }
22
+
23
+ export function buildRepomixArgs(flags, tier, exclude = []) {
24
+ const cfg = TIER_PACK_CONFIG[tier] || TIER_PACK_CONFIG.gratis
25
+ const args = []
26
+ const inc = flagValue(flags, '--include')
27
+ if (inc) args.push('--include', inc)
28
+ const extraIgnore = flagValue(flags, '--ignore')
29
+ const ignore = [...exclude, ...cfg.extraIgnore, ...(extraIgnore ? extraIgnore.split(',') : [])].filter(Boolean)
30
+ if (ignore.length) args.push('--ignore', ignore.join(','))
31
+ if (cfg.compress || flags.includes('--compress')) args.push('--compress')
32
+ return args
33
+ }
34
+
35
+ function flagValue(args, flag) {
36
+ const i = args.indexOf(flag)
37
+ return i >= 0 && args[i + 1] ? args[i + 1] : null
38
+ }