@ohzw/worktree-command-tui 0.1.4 → 0.1.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 +7 -3
- package/dist/app.d.ts +5 -2
- package/dist/app.js +210 -63
- package/dist/components/ActionPanel.js +9 -2
- package/dist/components/ContextBar.js +1 -0
- package/dist/components/HelpWindow.js +2 -0
- package/dist/components/WorktreeList.d.ts +4 -1
- package/dist/components/WorktreeList.js +7 -2
- package/dist/core/command-runner.d.ts +2 -1
- package/dist/core/command-runner.js +65 -2
- package/dist/core/config-lifecycle.d.ts +1 -1
- package/dist/core/config-lifecycle.js +83 -30
- package/dist/core/config.d.ts +2 -1
- package/dist/core/debounced-resize.d.ts +7 -0
- package/dist/core/debounced-resize.js +39 -0
- package/dist/core/git-metadata.d.ts +4 -0
- package/dist/core/git-metadata.js +9 -0
- package/dist/core/process-control.d.ts +1 -1
- package/dist/core/process-control.js +3 -1
- package/dist/core/runtime-state.d.ts +3 -1
- package/dist/core/runtime-state.js +32 -18
- package/dist/core/runtime.d.ts +3 -2
- package/dist/core/runtime.js +15 -8
- package/dist/core/session-store.d.ts +1 -0
- package/dist/core/session-store.js +33 -15
- package/dist/core/tui-interaction.d.ts +1 -0
- package/dist/core/tui-interaction.js +6 -0
- package/dist/core/worktree-projection.d.ts +9 -0
- package/dist/core/worktree-projection.js +13 -0
- package/dist/main.js +8 -16
- package/package.json +1 -1
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
const MAX_SESSION_BYTES = 16 * 1024;
|
|
4
3
|
function isSafeProcessId(value) {
|
|
5
4
|
return typeof value === 'number' && Number.isInteger(value) && value > 1;
|
|
6
5
|
}
|
|
7
|
-
function
|
|
6
|
+
function isSafePort(value) {
|
|
8
7
|
return typeof value === 'number' && Number.isInteger(value) && value > 0 && value <= 65535;
|
|
9
8
|
}
|
|
10
|
-
function
|
|
9
|
+
function normalizeSessionRecordPorts(port, ports) {
|
|
10
|
+
if (ports !== undefined && ports.length > 0) {
|
|
11
|
+
return ports;
|
|
12
|
+
}
|
|
13
|
+
if (port !== undefined) {
|
|
14
|
+
return [port];
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
function parseSessionRecord(value) {
|
|
11
19
|
if (typeof value !== 'object' || value === null) {
|
|
12
|
-
return
|
|
20
|
+
return null;
|
|
13
21
|
}
|
|
14
22
|
const record = value;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
typeof record.
|
|
18
|
-
|
|
19
|
-
isSafeProcessId(record.pgid)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const parsedPorts = normalizeSessionRecordPorts(record.port, Array.isArray(record.ports) ? record.ports : undefined);
|
|
24
|
+
if (typeof record.namespace !== 'string' ||
|
|
25
|
+
typeof record.worktreePath !== 'string' ||
|
|
26
|
+
typeof record.branch !== 'string' ||
|
|
27
|
+
!isSafeProcessId(record.pgid) ||
|
|
28
|
+
!isSafeProcessId(record.pid) ||
|
|
29
|
+
!isSafePort(record.port) ||
|
|
30
|
+
!parsedPorts.every(isSafePort) ||
|
|
31
|
+
parsedPorts.length === 0 ||
|
|
32
|
+
typeof record.logPath !== 'string' ||
|
|
33
|
+
typeof record.startedAt !== 'string') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return { ...record, ports: parsedPorts };
|
|
23
37
|
}
|
|
38
|
+
const MAX_SESSION_BYTES = 16 * 1024;
|
|
24
39
|
export function getSessionPaths(gitCommonDir, namespace) {
|
|
25
40
|
const baseDir = path.join(gitCommonDir, 'worktree-command-tui');
|
|
26
41
|
return {
|
|
@@ -42,8 +57,8 @@ export async function readSessionRecord(paths, { isSessionAlive }) {
|
|
|
42
57
|
if (source === null) {
|
|
43
58
|
return null;
|
|
44
59
|
}
|
|
45
|
-
const parsed = JSON.parse(source);
|
|
46
|
-
if (
|
|
60
|
+
const parsed = parseSessionRecord(JSON.parse(source));
|
|
61
|
+
if (parsed === null) {
|
|
47
62
|
await rm(paths.sessionFile, { force: true });
|
|
48
63
|
return null;
|
|
49
64
|
}
|
|
@@ -59,7 +74,10 @@ export async function readSessionRecord(paths, { isSessionAlive }) {
|
|
|
59
74
|
}
|
|
60
75
|
export async function writeSessionRecord(paths, record) {
|
|
61
76
|
await mkdir(paths.baseDir, { recursive: true });
|
|
62
|
-
await writeFile(paths.sessionFile, JSON.stringify(
|
|
77
|
+
await writeFile(paths.sessionFile, JSON.stringify({
|
|
78
|
+
...record,
|
|
79
|
+
ports: record.ports ?? [record.port],
|
|
80
|
+
}, null, 2));
|
|
63
81
|
}
|
|
64
82
|
export async function clearSessionRecord(paths) {
|
|
65
83
|
await rm(paths.sessionFile, { force: true });
|
|
@@ -26,6 +26,7 @@ export interface AsyncInteractionState {
|
|
|
26
26
|
export declare function getNextSelectedPath(rows: readonly AppRow[], currentPath: string | null): string | null;
|
|
27
27
|
export declare function getSelectedIndex(rows: readonly AppRow[], selectedPath: string | null): number;
|
|
28
28
|
export declare function clampSelectionIndex(nextIndex: number, rowCount: number): number | null;
|
|
29
|
+
export declare function wrapSelectionIndex(nextIndex: number, rowCount: number): number | null;
|
|
29
30
|
export declare function decideEnterInteraction(selected: AppRow | undefined, activePath: string | null): EnterInteractionDecision;
|
|
30
31
|
export declare function decideSetupInteraction(selected: AppRow | undefined, setupAvailable: boolean): SetupInteractionDecision;
|
|
31
32
|
export declare function shouldApplyAsyncResult(state: AsyncInteractionState): boolean;
|
|
@@ -20,6 +20,12 @@ export function clampSelectionIndex(nextIndex, rowCount) {
|
|
|
20
20
|
}
|
|
21
21
|
return Math.min(Math.max(nextIndex, 0), rowCount - 1);
|
|
22
22
|
}
|
|
23
|
+
export function wrapSelectionIndex(nextIndex, rowCount) {
|
|
24
|
+
if (rowCount <= 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return ((nextIndex % rowCount) + rowCount) % rowCount;
|
|
28
|
+
}
|
|
23
29
|
export function decideEnterInteraction(selected, activePath) {
|
|
24
30
|
if (selected === undefined) {
|
|
25
31
|
return { kind: 'ignore' };
|
|
@@ -41,6 +41,14 @@ export type UpstreamProjection = {
|
|
|
41
41
|
ahead: number;
|
|
42
42
|
behind: number;
|
|
43
43
|
};
|
|
44
|
+
export type HeadCommitProjection = {
|
|
45
|
+
kind: 'unavailable';
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'found';
|
|
48
|
+
hash?: string;
|
|
49
|
+
message?: string;
|
|
50
|
+
label: string;
|
|
51
|
+
};
|
|
44
52
|
export type WorkingTreePartKind = 'staged' | 'unstaged' | 'untracked' | 'conflicts';
|
|
45
53
|
export type WorkingTreeProjection = {
|
|
46
54
|
kind: 'unavailable';
|
|
@@ -68,6 +76,7 @@ export type PullRequestProjection = {
|
|
|
68
76
|
};
|
|
69
77
|
export declare function sanitizeInlineText(value: string): string;
|
|
70
78
|
export declare function projectWorktreeListRow(row: AppRow, isSelected: boolean): WorktreeListRowProjection;
|
|
79
|
+
export declare function projectHeadCommit(row: AppRow): HeadCommitProjection;
|
|
71
80
|
export declare function getOrderedNonActiveTags(tags: readonly string[]): TagProjection[];
|
|
72
81
|
export declare function projectAction(row: AppRow, activePath: string | null): ActionProjection;
|
|
73
82
|
export declare function projectNote(row: AppRow): NoteProjection;
|
|
@@ -43,6 +43,19 @@ export function projectWorktreeListRow(row, isSelected) {
|
|
|
43
43
|
isMain: hasTag(row, 'main'),
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
export function projectHeadCommit(row) {
|
|
47
|
+
const hash = sanitizeInlineText(row.headSha ?? '');
|
|
48
|
+
const message = sanitizeInlineText(row.headCommit?.message ?? '');
|
|
49
|
+
if (hash.length === 0 && message.length === 0) {
|
|
50
|
+
return { kind: 'unavailable' };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: 'found',
|
|
54
|
+
hash: hash.length === 0 ? undefined : hash,
|
|
55
|
+
message: message.length === 0 ? undefined : message,
|
|
56
|
+
label: [hash, message].filter(part => part.length > 0).join(' '),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
46
59
|
export function getOrderedNonActiveTags(tags) {
|
|
47
60
|
return tags
|
|
48
61
|
.filter(tag => tag !== 'active')
|
package/dist/main.js
CHANGED
|
@@ -6,8 +6,9 @@ import { sanitizeInlineText } from './core/worktree-projection.js';
|
|
|
6
6
|
import { ThemeProvider } from '@inkjs/ui';
|
|
7
7
|
import { render } from 'ink';
|
|
8
8
|
import { APP_RENDER_OPTIONS } from './render-options.js';
|
|
9
|
-
import { App } from './app.js';
|
|
9
|
+
import { App, RESIZE_DEBOUNCE_MS } from './app.js';
|
|
10
10
|
import { buildActions, buildInitialModel } from './core/runtime.js';
|
|
11
|
+
import { debounceResizeListeners } from './core/debounced-resize.js';
|
|
11
12
|
import { appTheme } from './ui-theme.js';
|
|
12
13
|
const cwd = process.cwd();
|
|
13
14
|
const args = process.argv.slice(2);
|
|
@@ -66,31 +67,22 @@ if (subcommand !== undefined) {
|
|
|
66
67
|
console.error(`Unknown command: ${sanitizeInlineText(subcommand)}`);
|
|
67
68
|
process.exit(1);
|
|
68
69
|
}
|
|
70
|
+
let restoreStdoutResize = () => { };
|
|
69
71
|
try {
|
|
70
72
|
const [initialModel, actions] = await Promise.all([buildInitialModel(cwd), buildActions(cwd)]);
|
|
71
73
|
const createApp = () => (_jsx(ThemeProvider, { theme: appTheme, children: _jsx(App, { initialModel: initialModel, actions: actions }) }));
|
|
74
|
+
if (process.stdout.isTTY) {
|
|
75
|
+
restoreStdoutResize = debounceResizeListeners(process.stdout, RESIZE_DEBOUNCE_MS);
|
|
76
|
+
}
|
|
72
77
|
const instance = render(createApp(), APP_RENDER_OPTIONS);
|
|
73
|
-
let repaintTimer;
|
|
74
|
-
const repaintAfterResize = () => {
|
|
75
|
-
// Give Ink/useWindowSize one tick to observe the new size, then force a fresh root render.
|
|
76
|
-
if (repaintTimer) {
|
|
77
|
-
clearTimeout(repaintTimer);
|
|
78
|
-
}
|
|
79
|
-
repaintTimer = setTimeout(() => {
|
|
80
|
-
instance.rerender(createApp());
|
|
81
|
-
}, 25);
|
|
82
|
-
};
|
|
83
78
|
if (process.stdout.isTTY) {
|
|
84
|
-
process.stdout.on('resize', repaintAfterResize);
|
|
85
79
|
void instance.waitUntilExit().finally(() => {
|
|
86
|
-
|
|
87
|
-
clearTimeout(repaintTimer);
|
|
88
|
-
}
|
|
89
|
-
process.stdout.off('resize', repaintAfterResize);
|
|
80
|
+
restoreStdoutResize();
|
|
90
81
|
});
|
|
91
82
|
}
|
|
92
83
|
}
|
|
93
84
|
catch (error) {
|
|
85
|
+
restoreStdoutResize();
|
|
94
86
|
console.error(sanitizeInlineText(describeError(error)));
|
|
95
87
|
process.exit(1);
|
|
96
88
|
}
|