@solaqua/gji 0.11.0 → 0.12.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 +82 -496
- package/dist/clean.js +2 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +40 -4
- package/dist/context-card.d.ts +1 -0
- package/dist/context-card.js +25 -0
- package/dist/doctor.js +21 -0
- package/dist/done.js +2 -0
- package/dist/gji-bundle.mjs +988 -350
- package/dist/go.d.ts +1 -0
- package/dist/go.js +8 -0
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.js +7 -1
- package/dist/hub.d.ts +31 -0
- package/dist/hub.js +287 -0
- package/dist/ls.js +9 -1
- package/dist/new.d.ts +1 -0
- package/dist/new.js +9 -0
- package/dist/pr-open.js +2 -10
- package/dist/pull-requests.d.ts +1 -0
- package/dist/pull-requests.js +32 -3
- package/dist/remove.js +2 -0
- package/dist/repo.d.ts +1 -0
- package/dist/repo.js +4 -0
- package/dist/run-hook.js +2 -0
- package/dist/shell-completion.js +11 -3
- package/dist/slots.d.ts +8 -0
- package/dist/slots.js +61 -0
- package/dist/status.d.ts +2 -0
- package/dist/status.js +9 -1
- package/dist/task-command.d.ts +9 -0
- package/dist/task-command.js +29 -0
- package/dist/task.d.ts +9 -0
- package/dist/task.js +37 -0
- package/dist/terminal-text.d.ts +4 -0
- package/dist/terminal-text.js +72 -0
- package/dist/worktree-bootstrap.js +3 -0
- package/dist/worktree-info.d.ts +6 -1
- package/dist/worktree-info.js +14 -4
- package/dist/worktree-picker.d.ts +3 -3
- package/dist/worktree-picker.js +71 -82
- package/dist/worktree-sources.d.ts +1 -0
- package/dist/worktree-sources.js +57 -11
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.1 +1 -1
- package/man/man1/gji-go.1 +4 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +4 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +16 -0
- package/man/man1/gji-undo.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +5 -1
- package/package.json +1 -1
package/dist/shell-completion.js
CHANGED
|
@@ -29,6 +29,7 @@ const TOP_LEVEL_COMMANDS = [
|
|
|
29
29
|
{ name: "jump", description: "alias of go" },
|
|
30
30
|
{ name: "root", description: "print the main repository root path" },
|
|
31
31
|
{ name: "status", description: "summarize repository and worktree health" },
|
|
32
|
+
{ name: "task", description: "show or update the current worktree task" },
|
|
32
33
|
{ name: "sync", description: "fetch and update one or all worktrees" },
|
|
33
34
|
{
|
|
34
35
|
name: "sync-files",
|
|
@@ -140,7 +141,7 @@ _gji_completion() {
|
|
|
140
141
|
cur="\${COMP_WORDS[COMP_CWORD]:-}"
|
|
141
142
|
|
|
142
143
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
143
|
-
|
|
144
|
+
COMPREPLY=( $(compgen -W "${topLevelCommands} --json --help" -- "$cur") )
|
|
144
145
|
return 0
|
|
145
146
|
fi
|
|
146
147
|
|
|
@@ -148,7 +149,7 @@ _gji_completion() {
|
|
|
148
149
|
|
|
149
150
|
case "$command_name" in
|
|
150
151
|
new)
|
|
151
|
-
|
|
152
|
+
COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --task --force --open --editor --dry-run --json --help" -- "$cur") )
|
|
152
153
|
;;
|
|
153
154
|
done)
|
|
154
155
|
COMPREPLY=( $(compgen -W "--force --keep-branch --json --help" -- "$cur") )
|
|
@@ -389,6 +390,7 @@ complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a de
|
|
|
389
390
|
complete -c gji -n '__fish_seen_subcommand_from new' -l from-current -d 'base the new branch on the current worktree instead of the main worktree'
|
|
390
391
|
complete -c gji -n '__fish_seen_subcommand_from new' -l take -d 'move current uncommitted changes into the new worktree'
|
|
391
392
|
complete -c gji -n '__fish_seen_subcommand_from new' -l copy -d 'copy current uncommitted changes instead of moving them (requires --take)'
|
|
393
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l task -r -d 'record the purpose of the new worktree'
|
|
392
394
|
complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
|
|
393
395
|
complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
|
|
394
396
|
complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
|
|
@@ -426,11 +428,14 @@ complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branch
|
|
|
426
428
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
|
|
427
429
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -l root -d 'navigate to the main repository root'
|
|
428
430
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -l json -d 'emit JSON for an existing worktree destination'
|
|
431
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l quiet -d 'suppress the context card'
|
|
429
432
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
430
433
|
|
|
431
434
|
complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
|
|
432
435
|
|
|
433
436
|
complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print repository and worktree health as JSON'
|
|
437
|
+
complete -c gji -n '__fish_seen_subcommand_from task' -l clear -d 'clear the current worktree task'
|
|
438
|
+
complete -c gji -n '__fish_seen_subcommand_from task' -l json -d 'emit task metadata as JSON'
|
|
434
439
|
|
|
435
440
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
|
|
436
441
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
@@ -456,6 +461,8 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
|
|
|
456
461
|
|
|
457
462
|
${hookLines}
|
|
458
463
|
|
|
464
|
+
complete -c gji -n '__fish_use_subcommand' -l json -d 'emit the repository hub as JSON'
|
|
465
|
+
|
|
459
466
|
complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
|
|
460
467
|
complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
461
468
|
|
|
@@ -534,6 +541,7 @@ local -a command_entries worktree_branches
|
|
|
534
541
|
|
|
535
542
|
command_entries=(
|
|
536
543
|
${commandLines}
|
|
544
|
+
'--json:emit the repository hub as JSON'
|
|
537
545
|
)
|
|
538
546
|
|
|
539
547
|
if (( CURRENT == 2 )); then
|
|
@@ -543,7 +551,7 @@ fi
|
|
|
543
551
|
|
|
544
552
|
case "\${words[2]}" in
|
|
545
553
|
new)
|
|
546
|
-
_arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
554
|
+
_arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--task[record the purpose of the new worktree]:description:' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
547
555
|
;;
|
|
548
556
|
done)
|
|
549
557
|
_arguments '--force[remove dirty or unmerged worktrees without prompting]' '--keep-branch[remove the worktree but preserve its branch]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
package/dist/slots.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SlotStore {
|
|
2
|
+
[worktreePath: string]: number;
|
|
3
|
+
}
|
|
4
|
+
export declare function SLOTS_FILE_PATH(home?: string): string;
|
|
5
|
+
export declare function loadSlots(home?: string): Promise<SlotStore>;
|
|
6
|
+
export declare function assignWorktreeSlot(repoRoot: string, worktreePath: string, home?: string): Promise<number>;
|
|
7
|
+
export declare function getWorktreeSlot(worktreePath: string, home?: string): Promise<number | null>;
|
|
8
|
+
export declare function releaseWorktreeSlot(worktreePath: string, home?: string): Promise<void>;
|
package/dist/slots.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
|
|
5
|
+
export function SLOTS_FILE_PATH(home = homedir()) {
|
|
6
|
+
const configDir = process.env.GJI_CONFIG_DIR;
|
|
7
|
+
return configDir
|
|
8
|
+
? join(resolve(configDir), "slots.json")
|
|
9
|
+
: join(home, GLOBAL_CONFIG_DIRECTORY, "slots.json");
|
|
10
|
+
}
|
|
11
|
+
export async function loadSlots(home = homedir()) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await readFile(SLOTS_FILE_PATH(home), "utf8"));
|
|
14
|
+
if (!isSlotStore(parsed))
|
|
15
|
+
return {};
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function assignWorktreeSlot(repoRoot, worktreePath, home = homedir()) {
|
|
23
|
+
const slots = await loadSlots(home);
|
|
24
|
+
const existing = slots[worktreePath];
|
|
25
|
+
if (existing !== undefined)
|
|
26
|
+
return existing;
|
|
27
|
+
if (worktreePath !== repoRoot && slots[repoRoot] === undefined)
|
|
28
|
+
slots[repoRoot] = 0;
|
|
29
|
+
const used = new Set(Object.values(slots));
|
|
30
|
+
const slot = worktreePath === repoRoot ? 0 : nextAvailableSlot(used);
|
|
31
|
+
slots[worktreePath] = slot;
|
|
32
|
+
await saveSlots(slots, home);
|
|
33
|
+
return slot;
|
|
34
|
+
}
|
|
35
|
+
export async function getWorktreeSlot(worktreePath, home = homedir()) {
|
|
36
|
+
const slot = (await loadSlots(home))[worktreePath];
|
|
37
|
+
return slot === undefined ? null : slot;
|
|
38
|
+
}
|
|
39
|
+
export async function releaseWorktreeSlot(worktreePath, home = homedir()) {
|
|
40
|
+
const slots = await loadSlots(home);
|
|
41
|
+
if (!(worktreePath in slots))
|
|
42
|
+
return;
|
|
43
|
+
delete slots[worktreePath];
|
|
44
|
+
await saveSlots(slots, home);
|
|
45
|
+
}
|
|
46
|
+
async function saveSlots(slots, home) {
|
|
47
|
+
const path = SLOTS_FILE_PATH(home);
|
|
48
|
+
await mkdir(dirname(path), { recursive: true });
|
|
49
|
+
await writeFile(path, `${JSON.stringify(slots, null, 2)}\n`, "utf8");
|
|
50
|
+
}
|
|
51
|
+
function nextAvailableSlot(used) {
|
|
52
|
+
let slot = 1;
|
|
53
|
+
while (used.has(slot))
|
|
54
|
+
slot += 1;
|
|
55
|
+
return slot;
|
|
56
|
+
}
|
|
57
|
+
function isSlotStore(value) {
|
|
58
|
+
return (typeof value === "object" &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
Object.values(value).every((slot) => Number.isInteger(slot) && slot >= 0));
|
|
61
|
+
}
|
package/dist/status.d.ts
CHANGED
package/dist/status.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readWorktreeHealth } from "./git.js";
|
|
2
2
|
import { comparePaths } from "./paths.js";
|
|
3
3
|
import { detectRepository, listWorktrees } from "./repo.js";
|
|
4
|
+
import { getWorktreeSlot } from "./slots.js";
|
|
5
|
+
import { readTask } from "./task.js";
|
|
4
6
|
export async function runStatusCommand(options) {
|
|
5
7
|
const repository = await detectRepository(options.cwd);
|
|
6
8
|
const worktrees = sortWorktreesByPath(await listWorktrees(options.cwd));
|
|
@@ -36,12 +38,18 @@ export function formatStatusJson(repoRoot, currentRoot, rows) {
|
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
async function buildStatusRow(worktree) {
|
|
39
|
-
const health = await
|
|
41
|
+
const [health, slot, task] = await Promise.all([
|
|
42
|
+
readWorktreeHealth(worktree.path),
|
|
43
|
+
getWorktreeSlot(worktree.path),
|
|
44
|
+
readTask(worktree.path),
|
|
45
|
+
]);
|
|
40
46
|
return {
|
|
41
47
|
branch: worktree.branch,
|
|
42
48
|
current: worktree.isCurrent,
|
|
43
49
|
path: worktree.path,
|
|
50
|
+
slot,
|
|
44
51
|
status: health.status,
|
|
52
|
+
task: task?.task ?? null,
|
|
45
53
|
upstream: buildUpstreamState(worktree.branch, health),
|
|
46
54
|
};
|
|
47
55
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface TaskCommandOptions {
|
|
2
|
+
clear?: boolean;
|
|
3
|
+
cwd: string;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
task?: string;
|
|
6
|
+
stderr: (chunk: string) => void;
|
|
7
|
+
stdout: (chunk: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function runTaskCommand(options: TaskCommandOptions): Promise<number>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { detectRepository } from "./repo.js";
|
|
2
|
+
import { clearTask, readTask, writeTask } from "./task.js";
|
|
3
|
+
export async function runTaskCommand(options) {
|
|
4
|
+
const repository = await detectRepository(options.cwd);
|
|
5
|
+
if (options.clear && options.task !== undefined)
|
|
6
|
+
return emitTaskError(options, "--clear cannot be combined with a task");
|
|
7
|
+
if (options.clear)
|
|
8
|
+
await clearTask(repository.currentRoot);
|
|
9
|
+
else if (options.task !== undefined)
|
|
10
|
+
await writeTask(repository.currentRoot, options.task);
|
|
11
|
+
const task = await readTask(repository.currentRoot);
|
|
12
|
+
if (options.json) {
|
|
13
|
+
options.stdout(`${JSON.stringify({ task: task?.task ?? null }, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
else if (task) {
|
|
16
|
+
options.stdout(`${task.task}\n`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
options.stdout('no task set — set one: gji task "..."\n');
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
function emitTaskError(options, message) {
|
|
24
|
+
if (options.json)
|
|
25
|
+
options.stderr(`${JSON.stringify({ error: message })}\n`);
|
|
26
|
+
else
|
|
27
|
+
options.stderr(`gji task: ${message}\n`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
package/dist/task.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type TaskSource = "manual" | "pr";
|
|
2
|
+
export interface TaskMetadata {
|
|
3
|
+
source: TaskSource;
|
|
4
|
+
task: string;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function readTask(worktreePath: string): Promise<TaskMetadata | null>;
|
|
8
|
+
export declare function writeTask(worktreePath: string, task: string, source?: TaskSource): Promise<TaskMetadata>;
|
|
9
|
+
export declare function clearTask(worktreePath: string): Promise<void>;
|
package/dist/task.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { worktreeGitDir } from "./repo.js";
|
|
4
|
+
export async function readTask(worktreePath) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(await readFile(await taskPath(worktreePath), "utf8"));
|
|
7
|
+
return isTaskMetadata(parsed) ? parsed : null;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function writeTask(worktreePath, task, source = "manual") {
|
|
14
|
+
const metadata = { source, task, updatedAt: Date.now() };
|
|
15
|
+
const path = await taskPath(worktreePath);
|
|
16
|
+
await writeFile(path, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
17
|
+
return metadata;
|
|
18
|
+
}
|
|
19
|
+
export async function clearTask(worktreePath) {
|
|
20
|
+
try {
|
|
21
|
+
await unlink(await taskPath(worktreePath));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Missing task metadata is already the desired state.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function taskPath(worktreePath) {
|
|
28
|
+
return join(await worktreeGitDir(worktreePath), "gji-task.json");
|
|
29
|
+
}
|
|
30
|
+
function isTaskMetadata(value) {
|
|
31
|
+
if (typeof value !== "object" || value === null)
|
|
32
|
+
return false;
|
|
33
|
+
const candidate = value;
|
|
34
|
+
return (typeof candidate.task === "string" &&
|
|
35
|
+
(candidate.source === "manual" || candidate.source === "pr") &&
|
|
36
|
+
typeof candidate.updatedAt === "number");
|
|
37
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function sanitizeTerminalText(value: string): string;
|
|
2
|
+
export declare function middleEllipsize(value: string, maxLength: number): string;
|
|
3
|
+
export declare function startEllipsize(value: string, maxLength: number): string;
|
|
4
|
+
export declare function terminalWidth(value: string): number;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function sanitizeTerminalText(value) {
|
|
2
|
+
return Array.from(value, (character) => {
|
|
3
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
4
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)
|
|
5
|
+
? " "
|
|
6
|
+
: character;
|
|
7
|
+
}).join("");
|
|
8
|
+
}
|
|
9
|
+
export function middleEllipsize(value, maxLength) {
|
|
10
|
+
if (terminalWidth(value) <= maxLength)
|
|
11
|
+
return value;
|
|
12
|
+
if (maxLength <= 1)
|
|
13
|
+
return "…";
|
|
14
|
+
const keep = maxLength - 1;
|
|
15
|
+
const start = takeTerminalColumns(value, Math.ceil(keep / 2), "start");
|
|
16
|
+
const end = takeTerminalColumns(value, Math.floor(keep / 2), "end");
|
|
17
|
+
return `${start}…${end}`;
|
|
18
|
+
}
|
|
19
|
+
export function startEllipsize(value, maxLength) {
|
|
20
|
+
if (terminalWidth(value) <= maxLength)
|
|
21
|
+
return value;
|
|
22
|
+
if (maxLength <= 1)
|
|
23
|
+
return "…";
|
|
24
|
+
return `…${takeTerminalColumns(value, maxLength - 1, "end")}`;
|
|
25
|
+
}
|
|
26
|
+
export function terminalWidth(value) {
|
|
27
|
+
let width = 0;
|
|
28
|
+
for (const character of Array.from(value)) {
|
|
29
|
+
width += characterTerminalWidth(character);
|
|
30
|
+
}
|
|
31
|
+
return width;
|
|
32
|
+
}
|
|
33
|
+
function takeTerminalColumns(value, maxWidth, direction) {
|
|
34
|
+
const characters = direction === "start" ? Array.from(value) : Array.from(value).reverse();
|
|
35
|
+
const kept = [];
|
|
36
|
+
let width = 0;
|
|
37
|
+
for (const character of characters) {
|
|
38
|
+
const characterWidth = characterTerminalWidth(character);
|
|
39
|
+
if (width + characterWidth > maxWidth)
|
|
40
|
+
break;
|
|
41
|
+
kept.push(character);
|
|
42
|
+
width += characterWidth;
|
|
43
|
+
}
|
|
44
|
+
return direction === "start" ? kept.join("") : kept.reverse().join("");
|
|
45
|
+
}
|
|
46
|
+
function characterTerminalWidth(character) {
|
|
47
|
+
const codePoint = character.codePointAt(0);
|
|
48
|
+
if (codePoint === undefined)
|
|
49
|
+
return 0;
|
|
50
|
+
if (isZeroWidthCodePoint(codePoint) || /\p{Mark}/u.test(character))
|
|
51
|
+
return 0;
|
|
52
|
+
return isWideCodePoint(codePoint) ? 2 : 1;
|
|
53
|
+
}
|
|
54
|
+
function isZeroWidthCodePoint(codePoint) {
|
|
55
|
+
return (codePoint === 0 ||
|
|
56
|
+
codePoint === 0x200d ||
|
|
57
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f));
|
|
58
|
+
}
|
|
59
|
+
function isWideCodePoint(codePoint) {
|
|
60
|
+
return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
61
|
+
codePoint === 0x2329 ||
|
|
62
|
+
codePoint === 0x232a ||
|
|
63
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
|
|
64
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
65
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
66
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
67
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
68
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
69
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
70
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
|
|
71
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
72
|
+
}
|
|
@@ -4,6 +4,7 @@ import { cloneDir } from "./dir-clone.js";
|
|
|
4
4
|
import { syncFiles } from "./file-sync.js";
|
|
5
5
|
import { extractHooks, runHook } from "./hooks.js";
|
|
6
6
|
import { maybeRunInstallPrompt, } from "./install-prompt.js";
|
|
7
|
+
import { assignWorktreeSlot } from "./slots.js";
|
|
7
8
|
import { executeSyncDirectoryPlan, } from "./sync-directories.js";
|
|
8
9
|
import { prepareSyncDirectoryPlan } from "./sync-plan.js";
|
|
9
10
|
export async function bootstrapWorktree(options) {
|
|
@@ -73,10 +74,12 @@ export async function bootstrapWorktree(options) {
|
|
|
73
74
|
await maybeRunInstallPrompt(options.worktreePath, options.repoRoot, options.config, options.reporter.write, options.installDependencies, options.nonInteractive);
|
|
74
75
|
}
|
|
75
76
|
const hooks = extractHooks(options.config);
|
|
77
|
+
const slot = await assignWorktreeSlot(options.repoRoot, options.worktreePath);
|
|
76
78
|
await runHook(hooks["after-create"], options.worktreePath, {
|
|
77
79
|
branch: options.branch,
|
|
78
80
|
path: options.worktreePath,
|
|
79
81
|
repo: basename(options.repoRoot),
|
|
82
|
+
slot,
|
|
80
83
|
}, options.reporter.write, options.json
|
|
81
84
|
? () => undefined
|
|
82
85
|
: (options.commandStdout ?? ((chunk) => process.stdout.write(chunk))));
|
package/dist/worktree-info.d.ts
CHANGED
|
@@ -2,14 +2,18 @@ import { type WorktreeHealth } from "./git.js";
|
|
|
2
2
|
import type { WorktreeEntry } from "./repo.js";
|
|
3
3
|
export interface WorktreeInfo extends WorktreeEntry {
|
|
4
4
|
lastCommitTimestamp: number | null;
|
|
5
|
+
slot: number | null;
|
|
5
6
|
status: WorktreeHealth["status"] | "unknown";
|
|
7
|
+
task: string | null;
|
|
6
8
|
upstream: UpstreamState;
|
|
7
9
|
}
|
|
8
10
|
export interface SerializedWorktreeInfo {
|
|
9
11
|
branch: string | null;
|
|
10
12
|
lastCommitTimestamp: number | null;
|
|
11
13
|
path: string;
|
|
14
|
+
slot: number | null;
|
|
12
15
|
status: WorktreeInfo["status"];
|
|
16
|
+
task: string | null;
|
|
13
17
|
upstream: UpstreamState;
|
|
14
18
|
}
|
|
15
19
|
export type UpstreamState = {
|
|
@@ -26,7 +30,8 @@ export type UpstreamState = {
|
|
|
26
30
|
kind: "unknown";
|
|
27
31
|
};
|
|
28
32
|
export declare function readWorktreeInfos(worktrees: WorktreeEntry[]): Promise<WorktreeInfo[]>;
|
|
33
|
+
export declare function readWorktreeInfo(worktree: WorktreeEntry): Promise<WorktreeInfo>;
|
|
29
34
|
export declare function serializeWorktreeInfo(info: WorktreeInfo): SerializedWorktreeInfo;
|
|
30
35
|
export declare function formatUpstreamState(upstream: UpstreamState): string;
|
|
31
36
|
export declare function formatLastCommit(timestampSeconds: number | null): string;
|
|
32
|
-
export declare function formatRelativeAge(timestampSeconds: number): string;
|
|
37
|
+
export declare function formatRelativeAge(timestampSeconds: number, nowSeconds?: number): string;
|
package/dist/worktree-info.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readBranchLastCommitTimestamp, readWorktreeHealth, } from "./git.js";
|
|
2
|
+
import { getWorktreeSlot } from "./slots.js";
|
|
3
|
+
import { readTask } from "./task.js";
|
|
2
4
|
const MAX_WORKTREE_INFO_READ_CONCURRENCY = 8;
|
|
3
5
|
export async function readWorktreeInfos(worktrees) {
|
|
4
6
|
return mapWithConcurrency(worktrees, MAX_WORKTREE_INFO_READ_CONCURRENCY, readWorktreeInfo);
|
|
@@ -18,19 +20,25 @@ async function mapWithConcurrency(items, limit, mapper) {
|
|
|
18
20
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
|
|
19
21
|
return results;
|
|
20
22
|
}
|
|
21
|
-
async function readWorktreeInfo(worktree) {
|
|
22
|
-
const [healthResult, lastCommitResult] = await Promise.allSettled([
|
|
23
|
+
export async function readWorktreeInfo(worktree) {
|
|
24
|
+
const [healthResult, lastCommitResult, slotResult, taskResult] = await Promise.allSettled([
|
|
23
25
|
readWorktreeHealth(worktree.path),
|
|
24
26
|
worktree.branch === null
|
|
25
27
|
? null
|
|
26
28
|
: readBranchLastCommitTimestamp(worktree.path, worktree.branch),
|
|
29
|
+
getWorktreeSlot(worktree.path),
|
|
30
|
+
readTask(worktree.path),
|
|
27
31
|
]);
|
|
28
32
|
const health = healthResult.status === "fulfilled" ? healthResult.value : null;
|
|
29
33
|
const lastCommitTimestamp = lastCommitResult.status === "fulfilled" ? lastCommitResult.value : null;
|
|
34
|
+
const slot = slotResult.status === "fulfilled" ? slotResult.value : null;
|
|
35
|
+
const task = taskResult.status === "fulfilled" ? (taskResult.value?.task ?? null) : null;
|
|
30
36
|
return {
|
|
31
37
|
...worktree,
|
|
32
38
|
lastCommitTimestamp,
|
|
39
|
+
slot,
|
|
33
40
|
status: health?.status ?? "unknown",
|
|
41
|
+
task,
|
|
34
42
|
upstream: buildUpstreamState(worktree.branch, health),
|
|
35
43
|
};
|
|
36
44
|
}
|
|
@@ -58,7 +66,9 @@ export function serializeWorktreeInfo(info) {
|
|
|
58
66
|
branch: info.branch,
|
|
59
67
|
lastCommitTimestamp: info.lastCommitTimestamp,
|
|
60
68
|
path: info.path,
|
|
69
|
+
slot: info.slot,
|
|
61
70
|
status: info.status,
|
|
71
|
+
task: info.task,
|
|
62
72
|
upstream: info.upstream,
|
|
63
73
|
};
|
|
64
74
|
}
|
|
@@ -94,8 +104,8 @@ export function formatLastCommit(timestampSeconds) {
|
|
|
94
104
|
? "n/a"
|
|
95
105
|
: formatRelativeAge(timestampSeconds);
|
|
96
106
|
}
|
|
97
|
-
export function formatRelativeAge(timestampSeconds) {
|
|
98
|
-
const ageSeconds = Math.max(0,
|
|
107
|
+
export function formatRelativeAge(timestampSeconds, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
108
|
+
const ageSeconds = Math.max(0, nowSeconds - timestampSeconds);
|
|
99
109
|
const units = [
|
|
100
110
|
{ label: "y", seconds: 365 * 24 * 60 * 60 },
|
|
101
111
|
{ label: "mo", seconds: 30 * 24 * 60 * 60 },
|
|
@@ -7,9 +7,9 @@ export interface WorktreePromptEntry extends WorktreeEntry {
|
|
|
7
7
|
group: "recent" | "other";
|
|
8
8
|
label: string;
|
|
9
9
|
metadata?: string | null;
|
|
10
|
-
|
|
11
|
-
pullRequestUrls?: string[];
|
|
10
|
+
pullRequests?: PullRequestInfo[];
|
|
12
11
|
repoName: string;
|
|
12
|
+
task?: string | null;
|
|
13
13
|
}
|
|
14
14
|
export type QueryWorktreePullRequests = (repoRoot: string, sourceBranch: string) => Promise<PullRequestInfo[]>;
|
|
15
15
|
export type QueryRepositoryPullRequests = (repoRoot: string) => Promise<PullRequestInfo[]>;
|
|
@@ -47,4 +47,4 @@ export declare function promptForSingleWorktree(message: string, worktrees: Work
|
|
|
47
47
|
scope?: WorktreePromptScope;
|
|
48
48
|
}): Promise<string | null>;
|
|
49
49
|
export declare function promptForMultipleWorktrees(message: string, worktrees: WorktreePromptEntry[], io?: WorktreePickerIO): Promise<string[] | null>;
|
|
50
|
-
export declare function formatWorktreeBranchLabel(branch: string | null,
|
|
50
|
+
export declare function formatWorktreeBranchLabel(branch: string | null, pullRequests?: PullRequestInfo[]): string;
|