@dsalazardev/ai-hub-cli 0.2.0 → 0.3.0
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/README.md +63 -4
- package/dist/backup/snapshots.js +12 -0
- package/dist/catalog.d.ts +8 -1
- package/dist/catalog.js +38 -9
- package/dist/cli/args.js +1 -0
- package/dist/cli/doctor.js +20 -11
- package/dist/cli/maintenance.js +21 -39
- package/dist/cli/memory.js +4 -0
- package/dist/cli/onboarding.d.ts +13 -0
- package/dist/cli/onboarding.js +128 -14
- package/dist/cli/run.d.ts +3 -1
- package/dist/cli/run.js +5 -4
- package/dist/cli/types.d.ts +3 -0
- package/dist/engine/engine.d.ts +19 -1
- package/dist/engine/engine.js +52 -0
- package/dist/engine/gates.d.ts +16 -0
- package/dist/engine/gates.js +102 -1
- package/dist/harness/memory/ledger.d.ts +6 -0
- package/dist/harness/memory/ledger.js +44 -2
- package/dist/manifest/manifest.d.ts +3 -1
- package/dist/manifest/manifest.js +16 -10
- package/dist/presets/materialize.d.ts +5 -0
- package/dist/presets/materialize.js +30 -0
- package/dist/speckit/binding.d.ts +15 -0
- package/dist/speckit/binding.js +66 -0
- package/dist/speckit/commands.d.ts +46 -0
- package/dist/speckit/commands.js +131 -0
- package/dist/speckit/doctor.d.ts +14 -0
- package/dist/speckit/doctor.js +103 -0
- package/dist/speckit/runner.d.ts +19 -0
- package/dist/speckit/runner.js +22 -0
- package/dist/speckit/seal.d.ts +12 -0
- package/dist/speckit/seal.js +47 -0
- package/dist/speckit/settings.d.ts +36 -0
- package/dist/speckit/settings.js +75 -0
- package/dist/speckit/state.d.ts +59 -0
- package/dist/speckit/state.js +279 -0
- package/dist/speckit/version.d.ts +8 -0
- package/dist/speckit/version.js +53 -0
- package/dist/sync/config.js +3 -2
- package/dist/sync/render.js +30 -0
- package/dist/sync/sync.d.ts +4 -0
- package/dist/sync/sync.js +48 -5
- package/dist/sync/targets.d.ts +13 -0
- package/dist/sync/targets.js +5 -0
- package/engine/engine.yaml +65 -31
- package/manifest.json +20 -48
- package/package.json +1 -1
- package/schema/engine.schema.json +22 -3
- package/schema/manifest.schema.json +10 -0
- package/skills/shared/analyze/SKILL.md +2 -1
- package/skills/shared/clarify/SKILL.md +2 -1
- package/skills/shared/implement/SKILL.md +2 -1
- package/skills/shared/plan/SKILL.md +2 -1
- package/skills/shared/review/SKILL.md +2 -1
- package/skills/shared/sdd/SKILL.md +2 -1
- package/skills/shared/sdd-init/SKILL.md +2 -1
- package/skills/shared/ship/SKILL.md +2 -1
- package/skills/shared/specify/SKILL.md +2 -1
- package/skills/shared/tasks/SKILL.md +2 -1
- package/skills/shared/verify/SKILL.md +2 -1
- package/skills/shared/speckit-analyze/SKILL.md +0 -35
- package/skills/shared/speckit-assess-decide/SKILL.md +0 -36
- package/skills/shared/speckit-assess-define/SKILL.md +0 -36
- package/skills/shared/speckit-assess-intake/SKILL.md +0 -38
- package/skills/shared/speckit-assess-research/SKILL.md +0 -36
- package/skills/shared/speckit-assess-shape/SKILL.md +0 -36
- package/skills/shared/speckit-bug-assess/SKILL.md +0 -38
- package/skills/shared/speckit-bug-fix/SKILL.md +0 -37
- package/skills/shared/speckit-bug-test/SKILL.md +0 -37
- package/skills/shared/speckit-checklist/SKILL.md +0 -35
- package/skills/shared/speckit-clarify/SKILL.md +0 -32
- package/skills/shared/speckit-constitution/SKILL.md +0 -35
- package/skills/shared/speckit-converge/SKILL.md +0 -36
- package/skills/shared/speckit-implement/SKILL.md +0 -32
- package/skills/shared/speckit-plan/SKILL.md +0 -37
- package/skills/shared/speckit-specify/SKILL.md +0 -36
- package/skills/shared/speckit-tasks/SKILL.md +0 -34
|
@@ -114,6 +114,44 @@ export function queryLedger(events, filter) {
|
|
|
114
114
|
return events.filter((event) => (filter.changeId === undefined || event.changeId === filter.changeId) &&
|
|
115
115
|
(filter.sessionId === undefined || event.sessionId === filter.sessionId));
|
|
116
116
|
}
|
|
117
|
+
const FEATURE_PREFIX = 'specs/';
|
|
118
|
+
/**
|
|
119
|
+
* Feature de Spec Kit: la nombra `.planning/state.json` (`activeChange: specs/<feature>`) o,
|
|
120
|
+
* si no, el puntero local `.specify/feature.json`. Solo se referencia la ruta; nunca se copia
|
|
121
|
+
* el contenido de la feature a `.planning/`.
|
|
122
|
+
*/
|
|
123
|
+
async function activeFeature(root, state) {
|
|
124
|
+
if (state?.activeChange?.startsWith(FEATURE_PREFIX)) {
|
|
125
|
+
return { dir: state.activeChange.replace(/\/+$/, ''), source: 'state' };
|
|
126
|
+
}
|
|
127
|
+
if (state?.activeChange) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const raw = await fs.readFile(path.join(root, '.specify', 'feature.json'), 'utf8').catch(() => null);
|
|
131
|
+
if (raw === null) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
return typeof parsed.feature_directory === 'string' && parsed.feature_directory.startsWith(FEATURE_PREFIX)
|
|
137
|
+
? { dir: parsed.feature_directory.replace(/\/+$/, ''), source: 'local' }
|
|
138
|
+
: null;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function countTasks(root, dir) {
|
|
145
|
+
const text = await fs.readFile(path.join(root, ...dir.split('/'), 'tasks.md'), 'utf8').catch(() => null);
|
|
146
|
+
if (text === null) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const lines = text.split(/\r?\n/);
|
|
150
|
+
return {
|
|
151
|
+
total: lines.filter((line) => /^\s*-\s*\[[ xX]\]/.test(line)).length,
|
|
152
|
+
done: lines.filter((line) => /^\s*-\s*\[[xX]\]/.test(line)).length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
117
155
|
export async function resumeLedger(root) {
|
|
118
156
|
const events = await readLedger(root, 1);
|
|
119
157
|
const last = events.length > 0 ? events[events.length - 1] : null;
|
|
@@ -126,11 +164,15 @@ export async function resumeLedger(root) {
|
|
|
126
164
|
throw error;
|
|
127
165
|
}
|
|
128
166
|
}
|
|
167
|
+
const feature = await activeFeature(root, state);
|
|
168
|
+
const featureTasks = feature === null ? null : await countTasks(root, feature.dir);
|
|
169
|
+
const tasks = featureTasks ?? state?.tasks ?? { total: 0, done: 0 };
|
|
129
170
|
return {
|
|
130
171
|
last,
|
|
131
|
-
activeChange: state?.activeChange ?? null,
|
|
172
|
+
activeChange: state?.activeChange ?? feature?.dir ?? null,
|
|
132
173
|
phase: state?.phase ?? 'idle',
|
|
133
|
-
tasks
|
|
174
|
+
tasks,
|
|
134
175
|
state,
|
|
176
|
+
feature: feature === null ? null : { ...feature, pending: tasks.total - tasks.done },
|
|
135
177
|
};
|
|
136
178
|
}
|
|
@@ -2,6 +2,7 @@ import { ENGINE_COUNTS, type EngineManifest } from '../engine/engine.js';
|
|
|
2
2
|
import { type ResolvedPreset } from '../presets/resolve.js';
|
|
3
3
|
import type { PresetDangerPattern } from '../presets/schema.js';
|
|
4
4
|
import type { SelectionMode } from '../catalog.js';
|
|
5
|
+
import { type ProjectSpeckit } from '../speckit/settings.js';
|
|
5
6
|
export declare const MANIFEST_FILE = "manifest.yaml";
|
|
6
7
|
export declare const INVENTORY_RELATIVE = "core/manifest.json";
|
|
7
8
|
export { ENGINE_COUNTS };
|
|
@@ -58,6 +59,7 @@ export interface ProjectManifest {
|
|
|
58
59
|
mcp?: {
|
|
59
60
|
source: string;
|
|
60
61
|
};
|
|
62
|
+
speckit?: ProjectSpeckit;
|
|
61
63
|
}
|
|
62
64
|
export declare class ManifestError extends Error {
|
|
63
65
|
readonly errors: string[];
|
|
@@ -94,7 +96,7 @@ export interface ManifestInventory {
|
|
|
94
96
|
id: string;
|
|
95
97
|
lane: string;
|
|
96
98
|
role: string;
|
|
97
|
-
|
|
99
|
+
speckit: string;
|
|
98
100
|
}>;
|
|
99
101
|
labels: string[];
|
|
100
102
|
targets: string[];
|
|
@@ -5,9 +5,10 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { Ajv } from 'ajv';
|
|
7
7
|
import { parse } from 'yaml';
|
|
8
|
-
import { ENGINE_COUNTS,
|
|
8
|
+
import { ENGINE_COUNTS, loadEngine } from '../engine/engine.js';
|
|
9
9
|
import { frontmatterString, splitFrontmatter } from '../frontmatter.js';
|
|
10
10
|
import { resolvePreset } from '../presets/resolve.js';
|
|
11
|
+
import { validateProjectSpeckit } from '../speckit/settings.js';
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schema', 'manifest.schema.json');
|
|
13
14
|
const ENGINE_AGENTS = path.join(__dirname, '..', '..', 'agents');
|
|
@@ -69,6 +70,18 @@ export async function loadManifestContext(root) {
|
|
|
69
70
|
return validateManifestData(await readManifestFile(root), root);
|
|
70
71
|
}
|
|
71
72
|
export async function validateManifestData(data, root = process.cwd()) {
|
|
73
|
+
if (typeof data === 'object' && data !== null && 'speckit' in data) {
|
|
74
|
+
// Los campos del motor se explican antes del schema, que solo diría "additional property".
|
|
75
|
+
const record = data;
|
|
76
|
+
const targets = Array.isArray(record.targets)
|
|
77
|
+
? record.targets.filter((item) => typeof item === 'string')
|
|
78
|
+
: [];
|
|
79
|
+
const block = record.speckit;
|
|
80
|
+
const speckitErrors = validateProjectSpeckit(block, targets, await loadEngine());
|
|
81
|
+
if (speckitErrors.length > 0) {
|
|
82
|
+
throw new ManifestError(speckitErrors);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
72
85
|
const validate = await validator();
|
|
73
86
|
if (!validate(data)) {
|
|
74
87
|
throw new ManifestError((validate.errors ?? []).map((error) => {
|
|
@@ -105,13 +118,6 @@ export async function validateManifestData(data, root = process.cwd()) {
|
|
|
105
118
|
errors.push(`skills.publish "${skill}" no existe en ${manifest.skills.source}`);
|
|
106
119
|
}
|
|
107
120
|
}
|
|
108
|
-
for (const skill of new Set(engine.commands.map((command) => command.skill))) {
|
|
109
|
-
const file = path.join(ENGINE_SKILLS_DIR, skill, 'SKILL.md');
|
|
110
|
-
skillFiles.add(file);
|
|
111
|
-
if (!(await exists(file))) {
|
|
112
|
-
errors.push(`skill del motor "${skill}" no existe en ${ENGINE_SKILLS_DIR}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
121
|
for (const file of skillFiles) {
|
|
116
122
|
if (await exists(file)) {
|
|
117
123
|
await validateCatalogFile(file, errors);
|
|
@@ -160,9 +166,9 @@ export function buildInventory(manifest, engine) {
|
|
|
160
166
|
const roles = engine.roles.map((role) => role.id).sort();
|
|
161
167
|
const commands = [...engine.commands]
|
|
162
168
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
163
|
-
.map((command) => ({ id: command.id, lane: command.lane, role: command.role,
|
|
169
|
+
.map((command) => ({ id: command.id, lane: command.lane, role: command.role, speckit: command.speckit }));
|
|
164
170
|
const labels = [...engine.labels].sort();
|
|
165
|
-
const skills = [...new Set(
|
|
171
|
+
const skills = [...new Set(manifest.skills.publish)].sort();
|
|
166
172
|
const agents = [...manifest.agents.publish].sort();
|
|
167
173
|
const rules = [...manifest.rules.publish].sort();
|
|
168
174
|
return {
|
|
@@ -24,3 +24,8 @@ export declare function materializeAgent(root: string, id: string, sourceFile?:
|
|
|
24
24
|
export declare function materializeUniversalRules(root: string, preset: ResolvedPreset, selectedRules?: string[]): Promise<MaterializeResult>;
|
|
25
25
|
/** Copia skills y agentes seleccionados del catálogo. */
|
|
26
26
|
export declare function materializeCatalog(root: string, skillIds: string[], agentIds: string[]): Promise<MaterializeResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Borra un activo materializado solo si lo escribió ai-hub (registro de scaffold o marcador);
|
|
29
|
+
* un archivo del proyecto se conserva.
|
|
30
|
+
*/
|
|
31
|
+
export declare function removeOwned(root: string, relative: string, directory: boolean): Promise<void>;
|
|
@@ -190,3 +190,33 @@ export async function materializeCatalog(root, skillIds, agentIds) {
|
|
|
190
190
|
await writeScaffold(root, record);
|
|
191
191
|
return result;
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Borra un activo materializado solo si lo escribió ai-hub (registro de scaffold o marcador);
|
|
195
|
+
* un archivo del proyecto se conserva.
|
|
196
|
+
*/
|
|
197
|
+
export async function removeOwned(root, relative, directory) {
|
|
198
|
+
const absolute = path.join(root, relative);
|
|
199
|
+
if (!(await exists(absolute))) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const record = await readScaffold(root);
|
|
203
|
+
const relativePosix = relative.split(path.sep).join('/');
|
|
204
|
+
if (directory) {
|
|
205
|
+
const files = record.files;
|
|
206
|
+
const prefix = `${relativePosix}/`;
|
|
207
|
+
const owned = Object.entries(files).filter(([name]) => name.startsWith(prefix));
|
|
208
|
+
if (owned.length === 0) {
|
|
209
|
+
const marker = await fs.readFile(path.join(absolute, 'SKILL.md'), 'utf8').catch(() => '');
|
|
210
|
+
if (!marker.includes('GENERADO por ai-hub')) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await fs.rm(absolute, { recursive: true, force: true });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const text = await fs.readFile(absolute, 'utf8').catch(() => null);
|
|
218
|
+
const owned = record.files[relativePosix] !== undefined;
|
|
219
|
+
if (text === null || owned || text.includes('GENERADO por ai-hub')) {
|
|
220
|
+
await fs.rm(absolute, { force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EngineManifest } from '../engine/engine.js';
|
|
2
|
+
import { type ToolId } from '../sync/targets.js';
|
|
3
|
+
/**
|
|
4
|
+
* Regla generada que enlaza Spec Kit con AI-Hub: roles por comando, invocación en la notación
|
|
5
|
+
* de cada destino, efecto de cada estado de salida sobre las etiquetas, rutas canónicas y sello.
|
|
6
|
+
* Se llama `ai-hub-speckit` y no `speckit-binding` porque en Pi el directorio de reglas
|
|
7
|
+
* (`.pi/prompts`) es también el de comandos de Spec Kit, donde `speckit-*` está reservado.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BINDING_RULE_ID = "ai-hub-speckit";
|
|
10
|
+
export declare const BINDING_SOURCE = "@engine/engine/engine.yaml";
|
|
11
|
+
export declare const BINDING_MARKER_SOURCE = "engine/engine.yaml";
|
|
12
|
+
/** Estados de salida en el orden en que aparecen en los comandos. */
|
|
13
|
+
export declare function exitStates(engine: EngineManifest): string[];
|
|
14
|
+
/** Cuerpo de la regla para un destino. Copilot no tiene Spec Kit y su copia no invoca comandos. */
|
|
15
|
+
export declare function bindingRule(engine: EngineManifest, tool: ToolId): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { TARGETS } from '../sync/targets.js';
|
|
2
|
+
import { invocationFor } from './commands.js';
|
|
3
|
+
/**
|
|
4
|
+
* Regla generada que enlaza Spec Kit con AI-Hub: roles por comando, invocación en la notación
|
|
5
|
+
* de cada destino, efecto de cada estado de salida sobre las etiquetas, rutas canónicas y sello.
|
|
6
|
+
* Se llama `ai-hub-speckit` y no `speckit-binding` porque en Pi el directorio de reglas
|
|
7
|
+
* (`.pi/prompts`) es también el de comandos de Spec Kit, donde `speckit-*` está reservado.
|
|
8
|
+
*/
|
|
9
|
+
export const BINDING_RULE_ID = 'ai-hub-speckit';
|
|
10
|
+
export const BINDING_SOURCE = '@engine/engine/engine.yaml';
|
|
11
|
+
export const BINDING_MARKER_SOURCE = 'engine/engine.yaml';
|
|
12
|
+
/** Estados de salida en el orden en que aparecen en los comandos. */
|
|
13
|
+
export function exitStates(engine) {
|
|
14
|
+
const states = [];
|
|
15
|
+
for (const command of engine.commands) {
|
|
16
|
+
for (const state of command.exit) {
|
|
17
|
+
if (!states.includes(state)) {
|
|
18
|
+
states.push(state);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return states;
|
|
23
|
+
}
|
|
24
|
+
function stateTable(engine) {
|
|
25
|
+
return [
|
|
26
|
+
'## Estados de salida y etiquetas',
|
|
27
|
+
'',
|
|
28
|
+
'| Estado | Efecto sobre la etiqueta |',
|
|
29
|
+
'|---|---|',
|
|
30
|
+
...exitStates(engine).map((state) => `| \`${state}\` | ${engine.exit_effects[state]} |`),
|
|
31
|
+
'',
|
|
32
|
+
'Ningún agente mueve una etiqueta. `hermes-control` aplica el efecto; ante `KILL` (assess-decide) o',
|
|
33
|
+
'`INVALID` (bug-assess) solo **propone** `ai::done` y un humano autorizado lo confirma.',
|
|
34
|
+
`Se mantienen las ${engine.labels.length} etiquetas: ${engine.labels.map((label) => `\`${label}\``).join(', ')}.`,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
const ARTIFACTS = [
|
|
38
|
+
'## Artefactos canónicos (rutas relativas a la raíz del repositorio)',
|
|
39
|
+
'',
|
|
40
|
+
'- Constitución: `.specify/memory/constitution.md`',
|
|
41
|
+
'- Assessment: `.specify/assessments/<slug>/{intake,research,problem,concept,decision}.md`',
|
|
42
|
+
'- Feature: `specs/<feature>/{spec,plan,tasks}.md` (Spec Kit crea `<feature>` como `NNN-slug`)',
|
|
43
|
+
'- Bug: `.specify/bugs/<slug>/{assessment,fix,test}.md`',
|
|
44
|
+
];
|
|
45
|
+
const SEAL = [
|
|
46
|
+
'## Sello de `ai::spec-ready`',
|
|
47
|
+
'',
|
|
48
|
+
'SHA-256 sobre `specs/<feature>/plan.md`, `specs/<feature>/spec.md` y `specs/<feature>/tasks.md`:',
|
|
49
|
+
'rutas relativas a la raíz, ordenadas, separador `/`, contenido con LF y sin BOM; por archivo',
|
|
50
|
+
'`ruta + "\\n" + bytes`. Cualquier cambio posterior invalida la aprobación y una feature con',
|
|
51
|
+
'alguno de los tres archivos ausente no se puede sellar.',
|
|
52
|
+
];
|
|
53
|
+
/** Cuerpo de la regla para un destino. Copilot no tiene Spec Kit y su copia no invoca comandos. */
|
|
54
|
+
export function bindingRule(engine, tool) {
|
|
55
|
+
const speckit = TARGETS[tool].speckit;
|
|
56
|
+
const lines = ['# Spec Kit en AI-Hub: roles, estados y etiquetas', ''];
|
|
57
|
+
if (speckit === null) {
|
|
58
|
+
lines.push(`Este destino (${tool}) no tiene Spec Kit instalado: sus comandos no están disponibles aquí.`, 'La ejecución ocurre en un destino con Spec Kit (Claude, OpenCode, Pi o Cursor) o por la CLI de Spec Kit.', 'Esta regla solo informa de cómo se interpretan sus resultados.', '', ...stateTable(engine), '', ...ARTIFACTS, '', ...SEAL, '');
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
lines.push(`Spec Kit (\`${engine.speckit.package}\` ${engine.speckit.pin}) ejecuta los ${engine.commands.length} comandos.`, 'AI-Hub define qué rol invoca cada uno, cómo se interpreta su salida y qué etiqueta se aplica.', '', '## Comandos', '', '| Comando | Invocación | Carril | Rol | Artefactos | Estados de salida |', '|---|---|---|---|---|---|', ...engine.commands.map((command) => {
|
|
62
|
+
const roles = [command.role, ...(command.co_roles ?? [])].join(', ');
|
|
63
|
+
return `| \`${command.speckit}\` | \`${invocationFor(command.speckit, speckit.separator)}\` | ${command.lane} | ${roles} | ${command.artifacts.join(', ')} | ${command.exit.join(', ')} |`;
|
|
64
|
+
}), '', 'Un agente solo invoca los comandos asignados a su rol; un comando de otro rol se rechaza.', '', ...stateTable(engine), '', ...ARTIFACTS, '', ...SEAL, '');
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TARGETS } from '../sync/targets.js';
|
|
2
|
+
import { type ProcessRunner } from './runner.js';
|
|
3
|
+
import type { SpeckitSettings } from './settings.js';
|
|
4
|
+
/** Argumentos de `uvx` que fijan el paquete y la fecha de resolución. */
|
|
5
|
+
export declare function uvxPrefix(settings: SpeckitSettings): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Secuencia de instalación (design D3, paso 9). Cada elemento son los argumentos de `uvx`.
|
|
8
|
+
* Nunca incluye `extension add --force`: borra la extensión de la integración anterior.
|
|
9
|
+
* `integration use` registra de forma aditiva las extensiones ya instaladas.
|
|
10
|
+
*/
|
|
11
|
+
export declare function speckitSequence(settings: SpeckitSettings): string[][];
|
|
12
|
+
export interface SpeckitEnvironment {
|
|
13
|
+
uvx: 'found' | 'missing';
|
|
14
|
+
cached: 'yes' | 'no' | 'unknown';
|
|
15
|
+
shell: 'found' | 'missing';
|
|
16
|
+
shellCommand: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Ejecuta el shell en lugar de buscarlo en el PATH: en Windows `bash` puede ser el lanzador
|
|
20
|
+
* de WSL, que existe pero no ejecuta sin una distribución instalada.
|
|
21
|
+
*/
|
|
22
|
+
export declare function probeShell(settings: SpeckitSettings, runner: ProcessRunner, cwd: string): Promise<{
|
|
23
|
+
ok: boolean;
|
|
24
|
+
command: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Sonda del entorno para el plan: describe la máquina y no entra en el hash. */
|
|
27
|
+
export declare function probeEnvironment(settings: SpeckitSettings, runner: ProcessRunner, cwd: string): Promise<SpeckitEnvironment>;
|
|
28
|
+
export interface PreflightResult {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
problems: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Preflight (design D3, paso 6). Se ejecuta antes de escribir cualquier archivo.
|
|
34
|
+
* Comprueba uv, la resolución de la versión fijada (caché o red) y el shell del script declarado.
|
|
35
|
+
*/
|
|
36
|
+
export declare function speckitPreflight(settings: SpeckitSettings, runner: ProcessRunner, cwd: string): Promise<PreflightResult>;
|
|
37
|
+
export interface SpeckitRunResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
steps: string[][];
|
|
40
|
+
failure: string | null;
|
|
41
|
+
}
|
|
42
|
+
/** Ejecuta la secuencia de instalación en `root`. Se detiene en el primer paso fallido. */
|
|
43
|
+
export declare function runSpeckitSequence(settings: SpeckitSettings, runner: ProcessRunner, root: string): Promise<SpeckitRunResult>;
|
|
44
|
+
/** Comando de Spec Kit en la notación de un destino: `/speckit-bug-assess` o `/speckit.bug.assess`. */
|
|
45
|
+
export declare function invocationFor(canonical: string, separator: '-' | '.'): string;
|
|
46
|
+
export declare function speckitTargetOf(tool: keyof typeof TARGETS): (typeof TARGETS)[keyof typeof TARGETS]['speckit'];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { TARGETS } from '../sync/targets.js';
|
|
2
|
+
import { succeeded } from './runner.js';
|
|
3
|
+
/** Argumentos de `uvx` que fijan el paquete y la fecha de resolución. */
|
|
4
|
+
export function uvxPrefix(settings) {
|
|
5
|
+
return ['--from', `${settings.package}==${settings.version}`, '--exclude-newer', settings.excludeNewer, 'specify'];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Secuencia de instalación (design D3, paso 9). Cada elemento son los argumentos de `uvx`.
|
|
9
|
+
* Nunca incluye `extension add --force`: borra la extensión de la integración anterior.
|
|
10
|
+
* `integration use` registra de forma aditiva las extensiones ya instaladas.
|
|
11
|
+
*/
|
|
12
|
+
export function speckitSequence(settings) {
|
|
13
|
+
if (settings.defaultIntegration === null) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const prefix = uvxPrefix(settings);
|
|
17
|
+
const extensions = Object.keys(settings.extensions)
|
|
18
|
+
.sort()
|
|
19
|
+
.reverse()
|
|
20
|
+
.flatMap((id) => ['--extension', id]);
|
|
21
|
+
const steps = [
|
|
22
|
+
[
|
|
23
|
+
...prefix,
|
|
24
|
+
'init',
|
|
25
|
+
'.',
|
|
26
|
+
'--here',
|
|
27
|
+
'--force',
|
|
28
|
+
'--non-interactive',
|
|
29
|
+
'--integration',
|
|
30
|
+
settings.defaultIntegration,
|
|
31
|
+
'--script',
|
|
32
|
+
settings.script,
|
|
33
|
+
...extensions,
|
|
34
|
+
],
|
|
35
|
+
];
|
|
36
|
+
for (const integration of settings.integrations.slice(1)) {
|
|
37
|
+
steps.push([...prefix, 'integration', 'install', integration, '--script', settings.script]);
|
|
38
|
+
steps.push([...prefix, 'integration', 'use', integration]);
|
|
39
|
+
}
|
|
40
|
+
if (settings.integrations.length > 1) {
|
|
41
|
+
steps.push([...prefix, 'integration', 'use', settings.defaultIntegration]);
|
|
42
|
+
}
|
|
43
|
+
return steps;
|
|
44
|
+
}
|
|
45
|
+
function shellCandidates(settings) {
|
|
46
|
+
return settings.script === 'ps'
|
|
47
|
+
? [
|
|
48
|
+
{ command: 'pwsh', args: ['-NoProfile', '-Command', 'echo ok'] },
|
|
49
|
+
{ command: 'powershell', args: ['-NoProfile', '-Command', 'echo ok'] },
|
|
50
|
+
]
|
|
51
|
+
: [{ command: 'bash', args: ['-c', 'echo ok'] }];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Ejecuta el shell en lugar de buscarlo en el PATH: en Windows `bash` puede ser el lanzador
|
|
55
|
+
* de WSL, que existe pero no ejecuta sin una distribución instalada.
|
|
56
|
+
*/
|
|
57
|
+
export async function probeShell(settings, runner, cwd) {
|
|
58
|
+
const candidates = shellCandidates(settings);
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
const result = await runner(candidate.command, candidate.args, { cwd, timeoutMs: 30_000 });
|
|
61
|
+
if (succeeded(result) && result.stdout.trim() === 'ok') {
|
|
62
|
+
return { ok: true, command: candidate.command };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, command: candidates[0].command };
|
|
66
|
+
}
|
|
67
|
+
async function uvxAvailable(runner, cwd) {
|
|
68
|
+
return succeeded(await runner('uvx', ['--version'], { cwd, timeoutMs: 30_000 }));
|
|
69
|
+
}
|
|
70
|
+
/** Sonda del entorno para el plan: describe la máquina y no entra en el hash. */
|
|
71
|
+
export async function probeEnvironment(settings, runner, cwd) {
|
|
72
|
+
const uvx = await uvxAvailable(runner, cwd);
|
|
73
|
+
let cached = 'unknown';
|
|
74
|
+
if (uvx) {
|
|
75
|
+
const offline = await runner('uvx', ['--offline', ...uvxPrefix(settings), '--version'], { cwd, timeoutMs: 60_000 });
|
|
76
|
+
cached = succeeded(offline) ? 'yes' : 'no';
|
|
77
|
+
}
|
|
78
|
+
const shell = await probeShell(settings, runner, cwd);
|
|
79
|
+
return {
|
|
80
|
+
uvx: uvx ? 'found' : 'missing',
|
|
81
|
+
cached,
|
|
82
|
+
shell: shell.ok ? 'found' : 'missing',
|
|
83
|
+
shellCommand: shell.command,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Preflight (design D3, paso 6). Se ejecuta antes de escribir cualquier archivo.
|
|
88
|
+
* Comprueba uv, la resolución de la versión fijada (caché o red) y el shell del script declarado.
|
|
89
|
+
*/
|
|
90
|
+
export async function speckitPreflight(settings, runner, cwd) {
|
|
91
|
+
const problems = [];
|
|
92
|
+
if (!(await uvxAvailable(runner, cwd))) {
|
|
93
|
+
problems.push('uv no está disponible (uvx no está en el PATH). Instálalo: https://docs.astral.sh/uv/getting-started/installation/ ' +
|
|
94
|
+
'— o repite con --no-speckit para instalar sin Spec Kit (el arnés quedará degradado).');
|
|
95
|
+
return { ok: false, problems };
|
|
96
|
+
}
|
|
97
|
+
const resolved = await runner('uvx', [...uvxPrefix(settings), '--version'], { cwd });
|
|
98
|
+
if (!succeeded(resolved)) {
|
|
99
|
+
problems.push(`no se pudo resolver ${settings.package}==${settings.version} con uvx: sin red y sin caché, o la versión no existe. ` +
|
|
100
|
+
'Ejecuta una vez con red (uvx descarga y cachea la versión fijada) o usa --no-speckit.' +
|
|
101
|
+
(resolved.stderr.trim() !== '' ? `\n${resolved.stderr.trim().split(/\r?\n/).slice(-3).join('\n')}` : ''));
|
|
102
|
+
}
|
|
103
|
+
const shell = await probeShell(settings, runner, cwd);
|
|
104
|
+
if (!shell.ok) {
|
|
105
|
+
problems.push(settings.script === 'ps'
|
|
106
|
+
? 'PowerShell no está disponible (pwsh o powershell) y speckit.script es ps. Instala PowerShell o usa speckit.script: sh.'
|
|
107
|
+
: 'bash no está disponible o no ejecuta `bash -c "echo ok"`, y speckit.script es sh. ' +
|
|
108
|
+
'En Windows instala Git for Windows (Git Bash) y ponlo en el PATH; el lanzador de WSL sin distribución no sirve. ' +
|
|
109
|
+
'Alternativa: declara speckit.script: ps en manifest.yaml.');
|
|
110
|
+
}
|
|
111
|
+
return { ok: problems.length === 0, problems };
|
|
112
|
+
}
|
|
113
|
+
/** Ejecuta la secuencia de instalación en `root`. Se detiene en el primer paso fallido. */
|
|
114
|
+
export async function runSpeckitSequence(settings, runner, root) {
|
|
115
|
+
const steps = speckitSequence(settings);
|
|
116
|
+
for (const step of steps) {
|
|
117
|
+
const result = await runner('uvx', step, { cwd: root });
|
|
118
|
+
if (!succeeded(result)) {
|
|
119
|
+
const detail = (result.error ?? result.stderr ?? result.stdout).trim().split(/\r?\n/).slice(-5).join('\n');
|
|
120
|
+
return { ok: false, steps, failure: `uvx ${step.join(' ')} falló (${result.status ?? 'sin código'}): ${detail}` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, steps, failure: null };
|
|
124
|
+
}
|
|
125
|
+
/** Comando de Spec Kit en la notación de un destino: `/speckit-bug-assess` o `/speckit.bug.assess`. */
|
|
126
|
+
export function invocationFor(canonical, separator) {
|
|
127
|
+
return `/${canonical.replace(/\./g, separator)}`;
|
|
128
|
+
}
|
|
129
|
+
export function speckitTargetOf(tool) {
|
|
130
|
+
return TARGETS[tool].speckit;
|
|
131
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EngineManifest } from '../engine/engine.js';
|
|
2
|
+
import type { ProjectManifest } from '../manifest/manifest.js';
|
|
3
|
+
import type { ProcessRunner } from './runner.js';
|
|
4
|
+
export interface SpeckitCheck {
|
|
5
|
+
id: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
/** Ids de los checks de Spec Kit, en el orden en que `doctor` los imprime. */
|
|
10
|
+
export declare const SPECKIT_CHECK_IDS: readonly ["speckit:managed", "speckit:installed", "speckit:version", "speckit:shell", "speckit:integrations", "speckit:extensions", "speckit:integrity", "speckit:collision", "speckit:constitution", "speckit:versioning"];
|
|
11
|
+
/** Checks de doctor para una instalación (dev-cli "Doctor verifies the Spec Kit installation"). */
|
|
12
|
+
export declare function speckitDoctorChecks(root: string, manifest: ProjectManifest, engine: EngineManifest, runner: ProcessRunner): Promise<SpeckitCheck[]>;
|
|
13
|
+
/** En el checkout del motor los checks de Spec Kit no aplican: su .specify/ es del mantenedor. */
|
|
14
|
+
export declare function speckitNotApplicable(): SpeckitCheck[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readLock } from '../sync/lock.js';
|
|
3
|
+
import { probeShell } from './commands.js';
|
|
4
|
+
import { effectiveSpeckit } from './settings.js';
|
|
5
|
+
import { checkExtensions, checkIntegrations, checkIntegrity, constitutionIsTemplate, inReservedNamespace, readSpeckitState, speckitOwnedPaths, } from './state.js';
|
|
6
|
+
/** Ids de los checks de Spec Kit, en el orden en que `doctor` los imprime. */
|
|
7
|
+
export const SPECKIT_CHECK_IDS = [
|
|
8
|
+
'speckit:managed',
|
|
9
|
+
'speckit:installed',
|
|
10
|
+
'speckit:version',
|
|
11
|
+
'speckit:shell',
|
|
12
|
+
'speckit:integrations',
|
|
13
|
+
'speckit:extensions',
|
|
14
|
+
'speckit:integrity',
|
|
15
|
+
'speckit:collision',
|
|
16
|
+
'speckit:constitution',
|
|
17
|
+
'speckit:versioning',
|
|
18
|
+
];
|
|
19
|
+
/** Estado de Spec Kit que el consumidor debe versionar: si git lo ignora, se pierde. */
|
|
20
|
+
const VERSIONED_STATE = ['.specify/memory/constitution.md', '.specify/init-options.json', 'specs/'];
|
|
21
|
+
function list(items, limit = 5) {
|
|
22
|
+
return items.slice(0, limit).join('; ') + (items.length > limit ? ` (+${items.length - limit})` : '');
|
|
23
|
+
}
|
|
24
|
+
function gitIgnored(root, rel) {
|
|
25
|
+
const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root, encoding: 'utf8', shell: false });
|
|
26
|
+
if (inside.status !== 0 || (inside.stdout ?? '').trim() !== 'true') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const result = spawnSync('git', ['check-ignore', '-q', '--no-index', rel], { cwd: root, encoding: 'utf8', shell: false });
|
|
30
|
+
return result.status === 0;
|
|
31
|
+
}
|
|
32
|
+
/** Checks de doctor para una instalación (dev-cli "Doctor verifies the Spec Kit installation"). */
|
|
33
|
+
export async function speckitDoctorChecks(root, manifest, engine, runner) {
|
|
34
|
+
const checks = [];
|
|
35
|
+
const add = (id, ok, detail) => {
|
|
36
|
+
checks.push({ id, ok, detail });
|
|
37
|
+
};
|
|
38
|
+
const settings = effectiveSpeckit(manifest, engine);
|
|
39
|
+
const copilot = manifest.targets.includes('copilot');
|
|
40
|
+
if (!settings.managed) {
|
|
41
|
+
add('speckit:managed', false, 'degradado: speckit.managed es false (--no-speckit); ejecuta ai-hub install para instalar Spec Kit');
|
|
42
|
+
for (const id of SPECKIT_CHECK_IDS.slice(1)) {
|
|
43
|
+
add(id, true, 'no aplica: Spec Kit no gestionado');
|
|
44
|
+
}
|
|
45
|
+
return checks;
|
|
46
|
+
}
|
|
47
|
+
add('speckit:managed', true, `gestionado: ${settings.package} ${settings.version} vía uvx`);
|
|
48
|
+
const state = await readSpeckitState(root);
|
|
49
|
+
add('speckit:installed', state.initOptions !== null, state.initOptions !== null ? '.specify/init-options.json presente' : 'falta .specify/init-options.json; ejecuta ai-hub install');
|
|
50
|
+
const installed = state.initOptions?.speckit_version;
|
|
51
|
+
add('speckit:version', installed === settings.version, installed === settings.version
|
|
52
|
+
? `${installed}`
|
|
53
|
+
: `instalada ${installed ?? '(ninguna)'}; se esperaba ${settings.version}; ejecuta ai-hub install`);
|
|
54
|
+
const shell = await probeShell(settings, runner, root);
|
|
55
|
+
add('speckit:shell', shell.ok, shell.ok
|
|
56
|
+
? `${shell.command} ejecuta los scripts ${settings.script}`
|
|
57
|
+
: settings.script === 'ps'
|
|
58
|
+
? 'PowerShell (pwsh o powershell) no está disponible para los scripts ps'
|
|
59
|
+
: 'bash no está disponible o no ejecuta; en Windows instala Git Bash, o declara speckit.script: ps');
|
|
60
|
+
const integrations = checkIntegrations(settings, state);
|
|
61
|
+
add('speckit:integrations', integrations.length === 0, integrations.length === 0
|
|
62
|
+
? `${settings.integrations.join(', ')} (por defecto ${settings.defaultIntegration ?? '-'})`
|
|
63
|
+
: list(integrations));
|
|
64
|
+
if (copilot) {
|
|
65
|
+
add('speckit:targets', true, 'copilot: sin Spec Kit (recibe agentes y reglas de AI-Hub; sus comandos se ejecutan en otro destino)');
|
|
66
|
+
}
|
|
67
|
+
const extensions = await checkExtensions(root, settings, state);
|
|
68
|
+
add('speckit:extensions', extensions.length === 0, extensions.length === 0
|
|
69
|
+
? Object.entries(settings.extensions)
|
|
70
|
+
.map(([id, version]) => `${id} ${version}`)
|
|
71
|
+
.join(', ') + ` en ${settings.targets.length} destinos`
|
|
72
|
+
: list(extensions));
|
|
73
|
+
const integrity = await checkIntegrity(root, state);
|
|
74
|
+
add('speckit:integrity', integrity.missing.length === 0, integrity.missing.length > 0
|
|
75
|
+
? `faltan ${integrity.missing.length} archivos de Spec Kit: ${list(integrity.missing)}; ejecuta ai-hub install`
|
|
76
|
+
: integrity.modified.length > 0
|
|
77
|
+
? `aviso: ${integrity.modified.length} archivos de Spec Kit modificados; el próximo init los sobrescribe (usa overrides): ${list(integrity.modified)}`
|
|
78
|
+
: `${integrity.checked} archivos de Spec Kit íntegros`);
|
|
79
|
+
const owned = await speckitOwnedPaths(root, manifest.targets);
|
|
80
|
+
const lock = await readLock(root).catch(() => null);
|
|
81
|
+
const shared = Object.keys(lock?.files ?? {})
|
|
82
|
+
.filter((rel) => owned.has(rel) || (state.present && inReservedNamespace(rel, manifest.targets)))
|
|
83
|
+
.sort();
|
|
84
|
+
add('speckit:collision', shared.length === 0, shared.length === 0 ? 'ninguna ruta de sync.lock.json pertenece a Spec Kit' : `rutas compartidas con Spec Kit: ${list(shared)}`);
|
|
85
|
+
const template = await constitutionIsTemplate(root);
|
|
86
|
+
add('speckit:constitution', true, template === true
|
|
87
|
+
? 'aviso: la constitución sigue siendo la plantilla; ejecuta /speckit.constitution'
|
|
88
|
+
: template === null
|
|
89
|
+
? 'aviso: no hay .specify/memory/constitution.md'
|
|
90
|
+
: 'constitución propia del proyecto');
|
|
91
|
+
const ignored = VERSIONED_STATE.filter((rel) => gitIgnored(root, rel) === true);
|
|
92
|
+
const applicable = gitIgnored(root, VERSIONED_STATE[0]) !== null;
|
|
93
|
+
add('speckit:versioning', ignored.length === 0, !applicable
|
|
94
|
+
? 'no aplica (sin repositorio git)'
|
|
95
|
+
: ignored.length === 0
|
|
96
|
+
? 'constitución, init-options.json y specs/ versionables'
|
|
97
|
+
: `git ignora estado del proyecto: ${ignored.join(', ')}; quítalo de .gitignore`);
|
|
98
|
+
return checks;
|
|
99
|
+
}
|
|
100
|
+
/** En el checkout del motor los checks de Spec Kit no aplican: su .specify/ es del mantenedor. */
|
|
101
|
+
export function speckitNotApplicable() {
|
|
102
|
+
return SPECKIT_CHECK_IDS.map((id) => ({ id, ok: true, detail: 'no aplica en el checkout del motor' }));
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface RunResult {
|
|
2
|
+
/** Código de salida; `null` si el proceso no llegó a ejecutarse o fue interrumpido. */
|
|
3
|
+
status: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
/** Error de arranque (por ejemplo `ENOENT` cuando el ejecutable no existe). */
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RunOptions {
|
|
10
|
+
cwd: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Ejecuta un proceso externo. Los argumentos viajan como array, nunca a través de un shell,
|
|
15
|
+
* para que las rutas con espacios no se interpreten. La entrada estándar se cierra.
|
|
16
|
+
*/
|
|
17
|
+
export type ProcessRunner = (command: string, args: string[], options: RunOptions) => Promise<RunResult>;
|
|
18
|
+
export declare const defaultRunner: ProcessRunner;
|
|
19
|
+
export declare function succeeded(result: RunResult): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
3
|
+
export const defaultRunner = async (command, args, options) => {
|
|
4
|
+
const result = spawnSync(command, args, {
|
|
5
|
+
cwd: options.cwd,
|
|
6
|
+
shell: false,
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
input: '',
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
11
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8', NO_COLOR: '1' },
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
status: result.status,
|
|
15
|
+
stdout: result.stdout ?? '',
|
|
16
|
+
stderr: result.stderr ?? '',
|
|
17
|
+
...(result.error ? { error: result.error.message } : {}),
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export function succeeded(result) {
|
|
21
|
+
return result.error === undefined && result.status === 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Archivos que sella `ai::spec-ready`, dentro de `specs/<feature>/`. */
|
|
2
|
+
export declare const SEALED_FILES: readonly ["plan.md", "spec.md", "tasks.md"];
|
|
3
|
+
export declare class IncompleteFeatureError extends Error {
|
|
4
|
+
readonly missing: string[];
|
|
5
|
+
constructor(missing: string[]);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sello SHA-256 de una feature de Spec Kit (implementación de referencia del contrato de
|
|
9
|
+
* `hermes-control`, AGENTS.md §5): rutas relativas a la raíz, ordenadas, separador `/`;
|
|
10
|
+
* por archivo `ruta + "\n" + bytes` normalizados a LF y sin BOM; SHA-256 sobre la secuencia.
|
|
11
|
+
*/
|
|
12
|
+
export declare function featureSeal(root: string, featureDir: string): Promise<string>;
|