sanjang 0.3.4 → 0.3.6
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 +25 -13
- package/dashboard/app.js +941 -38
- package/dashboard/index.html +151 -38
- package/dashboard/style.css +374 -7
- package/dist/bin/sanjang.js +152 -7
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +29 -5
- package/dist/lib/engine/change-report.d.ts +27 -0
- package/dist/lib/engine/change-report.js +233 -0
- package/dist/lib/engine/conflict.d.ts +13 -0
- package/dist/lib/engine/conflict.js +41 -0
- package/dist/lib/engine/diagnostics.js +2 -6
- package/dist/lib/engine/main-server.d.ts +19 -0
- package/dist/lib/engine/main-server.js +181 -0
- package/dist/lib/engine/naming.js +11 -2
- package/dist/lib/engine/ports.d.ts +2 -2
- package/dist/lib/engine/ports.js +33 -23
- package/dist/lib/engine/pr.js +1 -1
- package/dist/lib/engine/process-utils.d.ts +11 -0
- package/dist/lib/engine/process-utils.js +65 -0
- package/dist/lib/engine/process.d.ts +2 -0
- package/dist/lib/engine/process.js +27 -39
- package/dist/lib/engine/self-heal.js +16 -5
- package/dist/lib/engine/smart-init.js +7 -6
- package/dist/lib/engine/state.js +14 -5
- package/dist/lib/engine/suggest.js +1 -4
- package/dist/lib/engine/warp.d.ts +1 -1
- package/dist/lib/engine/warp.js +1 -1
- package/dist/lib/engine/worktree.d.ts +2 -0
- package/dist/lib/engine/worktree.js +30 -8
- package/dist/lib/server.d.ts +1 -0
- package/dist/lib/server.js +701 -94
- package/dist/lib/types.d.ts +25 -0
- package/package.json +2 -2
package/dist/lib/engine/ports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
2
|
const portConfig = {
|
|
3
3
|
fe: { base: 3000, slots: 8 },
|
|
4
4
|
be: { base: 8000, slots: 8 },
|
|
@@ -15,38 +15,48 @@ export function portsForSlot(slot) {
|
|
|
15
15
|
bePort: portConfig.be.base + slot,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
function probePort(port) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const srv = createServer();
|
|
21
|
+
srv.once("error", () => resolve(true));
|
|
22
|
+
srv.once("listening", () => {
|
|
23
|
+
srv.close(() => resolve(false));
|
|
24
|
+
});
|
|
25
|
+
srv.listen(port, "127.0.0.1");
|
|
26
|
+
});
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// --- 10-second cache ---
|
|
29
|
+
let _cache = null;
|
|
30
|
+
let _cacheTime = 0;
|
|
31
|
+
const CACHE_TTL = 10_000;
|
|
32
|
+
export async function scanPorts() {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
if (_cache && now - _cacheTime < CACHE_TTL)
|
|
35
|
+
return _cache;
|
|
29
36
|
const maxSlots = Math.max(portConfig.fe.slots, portConfig.be.slots);
|
|
30
|
-
|
|
37
|
+
const slots = Array.from({ length: maxSlots }, (_, i) => i);
|
|
38
|
+
const results = await Promise.all(slots.map(async (slot) => {
|
|
31
39
|
const { fePort, bePort } = portsForSlot(slot);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return status;
|
|
40
|
+
const [feBusy, beBusy] = await Promise.all([probePort(fePort), probePort(bePort)]);
|
|
41
|
+
return { slot, fePort, feBusy, bePort, beBusy };
|
|
42
|
+
}));
|
|
43
|
+
_cache = results;
|
|
44
|
+
_cacheTime = Date.now();
|
|
45
|
+
return results;
|
|
41
46
|
}
|
|
42
|
-
export function allocate(existingCamps) {
|
|
47
|
+
export async function allocate(existingCamps) {
|
|
43
48
|
const usedSlots = new Set(existingCamps.map((p) => p.slot));
|
|
49
|
+
const usedFePorts = new Set(existingCamps.map((p) => p.fePort));
|
|
50
|
+
const usedBePorts = new Set(existingCamps.map((p) => p.bePort));
|
|
44
51
|
const maxSlots = portConfig.fe.slots;
|
|
45
52
|
for (let slot = 1; slot < maxSlots; slot++) {
|
|
46
53
|
if (usedSlots.has(slot))
|
|
47
54
|
continue;
|
|
48
55
|
const { fePort, bePort } = portsForSlot(slot);
|
|
49
|
-
if (
|
|
56
|
+
if (usedFePorts.has(fePort) || usedBePorts.has(bePort))
|
|
57
|
+
continue;
|
|
58
|
+
const [feBusy, beBusy] = await Promise.all([probePort(fePort), probePort(bePort)]);
|
|
59
|
+
if (!feBusy && !beBusy) {
|
|
50
60
|
return { slot, fePort, bePort };
|
|
51
61
|
}
|
|
52
62
|
}
|
package/dist/lib/engine/pr.js
CHANGED
|
@@ -27,7 +27,7 @@ export function buildFallbackPrBody({ message, actions, diffStat }) {
|
|
|
27
27
|
export function buildClaudePrPrompt({ message, diffStat, diff }) {
|
|
28
28
|
return [
|
|
29
29
|
"You are writing a GitHub Pull Request description.",
|
|
30
|
-
|
|
30
|
+
`The author described the change as: "${message}"`,
|
|
31
31
|
"",
|
|
32
32
|
"Here is the diff stat:",
|
|
33
33
|
diffStat || "(no stat)",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
export declare function stripAnsi(s: string): string;
|
|
3
|
+
/** Build the full dev command, handling npm/yarn/pnpm's `--` separator requirement */
|
|
4
|
+
export declare function buildDevCommand(command: string, portFlag: string | null | undefined, port: number): string;
|
|
5
|
+
/** Kill a process and its entire process group (for shell: true + detached: true spawns) */
|
|
6
|
+
export declare function killProcessGroup(proc: ChildProcess): void;
|
|
7
|
+
/**
|
|
8
|
+
* Detect actual port from dev server stdout logs by polling for a localhost URL.
|
|
9
|
+
* Strips ANSI codes before matching. Verifies port is reachable via TCP probe.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectPortFromLogs(logs: string[], timeoutMs: number): Promise<number | null>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
3
|
+
export function stripAnsi(s) {
|
|
4
|
+
return s.replace(ANSI_RE, "");
|
|
5
|
+
}
|
|
6
|
+
/** Build the full dev command, handling npm/yarn/pnpm's `--` separator requirement */
|
|
7
|
+
export function buildDevCommand(command, portFlag, port) {
|
|
8
|
+
if (!portFlag)
|
|
9
|
+
return command;
|
|
10
|
+
const needsSeparator = /\b(npm|yarn|pnpm)\s+run\b/.test(command);
|
|
11
|
+
return `${command}${needsSeparator ? " --" : ""} ${portFlag} ${port}`;
|
|
12
|
+
}
|
|
13
|
+
/** Kill a process and its entire process group (for shell: true + detached: true spawns) */
|
|
14
|
+
export function killProcessGroup(proc) {
|
|
15
|
+
const pid = proc.pid;
|
|
16
|
+
if (pid) {
|
|
17
|
+
try {
|
|
18
|
+
process.kill(-pid, "SIGTERM");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
proc.kill("SIGTERM");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
proc.kill("SIGTERM");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Detect actual port from dev server stdout logs by polling for a localhost URL.
|
|
30
|
+
* Strips ANSI codes before matching. Verifies port is reachable via TCP probe.
|
|
31
|
+
*/
|
|
32
|
+
export function detectPortFromLogs(logs, timeoutMs) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
const portRe = /https?:\/\/localhost:(\d+)/;
|
|
36
|
+
function check() {
|
|
37
|
+
for (const line of logs) {
|
|
38
|
+
const match = portRe.exec(stripAnsi(line));
|
|
39
|
+
if (match?.[1]) {
|
|
40
|
+
const port = parseInt(match[1], 10);
|
|
41
|
+
const sock = createConnection({ port, host: "localhost" });
|
|
42
|
+
sock.once("connect", () => {
|
|
43
|
+
sock.destroy();
|
|
44
|
+
resolve(port);
|
|
45
|
+
});
|
|
46
|
+
sock.once("error", () => {
|
|
47
|
+
sock.destroy();
|
|
48
|
+
if (Date.now() < deadline)
|
|
49
|
+
setTimeout(check, 1000);
|
|
50
|
+
else
|
|
51
|
+
resolve(port);
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (Date.now() >= deadline) {
|
|
57
|
+
resolve(null);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
setTimeout(check, 1000);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
check();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -7,6 +7,8 @@ export declare function setConfig(config: SanjangConfig): void;
|
|
|
7
7
|
interface StartCampParams {
|
|
8
8
|
name: string;
|
|
9
9
|
fePort: number;
|
|
10
|
+
/** Ports reserved by other camps — detected port must not collide with these */
|
|
11
|
+
reservedPorts?: Set<number>;
|
|
10
12
|
}
|
|
11
13
|
export declare function startCamp(pg: StartCampParams, onEvent: EventCallback): Promise<number>;
|
|
12
14
|
export declare function stopCamp(name: string): void;
|
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createConnection } from "node:net";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { campPath, getProjectRoot } from "./worktree.js";
|
|
5
|
+
import { buildDevCommand, detectPortFromLogs, killProcessGroup } from "./process-utils.js";
|
|
5
6
|
const procs = new Map();
|
|
6
7
|
let projectConfig = null;
|
|
7
8
|
let sharedBeProc = null;
|
|
@@ -11,41 +12,8 @@ export function setConfig(config) {
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Helpers
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
return new Promise((resolve) => {
|
|
17
|
-
const deadline = Date.now() + timeoutMs;
|
|
18
|
-
// Patterns: Vite "Local: http://localhost:PORT/", Next "- Local: http://localhost:PORT"
|
|
19
|
-
const portRe = /https?:\/\/localhost:(\d+)/;
|
|
20
|
-
function check() {
|
|
21
|
-
for (const line of logs) {
|
|
22
|
-
const match = portRe.exec(line);
|
|
23
|
-
if (match?.[1]) {
|
|
24
|
-
const port = parseInt(match[1], 10);
|
|
25
|
-
// Wait briefly for the port to actually be ready
|
|
26
|
-
const sock = createConnection({ port, host: "localhost" });
|
|
27
|
-
sock.once("connect", () => { sock.destroy(); resolve(port); });
|
|
28
|
-
sock.once("error", () => {
|
|
29
|
-
sock.destroy();
|
|
30
|
-
// Port printed but not ready yet, retry
|
|
31
|
-
if (Date.now() < deadline)
|
|
32
|
-
setTimeout(check, 1000);
|
|
33
|
-
else
|
|
34
|
-
resolve(port); // return the port anyway
|
|
35
|
-
});
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (Date.now() >= deadline) {
|
|
40
|
-
resolve(null);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
setTimeout(check, 1000);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
check();
|
|
47
|
-
});
|
|
48
|
-
}
|
|
15
|
+
// Re-export detectPortFromLogs as the local name used throughout this file
|
|
16
|
+
const detectPortFromStdout = detectPortFromLogs;
|
|
49
17
|
function waitForPort(port, timeoutMs) {
|
|
50
18
|
return new Promise((resolve, reject) => {
|
|
51
19
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -169,8 +137,7 @@ export async function startCamp(pg, onEvent) {
|
|
|
169
137
|
// Step 2: Frontend
|
|
170
138
|
onEvent({ type: "log", source: "sanjang", data: "Frontend 준비 중..." });
|
|
171
139
|
onEvent({ type: "status", data: "starting-frontend" });
|
|
172
|
-
|
|
173
|
-
const fullCommand = dev.portFlag ? `${dev.command} ${dev.portFlag} ${fePort}` : dev.command;
|
|
140
|
+
const fullCommand = buildDevCommand(dev.command, dev.portFlag, fePort);
|
|
174
141
|
const cwd = dev.cwd ? join(wtPath, dev.cwd) : wtPath;
|
|
175
142
|
const feProc = spawn(fullCommand, [], {
|
|
176
143
|
cwd,
|
|
@@ -185,6 +152,7 @@ export async function startCamp(pg, onEvent) {
|
|
|
185
152
|
},
|
|
186
153
|
stdio: ["ignore", "pipe", "pipe"],
|
|
187
154
|
shell: true,
|
|
155
|
+
detached: true,
|
|
188
156
|
});
|
|
189
157
|
entry.feProc = feProc;
|
|
190
158
|
attachLogs(feProc, entry.feLogs, "frontend", onEvent);
|
|
@@ -198,6 +166,23 @@ export async function startCamp(pg, onEvent) {
|
|
|
198
166
|
if (!detectedPort) {
|
|
199
167
|
throw new Error("dev 서버가 시작되지 않았습니다. 로그를 확인하세요.");
|
|
200
168
|
}
|
|
169
|
+
// Validate: detected port must not collide with another camp's port
|
|
170
|
+
if (pg.reservedPorts?.has(detectedPort)) {
|
|
171
|
+
onEvent({
|
|
172
|
+
type: "log",
|
|
173
|
+
source: "sanjang",
|
|
174
|
+
data: `⚠️ 포트 충돌 감지: dev 서버가 ${fePort} 대신 ${detectedPort}을 사용했고, 다른 캠프와 겹칩니다. 프로세스를 종료합니다.`,
|
|
175
|
+
});
|
|
176
|
+
stopCamp(name);
|
|
177
|
+
throw new Error(`포트 충돌: dev 서버가 :${detectedPort}에 바인딩했지만, 다른 캠프가 이미 사용 중입니다. 해당 캠프를 정리하거나 포트를 확인하세요.`);
|
|
178
|
+
}
|
|
179
|
+
if (detectedPort !== fePort) {
|
|
180
|
+
onEvent({
|
|
181
|
+
type: "log",
|
|
182
|
+
source: "sanjang",
|
|
183
|
+
data: `ℹ️ dev 서버가 요청한 포트(${fePort}) 대신 ${detectedPort}을 사용합니다.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
201
186
|
const url = `http://localhost:${detectedPort}`;
|
|
202
187
|
onEvent({ type: "log", source: "sanjang", data: `Frontend 준비 완료 ✓` });
|
|
203
188
|
onEvent({ type: "url-detected", data: { url, port: detectedPort } });
|
|
@@ -209,12 +194,15 @@ export function stopCamp(name) {
|
|
|
209
194
|
if (!entry)
|
|
210
195
|
return;
|
|
211
196
|
if (entry.feProc && !entry.feProc.killed) {
|
|
212
|
-
entry.feProc
|
|
197
|
+
killProcessGroup(entry.feProc);
|
|
213
198
|
// SIGKILL fallback if still alive after 5s
|
|
214
199
|
const proc = entry.feProc;
|
|
200
|
+
const pid = proc.pid;
|
|
215
201
|
setTimeout(() => {
|
|
216
202
|
try {
|
|
217
|
-
if (!proc.killed)
|
|
203
|
+
if (!proc.killed && pid)
|
|
204
|
+
process.kill(-pid, "SIGKILL");
|
|
205
|
+
else if (!proc.killed)
|
|
218
206
|
proc.kill("SIGKILL");
|
|
219
207
|
}
|
|
220
208
|
catch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, copyFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
3
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
const PATTERNS = [
|
|
5
5
|
{
|
|
6
6
|
test: /Cannot find module|MODULE_NOT_FOUND/i,
|
|
@@ -64,7 +64,12 @@ export function executeHeal(action, campPath, projectRoot, setupCommand, copyFil
|
|
|
64
64
|
if (!setupCommand)
|
|
65
65
|
return { action, success: false, detail: "설치 명령이 없습니다." };
|
|
66
66
|
try {
|
|
67
|
-
execSync(setupCommand, {
|
|
67
|
+
execSync(setupCommand, {
|
|
68
|
+
cwd: campPath,
|
|
69
|
+
stdio: "pipe",
|
|
70
|
+
timeout: 120_000,
|
|
71
|
+
shell: true,
|
|
72
|
+
});
|
|
68
73
|
return { action, success: true };
|
|
69
74
|
}
|
|
70
75
|
catch {
|
|
@@ -82,10 +87,16 @@ export function executeHeal(action, campPath, projectRoot, setupCommand, copyFil
|
|
|
82
87
|
copyFileSync(src, dst);
|
|
83
88
|
copied++;
|
|
84
89
|
}
|
|
85
|
-
catch {
|
|
90
|
+
catch {
|
|
91
|
+
/* skip */
|
|
92
|
+
}
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
action,
|
|
97
|
+
success: copied > 0,
|
|
98
|
+
detail: copied > 0 ? `${copied}개 파일 복사됨` : "복사할 파일이 없습니다.",
|
|
99
|
+
};
|
|
89
100
|
}
|
|
90
101
|
case "restart":
|
|
91
102
|
// Restart is handled by the caller (stop + start)
|
|
@@ -32,7 +32,7 @@ export function deepFindEnvFiles(projectRoot, maxDepth = 4) {
|
|
|
32
32
|
export function detectSetupIssues(projectRoot) {
|
|
33
33
|
const issues = [];
|
|
34
34
|
// 1. Check for env variable references without .env files
|
|
35
|
-
const hasEnvFile = ENV_PATTERNS.some(f => existsSync(join(projectRoot, f)));
|
|
35
|
+
const hasEnvFile = ENV_PATTERNS.some((f) => existsSync(join(projectRoot, f)));
|
|
36
36
|
if (!hasEnvFile) {
|
|
37
37
|
const envRefFound = scanForEnvReferences(projectRoot);
|
|
38
38
|
if (envRefFound) {
|
|
@@ -52,7 +52,7 @@ export function detectSetupIssues(projectRoot) {
|
|
|
52
52
|
}
|
|
53
53
|
// 3. Check for missing lockfile
|
|
54
54
|
const lockfiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock"];
|
|
55
|
-
if (!lockfiles.some(f => existsSync(join(projectRoot, f)))) {
|
|
55
|
+
if (!lockfiles.some((f) => existsSync(join(projectRoot, f)))) {
|
|
56
56
|
issues.push({
|
|
57
57
|
type: "missing-lockfile",
|
|
58
58
|
message: "lockfile이 없습니다. 의존성 설치가 느릴 수 있습니다.",
|
|
@@ -97,7 +97,10 @@ function scanForEnvReferences(dir, depth = 0) {
|
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
if (entry.isDirectory() &&
|
|
100
|
+
if (entry.isDirectory() &&
|
|
101
|
+
!SKIP_DIRS.has(entry.name) &&
|
|
102
|
+
!entry.name.startsWith(".") &&
|
|
103
|
+
entry.name !== "node_modules") {
|
|
101
104
|
if (scanForEnvReferences(join(dir, entry.name), depth + 1))
|
|
102
105
|
return true;
|
|
103
106
|
}
|
|
@@ -129,9 +132,7 @@ function countTurboApps(root) {
|
|
|
129
132
|
if (scripts?.dev)
|
|
130
133
|
count++;
|
|
131
134
|
}
|
|
132
|
-
catch {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
+
catch { }
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
return count;
|
package/dist/lib/engine/state.js
CHANGED
|
@@ -15,23 +15,32 @@ function stateFile() {
|
|
|
15
15
|
function ensureDir() {
|
|
16
16
|
mkdirSync(getCampsDir(), { recursive: true });
|
|
17
17
|
}
|
|
18
|
+
// --- In-memory cache ---
|
|
19
|
+
let _stateCache = null;
|
|
18
20
|
function read() {
|
|
21
|
+
if (_stateCache !== null)
|
|
22
|
+
return _stateCache;
|
|
19
23
|
const f = stateFile();
|
|
20
|
-
if (!existsSync(f))
|
|
21
|
-
|
|
24
|
+
if (!existsSync(f)) {
|
|
25
|
+
_stateCache = [];
|
|
26
|
+
return _stateCache;
|
|
27
|
+
}
|
|
22
28
|
try {
|
|
23
|
-
|
|
29
|
+
_stateCache = JSON.parse(readFileSync(f, "utf8"));
|
|
30
|
+
return _stateCache;
|
|
24
31
|
}
|
|
25
32
|
catch {
|
|
26
|
-
|
|
33
|
+
_stateCache = [];
|
|
34
|
+
return _stateCache;
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
37
|
function write(records) {
|
|
30
38
|
ensureDir();
|
|
31
39
|
// Atomic write: write to temp file then rename to prevent corruption
|
|
32
|
-
const tmp = stateFile()
|
|
40
|
+
const tmp = `${stateFile()}.tmp`;
|
|
33
41
|
writeFileSync(tmp, JSON.stringify(records, null, 2), "utf8");
|
|
34
42
|
renameSync(tmp, stateFile());
|
|
43
|
+
_stateCache = records;
|
|
35
44
|
}
|
|
36
45
|
export function getAll() {
|
|
37
46
|
return read();
|
|
@@ -97,10 +97,7 @@ async function fetchRecentCommits(cwd) {
|
|
|
97
97
|
export async function suggestTasks(projectRoot) {
|
|
98
98
|
const results = [];
|
|
99
99
|
// gh-dependent fetches — tolerate failure (gh not installed / no repo)
|
|
100
|
-
const [issues, prs] = await Promise.allSettled([
|
|
101
|
-
fetchIssues(projectRoot),
|
|
102
|
-
fetchMyPrs(projectRoot),
|
|
103
|
-
]);
|
|
100
|
+
const [issues, prs] = await Promise.allSettled([fetchIssues(projectRoot), fetchMyPrs(projectRoot)]);
|
|
104
101
|
// PRs first — most actionable ("이어하기")
|
|
105
102
|
if (prs.status === "fulfilled") {
|
|
106
103
|
results.push(...prs.value);
|
|
@@ -15,7 +15,7 @@ export declare function detectWarp(): WarpDetectResult;
|
|
|
15
15
|
* Opens as a new tab in the existing Warp window (not a new window).
|
|
16
16
|
* The tab title naturally shows the directory name (= camp name).
|
|
17
17
|
*/
|
|
18
|
-
export declare function openWarpTab(
|
|
18
|
+
export declare function openWarpTab(_campName: string, worktreePath: string): WarpOpenResult;
|
|
19
19
|
/**
|
|
20
20
|
* No-op cleanup (launch config removed — using open -a instead).
|
|
21
21
|
*/
|
package/dist/lib/engine/warp.js
CHANGED
|
@@ -12,7 +12,7 @@ export function detectWarp() {
|
|
|
12
12
|
* Opens as a new tab in the existing Warp window (not a new window).
|
|
13
13
|
* The tab title naturally shows the directory name (= camp name).
|
|
14
14
|
*/
|
|
15
|
-
export function openWarpTab(
|
|
15
|
+
export function openWarpTab(_campName, worktreePath) {
|
|
16
16
|
const { installed } = detectWarp();
|
|
17
17
|
if (!installed) {
|
|
18
18
|
return { opened: false, terminal: null, path: worktreePath };
|
|
@@ -9,5 +9,7 @@ export declare function setProjectRoot(root: string): void;
|
|
|
9
9
|
export declare function getProjectRoot(): string;
|
|
10
10
|
export declare function campPath(name: string): string;
|
|
11
11
|
export declare function listBranches(): Promise<BranchInfo[]>;
|
|
12
|
+
export declare function invalidateBranchCache(): void;
|
|
13
|
+
export declare function startBranchRefresh(intervalMs?: number): ReturnType<typeof setInterval>;
|
|
12
14
|
export declare function addWorktree(name: string, branch: string): Promise<void>;
|
|
13
15
|
export declare function removeWorktree(name: string): Promise<void>;
|
|
@@ -16,14 +16,11 @@ export function campPath(name) {
|
|
|
16
16
|
function git() {
|
|
17
17
|
return simpleGit(getProjectRoot());
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
catch {
|
|
25
|
-
/* offline is OK */
|
|
26
|
-
}
|
|
19
|
+
// --- Branch cache ---
|
|
20
|
+
let _branchCache = null;
|
|
21
|
+
let _branchCacheTime = 0;
|
|
22
|
+
const BRANCH_CACHE_TTL = 30_000;
|
|
23
|
+
async function parseBranches() {
|
|
27
24
|
const raw = await git().raw([
|
|
28
25
|
"for-each-ref",
|
|
29
26
|
"--sort=-committerdate",
|
|
@@ -70,6 +67,31 @@ export async function listBranches() {
|
|
|
70
67
|
}
|
|
71
68
|
return branches;
|
|
72
69
|
}
|
|
70
|
+
async function refreshBranches() {
|
|
71
|
+
try {
|
|
72
|
+
await git().fetch(["--prune"]);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* offline is OK */
|
|
76
|
+
}
|
|
77
|
+
const branches = await parseBranches();
|
|
78
|
+
_branchCache = branches;
|
|
79
|
+
_branchCacheTime = Date.now();
|
|
80
|
+
return branches;
|
|
81
|
+
}
|
|
82
|
+
export async function listBranches() {
|
|
83
|
+
if (_branchCache && Date.now() - _branchCacheTime < BRANCH_CACHE_TTL) {
|
|
84
|
+
return _branchCache;
|
|
85
|
+
}
|
|
86
|
+
return refreshBranches();
|
|
87
|
+
}
|
|
88
|
+
export function invalidateBranchCache() {
|
|
89
|
+
_branchCacheTime = 0;
|
|
90
|
+
}
|
|
91
|
+
export function startBranchRefresh(intervalMs = 30_000) {
|
|
92
|
+
refreshBranches().catch(() => { });
|
|
93
|
+
return setInterval(() => refreshBranches().catch(() => { }), intervalMs);
|
|
94
|
+
}
|
|
73
95
|
export async function addWorktree(name, branch) {
|
|
74
96
|
const path = campPath(name);
|
|
75
97
|
const refs = [`origin/${branch}`, branch];
|
package/dist/lib/server.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ interface CreateAppResult {
|
|
|
14
14
|
installed: boolean;
|
|
15
15
|
};
|
|
16
16
|
watchers: Map<string, CampWatcher>;
|
|
17
|
+
healthCheckTimer: NodeJS.Timeout;
|
|
17
18
|
}
|
|
18
19
|
export declare function createApp(projectRoot: string, options?: CreateAppOptions): Promise<CreateAppResult>;
|
|
19
20
|
export declare function startServer(projectRoot: string, options?: CreateAppOptions): Promise<Server>;
|