@skillmarkdown/cli 0.2.0 → 0.2.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/README.md +33 -1
- package/dist/cli.js +5 -1
- package/dist/commands/init.js +37 -6
- package/dist/commands/login.js +37 -11
- package/dist/commands/logout.js +13 -6
- package/dist/commands/validate.js +52 -45
- package/dist/lib/auth-config.js +3 -1
- package/dist/lib/auth-defaults.js +1 -0
- package/dist/lib/auth-session.js +6 -0
- package/dist/lib/cli-text.js +1 -1
- package/dist/lib/firebase-auth.js +3 -1
- package/dist/lib/scaffold.js +53 -8
- package/dist/lib/skill-spec.js +2 -1
- package/dist/lib/templates.js +185 -3
- package/dist/lib/upstream-validator.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,31 @@ npx @skillmarkdown/cli init
|
|
|
28
28
|
skillmd init
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Default `init` creates the minimal filesystem scaffold:
|
|
32
|
+
|
|
33
|
+
- `SKILL.md` only
|
|
34
|
+
|
|
35
|
+
and runs spec validation.
|
|
36
|
+
|
|
37
|
+
To scaffold the full verbose template:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
skillmd init --template verbose
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `verbose` template includes `SKILL.md`, `.gitignore`, and optional directories (`scripts/`, `references/`, `assets/`) with starter placeholder content and `.gitkeep`, then runs strict validation.
|
|
44
|
+
|
|
45
|
+
Included starter files:
|
|
46
|
+
|
|
47
|
+
- `scripts/README.md`
|
|
48
|
+
- `scripts/extract.py`
|
|
49
|
+
- `references/REFERENCE.md`
|
|
50
|
+
- `references/FORMS.md`
|
|
51
|
+
- `assets/README.md`
|
|
52
|
+
- `assets/report-template.md`
|
|
53
|
+
- `assets/lookup-table.csv`
|
|
54
|
+
|
|
55
|
+
`SKILL.md` content is the same across `minimal` and `verbose` templates. Template selection only changes extra scaffold files/directories around `SKILL.md`.
|
|
32
56
|
|
|
33
57
|
To skip validation during init:
|
|
34
58
|
|
|
@@ -36,6 +60,11 @@ To skip validation during init:
|
|
|
36
60
|
skillmd init --no-validate
|
|
37
61
|
```
|
|
38
62
|
|
|
63
|
+
Supported templates:
|
|
64
|
+
|
|
65
|
+
- `minimal` (default)
|
|
66
|
+
- `verbose`
|
|
67
|
+
|
|
39
68
|
### Validate a skill folder
|
|
40
69
|
|
|
41
70
|
```bash
|
|
@@ -74,6 +103,7 @@ By default, `login` uses the project’s built-in development config. You can ov
|
|
|
74
103
|
|
|
75
104
|
- `SKILLMD_GITHUB_CLIENT_ID`
|
|
76
105
|
- `SKILLMD_FIREBASE_API_KEY`
|
|
106
|
+
- `SKILLMD_FIREBASE_PROJECT_ID`
|
|
77
107
|
|
|
78
108
|
See `.env.example` for the expected keys.
|
|
79
109
|
Maintainers: built-in defaults are defined in `src/lib/auth-defaults.ts`.
|
|
@@ -94,6 +124,8 @@ skillmd login --reauth
|
|
|
94
124
|
skillmd logout
|
|
95
125
|
```
|
|
96
126
|
|
|
127
|
+
`skillmd login --status` includes the authenticated Firebase project so you can confirm whether the active session is for `skillmarkdown` or `skillmarkdown-development`.
|
|
128
|
+
|
|
97
129
|
When a saved session exists, `skillmd login` verifies the stored refresh token. If it is invalid/expired, the CLI automatically starts a new login flow. If verification is inconclusive (for example network timeout), the command exits non-zero and keeps the current session.
|
|
98
130
|
|
|
99
131
|
## Development
|
package/dist/cli.js
CHANGED
|
@@ -30,4 +30,8 @@ async function main() {
|
|
|
30
30
|
console.error(cli_text_1.ROOT_USAGE);
|
|
31
31
|
process.exitCode = 1;
|
|
32
32
|
}
|
|
33
|
-
void main()
|
|
33
|
+
void main().catch((error) => {
|
|
34
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
35
|
+
console.error(`skillmd: ${message}`);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
});
|
package/dist/commands/init.js
CHANGED
|
@@ -5,22 +5,53 @@ const cli_text_1 = require("../lib/cli-text");
|
|
|
5
5
|
const command_output_1 = require("../lib/command-output");
|
|
6
6
|
const scaffold_1 = require("../lib/scaffold");
|
|
7
7
|
const validator_1 = require("../lib/validator");
|
|
8
|
+
function parseInitArgs(args) {
|
|
9
|
+
let skipValidation = false;
|
|
10
|
+
let template = "minimal";
|
|
11
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
12
|
+
const arg = args[index];
|
|
13
|
+
if (arg === "--no-validate") {
|
|
14
|
+
skipValidation = true;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (arg === "--template") {
|
|
18
|
+
const nextValue = args[index + 1];
|
|
19
|
+
const resolved = nextValue ? (0, scaffold_1.resolveInitTemplateId)(nextValue) : null;
|
|
20
|
+
if (!resolved) {
|
|
21
|
+
return { skipValidation: false, template: "minimal", valid: false };
|
|
22
|
+
}
|
|
23
|
+
template = resolved;
|
|
24
|
+
index += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (arg.startsWith("--template=")) {
|
|
28
|
+
const value = arg.slice("--template=".length);
|
|
29
|
+
const resolved = (0, scaffold_1.resolveInitTemplateId)(value);
|
|
30
|
+
if (!resolved) {
|
|
31
|
+
return { skipValidation: false, template: "minimal", valid: false };
|
|
32
|
+
}
|
|
33
|
+
template = resolved;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
return { skipValidation: false, template: "minimal", valid: false };
|
|
37
|
+
}
|
|
38
|
+
return { skipValidation, template, valid: true };
|
|
39
|
+
}
|
|
8
40
|
function runInitCommand(args, options = {}) {
|
|
9
41
|
const cwd = options.cwd ?? process.cwd();
|
|
10
|
-
const validateSkillFn = options.validateSkill ??
|
|
11
|
-
const skipValidation = args
|
|
12
|
-
|
|
13
|
-
if (hasUnsupportedArgs) {
|
|
42
|
+
const validateSkillFn = options.validateSkill ?? validator_1.validateSkill;
|
|
43
|
+
const { skipValidation, template, valid } = parseInitArgs(args);
|
|
44
|
+
if (!valid) {
|
|
14
45
|
return (0, command_output_1.failWithUsage)("skillmd init: unsupported argument(s)", cli_text_1.INIT_USAGE);
|
|
15
46
|
}
|
|
16
47
|
try {
|
|
17
|
-
const result = (0, scaffold_1.scaffoldSkillInDirectory)(cwd);
|
|
48
|
+
const result = (0, scaffold_1.scaffoldSkillInDirectory)(cwd, { template });
|
|
18
49
|
console.log(`Initialized skill '${result.skillName}'.`);
|
|
19
50
|
if (skipValidation) {
|
|
20
51
|
console.log("Validation skipped (--no-validate).");
|
|
21
52
|
return 0;
|
|
22
53
|
}
|
|
23
|
-
const validation = validateSkillFn(cwd);
|
|
54
|
+
const validation = validateSkillFn(cwd, { strict: result.template === "verbose" });
|
|
24
55
|
(0, command_output_1.printValidationResult)(validation);
|
|
25
56
|
if (validation.status === "passed") {
|
|
26
57
|
return 0;
|
package/dist/commands/login.js
CHANGED
|
@@ -26,16 +26,34 @@ function parseFlags(args) {
|
|
|
26
26
|
}
|
|
27
27
|
return { status, reauth, valid: true };
|
|
28
28
|
}
|
|
29
|
-
function
|
|
29
|
+
function formatSessionProject(session, currentConfigProjectId) {
|
|
30
|
+
if (!session.projectId) {
|
|
31
|
+
if (currentConfigProjectId) {
|
|
32
|
+
return { label: `unknown (current config: ${currentConfigProjectId})`, mismatch: false };
|
|
33
|
+
}
|
|
34
|
+
return { label: "unknown", mismatch: false };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
label: session.projectId,
|
|
38
|
+
mismatch: Boolean(currentConfigProjectId && session.projectId !== currentConfigProjectId),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function printSessionStatus(session, currentConfigProjectId) {
|
|
30
42
|
if (!session) {
|
|
31
43
|
console.log("Not logged in.");
|
|
32
44
|
return 1;
|
|
33
45
|
}
|
|
46
|
+
const project = formatSessionProject(session, currentConfigProjectId);
|
|
34
47
|
if (session.email) {
|
|
35
|
-
console.log(`Logged in with GitHub as ${session.email}.`);
|
|
36
|
-
|
|
48
|
+
console.log(`Logged in with GitHub as ${session.email} (project: ${project.label}).`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(`Logged in with GitHub (uid: ${session.uid}, project: ${project.label}).`);
|
|
52
|
+
}
|
|
53
|
+
if (project.mismatch && currentConfigProjectId) {
|
|
54
|
+
console.log(`Current CLI config targets project '${currentConfigProjectId}'. ` +
|
|
55
|
+
"Run 'skillmd login --reauth' to switch projects.");
|
|
37
56
|
}
|
|
38
|
-
console.log(`Logged in with GitHub (uid: ${session.uid}).`);
|
|
39
57
|
return 0;
|
|
40
58
|
}
|
|
41
59
|
function requireConfig(env) {
|
|
@@ -57,22 +75,29 @@ async function runLoginCommand(args, options = {}) {
|
|
|
57
75
|
const readSessionFn = options.readSession ?? auth_session_1.readAuthSession;
|
|
58
76
|
const writeSessionFn = options.writeSession ?? auth_session_1.writeAuthSession;
|
|
59
77
|
const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
|
|
60
|
-
if (status) {
|
|
61
|
-
return printSessionStatus(readSessionFn());
|
|
62
|
-
}
|
|
63
78
|
try {
|
|
64
79
|
const config = requireConfig(options.env ?? process.env);
|
|
80
|
+
if (status) {
|
|
81
|
+
return printSessionStatus(readSessionFn(), config.firebaseProjectId);
|
|
82
|
+
}
|
|
65
83
|
const existingSession = readSessionFn();
|
|
66
84
|
if (existingSession && !reauth) {
|
|
67
85
|
const verifyRefreshTokenFn = options.verifyRefreshToken ?? firebase_auth_1.verifyFirebaseRefreshToken;
|
|
68
86
|
try {
|
|
69
87
|
const validation = await verifyRefreshTokenFn(config.firebaseApiKey, existingSession.refreshToken);
|
|
70
88
|
if (validation.valid) {
|
|
89
|
+
const project = formatSessionProject(existingSession, config.firebaseProjectId);
|
|
71
90
|
if (existingSession.email) {
|
|
72
|
-
console.log(`Already logged in as ${existingSession.email}
|
|
91
|
+
console.log(`Already logged in as ${existingSession.email} (project: ${project.label}). ` +
|
|
92
|
+
"Run 'skillmd logout' first.");
|
|
73
93
|
}
|
|
74
94
|
else {
|
|
75
|
-
console.log(
|
|
95
|
+
console.log(`Already logged in (uid: ${existingSession.uid}, project: ${project.label}). ` +
|
|
96
|
+
"Run 'skillmd logout' first.");
|
|
97
|
+
}
|
|
98
|
+
if (project.mismatch) {
|
|
99
|
+
console.log(`Current CLI config targets project '${config.firebaseProjectId}'. ` +
|
|
100
|
+
"Run 'skillmd login --reauth' to switch projects.");
|
|
76
101
|
}
|
|
77
102
|
return 0;
|
|
78
103
|
}
|
|
@@ -100,12 +125,13 @@ async function runLoginCommand(args, options = {}) {
|
|
|
100
125
|
uid: firebaseSession.localId,
|
|
101
126
|
email: firebaseSession.email,
|
|
102
127
|
refreshToken: firebaseSession.refreshToken,
|
|
128
|
+
projectId: config.firebaseProjectId,
|
|
103
129
|
});
|
|
104
130
|
if (firebaseSession.email) {
|
|
105
|
-
console.log(`Login successful. Signed in as ${firebaseSession.email}.`);
|
|
131
|
+
console.log(`Login successful. Signed in as ${firebaseSession.email} (project: ${config.firebaseProjectId}).`);
|
|
106
132
|
}
|
|
107
133
|
else {
|
|
108
|
-
console.log(
|
|
134
|
+
console.log(`Login successful (project: ${config.firebaseProjectId}).`);
|
|
109
135
|
}
|
|
110
136
|
return 0;
|
|
111
137
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -8,12 +8,19 @@ function runLogoutCommand(args, options = {}) {
|
|
|
8
8
|
if (args.length > 0) {
|
|
9
9
|
return (0, command_output_1.failWithUsage)("skillmd logout: unsupported argument(s)", cli_text_1.LOGOUT_USAGE);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
try {
|
|
12
|
+
const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
|
|
13
|
+
const removed = clearSessionFn();
|
|
14
|
+
if (removed) {
|
|
15
|
+
console.log("Logged out.");
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
console.log("No active session to log out.");
|
|
15
19
|
return 0;
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
23
|
+
console.error(`skillmd logout: ${message}`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
19
26
|
}
|
|
@@ -7,56 +7,63 @@ const command_output_1 = require("../lib/command-output");
|
|
|
7
7
|
const upstream_validator_1 = require("../lib/upstream-validator");
|
|
8
8
|
const validator_1 = require("../lib/validator");
|
|
9
9
|
function runValidateCommand(args, options = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
strict
|
|
19
|
-
|
|
10
|
+
try {
|
|
11
|
+
const cwd = options.cwd ?? process.cwd();
|
|
12
|
+
const validateLocal = options.validateLocal ?? validator_1.validateSkill;
|
|
13
|
+
const validateUpstream = options.validateUpstream ?? upstream_validator_1.validateWithSkillsRef;
|
|
14
|
+
let strict = false;
|
|
15
|
+
let parity = false;
|
|
16
|
+
let pathArg;
|
|
17
|
+
for (const arg of args) {
|
|
18
|
+
if (arg === "--strict") {
|
|
19
|
+
strict = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg === "--parity") {
|
|
23
|
+
parity = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg.startsWith("-")) {
|
|
27
|
+
return (0, command_output_1.failWithUsage)(`skillmd validate: unsupported flag '${arg}'`, cli_text_1.VALIDATE_USAGE);
|
|
28
|
+
}
|
|
29
|
+
if (pathArg) {
|
|
30
|
+
return (0, command_output_1.failWithUsage)("skillmd validate: accepts at most one path argument", cli_text_1.VALIDATE_USAGE);
|
|
31
|
+
}
|
|
32
|
+
pathArg = arg;
|
|
20
33
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.error(`Validation parity unavailable: ${upstream.message}. Install skills-ref to use --parity.`);
|
|
39
|
-
return 1;
|
|
40
|
-
}
|
|
41
|
-
if (validation.status === "passed" && upstream.status !== "passed") {
|
|
42
|
-
console.error("Validation parity mismatch: local validation passed but skills-ref failed.");
|
|
43
|
-
console.error(`skills-ref: ${upstream.message}`);
|
|
44
|
-
return 1;
|
|
34
|
+
const targetDir = pathArg ? (0, node_path_1.resolve)(cwd, pathArg) : cwd;
|
|
35
|
+
const validation = validateLocal(targetDir, { strict });
|
|
36
|
+
if (parity) {
|
|
37
|
+
const upstream = validateUpstream(targetDir);
|
|
38
|
+
if (upstream.status === "unavailable") {
|
|
39
|
+
console.error(`Validation parity unavailable: ${upstream.message}. Install skills-ref to use --parity.`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
if (validation.status === "passed" && upstream.status !== "passed") {
|
|
43
|
+
console.error("Validation parity mismatch: local validation passed but skills-ref failed.");
|
|
44
|
+
console.error(`skills-ref: ${upstream.message}`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (validation.status === "failed" && upstream.status !== "failed") {
|
|
48
|
+
console.error("Validation parity mismatch: local validation failed but skills-ref passed.");
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
(0, command_output_1.printValidationResult)(validation);
|
|
53
|
+
if (validation.status === "passed") {
|
|
54
|
+
if (parity) {
|
|
55
|
+
console.log("Validation parity passed (skills-ref).");
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
49
58
|
}
|
|
50
|
-
}
|
|
51
|
-
(0, command_output_1.printValidationResult)(validation);
|
|
52
|
-
if (validation.status === "passed") {
|
|
53
59
|
if (parity) {
|
|
54
|
-
console.
|
|
60
|
+
console.error("Validation parity matched (skills-ref also failed).");
|
|
55
61
|
}
|
|
56
|
-
return
|
|
62
|
+
return 1;
|
|
57
63
|
}
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
66
|
+
console.error(`skillmd validate: ${message}`);
|
|
67
|
+
return 1;
|
|
60
68
|
}
|
|
61
|
-
return 1;
|
|
62
69
|
}
|
package/dist/lib/auth-config.js
CHANGED
|
@@ -54,12 +54,14 @@ function getLoginEnvConfig(env = process.env, options = {}) {
|
|
|
54
54
|
const dotEnv = loadDotEnv(getDefaultUserEnvPath(options));
|
|
55
55
|
const githubClientId = pickValue(env.SKILLMD_GITHUB_CLIENT_ID, dotEnv.SKILLMD_GITHUB_CLIENT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.githubClientId);
|
|
56
56
|
const firebaseApiKey = pickValue(env.SKILLMD_FIREBASE_API_KEY, dotEnv.SKILLMD_FIREBASE_API_KEY, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.firebaseApiKey);
|
|
57
|
-
|
|
57
|
+
const firebaseProjectId = pickValue(env.SKILLMD_FIREBASE_PROJECT_ID, dotEnv.SKILLMD_FIREBASE_PROJECT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.firebaseProjectId);
|
|
58
|
+
if (!githubClientId || !firebaseApiKey || !firebaseProjectId) {
|
|
58
59
|
throw new Error("missing login configuration");
|
|
59
60
|
}
|
|
60
61
|
return {
|
|
61
62
|
githubClientId,
|
|
62
63
|
firebaseApiKey,
|
|
64
|
+
firebaseProjectId,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
function getDefaultUserEnvPath(options = {}) {
|
package/dist/lib/auth-session.js
CHANGED
|
@@ -28,6 +28,12 @@ function readAuthSession(sessionPath = SESSION_PATH) {
|
|
|
28
28
|
if (parsed.email !== undefined && typeof parsed.email !== "string") {
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
|
+
if (parsed.projectId !== undefined && typeof parsed.projectId !== "string") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof parsed.projectId === "string" && parsed.projectId.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
31
37
|
return parsed;
|
|
32
38
|
}
|
|
33
39
|
catch {
|
package/dist/lib/cli-text.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LOGOUT_USAGE = exports.LOGIN_USAGE = exports.VALIDATE_USAGE = exports.INIT_USAGE = exports.ROOT_USAGE = void 0;
|
|
4
4
|
exports.ROOT_USAGE = "Usage: skillmd <init|validate|login|logout>";
|
|
5
|
-
exports.INIT_USAGE = "Usage: skillmd init [--no-validate]";
|
|
5
|
+
exports.INIT_USAGE = "Usage: skillmd init [--no-validate] [--template <minimal|verbose>]";
|
|
6
6
|
exports.VALIDATE_USAGE = "Usage: skillmd validate [path] [--strict] [--parity]";
|
|
7
7
|
exports.LOGIN_USAGE = "Usage: skillmd login [--status|--reauth]";
|
|
8
8
|
exports.LOGOUT_USAGE = "Usage: skillmd logout";
|
|
@@ -60,7 +60,9 @@ async function verifyFirebaseRefreshToken(apiKey, refreshToken) {
|
|
|
60
60
|
}
|
|
61
61
|
const errorMessage = payload.error?.message ?? "";
|
|
62
62
|
if (response.status === 400 &&
|
|
63
|
-
(errorMessage === "INVALID_REFRESH_TOKEN" ||
|
|
63
|
+
(errorMessage === "INVALID_REFRESH_TOKEN" ||
|
|
64
|
+
errorMessage === "TOKEN_EXPIRED" ||
|
|
65
|
+
errorMessage === "PROJECT_NUMBER_MISMATCH")) {
|
|
64
66
|
return { valid: false };
|
|
65
67
|
}
|
|
66
68
|
throw new Error(`Firebase token verification failed (${response.status}): ${errorMessage || "unknown error"}`);
|
package/dist/lib/scaffold.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isInitTemplateId = isInitTemplateId;
|
|
4
|
+
exports.resolveInitTemplateId = resolveInitTemplateId;
|
|
3
5
|
exports.scaffoldSkillInDirectory = scaffoldSkillInDirectory;
|
|
4
6
|
const node_fs_1 = require("node:fs");
|
|
5
7
|
const node_path_1 = require("node:path");
|
|
@@ -20,15 +22,58 @@ function assertDirectoryNameMatchesNormalized(targetDir) {
|
|
|
20
22
|
}
|
|
21
23
|
return normalizedName;
|
|
22
24
|
}
|
|
23
|
-
function
|
|
25
|
+
function isInitTemplateId(value) {
|
|
26
|
+
return skill_spec_1.INIT_TEMPLATE_IDS.includes(value);
|
|
27
|
+
}
|
|
28
|
+
function resolveInitTemplateId(value) {
|
|
29
|
+
return isInitTemplateId(value) ? value : null;
|
|
30
|
+
}
|
|
31
|
+
function createDirectory(path, createdPaths) {
|
|
32
|
+
(0, node_fs_1.mkdirSync)(path, { recursive: false, mode: 0o755 });
|
|
33
|
+
createdPaths.push({ kind: "dir", path });
|
|
34
|
+
}
|
|
35
|
+
function createFile(path, content, createdPaths) {
|
|
36
|
+
(0, node_fs_1.writeFileSync)(path, content, { encoding: "utf8", flag: "wx", mode: 0o644 });
|
|
37
|
+
(0, node_fs_1.chmodSync)(path, 0o644);
|
|
38
|
+
createdPaths.push({ kind: "file", path });
|
|
39
|
+
}
|
|
40
|
+
function rollbackCreatedPaths(createdPaths) {
|
|
41
|
+
for (const entry of [...createdPaths].reverse()) {
|
|
42
|
+
if (entry.kind === "file") {
|
|
43
|
+
(0, node_fs_1.rmSync)(entry.path, { force: true });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
(0, node_fs_1.rmSync)(entry.path, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function scaffoldSkillInDirectory(targetDir, options = {}) {
|
|
50
|
+
const template = options.template ?? "minimal";
|
|
24
51
|
const skillName = assertDirectoryNameMatchesNormalized(targetDir);
|
|
25
52
|
assertDirectoryEmpty(targetDir);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
(
|
|
29
|
-
|
|
53
|
+
const createdPaths = [];
|
|
54
|
+
try {
|
|
55
|
+
if (template === "verbose") {
|
|
56
|
+
for (const directory of skill_spec_1.SCAFFOLD_DIRECTORIES) {
|
|
57
|
+
const fullPath = (0, node_path_1.join)(targetDir, directory);
|
|
58
|
+
createDirectory(fullPath, createdPaths);
|
|
59
|
+
createFile((0, node_path_1.join)(fullPath, ".gitkeep"), "", createdPaths);
|
|
60
|
+
}
|
|
61
|
+
createFile((0, node_path_1.join)(targetDir, ".gitignore"), (0, templates_1.buildGitignore)(), createdPaths);
|
|
62
|
+
createFile((0, node_path_1.join)(targetDir, "SKILL.md"), (0, templates_1.buildVerboseSkillMarkdown)(skillName), createdPaths);
|
|
63
|
+
createFile((0, node_path_1.join)(targetDir, "scripts", "README.md"), (0, templates_1.buildScriptsReadme)(), createdPaths);
|
|
64
|
+
createFile((0, node_path_1.join)(targetDir, "scripts", "extract.py"), (0, templates_1.buildExtractScriptPython)(), createdPaths);
|
|
65
|
+
createFile((0, node_path_1.join)(targetDir, "references", "REFERENCE.md"), (0, templates_1.buildReferenceGuide)(), createdPaths);
|
|
66
|
+
createFile((0, node_path_1.join)(targetDir, "references", "FORMS.md"), (0, templates_1.buildFormsReference)(), createdPaths);
|
|
67
|
+
createFile((0, node_path_1.join)(targetDir, "assets", "README.md"), (0, templates_1.buildAssetsReadme)(), createdPaths);
|
|
68
|
+
createFile((0, node_path_1.join)(targetDir, "assets", "report-template.md"), (0, templates_1.buildReportTemplate)(), createdPaths);
|
|
69
|
+
createFile((0, node_path_1.join)(targetDir, "assets", "lookup-table.csv"), (0, templates_1.buildLookupTableCsv)(), createdPaths);
|
|
70
|
+
return { skillName, template };
|
|
71
|
+
}
|
|
72
|
+
createFile((0, node_path_1.join)(targetDir, "SKILL.md"), (0, templates_1.buildMinimalSkillMarkdown)(skillName), createdPaths);
|
|
73
|
+
return { skillName, template };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
rollbackCreatedPaths(createdPaths);
|
|
77
|
+
throw error;
|
|
30
78
|
}
|
|
31
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(targetDir, "SKILL.md"), (0, templates_1.buildSkillMarkdown)(skillName), "utf8");
|
|
32
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(targetDir, ".gitignore"), (0, templates_1.buildGitignore)(), "utf8");
|
|
33
|
-
return { skillName };
|
|
34
79
|
}
|
package/dist/lib/skill-spec.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.STRICT_SECTION_HEADINGS = exports.STRICT_SECTION_TITLES = exports.STRICT_REQUIRED_FILES = exports.SCAFFOLD_DIRECTORIES = exports.MAX_SKILL_NAME_LENGTH = void 0;
|
|
3
|
+
exports.STRICT_SECTION_HEADINGS = exports.STRICT_SECTION_TITLES = exports.STRICT_REQUIRED_FILES = exports.SCAFFOLD_DIRECTORIES = exports.INIT_TEMPLATE_IDS = exports.MAX_SKILL_NAME_LENGTH = void 0;
|
|
4
4
|
exports.MAX_SKILL_NAME_LENGTH = 64;
|
|
5
|
+
exports.INIT_TEMPLATE_IDS = ["minimal", "verbose"];
|
|
5
6
|
exports.SCAFFOLD_DIRECTORIES = ["scripts", "references", "assets"];
|
|
6
7
|
exports.STRICT_REQUIRED_FILES = [
|
|
7
8
|
".gitignore",
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildSkillMarkdown = buildSkillMarkdown;
|
|
4
|
+
exports.buildMinimalSkillMarkdown = buildMinimalSkillMarkdown;
|
|
5
|
+
exports.buildVerboseSkillMarkdown = buildVerboseSkillMarkdown;
|
|
4
6
|
exports.buildGitignore = buildGitignore;
|
|
7
|
+
exports.buildScriptsReadme = buildScriptsReadme;
|
|
8
|
+
exports.buildExtractScriptPython = buildExtractScriptPython;
|
|
9
|
+
exports.buildReferenceGuide = buildReferenceGuide;
|
|
10
|
+
exports.buildFormsReference = buildFormsReference;
|
|
11
|
+
exports.buildAssetsReadme = buildAssetsReadme;
|
|
12
|
+
exports.buildReportTemplate = buildReportTemplate;
|
|
13
|
+
exports.buildLookupTableCsv = buildLookupTableCsv;
|
|
5
14
|
const skill_spec_1 = require("./skill-spec");
|
|
15
|
+
const SECTION_PLACEHOLDERS = {
|
|
16
|
+
Scope: "Define the boundaries of this skill: what it should and should not handle.",
|
|
17
|
+
"When to use": "Describe the signals or request patterns that should trigger this skill. Keep SKILL.md focused and move deep details into references/ files.",
|
|
18
|
+
Inputs: "List required and optional inputs, expected formats, and assumptions. Add structured templates to references/FORMS.md when helpful.",
|
|
19
|
+
Outputs: "Describe expected outputs, side effects, and completion criteria. Use assets/report-template.md when a fixed output structure is useful.",
|
|
20
|
+
"Steps / Procedure": "Provide ordered steps the agent should follow, including key decision points. See [the reference guide](references/REFERENCE.md) for detailed rules. Run helper scripts such as scripts/extract.py when needed.",
|
|
21
|
+
Examples: "Add one or two realistic examples of inputs and expected outputs.",
|
|
22
|
+
"Limitations / Failure modes": "Document known limitations, failure cases, and recommended recovery actions.",
|
|
23
|
+
"Security / Tool access": "State required tools/permissions and any security constraints. If needed, set frontmatter allowed-tools to pre-approve tool usage.",
|
|
24
|
+
};
|
|
6
25
|
function buildSkillMarkdown(name) {
|
|
7
|
-
const sections = skill_spec_1.STRICT_SECTION_TITLES.map((title) => `## ${title}\
|
|
26
|
+
const sections = skill_spec_1.STRICT_SECTION_TITLES.map((title) => `## ${title}\n\n${SECTION_PLACEHOLDERS[title]}`).join("\n\n");
|
|
8
27
|
return `---
|
|
9
28
|
name: ${name}
|
|
10
|
-
description:
|
|
11
|
-
|
|
29
|
+
description: Explain what this skill does and when an agent should use it.
|
|
30
|
+
# compatibility: "Optional: environment requirements (products, packages, network access)."
|
|
31
|
+
# metadata:
|
|
32
|
+
# author: "your-org"
|
|
33
|
+
# version: "1.0.0"
|
|
34
|
+
# allowed-tools: "Optional: space-delimited pre-approved tools."
|
|
35
|
+
license: Optional. Add a license name or reference to a bundled license file.
|
|
12
36
|
---
|
|
13
37
|
|
|
14
38
|
${sections}
|
|
15
39
|
`;
|
|
16
40
|
}
|
|
41
|
+
function buildMinimalSkillMarkdown(name) {
|
|
42
|
+
return buildSkillMarkdown(name);
|
|
43
|
+
}
|
|
44
|
+
function buildVerboseSkillMarkdown(name) {
|
|
45
|
+
return buildSkillMarkdown(name);
|
|
46
|
+
}
|
|
17
47
|
function buildGitignore() {
|
|
18
48
|
return `node_modules/
|
|
19
49
|
dist/
|
|
@@ -21,3 +51,155 @@ dist/
|
|
|
21
51
|
npm-debug.log*
|
|
22
52
|
`;
|
|
23
53
|
}
|
|
54
|
+
function buildScriptsReadme() {
|
|
55
|
+
return `# scripts/
|
|
56
|
+
|
|
57
|
+
Use this folder for executable code that agents can run.
|
|
58
|
+
|
|
59
|
+
Guidelines:
|
|
60
|
+
- Keep scripts self-contained or clearly document dependencies.
|
|
61
|
+
- Emit actionable error messages.
|
|
62
|
+
- Handle edge cases (missing files, invalid input, empty data).
|
|
63
|
+
|
|
64
|
+
Starter script:
|
|
65
|
+
- \`scripts/extract.py\`
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function buildExtractScriptPython() {
|
|
69
|
+
return `#!/usr/bin/env python3
|
|
70
|
+
"""Example extraction script with defensive error handling.
|
|
71
|
+
|
|
72
|
+
Usage:
|
|
73
|
+
python3 scripts/extract.py --input ./input.txt --output ./output.json
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
|
|
78
|
+
import argparse
|
|
79
|
+
import json
|
|
80
|
+
import os
|
|
81
|
+
import sys
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_args() -> argparse.Namespace:
|
|
85
|
+
parser = argparse.ArgumentParser(description="Extract line and word statistics.")
|
|
86
|
+
parser.add_argument("--input", required=True, help="Path to input text file.")
|
|
87
|
+
parser.add_argument("--output", required=True, help="Path to output JSON file.")
|
|
88
|
+
return parser.parse_args()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
args = parse_args()
|
|
93
|
+
|
|
94
|
+
if not os.path.exists(args.input):
|
|
95
|
+
print(f"error: input file does not exist: {args.input}", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
with open(args.input, "r", encoding="utf-8") as source:
|
|
100
|
+
content = source.read()
|
|
101
|
+
except OSError as error:
|
|
102
|
+
print(f"error: failed to read input file: {error}", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
lines = [line for line in content.splitlines() if line.strip()]
|
|
106
|
+
result = {
|
|
107
|
+
"line_count": len(lines),
|
|
108
|
+
"word_count": len(content.split()),
|
|
109
|
+
"preview": lines[:3],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
with open(args.output, "w", encoding="utf-8") as destination:
|
|
114
|
+
json.dump(result, destination, indent=2)
|
|
115
|
+
destination.write("\\n")
|
|
116
|
+
except OSError as error:
|
|
117
|
+
print(f"error: failed to write output file: {error}", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
print(f"wrote extraction summary to {args.output}")
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
function buildReferenceGuide() {
|
|
129
|
+
return `# Reference Guide
|
|
130
|
+
|
|
131
|
+
Use this file for detailed technical reference that should not live in \`SKILL.md\`.
|
|
132
|
+
|
|
133
|
+
Suggested sections:
|
|
134
|
+
- Data model and field definitions
|
|
135
|
+
- Validation rules and constraints
|
|
136
|
+
- External API contract notes
|
|
137
|
+
- Error catalog and recovery guidance
|
|
138
|
+
|
|
139
|
+
Keep this file focused and concise. Add deeper detail in additional reference files when needed.
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
function buildFormsReference() {
|
|
143
|
+
return `# Forms and Structured Templates
|
|
144
|
+
|
|
145
|
+
Use this file to store reusable form structures and payload templates.
|
|
146
|
+
|
|
147
|
+
## Intake Form (Example)
|
|
148
|
+
|
|
149
|
+
\`\`\`yaml
|
|
150
|
+
request_id: "REQ-0001"
|
|
151
|
+
request_type: "analysis"
|
|
152
|
+
priority: "normal"
|
|
153
|
+
inputs:
|
|
154
|
+
source_path: "./input.txt"
|
|
155
|
+
output_path: "./output.json"
|
|
156
|
+
\`\`\`
|
|
157
|
+
|
|
158
|
+
## Result Envelope (Example)
|
|
159
|
+
|
|
160
|
+
\`\`\`json
|
|
161
|
+
{
|
|
162
|
+
"status": "ok",
|
|
163
|
+
"summary": "One-line result summary",
|
|
164
|
+
"artifacts": []
|
|
165
|
+
}
|
|
166
|
+
\`\`\`
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
function buildAssetsReadme() {
|
|
170
|
+
return `# assets/
|
|
171
|
+
|
|
172
|
+
Use this folder for static resources that scripts or instructions can reference:
|
|
173
|
+
- templates
|
|
174
|
+
- lookup tables
|
|
175
|
+
- example data
|
|
176
|
+
- diagrams
|
|
177
|
+
|
|
178
|
+
Keep asset files small and task-focused.
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
function buildReportTemplate() {
|
|
182
|
+
return `# Report Template
|
|
183
|
+
|
|
184
|
+
## Summary
|
|
185
|
+
- Status:
|
|
186
|
+
- Owner:
|
|
187
|
+
- Updated:
|
|
188
|
+
|
|
189
|
+
## Findings
|
|
190
|
+
1.
|
|
191
|
+
2.
|
|
192
|
+
3.
|
|
193
|
+
|
|
194
|
+
## Next Steps
|
|
195
|
+
1.
|
|
196
|
+
2.
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
function buildLookupTableCsv() {
|
|
200
|
+
return `code,label,description
|
|
201
|
+
E001,missing_input,Required input file is missing
|
|
202
|
+
E002,invalid_format,Input format is invalid or unsupported
|
|
203
|
+
E003,write_failed,Output could not be written
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateWithSkillsRef = validateWithSkillsRef;
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const SKILLS_REF_TIMEOUT_MS = 10000;
|
|
5
6
|
function formatOutput(stdout, stderr) {
|
|
6
7
|
return [stdout.trim(), stderr.trim()].filter((part) => part.length > 0).join("\n");
|
|
7
8
|
}
|
|
8
9
|
function validateWithSkillsRef(targetDir) {
|
|
9
10
|
const result = (0, node_child_process_1.spawnSync)("skills-ref", ["validate", targetDir], {
|
|
10
11
|
encoding: "utf8",
|
|
12
|
+
timeout: SKILLS_REF_TIMEOUT_MS,
|
|
11
13
|
});
|
|
12
14
|
if (result.error) {
|
|
13
15
|
if ("code" in result.error && result.error.code === "ENOENT") {
|
|
@@ -16,6 +18,12 @@ function validateWithSkillsRef(targetDir) {
|
|
|
16
18
|
message: "skills-ref is not installed or not on PATH",
|
|
17
19
|
};
|
|
18
20
|
}
|
|
21
|
+
if ("code" in result.error && result.error.code === "ETIMEDOUT") {
|
|
22
|
+
return {
|
|
23
|
+
status: "unavailable",
|
|
24
|
+
message: `skills-ref timed out after ${SKILLS_REF_TIMEOUT_MS}ms`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
19
27
|
return {
|
|
20
28
|
status: "unavailable",
|
|
21
29
|
message: `skills-ref execution failed: ${result.error.message}`,
|