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,450 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import simpleGit from 'simple-git';
|
|
5
|
+
import { config } from '../config.js';
|
|
6
|
+
import { theme } from '../ui/theme.js';
|
|
7
|
+
const RELEASE_TYPES = [
|
|
8
|
+
'major',
|
|
9
|
+
'minor',
|
|
10
|
+
'patch',
|
|
11
|
+
'premajor',
|
|
12
|
+
'preminor',
|
|
13
|
+
'prepatch',
|
|
14
|
+
];
|
|
15
|
+
const DEFAULT_CHANGELOG = `# Changelog
|
|
16
|
+
|
|
17
|
+
All notable changes to this project will be documented in this file.
|
|
18
|
+
|
|
19
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
20
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
21
|
+
|
|
22
|
+
## [Unreleased]
|
|
23
|
+
|
|
24
|
+
`;
|
|
25
|
+
const SECTION_ORDER = [
|
|
26
|
+
'Breaking Changes',
|
|
27
|
+
'Features',
|
|
28
|
+
'Fixes',
|
|
29
|
+
'Documentation',
|
|
30
|
+
'Performance',
|
|
31
|
+
'Refactors',
|
|
32
|
+
'Tests',
|
|
33
|
+
'Build',
|
|
34
|
+
'CI',
|
|
35
|
+
'Chores',
|
|
36
|
+
'Reverts',
|
|
37
|
+
'Other',
|
|
38
|
+
];
|
|
39
|
+
export async function performRelease(opts) {
|
|
40
|
+
return performReleaseAt(opts, config.cwd);
|
|
41
|
+
}
|
|
42
|
+
export async function releaseCommand(args, cwd) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = parseReleaseArgs(args);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return `${theme.warn(error.message)}\n`;
|
|
49
|
+
}
|
|
50
|
+
if (parsed.mode === 'status') {
|
|
51
|
+
const snapshot = await getReleaseSnapshot(cwd);
|
|
52
|
+
return formatStatus(snapshot);
|
|
53
|
+
}
|
|
54
|
+
const options = parsed.mode === 'preview'
|
|
55
|
+
? {
|
|
56
|
+
...parsed.options,
|
|
57
|
+
dryRun: true,
|
|
58
|
+
skipPublish: true,
|
|
59
|
+
}
|
|
60
|
+
: parsed.options;
|
|
61
|
+
try {
|
|
62
|
+
const result = await performReleaseAt(options, cwd);
|
|
63
|
+
return parsed.mode === 'preview' ? formatPreview(result) : formatReleaseResult(result);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return `${theme.err(`release failed: ${error.message}`)}\n`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function calculateNextVersion(currentVersion, type) {
|
|
70
|
+
const version = parseSemver(currentVersion);
|
|
71
|
+
switch (type) {
|
|
72
|
+
case 'major':
|
|
73
|
+
return formatSemver({ major: version.major + 1, minor: 0, patch: 0 });
|
|
74
|
+
case 'minor':
|
|
75
|
+
return formatSemver({ major: version.major, minor: version.minor + 1, patch: 0 });
|
|
76
|
+
case 'patch':
|
|
77
|
+
return formatSemver({ major: version.major, minor: version.minor, patch: version.patch + 1 });
|
|
78
|
+
case 'premajor':
|
|
79
|
+
return formatPrerelease({ major: version.major + 1, minor: 0, patch: 0 }, version);
|
|
80
|
+
case 'preminor':
|
|
81
|
+
return formatPrerelease({ major: version.major, minor: version.minor + 1, patch: 0 }, version);
|
|
82
|
+
case 'prepatch':
|
|
83
|
+
return formatPrerelease({ major: version.major, minor: version.minor, patch: version.patch + 1 }, version);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function parseSemver(version) {
|
|
87
|
+
const match = version
|
|
88
|
+
.trim()
|
|
89
|
+
.match(/^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
90
|
+
if (!match?.groups) {
|
|
91
|
+
throw new Error(`unsupported semver version: ${version}`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
major: Number.parseInt(match.groups.major, 10),
|
|
95
|
+
minor: Number.parseInt(match.groups.minor, 10),
|
|
96
|
+
patch: Number.parseInt(match.groups.patch, 10),
|
|
97
|
+
prerelease: match.groups.prerelease || undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function formatSemver(version) {
|
|
101
|
+
const core = `${version.major}.${version.minor}.${version.patch}`;
|
|
102
|
+
return version.prerelease ? `${core}-${version.prerelease}` : core;
|
|
103
|
+
}
|
|
104
|
+
async function performReleaseAt(opts, cwd) {
|
|
105
|
+
const snapshot = await getReleaseSnapshot(cwd);
|
|
106
|
+
if (snapshot.dirty && !opts.dryRun) {
|
|
107
|
+
throw new Error('working tree is dirty; commit or stash changes before releasing');
|
|
108
|
+
}
|
|
109
|
+
const nextVersion = calculateNextVersion(snapshot.currentVersion, opts.type);
|
|
110
|
+
const tagName = `v${nextVersion}`;
|
|
111
|
+
const changelogEntry = snapshot.unreleasedChanges || '- No changes recorded.';
|
|
112
|
+
if (opts.dryRun) {
|
|
113
|
+
return {
|
|
114
|
+
version: nextVersion,
|
|
115
|
+
changelog: changelogEntry,
|
|
116
|
+
tag: tagName,
|
|
117
|
+
published: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const git = simpleGit({ baseDir: cwd });
|
|
121
|
+
const originalPackageText = fs.readFileSync(snapshot.packagePath, 'utf8');
|
|
122
|
+
const originalChangelogText = fs.existsSync(snapshot.changelogPath)
|
|
123
|
+
? fs.readFileSync(snapshot.changelogPath, 'utf8')
|
|
124
|
+
: undefined;
|
|
125
|
+
let packageUpdated = false;
|
|
126
|
+
let changelogUpdated = false;
|
|
127
|
+
let committed = false;
|
|
128
|
+
try {
|
|
129
|
+
const updatedPackage = {
|
|
130
|
+
...snapshot.packageJson,
|
|
131
|
+
version: nextVersion,
|
|
132
|
+
};
|
|
133
|
+
fs.writeFileSync(snapshot.packagePath, `${JSON.stringify(updatedPackage, null, 2)}\n`, 'utf8');
|
|
134
|
+
packageUpdated = true;
|
|
135
|
+
const filesToAdd = ['package.json'];
|
|
136
|
+
if (!opts.skipChangelog) {
|
|
137
|
+
const nextChangelog = buildUpdatedChangelog(originalChangelogText, nextVersion, changelogEntry);
|
|
138
|
+
fs.writeFileSync(snapshot.changelogPath, nextChangelog, 'utf8');
|
|
139
|
+
changelogUpdated = true;
|
|
140
|
+
filesToAdd.push('CHANGELOG.md');
|
|
141
|
+
}
|
|
142
|
+
await git.add(filesToAdd);
|
|
143
|
+
await git.commit(`chore(release): ${tagName}`);
|
|
144
|
+
committed = true;
|
|
145
|
+
if (!opts.skipTag) {
|
|
146
|
+
await git.addTag(tagName);
|
|
147
|
+
}
|
|
148
|
+
let published = false;
|
|
149
|
+
if (!opts.skipPublish) {
|
|
150
|
+
runPublish(cwd, opts.tag);
|
|
151
|
+
published = true;
|
|
152
|
+
return {
|
|
153
|
+
version: nextVersion,
|
|
154
|
+
changelog: changelogEntry,
|
|
155
|
+
tag: tagName,
|
|
156
|
+
published,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
version: nextVersion,
|
|
161
|
+
changelog: changelogEntry,
|
|
162
|
+
tag: tagName,
|
|
163
|
+
published: false,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (!committed) {
|
|
168
|
+
if (packageUpdated) {
|
|
169
|
+
fs.writeFileSync(snapshot.packagePath, originalPackageText, 'utf8');
|
|
170
|
+
}
|
|
171
|
+
if (changelogUpdated) {
|
|
172
|
+
if (originalChangelogText === undefined) {
|
|
173
|
+
fs.rmSync(snapshot.changelogPath, { force: true });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
fs.writeFileSync(snapshot.changelogPath, originalChangelogText, 'utf8');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function getReleaseSnapshot(cwd) {
|
|
184
|
+
const git = simpleGit({ baseDir: cwd });
|
|
185
|
+
if (!(await git.checkIsRepo())) {
|
|
186
|
+
throw new Error(`not a git repository: ${cwd}`);
|
|
187
|
+
}
|
|
188
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
189
|
+
const changelogPath = path.join(cwd, 'CHANGELOG.md');
|
|
190
|
+
const packageJson = readPackageJson(packagePath);
|
|
191
|
+
const currentVersion = readVersion(packageJson);
|
|
192
|
+
const latestTag = (await git.tags()).latest ?? null;
|
|
193
|
+
const commits = mapConventionalCommits(await readReleaseCommits(git, latestTag));
|
|
194
|
+
const unreleasedChanges = renderConventionalChangelog(commits);
|
|
195
|
+
const status = await git.status();
|
|
196
|
+
return {
|
|
197
|
+
cwd,
|
|
198
|
+
packagePath,
|
|
199
|
+
changelogPath,
|
|
200
|
+
packageJson,
|
|
201
|
+
currentVersion,
|
|
202
|
+
latestTag,
|
|
203
|
+
commits,
|
|
204
|
+
unreleasedChanges,
|
|
205
|
+
dirty: !status.isClean(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function readReleaseCommits(git, latestTag) {
|
|
209
|
+
const range = latestTag ? [`${latestTag}..HEAD`] : ['HEAD'];
|
|
210
|
+
const result = await git.log(range);
|
|
211
|
+
return Array.from(result.all);
|
|
212
|
+
}
|
|
213
|
+
function readPackageJson(packagePath) {
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
throw new Error(`unable to read package.json: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function readVersion(pkg) {
|
|
222
|
+
if (typeof pkg.version !== 'string' || !pkg.version.trim()) {
|
|
223
|
+
throw new Error('package.json is missing a valid version field');
|
|
224
|
+
}
|
|
225
|
+
parseSemver(pkg.version);
|
|
226
|
+
return pkg.version;
|
|
227
|
+
}
|
|
228
|
+
function mapConventionalCommits(entries) {
|
|
229
|
+
return entries.map((entry) => {
|
|
230
|
+
const raw = entry.message.trim();
|
|
231
|
+
const match = raw.match(/^(?<type>[a-z]+)(?:\((?<scope>[^)]+)\))?(?<breaking>!)?: (?<description>.+)$/i);
|
|
232
|
+
const body = entry.body ?? '';
|
|
233
|
+
const type = match?.groups?.type?.toLowerCase() ?? 'other';
|
|
234
|
+
const scope = match?.groups?.scope || undefined;
|
|
235
|
+
const description = match?.groups?.description?.trim() ?? raw;
|
|
236
|
+
const breaking = match?.groups?.breaking === '!' ||
|
|
237
|
+
/BREAKING CHANGE:/i.test(raw) ||
|
|
238
|
+
/BREAKING CHANGE:/i.test(body);
|
|
239
|
+
return {
|
|
240
|
+
hash: entry.hash,
|
|
241
|
+
shortHash: entry.hash.slice(0, 7),
|
|
242
|
+
raw,
|
|
243
|
+
description,
|
|
244
|
+
type,
|
|
245
|
+
scope,
|
|
246
|
+
author: entry.author_name,
|
|
247
|
+
date: entry.date,
|
|
248
|
+
breaking,
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function renderConventionalChangelog(commits) {
|
|
253
|
+
if (commits.length === 0) {
|
|
254
|
+
return '- No changes recorded.';
|
|
255
|
+
}
|
|
256
|
+
const sections = new Map();
|
|
257
|
+
for (const section of SECTION_ORDER)
|
|
258
|
+
sections.set(section, []);
|
|
259
|
+
for (const commit of commits) {
|
|
260
|
+
if (commit.breaking) {
|
|
261
|
+
sections.get('Breaking Changes')?.push(formatCommitLine(commit));
|
|
262
|
+
}
|
|
263
|
+
sections.get(sectionForCommit(commit.type))?.push(formatCommitLine(commit));
|
|
264
|
+
}
|
|
265
|
+
const lines = [];
|
|
266
|
+
for (const section of SECTION_ORDER) {
|
|
267
|
+
const items = sections.get(section) ?? [];
|
|
268
|
+
if (items.length === 0)
|
|
269
|
+
continue;
|
|
270
|
+
lines.push(`### ${section}`, '', ...items, '');
|
|
271
|
+
}
|
|
272
|
+
return lines.join('\n').trim() || '- No changes recorded.';
|
|
273
|
+
}
|
|
274
|
+
function sectionForCommit(type) {
|
|
275
|
+
switch (type) {
|
|
276
|
+
case 'feat':
|
|
277
|
+
return 'Features';
|
|
278
|
+
case 'fix':
|
|
279
|
+
return 'Fixes';
|
|
280
|
+
case 'docs':
|
|
281
|
+
return 'Documentation';
|
|
282
|
+
case 'perf':
|
|
283
|
+
return 'Performance';
|
|
284
|
+
case 'refactor':
|
|
285
|
+
return 'Refactors';
|
|
286
|
+
case 'test':
|
|
287
|
+
return 'Tests';
|
|
288
|
+
case 'build':
|
|
289
|
+
return 'Build';
|
|
290
|
+
case 'ci':
|
|
291
|
+
return 'CI';
|
|
292
|
+
case 'chore':
|
|
293
|
+
case 'style':
|
|
294
|
+
return 'Chores';
|
|
295
|
+
case 'revert':
|
|
296
|
+
return 'Reverts';
|
|
297
|
+
default:
|
|
298
|
+
return 'Other';
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function formatCommitLine(commit) {
|
|
302
|
+
const scope = commit.scope ? `**${commit.scope}:** ` : '';
|
|
303
|
+
return `- ${scope}${commit.description} (${commit.shortHash})`;
|
|
304
|
+
}
|
|
305
|
+
function buildUpdatedChangelog(currentChangelog, version, changelogEntry) {
|
|
306
|
+
const changelog = currentChangelog?.trim().length
|
|
307
|
+
? currentChangelog
|
|
308
|
+
: DEFAULT_CHANGELOG.trimEnd();
|
|
309
|
+
const releaseSection = `## [${version}] - ${new Date().toISOString().slice(0, 10)}\n\n${changelogEntry.trim()}\n`;
|
|
310
|
+
const unreleasedHeading = /^## \[Unreleased\].*$/m;
|
|
311
|
+
const match = unreleasedHeading.exec(changelog);
|
|
312
|
+
if (!match) {
|
|
313
|
+
const base = changelog.trimEnd();
|
|
314
|
+
return `${base}\n\n## [Unreleased]\n\n${releaseSection}\n`;
|
|
315
|
+
}
|
|
316
|
+
const headingStart = match.index;
|
|
317
|
+
const headingEnd = headingStart + match[0].length;
|
|
318
|
+
const afterHeading = changelog.slice(headingEnd);
|
|
319
|
+
const nextHeadingRelative = afterHeading.search(/\n## \[/);
|
|
320
|
+
const sectionEnd = nextHeadingRelative === -1 ? changelog.length : headingEnd + nextHeadingRelative;
|
|
321
|
+
const before = changelog.slice(0, headingStart);
|
|
322
|
+
const after = changelog.slice(sectionEnd).replace(/^\r?\n/, '');
|
|
323
|
+
const freshUnreleased = `${match[0]}\n\n`;
|
|
324
|
+
const next = `${before}${freshUnreleased}${releaseSection}${after ? `\n${after}` : ''}`;
|
|
325
|
+
return next.endsWith('\n') ? next : `${next}\n`;
|
|
326
|
+
}
|
|
327
|
+
function runPublish(cwd, distTag) {
|
|
328
|
+
const args = ['publish'];
|
|
329
|
+
if (distTag) {
|
|
330
|
+
args.push('--tag', distTag);
|
|
331
|
+
}
|
|
332
|
+
const result = spawnSync('npm', args, {
|
|
333
|
+
cwd,
|
|
334
|
+
encoding: 'utf8',
|
|
335
|
+
shell: process.platform === 'win32',
|
|
336
|
+
stdio: 'pipe',
|
|
337
|
+
});
|
|
338
|
+
if (result.status !== 0) {
|
|
339
|
+
const details = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
340
|
+
throw new Error(details || `npm publish failed with exit ${result.status ?? 'unknown'}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function formatPrerelease(target, current) {
|
|
344
|
+
const base = formatSemver(target);
|
|
345
|
+
const sameCore = current.major === target.major &&
|
|
346
|
+
current.minor === target.minor &&
|
|
347
|
+
current.patch === target.patch;
|
|
348
|
+
if (!sameCore || !current.prerelease) {
|
|
349
|
+
return `${base}-0`;
|
|
350
|
+
}
|
|
351
|
+
const match = current.prerelease.match(/(?:^|\.)(\d+)$/);
|
|
352
|
+
const next = match ? Number.parseInt(match[1], 10) + 1 : 0;
|
|
353
|
+
return `${base}-${next}`;
|
|
354
|
+
}
|
|
355
|
+
function parseReleaseArgs(args) {
|
|
356
|
+
const [first, ...rest] = args;
|
|
357
|
+
if (!first) {
|
|
358
|
+
throw new Error('Usage: /release <major|minor|patch|premajor|preminor|prepatch> [--tag <dist-tag>] [--dry-run] [--skip-changelog] [--skip-tag] [--skip-publish] | /release preview [type] | /release status');
|
|
359
|
+
}
|
|
360
|
+
if (first === 'status') {
|
|
361
|
+
return { mode: 'status' };
|
|
362
|
+
}
|
|
363
|
+
if (first === 'preview') {
|
|
364
|
+
const [maybeType, ...tail] = rest;
|
|
365
|
+
const type = isReleaseType(maybeType) ? maybeType : 'patch';
|
|
366
|
+
const optionArgs = isReleaseType(maybeType) ? tail : rest;
|
|
367
|
+
return {
|
|
368
|
+
mode: 'preview',
|
|
369
|
+
options: parseReleaseOptions(type, optionArgs),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (!isReleaseType(first)) {
|
|
373
|
+
throw new Error('Usage: /release <major|minor|patch|premajor|preminor|prepatch> [--tag <dist-tag>] [--dry-run] [--skip-changelog] [--skip-tag] [--skip-publish] | /release preview [type] | /release status');
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
mode: 'release',
|
|
377
|
+
options: parseReleaseOptions(first, rest),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function parseReleaseOptions(type, args) {
|
|
381
|
+
const options = { type };
|
|
382
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
383
|
+
const arg = args[index];
|
|
384
|
+
switch (arg) {
|
|
385
|
+
case '--tag': {
|
|
386
|
+
const value = args[++index]?.trim();
|
|
387
|
+
if (!value) {
|
|
388
|
+
throw new Error('Usage: /release <type> [--tag <dist-tag>] [--dry-run] [--skip-changelog] [--skip-tag] [--skip-publish]');
|
|
389
|
+
}
|
|
390
|
+
options.tag = value;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case '--dry-run':
|
|
394
|
+
options.dryRun = true;
|
|
395
|
+
break;
|
|
396
|
+
case '--skip-changelog':
|
|
397
|
+
options.skipChangelog = true;
|
|
398
|
+
break;
|
|
399
|
+
case '--skip-tag':
|
|
400
|
+
options.skipTag = true;
|
|
401
|
+
break;
|
|
402
|
+
case '--skip-publish':
|
|
403
|
+
options.skipPublish = true;
|
|
404
|
+
break;
|
|
405
|
+
default:
|
|
406
|
+
throw new Error('Usage: /release <type> [--tag <dist-tag>] [--dry-run] [--skip-changelog] [--skip-tag] [--skip-publish]');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return options;
|
|
410
|
+
}
|
|
411
|
+
function isReleaseType(value) {
|
|
412
|
+
return Boolean(value && RELEASE_TYPES.includes(value));
|
|
413
|
+
}
|
|
414
|
+
function formatReleaseResult(result) {
|
|
415
|
+
return [
|
|
416
|
+
theme.ok(`✔ release prepared: ${result.version}`),
|
|
417
|
+
` tag: ${theme.hl(result.tag)}`,
|
|
418
|
+
` published: ${theme.hl(result.published ? 'yes' : 'no')}`,
|
|
419
|
+
'',
|
|
420
|
+
theme.brand('Changelog entry'),
|
|
421
|
+
result.changelog,
|
|
422
|
+
'',
|
|
423
|
+
].join('\n');
|
|
424
|
+
}
|
|
425
|
+
function formatPreview(result) {
|
|
426
|
+
return [
|
|
427
|
+
theme.brand('Release preview'),
|
|
428
|
+
` version: ${theme.hl(result.version)}`,
|
|
429
|
+
` tag: ${theme.hl(result.tag)}`,
|
|
430
|
+
` published: ${theme.hl('no (dry run)')}`,
|
|
431
|
+
'',
|
|
432
|
+
theme.brand('Changelog preview'),
|
|
433
|
+
result.changelog,
|
|
434
|
+
'',
|
|
435
|
+
].join('\n');
|
|
436
|
+
}
|
|
437
|
+
function formatStatus(snapshot) {
|
|
438
|
+
return [
|
|
439
|
+
theme.brand('Release status'),
|
|
440
|
+
` cwd: ${theme.dim(snapshot.cwd)}`,
|
|
441
|
+
` version: ${theme.hl(snapshot.currentVersion)}`,
|
|
442
|
+
` latest tag: ${theme.hl(snapshot.latestTag ?? 'none')}`,
|
|
443
|
+
` unreleased commits:${theme.hl(` ${snapshot.commits.length}`)}`,
|
|
444
|
+
` working tree: ${theme.hl(snapshot.dirty ? 'dirty' : 'clean')}`,
|
|
445
|
+
'',
|
|
446
|
+
theme.brand('Unreleased changes'),
|
|
447
|
+
snapshot.unreleasedChanges,
|
|
448
|
+
'',
|
|
449
|
+
].join('\n');
|
|
450
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import simpleGit from 'simple-git';
|
|
4
|
+
import { MultiRepoOrchestrator, } from '../agents/multi-repo.js';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
export const MULTI_REPO_ROOT_ENV = 'ICOPILOT_MULTI_REPO_ROOT';
|
|
7
|
+
export async function repoCommand(args, options) {
|
|
8
|
+
const rootDir = resolveMultiRepoRoot(options.cwd);
|
|
9
|
+
const orchestrator = new MultiRepoOrchestrator();
|
|
10
|
+
orchestrator.loadConfig(rootDir);
|
|
11
|
+
const [rawSubcommand = 'show', ...rest] = args;
|
|
12
|
+
const subcommand = rawSubcommand.toLowerCase();
|
|
13
|
+
try {
|
|
14
|
+
switch (subcommand) {
|
|
15
|
+
case 'show':
|
|
16
|
+
case 'list':
|
|
17
|
+
return formatRepoList(await orchestrator.getStatus());
|
|
18
|
+
case 'add':
|
|
19
|
+
return addRepo(orchestrator, rootDir, rest, options.cwd);
|
|
20
|
+
case 'remove':
|
|
21
|
+
case 'delete':
|
|
22
|
+
case 'rm':
|
|
23
|
+
return removeRepo(orchestrator, rest);
|
|
24
|
+
case 'switch':
|
|
25
|
+
return switchRepo(orchestrator, rootDir, rest, options);
|
|
26
|
+
case 'status':
|
|
27
|
+
return formatRepoStatus(await orchestrator.getStatus());
|
|
28
|
+
case 'search':
|
|
29
|
+
return searchRepos(orchestrator, rest);
|
|
30
|
+
default:
|
|
31
|
+
return repoUsage();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
return theme.err(`repo: ${message}\n`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function resolveMultiRepoRoot(cwd) {
|
|
40
|
+
const envRoot = process.env[MULTI_REPO_ROOT_ENV];
|
|
41
|
+
if (envRoot && fs.existsSync(envRoot)) {
|
|
42
|
+
return path.resolve(envRoot);
|
|
43
|
+
}
|
|
44
|
+
const discovered = findConfigRoot(cwd);
|
|
45
|
+
return discovered ?? path.resolve(cwd);
|
|
46
|
+
}
|
|
47
|
+
function findConfigRoot(startDir) {
|
|
48
|
+
let current = path.resolve(startDir);
|
|
49
|
+
// eslint-disable-next-line no-constant-condition
|
|
50
|
+
while (true) {
|
|
51
|
+
const candidate = path.join(current, '.icopilot', 'repos.yaml');
|
|
52
|
+
if (fs.existsSync(candidate))
|
|
53
|
+
return current;
|
|
54
|
+
const parent = path.dirname(current);
|
|
55
|
+
if (parent === current)
|
|
56
|
+
break;
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
async function addRepo(orchestrator, rootDir, args, cwd) {
|
|
62
|
+
const repoPathArg = args[0];
|
|
63
|
+
if (!repoPathArg)
|
|
64
|
+
return theme.warn('usage: /repo add <path> [name]\n');
|
|
65
|
+
const repoPath = path.resolve(cwd, repoPathArg);
|
|
66
|
+
if (!fs.existsSync(repoPath) || !fs.statSync(repoPath).isDirectory()) {
|
|
67
|
+
return theme.err(`repo path does not exist: ${repoPath}\n`);
|
|
68
|
+
}
|
|
69
|
+
const explicitName = args.slice(1).join(' ').trim();
|
|
70
|
+
const metadata = await detectRepoMetadata(repoPath);
|
|
71
|
+
const repos = orchestrator.listRepos();
|
|
72
|
+
const repo = orchestrator.addRepo({
|
|
73
|
+
name: explicitName || path.basename(repoPath),
|
|
74
|
+
path: repoPath,
|
|
75
|
+
remote: metadata.remote,
|
|
76
|
+
branch: metadata.branch,
|
|
77
|
+
role: repos.length === 0 ? 'primary' : 'peer',
|
|
78
|
+
});
|
|
79
|
+
process.env[MULTI_REPO_ROOT_ENV] = rootDir;
|
|
80
|
+
const parts = [`✔ added repo ${theme.hl(repo.name)} ${theme.dim(`→ ${repo.path}`)}`];
|
|
81
|
+
if (repo.branch)
|
|
82
|
+
parts.push(theme.dim(`branch=${repo.branch}`));
|
|
83
|
+
if (repo.remote)
|
|
84
|
+
parts.push(theme.dim(`remote=${repo.remote}`));
|
|
85
|
+
return `${parts.join(' ')}\n`;
|
|
86
|
+
}
|
|
87
|
+
function removeRepo(orchestrator, args) {
|
|
88
|
+
const name = args.join(' ').trim();
|
|
89
|
+
if (!name)
|
|
90
|
+
return theme.warn('usage: /repo remove <name>\n');
|
|
91
|
+
if (!orchestrator.removeRepo(name)) {
|
|
92
|
+
return theme.warn(`unknown repo: ${name}\n`);
|
|
93
|
+
}
|
|
94
|
+
return theme.ok(`✔ removed repo ${name}\n`);
|
|
95
|
+
}
|
|
96
|
+
function switchRepo(orchestrator, rootDir, args, options) {
|
|
97
|
+
const name = args.join(' ').trim();
|
|
98
|
+
if (!name)
|
|
99
|
+
return theme.warn('usage: /repo switch <name>\n');
|
|
100
|
+
const repo = orchestrator.switchRepo(name);
|
|
101
|
+
process.env[MULTI_REPO_ROOT_ENV] = rootDir;
|
|
102
|
+
options.onSwitch?.(repo, rootDir);
|
|
103
|
+
return theme.ok(`✔ switched repo ${repo.name} ${theme.dim(`→ ${repo.path}`)}\n`);
|
|
104
|
+
}
|
|
105
|
+
async function searchRepos(orchestrator, args) {
|
|
106
|
+
const query = args.join(' ').trim();
|
|
107
|
+
if (!query)
|
|
108
|
+
return theme.warn('usage: /repo search <query>\n');
|
|
109
|
+
const hits = await orchestrator.searchAcrossRepos(query);
|
|
110
|
+
if (!hits.length) {
|
|
111
|
+
return theme.dim(`No matches found across ${orchestrator.listRepos().length} repos.\n`);
|
|
112
|
+
}
|
|
113
|
+
return formatRepoSearchResults(query, hits);
|
|
114
|
+
}
|
|
115
|
+
async function detectRepoMetadata(repoPath) {
|
|
116
|
+
try {
|
|
117
|
+
const git = simpleGit(repoPath);
|
|
118
|
+
if (!(await git.checkIsRepo()))
|
|
119
|
+
return {};
|
|
120
|
+
const remotes = await git.getRemotes(true);
|
|
121
|
+
const branch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
|
122
|
+
return {
|
|
123
|
+
branch: branch || undefined,
|
|
124
|
+
remote: remotes[0]?.name,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function formatRepoList(status) {
|
|
132
|
+
if (!status.repos.length) {
|
|
133
|
+
return `${theme.brand('Repositories')} ${theme.dim(status.rootDir)}\n ${theme.dim('No repositories configured. Use /repo add <path> [name].')}\n`;
|
|
134
|
+
}
|
|
135
|
+
const lines = [`${theme.brand('Repositories')} ${theme.dim(status.rootDir)}`, ''];
|
|
136
|
+
for (const repo of status.repos) {
|
|
137
|
+
const marker = repo.name === status.current ? theme.ok('*') : theme.dim('-');
|
|
138
|
+
const branch = repo.branch || repo.currentBranch;
|
|
139
|
+
lines.push(` ${marker} ${theme.hl(repo.name)} ${theme.dim(`(${repo.role})`)} ${theme.dim('→')} ${repo.path}${branch ? theme.dim(` [${branch}]`) : ''}`);
|
|
140
|
+
}
|
|
141
|
+
lines.push('');
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
function formatRepoStatus(status) {
|
|
145
|
+
if (!status.repos.length) {
|
|
146
|
+
return `${theme.brand('Repo status')} ${theme.dim(status.rootDir)}\n ${theme.dim('No repositories configured.')}\n`;
|
|
147
|
+
}
|
|
148
|
+
const lines = [
|
|
149
|
+
`${theme.brand('Repo status')} ${theme.dim(status.rootDir)}`,
|
|
150
|
+
` config: ${status.configPath}`,
|
|
151
|
+
` current: ${status.current ? theme.hl(status.current) : theme.dim('none')}`,
|
|
152
|
+
'',
|
|
153
|
+
];
|
|
154
|
+
for (const repo of status.repos) {
|
|
155
|
+
const health = !repo.exists
|
|
156
|
+
? theme.err('missing')
|
|
157
|
+
: repo.error
|
|
158
|
+
? theme.err('error')
|
|
159
|
+
: repo.git
|
|
160
|
+
? theme.ok(repo.dirty ? 'dirty' : 'clean')
|
|
161
|
+
: theme.warn('not-git');
|
|
162
|
+
const branch = repo.currentBranch || repo.branch || theme.dim('n/a');
|
|
163
|
+
lines.push(` ${theme.hl(repo.name)} ${health} ${theme.dim(`(${repo.role})`)}`);
|
|
164
|
+
lines.push(` path: ${repo.path}`);
|
|
165
|
+
lines.push(` branch: ${branch}`);
|
|
166
|
+
if (repo.git) {
|
|
167
|
+
lines.push(` ahead/behind: ${repo.ahead}/${repo.behind}`);
|
|
168
|
+
}
|
|
169
|
+
if (repo.error) {
|
|
170
|
+
lines.push(` error: ${repo.error}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
lines.push('');
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
function formatRepoSearchResults(query, hits) {
|
|
177
|
+
const lines = [`${theme.brand('Repo search')} ${theme.dim(`for "${query}"`)}`, ''];
|
|
178
|
+
for (const hit of hits) {
|
|
179
|
+
lines.push(` ${theme.hl(hit.repo)} ${theme.dim('→')} ${hit.file}:${hit.line}`);
|
|
180
|
+
lines.push(` ${hit.text}`);
|
|
181
|
+
}
|
|
182
|
+
lines.push('');
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
function repoUsage() {
|
|
186
|
+
return [
|
|
187
|
+
'usage: /repo',
|
|
188
|
+
' /repo add <path> [name]',
|
|
189
|
+
' /repo remove <name>',
|
|
190
|
+
' /repo switch <name>',
|
|
191
|
+
' /repo status',
|
|
192
|
+
' /repo search <query>',
|
|
193
|
+
'',
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PROFILES, toProfile } from '../routing/profiles.js';
|
|
2
|
+
import { getProfile, setProfile } from '../routing/router.js';
|
|
3
|
+
export function routeCommand(arg) {
|
|
4
|
+
const [subcommand = 'get', profile] = arg.trim().split(/\s+/).filter(Boolean);
|
|
5
|
+
switch (subcommand.toLowerCase()) {
|
|
6
|
+
case 'get':
|
|
7
|
+
return `routing profile: ${getProfile()}\n`;
|
|
8
|
+
case 'list':
|
|
9
|
+
return `routing profiles: ${Object.keys(PROFILES).join(', ')}\n`;
|
|
10
|
+
case 'set': {
|
|
11
|
+
const next = toProfile(profile);
|
|
12
|
+
if (!next) {
|
|
13
|
+
return `unknown routing profile: ${profile || '(missing)'}\n`;
|
|
14
|
+
}
|
|
15
|
+
setProfile(next);
|
|
16
|
+
return `✔ routing profile → ${next}\n`;
|
|
17
|
+
}
|
|
18
|
+
default:
|
|
19
|
+
return 'usage: /route get | /route set <cheap|balanced|strong|fixed> | /route list\n';
|
|
20
|
+
}
|
|
21
|
+
}
|