@hydranium/cli 1.0.0-next.412 → 1.0.0-next.416
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 +5 -0
- package/lib/cli.js +2 -0
- package/lib/cli.js.map +1 -1
- package/lib/commands/check-skills-args.d.ts +11 -0
- package/lib/commands/check-skills-args.d.ts.map +1 -0
- package/lib/commands/check-skills-args.js +71 -0
- package/lib/commands/check-skills-args.js.map +1 -0
- package/lib/commands/generate-ast-builder.d.ts +1 -1
- package/lib/commands/generate-ast-builder.js +1 -1
- package/lib/commands/init-args.d.ts +3 -18
- package/lib/commands/init-args.d.ts.map +1 -1
- package/lib/commands/init-args.js +62 -9
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-skills.d.ts +41 -0
- package/lib/commands/init-skills.d.ts.map +1 -0
- package/lib/commands/init-skills.js +110 -0
- package/lib/commands/init-skills.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +7 -0
- package/lib/commands/init-wizard.d.ts.map +1 -1
- package/lib/commands/init-wizard.js +32 -2
- package/lib/commands/init-wizard.js.map +1 -1
- package/lib/commands/init.d.ts +6 -5
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +43 -6
- package/lib/commands/init.js.map +1 -1
- package/lib/commands/skills.d.ts +22 -0
- package/lib/commands/skills.d.ts.map +1 -0
- package/lib/commands/skills.js +129 -0
- package/lib/commands/skills.js.map +1 -0
- package/package.json +26 -20
- package/src/cli.ts +2 -0
- package/src/commands/check-skills-args.ts +70 -0
- package/src/commands/generate-ast-builder.ts +1 -1
- package/src/commands/init-args.ts +64 -9
- package/src/commands/init-skills.ts +129 -0
- package/src/commands/init-wizard.ts +44 -3
- package/src/commands/init.ts +50 -6
- package/src/commands/skills.ts +146 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
+
import { resolve } from 'import-meta-resolve';
|
|
15
|
+
import { parse } from 'yaml';
|
|
16
|
+
|
|
17
|
+
/** A compatibility result applies to one installed copy, not just its skill name. */
|
|
18
|
+
export interface SkillCompatibility {
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly status: 'compatible' | 'incompatible' | 'unverified';
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SKILL_DIRECTORIES = ['.agents/skills', '.claude/skills'] as const;
|
|
25
|
+
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Resolve from the adopter, never from a globally installed CLI's dependencies. */
|
|
31
|
+
export function installedPackageManifest(name: string, from: string): Record<string, unknown> | undefined {
|
|
32
|
+
const parent = path.join(path.resolve(from), 'package.json');
|
|
33
|
+
let manifest: string;
|
|
34
|
+
try {
|
|
35
|
+
manifest = createRequire(parent).resolve(`${name}/package.json`);
|
|
36
|
+
} catch {
|
|
37
|
+
try {
|
|
38
|
+
let directory = path.dirname(fileURLToPath(resolve(name, pathToFileURL(parent).href)));
|
|
39
|
+
while (true) {
|
|
40
|
+
const candidate = path.join(directory, 'package.json');
|
|
41
|
+
if (fs.existsSync(candidate)) {
|
|
42
|
+
const value: unknown = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
43
|
+
if (isRecord(value) && value.name === name) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const next = path.dirname(directory);
|
|
48
|
+
if (next === directory) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
directory = next;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const value: unknown = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
58
|
+
return isRecord(value) && value.name === name ? value : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Missing or malformed requirements never constitute evidence of compatibility. */
|
|
62
|
+
export function checkSkill(file: string, from: string): SkillCompatibility {
|
|
63
|
+
const result = (status: SkillCompatibility['status'], detail: string): SkillCompatibility => ({ file, status, detail });
|
|
64
|
+
try {
|
|
65
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
66
|
+
const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
67
|
+
const skill: unknown = frontmatter === undefined ? undefined : parse(frontmatter);
|
|
68
|
+
const metadata = isRecord(skill) ? skill.metadata : undefined;
|
|
69
|
+
if (!isRecord(metadata) || metadata['hydranium-compatibility-schema'] !== '1') {
|
|
70
|
+
return result('unverified', 'No supported Hydranium compatibility metadata.');
|
|
71
|
+
}
|
|
72
|
+
const encoded = metadata['hydranium-contracts'];
|
|
73
|
+
const contracts: unknown = typeof encoded === 'string' ? JSON.parse(encoded) : undefined;
|
|
74
|
+
if (
|
|
75
|
+
!isRecord(contracts) ||
|
|
76
|
+
Object.keys(contracts).length === 0 ||
|
|
77
|
+
Object.entries(contracts).some(
|
|
78
|
+
([name, declarations]) =>
|
|
79
|
+
!/^(@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(name) ||
|
|
80
|
+
!isRecord(declarations) ||
|
|
81
|
+
Object.keys(declarations).length === 0 ||
|
|
82
|
+
Object.entries(declarations).some(
|
|
83
|
+
([contract, revision]) => !contract.trim() || typeof revision !== 'string' || !revision.trim()
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
return result('unverified', 'Invalid package contract requirements.');
|
|
88
|
+
}
|
|
89
|
+
const problems: string[] = [];
|
|
90
|
+
const versions: string[] = [];
|
|
91
|
+
let mismatch = false;
|
|
92
|
+
for (const [name, requiredContracts] of Object.entries(contracts)) {
|
|
93
|
+
const manifest = installedPackageManifest(name, from);
|
|
94
|
+
const version = manifest?.version;
|
|
95
|
+
const configuration = manifest?.hydranium;
|
|
96
|
+
const suppliedContracts = isRecord(configuration) ? configuration.skillContracts : undefined;
|
|
97
|
+
const contractMatches =
|
|
98
|
+
isRecord(requiredContracts) &&
|
|
99
|
+
isRecord(suppliedContracts) &&
|
|
100
|
+
Object.entries(requiredContracts).every(
|
|
101
|
+
([contract, revision]) => Object.hasOwn(suppliedContracts, contract) && suppliedContracts[contract] === revision
|
|
102
|
+
);
|
|
103
|
+
if (manifest === undefined) {
|
|
104
|
+
problems.push(`${name} is not installed; install dependencies and retry.`);
|
|
105
|
+
} else if (!contractMatches) {
|
|
106
|
+
mismatch = true;
|
|
107
|
+
problems.push(`${name} does not declare the required skill contracts.`);
|
|
108
|
+
} else {
|
|
109
|
+
versions.push(`${name}${typeof version === 'string' ? `@${version}` : ''} (declared skill contract)`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return problems.length > 0
|
|
113
|
+
? result(mismatch ? 'incompatible' : 'unverified', problems.join(' '))
|
|
114
|
+
: result('compatible', `Declared requirements satisfied by ${versions.join(', ')}.`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return result('unverified', error instanceof Error ? error.message : String(error));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Check every selected copy so an outdated agent-specific copy cannot hide behind a shared one. */
|
|
121
|
+
export function checkSkills(project: string, from = project, names?: readonly string[]): SkillCompatibility[] {
|
|
122
|
+
const results: SkillCompatibility[] = [];
|
|
123
|
+
const found = new Set<string>();
|
|
124
|
+
for (const directory of SKILL_DIRECTORIES) {
|
|
125
|
+
const root = path.join(project, directory);
|
|
126
|
+
if (!fs.existsSync(root)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const name of fs.readdirSync(root)) {
|
|
130
|
+
if (!(names ? names.includes(name) : name.startsWith('hydranium-'))) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
found.add(name);
|
|
134
|
+
results.push(checkSkill(path.join(root, name, 'SKILL.md'), from));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const name of names ?? []) {
|
|
138
|
+
if (!found.has(name)) {
|
|
139
|
+
results.push({ file: name, status: 'unverified', detail: 'Skill is not installed in a checked project directory.' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (results.length === 0) {
|
|
143
|
+
results.push({ file: project, status: 'unverified', detail: 'No Hydranium project skills found.' });
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
}
|