@aladac/hu 0.1.0-a1
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/.tool-versions +1 -0
- package/CLAUDE.md +122 -0
- package/HOOKS-DATA-INTEGRATION.md +457 -0
- package/SAMPLE.md +378 -0
- package/TODO.md +25 -0
- package/biome.json +51 -0
- package/commands/bootstrap.md +13 -0
- package/commands/c.md +1 -0
- package/commands/check-name.md +62 -0
- package/commands/disk.md +141 -0
- package/commands/docs/archive.md +27 -0
- package/commands/docs/check-internal.md +53 -0
- package/commands/docs/cleanup.md +65 -0
- package/commands/docs/consolidate.md +72 -0
- package/commands/docs/get.md +101 -0
- package/commands/docs/list.md +61 -0
- package/commands/docs/sync.md +64 -0
- package/commands/docs/update.md +49 -0
- package/commands/plans/clear.md +23 -0
- package/commands/plans/create.md +71 -0
- package/commands/plans/list.md +21 -0
- package/commands/plans/sync.md +38 -0
- package/commands/reinstall.md +20 -0
- package/commands/replicate.md +303 -0
- package/commands/warp.md +0 -0
- package/doc/README.md +35 -0
- package/doc/claude-code/capabilities.md +202 -0
- package/doc/claude-code/directory-structure.md +246 -0
- package/doc/claude-code/hooks.md +348 -0
- package/doc/claude-code/overview.md +109 -0
- package/doc/claude-code/plugins.md +273 -0
- package/doc/claude-code/sdk-protocols.md +202 -0
- package/document-manifest.toml +29 -0
- package/justfile +39 -0
- package/package.json +33 -0
- package/plans/compiled-watching-feather.md +217 -0
- package/plans/crispy-crafting-pnueli.md +103 -0
- package/plans/greedy-booping-coral.md +146 -0
- package/plans/imperative-sleeping-flamingo.md +192 -0
- package/plans/jaunty-sprouting-marble.md +171 -0
- package/plans/jiggly-discovering-lake.md +68 -0
- package/plans/magical-nibbling-spark.md +144 -0
- package/plans/mellow-kindling-acorn.md +110 -0
- package/plans/recursive-questing-engelbart.md +65 -0
- package/plans/serialized-roaming-kernighan.md +227 -0
- package/plans/structured-wondering-wirth.md +230 -0
- package/plans/vectorized-dreaming-iverson.md +191 -0
- package/plans/velvety-enchanting-ocean.md +92 -0
- package/plans/wiggly-sparking-pixel.md +48 -0
- package/plans/zippy-shimmying-fox.md +188 -0
- package/plugins/installed_plugins.json +4 -0
- package/sample-hooks.json +298 -0
- package/settings.json +24 -0
- package/settings.local.json +7 -0
- package/src/commands/bump.ts +130 -0
- package/src/commands/disk.ts +419 -0
- package/src/commands/docs.ts +729 -0
- package/src/commands/plans.ts +259 -0
- package/src/commands/utils.ts +299 -0
- package/src/index.ts +26 -0
- package/src/lib/colors.ts +87 -0
- package/src/lib/exec.ts +25 -0
- package/src/lib/fs.ts +119 -0
- package/src/lib/html.ts +205 -0
- package/src/lib/spinner.ts +42 -0
- package/src/types/index.ts +61 -0
- package/tests/lib/colors.test.ts +69 -0
- package/tests/lib/fs.test.ts +65 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan management commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as readline from 'node:readline';
|
|
8
|
+
import { defineCommand } from 'citty';
|
|
9
|
+
import { c } from '../lib/colors.ts';
|
|
10
|
+
import { PLANS_DIR, exists, getStats, readFile } from '../lib/fs.ts';
|
|
11
|
+
import type { PlanInfo } from '../types/index.ts';
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────
|
|
14
|
+
// List Plans
|
|
15
|
+
// ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function extractTitle(content: string): string | null {
|
|
18
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
19
|
+
return match ? match[1].trim() : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractBullets(content: string, limit = 3): string[] {
|
|
23
|
+
const bullets: string[] = [];
|
|
24
|
+
const lines = content.split('\n');
|
|
25
|
+
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const match = line.match(/^\s*[-*]\s+(.+)$/) || line.match(/^\s*\d+\.\s+(.+)$/);
|
|
28
|
+
if (match) {
|
|
29
|
+
const text = match[1].trim();
|
|
30
|
+
const clean = text.replace(/^\[[ x]\]\s*/i, '');
|
|
31
|
+
if (clean.length > 0 && clean.length < 100) {
|
|
32
|
+
bullets.push(clean);
|
|
33
|
+
if (bullets.length >= limit) break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return bullets;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractPhases(content: string): string[] {
|
|
42
|
+
const phases: string[] = [];
|
|
43
|
+
const matches = content.matchAll(/^##\s+(?:Phase\s+\d+[:\s]*)?(.+)$/gm);
|
|
44
|
+
for (const match of matches) {
|
|
45
|
+
phases.push(match[1].trim());
|
|
46
|
+
}
|
|
47
|
+
return phases;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPlanInfo(filePath: string, full: boolean): PlanInfo | null {
|
|
51
|
+
const stats = getStats(filePath);
|
|
52
|
+
const content = readFile(filePath);
|
|
53
|
+
if (!stats || !content) return null;
|
|
54
|
+
|
|
55
|
+
const filename = path.basename(filePath, '.md');
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
filename,
|
|
59
|
+
path: filePath,
|
|
60
|
+
title: extractTitle(content) || filename,
|
|
61
|
+
bullets: extractBullets(content, full ? 6 : 3),
|
|
62
|
+
phases: extractPhases(content),
|
|
63
|
+
size: stats.size,
|
|
64
|
+
modified: stats.mtime.toISOString().split('T')[0],
|
|
65
|
+
resumeCmd: `claude -r "${filename}"`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function listPlans(format: string, full: boolean): void {
|
|
70
|
+
if (!exists(PLANS_DIR)) {
|
|
71
|
+
if (format === 'json') {
|
|
72
|
+
console.log(JSON.stringify({ plans: [], count: 0 }));
|
|
73
|
+
} else {
|
|
74
|
+
console.log('No plans directory found.');
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const files = fs
|
|
80
|
+
.readdirSync(PLANS_DIR)
|
|
81
|
+
.filter((f) => f.endsWith('.md'))
|
|
82
|
+
.map((f) => path.join(PLANS_DIR, f));
|
|
83
|
+
|
|
84
|
+
if (files.length === 0) {
|
|
85
|
+
if (format === 'json') {
|
|
86
|
+
console.log(JSON.stringify({ plans: [], count: 0 }));
|
|
87
|
+
} else {
|
|
88
|
+
console.log('No plans found in ~/.claude/plans/');
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const plans = files
|
|
94
|
+
.map((f) => getPlanInfo(f, full))
|
|
95
|
+
.filter((plan): plan is PlanInfo => plan !== null);
|
|
96
|
+
plans.sort((a, b) => b.modified.localeCompare(a.modified));
|
|
97
|
+
|
|
98
|
+
if (format === 'json') {
|
|
99
|
+
console.log(JSON.stringify({ plans, count: plans.length }, null, 2));
|
|
100
|
+
} else if (format === 'simple') {
|
|
101
|
+
for (const plan of plans) {
|
|
102
|
+
console.log(plan.filename);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`Found ${plans.length} plan(s) in ~/.claude/plans/\n`);
|
|
106
|
+
|
|
107
|
+
for (const plan of plans) {
|
|
108
|
+
console.log(`## ${plan.filename} - ${plan.title}`);
|
|
109
|
+
|
|
110
|
+
if (plan.phases.length > 0) {
|
|
111
|
+
console.log(
|
|
112
|
+
` Phases: ${plan.phases.slice(0, 4).join(', ')}${plan.phases.length > 4 ? '...' : ''}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (plan.bullets.length > 0) {
|
|
117
|
+
for (const bullet of plan.bullets) {
|
|
118
|
+
console.log(` - ${bullet}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(` Modified: ${plan.modified}`);
|
|
123
|
+
console.log(` Resume: ${plan.resumeCmd}`);
|
|
124
|
+
console.log('---');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─────────────────────────────────────────────────────────────
|
|
130
|
+
// Clear Plans
|
|
131
|
+
// ─────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async function confirm(message: string): Promise<boolean> {
|
|
134
|
+
const rl = readline.createInterface({
|
|
135
|
+
input: process.stdin,
|
|
136
|
+
output: process.stdout,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function clearPlans(options: { dryRun?: boolean; force?: boolean }): Promise<void> {
|
|
148
|
+
if (!exists(PLANS_DIR)) {
|
|
149
|
+
console.log('No plans directory found.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const files = fs.readdirSync(PLANS_DIR).filter((f) => f.endsWith('.md'));
|
|
154
|
+
|
|
155
|
+
if (files.length === 0) {
|
|
156
|
+
console.log('No plans to clear.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Show what will be deleted
|
|
161
|
+
console.log(`Found ${files.length} plan(s) to delete:`);
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const filePath = path.join(PLANS_DIR, file);
|
|
164
|
+
const content = readFile(filePath);
|
|
165
|
+
const title = content?.match(/^#\s+(.+)$/m)?.[1] || file;
|
|
166
|
+
console.log(` - ${file} — ${title}`);
|
|
167
|
+
}
|
|
168
|
+
console.log();
|
|
169
|
+
|
|
170
|
+
if (options.dryRun) {
|
|
171
|
+
console.log(`${c.yellow}Dry run:${c.reset} No files deleted.`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Confirm unless --force
|
|
176
|
+
if (!options.force) {
|
|
177
|
+
const confirmed = await confirm(
|
|
178
|
+
`${c.yellow}Delete all ${files.length} plans? (This cannot be undone)${c.reset}`,
|
|
179
|
+
);
|
|
180
|
+
if (!confirmed) {
|
|
181
|
+
console.log('Cancelled.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
fs.unlinkSync(path.join(PLANS_DIR, file));
|
|
188
|
+
}
|
|
189
|
+
console.log(`${c.green}✓${c.reset} Deleted ${files.length} plan(s)`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────────────────────────────────────
|
|
193
|
+
// Subcommands
|
|
194
|
+
// ─────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
const listCommand = defineCommand({
|
|
197
|
+
meta: {
|
|
198
|
+
name: 'list',
|
|
199
|
+
description: 'List saved plans from ~/.claude/plans/',
|
|
200
|
+
},
|
|
201
|
+
args: {
|
|
202
|
+
json: {
|
|
203
|
+
type: 'boolean',
|
|
204
|
+
alias: 'j',
|
|
205
|
+
description: 'Output as JSON',
|
|
206
|
+
},
|
|
207
|
+
simple: {
|
|
208
|
+
type: 'boolean',
|
|
209
|
+
alias: 's',
|
|
210
|
+
description: 'Output as simple list (filenames only)',
|
|
211
|
+
},
|
|
212
|
+
full: {
|
|
213
|
+
type: 'boolean',
|
|
214
|
+
alias: 'f',
|
|
215
|
+
description: 'Show more content preview',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
run: ({ args }) => {
|
|
219
|
+
const format = args.json ? 'json' : args.simple ? 'simple' : 'default';
|
|
220
|
+
listPlans(format, args.full || false);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const clearCommand = defineCommand({
|
|
225
|
+
meta: {
|
|
226
|
+
name: 'clear',
|
|
227
|
+
description: 'Delete all plans from ~/.claude/plans/',
|
|
228
|
+
},
|
|
229
|
+
args: {
|
|
230
|
+
dryRun: {
|
|
231
|
+
type: 'boolean',
|
|
232
|
+
alias: 'd',
|
|
233
|
+
description: 'Show what would be deleted without deleting',
|
|
234
|
+
},
|
|
235
|
+
force: {
|
|
236
|
+
type: 'boolean',
|
|
237
|
+
alias: 'f',
|
|
238
|
+
description: 'Skip confirmation prompt',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
run: async ({ args }) => {
|
|
242
|
+
await clearPlans({ dryRun: args.dryRun, force: args.force });
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ─────────────────────────────────────────────────────────────
|
|
247
|
+
// Main Command
|
|
248
|
+
// ─────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
export const plansCommand = defineCommand({
|
|
251
|
+
meta: {
|
|
252
|
+
name: 'plans',
|
|
253
|
+
description: 'Plan management tools',
|
|
254
|
+
},
|
|
255
|
+
subCommands: {
|
|
256
|
+
list: listCommand,
|
|
257
|
+
clear: clearCommand,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { defineCommand } from 'citty';
|
|
8
|
+
import { c } from '../lib/colors.ts';
|
|
9
|
+
import { exists, findRepoRoot, readFile, writeFile } from '../lib/fs.ts';
|
|
10
|
+
import { fetchAndConvert } from '../lib/html.ts';
|
|
11
|
+
import type { CheckboxResult } from '../types/index.ts';
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────
|
|
14
|
+
// Fetch HTML
|
|
15
|
+
// ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
async function fetchHtml(
|
|
18
|
+
url: string,
|
|
19
|
+
options: {
|
|
20
|
+
output?: string;
|
|
21
|
+
selector?: string;
|
|
22
|
+
title?: boolean;
|
|
23
|
+
clean?: boolean;
|
|
24
|
+
raw?: boolean;
|
|
25
|
+
},
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const markdown = await fetchAndConvert(url, {
|
|
28
|
+
selector: options.selector,
|
|
29
|
+
includeTitle: options.title,
|
|
30
|
+
clean: options.clean,
|
|
31
|
+
raw: options.raw,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (options.output) {
|
|
35
|
+
writeFile(options.output, markdown);
|
|
36
|
+
console.error(`Written to ${options.output} (${markdown.length} bytes)`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log(markdown);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────
|
|
43
|
+
// Sync Checkboxes
|
|
44
|
+
// ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function processFile(filePath: string): CheckboxResult {
|
|
47
|
+
if (!exists(filePath)) {
|
|
48
|
+
return { file: filePath, exists: false, changed: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const content = readFile(filePath);
|
|
52
|
+
if (!content) {
|
|
53
|
+
return { file: filePath, exists: false, changed: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lines = content.split('\n');
|
|
57
|
+
|
|
58
|
+
const beforePending = (content.match(/- \[ \]/g) || []).length;
|
|
59
|
+
const beforeCompleted = (content.match(/- \[x\]/gi) || []).length;
|
|
60
|
+
|
|
61
|
+
if (beforePending === 0 && beforeCompleted === 0) {
|
|
62
|
+
return {
|
|
63
|
+
file: filePath,
|
|
64
|
+
exists: true,
|
|
65
|
+
changed: false,
|
|
66
|
+
reason: 'no checkboxes found',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (beforePending === 0 && beforeCompleted > 0) {
|
|
71
|
+
return {
|
|
72
|
+
file: filePath,
|
|
73
|
+
exists: true,
|
|
74
|
+
changed: true,
|
|
75
|
+
delete: true,
|
|
76
|
+
before: { pending: beforePending, completed: beforeCompleted },
|
|
77
|
+
after: { pending: 0, completed: 0 },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result: string[] = [];
|
|
82
|
+
let currentSection: string[] = [];
|
|
83
|
+
let currentSectionHeader: string | null = null;
|
|
84
|
+
let sectionHasPending = false;
|
|
85
|
+
|
|
86
|
+
function flushSection(): void {
|
|
87
|
+
if (currentSectionHeader === null) {
|
|
88
|
+
result.push(...currentSection);
|
|
89
|
+
} else if (sectionHasPending) {
|
|
90
|
+
result.push(currentSectionHeader);
|
|
91
|
+
for (const line of currentSection) {
|
|
92
|
+
if (!line.match(/- \[x\]/i)) {
|
|
93
|
+
result.push(line);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
currentSection = [];
|
|
98
|
+
currentSectionHeader = null;
|
|
99
|
+
sectionHasPending = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (line.match(/^#{1,6}\s/)) {
|
|
104
|
+
flushSection();
|
|
105
|
+
currentSectionHeader = line;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (line.match(/- \[ \]/)) {
|
|
110
|
+
sectionHasPending = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
currentSection.push(line);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
flushSection();
|
|
117
|
+
|
|
118
|
+
let newContent = result.join('\n');
|
|
119
|
+
newContent = `${newContent.replace(/\n{3,}/g, '\n\n').trim()}\n`;
|
|
120
|
+
|
|
121
|
+
const afterPending = (newContent.match(/- \[ \]/g) || []).length;
|
|
122
|
+
const afterCompleted = (newContent.match(/- \[x\]/gi) || []).length;
|
|
123
|
+
|
|
124
|
+
const changed = newContent !== content;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
file: filePath,
|
|
128
|
+
exists: true,
|
|
129
|
+
changed,
|
|
130
|
+
delete: false,
|
|
131
|
+
before: { pending: beforePending, completed: beforeCompleted },
|
|
132
|
+
after: { pending: afterPending, completed: afterCompleted },
|
|
133
|
+
removed: beforeCompleted - afterCompleted,
|
|
134
|
+
newContent: changed ? newContent : null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function syncCheckboxes(
|
|
139
|
+
files: string[],
|
|
140
|
+
options: { dryRun?: boolean; verbose?: boolean; json?: boolean },
|
|
141
|
+
): void {
|
|
142
|
+
const repoRoot = findRepoRoot();
|
|
143
|
+
let filesToProcess = files;
|
|
144
|
+
|
|
145
|
+
if (filesToProcess.length === 0) {
|
|
146
|
+
filesToProcess = ['TODO.md', 'PLAN.md'].map((f) => path.join(repoRoot, f));
|
|
147
|
+
} else {
|
|
148
|
+
filesToProcess = filesToProcess.map((f) =>
|
|
149
|
+
path.isAbsolute(f) ? f : path.join(process.cwd(), f),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const results: CheckboxResult[] = [];
|
|
154
|
+
for (const file of filesToProcess) {
|
|
155
|
+
const result = processFile(file);
|
|
156
|
+
results.push(result);
|
|
157
|
+
|
|
158
|
+
if (!options.dryRun && result.changed) {
|
|
159
|
+
if (result.delete) {
|
|
160
|
+
fs.unlinkSync(file);
|
|
161
|
+
} else if (result.newContent) {
|
|
162
|
+
writeFile(file, result.newContent);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (options.json) {
|
|
168
|
+
console.log(JSON.stringify(results, null, 2));
|
|
169
|
+
} else {
|
|
170
|
+
for (const r of results) {
|
|
171
|
+
const name = path.basename(r.file);
|
|
172
|
+
|
|
173
|
+
if (!r.exists) {
|
|
174
|
+
console.log(`${name}: not found`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!r.changed) {
|
|
179
|
+
console.log(`${name}: unchanged${r.reason ? ` (${r.reason})` : ''}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (r.delete) {
|
|
184
|
+
const action = options.dryRun ? 'would delete' : 'deleted';
|
|
185
|
+
console.log(`${name}: ${action} (all ${r.before?.completed} items complete)`);
|
|
186
|
+
} else {
|
|
187
|
+
const action = options.dryRun ? 'would remove' : 'removed';
|
|
188
|
+
console.log(
|
|
189
|
+
`${name}: ${action} ${r.removed} completed items (${r.after?.pending} pending remain)`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (options.verbose && r.before) {
|
|
194
|
+
console.log(` Before: ${r.before.pending} pending, ${r.before.completed} completed`);
|
|
195
|
+
console.log(` After: ${r.after?.pending} pending, ${r.after?.completed} completed`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─────────────────────────────────────────────────────────────
|
|
202
|
+
// Subcommands
|
|
203
|
+
// ─────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
const fetchHtmlCommand = defineCommand({
|
|
206
|
+
meta: {
|
|
207
|
+
name: 'fetch-html',
|
|
208
|
+
description: 'Fetch a URL and convert HTML to markdown',
|
|
209
|
+
},
|
|
210
|
+
args: {
|
|
211
|
+
url: {
|
|
212
|
+
type: 'positional',
|
|
213
|
+
description: 'URL to fetch',
|
|
214
|
+
required: true,
|
|
215
|
+
},
|
|
216
|
+
output: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
alias: 'o',
|
|
219
|
+
description: 'Write to file instead of stdout',
|
|
220
|
+
},
|
|
221
|
+
selector: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
alias: 's',
|
|
224
|
+
description: 'CSS selector for main content',
|
|
225
|
+
},
|
|
226
|
+
title: {
|
|
227
|
+
type: 'boolean',
|
|
228
|
+
alias: 't',
|
|
229
|
+
description: 'Include page title as H1',
|
|
230
|
+
},
|
|
231
|
+
clean: {
|
|
232
|
+
type: 'boolean',
|
|
233
|
+
alias: 'c',
|
|
234
|
+
description: 'Extra cleaning (remove nav, footer, ads)',
|
|
235
|
+
},
|
|
236
|
+
raw: {
|
|
237
|
+
type: 'boolean',
|
|
238
|
+
alias: 'r',
|
|
239
|
+
description: 'Output raw markdown without post-processing',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
run: async ({ args }) => {
|
|
243
|
+
await fetchHtml(args.url as string, {
|
|
244
|
+
output: args.output as string | undefined,
|
|
245
|
+
selector: args.selector as string | undefined,
|
|
246
|
+
title: args.title,
|
|
247
|
+
clean: args.clean,
|
|
248
|
+
raw: args.raw,
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const syncCheckboxesCommand = defineCommand({
|
|
254
|
+
meta: {
|
|
255
|
+
name: 'sync-checkboxes',
|
|
256
|
+
description: 'Remove completed items from TODO.md/PLAN.md',
|
|
257
|
+
},
|
|
258
|
+
args: {
|
|
259
|
+
dryRun: {
|
|
260
|
+
type: 'boolean',
|
|
261
|
+
alias: 'd',
|
|
262
|
+
description: 'Show what would change without modifying files',
|
|
263
|
+
},
|
|
264
|
+
verbose: {
|
|
265
|
+
type: 'boolean',
|
|
266
|
+
alias: 'v',
|
|
267
|
+
description: 'Show detailed output',
|
|
268
|
+
},
|
|
269
|
+
json: {
|
|
270
|
+
type: 'boolean',
|
|
271
|
+
alias: 'j',
|
|
272
|
+
description: 'Output results as JSON',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
run: ({ args, rawArgs }) => {
|
|
276
|
+
// rawArgs contains everything after --, or non-flag arguments
|
|
277
|
+
const files = rawArgs.filter((arg) => !arg.startsWith('-'));
|
|
278
|
+
syncCheckboxes(files, {
|
|
279
|
+
dryRun: args.dryRun,
|
|
280
|
+
verbose: args.verbose,
|
|
281
|
+
json: args.json,
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─────────────────────────────────────────────────────────────
|
|
287
|
+
// Main Command
|
|
288
|
+
// ─────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
export const utilsCommand = defineCommand({
|
|
291
|
+
meta: {
|
|
292
|
+
name: 'utils',
|
|
293
|
+
description: 'Utility commands',
|
|
294
|
+
},
|
|
295
|
+
subCommands: {
|
|
296
|
+
'fetch-html': fetchHtmlCommand,
|
|
297
|
+
'sync-checkboxes': syncCheckboxesCommand,
|
|
298
|
+
},
|
|
299
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hu - CLI tools for Claude Code workflows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defineCommand, runMain } from 'citty';
|
|
7
|
+
import { diskCommand } from './commands/disk.ts';
|
|
8
|
+
import { docsCommand } from './commands/docs.ts';
|
|
9
|
+
import { plansCommand } from './commands/plans.ts';
|
|
10
|
+
import { utilsCommand } from './commands/utils.ts';
|
|
11
|
+
|
|
12
|
+
const main = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: 'hu',
|
|
15
|
+
version: '0.1.0-a1',
|
|
16
|
+
description: 'CLI tools for Claude Code workflows',
|
|
17
|
+
},
|
|
18
|
+
subCommands: {
|
|
19
|
+
disk: diskCommand,
|
|
20
|
+
docs: docsCommand,
|
|
21
|
+
plans: plansCommand,
|
|
22
|
+
utils: utilsCommand,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
runMain(main);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI color codes for terminal output
|
|
3
|
+
*/
|
|
4
|
+
export const c = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
bold: '\x1b[1m',
|
|
7
|
+
dim: '\x1b[2m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
yellow: '\x1b[33m',
|
|
11
|
+
blue: '\x1b[34m',
|
|
12
|
+
magenta: '\x1b[35m',
|
|
13
|
+
cyan: '\x1b[36m',
|
|
14
|
+
white: '\x1b[37m',
|
|
15
|
+
bgBlue: '\x1b[44m',
|
|
16
|
+
bgGreen: '\x1b[42m',
|
|
17
|
+
bgYellow: '\x1b[43m',
|
|
18
|
+
bgRed: '\x1b[41m',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse size string (e.g., "10G", "500M") to bytes
|
|
23
|
+
*/
|
|
24
|
+
export function parseSize(sizeStr: string): number {
|
|
25
|
+
const match = sizeStr.match(/^([\d.]+)([KMGTP]?)$/i);
|
|
26
|
+
if (!match) return 0;
|
|
27
|
+
const num = Number.parseFloat(match[1]);
|
|
28
|
+
const unit = (match[2] || 'B').toUpperCase();
|
|
29
|
+
const multipliers: Record<string, number> = {
|
|
30
|
+
B: 1,
|
|
31
|
+
K: 1024,
|
|
32
|
+
M: 1024 ** 2,
|
|
33
|
+
G: 1024 ** 3,
|
|
34
|
+
T: 1024 ** 4,
|
|
35
|
+
P: 1024 ** 5,
|
|
36
|
+
};
|
|
37
|
+
return num * (multipliers[unit] || 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format bytes to human-readable size string
|
|
42
|
+
*/
|
|
43
|
+
export function formatSize(bytes: number): string {
|
|
44
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
45
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
46
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
47
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Colorize size string based on magnitude
|
|
52
|
+
*/
|
|
53
|
+
export function colorizeSize(sizeStr: string): string {
|
|
54
|
+
const bytes = parseSize(sizeStr.replace(/\s/g, ''));
|
|
55
|
+
if (bytes >= 50 * 1024 ** 3) return `${c.red}${c.bold}${sizeStr}${c.reset}`;
|
|
56
|
+
if (bytes >= 10 * 1024 ** 3) return `${c.yellow}${sizeStr}${c.reset}`;
|
|
57
|
+
if (bytes >= 1 * 1024 ** 3) return `${c.cyan}${sizeStr}${c.reset}`;
|
|
58
|
+
return `${c.dim}${sizeStr}${c.reset}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a progress bar string
|
|
63
|
+
*/
|
|
64
|
+
export function progressBar(percent: number, width = 30): string {
|
|
65
|
+
const filled = Math.round((width * percent) / 100);
|
|
66
|
+
const empty = width - filled;
|
|
67
|
+
let color: string = c.green;
|
|
68
|
+
if (percent > 80) color = c.red;
|
|
69
|
+
else if (percent > 60) color = c.yellow;
|
|
70
|
+
return `${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Print a section header
|
|
75
|
+
*/
|
|
76
|
+
export function printHeader(text: string): void {
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(`${c.bgBlue}${c.white}${c.bold} ${text} ${c.reset}`);
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Print a sub-header
|
|
84
|
+
*/
|
|
85
|
+
export function printSubHeader(text: string): void {
|
|
86
|
+
console.log(`${c.cyan}${c.bold}▸ ${text}${c.reset}`);
|
|
87
|
+
}
|
package/src/lib/exec.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Execute a shell command and return the output
|
|
5
|
+
* Returns empty string on error
|
|
6
|
+
*/
|
|
7
|
+
export function exec(cmd: string): string {
|
|
8
|
+
try {
|
|
9
|
+
return execSync(cmd, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }).trim();
|
|
10
|
+
} catch {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Execute a shell command and return success status
|
|
17
|
+
*/
|
|
18
|
+
export function execSuccess(cmd: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
execSync(cmd, { encoding: 'utf8', stdio: 'ignore' });
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|