@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.
Files changed (70) hide show
  1. package/.tool-versions +1 -0
  2. package/CLAUDE.md +122 -0
  3. package/HOOKS-DATA-INTEGRATION.md +457 -0
  4. package/SAMPLE.md +378 -0
  5. package/TODO.md +25 -0
  6. package/biome.json +51 -0
  7. package/commands/bootstrap.md +13 -0
  8. package/commands/c.md +1 -0
  9. package/commands/check-name.md +62 -0
  10. package/commands/disk.md +141 -0
  11. package/commands/docs/archive.md +27 -0
  12. package/commands/docs/check-internal.md +53 -0
  13. package/commands/docs/cleanup.md +65 -0
  14. package/commands/docs/consolidate.md +72 -0
  15. package/commands/docs/get.md +101 -0
  16. package/commands/docs/list.md +61 -0
  17. package/commands/docs/sync.md +64 -0
  18. package/commands/docs/update.md +49 -0
  19. package/commands/plans/clear.md +23 -0
  20. package/commands/plans/create.md +71 -0
  21. package/commands/plans/list.md +21 -0
  22. package/commands/plans/sync.md +38 -0
  23. package/commands/reinstall.md +20 -0
  24. package/commands/replicate.md +303 -0
  25. package/commands/warp.md +0 -0
  26. package/doc/README.md +35 -0
  27. package/doc/claude-code/capabilities.md +202 -0
  28. package/doc/claude-code/directory-structure.md +246 -0
  29. package/doc/claude-code/hooks.md +348 -0
  30. package/doc/claude-code/overview.md +109 -0
  31. package/doc/claude-code/plugins.md +273 -0
  32. package/doc/claude-code/sdk-protocols.md +202 -0
  33. package/document-manifest.toml +29 -0
  34. package/justfile +39 -0
  35. package/package.json +33 -0
  36. package/plans/compiled-watching-feather.md +217 -0
  37. package/plans/crispy-crafting-pnueli.md +103 -0
  38. package/plans/greedy-booping-coral.md +146 -0
  39. package/plans/imperative-sleeping-flamingo.md +192 -0
  40. package/plans/jaunty-sprouting-marble.md +171 -0
  41. package/plans/jiggly-discovering-lake.md +68 -0
  42. package/plans/magical-nibbling-spark.md +144 -0
  43. package/plans/mellow-kindling-acorn.md +110 -0
  44. package/plans/recursive-questing-engelbart.md +65 -0
  45. package/plans/serialized-roaming-kernighan.md +227 -0
  46. package/plans/structured-wondering-wirth.md +230 -0
  47. package/plans/vectorized-dreaming-iverson.md +191 -0
  48. package/plans/velvety-enchanting-ocean.md +92 -0
  49. package/plans/wiggly-sparking-pixel.md +48 -0
  50. package/plans/zippy-shimmying-fox.md +188 -0
  51. package/plugins/installed_plugins.json +4 -0
  52. package/sample-hooks.json +298 -0
  53. package/settings.json +24 -0
  54. package/settings.local.json +7 -0
  55. package/src/commands/bump.ts +130 -0
  56. package/src/commands/disk.ts +419 -0
  57. package/src/commands/docs.ts +729 -0
  58. package/src/commands/plans.ts +259 -0
  59. package/src/commands/utils.ts +299 -0
  60. package/src/index.ts +26 -0
  61. package/src/lib/colors.ts +87 -0
  62. package/src/lib/exec.ts +25 -0
  63. package/src/lib/fs.ts +119 -0
  64. package/src/lib/html.ts +205 -0
  65. package/src/lib/spinner.ts +42 -0
  66. package/src/types/index.ts +61 -0
  67. package/tests/lib/colors.test.ts +69 -0
  68. package/tests/lib/fs.test.ts +65 -0
  69. package/tsconfig.json +20 -0
  70. 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
+ }
@@ -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
+ }