@solaqua/gji 0.12.2 → 0.12.4
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/bootstrap-output.d.ts +3 -2
- package/dist/bootstrap-output.js +1 -16
- package/dist/bootstrap-preview.d.ts +2 -3
- package/dist/bootstrap-preview.js +1 -11
- package/dist/cli.js +5 -1
- package/dist/config.d.ts +3 -13
- package/dist/config.js +12 -59
- package/dist/dependency-bootstrap.d.ts +11 -45
- package/dist/dependency-bootstrap.js +142 -608
- package/dist/gji-bundle.mjs +580 -2360
- package/dist/new.d.ts +4 -5
- package/dist/new.js +10 -45
- package/dist/pr.d.ts +4 -5
- package/dist/pr.js +9 -69
- package/dist/shell-completion.js +7 -5
- package/dist/uv-validation.d.ts +4 -0
- package/dist/uv-validation.js +232 -0
- package/dist/worktree-bootstrap.d.ts +5 -16
- package/dist/worktree-bootstrap.js +1 -24
- 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 +1 -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 +8 -5
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +6 -3
- 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 +1 -1
- package/man/man1/gji-undo.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +2 -2
- package/package.json +1 -1
- package/dist/dependency-bootstrap-prompt.d.ts +0 -24
- package/dist/dependency-bootstrap-prompt.js +0 -103
- package/dist/dir-clone.d.ts +0 -32
- package/dist/dir-clone.js +0 -641
- package/dist/install-prompt.d.ts +0 -12
- package/dist/install-prompt.js +0 -127
- package/dist/package-manager.d.ts +0 -5
- package/dist/package-manager.js +0 -159
- package/dist/sync-directories.d.ts +0 -29
- package/dist/sync-directories.js +0 -77
- package/dist/sync-plan.d.ts +0 -16
- package/dist/sync-plan.js +0 -128
package/dist/install-prompt.js
DELETED
|
@@ -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
|
-
}
|
package/dist/package-manager.js
DELETED
|
@@ -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[]>;
|
package/dist/sync-directories.js
DELETED
|
@@ -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
|
-
}
|
package/dist/sync-plan.d.ts
DELETED
|
@@ -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
|
-
}
|