@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 CHANGED
@@ -28,7 +28,31 @@ npx @skillmarkdown/cli init
28
28
  skillmd init
29
29
  ```
30
30
 
31
- This scaffolds a spec-aligned skill structure including `SKILL.md` and optional directories (`scripts/`, `references/`, `assets/`), then runs local validation.
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
+ });
@@ -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 ?? ((targetDir) => (0, validator_1.validateSkill)(targetDir, { strict: true }));
11
- const skipValidation = args.includes("--no-validate");
12
- const hasUnsupportedArgs = args.length > 1 || (args.length === 1 && args[0] !== "--no-validate");
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;
@@ -26,16 +26,34 @@ function parseFlags(args) {
26
26
  }
27
27
  return { status, reauth, valid: true };
28
28
  }
29
- function printSessionStatus(session) {
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
- return 0;
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}. Run 'skillmd logout' first.`);
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("Already logged in. Run 'skillmd logout' first.");
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("Login successful.");
134
+ console.log(`Login successful (project: ${config.firebaseProjectId}).`);
109
135
  }
110
136
  return 0;
111
137
  }
@@ -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
- const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
12
- const removed = clearSessionFn();
13
- if (removed) {
14
- console.log("Logged out.");
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
- console.log("No active session to log out.");
18
- return 0;
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
- const cwd = options.cwd ?? process.cwd();
11
- const validateLocal = options.validateLocal ?? validator_1.validateSkill;
12
- const validateUpstream = options.validateUpstream ?? upstream_validator_1.validateWithSkillsRef;
13
- let strict = false;
14
- let parity = false;
15
- let pathArg;
16
- for (const arg of args) {
17
- if (arg === "--strict") {
18
- strict = true;
19
- continue;
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
- if (arg === "--parity") {
22
- parity = true;
23
- continue;
24
- }
25
- if (arg.startsWith("-")) {
26
- return (0, command_output_1.failWithUsage)(`skillmd validate: unsupported flag '${arg}'`, cli_text_1.VALIDATE_USAGE);
27
- }
28
- if (pathArg) {
29
- return (0, command_output_1.failWithUsage)("skillmd validate: accepts at most one path argument", cli_text_1.VALIDATE_USAGE);
30
- }
31
- pathArg = arg;
32
- }
33
- const targetDir = pathArg ? (0, node_path_1.resolve)(cwd, pathArg) : cwd;
34
- const validation = validateLocal(targetDir, { strict });
35
- if (parity) {
36
- const upstream = validateUpstream(targetDir);
37
- if (upstream.status === "unavailable") {
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
- if (validation.status === "failed" && upstream.status !== "failed") {
47
- console.error("Validation parity mismatch: local validation failed but skills-ref passed.");
48
- return 1;
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.log("Validation parity passed (skills-ref).");
60
+ console.error("Validation parity matched (skills-ref also failed).");
55
61
  }
56
- return 0;
62
+ return 1;
57
63
  }
58
- if (parity) {
59
- console.error("Validation parity matched (skills-ref also failed).");
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
  }
@@ -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
- if (!githubClientId || !firebaseApiKey) {
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 = {}) {
@@ -5,4 +5,5 @@ exports.DEFAULT_LOGIN_AUTH_CONFIG = void 0;
5
5
  exports.DEFAULT_LOGIN_AUTH_CONFIG = Object.freeze({
6
6
  githubClientId: "Ov23lixkdtyLp35IFaBG",
7
7
  firebaseApiKey: "AIzaSyAkaZRmpCvZasFjeRAfW_b0V0nUcGOTjok",
8
+ firebaseProjectId: "skillmarkdown",
8
9
  });
@@ -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 {
@@ -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" || errorMessage === "TOKEN_EXPIRED")) {
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"}`);
@@ -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 scaffoldSkillInDirectory(targetDir) {
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
- for (const directory of skill_spec_1.SCAFFOLD_DIRECTORIES) {
27
- const fullPath = (0, node_path_1.join)(targetDir, directory);
28
- (0, node_fs_1.mkdirSync)(fullPath, { recursive: true });
29
- (0, node_fs_1.writeFileSync)((0, node_path_1.join)(fullPath, ".gitkeep"), "", "utf8");
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
  }
@@ -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",
@@ -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}\nTODO`).join("\n\n");
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: "TODO: Describe what this skill does and when to use it."
11
- license: TODO
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}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillmarkdown/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for scaffolding SKILL.md-based AI skills",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",