@ohzw/worktree-command-tui 0.1.1 → 0.1.3
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 +91 -26
- package/dist/app.d.ts +1 -1
- package/dist/app.js +242 -114
- package/dist/components/ActionPanel.d.ts +4 -2
- package/dist/components/ActionPanel.js +87 -135
- package/dist/components/ContextBar.d.ts +4 -1
- package/dist/components/ContextBar.js +29 -3
- package/dist/components/Header.js +5 -1
- package/dist/components/HelpWindow.d.ts +7 -0
- package/dist/components/HelpWindow.js +29 -0
- package/dist/components/LogPanel.d.ts +10 -3
- package/dist/components/LogPanel.js +240 -33
- package/dist/components/WorktreeList.js +20 -40
- package/dist/core/command-runner.d.ts +11 -0
- package/dist/core/command-runner.js +59 -7
- package/dist/core/config-lifecycle.d.ts +25 -0
- package/dist/core/config-lifecycle.js +160 -0
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +0 -48
- package/dist/core/git-metadata.d.ts +25 -0
- package/dist/core/git-metadata.js +84 -0
- package/dist/core/git-worktrees.d.ts +2 -1
- package/dist/core/git-worktrees.js +30 -11
- package/dist/core/github-metadata.d.ts +21 -0
- package/dist/core/github-metadata.js +153 -0
- package/dist/core/init.d.ts +3 -2
- package/dist/core/init.js +9 -57
- package/dist/core/log-reader.d.ts +7 -0
- package/dist/core/log-reader.js +59 -0
- package/dist/core/posix-process.d.ts +2 -2
- package/dist/core/posix-process.js +19 -4
- package/dist/core/process-control.d.ts +2 -2
- package/dist/core/process-control.js +5 -2
- package/dist/core/runtime-state.d.ts +42 -0
- package/dist/core/runtime-state.js +125 -0
- package/dist/core/runtime.d.ts +19 -39
- package/dist/core/runtime.js +112 -216
- package/dist/core/session-store.js +22 -7
- package/dist/core/tui-interaction.d.ts +31 -0
- package/dist/core/tui-interaction.js +59 -0
- package/dist/core/worktree-projection.d.ts +76 -0
- package/dist/core/worktree-projection.js +132 -0
- package/dist/main.js +6 -5
- package/dist/terminal/viewport.d.ts +15 -0
- package/dist/terminal/viewport.js +49 -0
- package/package.json +1 -1
package/dist/core/init.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { access, readFile
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import {
|
|
6
|
+
import { createDefaultToolConfig, renderConfigJsonc, writeToolConfigForRepo } from './config-lifecycle.js';
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
-
|
|
9
|
-
return /^[A-Za-z0-9._-]+$/u.test(value);
|
|
10
|
-
}
|
|
11
|
-
function toSafeNamespace(value) {
|
|
12
|
-
const replaced = value.replace(/[^A-Za-z0-9._-]+/gu, '-');
|
|
13
|
-
const trimmed = replaced.replace(/^-+/, '').replace(/-+$/, '');
|
|
14
|
-
return trimmed.length > 0 && isSafeNamespace(trimmed) ? trimmed : 'worktree-command-tui';
|
|
15
|
-
}
|
|
8
|
+
export { renderConfigJsonc };
|
|
16
9
|
async function resolveRepositoryRoot(cwd) {
|
|
17
10
|
const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd });
|
|
18
11
|
return stdout.trim();
|
|
@@ -85,56 +78,15 @@ function selectDefaultScript(scripts) {
|
|
|
85
78
|
export async function buildDefaultConfig(repoRoot) {
|
|
86
79
|
const packageJson = await readPackageJson(repoRoot);
|
|
87
80
|
const packageManager = await detectPackageManager(repoRoot, packageJson);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
port: 3000,
|
|
94
|
-
requiredFiles: ['package.json'],
|
|
95
|
-
orphanMatchers: [],
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
export function renderConfigJsonc(config) {
|
|
99
|
-
return `{
|
|
100
|
-
// Session namespace used for git-common-dir state files and logs.
|
|
101
|
-
// Keep this filesystem-safe: letters, numbers, dots, underscores, and hyphens only.
|
|
102
|
-
"namespace": ${JSON.stringify(config.namespace)},
|
|
103
|
-
|
|
104
|
-
// Command launched in the selected worktree.
|
|
105
|
-
// Use argv form so spaces and shell metacharacters are passed safely.
|
|
106
|
-
"command": ${JSON.stringify(config.command)},
|
|
107
|
-
|
|
108
|
-
// TCP port owned by the command, used when stopping stale/orphaned processes.
|
|
109
|
-
"port": ${JSON.stringify(config.port)},
|
|
110
|
-
|
|
111
|
-
// Files that must exist in a worktree before the command can be started there.
|
|
112
|
-
"requiredFiles": ${JSON.stringify(config.requiredFiles)},
|
|
113
|
-
|
|
114
|
-
// Extra process command-line substrings treated as orphans for cleanup.
|
|
115
|
-
// Example: ["node --watch", "vite --host 0.0.0.0"]
|
|
116
|
-
"orphanMatchers": ${JSON.stringify(config.orphanMatchers)},
|
|
117
|
-
}
|
|
118
|
-
`;
|
|
119
|
-
}
|
|
120
|
-
async function findExistingConfigPath(workspaceRoot) {
|
|
121
|
-
for (const fileName of CONFIG_FILE_NAMES) {
|
|
122
|
-
const configPath = path.join(workspaceRoot, fileName);
|
|
123
|
-
if (await fileExists(configPath)) {
|
|
124
|
-
return configPath;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
81
|
+
return createDefaultToolConfig({
|
|
82
|
+
namespaceSeed: packageJson?.name ?? path.basename(repoRoot),
|
|
83
|
+
packageManager,
|
|
84
|
+
script: selectDefaultScript(packageJson?.scripts),
|
|
85
|
+
});
|
|
128
86
|
}
|
|
129
87
|
export async function runInit(options) {
|
|
130
|
-
const configPath = path.join(options.workspaceRoot, CONFIG_FILE_NAME);
|
|
131
|
-
const existingConfigPath = await findExistingConfigPath(options.workspaceRoot);
|
|
132
|
-
if (!options.force && existingConfigPath) {
|
|
133
|
-
throw new Error(`Config file already exists: ${existingConfigPath}`);
|
|
134
|
-
}
|
|
135
88
|
const config = await buildDefaultConfig(options.workspaceRoot);
|
|
136
|
-
|
|
137
|
-
return { path: configPath, config };
|
|
89
|
+
return writeToolConfigForRepo({ ...options, config }, fileExists);
|
|
138
90
|
}
|
|
139
91
|
export async function createConfigForRepo(options) {
|
|
140
92
|
const workspaceRoot = await resolveRepositoryRoot(options.cwd);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { open, readdir, stat } from 'node:fs/promises';
|
|
3
|
+
const MAX_LOG_BYTES = 16 * 1024;
|
|
4
|
+
const MAX_LOG_LINES = 120;
|
|
5
|
+
const MAX_LOG_FILES = 100;
|
|
6
|
+
export function tailLogContent(content) {
|
|
7
|
+
const byteTrimmed = content.length > MAX_LOG_BYTES ? content.slice(-MAX_LOG_BYTES) : content;
|
|
8
|
+
const lines = byteTrimmed.replace(/\r\n/g, '\n').split('\n');
|
|
9
|
+
const tailLines = lines.length > MAX_LOG_LINES ? lines.slice(-MAX_LOG_LINES) : lines;
|
|
10
|
+
return tailLines.join('\n').trimEnd();
|
|
11
|
+
}
|
|
12
|
+
async function readLogTail(filePath) {
|
|
13
|
+
const stats = await stat(filePath);
|
|
14
|
+
const bytesToRead = Math.min(stats.size, MAX_LOG_BYTES);
|
|
15
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
16
|
+
const file = await open(filePath, 'r');
|
|
17
|
+
try {
|
|
18
|
+
await file.read(buffer, 0, bytesToRead, Math.max(0, stats.size - bytesToRead));
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await file.close();
|
|
22
|
+
}
|
|
23
|
+
return buffer.toString('utf8');
|
|
24
|
+
}
|
|
25
|
+
export async function readLogs(logsDir, activeLogPath) {
|
|
26
|
+
try {
|
|
27
|
+
const entries = (await readdir(logsDir, { withFileTypes: true }))
|
|
28
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.log'))
|
|
29
|
+
.slice(0, MAX_LOG_FILES)
|
|
30
|
+
.map(entry => ({ name: entry.name, path: path.join(logsDir, entry.name) }));
|
|
31
|
+
if (entries.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
let selectedEntries = entries;
|
|
35
|
+
if (activeLogPath !== null) {
|
|
36
|
+
const activeEntry = entries.find(entry => entry.path === activeLogPath);
|
|
37
|
+
selectedEntries = activeEntry ? [activeEntry] : [];
|
|
38
|
+
if (selectedEntries.length === 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const withStats = await Promise.all(entries.map(async (entry) => ({
|
|
44
|
+
...entry,
|
|
45
|
+
mtimeMs: (await stat(entry.path)).mtimeMs,
|
|
46
|
+
})));
|
|
47
|
+
withStats.sort((a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name));
|
|
48
|
+
selectedEntries = [withStats[0]];
|
|
49
|
+
}
|
|
50
|
+
return await Promise.all(selectedEntries.map(async (entry) => ({
|
|
51
|
+
name: entry.name,
|
|
52
|
+
path: entry.path,
|
|
53
|
+
content: tailLogContent(await readLogTail(entry.path)),
|
|
54
|
+
})));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare function isProcessGroupAlive(pgid: number): Promise<boolean>;
|
|
2
2
|
export declare function killProcessGroup(pgid: number, signal?: NodeJS.Signals): Promise<void>;
|
|
3
|
-
export declare function killPortOwner(port: number): Promise<void>;
|
|
4
|
-
export declare function killOrphans(matcher: string): Promise<void>;
|
|
3
|
+
export declare function killPortOwner(port: number, pgid: number): Promise<void>;
|
|
4
|
+
export declare function killOrphans(matcher: string, pgid: number): Promise<void>;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
|
+
async function readProcessGroupId(pid) {
|
|
5
|
+
try {
|
|
6
|
+
const { stdout } = await execFileAsync('ps', ['-o', 'pgid=', '-p', pid]);
|
|
7
|
+
const pgid = Number(stdout.trim());
|
|
8
|
+
return Number.isInteger(pgid) ? pgid : null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
export async function isProcessGroupAlive(pgid) {
|
|
5
15
|
try {
|
|
6
16
|
process.kill(-pgid, 0);
|
|
@@ -11,6 +21,9 @@ export async function isProcessGroupAlive(pgid) {
|
|
|
11
21
|
}
|
|
12
22
|
}
|
|
13
23
|
export async function killProcessGroup(pgid, signal = 'SIGTERM') {
|
|
24
|
+
if (pgid <= 1) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
14
27
|
try {
|
|
15
28
|
process.kill(-pgid, signal);
|
|
16
29
|
}
|
|
@@ -18,23 +31,25 @@ export async function killProcessGroup(pgid, signal = 'SIGTERM') {
|
|
|
18
31
|
// Process group already gone.
|
|
19
32
|
}
|
|
20
33
|
}
|
|
21
|
-
export async function killPortOwner(port) {
|
|
34
|
+
export async function killPortOwner(port, pgid) {
|
|
22
35
|
try {
|
|
23
36
|
const { stdout } = await execFileAsync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t']);
|
|
24
37
|
for (const pid of stdout
|
|
25
38
|
.split('\n')
|
|
26
39
|
.map(line => line.trim())
|
|
27
40
|
.filter(Boolean)) {
|
|
28
|
-
await
|
|
41
|
+
if (await readProcessGroupId(pid) === pgid) {
|
|
42
|
+
await execFileAsync('kill', [pid]);
|
|
43
|
+
}
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
46
|
catch {
|
|
32
47
|
// Port not owned or lsof found nothing.
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
|
-
export async function killOrphans(matcher) {
|
|
50
|
+
export async function killOrphans(matcher, pgid) {
|
|
36
51
|
try {
|
|
37
|
-
await execFileAsync('pkill', ['-f', matcher]);
|
|
52
|
+
await execFileAsync('pkill', ['-g', String(pgid), '-f', matcher]);
|
|
38
53
|
}
|
|
39
54
|
catch {
|
|
40
55
|
// No matching orphan process.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface CleanupDeps {
|
|
2
2
|
killProcessGroup: (pgid: number, signal?: NodeJS.Signals) => Promise<void>;
|
|
3
|
-
killPortOwner: (port: number) => Promise<void>;
|
|
4
|
-
killOrphans: (matcher: string) => Promise<void>;
|
|
3
|
+
killPortOwner: (port: number, pgid: number) => Promise<void>;
|
|
4
|
+
killOrphans: (matcher: string, pgid: number) => Promise<void>;
|
|
5
5
|
isSessionAlive: (pgid: number) => Promise<boolean>;
|
|
6
6
|
}
|
|
7
7
|
export declare function stopSessionWithFallback(input: {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export async function stopSessionWithFallback(input, deps) {
|
|
2
|
+
if (input.pgid <= 1) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
2
5
|
await deps.killProcessGroup(input.pgid, 'SIGTERM');
|
|
3
6
|
if (!(await deps.isSessionAlive(input.pgid))) {
|
|
4
7
|
return true;
|
|
5
8
|
}
|
|
6
|
-
await deps.killPortOwner(input.port);
|
|
9
|
+
await deps.killPortOwner(input.port, input.pgid);
|
|
7
10
|
for (const matcher of input.orphanMatchers) {
|
|
8
|
-
await deps.killOrphans(matcher);
|
|
11
|
+
await deps.killOrphans(matcher, input.pgid);
|
|
9
12
|
}
|
|
10
13
|
if (!(await deps.isSessionAlive(input.pgid))) {
|
|
11
14
|
return true;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ToolConfig } from './config.js';
|
|
2
|
+
import type { AppActions, AppLogEntry, AppModel, AppStatus } from './runtime.js';
|
|
3
|
+
import type { SessionPaths, SessionRecord } from './session-store.js';
|
|
4
|
+
export interface StartedCommand {
|
|
5
|
+
pid: number;
|
|
6
|
+
pgid: number;
|
|
7
|
+
logPath: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RuntimeStateAdapter {
|
|
10
|
+
refresh: () => Promise<AppModel>;
|
|
11
|
+
readActive: () => Promise<SessionRecord | null>;
|
|
12
|
+
readLogs: (logPath: string | null) => Promise<AppLogEntry[]>;
|
|
13
|
+
readWorktreeBranch: (worktreePath: string) => Promise<string>;
|
|
14
|
+
getInvalidReason: (worktreePath: string) => Promise<string | null>;
|
|
15
|
+
runSetup: (input: {
|
|
16
|
+
command: string[];
|
|
17
|
+
cwd: string;
|
|
18
|
+
logsDir: string;
|
|
19
|
+
logFileBase: string;
|
|
20
|
+
}) => Promise<{
|
|
21
|
+
logPath: string;
|
|
22
|
+
}>;
|
|
23
|
+
startCommand: (input: {
|
|
24
|
+
command: string[];
|
|
25
|
+
cwd: string;
|
|
26
|
+
logsDir: string;
|
|
27
|
+
logFileBase: string;
|
|
28
|
+
}) => Promise<StartedCommand>;
|
|
29
|
+
stopSession: (active: SessionRecord) => Promise<void>;
|
|
30
|
+
clearSession: () => Promise<void>;
|
|
31
|
+
writeSession: (record: SessionRecord) => Promise<void>;
|
|
32
|
+
openEditor: (worktreePath: string) => Promise<AppStatus>;
|
|
33
|
+
openPullRequest: (worktreePath: string) => Promise<AppStatus>;
|
|
34
|
+
deleteWorktree: (worktreePath: string) => Promise<AppStatus>;
|
|
35
|
+
nowIso: () => string;
|
|
36
|
+
}
|
|
37
|
+
export interface RuntimeStateOptions {
|
|
38
|
+
config: ToolConfig;
|
|
39
|
+
paths: SessionPaths;
|
|
40
|
+
adapter: RuntimeStateAdapter;
|
|
41
|
+
}
|
|
42
|
+
export declare function createRuntimeStateActions({ config, paths, adapter }: RuntimeStateOptions): AppActions;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
function toLogFileBase(branch) {
|
|
3
|
+
return branch.replace(/[\\/]/g, '-');
|
|
4
|
+
}
|
|
5
|
+
export function createRuntimeStateActions({ config, paths, adapter }) {
|
|
6
|
+
const refreshLogs = async () => {
|
|
7
|
+
const active = await adapter.readActive();
|
|
8
|
+
return {
|
|
9
|
+
logs: await adapter.readLogs(active?.logPath ?? null),
|
|
10
|
+
activePath: active?.worktreePath ?? null,
|
|
11
|
+
activeBranch: active?.branch ?? null,
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
const refreshWithStatus = async (run, preserveRunningStatus) => {
|
|
15
|
+
const status = await run();
|
|
16
|
+
const model = await adapter.refresh();
|
|
17
|
+
if (preserveRunningStatus && model.activePath !== null && status.kind === 'idle') {
|
|
18
|
+
return {
|
|
19
|
+
...model,
|
|
20
|
+
status: { kind: 'running', message: status.message },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
...model,
|
|
25
|
+
status,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const stop = async () => {
|
|
29
|
+
const active = await adapter.readActive();
|
|
30
|
+
if (active) {
|
|
31
|
+
await adapter.stopSession(active);
|
|
32
|
+
await adapter.clearSession();
|
|
33
|
+
}
|
|
34
|
+
const model = await adapter.refresh();
|
|
35
|
+
return {
|
|
36
|
+
...model,
|
|
37
|
+
activePath: null,
|
|
38
|
+
activeBranch: null,
|
|
39
|
+
status: { kind: 'idle', message: active ? 'stopped' : 'already stopped' },
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const setup = async (worktreePath) => {
|
|
43
|
+
const setupCommand = config.setupCommand;
|
|
44
|
+
if (setupCommand === undefined) {
|
|
45
|
+
const model = await adapter.refresh();
|
|
46
|
+
return {
|
|
47
|
+
...model,
|
|
48
|
+
status: { kind: 'idle', message: 'setup command is not configured' },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const branch = await adapter.readWorktreeBranch(worktreePath);
|
|
52
|
+
const logFileBase = `${toLogFileBase(branch)}.setup`;
|
|
53
|
+
const setupLogPath = path.join(paths.logsDir, `${logFileBase}.log`);
|
|
54
|
+
try {
|
|
55
|
+
await adapter.runSetup({
|
|
56
|
+
command: setupCommand,
|
|
57
|
+
cwd: worktreePath,
|
|
58
|
+
logsDir: paths.logsDir,
|
|
59
|
+
logFileBase,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const model = await adapter.refresh();
|
|
64
|
+
return {
|
|
65
|
+
...model,
|
|
66
|
+
status: { kind: 'error', message: error instanceof Error ? error.message : String(error) },
|
|
67
|
+
logs: await adapter.readLogs(setupLogPath),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const model = await adapter.refresh();
|
|
71
|
+
return {
|
|
72
|
+
...model,
|
|
73
|
+
status: { kind: model.activePath === null ? 'idle' : 'running', message: `setup complete for ${branch}` },
|
|
74
|
+
logs: await adapter.readLogs(setupLogPath),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const start = async (worktreePath) => {
|
|
78
|
+
const current = await adapter.readActive();
|
|
79
|
+
if (current?.worktreePath === worktreePath) {
|
|
80
|
+
const model = await adapter.refresh();
|
|
81
|
+
return {
|
|
82
|
+
...model,
|
|
83
|
+
activePath: current.worktreePath,
|
|
84
|
+
activeBranch: current.branch,
|
|
85
|
+
status: { kind: 'idle', message: 'already active' },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const invalidReason = await adapter.getInvalidReason(worktreePath);
|
|
89
|
+
if (invalidReason) {
|
|
90
|
+
throw new Error(invalidReason);
|
|
91
|
+
}
|
|
92
|
+
if (current) {
|
|
93
|
+
await adapter.stopSession(current);
|
|
94
|
+
await adapter.clearSession();
|
|
95
|
+
}
|
|
96
|
+
const branch = await adapter.readWorktreeBranch(worktreePath);
|
|
97
|
+
const started = await adapter.startCommand({
|
|
98
|
+
command: config.command,
|
|
99
|
+
cwd: worktreePath,
|
|
100
|
+
logsDir: paths.logsDir,
|
|
101
|
+
logFileBase: toLogFileBase(branch),
|
|
102
|
+
});
|
|
103
|
+
await adapter.writeSession({
|
|
104
|
+
namespace: config.namespace,
|
|
105
|
+
worktreePath,
|
|
106
|
+
branch,
|
|
107
|
+
pid: started.pid,
|
|
108
|
+
pgid: started.pgid,
|
|
109
|
+
port: config.port,
|
|
110
|
+
logPath: started.logPath,
|
|
111
|
+
startedAt: adapter.nowIso(),
|
|
112
|
+
});
|
|
113
|
+
const model = await adapter.refresh();
|
|
114
|
+
return {
|
|
115
|
+
...model,
|
|
116
|
+
activePath: worktreePath,
|
|
117
|
+
activeBranch: branch,
|
|
118
|
+
status: { kind: 'running', message: `started ${branch}` },
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
const openEditor = async (worktreePath) => refreshWithStatus(() => adapter.openEditor(worktreePath), true);
|
|
122
|
+
const openPullRequest = async (worktreePath) => refreshWithStatus(() => adapter.openPullRequest(worktreePath), true);
|
|
123
|
+
const deleteWorktree = async (worktreePath) => refreshWithStatus(() => adapter.deleteWorktree(worktreePath), false);
|
|
124
|
+
return { setup, start, stop, refresh: adapter.refresh, refreshLogs, openEditor, openPullRequest, deleteWorktree };
|
|
125
|
+
}
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
import { type WorktreeRow } from './git-worktrees.js';
|
|
2
|
+
import { type UpstreamInfo, type WorkingTreeInfo } from './git-metadata.js';
|
|
3
|
+
import { type PullRequestInfo } from './github-metadata.js';
|
|
4
|
+
import { type LogEntry } from './log-reader.js';
|
|
2
5
|
export type RowTag = 'main' | 'active' | 'invalid' | 'external' | 'legacy';
|
|
3
|
-
export interface UpstreamInfo {
|
|
4
|
-
branch: string;
|
|
5
|
-
ahead: number;
|
|
6
|
-
behind: number;
|
|
7
|
-
}
|
|
8
|
-
export interface WorkingTreeInfo {
|
|
9
|
-
staged: number;
|
|
10
|
-
unstaged: number;
|
|
11
|
-
untracked: number;
|
|
12
|
-
conflicts: number;
|
|
13
|
-
}
|
|
14
|
-
export type PullRequestInfo = {
|
|
15
|
-
kind: 'found';
|
|
16
|
-
number: number;
|
|
17
|
-
title: string;
|
|
18
|
-
url: string;
|
|
19
|
-
state: 'OPEN' | 'CLOSED' | 'MERGED';
|
|
20
|
-
isDraft: boolean;
|
|
21
|
-
baseBranch: string;
|
|
22
|
-
} | {
|
|
23
|
-
kind: 'none';
|
|
24
|
-
} | {
|
|
25
|
-
kind: 'unavailable';
|
|
26
|
-
};
|
|
27
6
|
export interface AppRow {
|
|
28
7
|
path: string;
|
|
29
8
|
shortPath: string;
|
|
@@ -34,17 +13,14 @@ export interface AppRow {
|
|
|
34
13
|
upstreamUnavailable?: boolean;
|
|
35
14
|
workingTree?: WorkingTreeInfo;
|
|
36
15
|
pullRequest?: PullRequestInfo;
|
|
16
|
+
branchCreatedAtMs?: number;
|
|
37
17
|
invalidReason?: string;
|
|
38
18
|
}
|
|
39
19
|
export interface AppStatus {
|
|
40
|
-
kind: 'idle' | 'starting' | 'running' | 'stopping' | 'error';
|
|
20
|
+
kind: 'idle' | 'setting-up' | 'starting' | 'running' | 'stopping' | 'error';
|
|
41
21
|
message: string;
|
|
42
22
|
}
|
|
43
|
-
export
|
|
44
|
-
name: string;
|
|
45
|
-
path: string;
|
|
46
|
-
content: string;
|
|
47
|
-
}
|
|
23
|
+
export type AppLogEntry = LogEntry;
|
|
48
24
|
export interface AppModel {
|
|
49
25
|
repoName: string;
|
|
50
26
|
namespace: string;
|
|
@@ -52,21 +28,25 @@ export interface AppModel {
|
|
|
52
28
|
activePath: string | null;
|
|
53
29
|
activeBranch: string | null;
|
|
54
30
|
status: AppStatus;
|
|
31
|
+
setupAvailable: boolean;
|
|
32
|
+
editorAvailable: boolean;
|
|
33
|
+
logs: AppLogEntry[];
|
|
34
|
+
}
|
|
35
|
+
export interface AppLogRefresh {
|
|
55
36
|
logs: AppLogEntry[];
|
|
37
|
+
activePath: string | null;
|
|
38
|
+
activeBranch: string | null;
|
|
56
39
|
}
|
|
57
40
|
export interface AppActions {
|
|
41
|
+
setup: (worktreePath: string) => Promise<AppModel>;
|
|
58
42
|
start: (worktreePath: string) => Promise<AppModel>;
|
|
59
43
|
stop: () => Promise<AppModel>;
|
|
60
44
|
refresh: () => Promise<AppModel>;
|
|
61
|
-
refreshLogs: () => Promise<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
upstreamUnavailable: boolean;
|
|
66
|
-
workingTree?: WorkingTreeInfo;
|
|
45
|
+
refreshLogs: () => Promise<AppLogRefresh>;
|
|
46
|
+
openEditor: (worktreePath: string) => Promise<AppModel>;
|
|
47
|
+
openPullRequest: (worktreePath: string) => Promise<AppModel>;
|
|
48
|
+
deleteWorktree: (worktreePath: string) => Promise<AppModel>;
|
|
67
49
|
}
|
|
68
|
-
export declare function
|
|
69
|
-
export declare function toAppRow(mainWorktreePath: string, worktree: WorktreeRow, activePath: string | null, invalidReason: string | null, metadata: Pick<AppRow, 'upstream' | 'upstreamUnavailable' | 'workingTree' | 'pullRequest'>): AppRow;
|
|
50
|
+
export declare function toAppRow(mainWorktreePath: string, worktree: WorktreeRow, activePath: string | null, invalidReason: string | null, metadata: Pick<AppRow, 'upstream' | 'upstreamUnavailable' | 'workingTree' | 'pullRequest' | 'branchCreatedAtMs'>): AppRow;
|
|
70
51
|
export declare function buildInitialModel(cwd: string): Promise<AppModel>;
|
|
71
52
|
export declare function buildActions(cwd: string): Promise<AppActions>;
|
|
72
|
-
export {};
|