supermind-claude 2.1.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +21 -0
- package/README.md +34 -46
- package/agents/code-reviewer.md +81 -0
- package/cli/commands/doctor.js +415 -79
- package/cli/commands/install.js +16 -17
- package/cli/commands/skill.js +164 -0
- package/cli/commands/uninstall.js +32 -3
- package/cli/commands/update.js +25 -4
- package/cli/index.js +16 -4
- package/cli/lib/agents.js +413 -0
- package/cli/lib/executor.js +365 -0
- package/cli/lib/hooks.js +8 -1
- package/cli/lib/logger.js +1 -1
- package/cli/lib/planning.js +502 -0
- package/cli/lib/platform.js +4 -0
- package/cli/lib/plugin.js +127 -0
- package/cli/lib/settings.js +2 -40
- package/cli/lib/skills.js +39 -2
- package/cli/lib/vendor-skills.js +594 -0
- package/hooks/bash-permissions.js +196 -176
- package/hooks/context-monitor.js +79 -0
- package/hooks/improvement-logger.js +94 -0
- package/hooks/pre-merge-checklist.js +102 -0
- package/hooks/session-start.js +109 -5
- package/hooks/statusline-command.js +123 -29
- package/package.json +4 -2
- package/skills/anti-rationalization/SKILL.md +38 -0
- package/skills/brainstorming/SKILL.md +165 -0
- package/skills/code-review/SKILL.md +144 -0
- package/skills/executing-plans/SKILL.md +138 -0
- package/skills/finishing-branches/SKILL.md +144 -0
- package/skills/project/SKILL.md +533 -0
- package/skills/quick/SKILL.md +178 -0
- package/skills/supermind/SKILL.md +58 -4
- package/skills/supermind-init/SKILL.md +48 -2
- package/skills/systematic-debugging/SKILL.md +129 -0
- package/skills/tdd/SKILL.md +179 -0
- package/skills/using-git-worktrees/SKILL.md +138 -0
- package/skills/verification-before-completion/SKILL.md +54 -0
- package/skills/writing-plans/SKILL.md +169 -0
- package/templates/CLAUDE.md +124 -62
- package/cli/lib/plugins.js +0 -23
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Path safety — mirrors planning.js / vendor-skills.js safeJoin pattern
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function safeJoin(trustedBase, segment, label) {
|
|
12
|
+
if (typeof segment !== 'string' || segment.length === 0) {
|
|
13
|
+
throw new Error(`Invalid ${label}: must be a non-empty string`);
|
|
14
|
+
}
|
|
15
|
+
if (path.isAbsolute(segment)) {
|
|
16
|
+
throw new Error(`Invalid ${label}: must not be an absolute path`);
|
|
17
|
+
}
|
|
18
|
+
const parts = segment.split(/[\\/]/);
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
if (part === '..') {
|
|
21
|
+
throw new Error(`Invalid ${label}: path traversal sequences are not allowed`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const resolved = trustedBase + path.sep + parts.join(path.sep);
|
|
25
|
+
if (!resolved.startsWith(trustedBase + path.sep) && resolved !== trustedBase) {
|
|
26
|
+
throw new Error(`Invalid ${label}: resolved path escapes base directory`);
|
|
27
|
+
}
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Constants
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
36
|
+
const SKILLS_DIR_NAME = 'skills';
|
|
37
|
+
|
|
38
|
+
/** Maps task types to the methodology skills injected into executor prompts. */
|
|
39
|
+
const SKILL_MAP = {
|
|
40
|
+
'write-feature': ['tdd', 'verification-before-completion', 'anti-rationalization', 'using-git-worktrees'],
|
|
41
|
+
'fix-bug': ['systematic-debugging', 'verification-before-completion', 'anti-rationalization', 'using-git-worktrees'],
|
|
42
|
+
'refactor': ['verification-before-completion', 'anti-rationalization', 'using-git-worktrees'],
|
|
43
|
+
'write-test': ['tdd', 'anti-rationalization'],
|
|
44
|
+
'research': [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// getSkillContent — reads SKILL.md from the skills directory
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read a skill's SKILL.md content.
|
|
53
|
+
*
|
|
54
|
+
* Search order:
|
|
55
|
+
* 1. ~/.claude/skills/{skillName}/SKILL.md
|
|
56
|
+
* 2. {projectRoot}/.claude/skills/{skillName}/SKILL.md (project-level)
|
|
57
|
+
*
|
|
58
|
+
* @param {string} skillName — e.g. 'tdd', 'anti-rationalization'
|
|
59
|
+
* @param {string} [projectRoot] — project root for project-level fallback
|
|
60
|
+
* @returns {string | null} file content or null if not found
|
|
61
|
+
*/
|
|
62
|
+
function getSkillContent(skillName, projectRoot) {
|
|
63
|
+
if (typeof skillName !== 'string' || skillName.length === 0) return null;
|
|
64
|
+
if (!/^[\w.-]+$/.test(skillName)) return null;
|
|
65
|
+
|
|
66
|
+
// Global: ~/.claude/skills/{skillName}/SKILL.md
|
|
67
|
+
const globalSkillsDir = safeJoin(CLAUDE_DIR, SKILLS_DIR_NAME, 'skills directory');
|
|
68
|
+
const globalPath = safeJoin(
|
|
69
|
+
safeJoin(globalSkillsDir, skillName, 'skill name'),
|
|
70
|
+
'SKILL.md',
|
|
71
|
+
'skill file',
|
|
72
|
+
);
|
|
73
|
+
try {
|
|
74
|
+
return fs.readFileSync(globalPath, 'utf-8');
|
|
75
|
+
} catch { /* fall through */ }
|
|
76
|
+
|
|
77
|
+
// Project-level: {projectRoot}/.claude/skills/{skillName}/SKILL.md
|
|
78
|
+
if (typeof projectRoot === 'string' && projectRoot.length > 0) {
|
|
79
|
+
const projectClaudeDir = safeJoin(projectRoot, '.claude', 'project claude dir');
|
|
80
|
+
const projectSkillsDir = safeJoin(projectClaudeDir, SKILLS_DIR_NAME, 'skills directory');
|
|
81
|
+
const projectPath = safeJoin(
|
|
82
|
+
safeJoin(projectSkillsDir, skillName, 'skill name'),
|
|
83
|
+
'SKILL.md',
|
|
84
|
+
'skill file',
|
|
85
|
+
);
|
|
86
|
+
try {
|
|
87
|
+
return fs.readFileSync(projectPath, 'utf-8');
|
|
88
|
+
} catch { /* fall through */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// buildTaskPacket — assembles everything a subagent needs
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Assemble a self-contained task packet for a fresh-context executor.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} task
|
|
102
|
+
* @param {string} task.id — unique task identifier
|
|
103
|
+
* @param {string} task.title — short task title
|
|
104
|
+
* @param {string} task.type — one of: write-feature, fix-bug, refactor, write-test, research
|
|
105
|
+
* @param {string} [task.description] — what to do (falls back to placeholder)
|
|
106
|
+
* @param {string[]} [task.files] — files the executor should read/modify
|
|
107
|
+
* @param {string[]} [task.acceptance] — acceptance criteria
|
|
108
|
+
* @param {string} [task.expectedOutput] — what the result should look like
|
|
109
|
+
* @param {string[]} [task.dependsOn] — IDs of tasks this depends on
|
|
110
|
+
* @param {object} [options]
|
|
111
|
+
* @param {string} [options.projectRoot] — for skill + context resolution
|
|
112
|
+
* @param {string} [options.branch] — current git branch
|
|
113
|
+
* @param {string} [options.recentCommits] — compact git log
|
|
114
|
+
* @param {string} [options.architectureExcerpt] — relevant ARCHITECTURE.md sections
|
|
115
|
+
* @param {string} [options.conventions] — key CLAUDE.md conventions
|
|
116
|
+
* @returns {string} a single prompt string ready for the Agent tool
|
|
117
|
+
*/
|
|
118
|
+
function buildTaskPacket(task, options = {}) {
|
|
119
|
+
if (!task || !task.id || !task.title || !task.type) {
|
|
120
|
+
throw new Error('buildTaskPacket: task must have id, title, and type');
|
|
121
|
+
}
|
|
122
|
+
if (!SKILL_MAP.hasOwnProperty(task.type)) {
|
|
123
|
+
const valid = Object.keys(SKILL_MAP).join(', ');
|
|
124
|
+
throw new Error(`buildTaskPacket: invalid task type "${task.type}" (valid: ${valid})`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sections = [];
|
|
128
|
+
|
|
129
|
+
// --- Task spec ---
|
|
130
|
+
sections.push('# Task Spec');
|
|
131
|
+
sections.push(`**ID:** ${task.id}`);
|
|
132
|
+
sections.push(`**Title:** ${task.title}`);
|
|
133
|
+
sections.push(`**Type:** ${task.type}`);
|
|
134
|
+
sections.push('');
|
|
135
|
+
sections.push('## What to Do');
|
|
136
|
+
sections.push(task.description || '(no description provided)');
|
|
137
|
+
|
|
138
|
+
if (task.files && task.files.length > 0) {
|
|
139
|
+
sections.push('');
|
|
140
|
+
sections.push('## Files');
|
|
141
|
+
for (const f of task.files) {
|
|
142
|
+
sections.push(`- ${f}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (task.acceptance && task.acceptance.length > 0) {
|
|
147
|
+
sections.push('');
|
|
148
|
+
sections.push('## Acceptance Criteria');
|
|
149
|
+
for (const a of task.acceptance) {
|
|
150
|
+
sections.push(`- [ ] ${a}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (task.expectedOutput) {
|
|
155
|
+
sections.push('');
|
|
156
|
+
sections.push('## Expected Output');
|
|
157
|
+
sections.push(task.expectedOutput);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- Project context (compact) ---
|
|
161
|
+
const contextParts = [];
|
|
162
|
+
if (options.branch) {
|
|
163
|
+
contextParts.push(`**Branch:** ${options.branch}`);
|
|
164
|
+
}
|
|
165
|
+
if (options.recentCommits) {
|
|
166
|
+
contextParts.push(`**Recent commits:**\n${options.recentCommits}`);
|
|
167
|
+
}
|
|
168
|
+
if (options.architectureExcerpt) {
|
|
169
|
+
contextParts.push(`**Architecture:**\n${options.architectureExcerpt}`);
|
|
170
|
+
}
|
|
171
|
+
if (options.conventions) {
|
|
172
|
+
contextParts.push(`**Conventions:**\n${options.conventions}`);
|
|
173
|
+
}
|
|
174
|
+
if (contextParts.length > 0) {
|
|
175
|
+
sections.push('');
|
|
176
|
+
sections.push('# Project Context');
|
|
177
|
+
sections.push(contextParts.join('\n\n'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Injected skills ---
|
|
181
|
+
const skillNames = SKILL_MAP[task.type] || [];
|
|
182
|
+
const skillSections = [];
|
|
183
|
+
for (const name of skillNames) {
|
|
184
|
+
const content = getSkillContent(name, options.projectRoot);
|
|
185
|
+
if (content) {
|
|
186
|
+
skillSections.push(`<skill name="${name}">\n${content}\n</skill>`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (skillSections.length > 0) {
|
|
190
|
+
sections.push('');
|
|
191
|
+
sections.push('# Methodology Skills');
|
|
192
|
+
sections.push('Follow these skills strictly:');
|
|
193
|
+
sections.push(skillSections.join('\n\n'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Completion contract ---
|
|
197
|
+
sections.push('');
|
|
198
|
+
sections.push('# Completion Contract');
|
|
199
|
+
sections.push('You MUST follow these rules:');
|
|
200
|
+
sections.push('- Commit your work atomically when done (one commit per task)');
|
|
201
|
+
sections.push('- Report results: files changed, tests run, tests passed');
|
|
202
|
+
sections.push('- Stay in scope — only modify files related to this task');
|
|
203
|
+
sections.push('- NEVER merge branches or push to main/master');
|
|
204
|
+
sections.push('- NEVER skip tests or verification steps');
|
|
205
|
+
sections.push('- If you cannot complete the task, report what failed and why');
|
|
206
|
+
|
|
207
|
+
return sections.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// executeTask — builds the prompt an orchestrator passes to Agent tool
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build a structured execution request from a task packet.
|
|
216
|
+
*
|
|
217
|
+
* This does NOT call the Agent tool — the orchestrator skill does that.
|
|
218
|
+
* It returns the prompt string and metadata the orchestrator needs.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} taskPacket — output of buildTaskPacket
|
|
221
|
+
* @param {object} [options]
|
|
222
|
+
* @param {boolean} [options.useWorktree] — whether executor should use a worktree
|
|
223
|
+
* @param {string} [options.model] — model override (e.g. 'sonnet', 'opus')
|
|
224
|
+
* @returns {{ prompt: string, description: string, model?: string, isolation?: string }}
|
|
225
|
+
*/
|
|
226
|
+
function executeTask(taskPacket, options = {}) {
|
|
227
|
+
if (typeof taskPacket !== 'string' || taskPacket.length === 0) {
|
|
228
|
+
throw new Error('executeTask: taskPacket must be a non-empty string');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Extract task title from the packet for the Agent description
|
|
232
|
+
const titleMatch = taskPacket.match(/^\*\*Title:\*\*\s*(.+)$/m);
|
|
233
|
+
const title = titleMatch ? titleMatch[1].trim() : 'Execute task';
|
|
234
|
+
|
|
235
|
+
// Truncate description to 5 words for the Agent tool's description field
|
|
236
|
+
const descWords = title.split(/\s+/).slice(0, 5).join(' ');
|
|
237
|
+
|
|
238
|
+
const result = {
|
|
239
|
+
prompt: taskPacket,
|
|
240
|
+
description: descWords,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (options.model) {
|
|
244
|
+
result.model = options.model;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (options.useWorktree) {
|
|
248
|
+
result.isolation = 'worktree';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// buildWavePlan — topological sort + wave grouping
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Group tasks into execution waves based on dependency graph.
|
|
260
|
+
*
|
|
261
|
+
* @param {Array<{ id: string, dependsOn?: string[] }>} tasks
|
|
262
|
+
* @returns {Array<{ wave: number, tasks: Array }>}
|
|
263
|
+
* @throws if circular dependencies are detected
|
|
264
|
+
*/
|
|
265
|
+
function buildWavePlan(tasks) {
|
|
266
|
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Build lookup: taskId → task object, and pre-compute string deps
|
|
271
|
+
const taskMap = new Map();
|
|
272
|
+
const depsMap = new Map(); // taskId → string[] of dependency IDs
|
|
273
|
+
|
|
274
|
+
for (const task of tasks) {
|
|
275
|
+
const id = String(task.id);
|
|
276
|
+
taskMap.set(id, task);
|
|
277
|
+
depsMap.set(id, (task.dependsOn || []).map(String));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate: all referenced dependencies must exist in the task list
|
|
281
|
+
for (const [id, deps] of depsMap) {
|
|
282
|
+
for (const dep of deps) {
|
|
283
|
+
if (!taskMap.has(dep)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Task "${id}" depends on "${dep}" which does not exist in the task list`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const waves = [];
|
|
292
|
+
const resolved = new Set();
|
|
293
|
+
const remaining = new Set(taskMap.keys());
|
|
294
|
+
|
|
295
|
+
while (remaining.size > 0) {
|
|
296
|
+
// Collect tasks whose dependencies are all resolved
|
|
297
|
+
const ready = [];
|
|
298
|
+
for (const id of remaining) {
|
|
299
|
+
if (depsMap.get(id).every(d => resolved.has(d))) {
|
|
300
|
+
ready.push(id);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (ready.length === 0) {
|
|
305
|
+
const stuck = Array.from(remaining).join(', ');
|
|
306
|
+
throw new Error(`Circular dependency detected among tasks: ${stuck}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const waveNum = waves.length + 1;
|
|
310
|
+
waves.push({ wave: waveNum, tasks: ready.map(id => taskMap.get(id)) });
|
|
311
|
+
|
|
312
|
+
for (const id of ready) {
|
|
313
|
+
resolved.add(id);
|
|
314
|
+
remaining.delete(id);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return waves;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// formatWaveProgress — Markdown progress table
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Render a Markdown progress table from a wave plan and results.
|
|
327
|
+
*
|
|
328
|
+
* @param {Array<{ wave: number, tasks: Array<{ id: string, title: string }> }>} wavePlan
|
|
329
|
+
* @param {Map<string, { status: string, commitHash?: string }>} [results] — taskId → result
|
|
330
|
+
* @returns {string} Markdown table
|
|
331
|
+
*/
|
|
332
|
+
function formatWaveProgress(wavePlan, results) {
|
|
333
|
+
const resultMap = results || new Map();
|
|
334
|
+
const lines = [];
|
|
335
|
+
|
|
336
|
+
lines.push('| Wave | Task | Status | Commit |');
|
|
337
|
+
lines.push('|------|------|--------|--------|');
|
|
338
|
+
|
|
339
|
+
for (const wave of wavePlan) {
|
|
340
|
+
for (const task of wave.tasks) {
|
|
341
|
+
const id = String(task.id);
|
|
342
|
+
const r = resultMap.get(id);
|
|
343
|
+
const status = r ? r.status : 'pending';
|
|
344
|
+
const commit = (r && r.commitHash) ? r.commitHash.slice(0, 7) : '';
|
|
345
|
+
const safeTitle = (task.title || id).replace(/\|/g, '\\|');
|
|
346
|
+
lines.push(`| ${wave.wave} | ${safeTitle} | ${status} | ${commit} |`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return lines.join('\n') + '\n';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Exports
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
module.exports = {
|
|
358
|
+
buildTaskPacket,
|
|
359
|
+
executeTask,
|
|
360
|
+
buildWavePlan,
|
|
361
|
+
formatWaveProgress,
|
|
362
|
+
getSkillContent,
|
|
363
|
+
// Exposed for testing
|
|
364
|
+
SKILL_MAP,
|
|
365
|
+
};
|
package/cli/lib/hooks.js
CHANGED
|
@@ -33,10 +33,17 @@ function getHookSettings() {
|
|
|
33
33
|
SessionStart: [{
|
|
34
34
|
hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'session-start.js')}"`, statusMessage: 'Loading session context...' }],
|
|
35
35
|
}],
|
|
36
|
+
PostToolUse: [{
|
|
37
|
+
matcher: 'Bash',
|
|
38
|
+
hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'pre-merge-checklist.js')}"`, timeout: 5 }],
|
|
39
|
+
}, {
|
|
40
|
+
hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'context-monitor.js')}"`, timeout: 3 }],
|
|
41
|
+
}],
|
|
36
42
|
Stop: [{
|
|
37
43
|
hooks: [
|
|
38
44
|
{ type: 'command', command: `node "${path.join(hooksDir, 'session-end.js')}"`, async: true },
|
|
39
45
|
{ type: 'command', command: `node "${path.join(hooksDir, 'cost-tracker.js')}"`, async: true },
|
|
46
|
+
{ type: 'command', command: `node "${path.join(hooksDir, 'improvement-logger.js')}"`, async: true },
|
|
40
47
|
],
|
|
41
48
|
}],
|
|
42
49
|
},
|
|
@@ -48,7 +55,7 @@ function getHookSettings() {
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
// Fallback list if package source is unavailable
|
|
51
|
-
const KNOWN_HOOKS = ['bash-permissions.js', 'session-start.js', 'session-end.js', 'cost-tracker.js', 'statusline-command.js'];
|
|
58
|
+
const KNOWN_HOOKS = ['bash-permissions.js', 'session-start.js', 'session-end.js', 'cost-tracker.js', 'statusline-command.js', 'pre-merge-checklist.js', 'improvement-logger.js', 'context-monitor.js'];
|
|
52
59
|
|
|
53
60
|
function removeHooks() {
|
|
54
61
|
if (!fs.existsSync(PATHS.hooksDir)) return;
|
package/cli/lib/logger.js
CHANGED