@soleri/cli 9.0.1 → 9.2.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 +116 -3
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.js +10 -3
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/hooks.js +43 -49
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +61 -12
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/pack.js +0 -1
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/staging.d.ts +2 -0
- package/dist/commands/staging.js +175 -0
- package/dist/commands/staging.js.map +1 -0
- package/dist/hook-packs/full/manifest.json +2 -2
- package/dist/hook-packs/installer.d.ts +4 -11
- package/dist/hook-packs/installer.js +192 -23
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +173 -60
- package/dist/hook-packs/registry.d.ts +16 -13
- package/dist/hook-packs/registry.js +13 -28
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +33 -46
- package/dist/hook-packs/yolo-safety/manifest.json +23 -0
- package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
- package/dist/hooks/templates.js +1 -1
- package/dist/hooks/templates.js.map +1 -1
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/create.test.ts +6 -2
- package/src/__tests__/hook-packs.test.ts +66 -44
- package/src/__tests__/wizard-e2e.mjs +17 -11
- package/src/commands/agent.ts +146 -3
- package/src/commands/create.ts +8 -2
- package/src/commands/hooks.ts +88 -187
- package/src/commands/install.ts +62 -22
- package/src/commands/pack.ts +0 -1
- package/src/commands/staging.ts +208 -0
- package/src/hook-packs/full/manifest.json +2 -2
- package/src/hook-packs/installer.ts +173 -60
- package/src/hook-packs/registry.ts +33 -46
- package/src/hook-packs/yolo-safety/manifest.json +23 -0
- package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
- package/src/hooks/templates.ts +1 -1
- package/src/main.ts +2 -0
- package/dist/commands/cognee.d.ts +0 -10
- package/dist/commands/cognee.js +0 -364
- package/dist/commands/cognee.js.map +0 -1
package/src/commands/hooks.ts
CHANGED
|
@@ -9,193 +9,94 @@ import * as log from '../utils/logger.js';
|
|
|
9
9
|
export function registerHooks(program: Command): void {
|
|
10
10
|
const hooks = program.command('hooks').description('Manage editor hooks and hook packs');
|
|
11
11
|
|
|
12
|
-
hooks
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
hooks
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const projectDir = opts.project ? process.cwd() : undefined;
|
|
101
|
-
const target = opts.project ? '.claude/' : '~/.claude/';
|
|
102
|
-
const { installed, skipped } = installPack(packName, { projectDir });
|
|
103
|
-
for (const hook of installed) {
|
|
104
|
-
log.pass(`Installed hookify.${hook}.local.md → ${target}`);
|
|
105
|
-
}
|
|
106
|
-
for (const hook of skipped) {
|
|
107
|
-
log.dim(` hookify.${hook}.local.md — already exists, skipped`);
|
|
108
|
-
}
|
|
109
|
-
if (installed.length > 0) {
|
|
110
|
-
log.info(`Pack "${packName}" installed (${installed.length} hooks) → ${target}`);
|
|
111
|
-
} else {
|
|
112
|
-
log.info(`Pack "${packName}" — all hooks already installed`);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
hooks
|
|
117
|
-
.command('remove-pack')
|
|
118
|
-
.argument('<pack>', 'Hook pack name')
|
|
119
|
-
.option('--project', 'Remove from project .claude/ instead of global ~/.claude/')
|
|
120
|
-
.description('Remove a hook pack')
|
|
121
|
-
.action((packName: string, opts: { project?: boolean }) => {
|
|
122
|
-
const pack = getPack(packName);
|
|
123
|
-
if (!pack) {
|
|
124
|
-
const available = listPacks().map((p) => p.name);
|
|
125
|
-
log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`);
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const projectDir = opts.project ? process.cwd() : undefined;
|
|
130
|
-
const { removed } = removePack(packName, { projectDir });
|
|
131
|
-
if (removed.length === 0) {
|
|
132
|
-
log.info(`No hooks from pack "${packName}" found to remove.`);
|
|
133
|
-
} else {
|
|
134
|
-
for (const hook of removed) {
|
|
135
|
-
log.warn(`Removed hookify.${hook}.local.md`);
|
|
136
|
-
}
|
|
137
|
-
log.info(`Pack "${packName}" removed (${removed.length} hooks)`);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
hooks
|
|
142
|
-
.command('list-packs')
|
|
143
|
-
.description('Show available hook packs and their status')
|
|
144
|
-
.action(() => {
|
|
145
|
-
const packs = listPacks();
|
|
146
|
-
|
|
147
|
-
log.heading('Hook Packs');
|
|
148
|
-
|
|
149
|
-
for (const pack of packs) {
|
|
150
|
-
const status = isPackInstalled(pack.name);
|
|
151
|
-
|
|
152
|
-
const versionLabel = pack.version ? ` v${pack.version}` : '';
|
|
153
|
-
const sourceLabel = pack.source === 'local' ? ' [local]' : '';
|
|
154
|
-
|
|
155
|
-
if (status === true) {
|
|
156
|
-
log.pass(
|
|
157
|
-
`${pack.name}${versionLabel}${sourceLabel}`,
|
|
158
|
-
`${pack.description} (${pack.hooks.length} hooks)`,
|
|
159
|
-
);
|
|
160
|
-
} else if (status === 'partial') {
|
|
161
|
-
log.warn(
|
|
162
|
-
`${pack.name}${versionLabel}${sourceLabel}`,
|
|
163
|
-
`${pack.description} (${pack.hooks.length} hooks) — partial`,
|
|
164
|
-
);
|
|
165
|
-
} else {
|
|
166
|
-
log.dim(
|
|
167
|
-
` ${pack.name}${versionLabel}${sourceLabel} — ${pack.description} (${pack.hooks.length} hooks)`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
hooks
|
|
174
|
-
.command('upgrade-pack')
|
|
175
|
-
.argument('<pack>', 'Hook pack name')
|
|
176
|
-
.option('--project', 'Upgrade in project .claude/ instead of global ~/.claude/')
|
|
177
|
-
.description('Upgrade a hook pack to the latest version (overwrites existing files)')
|
|
178
|
-
.action((packName: string, opts: { project?: boolean }) => {
|
|
179
|
-
const pack = getPack(packName);
|
|
180
|
-
if (!pack) {
|
|
181
|
-
const available = listPacks().map((p) => p.name);
|
|
182
|
-
log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const projectDir = opts.project ? process.cwd() : undefined;
|
|
187
|
-
const packVersion = pack.manifest.version ?? 'unknown';
|
|
188
|
-
|
|
189
|
-
// Remove then reinstall to force overwrite
|
|
190
|
-
removePack(packName, { projectDir });
|
|
191
|
-
const { installed } = installPack(packName, { projectDir });
|
|
192
|
-
|
|
193
|
-
for (const hook of installed) {
|
|
194
|
-
log.pass(`hookify.${hook}.local.md → v${packVersion}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
log.info(`Pack "${packName}" upgraded to v${packVersion} (${installed.length} hooks)`);
|
|
198
|
-
});
|
|
12
|
+
hooks.command('add').argument('<editor>', `Editor: ${SUPPORTED_EDITORS.join(', ')}`).description('Generate editor hooks/config files').action((editor: string) => {
|
|
13
|
+
if (!isValidEditor(editor)) { log.fail(`Unknown editor "${editor}". Supported: ${SUPPORTED_EDITORS.join(', ')}`); process.exit(1); }
|
|
14
|
+
const ctx = detectAgent();
|
|
15
|
+
if (!ctx) { log.fail('No agent project detected in current directory.'); process.exit(1); }
|
|
16
|
+
const files = installHooks(editor, ctx.agentPath);
|
|
17
|
+
for (const f of files) { log.pass(`Created ${f}`); }
|
|
18
|
+
log.info(`${editor} hooks installed for ${ctx.agentId}`);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
hooks.command('remove').argument('<editor>', `Editor: ${SUPPORTED_EDITORS.join(', ')}`).description('Remove editor hooks/config files').action((editor: string) => {
|
|
22
|
+
if (!isValidEditor(editor)) { log.fail(`Unknown editor "${editor}". Supported: ${SUPPORTED_EDITORS.join(', ')}`); process.exit(1); }
|
|
23
|
+
const ctx = detectAgent();
|
|
24
|
+
if (!ctx) { log.fail('No agent project detected in current directory.'); process.exit(1); }
|
|
25
|
+
const removed = removeHooks(editor, ctx.agentPath);
|
|
26
|
+
if (removed.length === 0) { log.info(`No ${editor} hooks found to remove.`); } else {
|
|
27
|
+
for (const f of removed) { log.warn(`Removed ${f}`); }
|
|
28
|
+
log.info(`${editor} hooks removed from ${ctx.agentId}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
hooks.command('list').description('Show which editor hooks are installed').action(() => {
|
|
33
|
+
const ctx = detectAgent();
|
|
34
|
+
if (!ctx) { log.fail('No agent project detected in current directory.'); process.exit(1); }
|
|
35
|
+
const installed = detectInstalledHooks(ctx.agentPath);
|
|
36
|
+
log.heading(`Editor hooks for ${ctx.agentId}`);
|
|
37
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
38
|
+
if (installed.includes(editor)) { log.pass(editor, 'installed'); } else { log.dim(` ${editor} — not installed`); }
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
hooks.command('add-pack').argument('<pack>', 'Hook pack name').option('--project', 'Install to project .claude/ instead of global ~/.claude/').description('Install a hook pack globally (~/.claude/) or per-project (--project)').action((packName: string, opts: { project?: boolean }) => {
|
|
43
|
+
const pack = getPack(packName);
|
|
44
|
+
if (!pack) { const available = listPacks().map((p) => p.name); log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`); process.exit(1); }
|
|
45
|
+
const projectDir = opts.project ? process.cwd() : undefined;
|
|
46
|
+
const target = opts.project ? '.claude/' : '~/.claude/';
|
|
47
|
+
const { installed, skipped, scripts, lifecycleHooks } = installPack(packName, { projectDir });
|
|
48
|
+
for (const hook of installed) { log.pass(`Installed hookify.${hook}.local.md → ${target}`); }
|
|
49
|
+
for (const hook of skipped) { log.dim(` hookify.${hook}.local.md — already exists, skipped`); }
|
|
50
|
+
for (const script of scripts) { log.pass(`Installed ${script} → ${target}`); }
|
|
51
|
+
for (const lc of lifecycleHooks) { log.pass(`Registered lifecycle hook: ${lc}`); }
|
|
52
|
+
const totalInstalled = installed.length + scripts.length + lifecycleHooks.length;
|
|
53
|
+
if (totalInstalled > 0) { log.info(`Pack "${packName}" installed (${totalInstalled} items) → ${target}`); } else { log.info(`Pack "${packName}" — all hooks already installed`); }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
hooks.command('remove-pack').argument('<pack>', 'Hook pack name').option('--project', 'Remove from project .claude/ instead of global ~/.claude/').description('Remove a hook pack').action((packName: string, opts: { project?: boolean }) => {
|
|
57
|
+
const pack = getPack(packName);
|
|
58
|
+
if (!pack) { const available = listPacks().map((p) => p.name); log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`); process.exit(1); }
|
|
59
|
+
const projectDir = opts.project ? process.cwd() : undefined;
|
|
60
|
+
const { removed, scripts, lifecycleHooks } = removePack(packName, { projectDir });
|
|
61
|
+
const totalRemoved = removed.length + scripts.length + lifecycleHooks.length;
|
|
62
|
+
if (totalRemoved === 0) { log.info(`No hooks from pack "${packName}" found to remove.`); } else {
|
|
63
|
+
for (const hook of removed) { log.warn(`Removed hookify.${hook}.local.md`); }
|
|
64
|
+
for (const script of scripts) { log.warn(`Removed ${script}`); }
|
|
65
|
+
for (const lc of lifecycleHooks) { log.warn(`Removed lifecycle hook: ${lc}`); }
|
|
66
|
+
log.info(`Pack "${packName}" removed (${totalRemoved} items)`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
hooks.command('list-packs').description('Show available hook packs and their status').action(() => {
|
|
71
|
+
const packs = listPacks();
|
|
72
|
+
log.heading('Hook Packs');
|
|
73
|
+
for (const pack of packs) {
|
|
74
|
+
const status = isPackInstalled(pack.name);
|
|
75
|
+
const versionLabel = pack.version ? ` v${pack.version}` : '';
|
|
76
|
+
const sourceLabel = pack.source === 'local' ? ' [local]' : '';
|
|
77
|
+
const hookCount = pack.hooks.length;
|
|
78
|
+
const scriptCount = pack.scripts?.length ?? 0;
|
|
79
|
+
const itemCount = hookCount + scriptCount;
|
|
80
|
+
const itemLabel = itemCount === 1 ? '1 item' : `${itemCount} items`;
|
|
81
|
+
if (status === true) { log.pass(`${pack.name}${versionLabel}${sourceLabel}`, `${pack.description} (${itemLabel})`); }
|
|
82
|
+
else if (status === 'partial') { log.warn(`${pack.name}${versionLabel}${sourceLabel}`, `${pack.description} (${itemLabel}) — partial`); }
|
|
83
|
+
else { log.dim(` ${pack.name}${versionLabel}${sourceLabel} — ${pack.description} (${itemLabel})`); }
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
hooks.command('upgrade-pack').argument('<pack>', 'Hook pack name').option('--project', 'Upgrade in project .claude/ instead of global ~/.claude/').description('Upgrade a hook pack to the latest version (overwrites existing files)').action((packName: string, opts: { project?: boolean }) => {
|
|
88
|
+
const pack = getPack(packName);
|
|
89
|
+
if (!pack) { const available = listPacks().map((p) => p.name); log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`); process.exit(1); }
|
|
90
|
+
const projectDir = opts.project ? process.cwd() : undefined;
|
|
91
|
+
const packVersion = pack.manifest.version ?? 'unknown';
|
|
92
|
+
removePack(packName, { projectDir });
|
|
93
|
+
const { installed, scripts, lifecycleHooks } = installPack(packName, { projectDir });
|
|
94
|
+
for (const hook of installed) { log.pass(`hookify.${hook}.local.md → v${packVersion}`); }
|
|
95
|
+
for (const script of scripts) { log.pass(`${script} → v${packVersion}`); }
|
|
96
|
+
for (const lc of lifecycleHooks) { log.pass(`lifecycle hook ${lc} → v${packVersion}`); }
|
|
97
|
+
const total = installed.length + scripts.length + lifecycleHooks.length;
|
|
98
|
+
log.info(`Pack "${packName}" upgraded to v${packVersion} (${total} items)`);
|
|
99
|
+
});
|
|
199
100
|
}
|
|
200
101
|
|
|
201
102
|
function isValidEditor(editor: string): editor is EditorId {
|
package/src/commands/install.ts
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
import * as p from '@clack/prompts';
|
|
6
7
|
import { detectAgent } from '../utils/agent-context.js';
|
|
7
8
|
|
|
9
|
+
/** Default parent directory for agents: ~/.soleri/ */
|
|
10
|
+
const SOLERI_HOME = process.env.SOLERI_HOME ?? join(homedir(), '.soleri');
|
|
11
|
+
|
|
8
12
|
type Target = 'claude' | 'codex' | 'opencode' | 'both' | 'all';
|
|
9
13
|
|
|
10
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the absolute path to the soleri-engine binary.
|
|
16
|
+
* Falls back to `npx @soleri/engine` if resolution fails (e.g. not installed globally).
|
|
17
|
+
*/
|
|
18
|
+
function resolveEngineBin(): { command: string; bin: string } {
|
|
19
|
+
try {
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const bin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
|
|
22
|
+
return { command: 'node', bin };
|
|
23
|
+
} catch {
|
|
24
|
+
return { command: 'npx', bin: '@soleri/engine' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** MCP server entry for file-tree agents (resolved engine path, no npx) */
|
|
11
29
|
function fileTreeMcpEntry(agentDir: string): Record<string, unknown> {
|
|
30
|
+
const engine = resolveEngineBin();
|
|
31
|
+
if (engine.command === 'node') {
|
|
32
|
+
return {
|
|
33
|
+
type: 'stdio',
|
|
34
|
+
command: 'node',
|
|
35
|
+
args: [engine.bin, '--agent', join(agentDir, 'agent.yaml')],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
12
38
|
return {
|
|
13
39
|
type: 'stdio',
|
|
14
40
|
command: 'npx',
|
|
@@ -26,7 +52,7 @@ function legacyMcpEntry(agentDir: string): Record<string, unknown> {
|
|
|
26
52
|
};
|
|
27
53
|
}
|
|
28
54
|
|
|
29
|
-
function installClaude(agentId: string, agentDir: string, isFileTree: boolean): void {
|
|
55
|
+
export function installClaude(agentId: string, agentDir: string, isFileTree: boolean): void {
|
|
30
56
|
const configPath = join(homedir(), '.claude.json');
|
|
31
57
|
let config: Record<string, unknown> = {};
|
|
32
58
|
|
|
@@ -72,7 +98,12 @@ function installCodex(agentId: string, agentDir: string, isFileTree: boolean): v
|
|
|
72
98
|
let section: string;
|
|
73
99
|
if (isFileTree) {
|
|
74
100
|
const agentYamlPath = join(agentDir, 'agent.yaml');
|
|
75
|
-
|
|
101
|
+
const engine = resolveEngineBin();
|
|
102
|
+
if (engine.command === 'node') {
|
|
103
|
+
section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${engine.bin}", "--agent", "${agentYamlPath}"]\n`;
|
|
104
|
+
} else {
|
|
105
|
+
section = `\n\n${sectionHeader}\ncommand = "npx"\nargs = ["@soleri/engine", "--agent", "${agentYamlPath}"]\n`;
|
|
106
|
+
}
|
|
76
107
|
} else {
|
|
77
108
|
const entryPoint = join(agentDir, 'dist', 'index.js');
|
|
78
109
|
section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${entryPoint}"]\n`;
|
|
@@ -111,24 +142,12 @@ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean)
|
|
|
111
142
|
|
|
112
143
|
const servers = config.mcp as Record<string, unknown>;
|
|
113
144
|
if (isFileTree) {
|
|
145
|
+
const engine = resolveEngineBin();
|
|
114
146
|
servers[agentId] = {
|
|
115
147
|
type: 'local',
|
|
116
|
-
command:
|
|
117
|
-
'node',
|
|
118
|
-
join(
|
|
119
|
-
agentDir,
|
|
120
|
-
'..',
|
|
121
|
-
'soleri',
|
|
122
|
-
'packages',
|
|
123
|
-
'core',
|
|
124
|
-
'dist',
|
|
125
|
-
'engine',
|
|
126
|
-
'bin',
|
|
127
|
-
'soleri-engine.js',
|
|
128
|
-
),
|
|
129
|
-
'--agent',
|
|
130
|
-
join(agentDir, 'agent.yaml'),
|
|
131
|
-
],
|
|
148
|
+
command: engine.command === 'node'
|
|
149
|
+
? ['node', engine.bin, '--agent', join(agentDir, 'agent.yaml')]
|
|
150
|
+
: ['npx', '-y', '@soleri/engine', '--agent', join(agentDir, 'agent.yaml')],
|
|
132
151
|
};
|
|
133
152
|
} else {
|
|
134
153
|
servers[agentId] = {
|
|
@@ -174,15 +193,30 @@ function installLauncher(agentId: string, agentDir: string): void {
|
|
|
174
193
|
export function registerInstall(program: Command): void {
|
|
175
194
|
program
|
|
176
195
|
.command('install')
|
|
177
|
-
.argument('[dir]', 'Agent directory (
|
|
196
|
+
.argument('[dir]', 'Agent directory or agent name (checks ~/.soleri/<name> first, then cwd)')
|
|
178
197
|
.option('--target <target>', 'Registration target: claude, opencode, codex, or all', 'claude')
|
|
179
198
|
.description('Register agent as MCP server in editor config')
|
|
180
199
|
.action(async (dir?: string, opts?: { target?: string }) => {
|
|
181
|
-
|
|
200
|
+
let resolvedDir: string | undefined;
|
|
201
|
+
|
|
202
|
+
if (dir) {
|
|
203
|
+
// If dir looks like a bare agent name (no slashes), check ~/.soleri/{name} first
|
|
204
|
+
if (!dir.includes('/') && !dir.includes('\\')) {
|
|
205
|
+
const soleriPath = join(SOLERI_HOME, dir);
|
|
206
|
+
if (existsSync(join(soleriPath, 'agent.yaml'))) {
|
|
207
|
+
resolvedDir = soleriPath;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!resolvedDir) {
|
|
211
|
+
resolvedDir = resolve(dir);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
182
215
|
const ctx = detectAgent(resolvedDir);
|
|
183
216
|
|
|
184
217
|
if (!ctx) {
|
|
185
218
|
p.log.error('Not in an agent project. Run from an agent directory or pass its path.');
|
|
219
|
+
p.log.info(`Tip: agents created with "soleri create" live in ${SOLERI_HOME}/`);
|
|
186
220
|
process.exit(1);
|
|
187
221
|
}
|
|
188
222
|
|
|
@@ -196,7 +230,13 @@ export function registerInstall(program: Command): void {
|
|
|
196
230
|
}
|
|
197
231
|
|
|
198
232
|
if (isFileTree) {
|
|
199
|
-
|
|
233
|
+
const engine = resolveEngineBin();
|
|
234
|
+
if (engine.command === 'node') {
|
|
235
|
+
p.log.info(`Detected file-tree agent (v7) — using resolved engine at ${engine.bin}`);
|
|
236
|
+
} else {
|
|
237
|
+
p.log.warn(`Could not resolve @soleri/core locally — falling back to npx (slower startup)`);
|
|
238
|
+
p.log.info(`For instant startup: npm install -g @soleri/cli`);
|
|
239
|
+
}
|
|
200
240
|
}
|
|
201
241
|
|
|
202
242
|
if (target === 'claude' || target === 'both' || target === 'all') {
|
package/src/commands/pack.ts
CHANGED
|
@@ -636,7 +636,6 @@ export function registerPack(program: Command): void {
|
|
|
636
636
|
|
|
637
637
|
function listMdFiles(dir: string): string[] {
|
|
638
638
|
try {
|
|
639
|
-
const { readdirSync } = require('node:fs');
|
|
640
639
|
const { basename } = require('node:path');
|
|
641
640
|
return readdirSync(dir)
|
|
642
641
|
.filter((f: string) => f.endsWith('.md'))
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { existsSync, readdirSync, statSync, rmSync, cpSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import * as log from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
const STAGING_ROOT = join(homedir(), '.soleri', 'staging');
|
|
8
|
+
|
|
9
|
+
interface StagedEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
path: string;
|
|
13
|
+
size: number;
|
|
14
|
+
isDirectory: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Walk a directory tree and collect all items with their relative paths.
|
|
19
|
+
*/
|
|
20
|
+
function walkDir(dir: string, base: string): { relPath: string; size: number }[] {
|
|
21
|
+
const results: { relPath: string; size: number }[] = [];
|
|
22
|
+
if (!existsSync(dir)) return results;
|
|
23
|
+
|
|
24
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = join(dir, entry.name);
|
|
27
|
+
const relPath = relative(base, fullPath);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
results.push(...walkDir(fullPath, base));
|
|
30
|
+
} else {
|
|
31
|
+
const stat = statSync(fullPath);
|
|
32
|
+
results.push({ relPath, size: stat.size });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* List all staged entries.
|
|
40
|
+
*/
|
|
41
|
+
function listStaged(): StagedEntry[] {
|
|
42
|
+
if (!existsSync(STAGING_ROOT)) return [];
|
|
43
|
+
|
|
44
|
+
const entries: StagedEntry[] = [];
|
|
45
|
+
const dirs = readdirSync(STAGING_ROOT, { withFileTypes: true });
|
|
46
|
+
|
|
47
|
+
for (const dir of dirs) {
|
|
48
|
+
if (!dir.isDirectory()) continue;
|
|
49
|
+
const stagePath = join(STAGING_ROOT, dir.name);
|
|
50
|
+
const _stat = statSync(stagePath);
|
|
51
|
+
const files = walkDir(stagePath, stagePath);
|
|
52
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
53
|
+
|
|
54
|
+
entries.push({
|
|
55
|
+
id: dir.name,
|
|
56
|
+
timestamp: dir.name,
|
|
57
|
+
path: stagePath,
|
|
58
|
+
size: totalSize,
|
|
59
|
+
isDirectory: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a duration string like "7d", "24h", "30m" into milliseconds.
|
|
68
|
+
*/
|
|
69
|
+
function parseDuration(duration: string): number | null {
|
|
70
|
+
const match = duration.match(/^(\d+)(d|h|m)$/);
|
|
71
|
+
if (!match) return null;
|
|
72
|
+
|
|
73
|
+
const value = parseInt(match[1], 10);
|
|
74
|
+
const unit = match[2];
|
|
75
|
+
|
|
76
|
+
switch (unit) {
|
|
77
|
+
case 'd':
|
|
78
|
+
return value * 24 * 60 * 60 * 1000;
|
|
79
|
+
case 'h':
|
|
80
|
+
return value * 60 * 60 * 1000;
|
|
81
|
+
case 'm':
|
|
82
|
+
return value * 60 * 1000;
|
|
83
|
+
default:
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatSize(bytes: number): string {
|
|
89
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
90
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
91
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function registerStaging(program: Command): void {
|
|
95
|
+
const staging = program.command('staging').description('Manage anti-deletion staging folder');
|
|
96
|
+
|
|
97
|
+
staging
|
|
98
|
+
.command('list')
|
|
99
|
+
.description('Show staged files with timestamps')
|
|
100
|
+
.action(() => {
|
|
101
|
+
const entries = listStaged();
|
|
102
|
+
|
|
103
|
+
if (entries.length === 0) {
|
|
104
|
+
log.info('No staged files found.');
|
|
105
|
+
log.dim(` Staging directory: ${STAGING_ROOT}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log.heading('Staged Files');
|
|
110
|
+
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const files = walkDir(entry.path, entry.path);
|
|
113
|
+
log.pass(entry.id, `${files.length} file(s), ${formatSize(entry.size)}`);
|
|
114
|
+
for (const file of files.slice(0, 10)) {
|
|
115
|
+
log.dim(` ${file.relPath}`);
|
|
116
|
+
}
|
|
117
|
+
if (files.length > 10) {
|
|
118
|
+
log.dim(` ... and ${files.length - 10} more`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
log.info(`${entries.length} staging snapshot(s) in ${STAGING_ROOT}`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
staging
|
|
126
|
+
.command('restore')
|
|
127
|
+
.argument('<id>', 'Staging snapshot ID (timestamp)')
|
|
128
|
+
.description('Restore files from staging to their original locations')
|
|
129
|
+
.action((id: string) => {
|
|
130
|
+
const stagePath = join(STAGING_ROOT, id);
|
|
131
|
+
|
|
132
|
+
if (!existsSync(stagePath)) {
|
|
133
|
+
log.fail(`Staging snapshot "${id}" not found.`);
|
|
134
|
+
const entries = listStaged();
|
|
135
|
+
if (entries.length > 0) {
|
|
136
|
+
log.info(`Available: ${entries.map((e) => e.id).join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const files = walkDir(stagePath, stagePath);
|
|
142
|
+
let restored = 0;
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
// The staging preserves absolute paths, so the relPath starts from root
|
|
146
|
+
const destPath = join('/', file.relPath);
|
|
147
|
+
const srcPath = join(stagePath, file.relPath);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const destDir = join(destPath, '..');
|
|
151
|
+
mkdirSync(destDir, { recursive: true });
|
|
152
|
+
cpSync(srcPath, destPath, { force: true });
|
|
153
|
+
restored++;
|
|
154
|
+
log.pass(`Restored ${destPath}`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
log.fail(`Failed to restore ${destPath}: ${err}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log.info(`Restored ${restored}/${files.length} file(s) from snapshot "${id}"`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
staging
|
|
164
|
+
.command('purge')
|
|
165
|
+
.option('--older-than <duration>', 'Only purge snapshots older than duration (e.g. 7d, 24h)')
|
|
166
|
+
.description('Permanently delete staged files')
|
|
167
|
+
.action((opts: { olderThan?: string }) => {
|
|
168
|
+
if (!existsSync(STAGING_ROOT)) {
|
|
169
|
+
log.info('No staging directory found. Nothing to purge.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const entries = listStaged();
|
|
174
|
+
|
|
175
|
+
if (entries.length === 0) {
|
|
176
|
+
log.info('No staged files to purge.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let toPurge = entries;
|
|
181
|
+
|
|
182
|
+
if (opts.olderThan) {
|
|
183
|
+
const maxAge = parseDuration(opts.olderThan);
|
|
184
|
+
if (!maxAge) {
|
|
185
|
+
log.fail(`Invalid duration: "${opts.olderThan}". Use format like 7d, 24h, 30m`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const cutoff = Date.now() - maxAge;
|
|
190
|
+
toPurge = entries.filter((entry) => {
|
|
191
|
+
const stat = statSync(entry.path);
|
|
192
|
+
return stat.mtimeMs < cutoff;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (toPurge.length === 0) {
|
|
197
|
+
log.info('No snapshots match the purge criteria.');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const entry of toPurge) {
|
|
202
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
203
|
+
log.warn(`Purged ${entry.id}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
log.info(`Purged ${toPurge.length} staging snapshot(s)`);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "full",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Complete quality suite — all
|
|
4
|
+
"description": "Complete quality suite — all hooks",
|
|
5
5
|
"hooks": [
|
|
6
6
|
"no-any-types",
|
|
7
7
|
"no-console-log",
|
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
"ux-touch-targets",
|
|
13
13
|
"no-ai-attribution"
|
|
14
14
|
],
|
|
15
|
-
"composedFrom": ["typescript-safety", "a11y", "css-discipline", "clean-commits"]
|
|
15
|
+
"composedFrom": ["typescript-safety", "a11y", "css-discipline", "clean-commits", "yolo-safety"]
|
|
16
16
|
}
|