@soleri/cli 9.3.1 → 9.5.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/dist/commands/agent.js +51 -2
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/hooks.js +126 -0
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.js +5 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/pack.js +62 -13
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/staging.d.ts +49 -0
- package/dist/commands/staging.js +108 -18
- package/dist/commands/staging.js.map +1 -1
- package/dist/commands/yolo.d.ts +2 -0
- package/dist/commands/yolo.js +86 -0
- package/dist/commands/yolo.js.map +1 -0
- package/dist/hook-packs/converter/README.md +99 -0
- package/dist/hook-packs/converter/template.d.ts +36 -0
- package/dist/hook-packs/converter/template.js +127 -0
- package/dist/hook-packs/converter/template.js.map +1 -0
- package/dist/hook-packs/converter/template.test.ts +133 -0
- package/dist/hook-packs/converter/template.ts +163 -0
- package/dist/hook-packs/flock-guard/README.md +65 -0
- package/dist/hook-packs/flock-guard/manifest.json +36 -0
- package/dist/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
- package/dist/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
- package/dist/hook-packs/full/manifest.json +8 -1
- package/dist/hook-packs/graduation.d.ts +11 -0
- package/dist/hook-packs/graduation.js +48 -0
- package/dist/hook-packs/graduation.js.map +1 -0
- package/dist/hook-packs/graduation.ts +65 -0
- package/dist/hook-packs/installer.js +3 -1
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +3 -1
- package/dist/hook-packs/marketing-research/README.md +37 -0
- package/dist/hook-packs/marketing-research/manifest.json +24 -0
- package/dist/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
- package/dist/hook-packs/registry.d.ts +1 -0
- package/dist/hook-packs/registry.js +14 -4
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +18 -4
- package/dist/hook-packs/safety/README.md +50 -0
- package/dist/hook-packs/safety/manifest.json +23 -0
- package/dist/hook-packs/safety/scripts/anti-deletion.sh +280 -0
- package/dist/hook-packs/validator.d.ts +32 -0
- package/dist/hook-packs/validator.js +126 -0
- package/dist/hook-packs/validator.js.map +1 -0
- package/dist/hook-packs/validator.ts +158 -0
- package/dist/hook-packs/yolo-safety/manifest.json +3 -19
- package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +121 -61
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/flock-guard.test.ts +225 -0
- package/src/__tests__/graduation.test.ts +199 -0
- package/src/__tests__/hook-packs.test.ts +45 -20
- package/src/__tests__/hooks-convert.test.ts +342 -0
- package/src/__tests__/validator.test.ts +265 -0
- package/src/__tests__/wizard-e2e.mjs +1 -1
- package/src/commands/agent.ts +65 -2
- package/src/commands/hooks.ts +172 -0
- package/src/commands/install.ts +6 -0
- package/src/commands/pack.ts +80 -14
- package/src/commands/staging.ts +143 -20
- package/src/commands/yolo.ts +103 -0
- package/src/hook-packs/converter/README.md +99 -0
- package/src/hook-packs/converter/template.test.ts +133 -0
- package/src/hook-packs/converter/template.ts +163 -0
- package/src/hook-packs/flock-guard/README.md +65 -0
- package/src/hook-packs/flock-guard/manifest.json +36 -0
- package/src/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
- package/src/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
- package/src/hook-packs/full/manifest.json +8 -1
- package/src/hook-packs/graduation.ts +65 -0
- package/src/hook-packs/installer.ts +3 -1
- package/src/hook-packs/marketing-research/README.md +37 -0
- package/src/hook-packs/marketing-research/manifest.json +24 -0
- package/src/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
- package/src/hook-packs/registry.ts +18 -4
- package/src/hook-packs/safety/README.md +50 -0
- package/src/hook-packs/safety/manifest.json +23 -0
- package/src/hook-packs/safety/scripts/anti-deletion.sh +280 -0
- package/src/hook-packs/validator.ts +158 -0
- package/src/hook-packs/yolo-safety/manifest.json +3 -19
- package/src/main.ts +2 -0
- package/vitest.config.ts +1 -0
- package/src/__tests__/archetypes.test.ts +0 -84
- package/src/__tests__/create.test.ts +0 -207
- package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +0 -214
- package/src/prompts/archetypes.ts +0 -343
package/src/commands/pack.ts
CHANGED
|
@@ -13,6 +13,18 @@ import type { Command } from 'commander';
|
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
14
|
import { PackLockfile, inferPackType, resolvePack, checkNpmVersion } from '@soleri/core';
|
|
15
15
|
import type { LockEntry, PackSource } from '@soleri/core';
|
|
16
|
+
|
|
17
|
+
// ─── Tier display helpers ────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const TIER_BADGES: Record<string, string> = {
|
|
20
|
+
default: '[default]',
|
|
21
|
+
community: '[community]',
|
|
22
|
+
premium: '[premium]',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function tierBadge(tier?: string): string {
|
|
26
|
+
return TIER_BADGES[tier ?? 'community'] ?? '[community]';
|
|
27
|
+
}
|
|
16
28
|
import { detectAgent } from '../utils/agent-context.js';
|
|
17
29
|
|
|
18
30
|
const LOCKFILE_NAME = 'soleri.lock';
|
|
@@ -45,14 +57,18 @@ export function registerPack(program: Command): void {
|
|
|
45
57
|
pack
|
|
46
58
|
.command('list')
|
|
47
59
|
.option('--type <type>', 'Filter by pack type (hooks, skills, knowledge, domain, bundle)')
|
|
60
|
+
.option('--tier <tier>', 'Filter by tier (default, community, premium)')
|
|
48
61
|
.description('List installed packs')
|
|
49
|
-
.action((opts: { type?: string }) => {
|
|
62
|
+
.action((opts: { type?: string; tier?: string }) => {
|
|
50
63
|
const lockfile = new PackLockfile(getLockfilePath());
|
|
51
64
|
let entries = lockfile.list();
|
|
52
65
|
|
|
53
66
|
if (opts.type) {
|
|
54
67
|
entries = entries.filter((e) => e.type === opts.type);
|
|
55
68
|
}
|
|
69
|
+
if (opts.tier) {
|
|
70
|
+
entries = entries.filter((e) => (e.tier ?? 'community') === opts.tier);
|
|
71
|
+
}
|
|
56
72
|
|
|
57
73
|
if (entries.length === 0) {
|
|
58
74
|
p.log.info('No packs installed.');
|
|
@@ -61,9 +77,10 @@ export function registerPack(program: Command): void {
|
|
|
61
77
|
|
|
62
78
|
p.log.info(`${entries.length} pack(s) installed:\n`);
|
|
63
79
|
for (const entry of entries) {
|
|
64
|
-
const
|
|
80
|
+
const source =
|
|
65
81
|
entry.source === 'built-in' ? ' [built-in]' : entry.source === 'npm' ? ' [npm]' : '';
|
|
66
|
-
|
|
82
|
+
const tier = ` ${tierBadge(entry.tier)}`;
|
|
83
|
+
console.log(` ${entry.id}@${entry.version} ${entry.type}${tier}${source}`);
|
|
67
84
|
if (entry.vaultEntries > 0) console.log(` vault: ${entry.vaultEntries} entries`);
|
|
68
85
|
if (entry.skills.length > 0) console.log(` skills: ${entry.skills.join(', ')}`);
|
|
69
86
|
if (entry.hooks.length > 0) console.log(` hooks: ${entry.hooks.join(', ')}`);
|
|
@@ -163,6 +180,7 @@ export function registerPack(program: Command): void {
|
|
|
163
180
|
skills,
|
|
164
181
|
hooks,
|
|
165
182
|
facadesRegistered: (manifest.facades?.length ?? 0) > 0,
|
|
183
|
+
tier: manifest.tier ?? 'community',
|
|
166
184
|
};
|
|
167
185
|
|
|
168
186
|
lockfile.set(entry);
|
|
@@ -219,9 +237,14 @@ export function registerPack(program: Command): void {
|
|
|
219
237
|
return;
|
|
220
238
|
}
|
|
221
239
|
|
|
240
|
+
const tierLabel = entry.tier ?? 'community';
|
|
241
|
+
const tierNote =
|
|
242
|
+
tierLabel === 'premium' ? ' (currently unlocked — premium platform coming soon)' : '';
|
|
243
|
+
|
|
222
244
|
console.log(`\n Pack: ${entry.id}`);
|
|
223
245
|
console.log(` Version: ${entry.version}`);
|
|
224
246
|
console.log(` Type: ${entry.type}`);
|
|
247
|
+
console.log(` Tier: ${tierLabel}${tierNote}`);
|
|
225
248
|
console.log(` Source: ${entry.source}`);
|
|
226
249
|
console.log(` Directory: ${entry.directory}`);
|
|
227
250
|
console.log(` Installed: ${entry.installedAt}`);
|
|
@@ -378,7 +401,16 @@ export function registerPack(program: Command): void {
|
|
|
378
401
|
return;
|
|
379
402
|
}
|
|
380
403
|
|
|
381
|
-
|
|
404
|
+
// Collect all packs with their tier
|
|
405
|
+
const allPacks: Array<{
|
|
406
|
+
id: string;
|
|
407
|
+
version: string;
|
|
408
|
+
description: string;
|
|
409
|
+
domains: string;
|
|
410
|
+
tier: string;
|
|
411
|
+
category: string;
|
|
412
|
+
}> = [];
|
|
413
|
+
|
|
382
414
|
for (const baseDir of searchDirs) {
|
|
383
415
|
const categories = readdirSync(baseDir, { withFileTypes: true })
|
|
384
416
|
.filter((d) => d.isDirectory())
|
|
@@ -390,18 +422,19 @@ export function registerPack(program: Command): void {
|
|
|
390
422
|
(d) => d.isDirectory() && existsSync(join(categoryDir, d.name, 'soleri-pack.json')),
|
|
391
423
|
);
|
|
392
424
|
|
|
393
|
-
if (packs.length === 0) continue;
|
|
394
|
-
|
|
395
|
-
console.log(`\n ${category}/`);
|
|
396
425
|
for (const pk of packs) {
|
|
397
426
|
try {
|
|
398
427
|
const manifest = JSON.parse(
|
|
399
428
|
readFileSync(join(categoryDir, pk.name, 'soleri-pack.json'), 'utf-8'),
|
|
400
429
|
);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
430
|
+
allPacks.push({
|
|
431
|
+
id: manifest.id,
|
|
432
|
+
version: manifest.version,
|
|
433
|
+
description: manifest.description || '',
|
|
434
|
+
domains: (manifest.domains as string[])?.join(', ') || '—',
|
|
435
|
+
tier: manifest.tier ?? 'community',
|
|
436
|
+
category,
|
|
437
|
+
});
|
|
405
438
|
} catch {
|
|
406
439
|
// skip malformed packs
|
|
407
440
|
}
|
|
@@ -409,11 +442,31 @@ export function registerPack(program: Command): void {
|
|
|
409
442
|
}
|
|
410
443
|
}
|
|
411
444
|
|
|
412
|
-
if (
|
|
445
|
+
if (allPacks.length === 0) {
|
|
413
446
|
p.log.info('No packs found.');
|
|
414
|
-
|
|
415
|
-
console.log(`\n ${total} pack(s) available.\n`);
|
|
447
|
+
return;
|
|
416
448
|
}
|
|
449
|
+
|
|
450
|
+
// Group by tier and display
|
|
451
|
+
const tierOrder: Array<{ key: string; label: string }> = [
|
|
452
|
+
{ key: 'default', label: 'Default (included with Soleri)' },
|
|
453
|
+
{ key: 'community', label: 'Community (free)' },
|
|
454
|
+
{ key: 'premium', label: 'Premium (included — premium platform coming soon)' },
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
for (const { key, label } of tierOrder) {
|
|
458
|
+
const tierPacks = allPacks.filter((pk) => pk.tier === key);
|
|
459
|
+
if (tierPacks.length === 0) continue;
|
|
460
|
+
|
|
461
|
+
console.log(`\n ${label}`);
|
|
462
|
+
console.log(` ${'─'.repeat(label.length)}`);
|
|
463
|
+
for (const pk of tierPacks) {
|
|
464
|
+
console.log(` ${pk.id}@${pk.version} ${pk.description}`);
|
|
465
|
+
console.log(` domains: ${pk.domains}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
console.log(`\n ${allPacks.length} pack(s) available.\n`);
|
|
417
470
|
});
|
|
418
471
|
|
|
419
472
|
// ─── create ─────────────────────────────────────────────────
|
|
@@ -441,6 +494,18 @@ export function registerPack(program: Command): void {
|
|
|
441
494
|
});
|
|
442
495
|
if (p.isCancel(description)) return;
|
|
443
496
|
|
|
497
|
+
const tier = await p.select({
|
|
498
|
+
message: 'Pack tier:',
|
|
499
|
+
options: [
|
|
500
|
+
{ value: 'community', label: 'Community — free, published to npm' },
|
|
501
|
+
{
|
|
502
|
+
value: 'premium',
|
|
503
|
+
label: 'Premium — requires Soleri platform account (coming soon)',
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
if (p.isCancel(tier)) return;
|
|
508
|
+
|
|
444
509
|
const author = await p.text({ message: 'Author:', placeholder: '@username' });
|
|
445
510
|
if (p.isCancel(author)) return;
|
|
446
511
|
|
|
@@ -454,6 +519,7 @@ export function registerPack(program: Command): void {
|
|
|
454
519
|
id: name,
|
|
455
520
|
version: '1.0.0',
|
|
456
521
|
description: description || '',
|
|
522
|
+
tier: tier || 'community',
|
|
457
523
|
author: author || '',
|
|
458
524
|
license: 'MIT',
|
|
459
525
|
soleri: '>=2.0.0',
|
package/src/commands/staging.ts
CHANGED
|
@@ -4,9 +4,12 @@ import { join, relative } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import * as log from '../utils/logger.js';
|
|
6
6
|
|
|
7
|
-
const STAGING_ROOT = join(homedir(), '.soleri', 'staging');
|
|
7
|
+
export const STAGING_ROOT = join(homedir(), '.soleri', 'staging');
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/** Default max age for stale staging entries (7 days). */
|
|
10
|
+
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
export interface StagedEntry {
|
|
10
13
|
id: string;
|
|
11
14
|
timestamp: string;
|
|
12
15
|
path: string;
|
|
@@ -17,7 +20,7 @@ interface StagedEntry {
|
|
|
17
20
|
/**
|
|
18
21
|
* Walk a directory tree and collect all items with their relative paths.
|
|
19
22
|
*/
|
|
20
|
-
function walkDir(dir: string, base: string): { relPath: string; size: number }[] {
|
|
23
|
+
export function walkDir(dir: string, base: string): { relPath: string; size: number }[] {
|
|
21
24
|
const results: { relPath: string; size: number }[] = [];
|
|
22
25
|
if (!existsSync(dir)) return results;
|
|
23
26
|
|
|
@@ -38,7 +41,7 @@ function walkDir(dir: string, base: string): { relPath: string; size: number }[]
|
|
|
38
41
|
/**
|
|
39
42
|
* List all staged entries.
|
|
40
43
|
*/
|
|
41
|
-
function listStaged(): StagedEntry[] {
|
|
44
|
+
export function listStaged(): StagedEntry[] {
|
|
42
45
|
if (!existsSync(STAGING_ROOT)) return [];
|
|
43
46
|
|
|
44
47
|
const entries: StagedEntry[] = [];
|
|
@@ -66,7 +69,7 @@ function listStaged(): StagedEntry[] {
|
|
|
66
69
|
/**
|
|
67
70
|
* Parse a duration string like "7d", "24h", "30m" into milliseconds.
|
|
68
71
|
*/
|
|
69
|
-
function parseDuration(duration: string): number | null {
|
|
72
|
+
export function parseDuration(duration: string): number | null {
|
|
70
73
|
const match = duration.match(/^(\d+)(d|h|m)$/);
|
|
71
74
|
if (!match) return null;
|
|
72
75
|
|
|
@@ -85,12 +88,78 @@ function parseDuration(duration: string): number | null {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
function formatSize(bytes: number): string {
|
|
91
|
+
export function formatSize(bytes: number): string {
|
|
89
92
|
if (bytes < 1024) return `${bytes} B`;
|
|
90
93
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
91
94
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
// ─── Reusable Utility Functions ──────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export interface StaleStagingInfo {
|
|
100
|
+
/** Entries older than maxAge. */
|
|
101
|
+
staleEntries: StagedEntry[];
|
|
102
|
+
/** Total bytes across stale entries. */
|
|
103
|
+
totalBytes: number;
|
|
104
|
+
/** Human-readable total size. */
|
|
105
|
+
totalSize: string;
|
|
106
|
+
/** Number of stale entries. */
|
|
107
|
+
count: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check for staging entries older than a given age.
|
|
112
|
+
* Pure function — no I/O side effects beyond reading the filesystem.
|
|
113
|
+
*
|
|
114
|
+
* @param maxAgeMs - Maximum age in milliseconds (default: 7 days)
|
|
115
|
+
* @returns Info about stale entries, or null if none found.
|
|
116
|
+
*/
|
|
117
|
+
export function getStaleStagingInfo(
|
|
118
|
+
maxAgeMs: number = DEFAULT_MAX_AGE_MS,
|
|
119
|
+
): StaleStagingInfo | null {
|
|
120
|
+
const entries = listStaged();
|
|
121
|
+
if (entries.length === 0) return null;
|
|
122
|
+
|
|
123
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
124
|
+
const staleEntries = entries.filter((entry) => {
|
|
125
|
+
try {
|
|
126
|
+
const stat = statSync(entry.path);
|
|
127
|
+
return stat.mtimeMs < cutoff;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (staleEntries.length === 0) return null;
|
|
134
|
+
|
|
135
|
+
const totalBytes = staleEntries.reduce((sum, e) => sum + e.size, 0);
|
|
136
|
+
return {
|
|
137
|
+
staleEntries,
|
|
138
|
+
totalBytes,
|
|
139
|
+
totalSize: formatSize(totalBytes),
|
|
140
|
+
count: staleEntries.length,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Purge stale staging entries. Returns the number of entries removed.
|
|
146
|
+
*
|
|
147
|
+
* @param entries - Entries to purge (from getStaleStagingInfo().staleEntries)
|
|
148
|
+
* @returns Number of entries successfully removed.
|
|
149
|
+
*/
|
|
150
|
+
export function purgeStagingEntries(entries: StagedEntry[]): number {
|
|
151
|
+
let removed = 0;
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
try {
|
|
154
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
155
|
+
removed++;
|
|
156
|
+
} catch {
|
|
157
|
+
// Skip failures silently — entry may have been removed concurrently
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return removed;
|
|
161
|
+
}
|
|
162
|
+
|
|
94
163
|
export function registerStaging(program: Command): void {
|
|
95
164
|
const staging = program.command('staging').description('Manage anti-deletion staging folder');
|
|
96
165
|
|
|
@@ -161,25 +230,31 @@ export function registerStaging(program: Command): void {
|
|
|
161
230
|
});
|
|
162
231
|
|
|
163
232
|
staging
|
|
164
|
-
.command('
|
|
165
|
-
.option(
|
|
166
|
-
|
|
167
|
-
|
|
233
|
+
.command('clean')
|
|
234
|
+
.option(
|
|
235
|
+
'--older-than <duration>',
|
|
236
|
+
'Only remove snapshots older than duration (default: 7d)',
|
|
237
|
+
'7d',
|
|
238
|
+
)
|
|
239
|
+
.option('--all', 'Remove all snapshots regardless of age')
|
|
240
|
+
.option('--dry-run', 'Show what would be removed without deleting')
|
|
241
|
+
.description('Remove staging backups older than 7 days (or --all)')
|
|
242
|
+
.action((opts: { olderThan: string; all?: boolean; dryRun?: boolean }) => {
|
|
168
243
|
if (!existsSync(STAGING_ROOT)) {
|
|
169
|
-
log.info('No staging directory found. Nothing to
|
|
244
|
+
log.info('No staging directory found. Nothing to clean.');
|
|
170
245
|
return;
|
|
171
246
|
}
|
|
172
247
|
|
|
173
248
|
const entries = listStaged();
|
|
174
249
|
|
|
175
250
|
if (entries.length === 0) {
|
|
176
|
-
log.info('No staged files to
|
|
251
|
+
log.info('No staged files to clean.');
|
|
177
252
|
return;
|
|
178
253
|
}
|
|
179
254
|
|
|
180
|
-
let
|
|
255
|
+
let toClean = entries;
|
|
181
256
|
|
|
182
|
-
if (opts.
|
|
257
|
+
if (!opts.all) {
|
|
183
258
|
const maxAge = parseDuration(opts.olderThan);
|
|
184
259
|
if (!maxAge) {
|
|
185
260
|
log.fail(`Invalid duration: "${opts.olderThan}". Use format like 7d, 24h, 30m`);
|
|
@@ -187,22 +262,70 @@ export function registerStaging(program: Command): void {
|
|
|
187
262
|
}
|
|
188
263
|
|
|
189
264
|
const cutoff = Date.now() - maxAge;
|
|
190
|
-
|
|
265
|
+
toClean = entries.filter((entry) => {
|
|
191
266
|
const stat = statSync(entry.path);
|
|
192
267
|
return stat.mtimeMs < cutoff;
|
|
193
268
|
});
|
|
194
269
|
}
|
|
195
270
|
|
|
196
|
-
if (
|
|
197
|
-
log.info('No snapshots match the
|
|
271
|
+
if (toClean.length === 0) {
|
|
272
|
+
log.info('No snapshots match the clean criteria.');
|
|
198
273
|
return;
|
|
199
274
|
}
|
|
200
275
|
|
|
201
|
-
|
|
276
|
+
if (opts.dryRun) {
|
|
277
|
+
log.heading('Dry run — would remove:');
|
|
278
|
+
for (const entry of toClean) {
|
|
279
|
+
log.warn(`${entry.id}`, formatSize(entry.size));
|
|
280
|
+
}
|
|
281
|
+
log.info(`Would remove ${toClean.length} staging snapshot(s)`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const entry of toClean) {
|
|
202
286
|
rmSync(entry.path, { recursive: true, force: true });
|
|
203
|
-
log.warn(`
|
|
287
|
+
log.warn(`Removed ${entry.id}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
log.info(`Removed ${toClean.length} staging snapshot(s)`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
staging
|
|
294
|
+
.command('cleanup')
|
|
295
|
+
.option('--older-than <duration>', 'Max age for stale entries (default: 7d)', '7d')
|
|
296
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
297
|
+
.description('Check for and remove stale staging backups (default: older than 7 days)')
|
|
298
|
+
.action((opts: { olderThan: string; yes?: boolean }) => {
|
|
299
|
+
const maxAge = parseDuration(opts.olderThan);
|
|
300
|
+
if (!maxAge) {
|
|
301
|
+
log.fail(`Invalid duration: "${opts.olderThan}". Use format like 7d, 24h, 30m`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const info = getStaleStagingInfo(maxAge);
|
|
306
|
+
|
|
307
|
+
if (!info) {
|
|
308
|
+
log.info('No stale staging backups found.');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
log.heading('Stale Staging Backups');
|
|
313
|
+
log.info(
|
|
314
|
+
`Found ${info.count} staging backup(s) older than ${opts.olderThan} (${info.totalSize}).`,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
for (const entry of info.staleEntries) {
|
|
318
|
+
log.dim(` ${entry.id} ${formatSize(entry.size)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!opts.yes) {
|
|
322
|
+
log.info(
|
|
323
|
+
`Run with --yes to remove, or use: soleri staging purge --older-than ${opts.olderThan}`,
|
|
324
|
+
);
|
|
325
|
+
return;
|
|
204
326
|
}
|
|
205
327
|
|
|
206
|
-
|
|
328
|
+
const removed = purgeStagingEntries(info.staleEntries);
|
|
329
|
+
log.pass(`Cleaned up ${removed} stale staging backup(s), freed ${info.totalSize}.`);
|
|
207
330
|
});
|
|
208
331
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import { isPackInstalled, installPack } from '../hook-packs/installer.js';
|
|
4
|
+
import { getPack } from '../hook-packs/registry.js';
|
|
5
|
+
import * as log from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
const YOLO_PACK = 'yolo-safety';
|
|
8
|
+
|
|
9
|
+
const RESET = '\x1b[0m';
|
|
10
|
+
const BOLD = '\x1b[1m';
|
|
11
|
+
const RED = '\x1b[31m';
|
|
12
|
+
const YELLOW = '\x1b[33m';
|
|
13
|
+
|
|
14
|
+
export function registerYolo(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command('yolo')
|
|
17
|
+
.description('Launch Claude Code in YOLO mode with safety guardrails')
|
|
18
|
+
.option('--dry-run', 'Show what would happen without launching Claude')
|
|
19
|
+
.option('--project', 'Install safety hooks to project .claude/ instead of global ~/.claude/')
|
|
20
|
+
.action((opts: { dryRun?: boolean; project?: boolean }) => {
|
|
21
|
+
runYolo(opts);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runYolo(opts: { dryRun?: boolean; project?: boolean }): void {
|
|
26
|
+
// 1. Verify the yolo-safety pack exists in registry
|
|
27
|
+
const pack = getPack(YOLO_PACK);
|
|
28
|
+
if (!pack) {
|
|
29
|
+
log.fail(`Hook pack "${YOLO_PACK}" not found in registry. Is @soleri/cli up to date?`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Check if already installed, install if not
|
|
34
|
+
const projectDir = opts.project ? process.cwd() : undefined;
|
|
35
|
+
const installed = isPackInstalled(YOLO_PACK, { projectDir });
|
|
36
|
+
|
|
37
|
+
if (installed === true) {
|
|
38
|
+
log.pass(`${YOLO_PACK} hook pack already installed`);
|
|
39
|
+
} else {
|
|
40
|
+
if (installed === 'partial') {
|
|
41
|
+
log.warn(`${YOLO_PACK} hook pack partially installed — reinstalling`);
|
|
42
|
+
}
|
|
43
|
+
const result = installPack(YOLO_PACK, { projectDir });
|
|
44
|
+
const target = opts.project ? '.claude/' : '~/.claude/';
|
|
45
|
+
for (const script of result.scripts) {
|
|
46
|
+
log.pass(`Installed ${script} → ${target}`);
|
|
47
|
+
}
|
|
48
|
+
for (const lc of result.lifecycleHooks) {
|
|
49
|
+
log.pass(`Registered lifecycle hook: ${lc}`);
|
|
50
|
+
}
|
|
51
|
+
const totalInstalled =
|
|
52
|
+
result.installed.length + result.scripts.length + result.lifecycleHooks.length;
|
|
53
|
+
if (totalInstalled > 0) {
|
|
54
|
+
log.pass(`${YOLO_PACK} hook pack installed (${totalInstalled} items)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Print safety warning
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(` ${RED}${BOLD}⚡ YOLO MODE${RESET}`);
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(
|
|
63
|
+
` ${YELLOW}Approval gates skipped — Claude will execute commands without asking.${RESET}`,
|
|
64
|
+
);
|
|
65
|
+
console.log(
|
|
66
|
+
` ${YELLOW}Safety hooks active — destructive commands (rm, git push --force,${RESET}`,
|
|
67
|
+
);
|
|
68
|
+
console.log(` ${YELLOW}git reset --hard, drop table, docker rm) are intercepted.${RESET}`);
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
if (opts.dryRun) {
|
|
72
|
+
log.info('Dry run — would launch:');
|
|
73
|
+
log.dim(' claude --dangerously-skip-permissions');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. Launch Claude Code with permissions skipped
|
|
78
|
+
log.info('Launching Claude Code in YOLO mode...');
|
|
79
|
+
console.log();
|
|
80
|
+
|
|
81
|
+
const child = spawn('claude', ['--dangerously-skip-permissions'], {
|
|
82
|
+
stdio: 'inherit',
|
|
83
|
+
env: { ...process.env },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
child.on('error', (err) => {
|
|
87
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
88
|
+
log.fail(
|
|
89
|
+
'Claude CLI not found. Install it first: https://docs.anthropic.com/en/docs/claude-code',
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
log.fail(`Failed to launch Claude: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
child.on('exit', (code, signal) => {
|
|
98
|
+
if (signal) {
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
process.exit(code ?? 0);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Skill-to-Hook Conversion System
|
|
2
|
+
|
|
3
|
+
Convert repeatedly-invoked skills into automated Claude Code hooks. Hooks fire automatically on matching events — no manual invocation, no LLM round trip.
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Score → Convert → Test → Graduate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### 1. Score the Candidate
|
|
12
|
+
|
|
13
|
+
Evaluate 4 dimensions (each HIGH or LOW):
|
|
14
|
+
|
|
15
|
+
| Dimension | HIGH when... |
|
|
16
|
+
| --------------------- | -------------------------------------------------------- |
|
|
17
|
+
| **Frequency** | 3+ manual calls per session for same event type |
|
|
18
|
+
| **Event Correlation** | Skill consistently triggers on a recognizable hook event |
|
|
19
|
+
| **Determinism** | Skill produces consistent, non-exploratory guidance |
|
|
20
|
+
| **Autonomy** | Skill requires no interactive user decisions |
|
|
21
|
+
|
|
22
|
+
**Threshold:** 3/4 HIGH = candidate for conversion.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { scoreCandidateForConversion } from '@soleri/core';
|
|
26
|
+
|
|
27
|
+
const result = scoreCandidateForConversion({
|
|
28
|
+
frequency: 'HIGH',
|
|
29
|
+
eventCorrelation: 'HIGH',
|
|
30
|
+
determinism: 'HIGH',
|
|
31
|
+
autonomy: 'LOW',
|
|
32
|
+
});
|
|
33
|
+
// result.candidate === true (3/4)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Convert
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
soleri hooks convert marketing-research \
|
|
40
|
+
--event PreToolUse \
|
|
41
|
+
--matcher "Write|Edit" \
|
|
42
|
+
--pattern "**/marketing/**" \
|
|
43
|
+
--action remind \
|
|
44
|
+
--message "Check brand guidelines and A/B testing data"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This creates a hook pack with `manifest.json` and a POSIX shell script.
|
|
48
|
+
|
|
49
|
+
### 3. Test
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
soleri hooks test marketing-research
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Runs 15 fixtures (5 matching + 10 non-matching) against the hook script. Reports false positives and false negatives. **Zero false positives required before graduation.**
|
|
56
|
+
|
|
57
|
+
### 4. Graduate
|
|
58
|
+
|
|
59
|
+
Hooks default to `remind` (gentle context injection). After proving zero false positives:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
soleri hooks promote marketing-research # remind → warn
|
|
63
|
+
soleri hooks promote marketing-research # warn → block
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To step back:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
soleri hooks demote marketing-research # block → warn
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Hook Events
|
|
73
|
+
|
|
74
|
+
| Event | When it fires |
|
|
75
|
+
| -------------- | -------------------------------------------- |
|
|
76
|
+
| `PreToolUse` | Before a tool call (Write, Edit, Bash, etc.) |
|
|
77
|
+
| `PostToolUse` | After a tool call completes |
|
|
78
|
+
| `PreCompact` | Before context compaction |
|
|
79
|
+
| `Notification` | On notification events |
|
|
80
|
+
| `Stop` | When the session ends |
|
|
81
|
+
|
|
82
|
+
## Action Levels
|
|
83
|
+
|
|
84
|
+
| Level | Behavior |
|
|
85
|
+
| -------- | ------------------------------------- |
|
|
86
|
+
| `remind` | Inject context, don't block (default) |
|
|
87
|
+
| `warn` | Inject warning context, don't block |
|
|
88
|
+
| `block` | Block the operation with a reason |
|
|
89
|
+
|
|
90
|
+
## CLI Commands
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
| --------------------------------- | ------------------------------------- |
|
|
94
|
+
| `soleri hooks convert <name>` | Create a new hook pack from a skill |
|
|
95
|
+
| `soleri hooks test <pack>` | Validate a hook pack against fixtures |
|
|
96
|
+
| `soleri hooks promote <pack>` | Step up action level |
|
|
97
|
+
| `soleri hooks demote <pack>` | Step down action level |
|
|
98
|
+
| `soleri hooks add-pack <pack>` | Install a hook pack |
|
|
99
|
+
| `soleri hooks remove-pack <pack>` | Uninstall a hook pack |
|