@iamk77/skill-checklist 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +159 -0
- package/bin/checklist.js +2 -0
- package/dist/builtins/description.js +73 -0
- package/dist/builtins/file-refs.js +74 -0
- package/dist/builtins/frontmatter.js +64 -0
- package/dist/builtins/has-checklist.js +52 -0
- package/dist/builtins/index.js +27 -0
- package/dist/builtins/line-count.js +51 -0
- package/dist/builtins/name-format.js +65 -0
- package/dist/builtins/no-secrets.js +85 -0
- package/dist/commands/check.js +38 -0
- package/dist/commands/init.js +67 -0
- package/dist/commands/phases.js +17 -0
- package/dist/commands/reset.js +63 -0
- package/dist/commands/show.js +31 -0
- package/dist/commands/verify.js +40 -0
- package/dist/formatter.js +148 -0
- package/dist/index.js +58 -0
- package/dist/loader.js +112 -0
- package/dist/resolver.js +181 -0
- package/dist/runner.js +136 -0
- package/dist/state.js +108 -0
- package/dist/types.js +2 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const init_js_1 = require("./commands/init.js");
|
|
5
|
+
const show_js_1 = require("./commands/show.js");
|
|
6
|
+
const verify_js_1 = require("./commands/verify.js");
|
|
7
|
+
const check_js_1 = require("./commands/check.js");
|
|
8
|
+
const phases_js_1 = require("./commands/phases.js");
|
|
9
|
+
const reset_js_1 = require("./commands/reset.js");
|
|
10
|
+
const program = new commander_1.Command()
|
|
11
|
+
.name('checklist')
|
|
12
|
+
.description('Flight checklist CLI for Claude Code skills')
|
|
13
|
+
.version('0.2.0');
|
|
14
|
+
// Flags are uniform across every command so an agent never has to remember
|
|
15
|
+
// which command accepts what. In the normal skill flow none are needed:
|
|
16
|
+
// `--dir` defaults to $CLAUDE_SKILL_DIR / the active checklist, and `--path`
|
|
17
|
+
// defaults to that same dir.
|
|
18
|
+
const DIR_OPT = ['-d, --dir <dir>', 'Directory containing .checklist.yml'];
|
|
19
|
+
const PATH_OPT = ['-p, --path <path>', 'Target skill directory for builtins (defaults to --dir)'];
|
|
20
|
+
program
|
|
21
|
+
.command('init [dir]')
|
|
22
|
+
.description('Load .checklist.yml, clear state, show ready summary')
|
|
23
|
+
.option(...DIR_OPT)
|
|
24
|
+
.option(...PATH_OPT)
|
|
25
|
+
.option('--force', 'Clear existing state without prompting')
|
|
26
|
+
.action(init_js_1.initCommand);
|
|
27
|
+
program
|
|
28
|
+
.command('show [phase]')
|
|
29
|
+
.description('Show checklist overview, or a specific phase with readings')
|
|
30
|
+
.option(...DIR_OPT)
|
|
31
|
+
.option(...PATH_OPT)
|
|
32
|
+
.action(show_js_1.showCommand);
|
|
33
|
+
program
|
|
34
|
+
.command('verify <phase>')
|
|
35
|
+
.description('Batch verify mechanical checks for a phase')
|
|
36
|
+
.option(...DIR_OPT)
|
|
37
|
+
.option(...PATH_OPT)
|
|
38
|
+
.action(verify_js_1.verifyCommand);
|
|
39
|
+
program
|
|
40
|
+
.command('check <phase> <item-id>')
|
|
41
|
+
.description('Manually confirm a human-judgment check item')
|
|
42
|
+
.option(...DIR_OPT)
|
|
43
|
+
.option(...PATH_OPT)
|
|
44
|
+
.action(check_js_1.checkCommand);
|
|
45
|
+
program
|
|
46
|
+
.command('phases')
|
|
47
|
+
.description('List all phases')
|
|
48
|
+
.option(...DIR_OPT)
|
|
49
|
+
.option(...PATH_OPT)
|
|
50
|
+
.action(phases_js_1.phasesCommand);
|
|
51
|
+
program
|
|
52
|
+
.command('reset')
|
|
53
|
+
.alias('done')
|
|
54
|
+
.description('End-of-run cleanup: clear this skill\'s state and active pointer')
|
|
55
|
+
.option(...DIR_OPT)
|
|
56
|
+
.option(...PATH_OPT)
|
|
57
|
+
.action(reset_js_1.resetCommand);
|
|
58
|
+
program.parse();
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadChecklist = loadChecklist;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const yaml = __importStar(require("js-yaml"));
|
|
40
|
+
const CONFIG_FILE = '.checklist.yml';
|
|
41
|
+
function loadChecklist(dir) {
|
|
42
|
+
const filePath = path.resolve(dir, CONFIG_FILE);
|
|
43
|
+
if (!fs.existsSync(filePath)) {
|
|
44
|
+
// Agents naturally point checklist at the project they are working on, but
|
|
45
|
+
// a checklist lives in the *skill* directory (next to SKILL.md). Redirect
|
|
46
|
+
// instead of dead-ending.
|
|
47
|
+
const skillHint = process.env.CLAUDE_SKILL_DIR
|
|
48
|
+
? `\n this skill's dir is: ${process.env.CLAUDE_SKILL_DIR}\n try: checklist init "${process.env.CLAUDE_SKILL_DIR}"`
|
|
49
|
+
: `\n a checklist lives in the skill directory (next to SKILL.md), not your project/working dir.\n try: checklist init <skill-dir>`;
|
|
50
|
+
throw new Error(`${CONFIG_FILE} not found in ${dir}${skillHint}`);
|
|
51
|
+
}
|
|
52
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
53
|
+
const data = yaml.load(raw);
|
|
54
|
+
if (!data || typeof data !== 'object') {
|
|
55
|
+
throw new Error(`${CONFIG_FILE} is empty or not a valid YAML object`);
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(data.phases)) {
|
|
58
|
+
throw new Error(`${CONFIG_FILE} missing "phases" array`);
|
|
59
|
+
}
|
|
60
|
+
const phases = data.phases.map((p, i) => {
|
|
61
|
+
const phase = p;
|
|
62
|
+
if (!phase.name || typeof phase.name !== 'string') {
|
|
63
|
+
throw new Error(`Phase ${i}: missing "name" field`);
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(phase.checks)) {
|
|
66
|
+
throw new Error(`Phase "${phase.name}": missing "checks" array`);
|
|
67
|
+
}
|
|
68
|
+
if (phase.checks.length === 0) {
|
|
69
|
+
// A phase with no checks is vacuously gate-complete (`[].every` is true),
|
|
70
|
+
// letting an empty stage pass with zero work. The format is documented as
|
|
71
|
+
// a non-empty checks list, so reject it at the parse boundary.
|
|
72
|
+
throw new Error(`Phase "${phase.name}": "checks" array is empty`);
|
|
73
|
+
}
|
|
74
|
+
const checks = phase.checks.map((c, j) => {
|
|
75
|
+
const check = c;
|
|
76
|
+
if (!check.id || typeof check.id !== 'string') {
|
|
77
|
+
throw new Error(`Phase "${phase.name}", check ${j}: missing "id"`);
|
|
78
|
+
}
|
|
79
|
+
if (!check.description || typeof check.description !== 'string') {
|
|
80
|
+
throw new Error(`Phase "${phase.name}", check "${check.id}": missing "description"`);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
id: check.id,
|
|
84
|
+
description: check.description,
|
|
85
|
+
verify: typeof check.verify === 'string' ? check.verify : undefined,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
const seenIds = new Set();
|
|
89
|
+
for (const ch of checks) {
|
|
90
|
+
if (seenIds.has(ch.id)) {
|
|
91
|
+
throw new Error(`Phase "${phase.name}": duplicate check id "${ch.id}"`);
|
|
92
|
+
}
|
|
93
|
+
seenIds.add(ch.id);
|
|
94
|
+
}
|
|
95
|
+
return { name: phase.name, checks };
|
|
96
|
+
});
|
|
97
|
+
if (phases.length === 0) {
|
|
98
|
+
throw new Error(`${CONFIG_FILE}: "phases" array is empty`);
|
|
99
|
+
}
|
|
100
|
+
// Phases are addressed BY NAME (findPhaseIndex, case-insensitively). Two phases
|
|
101
|
+
// sharing a name would make every non-first one unreachable through its only
|
|
102
|
+
// documented handle, so reject duplicates the same way duplicate check ids are.
|
|
103
|
+
const seenPhaseNames = new Set();
|
|
104
|
+
for (const ph of phases) {
|
|
105
|
+
const key = ph.name.toLowerCase();
|
|
106
|
+
if (seenPhaseNames.has(key)) {
|
|
107
|
+
throw new Error(`${CONFIG_FILE}: duplicate phase name "${ph.name}"`);
|
|
108
|
+
}
|
|
109
|
+
seenPhaseNames.add(key);
|
|
110
|
+
}
|
|
111
|
+
return { phases };
|
|
112
|
+
}
|
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveDir = resolveDir;
|
|
37
|
+
exports.writeActivePointer = writeActivePointer;
|
|
38
|
+
exports.clearActivePointer = clearActivePointer;
|
|
39
|
+
exports.findPhaseIndex = findPhaseIndex;
|
|
40
|
+
exports.runPhase = runPhase;
|
|
41
|
+
exports.gatePriorPhases = gatePriorPhases;
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const os = __importStar(require("node:os"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const state_js_1 = require("./state.js");
|
|
46
|
+
const runner_js_1 = require("./runner.js");
|
|
47
|
+
// Single, cwd-independent record of the active checklist dir. `init` writes it
|
|
48
|
+
// (its dir comes from the harness-expanded ${CLAUDE_SKILL_DIR}, so it is
|
|
49
|
+
// reliable), and every later command resolves it no matter which directory they
|
|
50
|
+
// run from — so the skill never has to pass --dir, and there is no cwd-coupled
|
|
51
|
+
// pointer that a stale copy could shadow. Location is overridable via
|
|
52
|
+
// CHECKLIST_HOME (used to sandbox tests).
|
|
53
|
+
const CONFIG_FILE = '.checklist.yml';
|
|
54
|
+
function activePointerPath() {
|
|
55
|
+
const dir = process.env.CHECKLIST_HOME ||
|
|
56
|
+
(process.env.XDG_CONFIG_HOME
|
|
57
|
+
? path.join(process.env.XDG_CONFIG_HOME, 'checklist')
|
|
58
|
+
: path.join(os.homedir(), '.config', 'checklist'));
|
|
59
|
+
return path.join(dir, 'active');
|
|
60
|
+
}
|
|
61
|
+
function resolveDir(explicit) {
|
|
62
|
+
if (explicit)
|
|
63
|
+
return explicit;
|
|
64
|
+
if (process.env.CHECKLIST_DIR)
|
|
65
|
+
return process.env.CHECKLIST_DIR;
|
|
66
|
+
// A running skill always knows its own dir; the harness exposes it here. This
|
|
67
|
+
// lets an agent run any checklist command (even before `init`) with no flags
|
|
68
|
+
// and no guesswork about where .checklist.yml lives.
|
|
69
|
+
if (process.env.CLAUDE_SKILL_DIR)
|
|
70
|
+
return process.env.CLAUDE_SKILL_DIR;
|
|
71
|
+
const pointerPath = activePointerPath();
|
|
72
|
+
let raw;
|
|
73
|
+
try {
|
|
74
|
+
raw = fs.readFileSync(pointerPath, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return process.cwd(); // no pointer (ENOENT) or unreadable
|
|
78
|
+
}
|
|
79
|
+
const target = raw.trim();
|
|
80
|
+
if (target) {
|
|
81
|
+
try {
|
|
82
|
+
fs.statSync(path.join(target, CONFIG_FILE));
|
|
83
|
+
return target; // points at a real checklist dir
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
// Only self-heal when the target is *definitely* gone. statSync returning
|
|
87
|
+
// ENOENT/ENOTDIR means the dir/file is absent; any other errno (EACCES,
|
|
88
|
+
// EIO, ELOOP, an NFS stall, ...) means "can't tell" — never delete a valid
|
|
89
|
+
// pointer over a transient read failure.
|
|
90
|
+
const code = e.code;
|
|
91
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
92
|
+
return target;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Empty content, or target is definitely gone: self-heal and fall through.
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(pointerPath);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* best-effort cleanup; ignore races */
|
|
102
|
+
}
|
|
103
|
+
return process.cwd();
|
|
104
|
+
}
|
|
105
|
+
function writeActivePointer(targetDir) {
|
|
106
|
+
const absDir = path.resolve(targetDir);
|
|
107
|
+
const pointerPath = activePointerPath();
|
|
108
|
+
fs.mkdirSync(path.dirname(pointerPath), { recursive: true });
|
|
109
|
+
fs.writeFileSync(pointerPath, absDir, 'utf-8');
|
|
110
|
+
}
|
|
111
|
+
// Remove the active pointer. With a targetDir, only removes it when it points
|
|
112
|
+
// there (so `reset` of skill A never clobbers an active pointer for skill B).
|
|
113
|
+
// Returns whether a pointer was removed.
|
|
114
|
+
function clearActivePointer(targetDir) {
|
|
115
|
+
const pointerPath = activePointerPath();
|
|
116
|
+
let current;
|
|
117
|
+
try {
|
|
118
|
+
current = fs.readFileSync(pointerPath, 'utf-8').trim();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false; // no pointer (handles the ENOENT race too)
|
|
122
|
+
}
|
|
123
|
+
if (targetDir && current !== path.resolve(targetDir))
|
|
124
|
+
return false;
|
|
125
|
+
try {
|
|
126
|
+
fs.unlinkSync(pointerPath);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function findPhaseIndex(config, nameOrIndex) {
|
|
134
|
+
const num = parseInt(nameOrIndex, 10);
|
|
135
|
+
if (!isNaN(num) && num >= 0 && num < config.phases.length) {
|
|
136
|
+
return num;
|
|
137
|
+
}
|
|
138
|
+
const idx = config.phases.findIndex(p => p.name.toLowerCase() === nameOrIndex.toLowerCase());
|
|
139
|
+
if (idx === -1) {
|
|
140
|
+
throw new Error(`Phase not found: "${nameOrIndex}". Use \`checklist phases\` to list available phases.`);
|
|
141
|
+
}
|
|
142
|
+
return idx;
|
|
143
|
+
}
|
|
144
|
+
async function runPhase(phase, phaseIndex, cwd, targetPath) {
|
|
145
|
+
const checks = await Promise.all(phase.checks.map(item => (0, runner_js_1.runCheck)(item, cwd, targetPath)));
|
|
146
|
+
let mechanicalPassed = 0;
|
|
147
|
+
let mechanicalTotal = 0;
|
|
148
|
+
let manualCount = 0;
|
|
149
|
+
for (const c of checks) {
|
|
150
|
+
if (c.kind === 'manual') {
|
|
151
|
+
manualCount++;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
mechanicalTotal++;
|
|
155
|
+
if (c.result?.status === 'pass')
|
|
156
|
+
mechanicalPassed++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
phaseName: phase.name,
|
|
161
|
+
phaseIndex,
|
|
162
|
+
checks,
|
|
163
|
+
mechanicalPassed,
|
|
164
|
+
mechanicalTotal,
|
|
165
|
+
manualCount,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function gatePriorPhases(config, targetPhaseIndex, state) {
|
|
169
|
+
for (let i = 0; i < targetPhaseIndex; i++) {
|
|
170
|
+
const phase = config.phases[i];
|
|
171
|
+
const ids = phase.checks.map(c => c.id);
|
|
172
|
+
if (!(0, state_js_1.isPhaseComplete)(state, i, ids)) {
|
|
173
|
+
return {
|
|
174
|
+
passed: false,
|
|
175
|
+
failedPhase: phase.name,
|
|
176
|
+
failedPhaseIndex: i,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { passed: true };
|
|
181
|
+
}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runCheck = runCheck;
|
|
37
|
+
const node_child_process_1 = require("node:child_process");
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const index_js_1 = require("./builtins/index.js");
|
|
41
|
+
const EXEC_TIMEOUT = 10_000;
|
|
42
|
+
const PREFIX_MAP = {
|
|
43
|
+
'builtin:': 'builtin',
|
|
44
|
+
'shell:': 'shell',
|
|
45
|
+
'script:': 'script',
|
|
46
|
+
};
|
|
47
|
+
function classifyVerify(verify) {
|
|
48
|
+
for (const [prefix, kind] of Object.entries(PREFIX_MAP)) {
|
|
49
|
+
if (verify.startsWith(prefix)) {
|
|
50
|
+
return { kind, value: verify.slice(prefix.length).trim(), explicit: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const firstToken = verify.split(/\s/)[0];
|
|
54
|
+
if (firstToken.includes('/') || /\.(sh|bash|ts|js|py)$/.test(firstToken)) {
|
|
55
|
+
return { kind: 'script', value: verify, explicit: false };
|
|
56
|
+
}
|
|
57
|
+
return { kind: 'shell', value: verify, explicit: false };
|
|
58
|
+
}
|
|
59
|
+
async function runShell(command, cwd) {
|
|
60
|
+
try {
|
|
61
|
+
const stdout = (0, node_child_process_1.execSync)(command, {
|
|
62
|
+
cwd,
|
|
63
|
+
timeout: EXEC_TIMEOUT,
|
|
64
|
+
encoding: 'utf-8',
|
|
65
|
+
shell: '/bin/bash',
|
|
66
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
67
|
+
}).trim();
|
|
68
|
+
return { status: 'pass', message: stdout || 'OK' };
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const err = e;
|
|
72
|
+
const detail = (err.stderr || err.message || 'command failed').trim();
|
|
73
|
+
return { status: 'fail', message: detail };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function runBuiltin(name, targetPath) {
|
|
77
|
+
const handler = (0, index_js_1.getBuiltin)(name);
|
|
78
|
+
if (!handler) {
|
|
79
|
+
const available = (0, index_js_1.listBuiltins)().join(', ');
|
|
80
|
+
return { status: 'error', message: `unknown builtin "${name}". available: ${available}` };
|
|
81
|
+
}
|
|
82
|
+
return handler(targetPath);
|
|
83
|
+
}
|
|
84
|
+
async function runScript(scriptPath, cwd, explicit) {
|
|
85
|
+
const base = path.resolve(cwd);
|
|
86
|
+
const resolved = path.resolve(base, scriptPath);
|
|
87
|
+
const rel = path.relative(base, resolved);
|
|
88
|
+
// Containment: the script must live inside the checklist dir. Absolute paths
|
|
89
|
+
// are fine as long as they resolve within `base`; escapes (`..`, other roots)
|
|
90
|
+
// are rejected to prevent a malicious .checklist.yml from executing arbitrary
|
|
91
|
+
// files elsewhere on disk.
|
|
92
|
+
if (rel === '..' || rel.startsWith('..' + path.sep) || path.isAbsolute(rel)) {
|
|
93
|
+
return {
|
|
94
|
+
status: 'error',
|
|
95
|
+
message: `script path escapes the checklist dir: "${scriptPath}" (resolved to "${resolved}")`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!fs.existsSync(resolved)) {
|
|
99
|
+
const hint = explicit
|
|
100
|
+
? `script not found: "${resolved}" (resolved from "${scriptPath}")`
|
|
101
|
+
: `auto-classified as script (first token contains "/" or has script extension), but file not found: "${resolved}". use "shell:" prefix if this is a shell command`;
|
|
102
|
+
return { status: 'error', message: hint };
|
|
103
|
+
}
|
|
104
|
+
// Containment (canonical): the lexical check above never follows symlinks, but
|
|
105
|
+
// execution does — a symlink planted inside the dir can point outside it. Re-check
|
|
106
|
+
// against the real (symlink-resolved) paths before running.
|
|
107
|
+
const realBase = fs.realpathSync(base);
|
|
108
|
+
const realResolved = fs.realpathSync(resolved);
|
|
109
|
+
const realRel = path.relative(realBase, realResolved);
|
|
110
|
+
if (realRel === '..' || realRel.startsWith('..' + path.sep) || path.isAbsolute(realRel)) {
|
|
111
|
+
return {
|
|
112
|
+
status: 'error',
|
|
113
|
+
message: `script path escapes the checklist dir via symlink: "${scriptPath}" (resolves to "${realResolved}")`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return runShell(realResolved, cwd);
|
|
117
|
+
}
|
|
118
|
+
async function runCheck(item, cwd, targetPath) {
|
|
119
|
+
if (!item.verify) {
|
|
120
|
+
return { item, kind: 'manual' };
|
|
121
|
+
}
|
|
122
|
+
const { kind, value, explicit } = classifyVerify(item.verify);
|
|
123
|
+
let result;
|
|
124
|
+
switch (kind) {
|
|
125
|
+
case 'builtin':
|
|
126
|
+
result = await runBuiltin(value, targetPath);
|
|
127
|
+
break;
|
|
128
|
+
case 'script':
|
|
129
|
+
result = await runScript(value, cwd, explicit);
|
|
130
|
+
break;
|
|
131
|
+
case 'shell':
|
|
132
|
+
result = await runShell(value, cwd);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
return { item, kind: 'mechanical', result };
|
|
136
|
+
}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadState = loadState;
|
|
37
|
+
exports.saveState = saveState;
|
|
38
|
+
exports.clearState = clearState;
|
|
39
|
+
exports.isItemChecked = isItemChecked;
|
|
40
|
+
exports.getItemResult = getItemResult;
|
|
41
|
+
exports.setItemResult = setItemResult;
|
|
42
|
+
exports.isPhaseComplete = isPhaseComplete;
|
|
43
|
+
exports.phaseProgress = phaseProgress;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const STATE_FILE = '.checklist.state.json';
|
|
47
|
+
function statePath(dir) {
|
|
48
|
+
return path.resolve(dir, STATE_FILE);
|
|
49
|
+
}
|
|
50
|
+
function loadState(dir) {
|
|
51
|
+
const p = statePath(dir);
|
|
52
|
+
if (!fs.existsSync(p)) {
|
|
53
|
+
return { checked: {} };
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`state file is corrupt: ${p}. run \`checklist init --force\` to reset it`);
|
|
61
|
+
}
|
|
62
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
63
|
+
throw new Error(`state file is malformed: ${p}. run \`checklist init --force\` to reset it`);
|
|
64
|
+
}
|
|
65
|
+
// `typeof null === 'object'` and `typeof [] === 'object'`, so a corrupt or
|
|
66
|
+
// partially-written file shaped like {"checked":null} or {"checked":[]} would
|
|
67
|
+
// slip past a bare typeof guard and then crash gate evaluation downstream with
|
|
68
|
+
// an unhandled TypeError. Reject any non-plain-object `checked` here instead,
|
|
69
|
+
// so the corruption surfaces as the documented malformed-state error.
|
|
70
|
+
const checked = parsed.checked;
|
|
71
|
+
if (typeof checked !== 'object' || checked === null || Array.isArray(checked)) {
|
|
72
|
+
throw new Error(`state file is malformed: ${p}. run \`checklist init --force\` to reset it`);
|
|
73
|
+
}
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
function saveState(dir, state) {
|
|
77
|
+
fs.writeFileSync(statePath(dir), JSON.stringify(state, null, 2), 'utf-8');
|
|
78
|
+
}
|
|
79
|
+
function clearState(dir) {
|
|
80
|
+
const p = statePath(dir);
|
|
81
|
+
if (fs.existsSync(p)) {
|
|
82
|
+
fs.unlinkSync(p);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isItemChecked(state, phaseIndex, itemId) {
|
|
86
|
+
// An item counts as checked only when its recorded result is a PASS — not
|
|
87
|
+
// merely present. A stored fail/error, or a once-green check that later
|
|
88
|
+
// regressed and was re-recorded, must NOT satisfy the gate. The gate is the
|
|
89
|
+
// safety floor: completeness means current pass-status, not existence of a row.
|
|
90
|
+
return state.checked[String(phaseIndex)]?.[itemId]?.status === 'pass';
|
|
91
|
+
}
|
|
92
|
+
function getItemResult(state, phaseIndex, itemId) {
|
|
93
|
+
return state.checked[String(phaseIndex)]?.[itemId];
|
|
94
|
+
}
|
|
95
|
+
function setItemResult(state, phaseIndex, itemId, result) {
|
|
96
|
+
const key = String(phaseIndex);
|
|
97
|
+
if (!state.checked[key]) {
|
|
98
|
+
state.checked[key] = {};
|
|
99
|
+
}
|
|
100
|
+
state.checked[key][itemId] = result;
|
|
101
|
+
}
|
|
102
|
+
function isPhaseComplete(state, phaseIndex, itemIds) {
|
|
103
|
+
return itemIds.every(id => isItemChecked(state, phaseIndex, id));
|
|
104
|
+
}
|
|
105
|
+
function phaseProgress(state, phaseIndex, itemIds) {
|
|
106
|
+
const done = itemIds.filter(id => isItemChecked(state, phaseIndex, id)).length;
|
|
107
|
+
return { done, total: itemIds.length };
|
|
108
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iamk77/skill-checklist",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Flight checklist CLI that gates a Claude Code skill's phases — a stage cannot open until every check in every prior stage is recorded as passing",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"skill",
|
|
9
|
+
"checklist",
|
|
10
|
+
"gate",
|
|
11
|
+
"cli",
|
|
12
|
+
"agent"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/IamK77/Skill/tree/main/devtools/checklist#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/IamK77/Skill/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/IamK77/Skill.git",
|
|
21
|
+
"directory": "devtools/checklist"
|
|
22
|
+
},
|
|
23
|
+
"author": "IamK77",
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"bin": {
|
|
26
|
+
"checklist": "bin/checklist.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"bin",
|
|
31
|
+
"NOTICE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"dev": "tsc --watch",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"test:coverage": "vitest run --coverage",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"commander": "^14.0.0",
|
|
43
|
+
"gray-matter": "^4.0.3",
|
|
44
|
+
"js-yaml": "^4.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/js-yaml": "^4.0.9",
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
50
|
+
"tsx": "^4.21.0",
|
|
51
|
+
"typescript": "^5.5.0",
|
|
52
|
+
"vitest": "^4.1.6"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|