@solaqua/gji 0.12.2 → 0.12.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.
Files changed (55) hide show
  1. package/dist/bootstrap-output.d.ts +3 -2
  2. package/dist/bootstrap-output.js +1 -16
  3. package/dist/bootstrap-preview.d.ts +2 -3
  4. package/dist/bootstrap-preview.js +1 -11
  5. package/dist/cli.js +5 -1
  6. package/dist/config.d.ts +3 -13
  7. package/dist/config.js +12 -59
  8. package/dist/dependency-bootstrap.d.ts +11 -45
  9. package/dist/dependency-bootstrap.js +142 -608
  10. package/dist/gji-bundle.mjs +567 -2367
  11. package/dist/new.d.ts +4 -5
  12. package/dist/new.js +10 -45
  13. package/dist/pr.d.ts +4 -5
  14. package/dist/pr.js +9 -69
  15. package/dist/shell-completion.js +7 -5
  16. package/dist/uv-validation.d.ts +4 -0
  17. package/dist/uv-validation.js +210 -0
  18. package/dist/worktree-bootstrap.d.ts +5 -16
  19. package/dist/worktree-bootstrap.js +1 -24
  20. package/man/man1/gji-back.1 +1 -1
  21. package/man/man1/gji-clean.1 +1 -1
  22. package/man/man1/gji-completion.1 +1 -1
  23. package/man/man1/gji-config.1 +1 -1
  24. package/man/man1/gji-doctor.1 +1 -1
  25. package/man/man1/gji-done.1 +1 -1
  26. package/man/man1/gji-go.1 +1 -1
  27. package/man/man1/gji-history.1 +1 -1
  28. package/man/man1/gji-init.1 +1 -1
  29. package/man/man1/gji-ls.1 +1 -1
  30. package/man/man1/gji-new.1 +8 -5
  31. package/man/man1/gji-open.1 +1 -1
  32. package/man/man1/gji-pr.1 +6 -3
  33. package/man/man1/gji-remove.1 +1 -1
  34. package/man/man1/gji-root.1 +1 -1
  35. package/man/man1/gji-run-hook.1 +1 -1
  36. package/man/man1/gji-status.1 +1 -1
  37. package/man/man1/gji-sync-files.1 +1 -1
  38. package/man/man1/gji-sync.1 +1 -1
  39. package/man/man1/gji-task.1 +1 -1
  40. package/man/man1/gji-undo.1 +1 -1
  41. package/man/man1/gji-warp.1 +1 -1
  42. package/man/man1/gji.1 +2 -2
  43. package/package.json +1 -1
  44. package/dist/dependency-bootstrap-prompt.d.ts +0 -24
  45. package/dist/dependency-bootstrap-prompt.js +0 -103
  46. package/dist/dir-clone.d.ts +0 -32
  47. package/dist/dir-clone.js +0 -641
  48. package/dist/install-prompt.d.ts +0 -12
  49. package/dist/install-prompt.js +0 -127
  50. package/dist/package-manager.d.ts +0 -5
  51. package/dist/package-manager.js +0 -159
  52. package/dist/sync-directories.d.ts +0 -29
  53. package/dist/sync-directories.js +0 -77
  54. package/dist/sync-plan.d.ts +0 -16
  55. package/dist/sync-plan.js +0 -128
