icopilot 2.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/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createPatch } from 'diff';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
const DEFAULT_SRC_DIR = 'src';
|
|
7
|
+
const DEFAULT_TESTS_DIR = 'tests';
|
|
8
|
+
const STOP_WORDS = new Set([
|
|
9
|
+
'a',
|
|
10
|
+
'an',
|
|
11
|
+
'and',
|
|
12
|
+
'auto',
|
|
13
|
+
'build',
|
|
14
|
+
'class',
|
|
15
|
+
'command',
|
|
16
|
+
'create',
|
|
17
|
+
'file',
|
|
18
|
+
'for',
|
|
19
|
+
'generate',
|
|
20
|
+
'module',
|
|
21
|
+
'new',
|
|
22
|
+
'slash',
|
|
23
|
+
'test',
|
|
24
|
+
'tests',
|
|
25
|
+
'tool',
|
|
26
|
+
'utility',
|
|
27
|
+
'util',
|
|
28
|
+
'with',
|
|
29
|
+
]);
|
|
30
|
+
export function generateModule(opts) {
|
|
31
|
+
const layout = detectProjectLayout();
|
|
32
|
+
const normalized = normalizeOptions(opts, layout.extension);
|
|
33
|
+
const paths = buildPaths(layout, normalized);
|
|
34
|
+
const testImportPath = toImportPath(paths.test, paths.src);
|
|
35
|
+
switch (normalized.type) {
|
|
36
|
+
case 'command':
|
|
37
|
+
return {
|
|
38
|
+
source: commandTemplate(normalized),
|
|
39
|
+
test: commandTestTemplate(normalized, testImportPath),
|
|
40
|
+
paths,
|
|
41
|
+
};
|
|
42
|
+
case 'tool':
|
|
43
|
+
return {
|
|
44
|
+
source: toolTemplate(normalized),
|
|
45
|
+
test: toolTestTemplate(normalized, testImportPath),
|
|
46
|
+
paths,
|
|
47
|
+
};
|
|
48
|
+
case 'util':
|
|
49
|
+
return {
|
|
50
|
+
source: utilTemplate(normalized),
|
|
51
|
+
test: utilTestTemplate(normalized, testImportPath),
|
|
52
|
+
paths,
|
|
53
|
+
};
|
|
54
|
+
case 'class':
|
|
55
|
+
return {
|
|
56
|
+
source: classTemplate(normalized),
|
|
57
|
+
test: classTestTemplate(normalized, testImportPath),
|
|
58
|
+
paths,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function codegenCommand(args, cwd = config.cwd) {
|
|
63
|
+
const parsed = parseCodegenArgs(args);
|
|
64
|
+
if ('error' in parsed)
|
|
65
|
+
return `${theme.warn(parsed.error)}\n`;
|
|
66
|
+
const root = path.resolve(cwd || process.cwd());
|
|
67
|
+
const previousCwd = config.cwd;
|
|
68
|
+
config.cwd = root;
|
|
69
|
+
try {
|
|
70
|
+
const generated = generateModule(parsed);
|
|
71
|
+
const layout = detectProjectLayout();
|
|
72
|
+
const normalized = normalizeOptions(parsed, layout.extension);
|
|
73
|
+
const planned = collectPlannedChanges(root, layout, normalized, generated);
|
|
74
|
+
const changed = planned.filter((entry) => entry.previous !== entry.content);
|
|
75
|
+
const preview = changed.length > 0 ? renderPreview(changed) : theme.dim('No file changes required.');
|
|
76
|
+
applyPlannedChanges(root, changed);
|
|
77
|
+
const summary = changed.length === 0
|
|
78
|
+
? `${theme.dim('Everything is already up to date.')}\n`
|
|
79
|
+
: `${theme.ok(`Generated ${changed.length} change(s).`)}\n${changed
|
|
80
|
+
.map((entry) => ` ${entry.previous === undefined ? theme.ok('created') : theme.ok('updated')} ${theme.hl(entry.path)}`)
|
|
81
|
+
.join('\n')}\n`;
|
|
82
|
+
return `${theme.brand('Codegen preview')} ${theme.dim(`${normalized.type} ${normalized.kebabName}`)}\n\n${preview}\n\n${summary}`;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
config.cwd = previousCwd;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function parseCodegenArgs(args) {
|
|
89
|
+
let name = '';
|
|
90
|
+
let description = '';
|
|
91
|
+
let type;
|
|
92
|
+
let language;
|
|
93
|
+
const remaining = [];
|
|
94
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
95
|
+
const arg = args[index] ?? '';
|
|
96
|
+
if (arg === '--name') {
|
|
97
|
+
name = args[index + 1]?.trim() ?? '';
|
|
98
|
+
index += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (arg.startsWith('--name=')) {
|
|
102
|
+
name = arg.slice('--name='.length).trim();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === '--type') {
|
|
106
|
+
const value = args[index + 1]?.trim().toLowerCase() ?? '';
|
|
107
|
+
if (!isCodegenType(value)) {
|
|
108
|
+
return { error: `unsupported codegen type: ${value || '(missing)'}` };
|
|
109
|
+
}
|
|
110
|
+
type = value;
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg.startsWith('--type=')) {
|
|
115
|
+
const value = arg.slice('--type='.length).trim().toLowerCase();
|
|
116
|
+
if (!isCodegenType(value))
|
|
117
|
+
return { error: `unsupported codegen type: ${value || '(missing)'}` };
|
|
118
|
+
type = value;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (arg === '--language') {
|
|
122
|
+
language = args[index + 1]?.trim();
|
|
123
|
+
index += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (arg.startsWith('--language=')) {
|
|
127
|
+
language = arg.slice('--language='.length).trim();
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
remaining.push(arg);
|
|
131
|
+
}
|
|
132
|
+
description = remaining.join(' ').trim();
|
|
133
|
+
if (!description) {
|
|
134
|
+
return {
|
|
135
|
+
error: 'usage: /codegen [--type command|tool|util|class] [--name slug] [--language ts|js] <description>',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
name: name || inferName(description),
|
|
140
|
+
description,
|
|
141
|
+
type: type ?? inferType(description),
|
|
142
|
+
language,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function isCodegenType(value) {
|
|
146
|
+
return value === 'command' || value === 'tool' || value === 'util' || value === 'class';
|
|
147
|
+
}
|
|
148
|
+
function inferType(description) {
|
|
149
|
+
const lower = description.toLowerCase();
|
|
150
|
+
if (/\b(tool|schema|registry)\b/.test(lower))
|
|
151
|
+
return 'tool';
|
|
152
|
+
if (/\b(class|service|manager|client)\b/.test(lower))
|
|
153
|
+
return 'class';
|
|
154
|
+
if (/\b(command|slash)\b/.test(lower))
|
|
155
|
+
return 'command';
|
|
156
|
+
return 'util';
|
|
157
|
+
}
|
|
158
|
+
function inferName(description) {
|
|
159
|
+
const words = description
|
|
160
|
+
.toLowerCase()
|
|
161
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
162
|
+
.split(/\s+/)
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
const filtered = words.filter((word) => !STOP_WORDS.has(word));
|
|
165
|
+
const source = filtered.length > 0 ? filtered : words;
|
|
166
|
+
return toKebabCase(source.slice(0, 4).join('-') || 'generated-module');
|
|
167
|
+
}
|
|
168
|
+
function detectProjectLayout() {
|
|
169
|
+
const root = path.resolve(config.cwd || process.cwd());
|
|
170
|
+
const srcDir = resolveProjectDir(root, [DEFAULT_SRC_DIR, 'lib', 'source']) ?? DEFAULT_SRC_DIR;
|
|
171
|
+
const testsDir = resolveProjectDir(root, [DEFAULT_TESTS_DIR, 'test', '__tests__']) ?? DEFAULT_TESTS_DIR;
|
|
172
|
+
const extension = fs.existsSync(path.join(root, 'tsconfig.json')) ? 'ts' : 'js';
|
|
173
|
+
return { root, srcDir, testsDir, extension };
|
|
174
|
+
}
|
|
175
|
+
function resolveProjectDir(root, candidates) {
|
|
176
|
+
return candidates.find((candidate) => fs.existsSync(path.join(root, candidate)));
|
|
177
|
+
}
|
|
178
|
+
function normalizeOptions(opts, defaultExtension) {
|
|
179
|
+
const kebabName = toKebabCase(opts.name || inferName(opts.description));
|
|
180
|
+
const extension = normalizeExtension(opts.language, defaultExtension);
|
|
181
|
+
return {
|
|
182
|
+
...opts,
|
|
183
|
+
name: kebabName,
|
|
184
|
+
extension,
|
|
185
|
+
kebabName,
|
|
186
|
+
camelName: toCamelCase(kebabName),
|
|
187
|
+
pascalName: toPascalCase(kebabName),
|
|
188
|
+
constantName: toConstantCase(kebabName),
|
|
189
|
+
toolId: kebabName.replace(/-/g, '_'),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function normalizeExtension(language, defaultExtension) {
|
|
193
|
+
if (!language)
|
|
194
|
+
return defaultExtension;
|
|
195
|
+
const normalized = language.trim().toLowerCase();
|
|
196
|
+
if (normalized === 'js' || normalized === 'javascript')
|
|
197
|
+
return 'js';
|
|
198
|
+
if (normalized === 'ts' || normalized === 'typescript')
|
|
199
|
+
return 'ts';
|
|
200
|
+
return defaultExtension;
|
|
201
|
+
}
|
|
202
|
+
function buildPaths(layout, opts) {
|
|
203
|
+
const extension = opts.extension;
|
|
204
|
+
switch (opts.type) {
|
|
205
|
+
case 'command':
|
|
206
|
+
return {
|
|
207
|
+
src: `${layout.srcDir}/commands/${opts.kebabName}-cmd.${extension}`,
|
|
208
|
+
test: `${layout.testsDir}/commands/${opts.kebabName}-cmd.test.${extension}`,
|
|
209
|
+
};
|
|
210
|
+
case 'tool':
|
|
211
|
+
return {
|
|
212
|
+
src: `${layout.srcDir}/tools/${opts.kebabName}.${extension}`,
|
|
213
|
+
test: `${layout.testsDir}/tools/${opts.kebabName}.test.${extension}`,
|
|
214
|
+
};
|
|
215
|
+
case 'util':
|
|
216
|
+
return {
|
|
217
|
+
src: `${layout.srcDir}/util/${opts.kebabName}.${extension}`,
|
|
218
|
+
test: `${layout.testsDir}/util/${opts.kebabName}.test.${extension}`,
|
|
219
|
+
};
|
|
220
|
+
case 'class':
|
|
221
|
+
return {
|
|
222
|
+
src: `${layout.srcDir}/${opts.kebabName}.${extension}`,
|
|
223
|
+
test: `${layout.testsDir}/${opts.kebabName}.test.${extension}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function collectPlannedChanges(root, layout, opts, generated) {
|
|
228
|
+
const changes = [
|
|
229
|
+
planFileChange(root, generated.paths.src, generated.source),
|
|
230
|
+
planFileChange(root, generated.paths.test, generated.test),
|
|
231
|
+
];
|
|
232
|
+
if (opts.type === 'command') {
|
|
233
|
+
changes.push(...buildCommandRegistrations(root, layout, opts));
|
|
234
|
+
}
|
|
235
|
+
if (opts.type === 'tool') {
|
|
236
|
+
changes.push(...buildToolRegistrations(root, layout, opts));
|
|
237
|
+
}
|
|
238
|
+
return dedupeChanges(changes);
|
|
239
|
+
}
|
|
240
|
+
function planFileChange(root, relativePath, content) {
|
|
241
|
+
const absolutePath = path.join(root, ...relativePath.split('/'));
|
|
242
|
+
return {
|
|
243
|
+
path: relativePath,
|
|
244
|
+
content,
|
|
245
|
+
previous: readIfExists(absolutePath),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function buildCommandRegistrations(root, layout, opts) {
|
|
249
|
+
const slashPath = `${layout.srcDir}/commands/slash.ts`;
|
|
250
|
+
const completionPath = `${layout.srcDir}/util/completion.ts`;
|
|
251
|
+
const changes = [];
|
|
252
|
+
const slashAbsolute = path.join(root, ...slashPath.split('/'));
|
|
253
|
+
const slashContent = readIfExists(slashAbsolute);
|
|
254
|
+
if (slashContent !== undefined) {
|
|
255
|
+
const updated = updateSlashRegistry(slashContent, opts);
|
|
256
|
+
changes.push({ path: slashPath, previous: slashContent, content: updated });
|
|
257
|
+
}
|
|
258
|
+
const completionAbsolute = path.join(root, ...completionPath.split('/'));
|
|
259
|
+
const completionContent = readIfExists(completionAbsolute);
|
|
260
|
+
if (completionContent !== undefined) {
|
|
261
|
+
const updated = updateCompletionRegistry(completionContent, opts);
|
|
262
|
+
changes.push({ path: completionPath, previous: completionContent, content: updated });
|
|
263
|
+
}
|
|
264
|
+
return changes;
|
|
265
|
+
}
|
|
266
|
+
function buildToolRegistrations(root, layout, opts) {
|
|
267
|
+
const registryPath = `${layout.srcDir}/tools/registry.ts`;
|
|
268
|
+
const absolutePath = path.join(root, ...registryPath.split('/'));
|
|
269
|
+
const content = readIfExists(absolutePath);
|
|
270
|
+
if (content === undefined)
|
|
271
|
+
return [];
|
|
272
|
+
const updated = updateToolRegistry(content, opts);
|
|
273
|
+
return [{ path: registryPath, previous: content, content: updated }];
|
|
274
|
+
}
|
|
275
|
+
function updateSlashRegistry(content, opts) {
|
|
276
|
+
const importLine = `import { ${opts.camelName}Command } from './${opts.kebabName}-cmd.js';\n`;
|
|
277
|
+
const helpLine = ` /${opts.kebabName.padEnd(24)} ${summarizeDescription(opts.description, 44)}\n`;
|
|
278
|
+
const caseBlock = ` case '${opts.kebabName}':\n` +
|
|
279
|
+
` process.stdout.write(${opts.camelName}Command(rest));\n` +
|
|
280
|
+
' return done();\n';
|
|
281
|
+
let updated = insertBeforeMarker(content, 'export interface SlashContext {', importLine);
|
|
282
|
+
updated = insertBeforeMarker(updated, ' /exit, /quit', helpLine);
|
|
283
|
+
updated = insertBeforeMarker(updated, " case 'exit':", caseBlock);
|
|
284
|
+
return updated;
|
|
285
|
+
}
|
|
286
|
+
function updateCompletionRegistry(content, opts) {
|
|
287
|
+
return insertBeforeMarker(content, " 'exit',", ` '${opts.kebabName}',\n`);
|
|
288
|
+
}
|
|
289
|
+
function updateToolRegistry(content, opts) {
|
|
290
|
+
const importLine = `import { ${opts.camelName}Tool, ${opts.constantName}_SCHEMA } from './${opts.kebabName}.js';\n`;
|
|
291
|
+
const schemaLine = ` ${opts.constantName}_SCHEMA,\n`;
|
|
292
|
+
const caseBlock = ` case '${opts.toolId}':\n` +
|
|
293
|
+
` return ${opts.camelName}Tool({ input: String(args.input ?? '') });\n`;
|
|
294
|
+
let updated = insertBeforeMarker(content, 'type McpTools = {', importLine);
|
|
295
|
+
updated = insertBeforeMarker(updated, ' searchSymbolsSchema,', schemaLine);
|
|
296
|
+
updated = insertBeforeMarker(updated, " case 'search_symbols':", caseBlock);
|
|
297
|
+
return updated;
|
|
298
|
+
}
|
|
299
|
+
function insertBeforeMarker(content, marker, insertion) {
|
|
300
|
+
if (content.includes(insertion.trim()))
|
|
301
|
+
return content;
|
|
302
|
+
const index = content.indexOf(marker);
|
|
303
|
+
if (index === -1) {
|
|
304
|
+
return content.endsWith('\n') ? `${content}${insertion}` : `${content}\n${insertion}`;
|
|
305
|
+
}
|
|
306
|
+
return `${content.slice(0, index)}${insertion}${content.slice(index)}`;
|
|
307
|
+
}
|
|
308
|
+
function dedupeChanges(changes) {
|
|
309
|
+
const byPath = new Map();
|
|
310
|
+
for (const change of changes)
|
|
311
|
+
byPath.set(change.path, change);
|
|
312
|
+
return [...byPath.values()];
|
|
313
|
+
}
|
|
314
|
+
function renderPreview(changes) {
|
|
315
|
+
return changes
|
|
316
|
+
.map((change) => createPatch(change.path, change.previous ?? '', change.content, change.previous === undefined ? 'empty' : 'current', 'generated').trimEnd())
|
|
317
|
+
.join('\n\n');
|
|
318
|
+
}
|
|
319
|
+
function applyPlannedChanges(root, changes) {
|
|
320
|
+
for (const change of changes) {
|
|
321
|
+
const absolutePath = path.join(root, ...change.path.split('/'));
|
|
322
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
323
|
+
fs.writeFileSync(absolutePath, change.content, 'utf8');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function readIfExists(filePath) {
|
|
327
|
+
try {
|
|
328
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function commandTemplate(opts) {
|
|
335
|
+
return `import { theme } from '../ui/theme.js';
|
|
336
|
+
|
|
337
|
+
export interface ${opts.pascalName}Payload {
|
|
338
|
+
target: string;
|
|
339
|
+
prompt: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function build${opts.pascalName}Prompt(target: string): ${opts.pascalName}Payload {
|
|
343
|
+
const normalized = target.trim();
|
|
344
|
+
const prompt = [
|
|
345
|
+
'Implement the following slash command workflow.',
|
|
346
|
+
'Keep the generated behavior concise and reviewable.',
|
|
347
|
+
'Command: /${opts.kebabName}',
|
|
348
|
+
\`Request: \${normalized}\`,
|
|
349
|
+
'Notes: ${escapeSingleLine(opts.description)}',
|
|
350
|
+
].join('\\n');
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
target: normalized,
|
|
354
|
+
prompt,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function ${opts.camelName}Command(args: string[]): string {
|
|
359
|
+
const target = args.join(' ').trim();
|
|
360
|
+
if (!target) return \`\${theme.warn('usage: /${opts.kebabName} <target>')}\\n\`;
|
|
361
|
+
|
|
362
|
+
const payload = build${opts.pascalName}Prompt(target);
|
|
363
|
+
return \`\${theme.brand('${opts.pascalName} prompt')} \${theme.dim(payload.target)}\\n\\n\${payload.prompt}\\n\`;
|
|
364
|
+
}
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
function commandTestTemplate(opts, importPath) {
|
|
368
|
+
return `import { describe, expect, it } from 'vitest';
|
|
369
|
+
import { build${opts.pascalName}Prompt, ${opts.camelName}Command } from '${importPath}';
|
|
370
|
+
|
|
371
|
+
describe('${opts.camelName}Command', () => {
|
|
372
|
+
it('shows usage when no target is supplied', () => {
|
|
373
|
+
expect(${opts.camelName}Command([])).toContain('usage: /${opts.kebabName} <target>');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('builds a prompt for the requested target', () => {
|
|
377
|
+
const payload = build${opts.pascalName}Prompt('example target');
|
|
378
|
+
|
|
379
|
+
expect(payload.target).toBe('example target');
|
|
380
|
+
expect(payload.prompt).toContain('Command: /${opts.kebabName}');
|
|
381
|
+
expect(${opts.camelName}Command(['example', 'target'])).toContain('${opts.pascalName} prompt');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
function toolTemplate(opts) {
|
|
387
|
+
return `import type { ChatCompletionTool } from 'openai/resources/chat/completions';
|
|
388
|
+
|
|
389
|
+
export interface ${opts.pascalName}ToolArgs {
|
|
390
|
+
input: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export const ${opts.constantName}_SCHEMA: ChatCompletionTool = {
|
|
394
|
+
type: 'function',
|
|
395
|
+
function: {
|
|
396
|
+
name: '${opts.toolId}',
|
|
397
|
+
description: '${escapeSingleLine(opts.description)}',
|
|
398
|
+
parameters: {
|
|
399
|
+
type: 'object',
|
|
400
|
+
properties: {
|
|
401
|
+
input: {
|
|
402
|
+
type: 'string',
|
|
403
|
+
description: 'Primary input for ${opts.kebabName}.',
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
required: ['input'],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
export async function ${opts.camelName}Tool(args: ${opts.pascalName}ToolArgs): Promise<string> {
|
|
412
|
+
const input = args.input.trim();
|
|
413
|
+
return JSON.stringify({
|
|
414
|
+
ok: input.length > 0,
|
|
415
|
+
input,
|
|
416
|
+
message: input
|
|
417
|
+
? 'Processed input for ${escapeSingleLine(opts.description)}.'
|
|
418
|
+
: 'Provide input to continue.',
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
function toolTestTemplate(opts, importPath) {
|
|
424
|
+
return `import { describe, expect, it } from 'vitest';
|
|
425
|
+
import { ${opts.constantName}_SCHEMA, ${opts.camelName}Tool } from '${importPath}';
|
|
426
|
+
|
|
427
|
+
describe('${opts.camelName}Tool', () => {
|
|
428
|
+
it('exposes a function schema', () => {
|
|
429
|
+
expect(${opts.constantName}_SCHEMA.function.name).toBe('${opts.toolId}');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('returns a JSON payload', async () => {
|
|
433
|
+
const response = await ${opts.camelName}Tool({ input: 'demo input' });
|
|
434
|
+
const parsed = JSON.parse(response) as { ok: boolean; input: string };
|
|
435
|
+
|
|
436
|
+
expect(parsed.ok).toBe(true);
|
|
437
|
+
expect(parsed.input).toBe('demo input');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
442
|
+
function utilTemplate(opts) {
|
|
443
|
+
return `export function normalize${opts.pascalName}Input(value: string): string {
|
|
444
|
+
return value.trim().replace(/\\s+/g, ' ');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function format${opts.pascalName}Message(value: string): string {
|
|
448
|
+
const normalized = normalize${opts.pascalName}Input(value);
|
|
449
|
+
return normalized ? '${opts.pascalName} ready: ' + normalized : '${opts.pascalName} ready.';
|
|
450
|
+
}
|
|
451
|
+
`;
|
|
452
|
+
}
|
|
453
|
+
function utilTestTemplate(opts, importPath) {
|
|
454
|
+
return `import { describe, expect, it } from 'vitest';
|
|
455
|
+
import { format${opts.pascalName}Message, normalize${opts.pascalName}Input } from '${importPath}';
|
|
456
|
+
|
|
457
|
+
describe('${opts.pascalName} util', () => {
|
|
458
|
+
it('normalizes whitespace', () => {
|
|
459
|
+
expect(normalize${opts.pascalName}Input(' many spaces ')).toBe('many spaces');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('formats a readable message', () => {
|
|
463
|
+
expect(format${opts.pascalName}Message('demo')).toContain('${opts.pascalName} ready: demo');
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
`;
|
|
467
|
+
}
|
|
468
|
+
function classTemplate(opts) {
|
|
469
|
+
return `export interface ${opts.pascalName}Options {
|
|
470
|
+
name: string;
|
|
471
|
+
description?: string;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export class ${opts.pascalName} {
|
|
475
|
+
private name: string;
|
|
476
|
+
private readonly description: string;
|
|
477
|
+
|
|
478
|
+
constructor(options: ${opts.pascalName}Options) {
|
|
479
|
+
this.name = options.name.trim();
|
|
480
|
+
this.description = options.description?.trim() || '${escapeSingleLine(opts.description)}';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
rename(nextName: string): void {
|
|
484
|
+
const normalized = nextName.trim();
|
|
485
|
+
if (normalized) this.name = normalized;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
summary(): string {
|
|
489
|
+
return \`\${this.name}: \${this.description}\`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
`;
|
|
493
|
+
}
|
|
494
|
+
function classTestTemplate(opts, importPath) {
|
|
495
|
+
return `import { describe, expect, it } from 'vitest';
|
|
496
|
+
import { ${opts.pascalName} } from '${importPath}';
|
|
497
|
+
|
|
498
|
+
describe('${opts.pascalName}', () => {
|
|
499
|
+
it('builds a summary from constructor options', () => {
|
|
500
|
+
const instance = new ${opts.pascalName}({ name: 'Demo' });
|
|
501
|
+
|
|
502
|
+
expect(instance.summary()).toContain('Demo');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('renames the instance', () => {
|
|
506
|
+
const instance = new ${opts.pascalName}({ name: 'Old name' });
|
|
507
|
+
instance.rename('New name');
|
|
508
|
+
|
|
509
|
+
expect(instance.summary()).toContain('New name');
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
`;
|
|
513
|
+
}
|
|
514
|
+
function toImportPath(fromFile, toFile) {
|
|
515
|
+
const fromDir = path.posix.dirname(fromFile);
|
|
516
|
+
const relative = path.posix.relative(fromDir, toFile).replace(/\.(ts|js)$/u, '.js');
|
|
517
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
518
|
+
}
|
|
519
|
+
function toKebabCase(value) {
|
|
520
|
+
return value
|
|
521
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
522
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
523
|
+
.replace(/^-+|-+$/g, '')
|
|
524
|
+
.toLowerCase();
|
|
525
|
+
}
|
|
526
|
+
function toCamelCase(value) {
|
|
527
|
+
return value.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase());
|
|
528
|
+
}
|
|
529
|
+
function toPascalCase(value) {
|
|
530
|
+
const camel = toCamelCase(value);
|
|
531
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
532
|
+
}
|
|
533
|
+
function toConstantCase(value) {
|
|
534
|
+
return value.replace(/-/g, '_').toUpperCase();
|
|
535
|
+
}
|
|
536
|
+
function summarizeDescription(value, maxLength) {
|
|
537
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
538
|
+
if (normalized.length <= maxLength)
|
|
539
|
+
return normalized;
|
|
540
|
+
return `${normalized.slice(0, maxLength - 1).trimEnd()}…`;
|
|
541
|
+
}
|
|
542
|
+
function escapeSingleLine(value) {
|
|
543
|
+
return value.replace(/\s+/g, ' ').trim().replace(/'/g, "\\'");
|
|
544
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createTwoFilesPatch, diffLines } from 'diff';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
export function compareFiles(pathA, pathB, cwd) {
|
|
6
|
+
const fileA = path.resolve(cwd, pathA);
|
|
7
|
+
const fileB = path.resolve(cwd, pathB);
|
|
8
|
+
const missing = firstMissingFile(fileA, fileB);
|
|
9
|
+
if (missing) {
|
|
10
|
+
return { error: `file not found: ${missing}` };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const textA = fs.readFileSync(fileA, 'utf8');
|
|
14
|
+
const textB = fs.readFileSync(fileB, 'utf8');
|
|
15
|
+
const stats = calculateStats(textA, textB);
|
|
16
|
+
const diff = createTwoFilesPatch(pathA, pathB, textA, textB);
|
|
17
|
+
return {
|
|
18
|
+
fileA,
|
|
19
|
+
fileB,
|
|
20
|
+
diff,
|
|
21
|
+
stats,
|
|
22
|
+
prompt: buildPrompt(fileA, fileB, diff, stats),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
return { error: `compare failed: ${message}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function compareCommand(args, cwd) {
|
|
31
|
+
if (args.length !== 2) {
|
|
32
|
+
return `${theme.warn('usage: /compare <file-a> <file-b>')}\n`;
|
|
33
|
+
}
|
|
34
|
+
const result = compareFiles(args[0], args[1], cwd);
|
|
35
|
+
if ('error' in result) {
|
|
36
|
+
return `${theme.err(result.error)}\n`;
|
|
37
|
+
}
|
|
38
|
+
const hasChanges = result.stats.additions > 0 || result.stats.deletions > 0;
|
|
39
|
+
const lines = [
|
|
40
|
+
`${theme.brand('Compare')} ${theme.dim(`${result.fileA} ↔ ${result.fileB}`)}`,
|
|
41
|
+
` ${theme.hl(`A:${result.stats.linesA}`)} ${theme.hl(`B:${result.stats.linesB}`)} ${theme.ok(`+${result.stats.additions}`)} ${theme.err(`-${result.stats.deletions}`)} ${theme.dim(`=${result.stats.unchanged}`)}`,
|
|
42
|
+
'',
|
|
43
|
+
theme.brand('Unified diff'),
|
|
44
|
+
hasChanges ? formatDiff(result.diff) : theme.dim('No content changes detected.'),
|
|
45
|
+
'',
|
|
46
|
+
theme.brand('AI analysis prompt'),
|
|
47
|
+
result.prompt,
|
|
48
|
+
];
|
|
49
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
50
|
+
}
|
|
51
|
+
function firstMissingFile(...filePaths) {
|
|
52
|
+
return filePaths.find((filePath) => {
|
|
53
|
+
try {
|
|
54
|
+
return !fs.statSync(filePath).isFile();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function calculateStats(textA, textB) {
|
|
62
|
+
const stats = {
|
|
63
|
+
linesA: countLines(textA),
|
|
64
|
+
linesB: countLines(textB),
|
|
65
|
+
additions: 0,
|
|
66
|
+
deletions: 0,
|
|
67
|
+
unchanged: 0,
|
|
68
|
+
};
|
|
69
|
+
for (const part of diffLines(textA, textB)) {
|
|
70
|
+
const lines = countLines(part.value);
|
|
71
|
+
if (part.added) {
|
|
72
|
+
stats.additions += lines;
|
|
73
|
+
}
|
|
74
|
+
else if (part.removed) {
|
|
75
|
+
stats.deletions += lines;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
stats.unchanged += lines;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return stats;
|
|
82
|
+
}
|
|
83
|
+
function countLines(text) {
|
|
84
|
+
if (!text)
|
|
85
|
+
return 0;
|
|
86
|
+
return text.split(/\r?\n/).length - (text.endsWith('\n') ? 1 : 0);
|
|
87
|
+
}
|
|
88
|
+
function buildPrompt(fileA, fileB, diff, stats) {
|
|
89
|
+
return [
|
|
90
|
+
'Analyze the differences between these two files.',
|
|
91
|
+
`File A: ${fileA}`,
|
|
92
|
+
`File B: ${fileB}`,
|
|
93
|
+
'Explain what changed, why those changes likely happened, and whether one version looks better for maintainability, correctness, or clarity.',
|
|
94
|
+
'Use the unified diff and stats below as evidence.',
|
|
95
|
+
'',
|
|
96
|
+
`Stats: linesA=${stats.linesA}, linesB=${stats.linesB}, additions=${stats.additions}, deletions=${stats.deletions}, unchanged=${stats.unchanged}`,
|
|
97
|
+
'',
|
|
98
|
+
diff,
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
|
101
|
+
function formatDiff(diffText) {
|
|
102
|
+
return diffText
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map((line) => {
|
|
105
|
+
if (line.startsWith('+++') || line.startsWith('---'))
|
|
106
|
+
return theme.hl(line);
|
|
107
|
+
if (line.startsWith('@@') || line.startsWith('==='))
|
|
108
|
+
return theme.dim(line);
|
|
109
|
+
if (line.startsWith('+'))
|
|
110
|
+
return theme.ok(line);
|
|
111
|
+
if (line.startsWith('-'))
|
|
112
|
+
return theme.err(line);
|
|
113
|
+
return line.startsWith(' ') ? theme.dim(line) : line;
|
|
114
|
+
})
|
|
115
|
+
.join('\n');
|
|
116
|
+
}
|