panopticon-cli 0.4.32 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -210
- package/dist/{agents-BDFHF4T3.js → agents-E43Y3HNU.js} +10 -7
- package/dist/chunk-7SN4L4PH.js +150 -0
- package/dist/chunk-7SN4L4PH.js.map +1 -0
- package/dist/{chunk-2NIAOCIC.js → chunk-AAFQANKW.js} +358 -97
- package/dist/chunk-AAFQANKW.js.map +1 -0
- package/dist/chunk-AQXETQHW.js +113 -0
- package/dist/chunk-AQXETQHW.js.map +1 -0
- package/dist/chunk-B3PF6JPQ.js +212 -0
- package/dist/chunk-B3PF6JPQ.js.map +1 -0
- package/dist/chunk-CFCUOV3Q.js +669 -0
- package/dist/chunk-CFCUOV3Q.js.map +1 -0
- package/dist/chunk-CWELWPWQ.js +32 -0
- package/dist/chunk-CWELWPWQ.js.map +1 -0
- package/dist/chunk-DI7ABPNQ.js +352 -0
- package/dist/chunk-DI7ABPNQ.js.map +1 -0
- package/dist/{chunk-VU4FLXV5.js → chunk-FQ66DECN.js} +31 -4
- package/dist/chunk-FQ66DECN.js.map +1 -0
- package/dist/{chunk-VIWUCJ4V.js → chunk-FTCPTHIJ.js} +57 -432
- package/dist/chunk-FTCPTHIJ.js.map +1 -0
- package/dist/{review-status-GWQYY77L.js → chunk-GFP3PIPB.js} +14 -7
- package/dist/chunk-GFP3PIPB.js.map +1 -0
- package/dist/chunk-GR6ZZMCX.js +816 -0
- package/dist/chunk-GR6ZZMCX.js.map +1 -0
- package/dist/chunk-HJSM6E6U.js +1038 -0
- package/dist/chunk-HJSM6E6U.js.map +1 -0
- package/dist/{chunk-XP2DXWYP.js → chunk-HZT2AOPN.js} +164 -39
- package/dist/chunk-HZT2AOPN.js.map +1 -0
- package/dist/chunk-JQBV3Q2W.js +29 -0
- package/dist/chunk-JQBV3Q2W.js.map +1 -0
- package/dist/{chunk-BWGFN44T.js → chunk-JT4O4YVM.js} +28 -16
- package/dist/chunk-JT4O4YVM.js.map +1 -0
- package/dist/chunk-NTO3EDB3.js +600 -0
- package/dist/chunk-NTO3EDB3.js.map +1 -0
- package/dist/{chunk-JY7R7V4G.js → chunk-OMNXYPXC.js} +2 -2
- package/dist/chunk-OMNXYPXC.js.map +1 -0
- package/dist/chunk-PELXV435.js +215 -0
- package/dist/chunk-PELXV435.js.map +1 -0
- package/dist/chunk-PPRFKTVC.js +154 -0
- package/dist/chunk-PPRFKTVC.js.map +1 -0
- package/dist/chunk-WQG2TYCB.js +677 -0
- package/dist/chunk-WQG2TYCB.js.map +1 -0
- package/dist/{chunk-HCTJFIJJ.js → chunk-YLPSQAM2.js} +2 -2
- package/dist/{chunk-HCTJFIJJ.js.map → chunk-YLPSQAM2.js.map} +1 -1
- package/dist/{chunk-6HXKTOD7.js → chunk-ZTFNYOC7.js} +53 -38
- package/dist/chunk-ZTFNYOC7.js.map +1 -0
- package/dist/cli/index.js +5103 -3165
- package/dist/cli/index.js.map +1 -1
- package/dist/{config-BOAMSKTF.js → config-4CJNUE3O.js} +7 -3
- package/dist/dashboard/prompts/merge-agent.md +217 -0
- package/dist/dashboard/prompts/review-agent.md +409 -0
- package/dist/dashboard/prompts/sync-main.md +84 -0
- package/dist/dashboard/prompts/test-agent.md +283 -0
- package/dist/dashboard/prompts/work-agent.md +249 -0
- package/dist/dashboard/public/assets/index-BxpjweAL.css +32 -0
- package/dist/dashboard/public/assets/index-DQHkwvvJ.js +743 -0
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/server.js +17619 -4044
- package/dist/{dns-L3L2BB27.js → dns-7BDJSD3E.js} +4 -2
- package/dist/{feedback-writer-AAKF5BTK.js → feedback-writer-LVZ5TFYZ.js} +8 -4
- package/dist/feedback-writer-LVZ5TFYZ.js.map +1 -0
- package/dist/hume-WMAUBBV2.js +13 -0
- package/dist/index.d.ts +162 -40
- package/dist/index.js +67 -23
- package/dist/index.js.map +1 -1
- package/dist/{projects-VXRUCMLM.js → projects-JEIVIYC6.js} +3 -3
- package/dist/rally-RKFSWC7E.js +10 -0
- package/dist/{remote-agents-Z3R2A5BN.js → remote-agents-TFSMW7GN.js} +2 -2
- package/dist/{remote-workspace-2G6V2KNP.js → remote-workspace-AHVHQEES.js} +8 -8
- package/dist/review-status-EPFG4XM7.js +19 -0
- package/dist/shadow-state-5MDP6YXH.js +30 -0
- package/dist/shadow-state-5MDP6YXH.js.map +1 -0
- package/dist/{specialist-context-N32QBNNQ.js → specialist-context-ZC6A4M3I.js} +8 -7
- package/dist/{specialist-context-N32QBNNQ.js.map → specialist-context-ZC6A4M3I.js.map} +1 -1
- package/dist/{specialist-logs-GF3YV4KL.js → specialist-logs-KLGJCEUL.js} +7 -6
- package/dist/specialist-logs-KLGJCEUL.js.map +1 -0
- package/dist/{specialists-JBIW6MP4.js → specialists-O4HWDJL5.js} +7 -6
- package/dist/specialists-O4HWDJL5.js.map +1 -0
- package/dist/tldr-daemon-T3THOUGT.js +21 -0
- package/dist/tldr-daemon-T3THOUGT.js.map +1 -0
- package/dist/traefik-QN7R5I6V.js +19 -0
- package/dist/traefik-QN7R5I6V.js.map +1 -0
- package/dist/tunnel-W2GZBLEV.js +13 -0
- package/dist/tunnel-W2GZBLEV.js.map +1 -0
- package/dist/workspace-manager-IE4JL2JP.js +22 -0
- package/dist/workspace-manager-IE4JL2JP.js.map +1 -0
- package/package.json +2 -2
- package/scripts/heartbeat-hook +37 -10
- package/scripts/patches/llm-tldr-tsx-support.py +109 -0
- package/scripts/pre-tool-hook +26 -15
- package/scripts/record-cost-event.js +177 -43
- package/scripts/record-cost-event.ts +87 -3
- package/scripts/statusline.sh +169 -0
- package/scripts/stop-hook +21 -11
- package/scripts/tldr-post-edit +72 -0
- package/scripts/tldr-read-enforcer +275 -0
- package/scripts/work-agent-stop-hook +137 -0
- package/skills/check-merged/SKILL.md +143 -0
- package/skills/crash-investigation/SKILL.md +301 -0
- package/skills/github-cli/SKILL.md +185 -0
- package/skills/myn-standards/SKILL.md +351 -0
- package/skills/pan-reopen/SKILL.md +65 -0
- package/skills/pan-sync-main/SKILL.md +87 -0
- package/skills/pan-tldr/SKILL.md +149 -0
- package/skills/react-best-practices/SKILL.md +125 -0
- package/skills/spec-readiness/REPORT-TEMPLATE.md +158 -0
- package/skills/spec-readiness/SCORING-REFERENCE.md +369 -0
- package/skills/spec-readiness/SKILL.md +400 -0
- package/skills/spec-readiness-setup/SKILL.md +361 -0
- package/skills/workspace-status/SKILL.md +56 -0
- package/skills/write-spec/SKILL.md +138 -0
- package/templates/traefik/dynamic/panopticon.yml.template +0 -5
- package/templates/traefik/traefik.yml +0 -8
- package/dist/chunk-2NIAOCIC.js.map +0 -1
- package/dist/chunk-3XAB4IXF.js +0 -51
- package/dist/chunk-3XAB4IXF.js.map +0 -1
- package/dist/chunk-6HXKTOD7.js.map +0 -1
- package/dist/chunk-BBCUK6N2.js +0 -241
- package/dist/chunk-BBCUK6N2.js.map +0 -1
- package/dist/chunk-BWGFN44T.js.map +0 -1
- package/dist/chunk-ELK6Q7QI.js +0 -545
- package/dist/chunk-ELK6Q7QI.js.map +0 -1
- package/dist/chunk-JY7R7V4G.js.map +0 -1
- package/dist/chunk-LYSBSZYV.js +0 -1523
- package/dist/chunk-LYSBSZYV.js.map +0 -1
- package/dist/chunk-VIWUCJ4V.js.map +0 -1
- package/dist/chunk-VU4FLXV5.js.map +0 -1
- package/dist/chunk-XP2DXWYP.js.map +0 -1
- package/dist/dashboard/public/assets/index-C7X6LP5Z.css +0 -32
- package/dist/dashboard/public/assets/index-ClYqpcAJ.js +0 -645
- package/dist/feedback-writer-AAKF5BTK.js.map +0 -1
- package/dist/review-status-GWQYY77L.js.map +0 -1
- package/dist/traefik-CUJM6K5Z.js +0 -12
- /package/dist/{agents-BDFHF4T3.js.map → agents-E43Y3HNU.js.map} +0 -0
- /package/dist/{config-BOAMSKTF.js.map → config-4CJNUE3O.js.map} +0 -0
- /package/dist/{dns-L3L2BB27.js.map → dns-7BDJSD3E.js.map} +0 -0
- /package/dist/{projects-VXRUCMLM.js.map → hume-WMAUBBV2.js.map} +0 -0
- /package/dist/{remote-agents-Z3R2A5BN.js.map → projects-JEIVIYC6.js.map} +0 -0
- /package/dist/{specialist-logs-GF3YV4KL.js.map → rally-RKFSWC7E.js.map} +0 -0
- /package/dist/{specialists-JBIW6MP4.js.map → remote-agents-TFSMW7GN.js.map} +0 -0
- /package/dist/{remote-workspace-2G6V2KNP.js.map → remote-workspace-AHVHQEES.js.map} +0 -0
- /package/dist/{traefik-CUJM6K5Z.js.map → review-status-EPFG4XM7.js.map} +0 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHumeConfig,
|
|
3
|
+
deleteHumeConfig,
|
|
4
|
+
init_hume
|
|
5
|
+
} from "./chunk-7SN4L4PH.js";
|
|
6
|
+
import {
|
|
7
|
+
collectSourceFiles,
|
|
8
|
+
compareFileToManifest,
|
|
9
|
+
hashFile,
|
|
10
|
+
init_manifest,
|
|
11
|
+
readManifest,
|
|
12
|
+
setManifestEntry,
|
|
13
|
+
writeManifest
|
|
14
|
+
} from "./chunk-AQXETQHW.js";
|
|
15
|
+
import {
|
|
16
|
+
CACHE_AGENTS_DIR,
|
|
17
|
+
CACHE_RULES_DIR,
|
|
18
|
+
SKILLS_DIR,
|
|
19
|
+
init_paths
|
|
20
|
+
} from "./chunk-ZTFNYOC7.js";
|
|
21
|
+
import {
|
|
22
|
+
addDnsEntry,
|
|
23
|
+
init_dns,
|
|
24
|
+
removeDnsEntry,
|
|
25
|
+
syncDnsToWindows
|
|
26
|
+
} from "./chunk-JT4O4YVM.js";
|
|
27
|
+
import {
|
|
28
|
+
addTunnelIngress,
|
|
29
|
+
init_tunnel,
|
|
30
|
+
removeTunnelIngress
|
|
31
|
+
} from "./chunk-PELXV435.js";
|
|
32
|
+
import {
|
|
33
|
+
getDefaultWorkspaceConfig,
|
|
34
|
+
init_workspace_config,
|
|
35
|
+
replacePlaceholders
|
|
36
|
+
} from "./chunk-CWELWPWQ.js";
|
|
37
|
+
import {
|
|
38
|
+
__esm,
|
|
39
|
+
init_esm_shims
|
|
40
|
+
} from "./chunk-ZHC57RCV.js";
|
|
41
|
+
|
|
42
|
+
// src/lib/skills-merge.ts
|
|
43
|
+
import {
|
|
44
|
+
existsSync,
|
|
45
|
+
readdirSync,
|
|
46
|
+
mkdirSync,
|
|
47
|
+
readFileSync,
|
|
48
|
+
writeFileSync,
|
|
49
|
+
copyFileSync
|
|
50
|
+
} from "fs";
|
|
51
|
+
import { join, relative, dirname } from "path";
|
|
52
|
+
function copyTree(sourceDir, targetDir) {
|
|
53
|
+
const copied = [];
|
|
54
|
+
if (!existsSync(sourceDir)) return copied;
|
|
55
|
+
function walk(dir) {
|
|
56
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const fullPath = join(dir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
walk(fullPath);
|
|
61
|
+
} else if (entry.isFile()) {
|
|
62
|
+
const rel = relative(sourceDir, fullPath);
|
|
63
|
+
const targetPath = join(targetDir, rel);
|
|
64
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
65
|
+
copyFileSync(fullPath, targetPath);
|
|
66
|
+
copied.push(rel);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(sourceDir);
|
|
71
|
+
return copied;
|
|
72
|
+
}
|
|
73
|
+
function mergeSkillsIntoWorkspace(workspacePath) {
|
|
74
|
+
const claudeDir = join(workspacePath, ".claude");
|
|
75
|
+
const manifestPath = join(claudeDir, ".panopticon-manifest.json");
|
|
76
|
+
const manifest = readManifest(manifestPath);
|
|
77
|
+
const result = {
|
|
78
|
+
added: [],
|
|
79
|
+
updated: [],
|
|
80
|
+
skipped: [],
|
|
81
|
+
overlayed: []
|
|
82
|
+
};
|
|
83
|
+
mkdirSync(join(claudeDir, "skills"), { recursive: true });
|
|
84
|
+
mkdirSync(join(claudeDir, "agents"), { recursive: true });
|
|
85
|
+
const sources = [
|
|
86
|
+
{ category: "skills", sourceDir: SKILLS_DIR, targetSubdir: "skills" },
|
|
87
|
+
{ category: "agents", sourceDir: CACHE_AGENTS_DIR, targetSubdir: "agents" },
|
|
88
|
+
{ category: "rules", sourceDir: CACHE_RULES_DIR, targetSubdir: "rules" }
|
|
89
|
+
];
|
|
90
|
+
for (const { category, sourceDir, targetSubdir } of sources) {
|
|
91
|
+
if (!existsSync(sourceDir)) continue;
|
|
92
|
+
const prefix = targetSubdir ? `${targetSubdir}/` : "";
|
|
93
|
+
const files = collectSourceFiles(sourceDir, "");
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const relativePath = `${prefix}${file.relativePath}`;
|
|
96
|
+
const targetPath = join(claudeDir, relativePath);
|
|
97
|
+
const sourceHash = hashFile(file.absolutePath);
|
|
98
|
+
const status = compareFileToManifest(targetPath, relativePath, manifest);
|
|
99
|
+
switch (status.action) {
|
|
100
|
+
case "new":
|
|
101
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
102
|
+
copyFileSync(file.absolutePath, targetPath);
|
|
103
|
+
setManifestEntry(manifest, relativePath, sourceHash, "panopticon");
|
|
104
|
+
result.added.push(relativePath);
|
|
105
|
+
break;
|
|
106
|
+
case "update":
|
|
107
|
+
copyFileSync(file.absolutePath, targetPath);
|
|
108
|
+
setManifestEntry(manifest, relativePath, sourceHash, "panopticon");
|
|
109
|
+
result.updated.push(relativePath);
|
|
110
|
+
break;
|
|
111
|
+
case "modified":
|
|
112
|
+
result.skipped.push(`${relativePath} (modified by user)`);
|
|
113
|
+
break;
|
|
114
|
+
case "user-owned":
|
|
115
|
+
result.skipped.push(`${relativePath} (user-owned)`);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
writeManifest(manifestPath, manifest);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function applyProjectTemplateOverlay(workspacePath, templateDir, templates) {
|
|
124
|
+
const claudeDir = join(workspacePath, ".claude");
|
|
125
|
+
const manifestPath = join(claudeDir, ".panopticon-manifest.json");
|
|
126
|
+
const manifest = readManifest(manifestPath);
|
|
127
|
+
const overlayed = [];
|
|
128
|
+
if (!existsSync(templateDir)) return overlayed;
|
|
129
|
+
if (templates && templates.length > 0) {
|
|
130
|
+
for (const { source, target } of templates) {
|
|
131
|
+
const sourcePath = join(templateDir, source);
|
|
132
|
+
if (!existsSync(sourcePath)) continue;
|
|
133
|
+
const targetPath = join(workspacePath, target);
|
|
134
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
135
|
+
if (source.endsWith(".template")) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
copyFileSync(sourcePath, targetPath);
|
|
139
|
+
if (target.startsWith(".claude/")) {
|
|
140
|
+
const relativePath = target.slice(".claude/".length);
|
|
141
|
+
const hash = hashFile(targetPath);
|
|
142
|
+
setManifestEntry(manifest, relativePath, hash, "project-template");
|
|
143
|
+
overlayed.push(relativePath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const claudeInTemplate = join(templateDir, ".claude");
|
|
148
|
+
if (existsSync(claudeInTemplate)) {
|
|
149
|
+
const copied = copyTree(claudeInTemplate, claudeDir);
|
|
150
|
+
for (const rel of copied) {
|
|
151
|
+
const targetPath = join(claudeDir, rel);
|
|
152
|
+
const hash = hashFile(targetPath);
|
|
153
|
+
setManifestEntry(manifest, rel, hash, "project-template");
|
|
154
|
+
overlayed.push(rel);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
writeManifest(manifestPath, manifest);
|
|
159
|
+
return overlayed;
|
|
160
|
+
}
|
|
161
|
+
var init_skills_merge = __esm({
|
|
162
|
+
"src/lib/skills-merge.ts"() {
|
|
163
|
+
"use strict";
|
|
164
|
+
init_esm_shims();
|
|
165
|
+
init_paths();
|
|
166
|
+
init_manifest();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// src/lib/workspace-manager.ts
|
|
171
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, copyFileSync as copyFileSync2, symlinkSync, chmodSync, realpathSync } from "fs";
|
|
172
|
+
import { join as join2, dirname as dirname2, basename, extname } from "path";
|
|
173
|
+
import { homedir } from "os";
|
|
174
|
+
import { exec } from "child_process";
|
|
175
|
+
import { promisify } from "util";
|
|
176
|
+
function createPlaceholders(projectConfig, featureName, workspacePath) {
|
|
177
|
+
const featureFolder = `feature-${featureName}`;
|
|
178
|
+
const domain = projectConfig.workspace?.dns?.domain || "localhost";
|
|
179
|
+
return {
|
|
180
|
+
FEATURE_NAME: featureName,
|
|
181
|
+
FEATURE_FOLDER: featureFolder,
|
|
182
|
+
BRANCH_NAME: `feature/${featureName}`,
|
|
183
|
+
COMPOSE_PROJECT: `${basename(projectConfig.path)}-${featureFolder}`,
|
|
184
|
+
DOMAIN: domain,
|
|
185
|
+
PROJECT_NAME: basename(projectConfig.path),
|
|
186
|
+
PROJECT_PATH: projectConfig.path,
|
|
187
|
+
WORKSPACE_PATH: workspacePath,
|
|
188
|
+
HOME: homedir()
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function sanitizeComposeFile(filePath) {
|
|
192
|
+
if (!existsSync2(filePath)) return;
|
|
193
|
+
let content = readFileSync2(filePath, "utf-8");
|
|
194
|
+
const originalContent = content;
|
|
195
|
+
const homePatterns = [
|
|
196
|
+
/\/home\/[a-zA-Z0-9_-]+\//g,
|
|
197
|
+
// Linux: /home/username/
|
|
198
|
+
/\/Users\/[a-zA-Z0-9_-]+\//g
|
|
199
|
+
// macOS: /Users/username/
|
|
200
|
+
];
|
|
201
|
+
for (const pattern of homePatterns) {
|
|
202
|
+
content = content.replace(pattern, "${HOME}/");
|
|
203
|
+
}
|
|
204
|
+
if (content !== originalContent) {
|
|
205
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function validateFeatureName(name) {
|
|
209
|
+
return /^[a-zA-Z0-9-]+$/.test(name);
|
|
210
|
+
}
|
|
211
|
+
async function createWorktree(repoPath, targetPath, branchName, defaultBranch = "main") {
|
|
212
|
+
try {
|
|
213
|
+
await execAsync("git fetch origin", { cwd: repoPath });
|
|
214
|
+
await execAsync("git worktree prune", { cwd: repoPath });
|
|
215
|
+
const { stdout: localBranches } = await execAsync("git branch --list", { cwd: repoPath });
|
|
216
|
+
const { stdout: remoteBranches } = await execAsync("git branch -r --list", { cwd: repoPath });
|
|
217
|
+
const branchExists = localBranches.includes(branchName) || remoteBranches.includes(`origin/${branchName}`);
|
|
218
|
+
if (branchExists) {
|
|
219
|
+
await execAsync(`git worktree add "${targetPath}" "${branchName}"`, { cwd: repoPath });
|
|
220
|
+
} else {
|
|
221
|
+
await execAsync(`git worktree add "${targetPath}" -b "${branchName}" "${defaultBranch}"`, { cwd: repoPath });
|
|
222
|
+
}
|
|
223
|
+
return { success: true, message: `Created worktree at ${targetPath}` };
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return { success: false, message: `Failed to create worktree: ${error}` };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function removeWorktree(repoPath, targetPath, branchName) {
|
|
229
|
+
try {
|
|
230
|
+
await execAsync(`git worktree remove "${targetPath}" --force`, { cwd: repoPath }).catch(() => {
|
|
231
|
+
});
|
|
232
|
+
await execAsync(`git branch -D "${branchName}"`, { cwd: repoPath }).catch(() => {
|
|
233
|
+
});
|
|
234
|
+
return { success: true, message: `Removed worktree at ${targetPath}` };
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return { success: false, message: `Failed to remove worktree: ${error}` };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function assignPort(portFile, featureFolder, range) {
|
|
240
|
+
if (!existsSync2(portFile)) {
|
|
241
|
+
mkdirSync2(dirname2(portFile), { recursive: true });
|
|
242
|
+
writeFileSync2(portFile, "");
|
|
243
|
+
}
|
|
244
|
+
const content = readFileSync2(portFile, "utf-8");
|
|
245
|
+
const lines = content.split("\n").filter(Boolean);
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
const [folder, port] = line.split(":");
|
|
248
|
+
if (folder === featureFolder) {
|
|
249
|
+
return parseInt(port, 10);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const usedPorts = new Set(lines.map((l) => parseInt(l.split(":")[1], 10)));
|
|
253
|
+
for (let port = range[0]; port <= range[1]; port++) {
|
|
254
|
+
if (!usedPorts.has(port)) {
|
|
255
|
+
writeFileSync2(portFile, content + (content.endsWith("\n") ? "" : "\n") + `${featureFolder}:${port}
|
|
256
|
+
`);
|
|
257
|
+
return port;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`No available ports in range ${range[0]}-${range[1]}`);
|
|
261
|
+
}
|
|
262
|
+
function releasePort(portFile, featureFolder) {
|
|
263
|
+
try {
|
|
264
|
+
if (!existsSync2(portFile)) return true;
|
|
265
|
+
let content = readFileSync2(portFile, "utf-8");
|
|
266
|
+
const lines = content.split("\n").filter((line) => !line.startsWith(`${featureFolder}:`));
|
|
267
|
+
writeFileSync2(portFile, lines.join("\n"));
|
|
268
|
+
return true;
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function processTemplates(templateDir, targetDir, placeholders, templates) {
|
|
274
|
+
const steps = [];
|
|
275
|
+
if (!existsSync2(templateDir)) {
|
|
276
|
+
return steps;
|
|
277
|
+
}
|
|
278
|
+
if (templates && templates.length > 0) {
|
|
279
|
+
for (const { source, target } of templates) {
|
|
280
|
+
const sourcePath = join2(templateDir, source);
|
|
281
|
+
const targetPath = join2(targetDir, target);
|
|
282
|
+
if (existsSync2(sourcePath)) {
|
|
283
|
+
const content = readFileSync2(sourcePath, "utf-8");
|
|
284
|
+
const processed = replacePlaceholders(content, placeholders);
|
|
285
|
+
mkdirSync2(dirname2(targetPath), { recursive: true });
|
|
286
|
+
writeFileSync2(targetPath, processed);
|
|
287
|
+
steps.push(`Processed template: ${source} -> ${target}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
const files = readdirSync2(templateDir);
|
|
292
|
+
for (const file of files) {
|
|
293
|
+
if (file.endsWith(".template")) {
|
|
294
|
+
const sourcePath = join2(templateDir, file);
|
|
295
|
+
const targetPath = join2(targetDir, file.replace(".template", ""));
|
|
296
|
+
const content = readFileSync2(sourcePath, "utf-8");
|
|
297
|
+
const processed = replacePlaceholders(content, placeholders);
|
|
298
|
+
writeFileSync2(targetPath, processed);
|
|
299
|
+
steps.push(`Processed template: ${file}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return steps;
|
|
304
|
+
}
|
|
305
|
+
function copyProjectTemplateDirs(sourceDir, targetDir, dirs, placeholders) {
|
|
306
|
+
const steps = [];
|
|
307
|
+
for (const dir of dirs) {
|
|
308
|
+
let copyDir2 = function(src, dest) {
|
|
309
|
+
let count2 = 0;
|
|
310
|
+
mkdirSync2(dest, { recursive: true });
|
|
311
|
+
const entries = readdirSync2(src, { withFileTypes: true });
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
const srcEntry = join2(src, entry.name);
|
|
314
|
+
const destEntry = join2(dest, entry.name);
|
|
315
|
+
if (entry.isDirectory()) {
|
|
316
|
+
count2 += copyDir2(srcEntry, destEntry);
|
|
317
|
+
} else if (entry.isFile()) {
|
|
318
|
+
const ext = extname(entry.name).toLowerCase();
|
|
319
|
+
if (placeholders && TEXT_EXTENSIONS.has(ext)) {
|
|
320
|
+
const content = readFileSync2(srcEntry, "utf-8");
|
|
321
|
+
writeFileSync2(destEntry, replacePlaceholders(content, placeholders));
|
|
322
|
+
} else {
|
|
323
|
+
copyFileSync2(srcEntry, destEntry);
|
|
324
|
+
}
|
|
325
|
+
count2++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return count2;
|
|
329
|
+
};
|
|
330
|
+
var copyDir = copyDir2;
|
|
331
|
+
const sourcePath = join2(sourceDir, dir);
|
|
332
|
+
const targetPath = join2(targetDir, dir);
|
|
333
|
+
if (!existsSync2(sourcePath)) continue;
|
|
334
|
+
const count = copyDir2(sourcePath, targetPath);
|
|
335
|
+
steps.push(`Copied ${count} files from project template: ${dir}`);
|
|
336
|
+
}
|
|
337
|
+
return steps;
|
|
338
|
+
}
|
|
339
|
+
async function createWorkspace(options) {
|
|
340
|
+
const { projectConfig, featureName, startDocker, dryRun } = options;
|
|
341
|
+
const result = {
|
|
342
|
+
success: true,
|
|
343
|
+
workspacePath: "",
|
|
344
|
+
errors: [],
|
|
345
|
+
steps: []
|
|
346
|
+
};
|
|
347
|
+
if (!validateFeatureName(featureName)) {
|
|
348
|
+
result.success = false;
|
|
349
|
+
result.errors.push("Invalid feature name. Use alphanumeric and hyphens only.");
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
if (featureName === "main") {
|
|
353
|
+
result.success = false;
|
|
354
|
+
result.errors.push('Cannot create workspace for "main". Use base repos directly.');
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
const workspaceConfig = projectConfig.workspace || getDefaultWorkspaceConfig();
|
|
358
|
+
const workspacesDir = join2(projectConfig.path, workspaceConfig.workspaces_dir || "workspaces");
|
|
359
|
+
const featureFolder = `feature-${featureName}`;
|
|
360
|
+
const workspacePath = join2(workspacesDir, featureFolder);
|
|
361
|
+
result.workspacePath = workspacePath;
|
|
362
|
+
if (existsSync2(workspacePath)) {
|
|
363
|
+
result.success = false;
|
|
364
|
+
result.errors.push(`Workspace already exists at ${workspacePath}`);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
if (dryRun) {
|
|
368
|
+
result.steps.push("[DRY RUN] Would create workspace at: " + workspacePath);
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
const placeholders = createPlaceholders(projectConfig, featureName, workspacePath);
|
|
372
|
+
mkdirSync2(workspacePath, { recursive: true });
|
|
373
|
+
result.steps.push("Created workspace directory");
|
|
374
|
+
if (workspaceConfig.type === "polyrepo" && workspaceConfig.repos) {
|
|
375
|
+
for (const repo of workspaceConfig.repos) {
|
|
376
|
+
const rawRepoPath = join2(projectConfig.path, repo.path);
|
|
377
|
+
const repoPath = existsSync2(rawRepoPath) ? realpathSync(rawRepoPath) : rawRepoPath;
|
|
378
|
+
const targetPath = join2(workspacePath, repo.name);
|
|
379
|
+
const branchPrefix = repo.branch_prefix || "feature/";
|
|
380
|
+
const branchName = `${branchPrefix}${featureName}`;
|
|
381
|
+
const defaultBranch = repo.default_branch || workspaceConfig.default_branch || "main";
|
|
382
|
+
const worktreeResult = await createWorktree(repoPath, targetPath, branchName, defaultBranch);
|
|
383
|
+
if (worktreeResult.success) {
|
|
384
|
+
result.steps.push(`Created worktree for ${repo.name}: ${branchName} (from ${defaultBranch})`);
|
|
385
|
+
} else {
|
|
386
|
+
result.errors.push(`${repo.name}: ${worktreeResult.message}`);
|
|
387
|
+
result.success = false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const branchName = `feature/${featureName}`;
|
|
392
|
+
const defaultBranch = workspaceConfig.default_branch || "main";
|
|
393
|
+
const worktreeResult = await createWorktree(projectConfig.path, workspacePath, branchName, defaultBranch);
|
|
394
|
+
if (worktreeResult.success) {
|
|
395
|
+
result.steps.push(`Created worktree: ${branchName} (from ${defaultBranch})`);
|
|
396
|
+
} else {
|
|
397
|
+
result.errors.push(worktreeResult.message);
|
|
398
|
+
result.success = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const devcontainerDir = join2(workspacePath, ".devcontainer");
|
|
402
|
+
if (existsSync2(devcontainerDir)) {
|
|
403
|
+
const composeFiles = readdirSync2(devcontainerDir).filter((f) => f.includes("compose") && (f.endsWith(".yml") || f.endsWith(".yaml")));
|
|
404
|
+
for (const composeFile of composeFiles) {
|
|
405
|
+
sanitizeComposeFile(join2(devcontainerDir, composeFile));
|
|
406
|
+
}
|
|
407
|
+
if (composeFiles.length > 0) {
|
|
408
|
+
result.steps.push(`Sanitized ${composeFiles.length} compose file(s) for platform compatibility`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
await execAsync("python3 --version");
|
|
413
|
+
const venvPath = join2(workspacePath, ".venv");
|
|
414
|
+
const tldrBin = join2(venvPath, "bin", "tldr");
|
|
415
|
+
const mainVenvTldr = join2(projectConfig.path, ".venv", "bin", "tldr");
|
|
416
|
+
const mainVenvExists = existsSync2(mainVenvTldr);
|
|
417
|
+
if (mainVenvExists) {
|
|
418
|
+
await execAsync(`cp -a "${join2(projectConfig.path, ".venv")}" "${venvPath}"`);
|
|
419
|
+
result.steps.push("Copied Python venv from main branch");
|
|
420
|
+
} else {
|
|
421
|
+
await execAsync(`python3 -m venv "${venvPath}"`, { cwd: workspacePath });
|
|
422
|
+
const pipPath = join2(venvPath, "bin", "pip");
|
|
423
|
+
await execAsync(`"${pipPath}" install llm-tldr`, { cwd: workspacePath, timeout: 12e4 });
|
|
424
|
+
result.steps.push("Created Python venv and installed llm-tldr");
|
|
425
|
+
const patchScript = join2(projectConfig.path, "scripts", "patches", "llm-tldr-tsx-support.py");
|
|
426
|
+
if (existsSync2(patchScript)) {
|
|
427
|
+
await execAsync(`python3 "${patchScript}" "${venvPath}"`);
|
|
428
|
+
result.steps.push("Applied llm-tldr .tsx/.jsx patch");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!existsSync2(tldrBin)) {
|
|
432
|
+
result.steps.push("TLDR setup incomplete: tldr binary not found after venv creation");
|
|
433
|
+
} else {
|
|
434
|
+
const mainTldrDir = join2(projectConfig.path, ".tldr");
|
|
435
|
+
const workspaceTldrDir = join2(workspacePath, ".tldr");
|
|
436
|
+
if (existsSync2(mainTldrDir)) {
|
|
437
|
+
await execAsync(`cp -r "${mainTldrDir}" "${workspaceTldrDir}"`);
|
|
438
|
+
result.steps.push("Copied TLDR index from main branch");
|
|
439
|
+
}
|
|
440
|
+
const { getTldrDaemonService } = await import("./tldr-daemon-T3THOUGT.js");
|
|
441
|
+
const tldrService = getTldrDaemonService(workspacePath, venvPath);
|
|
442
|
+
await tldrService.start(true);
|
|
443
|
+
result.steps.push("Started TLDR daemon");
|
|
444
|
+
try {
|
|
445
|
+
await tldrService.warm(true);
|
|
446
|
+
result.steps.push("TLDR index warm initiated (background)");
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error.message?.includes("python3")) {
|
|
452
|
+
result.steps.push("Skipped TLDR setup (python3 not available)");
|
|
453
|
+
} else {
|
|
454
|
+
console.warn(`\u26A0 TLDR setup failed: ${error.message}`);
|
|
455
|
+
result.steps.push(`TLDR setup failed: ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (workspaceConfig.dns) {
|
|
459
|
+
const dnsMethod = workspaceConfig.dns.sync_method || "wsl2hosts";
|
|
460
|
+
for (const entryPattern of workspaceConfig.dns.entries) {
|
|
461
|
+
const hostname = replacePlaceholders(entryPattern, placeholders);
|
|
462
|
+
if (addDnsEntry(dnsMethod, hostname)) {
|
|
463
|
+
result.steps.push(`Added DNS entry: ${hostname} (${dnsMethod})`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (dnsMethod === "wsl2hosts") {
|
|
467
|
+
const synced = await syncDnsToWindows();
|
|
468
|
+
if (synced) {
|
|
469
|
+
result.steps.push("Synced DNS to Windows hosts file");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (workspaceConfig.ports) {
|
|
474
|
+
for (const [portName, portConfig] of Object.entries(workspaceConfig.ports)) {
|
|
475
|
+
const portFile = join2(projectConfig.path, `.${portName}-ports`);
|
|
476
|
+
try {
|
|
477
|
+
const port = assignPort(portFile, featureFolder, portConfig.range);
|
|
478
|
+
result.steps.push(`Assigned ${portName} port: ${port}`);
|
|
479
|
+
placeholders[`${portName.toUpperCase()}_PORT`] = String(port);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
result.errors.push(`Failed to assign ${portName} port: ${error}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const mergeResult = mergeSkillsIntoWorkspace(workspacePath);
|
|
486
|
+
const mergeTotal = mergeResult.added.length + mergeResult.updated.length;
|
|
487
|
+
if (mergeTotal > 0) {
|
|
488
|
+
result.steps.push(`Installed ${mergeTotal} Panopticon files (${mergeResult.added.length} new, ${mergeResult.updated.length} updated)`);
|
|
489
|
+
}
|
|
490
|
+
if (workspaceConfig.agent?.template_dir) {
|
|
491
|
+
const templateDir = join2(projectConfig.path, workspaceConfig.agent.template_dir);
|
|
492
|
+
const templateSteps = processTemplates(
|
|
493
|
+
templateDir,
|
|
494
|
+
workspacePath,
|
|
495
|
+
placeholders,
|
|
496
|
+
workspaceConfig.agent.templates
|
|
497
|
+
);
|
|
498
|
+
result.steps.push(...templateSteps);
|
|
499
|
+
const dirsToSync = workspaceConfig.agent.copy_dirs || workspaceConfig.agent.symlinks;
|
|
500
|
+
if (dirsToSync) {
|
|
501
|
+
const copySteps = copyProjectTemplateDirs(templateDir, workspacePath, dirsToSync, placeholders);
|
|
502
|
+
result.steps.push(...copySteps);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (workspaceConfig.env?.template) {
|
|
506
|
+
const envContent = replacePlaceholders(workspaceConfig.env.template, placeholders);
|
|
507
|
+
writeFileSync2(join2(workspacePath, ".env"), envContent);
|
|
508
|
+
result.steps.push("Created .env file");
|
|
509
|
+
}
|
|
510
|
+
if (workspaceConfig.docker?.compose_template) {
|
|
511
|
+
const templateDir = join2(projectConfig.path, workspaceConfig.docker.compose_template);
|
|
512
|
+
const devcontainerDir2 = join2(workspacePath, ".devcontainer");
|
|
513
|
+
mkdirSync2(devcontainerDir2, { recursive: true });
|
|
514
|
+
const templateSteps = processTemplates(templateDir, devcontainerDir2, placeholders);
|
|
515
|
+
result.steps.push(...templateSteps);
|
|
516
|
+
if (existsSync2(templateDir)) {
|
|
517
|
+
const files = readdirSync2(templateDir);
|
|
518
|
+
for (const file of files) {
|
|
519
|
+
if (!file.endsWith(".template")) {
|
|
520
|
+
const sourcePath = join2(templateDir, file);
|
|
521
|
+
const targetPath = join2(devcontainerDir2, file);
|
|
522
|
+
copyFileSync2(sourcePath, targetPath);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const composeFiles = readdirSync2(devcontainerDir2).filter((f) => f.includes("compose") && (f.endsWith(".yml") || f.endsWith(".yaml")));
|
|
527
|
+
for (const composeFile of composeFiles) {
|
|
528
|
+
sanitizeComposeFile(join2(devcontainerDir2, composeFile));
|
|
529
|
+
}
|
|
530
|
+
if (composeFiles.length > 0) {
|
|
531
|
+
result.steps.push(`Sanitized ${composeFiles.length} compose file(s) for platform compatibility`);
|
|
532
|
+
}
|
|
533
|
+
const devScriptInContainer = join2(devcontainerDir2, "dev");
|
|
534
|
+
const devScriptAtRoot = join2(workspacePath, "dev");
|
|
535
|
+
if (existsSync2(devScriptInContainer) && !existsSync2(devScriptAtRoot)) {
|
|
536
|
+
try {
|
|
537
|
+
symlinkSync(".devcontainer/dev", devScriptAtRoot);
|
|
538
|
+
chmodSync(devScriptInContainer, 493);
|
|
539
|
+
result.steps.push("Created ./dev symlink");
|
|
540
|
+
} catch (error) {
|
|
541
|
+
result.errors.push(`Failed to create ./dev symlink: ${error}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (workspaceConfig.tunnel) {
|
|
546
|
+
const tunnelResult = await addTunnelIngress(workspaceConfig.tunnel, placeholders);
|
|
547
|
+
result.steps.push(...tunnelResult.steps);
|
|
548
|
+
if (!tunnelResult.success) {
|
|
549
|
+
result.errors.push("Tunnel setup had failures (see steps for details)");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (workspaceConfig.hume) {
|
|
553
|
+
const humeResult = await createHumeConfig(workspaceConfig.hume, placeholders);
|
|
554
|
+
result.steps.push(...humeResult.steps);
|
|
555
|
+
if (humeResult.configId) {
|
|
556
|
+
writeFileSync2(
|
|
557
|
+
join2(workspacePath, ".hume-config"),
|
|
558
|
+
`HUME_CONFIG_ID=${humeResult.configId}
|
|
559
|
+
VITE_HUME_CONFIG_ID=${humeResult.configId}
|
|
560
|
+
`
|
|
561
|
+
);
|
|
562
|
+
result.steps.push("Wrote .hume-config with Hume EVI config ID");
|
|
563
|
+
}
|
|
564
|
+
if (!humeResult.success) {
|
|
565
|
+
result.errors.push("Hume EVI config setup had failures (see steps for details)");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (startDocker) {
|
|
569
|
+
if (workspaceConfig.docker?.traefik) {
|
|
570
|
+
const traefikPath = join2(projectConfig.path, workspaceConfig.docker.traefik);
|
|
571
|
+
if (existsSync2(traefikPath)) {
|
|
572
|
+
try {
|
|
573
|
+
await execAsync(`docker compose -f "${traefikPath}" up -d`, { cwd: projectConfig.path });
|
|
574
|
+
result.steps.push("Started Traefik");
|
|
575
|
+
} catch (error) {
|
|
576
|
+
const msg = error?.message || String(error);
|
|
577
|
+
if (msg.includes("port is already allocated") || msg.includes("address already in use")) {
|
|
578
|
+
result.steps.push("Traefik already running (port in use)");
|
|
579
|
+
} else {
|
|
580
|
+
result.errors.push(`Failed to start Traefik: ${error}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const composeLocations = [
|
|
586
|
+
join2(workspacePath, "docker-compose.yml"),
|
|
587
|
+
join2(workspacePath, "docker-compose.yaml"),
|
|
588
|
+
join2(workspacePath, ".devcontainer", "docker-compose.yml"),
|
|
589
|
+
join2(workspacePath, ".devcontainer", "docker-compose.devcontainer.yml")
|
|
590
|
+
];
|
|
591
|
+
for (const composePath of composeLocations) {
|
|
592
|
+
if (existsSync2(composePath)) {
|
|
593
|
+
try {
|
|
594
|
+
await execAsync(`docker compose -f "${composePath}" up -d --build`, { cwd: dirname2(composePath), timeout: 3e5 });
|
|
595
|
+
result.steps.push(`Started containers from ${basename(composePath)}`);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
result.errors.push(`Failed to start containers: ${error}`);
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
preTrustDirectory(workspacePath);
|
|
605
|
+
result.steps.push("Pre-trusted workspace in Claude Code");
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
result.success = result.errors.length === 0;
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
function preTrustDirectory(dirPath) {
|
|
612
|
+
const claudeJsonPath = join2(homedir(), ".claude.json");
|
|
613
|
+
if (!existsSync2(claudeJsonPath)) return;
|
|
614
|
+
const data = JSON.parse(readFileSync2(claudeJsonPath, "utf8"));
|
|
615
|
+
if (!data.projects) data.projects = {};
|
|
616
|
+
if (data.projects[dirPath]) {
|
|
617
|
+
if (!data.projects[dirPath].hasTrustDialogAccepted) {
|
|
618
|
+
data.projects[dirPath].hasTrustDialogAccepted = true;
|
|
619
|
+
writeFileSync2(claudeJsonPath, JSON.stringify(data, null, 2), "utf8");
|
|
620
|
+
}
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
data.projects[dirPath] = {
|
|
624
|
+
allowedTools: [],
|
|
625
|
+
mcpContextUris: [],
|
|
626
|
+
mcpServers: {},
|
|
627
|
+
enabledMcpjsonServers: [],
|
|
628
|
+
disabledMcpjsonServers: [],
|
|
629
|
+
hasTrustDialogAccepted: true,
|
|
630
|
+
projectOnboardingSeenCount: 0,
|
|
631
|
+
hasClaudeMdExternalIncludesApproved: false,
|
|
632
|
+
hasClaudeMdExternalIncludesWarningShown: false
|
|
633
|
+
};
|
|
634
|
+
writeFileSync2(claudeJsonPath, JSON.stringify(data, null, 2), "utf8");
|
|
635
|
+
}
|
|
636
|
+
async function stopWorkspaceDocker(workspacePath, projectName, featureName) {
|
|
637
|
+
const result = {
|
|
638
|
+
containersFound: false,
|
|
639
|
+
steps: []
|
|
640
|
+
};
|
|
641
|
+
const devcontainerDir = join2(workspacePath, ".devcontainer");
|
|
642
|
+
const composeFiles = [];
|
|
643
|
+
if (existsSync2(devcontainerDir)) {
|
|
644
|
+
const possibleFiles = [
|
|
645
|
+
"docker-compose.devcontainer.yml",
|
|
646
|
+
"docker-compose.yml",
|
|
647
|
+
"compose.yml",
|
|
648
|
+
"compose.infra.yml",
|
|
649
|
+
"compose.override.yml"
|
|
650
|
+
];
|
|
651
|
+
for (const file of possibleFiles) {
|
|
652
|
+
const fullPath = join2(devcontainerDir, file);
|
|
653
|
+
if (existsSync2(fullPath)) {
|
|
654
|
+
composeFiles.push(fullPath);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (composeFiles.length === 0) {
|
|
659
|
+
const rootCompose = join2(workspacePath, "docker-compose.yml");
|
|
660
|
+
if (existsSync2(rootCompose)) {
|
|
661
|
+
composeFiles.push(rootCompose);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (composeFiles.length > 0) {
|
|
665
|
+
result.containersFound = true;
|
|
666
|
+
try {
|
|
667
|
+
const fileFlags = composeFiles.map((f) => `-f "${f}"`).join(" ");
|
|
668
|
+
const cwd = existsSync2(devcontainerDir) ? devcontainerDir : workspacePath;
|
|
669
|
+
await execAsync(`docker compose ${fileFlags} down -v --remove-orphans`, {
|
|
670
|
+
cwd,
|
|
671
|
+
timeout: 6e4
|
|
672
|
+
});
|
|
673
|
+
result.steps.push(`Stopped Docker containers (${composeFiles.length} compose files)`);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
result.steps.push(`Docker cleanup attempted (${error.message?.split("\n")[0] || "containers may not be running"})`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
await execAsync(
|
|
680
|
+
`docker run --rm -v "${workspacePath}:/workspace" alpine sh -c "find /workspace -user root -delete 2>&1 | tail -100 || true"`,
|
|
681
|
+
{ timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
|
|
682
|
+
);
|
|
683
|
+
result.steps.push("Cleaned up Docker-created files");
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
async function removeWorkspace(options) {
|
|
689
|
+
const { projectConfig, featureName, dryRun } = options;
|
|
690
|
+
const result = {
|
|
691
|
+
success: true,
|
|
692
|
+
errors: [],
|
|
693
|
+
steps: []
|
|
694
|
+
};
|
|
695
|
+
const workspaceConfig = projectConfig.workspace || getDefaultWorkspaceConfig();
|
|
696
|
+
const workspacesDir = join2(projectConfig.path, workspaceConfig.workspaces_dir || "workspaces");
|
|
697
|
+
const featureFolder = `feature-${featureName}`;
|
|
698
|
+
const workspacePath = join2(workspacesDir, featureFolder);
|
|
699
|
+
if (!existsSync2(workspacePath)) {
|
|
700
|
+
result.success = false;
|
|
701
|
+
result.errors.push(`Workspace not found at ${workspacePath}`);
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
if (dryRun) {
|
|
705
|
+
result.steps.push("[DRY RUN] Would remove workspace at: " + workspacePath);
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
const venvPath = join2(workspacePath, ".venv");
|
|
709
|
+
if (existsSync2(venvPath)) {
|
|
710
|
+
try {
|
|
711
|
+
const { getTldrDaemonService } = await import("./tldr-daemon-T3THOUGT.js");
|
|
712
|
+
const tldrService = getTldrDaemonService(workspacePath, venvPath);
|
|
713
|
+
await tldrService.stop();
|
|
714
|
+
result.steps.push("Stopped TLDR daemon");
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.warn(`\u26A0 Failed to stop TLDR daemon: ${error?.message}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const dockerResult = await stopWorkspaceDocker(workspacePath, projectConfig.name || "workspace", featureName);
|
|
720
|
+
result.steps.push(...dockerResult.steps);
|
|
721
|
+
if (workspaceConfig.type === "polyrepo" && workspaceConfig.repos) {
|
|
722
|
+
for (const repo of workspaceConfig.repos) {
|
|
723
|
+
const repoPath = join2(projectConfig.path, repo.path);
|
|
724
|
+
const targetPath = join2(workspacePath, repo.name);
|
|
725
|
+
const branchPrefix = repo.branch_prefix || "feature/";
|
|
726
|
+
const branchName = `${branchPrefix}${featureName}`;
|
|
727
|
+
const worktreeResult = await removeWorktree(repoPath, targetPath, branchName);
|
|
728
|
+
if (worktreeResult.success) {
|
|
729
|
+
result.steps.push(`Removed worktree for ${repo.name}`);
|
|
730
|
+
} else {
|
|
731
|
+
result.errors.push(worktreeResult.message);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
const branchName = `feature/${featureName}`;
|
|
736
|
+
const worktreeResult = await removeWorktree(projectConfig.path, workspacePath, branchName);
|
|
737
|
+
if (worktreeResult.success) {
|
|
738
|
+
result.steps.push("Removed worktree");
|
|
739
|
+
} else {
|
|
740
|
+
result.errors.push(worktreeResult.message);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (workspaceConfig.dns) {
|
|
744
|
+
const placeholders = createPlaceholders(projectConfig, featureName, workspacePath);
|
|
745
|
+
const dnsMethod = workspaceConfig.dns.sync_method || "wsl2hosts";
|
|
746
|
+
for (const entryPattern of workspaceConfig.dns.entries) {
|
|
747
|
+
const hostname = replacePlaceholders(entryPattern, placeholders);
|
|
748
|
+
if (removeDnsEntry(dnsMethod, hostname)) {
|
|
749
|
+
result.steps.push(`Removed DNS entry: ${hostname}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (workspaceConfig.tunnel) {
|
|
754
|
+
const placeholders = createPlaceholders(projectConfig, featureName, workspacePath);
|
|
755
|
+
const tunnelResult = await removeTunnelIngress(workspaceConfig.tunnel, placeholders);
|
|
756
|
+
result.steps.push(...tunnelResult.steps);
|
|
757
|
+
}
|
|
758
|
+
if (workspaceConfig.hume) {
|
|
759
|
+
const placeholders = createPlaceholders(projectConfig, featureName, workspacePath);
|
|
760
|
+
const humeResult = await deleteHumeConfig(workspaceConfig.hume, placeholders);
|
|
761
|
+
result.steps.push(...humeResult.steps);
|
|
762
|
+
}
|
|
763
|
+
if (workspaceConfig.ports) {
|
|
764
|
+
for (const [portName] of Object.entries(workspaceConfig.ports)) {
|
|
765
|
+
const portFile = join2(projectConfig.path, `.${portName}-ports`);
|
|
766
|
+
if (releasePort(portFile, featureFolder)) {
|
|
767
|
+
result.steps.push(`Released ${portName} port`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
await execAsync(`rm -rf "${workspacePath}"`, { maxBuffer: 10 * 1024 * 1024 });
|
|
773
|
+
result.steps.push("Removed workspace directory");
|
|
774
|
+
} catch (error) {
|
|
775
|
+
result.errors.push(`Failed to remove workspace directory: ${error}`);
|
|
776
|
+
}
|
|
777
|
+
result.success = result.errors.length === 0;
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
var execAsync, TEXT_EXTENSIONS;
|
|
781
|
+
var init_workspace_manager = __esm({
|
|
782
|
+
"src/lib/workspace-manager.ts"() {
|
|
783
|
+
init_esm_shims();
|
|
784
|
+
init_workspace_config();
|
|
785
|
+
init_dns();
|
|
786
|
+
init_tunnel();
|
|
787
|
+
init_hume();
|
|
788
|
+
init_skills_merge();
|
|
789
|
+
execAsync = promisify(exec);
|
|
790
|
+
TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
791
|
+
".md",
|
|
792
|
+
".sh",
|
|
793
|
+
".yml",
|
|
794
|
+
".yaml",
|
|
795
|
+
".json",
|
|
796
|
+
".ts",
|
|
797
|
+
".js",
|
|
798
|
+
".env",
|
|
799
|
+
".txt",
|
|
800
|
+
".toml",
|
|
801
|
+
".template"
|
|
802
|
+
]);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
export {
|
|
807
|
+
mergeSkillsIntoWorkspace,
|
|
808
|
+
applyProjectTemplateOverlay,
|
|
809
|
+
init_skills_merge,
|
|
810
|
+
createWorkspace,
|
|
811
|
+
preTrustDirectory,
|
|
812
|
+
stopWorkspaceDocker,
|
|
813
|
+
removeWorkspace,
|
|
814
|
+
init_workspace_manager
|
|
815
|
+
};
|
|
816
|
+
//# sourceMappingURL=chunk-GR6ZZMCX.js.map
|