@@ -1,127 +0,0 @@
1
- import { isCancel, select } from "@clack/prompts";
2
- import { runCommand } from "./command-runner.js";
3
- import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey, } from "./config.js";
4
- import { isHeadless } from "./headless.js";
5
- import { extractHooks } from "./hooks.js";
6
- import { detectPackageManager, } from "./package-manager.js";
7
- export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
8
- // Skip in non-interactive mode — no prompt can be shown.
9
- if (isHeadless() || nonInteractive) {
10
- return;
11
- }
12
- // Skip if an after-create hook is already configured (accepts both kebab and legacy camelCase).
13
- if (extractHooks(config)["after-create"]) {
14
- return;
15
- }
16
- // Skip if user has permanently opted out of install prompts.
17
- if (config.skipInstallPrompt === true) {
18
- return;
19
- }
20
- const detect = dependencies.detectInstallPackageManager ?? detectPackageManager;
21
- const pm = await detect(worktreePath);
22
- if (!pm) {
23
- return;
24
- }
25
- const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
26
- const choice = await prompt(pm);
27
- if (!choice || choice === "no") {
28
- return;
29
- }
30
- if (choice === "yes" || choice === "always") {
31
- const runner = dependencies.runInstallCommand ?? runInstallCommand;
32
- try {
33
- await runner(pm.installCommand, worktreePath, stderr);
34
- }
35
- catch (error) {
36
- stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}\n`);
37
- }
38
- }
39
- const saveGlobal = config.installSaveTarget === "global";
40
- const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
41
- const writeGlobalKey = dependencies.writeGlobalRepoConfigKey ?? defaultWriteGlobalRepoConfigKey;
42
- if (choice === "always") {
43
- try {
44
- if (saveGlobal) {
45
- // Deep-merge with any existing per-repo global hooks so other keys are preserved.
46
- const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
47
- const existing = extractHooks({ hooks: existingRaw });
48
- await writeGlobalKey(repoRoot, "hooks", {
49
- ...(existing["after-enter"] !== undefined && {
50
- "after-enter": existing["after-enter"],
51
- }),
52
- ...(existing["before-remove"] !== undefined && {
53
- "before-remove": existing["before-remove"],
54
- }),
55
- "after-create": pm.installCommand,
56
- });
57
- }
58
- else {
59
- // Read local config hooks to deep-merge so other hook keys (e.g. after-enter) are preserved.
60
- const { config: localConfig } = await loadConfig(repoRoot);
61
- const existing = extractHooks(localConfig);
62
- await writeKey(repoRoot, "hooks", {
63
- ...(existing["after-enter"] !== undefined && {
64
- "after-enter": existing["after-enter"],
65
- }),
66
- ...(existing["before-remove"] !== undefined && {
67
- "before-remove": existing["before-remove"],
68
- }),
69
- "after-create": pm.installCommand,
70
- });
71
- }
72
- }
73
- catch (error) {
74
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}\n`);
75
- }
76
- }
77
- if (choice === "never") {
78
- try {
79
- if (saveGlobal) {
80
- await writeGlobalKey(repoRoot, "skipInstallPrompt", true);
81
- }
82
- else {
83
- await writeKey(repoRoot, "skipInstallPrompt", true);
84
- }
85
- }
86
- catch (error) {
87
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}\n`);
88
- }
89
- }
90
- }
91
- export const runInstallCommand = runCommand;
92
- async function defaultWriteConfigKey(root, key, value) {
93
- await updateLocalConfigKey(root, key, value);
94
- }
95
- async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
96
- await updateGlobalRepoConfigKey(repoRoot, key, value);
97
- }
98
- async function loadExistingGlobalRepoHooks(repoRoot) {
99
- const { config: globalConfig } = await loadGlobalConfig();
100
- const repos = isPlainObject(globalConfig.repos) ? globalConfig.repos : {};
101
- const perRepo = isPlainObject(repos[repoRoot])
102
- ? repos[repoRoot]
103
- : {};
104
- return isPlainObject(perRepo.hooks) ? perRepo.hooks : {};
105
- }
106
- async function defaultPromptForInstallChoice(pm) {
107
- const choice = await select({
108
- message: `Run \`${pm.installCommand}\` in the new worktree?`,
109
- options: [
110
- { value: "yes", label: "Yes", hint: "run once" },
111
- { value: "no", label: "No", hint: "skip this time" },
112
- { value: "always", label: "Always", hint: "save as after-create hook" },
113
- {
114
- value: "never",
115
- label: "Never",
116
- hint: "disable this prompt for this repo",
117
- },
118
- ],
119
- });
120
- if (isCancel(choice)) {
121
- return null;
122
- }
123
- return choice;
124
- }
125
- function isPlainObject(value) {
126
- return typeof value === "object" && value !== null && !Array.isArray(value);
127
- }
@@ -1,5 +0,0 @@
1
- export interface PackageManager {
2
- name: string;
3
- installCommand: string;
4
- }
5
- export declare function detectPackageManager(repoRoot: string): Promise<PackageManager | null>;
@@ -1,159 +0,0 @@
1
- import { access, readdir } from "node:fs/promises";
2
- import { join } from "node:path";
3
- const ENTRIES = [
4
- // JavaScript / TypeScript
5
- { name: "pnpm", signals: ["pnpm-lock.yaml"], command: "pnpm install" },
6
- { name: "yarn", signals: ["yarn.lock"], command: "yarn install" },
7
- { name: "bun", signals: ["bun.lockb"], command: "bun install" },
8
- { name: "npm", signals: ["package-lock.json"], command: "npm install" },
9
- { name: "deno", signals: ["deno.json", "deno.jsonc"], command: "deno cache" },
10
- // Python
11
- { name: "poetry", signals: ["poetry.lock"], command: "poetry install" },
12
- { name: "uv", signals: ["uv.lock"], command: "uv sync" },
13
- { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
14
- { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
15
- {
16
- name: "conda-lock",
17
- signals: ["conda-lock.yml"],
18
- command: "conda-lock install",
19
- },
20
- {
21
- name: "conda",
22
- signals: ["environment.yml"],
23
- command: "conda env update --file environment.yml",
24
- },
25
- // R
26
- {
27
- name: "renv",
28
- signals: ["renv.lock"],
29
- command: "Rscript -e 'renv::restore()'",
30
- },
31
- // Rust
32
- { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
33
- // Go
34
- { name: "go", signals: ["go.sum"], command: "go mod download" },
35
- // Ruby
36
- { name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
37
- // PHP
38
- { name: "composer", signals: ["composer.lock"], command: "composer install" },
39
- // Elixir / Erlang
40
- { name: "mix", signals: ["mix.lock"], command: "mix deps.get" },
41
- { name: "rebar3", signals: ["rebar.lock"], command: "rebar3 deps" },
42
- // Dart / Flutter
43
- { name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
44
- // Java / Kotlin / Scala
45
- { name: "maven", signals: ["pom.xml"], command: "mvn install" },
46
- { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
47
- {
48
- name: "gradle",
49
- signals: ["build.gradle", "build.gradle.kts"],
50
- command: "gradle build",
51
- },
52
- { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
53
- // .NET (C# / F# / VB)
54
- {
55
- name: "dotnet",
56
- signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
57
- command: "dotnet restore",
58
- glob: true,
59
- },
60
- // Swift
61
- {
62
- name: "swift",
63
- signals: ["Package.swift"],
64
- command: "swift package resolve",
65
- },
66
- // Haskell
67
- { name: "stack", signals: ["stack.yaml"], command: "stack build" },
68
- {
69
- name: "cabal",
70
- signals: ["cabal.project"],
71
- command: "cabal install --only-dependencies",
72
- },
73
- {
74
- name: "cabal",
75
- signals: ["*.cabal"],
76
- command: "cabal install --only-dependencies",
77
- glob: true,
78
- },
79
- // Clojure
80
- { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
81
- { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
82
- // OCaml
83
- { name: "dune", signals: ["dune-project"], command: "dune build" },
84
- // Julia
85
- {
86
- name: "julia",
87
- signals: ["Manifest.toml"],
88
- command: "julia --project -e 'using Pkg; Pkg.instantiate()'",
89
- },
90
- // Nim
91
- {
92
- name: "nimble",
93
- signals: ["*.nimble"],
94
- command: "nimble install",
95
- glob: true,
96
- },
97
- // Crystal
98
- { name: "shards", signals: ["shard.yml"], command: "shards install" },
99
- // Perl
100
- { name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
101
- // Zig
102
- { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
103
- // C / C++
104
- { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
105
- {
106
- name: "conan",
107
- signals: ["conanfile.py", "conanfile.txt"],
108
- command: "conan install .",
109
- },
110
- // Nix
111
- { name: "nix", signals: ["flake.nix"], command: "nix develop" },
112
- { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
113
- // Terraform / OpenTofu
114
- {
115
- name: "terraform",
116
- signals: ["terraform.lock.hcl"],
117
- command: "terraform init",
118
- },
119
- ];
120
- export async function detectPackageManager(repoRoot) {
121
- for (const entry of ENTRIES) {
122
- const matched = entry.glob
123
- ? await matchesGlob(repoRoot, entry.signals)
124
- : await matchesExact(repoRoot, entry.signals);
125
- if (matched) {
126
- return { name: entry.name, installCommand: entry.command };
127
- }
128
- }
129
- return null;
130
- }
131
- async function matchesExact(repoRoot, signals) {
132
- for (const signal of signals) {
133
- try {
134
- await access(join(repoRoot, signal));
135
- return true;
136
- }
137
- catch {
138
- // file not found, try next signal
139
- }
140
- }
141
- return false;
142
- }
143
- async function matchesGlob(repoRoot, patterns) {
144
- let files;
145
- try {
146
- files = await readdir(repoRoot);
147
- }
148
- catch {
149
- return false;
150
- }
151
- const regexes = patterns.map(patternToRegex);
152
- return files.some((file) => regexes.some((re) => re.test(file)));
153
- }
154
- function patternToRegex(pattern) {
155
- const escaped = pattern
156
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
157
- .replace(/\*/g, "[^/]*");
158
- return new RegExp(`^${escaped}$`);
159
- }
@@ -1,29 +0,0 @@
1
- import type { CloneDirectory } from "./dir-clone.js";
2
- import type { SyncDirectoryPlan } from "./sync-plan.js";
3
- export interface ClonedDirectory {
4
- bytes?: number;
5
- dir: string;
6
- ms: number;
7
- }
8
- export type SyncDirectoryOutcome = {
9
- kind: "cloned";
10
- directory: ClonedDirectory;
11
- } | {
12
- kind: "skipped";
13
- dir: string;
14
- reason: string;
15
- };
16
- export interface SyncDirectoryReporter {
17
- readonly measureCloneSize: boolean;
18
- write(message: string): void;
19
- cloned(directory: ClonedDirectory): void;
20
- skipped?(directory: {
21
- dir: string;
22
- reason: string;
23
- }): void;
24
- }
25
- export interface SyncDirectoryExecutionOptions {
26
- cloneDirectory: CloneDirectory;
27
- reporter: SyncDirectoryReporter;
28
- }
29
- export declare function executeSyncDirectoryPlan(plan: readonly SyncDirectoryPlan[], options: SyncDirectoryExecutionOptions): Promise<SyncDirectoryOutcome[]>;
@@ -1,77 +0,0 @@
1
- import { isCloneDestinationExistsError, isCloneInProgressError, } from "./dir-clone.js";
2
- import { inspectDestination } from "./safe-destination.js";
3
- export async function executeSyncDirectoryPlan(plan, options) {
4
- const outcomes = [];
5
- for (const entry of plan) {
6
- if (entry.destinationWarning) {
7
- recordSkipped(outcomes, options.reporter, entry.directory, entry.destinationWarning);
8
- continue;
9
- }
10
- const destinationState = await inspectDestination(entry.worktreePath, entry.destination);
11
- if (destinationState.kind === "exists") {
12
- recordSkipped(outcomes, options.reporter, entry.directory, "destination already exists");
13
- continue;
14
- }
15
- if (destinationState.kind === "unsafe") {
16
- const reason = destinationState.reason;
17
- recordSkipped(outcomes, options.reporter, entry.directory, reason);
18
- continue;
19
- }
20
- if (entry.warning) {
21
- recordSkipped(outcomes, options.reporter, entry.directory, entry.warning);
22
- continue;
23
- }
24
- if (!entry.source) {
25
- recordSkipped(outcomes, options.reporter, entry.directory, "source does not exist");
26
- continue;
27
- }
28
- const refreshedDestinationState = await inspectDestination(entry.worktreePath, entry.destination);
29
- if (refreshedDestinationState.kind !== "missing") {
30
- recordSkipped(outcomes, options.reporter, entry.directory, refreshedDestinationState.kind === "unsafe"
31
- ? refreshedDestinationState.reason
32
- : "destination already exists");
33
- continue;
34
- }
35
- let result;
36
- try {
37
- const cloneOptions = {
38
- destinationRoot: entry.worktreePath,
39
- measureBytes: options.reporter.measureCloneSize,
40
- };
41
- result = await options.cloneDirectory(entry.source, entry.destination, cloneOptions);
42
- }
43
- catch (error) {
44
- if (isCloneDestinationExistsError(error)) {
45
- recordSkipped(outcomes, options.reporter, entry.directory, "destination already exists");
46
- continue;
47
- }
48
- if (isCloneInProgressError(error)) {
49
- const reason = "copy-on-write clone already in progress";
50
- recordSkipped(outcomes, options.reporter, entry.directory, reason);
51
- continue;
52
- }
53
- const reason = toErrorMessage(error);
54
- recordSkipped(outcomes, options.reporter, entry.directory, reason);
55
- continue;
56
- }
57
- const clonedDirectory = {
58
- bytes: result.bytes,
59
- dir: entry.directory,
60
- ms: result.ms,
61
- };
62
- const outcome = { kind: "cloned", directory: clonedDirectory };
63
- outcomes.push(outcome);
64
- options.reporter.cloned(clonedDirectory);
65
- }
66
- return outcomes;
67
- }
68
- function recordSkipped(outcomes, reporter, dir, reason) {
69
- outcomes.push({ kind: "skipped", dir, reason });
70
- if (reporter.skipped)
71
- reporter.skipped({ dir, reason });
72
- else
73
- reporter.write(`syncDirs: ${reason}, skipped ${dir}\n`);
74
- }
75
- function toErrorMessage(error) {
76
- return error instanceof Error ? error.message : String(error);
77
- }
@@ -1,16 +0,0 @@
1
- export interface SyncDirectoryPlan {
2
- directory: string;
3
- destination: string;
4
- worktreePath: string;
5
- destinationWasPresent: boolean;
6
- destinationWarning?: string;
7
- source?: string;
8
- warning?: string;
9
- }
10
- export interface SyncDirectoryEstimate {
11
- bytes: number;
12
- dir: string;
13
- }
14
- export declare function prepareSyncDirectoryPlan(repoRoot: string, worktreePath: string, directories: readonly string[]): Promise<SyncDirectoryPlan[]>;
15
- export declare function estimateSyncDirectoryPlan(plan: readonly SyncDirectoryPlan[]): Promise<SyncDirectoryEstimate[]>;
16
- export declare function estimateSyncDirectories(repoRoot: string, worktreePath: string, directories: readonly string[]): Promise<SyncDirectoryEstimate[]>;
package/dist/sync-plan.js DELETED
@@ -1,128 +0,0 @@
1
- import { lstat, realpath } from "node:fs/promises";
2
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
- import { validateSyncDirPattern } from "./config.js";
4
- import { directorySize } from "./dir-clone.js";
5
- import { isNotFoundError } from "./fs-utils.js";
6
- import { inspectDestination } from "./safe-destination.js";
7
- export async function prepareSyncDirectoryPlan(repoRoot, worktreePath, directories) {
8
- const normalizedDirectories = directories
9
- .map(validateSyncDirPattern)
10
- .sort(directoryDepthAscending);
11
- const plan = [];
12
- for (const directory of normalizedDirectories) {
13
- const destination = join(worktreePath, directory);
14
- const destinationState = await inspectDestination(worktreePath, destination);
15
- const destinationWasPresent = destinationState.kind === "exists";
16
- const destinationWarning = destinationState.kind === "unsafe" ? destinationState.reason : undefined;
17
- try {
18
- const source = await resolveSyncDirectorySource(repoRoot, directory);
19
- if (!source) {
20
- plan.push({
21
- directory,
22
- destination,
23
- worktreePath,
24
- destinationWasPresent,
25
- destinationWarning,
26
- });
27
- }
28
- else if ("warning" in source) {
29
- plan.push({
30
- directory,
31
- destination,
32
- worktreePath,
33
- destinationWasPresent,
34
- destinationWarning,
35
- warning: source.warning,
36
- });
37
- }
38
- else {
39
- plan.push({
40
- directory,
41
- destination,
42
- worktreePath,
43
- destinationWasPresent,
44
- destinationWarning,
45
- source: source.path,
46
- });
47
- }
48
- }
49
- catch (error) {
50
- plan.push({
51
- directory,
52
- destination,
53
- worktreePath,
54
- destinationWasPresent,
55
- destinationWarning,
56
- warning: `could not inspect ${directory}: ${toErrorMessage(error)}`,
57
- });
58
- }
59
- }
60
- return plan;
61
- }
62
- export async function estimateSyncDirectoryPlan(plan) {
63
- const estimates = [];
64
- for (const entry of plan) {
65
- if (entry.destinationWasPresent ||
66
- entry.destinationWarning ||
67
- !entry.source ||
68
- entry.warning ||
69
- isCoveredByCloneAncestor(entry, plan)) {
70
- continue;
71
- }
72
- try {
73
- estimates.push({
74
- bytes: await directorySize(entry.source),
75
- dir: entry.directory,
76
- });
77
- }
78
- catch {
79
- // A dry-run is informational; an unreadable source is omitted.
80
- }
81
- }
82
- return estimates;
83
- }
84
- export async function estimateSyncDirectories(repoRoot, worktreePath, directories) {
85
- const plan = await prepareSyncDirectoryPlan(repoRoot, worktreePath, directories);
86
- return estimateSyncDirectoryPlan(plan);
87
- }
88
- async function resolveSyncDirectorySource(repoRoot, directory) {
89
- const source = join(repoRoot, directory);
90
- let resolvedSource;
91
- try {
92
- resolvedSource = await realpath(source);
93
- }
94
- catch (error) {
95
- if (isNotFoundError(error))
96
- return null;
97
- throw error;
98
- }
99
- if (!isPathInside(repoRoot, resolvedSource)) {
100
- return {
101
- warning: `source symlink resolves outside the repository (${resolvedSource})`,
102
- };
103
- }
104
- const stats = await lstat(resolvedSource);
105
- if (!stats.isDirectory())
106
- return { warning: "source is not a directory" };
107
- return { path: resolvedSource };
108
- }
109
- function isCoveredByCloneAncestor(entry, plan) {
110
- return plan.some((candidate) => candidate !== entry &&
111
- !candidate.destinationWasPresent &&
112
- !candidate.destinationWarning &&
113
- !!candidate.source &&
114
- isPathInside(candidate.directory, entry.directory));
115
- }
116
- function directoryDepthAscending(left, right) {
117
- return left.split(/[\\/]+/u).length - right.split(/[\\/]+/u).length;
118
- }
119
- function isPathInside(parent, child) {
120
- const relativePath = relative(resolve(parent), resolve(child));
121
- return (relativePath !== "" &&
122
- !isAbsolute(relativePath) &&
123
- relativePath !== ".." &&
124
- !relativePath.startsWith(`..${sep}`));
125
- }
126
- function toErrorMessage(error) {
127
- return error instanceof Error ? error.message : String(error);
128
- }