sanjang 0.3.0 → 0.3.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/dist/bin/sanjang.d.ts +1 -0
- package/dist/bin/sanjang.js +138 -0
- package/dist/lib/config.d.ts +19 -0
- package/dist/lib/config.js +318 -0
- package/dist/lib/engine/cache.d.ts +7 -0
- package/dist/lib/engine/cache.js +183 -0
- package/dist/lib/engine/config-hotfix.d.ts +7 -0
- package/dist/lib/engine/config-hotfix.js +129 -0
- package/dist/lib/engine/conflict.d.ts +12 -0
- package/dist/lib/engine/conflict.js +32 -0
- package/dist/lib/engine/diagnostics.d.ts +15 -0
- package/dist/lib/engine/diagnostics.js +58 -0
- package/dist/lib/engine/naming.d.ts +10 -0
- package/dist/lib/engine/naming.js +83 -0
- package/dist/lib/engine/ports.d.ts +9 -0
- package/dist/lib/engine/ports.js +55 -0
- package/dist/lib/engine/pr.d.ts +27 -0
- package/dist/lib/engine/pr.js +54 -0
- package/dist/lib/engine/process.d.ts +15 -0
- package/dist/lib/engine/process.js +250 -0
- package/dist/lib/engine/self-heal.d.ts +12 -0
- package/dist/lib/engine/self-heal.js +98 -0
- package/dist/lib/engine/smart-init.d.ts +7 -0
- package/dist/lib/engine/smart-init.js +138 -0
- package/dist/lib/engine/smart-pr.d.ts +19 -0
- package/dist/lib/engine/smart-pr.js +105 -0
- package/dist/lib/engine/snapshot.d.ts +10 -0
- package/dist/lib/engine/snapshot.js +35 -0
- package/dist/lib/engine/state.d.ts +7 -0
- package/dist/lib/engine/state.js +53 -0
- package/dist/lib/engine/suggest.d.ts +21 -0
- package/dist/lib/engine/suggest.js +121 -0
- package/dist/lib/engine/warp.d.ts +23 -0
- package/dist/lib/engine/warp.js +32 -0
- package/dist/lib/engine/watcher.d.ts +11 -0
- package/dist/lib/engine/watcher.js +43 -0
- package/dist/lib/engine/worktree.d.ts +13 -0
- package/dist/lib/engine/worktree.js +91 -0
- package/dist/lib/server.d.ts +20 -0
- package/dist/lib/server.js +1399 -0
- package/dist/lib/types.d.ts +109 -0
- package/dist/lib/types.js +2 -0
- package/package.json +5 -5
- package/bin/__tests__/sanjang.test.ts +0 -42
- package/bin/sanjang.js +0 -17
- package/bin/sanjang.ts +0 -144
- package/lib/config.ts +0 -337
- package/lib/engine/cache.ts +0 -218
- package/lib/engine/config-hotfix.ts +0 -161
- package/lib/engine/conflict.ts +0 -33
- package/lib/engine/diagnostics.ts +0 -81
- package/lib/engine/naming.ts +0 -93
- package/lib/engine/ports.ts +0 -61
- package/lib/engine/pr.ts +0 -71
- package/lib/engine/process.ts +0 -283
- package/lib/engine/self-heal.ts +0 -130
- package/lib/engine/smart-init.ts +0 -136
- package/lib/engine/smart-pr.ts +0 -130
- package/lib/engine/snapshot.ts +0 -45
- package/lib/engine/state.ts +0 -60
- package/lib/engine/suggest.ts +0 -169
- package/lib/engine/warp.ts +0 -47
- package/lib/engine/watcher.ts +0 -40
- package/lib/engine/worktree.ts +0 -100
- package/lib/server.ts +0 -1560
- package/lib/types.ts +0 -130
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
let campsDir = null;
|
|
4
|
+
export function setCampsDir(dir) {
|
|
5
|
+
campsDir = dir;
|
|
6
|
+
}
|
|
7
|
+
export function getCampsDir() {
|
|
8
|
+
if (!campsDir)
|
|
9
|
+
throw new Error("campsDir not initialized. Call setCampsDir() first.");
|
|
10
|
+
return campsDir;
|
|
11
|
+
}
|
|
12
|
+
function stateFile() {
|
|
13
|
+
return join(getCampsDir(), "state.json");
|
|
14
|
+
}
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
mkdirSync(getCampsDir(), { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
function read() {
|
|
19
|
+
const f = stateFile();
|
|
20
|
+
if (!existsSync(f))
|
|
21
|
+
return [];
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(f, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function write(records) {
|
|
30
|
+
ensureDir();
|
|
31
|
+
// Atomic write: write to temp file then rename to prevent corruption
|
|
32
|
+
const tmp = stateFile() + ".tmp";
|
|
33
|
+
writeFileSync(tmp, JSON.stringify(records, null, 2), "utf8");
|
|
34
|
+
renameSync(tmp, stateFile());
|
|
35
|
+
}
|
|
36
|
+
export function getAll() {
|
|
37
|
+
return read();
|
|
38
|
+
}
|
|
39
|
+
export function getOne(name) {
|
|
40
|
+
return read().find((r) => r.name === name) ?? null;
|
|
41
|
+
}
|
|
42
|
+
export function upsert(record) {
|
|
43
|
+
const records = read();
|
|
44
|
+
const idx = records.findIndex((r) => r.name === record.name);
|
|
45
|
+
if (idx === -1)
|
|
46
|
+
records.push(record);
|
|
47
|
+
else
|
|
48
|
+
records[idx] = record;
|
|
49
|
+
write(records);
|
|
50
|
+
}
|
|
51
|
+
export function remove(name) {
|
|
52
|
+
write(read().filter((r) => r.name !== name));
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task suggestion engine.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates open issues, PRs, and recent git activity to surface
|
|
5
|
+
* actionable suggestions on the dashboard — no LLM required.
|
|
6
|
+
*/
|
|
7
|
+
export interface Suggestion {
|
|
8
|
+
type: "issue" | "pr" | "recent";
|
|
9
|
+
title: string;
|
|
10
|
+
detail?: string;
|
|
11
|
+
action?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Suggest tasks the user might work on next.
|
|
15
|
+
*
|
|
16
|
+
* Aggregates data from GitHub (issues, PRs) and git (recent commits).
|
|
17
|
+
* If `gh` CLI is unavailable, returns git-based suggestions only.
|
|
18
|
+
*
|
|
19
|
+
* Results are sorted by relevance: PRs (이어하기) > Issues (이슈) > Recent (최근 작업).
|
|
20
|
+
*/
|
|
21
|
+
export declare function suggestTasks(projectRoot: string): Promise<Suggestion[]>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task suggestion engine.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates open issues, PRs, and recent git activity to surface
|
|
5
|
+
* actionable suggestions on the dashboard — no LLM required.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const TIMEOUT_MS = 10_000;
|
|
12
|
+
/**
|
|
13
|
+
* Async spawn wrapper — resolves with stdout or rejects on timeout / error.
|
|
14
|
+
*/
|
|
15
|
+
function run(cmd, args, cwd) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const child = spawn(cmd, args, {
|
|
18
|
+
cwd,
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
20
|
+
env: { ...process.env },
|
|
21
|
+
});
|
|
22
|
+
let stdout = "";
|
|
23
|
+
let stderr = "";
|
|
24
|
+
child.stdout.on("data", (chunk) => {
|
|
25
|
+
stdout += chunk.toString();
|
|
26
|
+
});
|
|
27
|
+
child.stderr.on("data", (chunk) => {
|
|
28
|
+
stderr += chunk.toString();
|
|
29
|
+
});
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
child.kill("SIGTERM");
|
|
32
|
+
reject(new Error(`Command timed out: ${cmd} ${args.join(" ")}`));
|
|
33
|
+
}, TIMEOUT_MS);
|
|
34
|
+
child.on("close", (code) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
if (code === 0)
|
|
37
|
+
resolve(stdout);
|
|
38
|
+
else
|
|
39
|
+
reject(new Error(`Exit ${code}: ${stderr || stdout}`));
|
|
40
|
+
});
|
|
41
|
+
child.on("error", (err) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
reject(err);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Data fetchers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
async function fetchIssues(cwd) {
|
|
51
|
+
const raw = await run("gh", ["issue", "list", "--state", "open", "--limit", "5", "--json", "number,title,labels"], cwd);
|
|
52
|
+
const issues = JSON.parse(raw);
|
|
53
|
+
return issues.map((i) => {
|
|
54
|
+
const labelStr = i.labels.map((l) => l.name).join(", ");
|
|
55
|
+
return {
|
|
56
|
+
type: "issue",
|
|
57
|
+
title: `#${i.number} ${i.title}`,
|
|
58
|
+
detail: labelStr || undefined,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function fetchMyPrs(cwd) {
|
|
63
|
+
const raw = await run("gh", ["pr", "list", "--state", "open", "--author", "@me", "--limit", "3", "--json", "number,title,headRefName"], cwd);
|
|
64
|
+
const prs = JSON.parse(raw);
|
|
65
|
+
return prs.map((p) => ({
|
|
66
|
+
type: "pr",
|
|
67
|
+
title: `#${p.number} ${p.title}`,
|
|
68
|
+
detail: p.headRefName,
|
|
69
|
+
action: p.headRefName,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
async function fetchRecentCommits(cwd) {
|
|
73
|
+
const raw = await run("git", ["log", "--oneline", "-10"], cwd);
|
|
74
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
75
|
+
return lines.map((line) => {
|
|
76
|
+
const spaceIdx = line.indexOf(" ");
|
|
77
|
+
const hash = spaceIdx > 0 ? line.slice(0, spaceIdx) : line;
|
|
78
|
+
const msg = spaceIdx > 0 ? line.slice(spaceIdx + 1) : "";
|
|
79
|
+
return {
|
|
80
|
+
type: "recent",
|
|
81
|
+
title: msg || hash,
|
|
82
|
+
detail: hash,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Public API
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
/**
|
|
90
|
+
* Suggest tasks the user might work on next.
|
|
91
|
+
*
|
|
92
|
+
* Aggregates data from GitHub (issues, PRs) and git (recent commits).
|
|
93
|
+
* If `gh` CLI is unavailable, returns git-based suggestions only.
|
|
94
|
+
*
|
|
95
|
+
* Results are sorted by relevance: PRs (이어하기) > Issues (이슈) > Recent (최근 작업).
|
|
96
|
+
*/
|
|
97
|
+
export async function suggestTasks(projectRoot) {
|
|
98
|
+
const results = [];
|
|
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
|
+
]);
|
|
104
|
+
// PRs first — most actionable ("이어하기")
|
|
105
|
+
if (prs.status === "fulfilled") {
|
|
106
|
+
results.push(...prs.value);
|
|
107
|
+
}
|
|
108
|
+
// Issues next ("이슈")
|
|
109
|
+
if (issues.status === "fulfilled") {
|
|
110
|
+
results.push(...issues.value);
|
|
111
|
+
}
|
|
112
|
+
// Recent commits always available ("최근 작업")
|
|
113
|
+
try {
|
|
114
|
+
const recent = await fetchRecentCommits(projectRoot);
|
|
115
|
+
results.push(...recent);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// No git history — return whatever we have
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface WarpDetectResult {
|
|
2
|
+
installed: boolean;
|
|
3
|
+
}
|
|
4
|
+
interface WarpOpenResult {
|
|
5
|
+
opened: boolean;
|
|
6
|
+
terminal: string | null;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detect if Warp terminal is installed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectWarp(): WarpDetectResult;
|
|
13
|
+
/**
|
|
14
|
+
* Open a Warp tab at the given worktree path.
|
|
15
|
+
* Opens as a new tab in the existing Warp window (not a new window).
|
|
16
|
+
* The tab title naturally shows the directory name (= camp name).
|
|
17
|
+
*/
|
|
18
|
+
export declare function openWarpTab(campName: string, worktreePath: string): WarpOpenResult;
|
|
19
|
+
/**
|
|
20
|
+
* No-op cleanup (launch config removed — using open -a instead).
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeLaunchConfig(_campName: string): void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* Detect if Warp terminal is installed.
|
|
5
|
+
*/
|
|
6
|
+
export function detectWarp() {
|
|
7
|
+
const installed = existsSync("/Applications/Warp.app");
|
|
8
|
+
return { installed };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Open a Warp tab at the given worktree path.
|
|
12
|
+
* Opens as a new tab in the existing Warp window (not a new window).
|
|
13
|
+
* The tab title naturally shows the directory name (= camp name).
|
|
14
|
+
*/
|
|
15
|
+
export function openWarpTab(campName, worktreePath) {
|
|
16
|
+
const { installed } = detectWarp();
|
|
17
|
+
if (!installed) {
|
|
18
|
+
return { opened: false, terminal: null, path: worktreePath };
|
|
19
|
+
}
|
|
20
|
+
// open -a Warp {path} → opens tab in existing window with dir name as title
|
|
21
|
+
const result = spawnSync("open", ["-a", "Warp", worktreePath], { stdio: "pipe" });
|
|
22
|
+
return {
|
|
23
|
+
opened: result.status === 0,
|
|
24
|
+
terminal: "warp",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* No-op cleanup (launch config removed — using open -a instead).
|
|
29
|
+
*/
|
|
30
|
+
export function removeLaunchConfig(_campName) {
|
|
31
|
+
// Intentionally empty — kept for API compatibility with server.ts
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class CampWatcher {
|
|
2
|
+
private readonly dir;
|
|
3
|
+
private readonly onChange;
|
|
4
|
+
private readonly debounceMs;
|
|
5
|
+
private watcher;
|
|
6
|
+
private timer;
|
|
7
|
+
private stopped;
|
|
8
|
+
constructor(dir: string, onChange: () => void, debounceMs?: number);
|
|
9
|
+
start(): void;
|
|
10
|
+
stop(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
export class CampWatcher {
|
|
3
|
+
dir;
|
|
4
|
+
onChange;
|
|
5
|
+
debounceMs;
|
|
6
|
+
watcher = null;
|
|
7
|
+
timer = null;
|
|
8
|
+
stopped = false;
|
|
9
|
+
constructor(dir, onChange, debounceMs = 500) {
|
|
10
|
+
this.dir = dir;
|
|
11
|
+
this.onChange = onChange;
|
|
12
|
+
this.debounceMs = debounceMs;
|
|
13
|
+
}
|
|
14
|
+
start() {
|
|
15
|
+
this.stopped = false;
|
|
16
|
+
try {
|
|
17
|
+
this.watcher = watch(this.dir, { recursive: true }, () => {
|
|
18
|
+
if (this.stopped)
|
|
19
|
+
return;
|
|
20
|
+
if (this.timer)
|
|
21
|
+
clearTimeout(this.timer);
|
|
22
|
+
this.timer = setTimeout(() => {
|
|
23
|
+
if (!this.stopped)
|
|
24
|
+
this.onChange();
|
|
25
|
+
}, this.debounceMs);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// fs.watch can fail on some platforms/dirs — silently degrade
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
stop() {
|
|
33
|
+
this.stopped = true;
|
|
34
|
+
if (this.timer) {
|
|
35
|
+
clearTimeout(this.timer);
|
|
36
|
+
this.timer = null;
|
|
37
|
+
}
|
|
38
|
+
if (this.watcher) {
|
|
39
|
+
this.watcher.close();
|
|
40
|
+
this.watcher = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BranchInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
remote: boolean;
|
|
4
|
+
local: boolean;
|
|
5
|
+
date: string;
|
|
6
|
+
category?: "default" | "feature" | "fix" | "other";
|
|
7
|
+
}
|
|
8
|
+
export declare function setProjectRoot(root: string): void;
|
|
9
|
+
export declare function getProjectRoot(): string;
|
|
10
|
+
export declare function campPath(name: string): string;
|
|
11
|
+
export declare function listBranches(): Promise<BranchInfo[]>;
|
|
12
|
+
export declare function addWorktree(name: string, branch: string): Promise<void>;
|
|
13
|
+
export declare function removeWorktree(name: string): Promise<void>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { simpleGit } from "simple-git";
|
|
3
|
+
import { getCampsDir } from "./state.js";
|
|
4
|
+
let projectRoot = null;
|
|
5
|
+
export function setProjectRoot(root) {
|
|
6
|
+
projectRoot = root;
|
|
7
|
+
}
|
|
8
|
+
export function getProjectRoot() {
|
|
9
|
+
if (!projectRoot)
|
|
10
|
+
throw new Error("projectRoot not initialized. Call setProjectRoot() first.");
|
|
11
|
+
return projectRoot;
|
|
12
|
+
}
|
|
13
|
+
export function campPath(name) {
|
|
14
|
+
return join(getCampsDir(), name);
|
|
15
|
+
}
|
|
16
|
+
function git() {
|
|
17
|
+
return simpleGit(getProjectRoot());
|
|
18
|
+
}
|
|
19
|
+
export async function listBranches() {
|
|
20
|
+
// Best-effort fetch — continue with local refs on network failure
|
|
21
|
+
try {
|
|
22
|
+
await git().fetch(["--prune"]);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* offline is OK */
|
|
26
|
+
}
|
|
27
|
+
const raw = await git().raw([
|
|
28
|
+
"for-each-ref",
|
|
29
|
+
"--sort=-committerdate",
|
|
30
|
+
"--format=%(refname:short)\t%(committerdate:relative)\t%(refname)",
|
|
31
|
+
"refs/heads/",
|
|
32
|
+
"refs/remotes/origin/",
|
|
33
|
+
]);
|
|
34
|
+
const map = new Map();
|
|
35
|
+
for (const line of raw.trim().split("\n")) {
|
|
36
|
+
if (!line)
|
|
37
|
+
continue;
|
|
38
|
+
const [shortName, date, fullRef] = line.split("\t");
|
|
39
|
+
if (!shortName || !fullRef)
|
|
40
|
+
continue;
|
|
41
|
+
if (shortName.includes("HEAD"))
|
|
42
|
+
continue;
|
|
43
|
+
const isRemote = fullRef.startsWith("refs/remotes/origin/");
|
|
44
|
+
const clean = shortName.replace(/^origin\//, "").trim();
|
|
45
|
+
if (!clean)
|
|
46
|
+
continue;
|
|
47
|
+
const entry = map.get(clean) || { name: clean, remote: false, local: false, date: date ?? "" };
|
|
48
|
+
if (isRemote)
|
|
49
|
+
entry.remote = true;
|
|
50
|
+
else
|
|
51
|
+
entry.local = true;
|
|
52
|
+
if (!entry.date)
|
|
53
|
+
entry.date = date ?? "";
|
|
54
|
+
map.set(clean, entry);
|
|
55
|
+
}
|
|
56
|
+
const branches = [...map.values()];
|
|
57
|
+
for (const b of branches) {
|
|
58
|
+
if (["dev", "main", "master"].includes(b.name)) {
|
|
59
|
+
b.category = "default";
|
|
60
|
+
}
|
|
61
|
+
else if (b.name.startsWith("feature/")) {
|
|
62
|
+
b.category = "feature";
|
|
63
|
+
}
|
|
64
|
+
else if (b.name.startsWith("fix/") || b.name.startsWith("hotfix/")) {
|
|
65
|
+
b.category = "fix";
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
b.category = "other";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return branches;
|
|
72
|
+
}
|
|
73
|
+
export async function addWorktree(name, branch) {
|
|
74
|
+
const path = campPath(name);
|
|
75
|
+
const refs = [`origin/${branch}`, branch];
|
|
76
|
+
let lastErr;
|
|
77
|
+
for (const ref of refs) {
|
|
78
|
+
try {
|
|
79
|
+
await git().raw(["worktree", "add", "--detach", path, ref]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
lastErr = err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw lastErr;
|
|
87
|
+
}
|
|
88
|
+
export async function removeWorktree(name) {
|
|
89
|
+
const path = campPath(name);
|
|
90
|
+
await git().raw(["worktree", "remove", "--force", path]);
|
|
91
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import { type Server } from "node:http";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import { CampWatcher } from "./engine/watcher.ts";
|
|
5
|
+
interface CreateAppOptions {
|
|
6
|
+
port?: number;
|
|
7
|
+
}
|
|
8
|
+
interface CreateAppResult {
|
|
9
|
+
app: express.Application;
|
|
10
|
+
server: Server;
|
|
11
|
+
port: number;
|
|
12
|
+
runningTasks: Map<string, ChildProcess>;
|
|
13
|
+
warpStatus: {
|
|
14
|
+
installed: boolean;
|
|
15
|
+
};
|
|
16
|
+
watchers: Map<string, CampWatcher>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createApp(projectRoot: string, options?: CreateAppOptions): Promise<CreateAppResult>;
|
|
19
|
+
export declare function startServer(projectRoot: string, options?: CreateAppOptions): Promise<Server>;
|
|
20
|
+
export {};
|