chisle 2.0.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/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +48 -0
- package/.clinerules/chisle.md +36 -0
- package/.cursor/rules/chisle.mdc +41 -0
- package/.github/copilot-instructions.md +36 -0
- package/.kiro/steering/chisle.md +40 -0
- package/.windsurf/rules/chisle.md +40 -0
- package/AGENTS.md +46 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/bin/install.js +485 -0
- package/bin/lib/settings.js +148 -0
- package/commands/chisle-audit.toml +5 -0
- package/commands/chisle-help.toml +4 -0
- package/commands/chisle-review.toml +5 -0
- package/commands/chisle.toml +8 -0
- package/hooks/chisle-activate.js +160 -0
- package/hooks/chisle-compress-output.js +266 -0
- package/hooks/chisle-config.js +152 -0
- package/hooks/chisle-mode-tracker.js +88 -0
- package/hooks/chisle-statusline.ps1 +54 -0
- package/hooks/chisle-statusline.sh +94 -0
- package/hooks/package.json +1 -0
- package/package.json +73 -0
- package/rules/chisle-activate.md +13 -0
- package/skills/chisle/SKILL.md +142 -0
- package/skills/chisle-audit/SKILL.md +65 -0
- package/skills/chisle-help/SKILL.md +45 -0
- package/skills/chisle-review/SKILL.md +45 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// chisle — cross-platform installer.
|
|
3
|
+
//
|
|
4
|
+
// Detects the AI coding agents on this machine and installs Chisle for each:
|
|
5
|
+
// - Claude Code → plugin (marketplace add + install), fallback to standalone
|
|
6
|
+
// hooks + settings.json merge + statusline badge
|
|
7
|
+
// - Gemini CLI → gemini extensions install
|
|
8
|
+
// - Codex → fenced ruleset appended to ~/.codex/AGENTS.md
|
|
9
|
+
// - Cursor/Windsurf/Cline/Kiro/Copilot → project rule file dropped into CWD
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// npx chisle auto-detect + install
|
|
13
|
+
// npx chisle --list show detected agents, install nothing
|
|
14
|
+
// npx chisle --only claude install for one agent
|
|
15
|
+
// npx chisle --dry-run print actions, change nothing
|
|
16
|
+
// npx chisle --uninstall remove what we installed
|
|
17
|
+
// npx chisle --help
|
|
18
|
+
//
|
|
19
|
+
// Pure stdlib, zero runtime deps.
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const cp = require('child_process');
|
|
27
|
+
|
|
28
|
+
const SETTINGS = require('./lib/settings');
|
|
29
|
+
|
|
30
|
+
const REPO = 'JayPokale/Chisle';
|
|
31
|
+
const IS_WIN = process.platform === 'win32';
|
|
32
|
+
|
|
33
|
+
// Repo root = parent of bin/. Installed package or local clone both work.
|
|
34
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
35
|
+
|
|
36
|
+
// ── Provider matrix ─────────────────────────────────────────────────────────
|
|
37
|
+
// scope: 'global' installs once for the whole machine/user.
|
|
38
|
+
// 'project' writes a per-repo rule file into the current directory
|
|
39
|
+
// (that's how Cursor/Cline/Copilot rules actually work).
|
|
40
|
+
const PROVIDERS = [
|
|
41
|
+
{ id: 'claude', label: 'Claude Code', scope: 'global', detect: 'cmd:claude' },
|
|
42
|
+
{ id: 'gemini', label: 'Gemini CLI', scope: 'global', detect: 'cmd:gemini' },
|
|
43
|
+
{ id: 'codex', label: 'Codex CLI', scope: 'global', detect: 'cmd:codex||dir:~/.codex' },
|
|
44
|
+
{ id: 'cursor', label: 'Cursor', scope: 'project', detect: 'cmd:cursor||dir:~/.cursor',
|
|
45
|
+
rule: '.cursor/rules/chisle.mdc' },
|
|
46
|
+
{ id: 'windsurf', label: 'Windsurf', scope: 'project', detect: 'cmd:windsurf||dir:~/.windsurf||dir:~/.codeium/windsurf',
|
|
47
|
+
rule: '.windsurf/rules/chisle.md' },
|
|
48
|
+
{ id: 'cline', label: 'Cline', scope: 'project', detect: 'vscode-ext:cline',
|
|
49
|
+
rule: '.clinerules/chisle.md' },
|
|
50
|
+
{ id: 'kiro', label: 'Kiro', scope: 'project', detect: 'cmd:kiro||dir:~/.kiro',
|
|
51
|
+
rule: '.kiro/steering/chisle.md' },
|
|
52
|
+
{ id: 'copilot', label: 'GitHub Copilot',scope: 'project', detect: 'vscode-ext:github.copilot||vscode-ext:github.copilot-chat',
|
|
53
|
+
rule: '.github/copilot-instructions.md' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ── argv ────────────────────────────────────────────────────────────────────
|
|
57
|
+
function parseArgs(argv) {
|
|
58
|
+
const opts = { dryRun: false, force: false, list: false, uninstall: false,
|
|
59
|
+
noColor: false, help: false, only: [], configDir: null };
|
|
60
|
+
for (let i = 0; i < argv.length; i++) {
|
|
61
|
+
const a = argv[i];
|
|
62
|
+
switch (a) {
|
|
63
|
+
case '--dry-run': opts.dryRun = true; break;
|
|
64
|
+
case '--force': opts.force = true; break;
|
|
65
|
+
case '--list': opts.list = true; break;
|
|
66
|
+
case '--uninstall': case '-u': opts.uninstall = true; break;
|
|
67
|
+
case '--no-color': opts.noColor = true; break;
|
|
68
|
+
case '-h': case '--help': opts.help = true; break;
|
|
69
|
+
case '--': break; // npx sometimes forwards the literal end-of-options marker
|
|
70
|
+
case '--only': {
|
|
71
|
+
const v = argv[++i];
|
|
72
|
+
if (!v) die('error: --only requires an agent id');
|
|
73
|
+
opts.only.push(v);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case '--config-dir': {
|
|
77
|
+
const v = argv[++i];
|
|
78
|
+
if (!v || v.startsWith('--')) die('error: --config-dir requires a path');
|
|
79
|
+
opts.configDir = expandHome(v);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
default: die(`error: unknown flag: ${a}\nrun 'npx chisle --help' for usage`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (opts.only.length) {
|
|
86
|
+
const known = new Set(PROVIDERS.map(p => p.id));
|
|
87
|
+
for (const id of opts.only) if (!known.has(id)) die(`error: unknown agent: ${id}\n see 'npx chisle --list'`);
|
|
88
|
+
}
|
|
89
|
+
return opts;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function die(msg) { process.stderr.write(msg + '\n'); process.exit(2); }
|
|
93
|
+
|
|
94
|
+
// ── color ───────────────────────────────────────────────────────────────────
|
|
95
|
+
function makeChalk(noColor) {
|
|
96
|
+
const use = !noColor && process.stdout.isTTY && !process.env.NO_COLOR;
|
|
97
|
+
const wrap = (c) => (s) => use ? `\x1b[${c}m${s}\x1b[0m` : s;
|
|
98
|
+
return { orange: wrap('38;5;172'), dim: wrap('2'), red: wrap('31'), green: wrap('32'), yellow: wrap('33') };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── detection ───────────────────────────────────────────────────────────────
|
|
102
|
+
function expandHome(p) { return p.replace(/^~/, os.homedir()).replace(/^\$HOME/, os.homedir()); }
|
|
103
|
+
|
|
104
|
+
function hasCmd(cmd) {
|
|
105
|
+
try {
|
|
106
|
+
if (IS_WIN) return cp.spawnSync('where', [cmd], { stdio: 'ignore' }).status === 0;
|
|
107
|
+
return cp.spawnSync('sh', ['-c', `command -v '${cmd.replace(/'/g, "")}'`], { stdio: 'ignore' }).status === 0;
|
|
108
|
+
} catch (_) { return false; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function safeStat(p, method) { try { return fs.statSync(p)[method](); } catch (_) { return false; } }
|
|
112
|
+
|
|
113
|
+
function vscodeExt(needle) {
|
|
114
|
+
const roots = ['.vscode/extensions', '.vscode-server/extensions', '.cursor/extensions', '.windsurf/extensions']
|
|
115
|
+
.map(r => path.join(os.homedir(), r));
|
|
116
|
+
const re = new RegExp(needle.replace(/\./g, '\\.'), 'i');
|
|
117
|
+
for (const r of roots) {
|
|
118
|
+
if (!fs.existsSync(r)) continue;
|
|
119
|
+
try { if (fs.readdirSync(r).some(e => re.test(e))) return true; } catch (_) {}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function detectMatch(spec) {
|
|
125
|
+
for (const clause of spec.split('||')) {
|
|
126
|
+
const c = clause.trim();
|
|
127
|
+
const colon = c.indexOf(':');
|
|
128
|
+
const kind = c.slice(0, colon), val = expandHome(c.slice(colon + 1));
|
|
129
|
+
let ok = false;
|
|
130
|
+
if (kind === 'cmd') ok = hasCmd(val);
|
|
131
|
+
else if (kind === 'dir') ok = safeStat(val, 'isDirectory');
|
|
132
|
+
else if (kind === 'file') ok = safeStat(val, 'isFile');
|
|
133
|
+
else if (kind === 'vscode-ext') ok = vscodeExt(val);
|
|
134
|
+
if (ok) return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── spawn helpers ───────────────────────────────────────────────────────────
|
|
140
|
+
function quoteWin(a) {
|
|
141
|
+
if (!IS_WIN) return a;
|
|
142
|
+
if (a === '' || /[\s"]/.test(a)) return '"' + String(a).replace(/\\(?=\\*"|$)/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
143
|
+
return a;
|
|
144
|
+
}
|
|
145
|
+
function spawnX(cmd, args, o) {
|
|
146
|
+
if (IS_WIN) return cp.spawnSync(`${cmd} ${args.map(quoteWin).join(' ')}`, [], Object.assign({ shell: true }, o || {}));
|
|
147
|
+
return cp.spawnSync(cmd, args, o || {});
|
|
148
|
+
}
|
|
149
|
+
function run(cmd, args, dry) {
|
|
150
|
+
if (dry) { process.stdout.write(` would run: ${cmd} ${args.join(' ')}\n`); return { status: 0 }; }
|
|
151
|
+
process.stdout.write(` $ ${cmd} ${args.join(' ')}\n`);
|
|
152
|
+
return spawnX(cmd, args, { stdio: 'inherit' });
|
|
153
|
+
}
|
|
154
|
+
function capture(cmd, args) {
|
|
155
|
+
try { return spawnX(cmd, args, { encoding: 'utf8' }); } catch (_) { return { status: 1, stdout: '', stderr: '' }; }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── config dir ──────────────────────────────────────────────────────────────
|
|
159
|
+
function claudeDir(opts) {
|
|
160
|
+
if (opts.configDir) return opts.configDir;
|
|
161
|
+
if (process.env.CLAUDE_CONFIG_DIR) return process.env.CLAUDE_CONFIG_DIR;
|
|
162
|
+
return path.join(os.homedir(), '.claude');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Claude Code ─────────────────────────────────────────────────────────────
|
|
166
|
+
function installClaude(ctx) {
|
|
167
|
+
const { say, note, warn, opts, results } = ctx;
|
|
168
|
+
results.detected++;
|
|
169
|
+
say('→ Claude Code detected');
|
|
170
|
+
|
|
171
|
+
let pluginOK = false;
|
|
172
|
+
if (hasCmd('claude')) {
|
|
173
|
+
let already = false;
|
|
174
|
+
if (!opts.force) {
|
|
175
|
+
const r = capture('claude', ['plugin', 'list']);
|
|
176
|
+
if (r.status === 0 && /chisle/i.test(r.stdout || '')) already = true;
|
|
177
|
+
}
|
|
178
|
+
if (already) {
|
|
179
|
+
note(' chisle plugin already installed (use --force to reinstall)');
|
|
180
|
+
results.skipped.push(['claude', 'plugin already installed']);
|
|
181
|
+
pluginOK = true;
|
|
182
|
+
} else {
|
|
183
|
+
const r1 = run('claude', ['plugin', 'marketplace', 'add', REPO], opts.dryRun);
|
|
184
|
+
const r2 = run('claude', ['plugin', 'install', 'chisle@chisle'], opts.dryRun);
|
|
185
|
+
if ((r1.status || 0) === 0 && (r2.status || 0) === 0) { results.installed.push('claude'); pluginOK = true; }
|
|
186
|
+
else warn(' claude plugin install failed — falling back to standalone hooks');
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
note(' claude CLI not on PATH — wiring standalone hooks in settings.json');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!pluginOK) {
|
|
193
|
+
const r = installClaudeHooks(ctx);
|
|
194
|
+
if (r === 'ok') results.installed.push('claude-hooks');
|
|
195
|
+
else if (r === 'skip') results.skipped.push(['claude-hooks', 'already wired']);
|
|
196
|
+
else results.failed.push(['claude-hooks', r]);
|
|
197
|
+
} else {
|
|
198
|
+
note(' hooks: plugin manifest handles SessionStart + UserPromptSubmit + PostToolUse');
|
|
199
|
+
}
|
|
200
|
+
process.stdout.write('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Standalone wiring — copy hooks into <configDir>/chisle-hooks/ and merge
|
|
204
|
+
// settings.json. Used when the plugin path is unavailable.
|
|
205
|
+
function installClaudeHooks(ctx) {
|
|
206
|
+
const { opts, warn, note } = ctx;
|
|
207
|
+
const cfg = claudeDir(opts);
|
|
208
|
+
const hooksSrc = path.join(REPO_ROOT, 'hooks');
|
|
209
|
+
const hooksDst = path.join(cfg, 'chisle-hooks');
|
|
210
|
+
const settingsPath = path.join(cfg, 'settings.json');
|
|
211
|
+
const HOOK_FILES = ['package.json', 'chisle-config.js', 'chisle-activate.js',
|
|
212
|
+
'chisle-mode-tracker.js', 'chisle-compress-output.js',
|
|
213
|
+
'chisle-statusline.sh', 'chisle-statusline.ps1'];
|
|
214
|
+
|
|
215
|
+
if (opts.dryRun) {
|
|
216
|
+
note(` would copy ${HOOK_FILES.length} hook files → ${hooksDst}`);
|
|
217
|
+
note(` would merge SessionStart + UserPromptSubmit + PostToolUse + statusline → ${settingsPath}`);
|
|
218
|
+
return 'ok';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fs.mkdirSync(hooksDst, { recursive: true });
|
|
222
|
+
for (const f of HOOK_FILES) {
|
|
223
|
+
const s = path.join(hooksSrc, f);
|
|
224
|
+
if (fs.existsSync(s)) fs.copyFileSync(s, path.join(hooksDst, f));
|
|
225
|
+
}
|
|
226
|
+
try { fs.chmodSync(path.join(hooksDst, 'chisle-statusline.sh'), 0o755); } catch (_) {}
|
|
227
|
+
|
|
228
|
+
const settings = SETTINGS.readSettings(settingsPath);
|
|
229
|
+
if (settings === null) { warn(' settings.json unparseable; not touching it.'); return 'settings.json unparseable'; }
|
|
230
|
+
|
|
231
|
+
const bak = settingsPath + '.bak';
|
|
232
|
+
if (fs.existsSync(settingsPath) && !fs.existsSync(bak)) { try { fs.copyFileSync(settingsPath, bak); } catch (_) {} }
|
|
233
|
+
|
|
234
|
+
const node = process.execPath;
|
|
235
|
+
const activate = path.join(hooksDst, 'chisle-activate.js');
|
|
236
|
+
const tracker = path.join(hooksDst, 'chisle-mode-tracker.js');
|
|
237
|
+
const compress = path.join(hooksDst, 'chisle-compress-output.js');
|
|
238
|
+
|
|
239
|
+
SETTINGS.addCommandHook(settings, 'SessionStart',
|
|
240
|
+
{ command: `"${node}" "${activate}"`, marker: 'chisle-activate', timeout: 5, statusMessage: 'Loading chisle mode...' });
|
|
241
|
+
SETTINGS.addCommandHook(settings, 'UserPromptSubmit',
|
|
242
|
+
{ command: `"${node}" "${tracker}"`, marker: 'chisle-mode-tracker', timeout: 5, statusMessage: 'Tracking chisle mode...' });
|
|
243
|
+
SETTINGS.addCommandHook(settings, 'PostToolUse',
|
|
244
|
+
{ command: `"${node}" "${compress}"`, marker: 'chisle-compress-output', timeout: 10,
|
|
245
|
+
matcher: 'Bash|Agent|WebFetch|WebSearch|Grep|Glob|mcp__.*', statusMessage: 'Compressing tool output...' });
|
|
246
|
+
|
|
247
|
+
const psHost = IS_WIN && hasCmd('pwsh') ? 'pwsh' : (IS_WIN ? 'powershell' : null);
|
|
248
|
+
const slCmd = IS_WIN
|
|
249
|
+
? `${psHost} -NoProfile -ExecutionPolicy Bypass -File "${path.join(hooksDst, 'chisle-statusline.ps1')}"`
|
|
250
|
+
: `bash "${path.join(hooksDst, 'chisle-statusline.sh')}"`;
|
|
251
|
+
if (!settings.statusLine) {
|
|
252
|
+
settings.statusLine = { type: 'command', command: slCmd };
|
|
253
|
+
process.stdout.write(' statusline badge configured.\n');
|
|
254
|
+
} else {
|
|
255
|
+
const existing = typeof settings.statusLine === 'string' ? settings.statusLine : (settings.statusLine.command || '');
|
|
256
|
+
if (existing.includes('chisle-statusline')) process.stdout.write(' statusline badge already configured.\n');
|
|
257
|
+
else process.stdout.write(' NOTE: existing statusline detected — CHISLE badge NOT added (see docs/install-windows.md).\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
SETTINGS.validateHookFields(settings);
|
|
261
|
+
SETTINGS.writeSettings(settingsPath, settings);
|
|
262
|
+
process.stdout.write(` hooks wired in ${settingsPath}\n`);
|
|
263
|
+
return 'ok';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Gemini ──────────────────────────────────────────────────────────────────
|
|
267
|
+
function installGemini(ctx) {
|
|
268
|
+
const { say, note, opts, results } = ctx;
|
|
269
|
+
results.detected++;
|
|
270
|
+
say('→ Gemini CLI detected');
|
|
271
|
+
if (!opts.force) {
|
|
272
|
+
const r = capture('gemini', ['extensions', 'list']);
|
|
273
|
+
if (r.status === 0 && /chisle/i.test(r.stdout || '')) {
|
|
274
|
+
note(' chisle extension already installed (use --force)');
|
|
275
|
+
results.skipped.push(['gemini', 'already installed']); process.stdout.write('\n'); return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const r = run('gemini', ['extensions', 'install', `https://github.com/${REPO}`], opts.dryRun);
|
|
279
|
+
if ((r.status || 0) === 0) results.installed.push('gemini');
|
|
280
|
+
else results.failed.push(['gemini', 'gemini extensions install failed']);
|
|
281
|
+
process.stdout.write('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Codex (fenced ruleset in ~/.codex/AGENTS.md) ────────────────────────────
|
|
285
|
+
const FENCE_BEGIN = '<!-- chisle-begin -->';
|
|
286
|
+
const FENCE_END = '<!-- chisle-end -->';
|
|
287
|
+
|
|
288
|
+
function installCodex(ctx) {
|
|
289
|
+
const { say, note, opts, results } = ctx;
|
|
290
|
+
results.detected++;
|
|
291
|
+
say('→ Codex detected');
|
|
292
|
+
const target = path.join(os.homedir(), '.codex', 'AGENTS.md');
|
|
293
|
+
const body = fs.readFileSync(path.join(REPO_ROOT, 'AGENTS.md'), 'utf8').trimEnd() + '\n';
|
|
294
|
+
const block = `${FENCE_BEGIN}\n${body}${FENCE_END}\n`;
|
|
295
|
+
|
|
296
|
+
if (opts.dryRun) { note(` would write chisle ruleset → ${target}`); results.installed.push('codex'); process.stdout.write('\n'); return; }
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
300
|
+
let existing = '';
|
|
301
|
+
try { existing = fs.readFileSync(target, 'utf8'); } catch (_) {}
|
|
302
|
+
if (existing.includes(FENCE_BEGIN)) {
|
|
303
|
+
if (opts.force) {
|
|
304
|
+
const rewritten = existing.replace(new RegExp(`${FENCE_BEGIN}[\\s\\S]*?${FENCE_END}\\n?`), block);
|
|
305
|
+
fs.writeFileSync(target, rewritten, { mode: 0o644 });
|
|
306
|
+
process.stdout.write(` refreshed ruleset in ${target}\n`);
|
|
307
|
+
} else { note(` ${target} already contains chisle ruleset (--force to refresh)`); }
|
|
308
|
+
results.skipped.push(['codex', 'already present']);
|
|
309
|
+
} else {
|
|
310
|
+
const sep = existing && !existing.endsWith('\n\n') ? (existing.endsWith('\n') ? '\n' : '\n\n') : '';
|
|
311
|
+
fs.writeFileSync(target, existing + sep + block, { mode: 0o644 });
|
|
312
|
+
process.stdout.write(` installed: ${target}\n`);
|
|
313
|
+
results.installed.push('codex');
|
|
314
|
+
}
|
|
315
|
+
} catch (e) { results.failed.push(['codex', (e && e.message) || 'write failed']); }
|
|
316
|
+
process.stdout.write('\n');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Project-scoped rule agents (Cursor/Windsurf/Cline/Kiro/Copilot) ─────────
|
|
320
|
+
function installProjectRule(ctx, prov) {
|
|
321
|
+
const { say, note, warn, opts, results } = ctx;
|
|
322
|
+
results.detected++;
|
|
323
|
+
say(`→ ${prov.label} detected`);
|
|
324
|
+
const src = path.join(REPO_ROOT, prov.rule);
|
|
325
|
+
const dst = path.join(process.cwd(), prov.rule);
|
|
326
|
+
|
|
327
|
+
if (!fs.existsSync(src)) { warn(` missing source rule ${prov.rule} (run scripts/build-rules.js)`); results.failed.push([prov.id, 'source rule missing']); process.stdout.write('\n'); return; }
|
|
328
|
+
|
|
329
|
+
if (opts.dryRun) { note(` would write ${dst} (project-scoped)`); results.installed.push(prov.id); process.stdout.write('\n'); return; }
|
|
330
|
+
|
|
331
|
+
if (fs.existsSync(dst) && !opts.force) {
|
|
332
|
+
note(` ${prov.rule} already in this project (--force to overwrite)`);
|
|
333
|
+
results.skipped.push([prov.id, 'already in project']); process.stdout.write('\n'); return;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
337
|
+
fs.copyFileSync(src, dst);
|
|
338
|
+
process.stdout.write(` installed: ${dst} (project-scoped)\n`);
|
|
339
|
+
results.installed.push(prov.id);
|
|
340
|
+
} catch (e) { results.failed.push([prov.id, (e && e.message) || 'copy failed']); }
|
|
341
|
+
process.stdout.write('\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── uninstall ───────────────────────────────────────────────────────────────
|
|
345
|
+
function uninstall(ctx) {
|
|
346
|
+
const { say, note, opts, c } = ctx;
|
|
347
|
+
say(c.orange('Chisle uninstall'));
|
|
348
|
+
let touched = 0;
|
|
349
|
+
|
|
350
|
+
// Claude plugin
|
|
351
|
+
if (hasCmd('claude') && !opts.dryRun) {
|
|
352
|
+
const r = capture('claude', ['plugin', 'list']);
|
|
353
|
+
if (r.status === 0 && /chisle/i.test(r.stdout || '')) {
|
|
354
|
+
run('claude', ['plugin', 'uninstall', 'chisle@chisle'], opts.dryRun); touched++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Claude standalone hooks + statusline
|
|
359
|
+
const cfg = claudeDir(opts);
|
|
360
|
+
const settingsPath = path.join(cfg, 'settings.json');
|
|
361
|
+
const settings = SETTINGS.readSettings(settingsPath);
|
|
362
|
+
if (settings) {
|
|
363
|
+
const removed = SETTINGS.removeHooks(settings, 'chisle-');
|
|
364
|
+
let slRemoved = false;
|
|
365
|
+
if (settings.statusLine && typeof settings.statusLine.command === 'string'
|
|
366
|
+
&& settings.statusLine.command.includes('chisle-statusline')) { delete settings.statusLine; slRemoved = true; }
|
|
367
|
+
if (removed > 0 || slRemoved) {
|
|
368
|
+
if (!opts.dryRun) { SETTINGS.validateHookFields(settings); SETTINGS.writeSettings(settingsPath, settings); }
|
|
369
|
+
note(` removed ${removed} hook entr${removed === 1 ? 'y' : 'ies'}${slRemoved ? ' + statusline' : ''} from settings.json`);
|
|
370
|
+
touched++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const hooksDst = path.join(cfg, 'chisle-hooks');
|
|
374
|
+
if (fs.existsSync(hooksDst)) { if (!opts.dryRun) fs.rmSync(hooksDst, { recursive: true, force: true }); note(` removed ${hooksDst}`); touched++; }
|
|
375
|
+
|
|
376
|
+
// Flag files
|
|
377
|
+
for (const f of ['.chisle-active', '.chisle-session-turns', '.chisle-statusline-suffix',
|
|
378
|
+
'.chisle-compress-stats.json', '.chisle-compress-last.json', '.chisle-update-check.json']) {
|
|
379
|
+
const p = path.join(cfg, f);
|
|
380
|
+
if (fs.existsSync(p)) { if (!opts.dryRun) { try { fs.unlinkSync(p); } catch (_) {} } note(` removed ${p}`); touched++; }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Codex fenced block
|
|
384
|
+
const codexMd = path.join(os.homedir(), '.codex', 'AGENTS.md');
|
|
385
|
+
if (fs.existsSync(codexMd)) {
|
|
386
|
+
const txt = fs.readFileSync(codexMd, 'utf8');
|
|
387
|
+
if (txt.includes(FENCE_BEGIN)) {
|
|
388
|
+
const stripped = txt.replace(new RegExp(`\\n?${FENCE_BEGIN}[\\s\\S]*?${FENCE_END}\\n?`), '\n').replace(/\n{3,}/g, '\n\n');
|
|
389
|
+
if (!opts.dryRun) fs.writeFileSync(codexMd, stripped, { mode: 0o644 });
|
|
390
|
+
note(` removed chisle block from ${codexMd}`); touched++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
note('');
|
|
395
|
+
note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
|
|
396
|
+
note('live in your project repos — remove them per-project with git if you added them there.');
|
|
397
|
+
say(touched ? c.green(`\nUninstalled. ${touched} item(s) cleaned.`) : c.yellow('\nNothing to uninstall.'));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── help / banner ───────────────────────────────────────────────────────────
|
|
401
|
+
function printHelp(c) {
|
|
402
|
+
process.stdout.write(`${c.orange('chisle')} — maximum-efficiency dev mode installer
|
|
403
|
+
|
|
404
|
+
Usage:
|
|
405
|
+
npx chisle [flags]
|
|
406
|
+
|
|
407
|
+
Flags:
|
|
408
|
+
--list Detect agents and print them; install nothing
|
|
409
|
+
--only <id> Install only for one agent (repeatable)
|
|
410
|
+
--dry-run Print actions, change nothing
|
|
411
|
+
--force Reinstall / overwrite even if already present
|
|
412
|
+
--uninstall, -u Remove what chisle installed
|
|
413
|
+
--config-dir <p> Override Claude config dir (default: $CLAUDE_CONFIG_DIR or ~/.claude)
|
|
414
|
+
--no-color Disable ANSI color
|
|
415
|
+
--help, -h This help
|
|
416
|
+
|
|
417
|
+
Agents: ${PROVIDERS.map(p => p.id).join(', ')}
|
|
418
|
+
|
|
419
|
+
Examples:
|
|
420
|
+
npx chisle # auto-detect + install
|
|
421
|
+
npx chisle --only claude # just Claude Code
|
|
422
|
+
npx chisle --dry-run # preview
|
|
423
|
+
`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function printList(c) {
|
|
427
|
+
process.stdout.write(c.orange('chisle') + ' — detected agents:\n\n');
|
|
428
|
+
for (const p of PROVIDERS) {
|
|
429
|
+
const found = detectMatch(p.detect);
|
|
430
|
+
const mark = found ? c.green('✓') : c.dim('·');
|
|
431
|
+
const scope = p.scope === 'project' ? c.dim(' (project-scoped)') : '';
|
|
432
|
+
process.stdout.write(` ${mark} ${p.label}${scope}\n`);
|
|
433
|
+
}
|
|
434
|
+
process.stdout.write('\nRun ' + c.orange('npx chisle') + ' to install for the detected (✓) agents.\n');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── main ────────────────────────────────────────────────────────────────────
|
|
438
|
+
function main() {
|
|
439
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
440
|
+
const c = makeChalk(opts.noColor);
|
|
441
|
+
|
|
442
|
+
if (opts.help) return printHelp(c);
|
|
443
|
+
if (opts.list) return printList(c);
|
|
444
|
+
|
|
445
|
+
const results = { detected: 0, installed: [], skipped: [], failed: [] };
|
|
446
|
+
const say = (s) => process.stdout.write(s + '\n');
|
|
447
|
+
const note = (s) => process.stdout.write(c.dim(s) + '\n');
|
|
448
|
+
const warn = (s) => process.stdout.write(c.yellow(s) + '\n');
|
|
449
|
+
const ctx = { say, note, warn, opts, results, c };
|
|
450
|
+
|
|
451
|
+
if (opts.uninstall) return uninstall(ctx);
|
|
452
|
+
|
|
453
|
+
say(c.orange('╭─ Chisle installer ─╮'));
|
|
454
|
+
say(c.dim(opts.dryRun ? ' (dry run — nothing will change)' : ' maximum signal, minimum noise'));
|
|
455
|
+
say('');
|
|
456
|
+
|
|
457
|
+
const targets = PROVIDERS.filter(p => opts.only.length ? opts.only.includes(p.id) : detectMatch(p.detect));
|
|
458
|
+
if (targets.length === 0) {
|
|
459
|
+
warn('No supported agents detected.');
|
|
460
|
+
note('Run `npx chisle --list` to see what we look for, or `--only <id>` to force one.');
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (const p of targets) {
|
|
465
|
+
if (p.id === 'claude') installClaude(ctx);
|
|
466
|
+
else if (p.id === 'gemini') installGemini(ctx);
|
|
467
|
+
else if (p.id === 'codex') installCodex(ctx);
|
|
468
|
+
else installProjectRule(ctx, p);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Summary
|
|
472
|
+
say(c.orange('── summary ──'));
|
|
473
|
+
say(` detected: ${results.detected}`);
|
|
474
|
+
if (results.installed.length) say(c.green(` installed: ${results.installed.join(', ')}`));
|
|
475
|
+
if (results.skipped.length) say(c.dim(` skipped: ${results.skipped.map(s => s[0]).join(', ')}`));
|
|
476
|
+
if (results.failed.length) say(c.red(` failed: ${results.failed.map(s => s[0] + ' (' + s[1] + ')').join(', ')}`));
|
|
477
|
+
say('');
|
|
478
|
+
if (!opts.dryRun && results.installed.length) {
|
|
479
|
+
say(c.orange('Done.') + ' Restart your agent. Type ' + c.orange('/chisle') + ' (Claude Code) or just start coding.');
|
|
480
|
+
note('If Chisle earns its keep, a star helps others find it: https://github.com/JayPokale/Chisle');
|
|
481
|
+
}
|
|
482
|
+
process.exit(results.failed.length ? 1 : 0);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
main();
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// chisle — JSONC-tolerant settings.json read/write + defensive hook validation.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code applies strict Zod to settings.json: one malformed hook silently
|
|
4
|
+
// discards the WHOLE file. So every mutation validates before write, and reads
|
|
5
|
+
// tolerate the // and /* */ comments users sometimes add.
|
|
6
|
+
//
|
|
7
|
+
// Public API:
|
|
8
|
+
// readSettings(path) → object, {}, or null on hard parse failure
|
|
9
|
+
// writeSettings(path, obj) → atomic write, mode 0600
|
|
10
|
+
// stripJsonComments(src) → comment + trailing-comma stripped
|
|
11
|
+
// validateHookFields(settings) → mutates: drops malformed hook entries
|
|
12
|
+
// hasHook(settings, ev, marker) → idempotency probe
|
|
13
|
+
// addCommandHook(settings, ev, opts) → no-op if marker already present
|
|
14
|
+
// removeHooks(settings, marker) → uninstall helper
|
|
15
|
+
//
|
|
16
|
+
// Pure stdlib, CommonJS, Node ≥18.
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
|
|
24
|
+
// ── stripJsonComments ──────────────────────────────────────────────────────
|
|
25
|
+
// State machine tracking string + escape state so a comment-looking sequence
|
|
26
|
+
// inside a quoted string survives. Trailing commas swept in a final pass.
|
|
27
|
+
function stripJsonComments(src) {
|
|
28
|
+
if (typeof src !== 'string') return src;
|
|
29
|
+
let out = '';
|
|
30
|
+
let i = 0;
|
|
31
|
+
const n = src.length;
|
|
32
|
+
let inString = false, stringChar = '', inLine = false, inBlock = false;
|
|
33
|
+
while (i < n) {
|
|
34
|
+
const c = src[i];
|
|
35
|
+
const next = i + 1 < n ? src[i + 1] : '';
|
|
36
|
+
if (inLine) { if (c === '\n') { inLine = false; out += c; } i++; continue; }
|
|
37
|
+
if (inBlock) { if (c === '*' && next === '/') { inBlock = false; i += 2; continue; } i++; continue; }
|
|
38
|
+
if (inString) {
|
|
39
|
+
out += c;
|
|
40
|
+
if (c === '\\') { if (i + 1 < n) { out += src[i + 1]; i += 2; continue; } }
|
|
41
|
+
if (c === stringChar) inString = false;
|
|
42
|
+
i++; continue;
|
|
43
|
+
}
|
|
44
|
+
if (c === '"' || c === "'") { inString = true; stringChar = c; out += c; i++; continue; }
|
|
45
|
+
if (c === '/' && next === '/') { inLine = true; i += 2; continue; }
|
|
46
|
+
if (c === '/' && next === '*') { inBlock = true; i += 2; continue; }
|
|
47
|
+
out += c; i++;
|
|
48
|
+
}
|
|
49
|
+
return out.replace(/,(\s*[}\]])/g, '$1');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── readSettings ───────────────────────────────────────────────────────────
|
|
53
|
+
// Strict JSON fast path → JSONC fallback → null (never silently clobber a
|
|
54
|
+
// recoverable file with {}).
|
|
55
|
+
function readSettings(p) {
|
|
56
|
+
if (!fs.existsSync(p)) return {};
|
|
57
|
+
let raw;
|
|
58
|
+
try { raw = fs.readFileSync(p, 'utf8'); }
|
|
59
|
+
catch (e) { process.stderr.write(`chisle: cannot read ${p}: ${e.message}\n`); return null; }
|
|
60
|
+
if (!raw.trim()) return {};
|
|
61
|
+
try { return JSON.parse(raw.replace(/^/, '')); } catch (_) {}
|
|
62
|
+
try { return JSON.parse(stripJsonComments(raw.replace(/^/, ''))); }
|
|
63
|
+
catch (e) {
|
|
64
|
+
process.stderr.write(`chisle: warning — ${p} is not valid JSON/JSONC: ${e.message}\n`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── writeSettings ──────────────────────────────────────────────────────────
|
|
70
|
+
// Atomic: temp + rename. mode 0600 (settings often holds tokens).
|
|
71
|
+
function writeSettings(p, obj) {
|
|
72
|
+
const dir = path.dirname(p);
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
const tmp = path.join(dir, `.${path.basename(p)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
|
|
75
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
|
|
76
|
+
fs.renameSync(tmp, p);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── validateHookFields ─────────────────────────────────────────────────────
|
|
80
|
+
// Mutate-to-valid before write. Drops any entry Claude Code's Zod would reject.
|
|
81
|
+
function validateHookFields(settings) {
|
|
82
|
+
if (!settings || typeof settings !== 'object') return settings;
|
|
83
|
+
if (!settings.hooks || typeof settings.hooks !== 'object') return settings;
|
|
84
|
+
for (const ev of Object.keys(settings.hooks)) {
|
|
85
|
+
const arr = settings.hooks[ev];
|
|
86
|
+
if (!Array.isArray(arr)) { delete settings.hooks[ev]; continue; }
|
|
87
|
+
settings.hooks[ev] = arr.filter(entry => {
|
|
88
|
+
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.hooks)) return false;
|
|
89
|
+
entry.hooks = entry.hooks.filter(h => {
|
|
90
|
+
if (!h || typeof h !== 'object') return false;
|
|
91
|
+
if (h.type === 'command') return typeof h.command === 'string' && h.command.length > 0;
|
|
92
|
+
return false;
|
|
93
|
+
});
|
|
94
|
+
return entry.hooks.length > 0;
|
|
95
|
+
});
|
|
96
|
+
if (settings.hooks[ev].length === 0) delete settings.hooks[ev];
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
99
|
+
return settings;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── idempotency probe ──────────────────────────────────────────────────────
|
|
103
|
+
function hasHook(settings, event, marker) {
|
|
104
|
+
const arr = settings && settings.hooks && settings.hooks[event];
|
|
105
|
+
if (!Array.isArray(arr)) return false;
|
|
106
|
+
return arr.some(e => e && Array.isArray(e.hooks) &&
|
|
107
|
+
e.hooks.some(h => h && typeof h.command === 'string' && h.command.includes(marker)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── addCommandHook ─────────────────────────────────────────────────────────
|
|
111
|
+
function addCommandHook(settings, event, opts) {
|
|
112
|
+
if (!settings.hooks) settings.hooks = {};
|
|
113
|
+
if (!Array.isArray(settings.hooks[event])) settings.hooks[event] = [];
|
|
114
|
+
const marker = opts.marker || opts.command;
|
|
115
|
+
if (hasHook(settings, event, marker)) return false;
|
|
116
|
+
const hook = { type: 'command', command: opts.command };
|
|
117
|
+
if (typeof opts.timeout === 'number') hook.timeout = opts.timeout;
|
|
118
|
+
if (typeof opts.statusMessage === 'string') hook.statusMessage = opts.statusMessage;
|
|
119
|
+
const entry = { hooks: [hook] };
|
|
120
|
+
if (typeof opts.matcher === 'string') entry.matcher = opts.matcher;
|
|
121
|
+
settings.hooks[event].push(entry);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── removeHooks (uninstall) ────────────────────────────────────────────────
|
|
126
|
+
function removeHooks(settings, marker) {
|
|
127
|
+
if (!settings || !settings.hooks) return 0;
|
|
128
|
+
validateHookFields(settings);
|
|
129
|
+
if (!settings.hooks) return 0;
|
|
130
|
+
let removed = 0;
|
|
131
|
+
for (const ev of Object.keys(settings.hooks)) {
|
|
132
|
+
if (!Array.isArray(settings.hooks[ev])) { delete settings.hooks[ev]; continue; }
|
|
133
|
+
const before = settings.hooks[ev].length;
|
|
134
|
+
settings.hooks[ev] = settings.hooks[ev].filter(entry => {
|
|
135
|
+
if (!entry || !Array.isArray(entry.hooks)) return true;
|
|
136
|
+
return !entry.hooks.some(h => h && typeof h.command === 'string' && h.command.includes(marker));
|
|
137
|
+
});
|
|
138
|
+
removed += before - settings.hooks[ev].length;
|
|
139
|
+
if (settings.hooks[ev].length === 0) delete settings.hooks[ev];
|
|
140
|
+
}
|
|
141
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
142
|
+
return removed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
stripJsonComments, readSettings, writeSettings,
|
|
147
|
+
validateHookFields, hasHook, addCommandHook, removeHooks,
|
|
148
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# chisle-audit — one-shot efficiency audit across both axes
|
|
2
|
+
[command]
|
|
3
|
+
name = "chisle-audit"
|
|
4
|
+
description = "Audit a diff, file, or repo for BOTH over-engineered code and bloated prose/docs. Ranked report, biggest cut first. Changes nothing."
|
|
5
|
+
argument-hint = "[path | repo | empty for current diff]"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# chisle slash command
|
|
2
|
+
# Maximum-efficiency dev mode
|
|
3
|
+
# Usage: /chisle [lite|full|ultra]
|
|
4
|
+
|
|
5
|
+
[command]
|
|
6
|
+
name = "chisle"
|
|
7
|
+
description = "Activate chisle mode: zero-fluff prose + YAGNI ladder code decisions. Levels: lite, full (default), ultra."
|
|
8
|
+
argument-hint = "[lite|full|ultra]"
|