@dsalazardev/ai-hub-cli 0.2.0 → 0.3.1
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 +64 -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 +152 -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 +64 -0
- package/dist/speckit/commands.js +171 -0
- package/dist/speckit/doctor.d.ts +17 -0
- package/dist/speckit/doctor.js +119 -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
|
@@ -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>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** Archivos que sella `ai::spec-ready`, dentro de `specs/<feature>/`. */
|
|
5
|
+
export const SEALED_FILES = ['plan.md', 'spec.md', 'tasks.md'];
|
|
6
|
+
export class IncompleteFeatureError extends Error {
|
|
7
|
+
missing;
|
|
8
|
+
constructor(missing) {
|
|
9
|
+
super(`la feature no se puede sellar; faltan: ${missing.join(', ')}`);
|
|
10
|
+
this.missing = missing;
|
|
11
|
+
this.name = 'IncompleteFeatureError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Normaliza el contenido como exige el contrato: sin BOM y con LF. */
|
|
15
|
+
function canonicalContent(bytes) {
|
|
16
|
+
const text = bytes.toString('utf8').replace(/^/, '').replace(/\r\n?/g, '\n');
|
|
17
|
+
return Buffer.from(text, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sello SHA-256 de una feature de Spec Kit (implementación de referencia del contrato de
|
|
21
|
+
* `hermes-control`, AGENTS.md §5): rutas relativas a la raíz, ordenadas, separador `/`;
|
|
22
|
+
* por archivo `ruta + "\n" + bytes` normalizados a LF y sin BOM; SHA-256 sobre la secuencia.
|
|
23
|
+
*/
|
|
24
|
+
export async function featureSeal(root, featureDir) {
|
|
25
|
+
const dir = featureDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
26
|
+
const rels = SEALED_FILES.map((name) => `${dir}/${name}`).sort();
|
|
27
|
+
const contents = [];
|
|
28
|
+
const missing = [];
|
|
29
|
+
for (const rel of rels) {
|
|
30
|
+
const bytes = await fs.readFile(path.join(root, ...rel.split('/'))).catch(() => null);
|
|
31
|
+
if (bytes === null) {
|
|
32
|
+
missing.push(rel);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
contents.push([rel, canonicalContent(bytes)]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (missing.length > 0) {
|
|
39
|
+
throw new IncompleteFeatureError(missing);
|
|
40
|
+
}
|
|
41
|
+
const hash = createHash('sha256');
|
|
42
|
+
for (const [rel, bytes] of contents) {
|
|
43
|
+
hash.update(`${rel}\n`, 'utf8');
|
|
44
|
+
hash.update(bytes);
|
|
45
|
+
}
|
|
46
|
+
return hash.digest('hex');
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EngineManifest, SpeckitScript } from '../engine/engine.js';
|
|
2
|
+
import { type ToolId } from '../sync/targets.js';
|
|
3
|
+
/** Bloque `speckit` de manifest.yaml: solo restringe lo que fija el paquete. */
|
|
4
|
+
export interface ProjectSpeckit {
|
|
5
|
+
managed?: boolean;
|
|
6
|
+
version?: string;
|
|
7
|
+
script?: SpeckitScript;
|
|
8
|
+
defaultTarget?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Campos que pertenecen al motor y que un proyecto no puede redefinir. */
|
|
11
|
+
export declare const ENGINE_OWNED_SPECKIT_FIELDS: readonly ["extensions", "excludeNewer", "integrations", "package", "pin", "supported"];
|
|
12
|
+
export interface SpeckitSettings {
|
|
13
|
+
managed: boolean;
|
|
14
|
+
package: string;
|
|
15
|
+
version: string;
|
|
16
|
+
excludeNewer: string;
|
|
17
|
+
script: SpeckitScript;
|
|
18
|
+
/** Destinos declarados que reciben Spec Kit; el de por defecto va primero. */
|
|
19
|
+
targets: ToolId[];
|
|
20
|
+
defaultTarget: ToolId | null;
|
|
21
|
+
defaultIntegration: string | null;
|
|
22
|
+
/** Integraciones de Spec Kit en el mismo orden que `targets`. */
|
|
23
|
+
integrations: string[];
|
|
24
|
+
extensions: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export declare function speckitEnabledTargets(targets: readonly string[]): ToolId[];
|
|
27
|
+
export declare function integrationOf(target: ToolId): string;
|
|
28
|
+
export declare function effectiveSpeckit(manifest: {
|
|
29
|
+
targets: readonly string[];
|
|
30
|
+
speckit?: ProjectSpeckit;
|
|
31
|
+
}, engine: EngineManifest): SpeckitSettings;
|
|
32
|
+
/**
|
|
33
|
+
* Valida el bloque `speckit` del proyecto contra el paquete. Devuelve errores con el nombre del campo.
|
|
34
|
+
* Se ejecuta antes del schema para poder explicar los campos que pertenecen al motor.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateProjectSpeckit(block: unknown, targets: readonly string[], engine: EngineManifest): string[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { TARGETS, isToolId } from '../sync/targets.js';
|
|
2
|
+
import { satisfiesRange } from './version.js';
|
|
3
|
+
/** Campos que pertenecen al motor y que un proyecto no puede redefinir. */
|
|
4
|
+
export const ENGINE_OWNED_SPECKIT_FIELDS = ['extensions', 'excludeNewer', 'integrations', 'package', 'pin', 'supported'];
|
|
5
|
+
export function speckitEnabledTargets(targets) {
|
|
6
|
+
return targets.filter((target) => isToolId(target) && TARGETS[target].speckit !== null);
|
|
7
|
+
}
|
|
8
|
+
export function integrationOf(target) {
|
|
9
|
+
const speckit = TARGETS[target].speckit;
|
|
10
|
+
if (speckit === null) {
|
|
11
|
+
throw new Error(`el destino ${target} no recibe Spec Kit`);
|
|
12
|
+
}
|
|
13
|
+
return speckit.integration;
|
|
14
|
+
}
|
|
15
|
+
export function effectiveSpeckit(manifest, engine) {
|
|
16
|
+
const block = manifest.speckit ?? {};
|
|
17
|
+
const enabled = speckitEnabledTargets(manifest.targets);
|
|
18
|
+
const declaredDefault = block.defaultTarget !== undefined && enabled.includes(block.defaultTarget)
|
|
19
|
+
? block.defaultTarget
|
|
20
|
+
: null;
|
|
21
|
+
const defaultTarget = declaredDefault ?? enabled[0] ?? null;
|
|
22
|
+
const targets = defaultTarget === null ? [] : [defaultTarget, ...enabled.filter((target) => target !== defaultTarget)];
|
|
23
|
+
return {
|
|
24
|
+
managed: block.managed !== false,
|
|
25
|
+
package: engine.speckit.package,
|
|
26
|
+
version: block.version ?? engine.speckit.pin,
|
|
27
|
+
excludeNewer: engine.speckit.excludeNewer,
|
|
28
|
+
script: block.script ?? engine.speckit.script,
|
|
29
|
+
targets,
|
|
30
|
+
defaultTarget,
|
|
31
|
+
defaultIntegration: defaultTarget === null ? null : integrationOf(defaultTarget),
|
|
32
|
+
integrations: targets.map(integrationOf),
|
|
33
|
+
extensions: { ...engine.speckit.extensions },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Valida el bloque `speckit` del proyecto contra el paquete. Devuelve errores con el nombre del campo.
|
|
41
|
+
* Se ejecuta antes del schema para poder explicar los campos que pertenecen al motor.
|
|
42
|
+
*/
|
|
43
|
+
export function validateProjectSpeckit(block, targets, engine) {
|
|
44
|
+
if (block === undefined) {
|
|
45
|
+
return speckitEnabledTargets(targets).length === 0
|
|
46
|
+
? ['speckit requiere al menos un destino con Spec Kit (claude, opencode, pi, cursor) o speckit.managed: false']
|
|
47
|
+
: [];
|
|
48
|
+
}
|
|
49
|
+
if (!isRecord(block)) {
|
|
50
|
+
return ['speckit debe ser un objeto'];
|
|
51
|
+
}
|
|
52
|
+
const errors = [];
|
|
53
|
+
for (const field of ENGINE_OWNED_SPECKIT_FIELDS) {
|
|
54
|
+
if (field in block) {
|
|
55
|
+
errors.push(`speckit.${field} pertenece al motor; el proyecto no puede redefinirlo`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (block.version !== undefined) {
|
|
59
|
+
if (typeof block.version !== 'string' || !satisfiesRange(block.version, engine.speckit.supported)) {
|
|
60
|
+
errors.push(`speckit.version ${String(block.version)} está fuera del rango soportado por el paquete "${engine.speckit.supported}"`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const enabled = speckitEnabledTargets(targets);
|
|
64
|
+
if (block.defaultTarget !== undefined) {
|
|
65
|
+
const value = String(block.defaultTarget);
|
|
66
|
+
if (!enabled.includes(value)) {
|
|
67
|
+
const all = Object.keys(TARGETS).filter((tool) => TARGETS[tool].speckit !== null);
|
|
68
|
+
errors.push(`speckit.defaultTarget "${value}" debe ser un destino declarado con Spec Kit: ${all.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (block.managed !== false && enabled.length === 0) {
|
|
72
|
+
errors.push('speckit requiere al menos un destino con Spec Kit (claude, opencode, pi, cursor) o speckit.managed: false');
|
|
73
|
+
}
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type SpeckitTarget } from '../sync/targets.js';
|
|
2
|
+
import type { SpeckitSettings } from './settings.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lectura de `.specify/` sin ejecutar Python. Los timestamps `installed_at` de los manifiestos
|
|
5
|
+
* de integración y del registro de extensiones nunca se comparan ni se hashean.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SPECIFY_DIR = ".specify";
|
|
8
|
+
export interface IntegrationManifest {
|
|
9
|
+
integration: string;
|
|
10
|
+
files: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export interface SpeckitState {
|
|
13
|
+
present: boolean;
|
|
14
|
+
initOptions: {
|
|
15
|
+
speckit_version?: string;
|
|
16
|
+
script?: string;
|
|
17
|
+
} | null;
|
|
18
|
+
integration: {
|
|
19
|
+
installed_integrations: string[];
|
|
20
|
+
default_integration?: string;
|
|
21
|
+
} | null;
|
|
22
|
+
installedExtensions: string[];
|
|
23
|
+
registry: Record<string, {
|
|
24
|
+
version?: string;
|
|
25
|
+
registered_commands?: Record<string, string[]>;
|
|
26
|
+
}>;
|
|
27
|
+
manifests: IntegrationManifest[];
|
|
28
|
+
}
|
|
29
|
+
export declare function readSpeckitState(root: string): Promise<SpeckitState>;
|
|
30
|
+
/** Ruta de un comando de Spec Kit en un destino: `<dir>/speckit-bug-assess/SKILL.md` o `<dir>/speckit.bug.assess.md`. */
|
|
31
|
+
export declare function commandPath(target: SpeckitTarget, canonical: string): string;
|
|
32
|
+
/** Ids canónicos de los comandos de una extensión, leídos de `.specify/extensions/<id>/commands/`. */
|
|
33
|
+
export declare function extensionCommands(root: string, extension: string): Promise<string[]>;
|
|
34
|
+
/** `true` si la ruta cae en el espacio de nombres reservado de algún destino con Spec Kit declarado. */
|
|
35
|
+
export declare function inReservedNamespace(rel: string, tools: readonly string[]): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Rutas que pertenecen a Spec Kit (agent-guardrails "Every generated file has one owner"):
|
|
38
|
+
* las de sus manifiestos de integración y los comandos de sus extensiones en cada destino con Spec Kit.
|
|
39
|
+
* El espacio de nombres reservado se evalúa aparte con `inReservedNamespace`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function speckitOwnedPaths(root: string, tools: readonly string[]): Promise<Set<string>>;
|
|
42
|
+
/** Decide si una ruta es de Spec Kit, sea por manifiesto, por extensión o por espacio de nombres. */
|
|
43
|
+
export declare function ownedBySpeckit(rel: string, owned: Set<string>, tools: readonly string[], present: boolean): boolean;
|
|
44
|
+
/** speckit:integrations — exactamente las integraciones de los destinos declarados, sin Copilot. */
|
|
45
|
+
export declare function checkIntegrations(settings: SpeckitSettings, state: SpeckitState): string[];
|
|
46
|
+
/** speckit:extensions — extensiones fijadas, con su versión y sus comandos en cada destino con Spec Kit. */
|
|
47
|
+
export declare function checkExtensions(root: string, settings: SpeckitSettings, state: SpeckitState): Promise<string[]>;
|
|
48
|
+
export interface IntegrityReport {
|
|
49
|
+
missing: string[];
|
|
50
|
+
modified: string[];
|
|
51
|
+
checked: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* speckit:integrity — cada ruta de los manifiestos de integración existe y conserva su hash.
|
|
55
|
+
* Un archivo con finales de línea CRLF (autocrlf de git) se compara también normalizado a LF.
|
|
56
|
+
*/
|
|
57
|
+
export declare function checkIntegrity(root: string, state: SpeckitState): Promise<IntegrityReport>;
|
|
58
|
+
/** speckit:constitution — `true` si la constitución sigue siendo la plantilla. */
|
|
59
|
+
export declare function constitutionIsTemplate(root: string): Promise<boolean | null>;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { TARGETS, isToolId } from '../sync/targets.js';
|
|
6
|
+
/**
|
|
7
|
+
* Lectura de `.specify/` sin ejecutar Python. Los timestamps `installed_at` de los manifiestos
|
|
8
|
+
* de integración y del registro de extensiones nunca se comparan ni se hashean.
|
|
9
|
+
*/
|
|
10
|
+
export const SPECIFY_DIR = '.specify';
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
async function readJson(file) {
|
|
15
|
+
const text = await fs.readFile(file, 'utf8').catch(() => null);
|
|
16
|
+
if (text === null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(text);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function exists(file) {
|
|
27
|
+
return fs.access(file).then(() => true, () => false);
|
|
28
|
+
}
|
|
29
|
+
export async function readSpeckitState(root) {
|
|
30
|
+
const dir = path.join(root, SPECIFY_DIR);
|
|
31
|
+
const present = await exists(dir);
|
|
32
|
+
const initRaw = await readJson(path.join(dir, 'init-options.json'));
|
|
33
|
+
const integrationRaw = await readJson(path.join(dir, 'integration.json'));
|
|
34
|
+
const registryRaw = await readJson(path.join(dir, 'extensions', '.registry'));
|
|
35
|
+
const extensionsText = await fs.readFile(path.join(dir, 'extensions.yml'), 'utf8').catch(() => null);
|
|
36
|
+
let installedExtensions = [];
|
|
37
|
+
if (extensionsText !== null) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = parse(extensionsText);
|
|
40
|
+
if (isRecord(parsed) && Array.isArray(parsed.installed)) {
|
|
41
|
+
installedExtensions = parsed.installed.filter((item) => typeof item === 'string');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
installedExtensions = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const manifests = [];
|
|
49
|
+
const integrationsDir = path.join(dir, 'integrations');
|
|
50
|
+
for (const name of (await fs.readdir(integrationsDir).catch(() => [])).sort()) {
|
|
51
|
+
if (!name.endsWith('.manifest.json')) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const raw = await readJson(path.join(integrationsDir, name));
|
|
55
|
+
if (isRecord(raw) && isRecord(raw.files)) {
|
|
56
|
+
manifests.push({
|
|
57
|
+
integration: typeof raw.integration === 'string' ? raw.integration : name.replace(/\.manifest\.json$/, ''),
|
|
58
|
+
files: Object.fromEntries(Object.entries(raw.files).filter((entry) => typeof entry[1] === 'string')),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const registry = {};
|
|
63
|
+
if (isRecord(registryRaw) && isRecord(registryRaw.extensions)) {
|
|
64
|
+
for (const [id, entry] of Object.entries(registryRaw.extensions)) {
|
|
65
|
+
if (!isRecord(entry)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const commands = {};
|
|
69
|
+
if (isRecord(entry.registered_commands)) {
|
|
70
|
+
for (const [integration, list] of Object.entries(entry.registered_commands)) {
|
|
71
|
+
if (Array.isArray(list)) {
|
|
72
|
+
commands[integration] = list.filter((item) => typeof item === 'string');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
registry[id] = {
|
|
77
|
+
...(typeof entry.version === 'string' ? { version: entry.version } : {}),
|
|
78
|
+
registered_commands: commands,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
present,
|
|
84
|
+
initOptions: isRecord(initRaw)
|
|
85
|
+
? {
|
|
86
|
+
...(typeof initRaw.speckit_version === 'string' ? { speckit_version: initRaw.speckit_version } : {}),
|
|
87
|
+
...(typeof initRaw.script === 'string' ? { script: initRaw.script } : {}),
|
|
88
|
+
}
|
|
89
|
+
: null,
|
|
90
|
+
integration: isRecord(integrationRaw) && Array.isArray(integrationRaw.installed_integrations)
|
|
91
|
+
? {
|
|
92
|
+
installed_integrations: integrationRaw.installed_integrations.filter((item) => typeof item === 'string'),
|
|
93
|
+
...(typeof integrationRaw.default_integration === 'string'
|
|
94
|
+
? { default_integration: integrationRaw.default_integration }
|
|
95
|
+
: {}),
|
|
96
|
+
}
|
|
97
|
+
: null,
|
|
98
|
+
installedExtensions,
|
|
99
|
+
registry,
|
|
100
|
+
manifests,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Ruta de un comando de Spec Kit en un destino: `<dir>/speckit-bug-assess/SKILL.md` o `<dir>/speckit.bug.assess.md`. */
|
|
104
|
+
export function commandPath(target, canonical) {
|
|
105
|
+
return target.layout === 'skill-dir'
|
|
106
|
+
? `${target.commandsDir}/${canonical.replace(/\./g, '-')}/SKILL.md`
|
|
107
|
+
: `${target.commandsDir}/${canonical}.md`;
|
|
108
|
+
}
|
|
109
|
+
/** Ids canónicos de los comandos de una extensión, leídos de `.specify/extensions/<id>/commands/`. */
|
|
110
|
+
export async function extensionCommands(root, extension) {
|
|
111
|
+
const dir = path.join(root, SPECIFY_DIR, 'extensions', extension, 'commands');
|
|
112
|
+
return (await fs.readdir(dir).catch(() => []))
|
|
113
|
+
.filter((name) => name.endsWith('.md'))
|
|
114
|
+
.map((name) => name.slice(0, -'.md'.length))
|
|
115
|
+
.sort();
|
|
116
|
+
}
|
|
117
|
+
/** Destinos declarados que reciben Spec Kit, con su fila del registro. */
|
|
118
|
+
function enabledTargets(tools) {
|
|
119
|
+
const rows = [];
|
|
120
|
+
for (const tool of tools) {
|
|
121
|
+
if (!isToolId(tool)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const speckit = TARGETS[tool].speckit;
|
|
125
|
+
if (speckit !== null) {
|
|
126
|
+
rows.push([tool, speckit]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return rows;
|
|
130
|
+
}
|
|
131
|
+
/** `true` si la ruta cae en el espacio de nombres reservado de algún destino con Spec Kit declarado. */
|
|
132
|
+
export function inReservedNamespace(rel, tools) {
|
|
133
|
+
return enabledTargets(tools).some(([, speckit]) => {
|
|
134
|
+
const prefix = `${speckit.commandsDir}/`;
|
|
135
|
+
if (!rel.startsWith(prefix)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const name = rel.slice(prefix.length);
|
|
139
|
+
return name.startsWith('speckit-') || name.startsWith('speckit.');
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Rutas que pertenecen a Spec Kit (agent-guardrails "Every generated file has one owner"):
|
|
144
|
+
* las de sus manifiestos de integración y los comandos de sus extensiones en cada destino con Spec Kit.
|
|
145
|
+
* El espacio de nombres reservado se evalúa aparte con `inReservedNamespace`.
|
|
146
|
+
*/
|
|
147
|
+
export async function speckitOwnedPaths(root, tools) {
|
|
148
|
+
const owned = new Set();
|
|
149
|
+
const state = await readSpeckitState(root);
|
|
150
|
+
if (!state.present) {
|
|
151
|
+
return owned;
|
|
152
|
+
}
|
|
153
|
+
for (const manifest of state.manifests) {
|
|
154
|
+
for (const rel of Object.keys(manifest.files)) {
|
|
155
|
+
owned.add(rel);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const extensions = new Set([...state.installedExtensions, ...Object.keys(state.registry)]);
|
|
159
|
+
for (const extension of extensions) {
|
|
160
|
+
const commands = await extensionCommands(root, extension);
|
|
161
|
+
for (const [, speckit] of enabledTargets(tools)) {
|
|
162
|
+
for (const canonical of commands) {
|
|
163
|
+
owned.add(commandPath(speckit, canonical));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return owned;
|
|
168
|
+
}
|
|
169
|
+
/** Decide si una ruta es de Spec Kit, sea por manifiesto, por extensión o por espacio de nombres. */
|
|
170
|
+
export function ownedBySpeckit(rel, owned, tools, present) {
|
|
171
|
+
return owned.has(rel) || (present && inReservedNamespace(rel, tools));
|
|
172
|
+
}
|
|
173
|
+
/** speckit:integrations — exactamente las integraciones de los destinos declarados, sin Copilot. */
|
|
174
|
+
export function checkIntegrations(settings, state) {
|
|
175
|
+
if (state.integration === null) {
|
|
176
|
+
return ['falta .specify/integration.json'];
|
|
177
|
+
}
|
|
178
|
+
const problems = [];
|
|
179
|
+
const installed = state.integration.installed_integrations;
|
|
180
|
+
for (const [index, integration] of settings.integrations.entries()) {
|
|
181
|
+
if (!installed.includes(integration)) {
|
|
182
|
+
problems.push(`${settings.targets[index]}: falta la integración ${integration}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const integration of installed) {
|
|
186
|
+
if (!settings.integrations.includes(integration)) {
|
|
187
|
+
problems.push(integration === 'copilot'
|
|
188
|
+
? 'copilot: integración instalada aunque Copilot no recibe Spec Kit; ejecuta specify integration uninstall copilot'
|
|
189
|
+
: `integración no declarada: ${integration}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (settings.defaultIntegration !== null && state.integration.default_integration !== settings.defaultIntegration) {
|
|
193
|
+
problems.push(`integración por defecto ${state.integration.default_integration ?? '(ninguna)'}; se esperaba ${settings.defaultIntegration}`);
|
|
194
|
+
}
|
|
195
|
+
return problems;
|
|
196
|
+
}
|
|
197
|
+
/** speckit:extensions — extensiones fijadas, con su versión y sus comandos en cada destino con Spec Kit. */
|
|
198
|
+
export async function checkExtensions(root, settings, state) {
|
|
199
|
+
const problems = [];
|
|
200
|
+
for (const [extension, version] of Object.entries(settings.extensions).sort()) {
|
|
201
|
+
if (!state.installedExtensions.includes(extension)) {
|
|
202
|
+
problems.push(`${extension}: extensión no instalada`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const registered = state.registry[extension]?.version;
|
|
206
|
+
if (registered !== version) {
|
|
207
|
+
problems.push(`${extension}: versión ${registered ?? '(desconocida)'}; se esperaba ${version}`);
|
|
208
|
+
}
|
|
209
|
+
const commands = await extensionCommands(root, extension);
|
|
210
|
+
if (commands.length === 0) {
|
|
211
|
+
problems.push(`${extension}: no hay comandos en .specify/extensions/${extension}/commands`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
for (const tool of settings.targets) {
|
|
215
|
+
const speckit = TARGETS[tool].speckit;
|
|
216
|
+
if (speckit === null) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// Un mensaje por extensión y destino, para que el informe nombre todos los destinos afectados.
|
|
220
|
+
const registeredFor = state.registry[extension]?.registered_commands?.[speckit.integration] ?? [];
|
|
221
|
+
const unregistered = [];
|
|
222
|
+
const absent = [];
|
|
223
|
+
for (const canonical of commands) {
|
|
224
|
+
if (!registeredFor.includes(canonical)) {
|
|
225
|
+
unregistered.push(canonical);
|
|
226
|
+
}
|
|
227
|
+
else if (!(await exists(path.join(root, ...commandPath(speckit, canonical).split('/'))))) {
|
|
228
|
+
absent.push(canonical);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (unregistered.length > 0) {
|
|
232
|
+
problems.push(`${extension}: ${tool} (${speckit.integration}) no tiene registrados ${unregistered.join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
if (absent.length > 0) {
|
|
235
|
+
problems.push(`${extension}: ${tool} no tiene en ${speckit.commandsDir} ${absent.join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return problems;
|
|
240
|
+
}
|
|
241
|
+
function sha(bytes) {
|
|
242
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* speckit:integrity — cada ruta de los manifiestos de integración existe y conserva su hash.
|
|
246
|
+
* Un archivo con finales de línea CRLF (autocrlf de git) se compara también normalizado a LF.
|
|
247
|
+
*/
|
|
248
|
+
export async function checkIntegrity(root, state) {
|
|
249
|
+
const missing = [];
|
|
250
|
+
const modified = [];
|
|
251
|
+
let checked = 0;
|
|
252
|
+
for (const manifest of state.manifests) {
|
|
253
|
+
for (const [rel, expected] of Object.entries(manifest.files).sort()) {
|
|
254
|
+
checked += 1;
|
|
255
|
+
const bytes = await fs.readFile(path.join(root, ...rel.split('/'))).catch(() => null);
|
|
256
|
+
if (bytes === null) {
|
|
257
|
+
missing.push(rel);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (sha(bytes) !== expected && sha(bytes.toString('utf8').replace(/\r\n/g, '\n')) !== expected) {
|
|
261
|
+
modified.push(rel);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { missing: [...new Set(missing)].sort(), modified: [...new Set(modified)].sort(), checked };
|
|
266
|
+
}
|
|
267
|
+
/** speckit:constitution — `true` si la constitución sigue siendo la plantilla. */
|
|
268
|
+
export async function constitutionIsTemplate(root) {
|
|
269
|
+
const memory = path.join(root, SPECIFY_DIR, 'memory');
|
|
270
|
+
const marker = await readJson(path.join(memory, '.constitution-template.json'));
|
|
271
|
+
const constitution = await fs.readFile(path.join(memory, 'constitution.md')).catch(() => null);
|
|
272
|
+
if (constitution === null) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (!isRecord(marker) || typeof marker.sha256 !== 'string') {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
return sha(constitution) === marker.sha256 || sha(constitution.toString('utf8').replace(/\r\n/g, '\n')) === marker.sha256;
|
|
279
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparación mínima de versiones `x.y.z` para validar el pin de Spec Kit.
|
|
3
|
+
* Una versión con sufijo (por ejemplo `1.2.1.dev0`) no se considera estable y no satisface ningún rango.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseVersion(version: string): [number, number, number] | null;
|
|
6
|
+
export declare function compareVersions(left: [number, number, number], right: [number, number, number]): number;
|
|
7
|
+
/** Evalúa un rango de comparadores separados por espacios: `>=1.2.3 <1.3.0`. */
|
|
8
|
+
export declare function satisfiesRange(version: string, range: string): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparación mínima de versiones `x.y.z` para validar el pin de Spec Kit.
|
|
3
|
+
* Una versión con sufijo (por ejemplo `1.2.1.dev0`) no se considera estable y no satisface ningún rango.
|
|
4
|
+
*/
|
|
5
|
+
export function parseVersion(version) {
|
|
6
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
7
|
+
if (match === null) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
11
|
+
}
|
|
12
|
+
export function compareVersions(left, right) {
|
|
13
|
+
for (let index = 0; index < 3; index += 1) {
|
|
14
|
+
if (left[index] !== right[index]) {
|
|
15
|
+
return left[index] < right[index] ? -1 : 1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
/** Evalúa un rango de comparadores separados por espacios: `>=1.2.3 <1.3.0`. */
|
|
21
|
+
export function satisfiesRange(version, range) {
|
|
22
|
+
const parsed = parseVersion(version);
|
|
23
|
+
if (parsed === null) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const comparators = range.trim().split(/\s+/).filter((item) => item !== '');
|
|
27
|
+
if (comparators.length === 0) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return comparators.every((comparator) => {
|
|
31
|
+
const match = /^(>=|<=|>|<|=)?(\d+\.\d+\.\d+)$/.exec(comparator);
|
|
32
|
+
if (match === null) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const bound = parseVersion(match[2]);
|
|
36
|
+
if (bound === null) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const order = compareVersions(parsed, bound);
|
|
40
|
+
switch (match[1] ?? '=') {
|
|
41
|
+
case '>=':
|
|
42
|
+
return order >= 0;
|
|
43
|
+
case '<=':
|
|
44
|
+
return order <= 0;
|
|
45
|
+
case '>':
|
|
46
|
+
return order > 0;
|
|
47
|
+
case '<':
|
|
48
|
+
return order < 0;
|
|
49
|
+
default:
|
|
50
|
+
return order === 0;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/sync/config.js
CHANGED
|
@@ -53,8 +53,9 @@ export async function loadSyncConfig(root) {
|
|
|
53
53
|
const spec = TARGETS[tool];
|
|
54
54
|
ruleTargets[tool] = { path: spec.rules, shape: spec.ruleShape };
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Los 17 comandos los provee Spec Kit en cada destino; AI-Hub no proyecta sus cuerpos.
|
|
57
|
+
void engine;
|
|
58
|
+
const sources = [];
|
|
58
59
|
if (manifest.skills.publish.length > 0) {
|
|
59
60
|
sources.push({ source: manifest.skills.source, publish: manifest.skills.publish });
|
|
60
61
|
}
|