@itz4blitz/agentful 1.8.0 โ 1.8.2
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/README.md +0 -3
- package/bin/cli.js +165 -109
- package/bin/hooks/architect-drift-detector.js +3 -1
- package/bin/hooks/block-random-docs.js +0 -2
- package/bin/hooks/ensure-worktree.js +234 -0
- package/bin/hooks/mcp-health-check.js +56 -0
- package/bin/hooks/package-metadata-guard.js +118 -0
- package/bin/hooks/session-start.js +32 -5
- package/bin/hooks/worktree-service.js +514 -0
- package/lib/presets.js +22 -1
- package/package.json +7 -8
- package/template/.claude/agents/backend.md +19 -0
- package/template/.claude/agents/frontend.md +19 -0
- package/template/.claude/agents/orchestrator.md +182 -0
- package/template/.claude/agents/reviewer.md +18 -0
- package/template/.claude/commands/agentful-worktree.md +106 -0
- package/template/.claude/settings.json +32 -9
- package/template/CLAUDE.md +37 -2
- package/template/bin/hooks/block-random-docs.js +0 -1
- package/version.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Package Metadata Guard Hook
|
|
5
|
+
*
|
|
6
|
+
* Protects package.json metadata from accidental ownership corruption.
|
|
7
|
+
*
|
|
8
|
+
* Catches:
|
|
9
|
+
* - repository.type being changed away from "git"
|
|
10
|
+
* - repository.url being changed to github.com/itz4blitz/* in repos owned by someone else
|
|
11
|
+
*
|
|
12
|
+
* Run: PostToolUse (Write|Edit)
|
|
13
|
+
* Action: Exit non-zero so the agent corrects the change immediately.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
|
|
20
|
+
const AGENTFUL_OWNER = 'itz4blitz';
|
|
21
|
+
|
|
22
|
+
function getOriginOwner() {
|
|
23
|
+
try {
|
|
24
|
+
const remote = execSync('git config --get remote.origin.url', {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
27
|
+
}).trim();
|
|
28
|
+
|
|
29
|
+
if (!remote) return null;
|
|
30
|
+
|
|
31
|
+
const httpsMatch = remote.match(/github\.com[:/]+([^/]+)\/[^/]+(?:\.git)?$/i);
|
|
32
|
+
if (httpsMatch?.[1]) {
|
|
33
|
+
return httpsMatch[1].toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveFilePath(filePath) {
|
|
43
|
+
if (!filePath) return null;
|
|
44
|
+
return path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isPackageJsonTarget(filePath) {
|
|
48
|
+
if (!filePath) return false;
|
|
49
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
50
|
+
return path.basename(normalized) === 'package.json' && !normalized.includes('/node_modules/');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inspectPackageMetadata(packageJsonPath, originOwner) {
|
|
54
|
+
const issues = [];
|
|
55
|
+
|
|
56
|
+
let pkg;
|
|
57
|
+
try {
|
|
58
|
+
pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
59
|
+
} catch {
|
|
60
|
+
return issues;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!pkg.repository || typeof pkg.repository !== 'object') {
|
|
64
|
+
return issues;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const repoType = pkg.repository.type;
|
|
68
|
+
if (typeof repoType === 'string' && repoType.trim().toLowerCase() !== 'git') {
|
|
69
|
+
issues.push(`repository.type is "${repoType}" (expected "git")`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const repoUrl = pkg.repository.url;
|
|
73
|
+
if (typeof repoUrl === 'string') {
|
|
74
|
+
const ownerInUrl = repoUrl.match(/github\.com[:/]+([^/]+)\//i)?.[1]?.toLowerCase();
|
|
75
|
+
if (ownerInUrl === AGENTFUL_OWNER && originOwner && originOwner !== AGENTFUL_OWNER) {
|
|
76
|
+
issues.push(`repository.url points to "${AGENTFUL_OWNER}" but git remote owner is "${originOwner}"`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return issues;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
const filePath = process.env.FILE || '';
|
|
85
|
+
const toolName = process.env.TOOL_NAME || '';
|
|
86
|
+
|
|
87
|
+
if (toolName !== 'Write' && toolName !== 'Edit') {
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!isPackageJsonTarget(filePath)) {
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const absolutePath = resolveFilePath(filePath);
|
|
96
|
+
if (!absolutePath || !fs.existsSync(absolutePath)) {
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const originOwner = getOriginOwner();
|
|
101
|
+
const issues = inspectPackageMetadata(absolutePath, originOwner);
|
|
102
|
+
|
|
103
|
+
if (issues.length === 0) {
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.error('\nโ BLOCKED: Suspicious package.json metadata change\n');
|
|
108
|
+
console.error(`File: ${filePath}`);
|
|
109
|
+
for (const issue of issues) {
|
|
110
|
+
console.error(`- ${issue}`);
|
|
111
|
+
}
|
|
112
|
+
console.error('\nExpected behavior: preserve project ownership metadata and keep repository.type as "git".');
|
|
113
|
+
console.error('Action: restore the package.json metadata to match the current project repository.\n');
|
|
114
|
+
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main();
|
|
@@ -29,15 +29,42 @@ try {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Detect if TeammateTool (parallel execution) is enabled
|
|
32
|
+
* Supports Windows, macOS, and Linux
|
|
32
33
|
*/
|
|
33
34
|
function detectParallelExecution() {
|
|
35
|
+
// Check environment variable first (user can set AGENTFUL_PARALLEL=true)
|
|
36
|
+
if (process.env.AGENTFUL_PARALLEL === 'true') {
|
|
37
|
+
return { enabled: true, method: 'env_var' };
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
try {
|
|
35
|
-
// Find Claude Code binary
|
|
36
|
-
|
|
37
|
-
const
|
|
41
|
+
// Find Claude Code binary - try multiple paths for Windows/Unix
|
|
42
|
+
let cliPath = null;
|
|
43
|
+
const possiblePaths = [
|
|
44
|
+
// Unix npm global
|
|
45
|
+
'/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
46
|
+
// Homebrew on macOS
|
|
47
|
+
'/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// npm root -g can throw on Windows if npm isn't in PATH
|
|
51
|
+
try {
|
|
52
|
+
const npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
53
|
+
possiblePaths.unshift(path.join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js'));
|
|
54
|
+
} catch {
|
|
55
|
+
// npm not available - continue with static paths
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const p of possiblePaths) {
|
|
59
|
+
if (fs.existsSync(p)) {
|
|
60
|
+
cliPath = p;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
38
64
|
|
|
39
|
-
if (!
|
|
40
|
-
|
|
65
|
+
if (!cliPath) {
|
|
66
|
+
// Assume enabled if we can't find CLI (newer versions have it by default)
|
|
67
|
+
return { enabled: true, method: 'assumed' };
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
// Check for TeammateTool pattern
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Git Worktree Service
|
|
5
|
+
*
|
|
6
|
+
* Core service for managing git worktrees in agentful.
|
|
7
|
+
* Provides worktree creation, tracking, and cleanup functions.
|
|
8
|
+
*
|
|
9
|
+
* Environment Variables:
|
|
10
|
+
* AGENTFUL_WORKTREE_MODE - auto|block|off (default: auto)
|
|
11
|
+
* AGENTFUL_WORKTREE_LOCATION - Where to create worktrees (default: ../)
|
|
12
|
+
* AGENTFUL_WORKTREE_AUTO_CLEANUP - Auto-remove after completion (default: true)
|
|
13
|
+
* AGENTFUL_WORKTREE_RETENTION_DAYS - Days before cleanup (default: 7)
|
|
14
|
+
* AGENTFUL_WORKTREE_MAX_ACTIVE - Max active worktrees (default: 5)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node worktree-service.js create <purpose> <branch>
|
|
18
|
+
* node worktree-service.js list
|
|
19
|
+
* node worktree-service.js cleanup
|
|
20
|
+
* node worktree-service.js prune
|
|
21
|
+
* node worktree-service.js status
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { execSync } from 'child_process';
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
// Configuration from environment
|
|
33
|
+
const CONFIG = {
|
|
34
|
+
mode: process.env.AGENTFUL_WORKTREE_MODE || 'auto',
|
|
35
|
+
location: process.env.AGENTFUL_WORKTREE_LOCATION || '../',
|
|
36
|
+
autoCleanup: process.env.AGENTFUL_WORKTREE_AUTO_CLEANUP !== 'false',
|
|
37
|
+
retentionDays: parseInt(process.env.AGENTFUL_WORKTREE_RETENTION_DAYS || '7', 10),
|
|
38
|
+
maxActive: parseInt(process.env.AGENTFUL_WORKTREE_MAX_ACTIVE || '5', 10),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Paths
|
|
42
|
+
const REPO_ROOT = findRepoRoot();
|
|
43
|
+
const WORKTREES_DIR = path.join(REPO_ROOT, '.git', 'worktrees');
|
|
44
|
+
const TRACKING_FILE = path.join(REPO_ROOT, '.agentful', 'worktrees.json');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find git repository root
|
|
48
|
+
*/
|
|
49
|
+
function findRepoRoot() {
|
|
50
|
+
try {
|
|
51
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
cwd: process.cwd(),
|
|
54
|
+
}).trim();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('โ Not in a git repository');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get current git branch
|
|
63
|
+
*/
|
|
64
|
+
function getCurrentBranch() {
|
|
65
|
+
try {
|
|
66
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
cwd: process.cwd(),
|
|
69
|
+
}).trim();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return 'unknown';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get current working directory relative to repo root
|
|
77
|
+
*/
|
|
78
|
+
function getCurrentWorktree() {
|
|
79
|
+
const cwd = process.cwd();
|
|
80
|
+
const gitCommonDir = execSync('git rev-parse --git-common-dir', {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
cwd,
|
|
83
|
+
}).trim();
|
|
84
|
+
|
|
85
|
+
// If .git is a file, we're in a worktree
|
|
86
|
+
const gitFile = path.join(cwd, '.git');
|
|
87
|
+
const isWorktree = fs.existsSync(gitFile) && fs.statSync(gitFile).isFile();
|
|
88
|
+
|
|
89
|
+
if (isWorktree) {
|
|
90
|
+
// Read the worktree path from .git file
|
|
91
|
+
const gitDir = fs.readFileSync(gitFile, 'utf8').trim();
|
|
92
|
+
const worktreePath = gitDir.replace('gitdir: ', '').replace('/.git', '');
|
|
93
|
+
return path.basename(worktreePath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sanitize branch name for use in worktree name
|
|
101
|
+
*/
|
|
102
|
+
function sanitizeBranchName(branch) {
|
|
103
|
+
return branch
|
|
104
|
+
.replace(/\//g, '-') // Replace / with -
|
|
105
|
+
.replace(/[^a-zA-Z0-9-]/g, '') // Remove special chars
|
|
106
|
+
.replace(/-+/g, '-') // Collapse multiple dashes
|
|
107
|
+
.replace(/^-|-$/g, ''); // Trim leading/trailing dashes
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validate worktree name for security
|
|
112
|
+
*/
|
|
113
|
+
function validateWorktreeName(name) {
|
|
114
|
+
// Prevent path traversal
|
|
115
|
+
const resolvedPath = path.resolve(REPO_ROOT, CONFIG.location, name);
|
|
116
|
+
if (!resolvedPath.startsWith(REPO_ROOT)) {
|
|
117
|
+
throw new Error('Invalid worktree name: path traversal detected');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for valid characters
|
|
121
|
+
if (!/^[a-zA-Z0-9-]+$/.test(name)) {
|
|
122
|
+
throw new Error('Invalid worktree name: contains invalid characters');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get tracked worktrees from .agentful/worktrees.json
|
|
130
|
+
*/
|
|
131
|
+
function getTrackedWorktrees() {
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(TRACKING_FILE)) {
|
|
134
|
+
const content = fs.readFileSync(TRACKING_FILE, 'utf8');
|
|
135
|
+
return JSON.parse(content);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Invalid tracking file, start fresh
|
|
139
|
+
console.warn(`โ ๏ธ Corrupted tracking file, starting fresh: ${TRACKING_FILE}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { active: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Save tracked worktrees to .agentful/worktrees.json
|
|
147
|
+
*/
|
|
148
|
+
function saveTrackedWorktrees(data) {
|
|
149
|
+
const dir = path.dirname(TRACKING_FILE);
|
|
150
|
+
if (!fs.existsSync(dir)) {
|
|
151
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
TRACKING_FILE,
|
|
156
|
+
JSON.stringify(data, null, 2),
|
|
157
|
+
'utf8'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get list of all git worktrees
|
|
163
|
+
*/
|
|
164
|
+
function getGitWorktrees() {
|
|
165
|
+
try {
|
|
166
|
+
const output = execSync('git worktree list', {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
cwd: REPO_ROOT,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return output.trim().split('\n').map(line => {
|
|
172
|
+
const [worktreePath, commit, branch] = line.split(/\s+/);
|
|
173
|
+
return {
|
|
174
|
+
path: worktreePath,
|
|
175
|
+
commit,
|
|
176
|
+
branch: branch.replace(/[\[\]]/g, ''), // Remove [ ] brackets
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a branch is already checked out
|
|
186
|
+
*/
|
|
187
|
+
function isBranchCheckedOut(branch) {
|
|
188
|
+
const worktrees = getGitWorktrees();
|
|
189
|
+
return worktrees.some(w => w.branch === branch);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a unique branch name if original is already checked out
|
|
194
|
+
*/
|
|
195
|
+
function getUniqueBranch(originalBranch, timestamp) {
|
|
196
|
+
if (!isBranchCheckedOut(originalBranch)) {
|
|
197
|
+
return originalBranch;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Create unique branch with timestamp
|
|
201
|
+
const uniqueName = `${originalBranch}-agentful-${timestamp}`;
|
|
202
|
+
console.log(`โ ๏ธ Branch "${originalBranch}" already checked out`);
|
|
203
|
+
console.log(` Creating unique branch: ${uniqueName}`);
|
|
204
|
+
return uniqueName;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check disk space
|
|
209
|
+
*/
|
|
210
|
+
function checkDiskSpace(requiredBytes) {
|
|
211
|
+
try {
|
|
212
|
+
const stats = fs.statSync(REPO_ROOT);
|
|
213
|
+
// Note: This is a basic check. For production, use df or similar
|
|
214
|
+
return true;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.warn('โ ๏ธ Could not check disk space');
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a new worktree
|
|
223
|
+
*/
|
|
224
|
+
function createWorktree(purpose, branch = null) {
|
|
225
|
+
const timestamp = Date.now();
|
|
226
|
+
const currentBranch = branch || getCurrentBranch();
|
|
227
|
+
const sanitizedBranch = sanitizeBranchName(currentBranch);
|
|
228
|
+
|
|
229
|
+
// Generate worktree name
|
|
230
|
+
const worktreeName = `agentful-${purpose}-${sanitizedBranch}-${timestamp}`;
|
|
231
|
+
|
|
232
|
+
// Validate
|
|
233
|
+
validateWorktreeName(worktreeName);
|
|
234
|
+
|
|
235
|
+
// Get unique branch if needed
|
|
236
|
+
const targetBranch = getUniqueBranch(currentBranch, timestamp);
|
|
237
|
+
|
|
238
|
+
// Check max active worktrees
|
|
239
|
+
const tracked = getTrackedWorktrees();
|
|
240
|
+
if (tracked.active.length >= CONFIG.maxActive) {
|
|
241
|
+
console.warn(`โ ๏ธ Maximum active worktrees reached (${CONFIG.maxActive})`);
|
|
242
|
+
console.warn(' Run cleanup first: node worktree-service.js cleanup');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Create worktree
|
|
246
|
+
const worktreePath = path.join(REPO_ROOT, CONFIG.location, worktreeName);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
console.log(`๐ณ Creating worktree: ${worktreeName}`);
|
|
250
|
+
console.log(` Branch: ${targetBranch}`);
|
|
251
|
+
console.log(` Path: ${worktreePath}`);
|
|
252
|
+
|
|
253
|
+
execSync(
|
|
254
|
+
`git worktree add "${worktreePath}" -b "${targetBranch}"`,
|
|
255
|
+
{ cwd: REPO_ROOT, stdio: 'inherit' }
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Initialize submodules if present
|
|
259
|
+
const submodulePath = path.join(worktreePath, '.gitmodules');
|
|
260
|
+
if (fs.existsSync(submodulePath)) {
|
|
261
|
+
console.log(' ๐ฆ Initializing submodules...');
|
|
262
|
+
execSync('git submodule update --init --recursive', {
|
|
263
|
+
cwd: worktreePath,
|
|
264
|
+
stdio: 'inherit',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Track the worktree
|
|
269
|
+
const worktreeData = {
|
|
270
|
+
name: worktreeName,
|
|
271
|
+
path: worktreePath,
|
|
272
|
+
branch: targetBranch,
|
|
273
|
+
purpose,
|
|
274
|
+
created_at: new Date().toISOString(),
|
|
275
|
+
last_activity: new Date().toISOString(),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
tracked.active.push(worktreeData);
|
|
279
|
+
saveTrackedWorktrees(tracked);
|
|
280
|
+
|
|
281
|
+
console.log(`โ
Worktree created successfully`);
|
|
282
|
+
console.log(` Export AGENTFUL_WORKTREE_DIR="${worktreePath}"`);
|
|
283
|
+
|
|
284
|
+
// Output worktree path for easy capture
|
|
285
|
+
console.log(`WORKTREE_PATH=${worktreePath}`);
|
|
286
|
+
|
|
287
|
+
return worktreeData;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(`โ Failed to create worktree: ${error.message}`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* List all tracked worktrees
|
|
296
|
+
*/
|
|
297
|
+
function listWorktrees() {
|
|
298
|
+
const tracked = getTrackedWorktrees();
|
|
299
|
+
const gitWorktrees = getGitWorktrees();
|
|
300
|
+
|
|
301
|
+
console.log('\n๐ Active Worktrees:\n');
|
|
302
|
+
|
|
303
|
+
if (tracked.active.length === 0) {
|
|
304
|
+
console.log(' No active worktrees\n');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
tracked.active.forEach(w => {
|
|
309
|
+
const gitWt = gitWorktrees.find(gw => gw.path === w.path);
|
|
310
|
+
const exists = !!gitWt;
|
|
311
|
+
const ageMs = Date.now() - new Date(w.created_at).getTime();
|
|
312
|
+
const ageMins = Math.floor(ageMs / 60000);
|
|
313
|
+
|
|
314
|
+
let status = exists ? '๐ข Active' : '๐ด Orphaned';
|
|
315
|
+
if (ageMins > 60) status = '๐ก Stale';
|
|
316
|
+
|
|
317
|
+
console.log(`โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ`);
|
|
318
|
+
console.log(`โ ${w.name}`);
|
|
319
|
+
console.log(`โ โโ Branch: ${w.branch}`);
|
|
320
|
+
console.log(`โ โโ Purpose: ${w.purpose}`);
|
|
321
|
+
console.log(`โ โโ Path: ${w.path}`);
|
|
322
|
+
console.log(`โ โโ Status: ${status} (${ageMins} min ago)`);
|
|
323
|
+
console.log(`โ โโ Created: ${w.created_at}`);
|
|
324
|
+
console.log(`โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ`);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.log('');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get status of current session
|
|
332
|
+
*/
|
|
333
|
+
function getStatus() {
|
|
334
|
+
const currentWorktree = getCurrentWorktree();
|
|
335
|
+
const tracked = getTrackedWorktrees();
|
|
336
|
+
|
|
337
|
+
if (currentWorktree) {
|
|
338
|
+
const wt = tracked.active.find(w => w.name === currentWorktree);
|
|
339
|
+
if (wt) {
|
|
340
|
+
console.log(`\n๐ณ Current worktree: ${wt.name}`);
|
|
341
|
+
console.log(` Branch: ${wt.branch}`);
|
|
342
|
+
console.log(` Purpose: ${wt.purpose}`);
|
|
343
|
+
console.log(` Path: ${wt.path}\n`);
|
|
344
|
+
} else {
|
|
345
|
+
console.log(`\nโ ๏ธ In untracked worktree: ${currentWorktree}`);
|
|
346
|
+
console.log(` Worktree exists but not tracked by agentful\n`);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
console.log('\n๐ Working in root repository');
|
|
350
|
+
console.log(' No active worktree for this session\n');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(`Mode: ${CONFIG.mode}`);
|
|
354
|
+
console.log(`Location: ${CONFIG.location}`);
|
|
355
|
+
console.log(`Auto-cleanup: ${CONFIG.autoCleanup ? 'enabled' : 'disabled'}`);
|
|
356
|
+
console.log(`Max active: ${CONFIG.maxActive}\n`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Cleanup stale worktrees
|
|
361
|
+
*/
|
|
362
|
+
function cleanupWorktrees() {
|
|
363
|
+
const tracked = getTrackedWorktrees();
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
const retentionMs = CONFIG.retentionDays * 24 * 60 * 60 * 1000;
|
|
366
|
+
let cleanedCount = 0;
|
|
367
|
+
|
|
368
|
+
console.log(`\n๐งน Cleaning up worktrees older than ${CONFIG.retentionDays} days...\n`);
|
|
369
|
+
|
|
370
|
+
const active = tracked.active.filter(w => {
|
|
371
|
+
const age = now - new Date(w.created_at).getTime();
|
|
372
|
+
const isStale = age > retentionMs;
|
|
373
|
+
|
|
374
|
+
if (isStale) {
|
|
375
|
+
console.log(`๐๏ธ Removing stale: ${w.name}`);
|
|
376
|
+
try {
|
|
377
|
+
execSync(`git worktree remove "${w.path}"`, {
|
|
378
|
+
cwd: REPO_ROOT,
|
|
379
|
+
stdio: 'inherit',
|
|
380
|
+
});
|
|
381
|
+
cleanedCount++;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.warn(` โ ๏ธ Failed to remove: ${error.message}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return !isStale; // Keep only non-stale
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Save updated tracking
|
|
391
|
+
saveTrackedWorktrees({ active });
|
|
392
|
+
|
|
393
|
+
// Also run git prune to clean metadata
|
|
394
|
+
console.log('\n๐งน Pruning git worktree metadata...');
|
|
395
|
+
try {
|
|
396
|
+
execSync('git worktree prune', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.warn(` โ ๏ธ Prune warning: ${error.message}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(`\nโ
Cleanup complete. Removed ${cleanedCount} worktree(s).\n`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Prune git worktree metadata
|
|
406
|
+
*/
|
|
407
|
+
function pruneWorktrees() {
|
|
408
|
+
console.log('\n๐งน Pruning git worktree metadata...\n');
|
|
409
|
+
try {
|
|
410
|
+
execSync('git worktree prune', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
411
|
+
console.log('โ
Prune complete\n');
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error(`โ Prune failed: ${error.message}`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Remove a specific worktree
|
|
420
|
+
*/
|
|
421
|
+
function removeWorktree(worktreeName) {
|
|
422
|
+
const tracked = getTrackedWorktrees();
|
|
423
|
+
const worktree = tracked.active.find(w => w.name === worktreeName);
|
|
424
|
+
|
|
425
|
+
if (!worktree) {
|
|
426
|
+
console.error(`โ Worktree not found: ${worktreeName}`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
console.log(`๐๏ธ Removing worktree: ${worktreeName}`);
|
|
432
|
+
execSync(`git worktree remove "${worktree.path}"`, {
|
|
433
|
+
cwd: REPO_ROOT,
|
|
434
|
+
stdio: 'inherit',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Remove from tracking
|
|
438
|
+
const active = tracked.active.filter(w => w.name !== worktreeName);
|
|
439
|
+
saveTrackedWorktrees({ active });
|
|
440
|
+
|
|
441
|
+
console.log('โ
Worktree removed\n');
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error(`โ Failed to remove worktree: ${error.message}`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Detect if running in CI environment
|
|
450
|
+
*/
|
|
451
|
+
function isCIEnvironment() {
|
|
452
|
+
return process.env.CI === 'true' ||
|
|
453
|
+
process.env.GITHUB_ACTIONS === 'true' ||
|
|
454
|
+
process.env.GITLAB_CI === 'true' ||
|
|
455
|
+
process.env.CIRCLECI === 'true';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// CLI interface
|
|
459
|
+
const command = process.argv[2];
|
|
460
|
+
const args = process.argv.slice(3);
|
|
461
|
+
|
|
462
|
+
// Auto-disable worktree mode in CI
|
|
463
|
+
if (isCIEnvironment()) {
|
|
464
|
+
console.log('๐ค CI environment detected. Worktree mode auto-disabled.\n');
|
|
465
|
+
process.exit(0);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
switch (command) {
|
|
469
|
+
case 'create':
|
|
470
|
+
if (args.length < 1) {
|
|
471
|
+
console.error('Usage: worktree-service.js create <purpose> [branch]');
|
|
472
|
+
console.error('Example: worktree-service.js create feature feature/auth');
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
createWorktree(args[0], args[1]);
|
|
476
|
+
break;
|
|
477
|
+
|
|
478
|
+
case 'list':
|
|
479
|
+
listWorktrees();
|
|
480
|
+
break;
|
|
481
|
+
|
|
482
|
+
case 'status':
|
|
483
|
+
getStatus();
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'cleanup':
|
|
487
|
+
cleanupWorktrees();
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'prune':
|
|
491
|
+
pruneWorktrees();
|
|
492
|
+
break;
|
|
493
|
+
|
|
494
|
+
case 'remove':
|
|
495
|
+
if (args.length < 1) {
|
|
496
|
+
console.error('Usage: worktree-service.js remove <worktree-name>');
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
removeWorktree(args[0]);
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
default:
|
|
503
|
+
console.log('Git Worktree Service for agentful\n');
|
|
504
|
+
console.log('Usage: node worktree-service.js <command> [args]\n');
|
|
505
|
+
console.log('Commands:');
|
|
506
|
+
console.log(' create <purpose> [branch] Create a new worktree');
|
|
507
|
+
console.log(' list List all tracked worktrees');
|
|
508
|
+
console.log(' status Show current worktree status');
|
|
509
|
+
console.log(' cleanup Remove stale worktrees');
|
|
510
|
+
console.log(' prune Prune git worktree metadata');
|
|
511
|
+
console.log(' remove <name> Remove specific worktree\n');
|
|
512
|
+
console.log(`Environment: AGENTFUL_WORKTREE_MODE=${CONFIG.mode}`);
|
|
513
|
+
process.exit(0);
|
|
514
|
+
}
|