@ozyman42/ozy-cli 0.4.6-linux-x64.0 → 0.4.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/bin/ozy +191 -0
- package/bin/ozy-signing-agent +191 -0
- package/bin/ozy-ssh-keygen +191 -0
- package/bin/ozy-virtual-security-key +191 -0
- package/dist/commands/git/hosts.js +18 -0
- package/dist/commands/git/hosts.js.map +1 -0
- package/dist/commands/git/index.js +8 -0
- package/dist/commands/git/index.js.map +1 -0
- package/dist/commands/git/setup.js +145 -0
- package/dist/commands/git/setup.js.map +1 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/npm/index.js +7 -0
- package/dist/commands/npm/index.js.map +1 -0
- package/dist/commands/npm/setup/git-remote.js +66 -0
- package/dist/commands/npm/setup/git-remote.js.map +1 -0
- package/dist/commands/npm/setup/index.js +168 -0
- package/dist/commands/npm/setup/index.js.map +1 -0
- package/dist/commands/npm/setup/npm-auth.js +62 -0
- package/dist/commands/npm/setup/npm-auth.js.map +1 -0
- package/dist/commands/npm/setup/package-json-validate.js +47 -0
- package/dist/commands/npm/setup/package-json-validate.js.map +1 -0
- package/dist/commands/npm/setup/package-registry.js +20 -0
- package/dist/commands/npm/setup/package-registry.js.map +1 -0
- package/dist/commands/npm/setup/setup.test.js +95 -0
- package/dist/commands/npm/setup/setup.test.js.map +1 -0
- package/dist/commands/npm/setup/trusted-publishing.js +84 -0
- package/dist/commands/npm/setup/trusted-publishing.js.map +1 -0
- package/dist/commands/npm/setup/workflow.js +19 -0
- package/dist/commands/npm/setup/workflow.js.map +1 -0
- package/dist/commands/ssh/get-sk-credential.js +104 -0
- package/dist/commands/ssh/get-sk-credential.js.map +1 -0
- package/dist/commands/ssh/index.js +7 -0
- package/dist/commands/ssh/index.js.map +1 -0
- package/dist/commands/upgrade.js +81 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/common/command.js +15 -0
- package/dist/common/command.js.map +1 -0
- package/dist/common/constants.js +25 -0
- package/dist/common/constants.js.map +1 -0
- package/dist/common/effective-modules-extensions.js +13 -0
- package/dist/common/effective-modules-extensions.js.map +1 -0
- package/dist/common/log.js +4 -0
- package/dist/common/log.js.map +1 -0
- package/dist/common/render-caller-tree.js +20 -0
- package/dist/common/render-caller-tree.js.map +1 -0
- package/dist/entrypoints/ozy-signing-agent.js +48 -0
- package/dist/entrypoints/ozy-signing-agent.js.map +1 -0
- package/dist/entrypoints/ozy-ssh-keygen.js +9 -0
- package/dist/entrypoints/ozy-ssh-keygen.js.map +1 -0
- package/dist/entrypoints/ozy-virtual-security-key.js +18 -0
- package/dist/entrypoints/ozy-virtual-security-key.js.map +1 -0
- package/dist/entrypoints/ozy.js +3 -0
- package/dist/entrypoints/ozy.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/thing.js +4 -0
- package/dist/internal/thing.js.map +1 -0
- package/dist/modules/cli/agent-client/impl.js +109 -0
- package/dist/modules/cli/agent-client/impl.js.map +1 -0
- package/dist/modules/cli/agent-client/interface.js +11 -0
- package/dist/modules/cli/agent-client/interface.js.map +1 -0
- package/dist/modules/cli/git/impl.js +124 -0
- package/dist/modules/cli/git/impl.js.map +1 -0
- package/dist/modules/cli/git/interface.js +3 -0
- package/dist/modules/cli/git/interface.js.map +1 -0
- package/dist/modules/cli/git/submodule/fix.js +257 -0
- package/dist/modules/cli/git/submodule/fix.js.map +1 -0
- package/dist/modules/cli/git/submodule/git-state.js +154 -0
- package/dist/modules/cli/git/submodule/git-state.js.map +1 -0
- package/dist/modules/cli/git/submodule/gitmodules.js +113 -0
- package/dist/modules/cli/git/submodule/gitmodules.js.map +1 -0
- package/dist/modules/cli/git/submodule/printer.js +232 -0
- package/dist/modules/cli/git/submodule/printer.js.map +1 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js +16 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js.map +1 -0
- package/dist/modules/cli/github/impl.js +137 -0
- package/dist/modules/cli/github/impl.js.map +1 -0
- package/dist/modules/cli/github/interface.js +2 -0
- package/dist/modules/cli/github/interface.js.map +1 -0
- package/dist/modules/cli/index.js +9 -0
- package/dist/modules/cli/index.js.map +1 -0
- package/dist/modules/common/crypto/impl.js +133 -0
- package/dist/modules/common/crypto/impl.js.map +1 -0
- package/dist/modules/common/crypto/interface.js +2 -0
- package/dist/modules/common/crypto/interface.js.map +1 -0
- package/dist/modules/common/index.js +10 -0
- package/dist/modules/common/index.js.map +1 -0
- package/dist/modules/common/kep-map-store/impl.js +42 -0
- package/dist/modules/common/kep-map-store/impl.js.map +1 -0
- package/dist/modules/common/kep-map-store/interface.js +2 -0
- package/dist/modules/common/kep-map-store/interface.js.map +1 -0
- package/dist/modules/common/os-platform/caller-info.js +132 -0
- package/dist/modules/common/os-platform/caller-info.js.map +1 -0
- package/dist/modules/common/os-platform/impl.js +87 -0
- package/dist/modules/common/os-platform/impl.js.map +1 -0
- package/dist/modules/common/os-platform/interface.js +2 -0
- package/dist/modules/common/os-platform/interface.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js +12 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js +119 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js.map +1 -0
- package/dist/modules/common/ssh-config/impl.js +116 -0
- package/dist/modules/common/ssh-config/impl.js.map +1 -0
- package/dist/modules/common/ssh-config/interface.js +10 -0
- package/dist/modules/common/ssh-config/interface.js.map +1 -0
- package/dist/modules/ssh-agent/index.js +8 -0
- package/dist/modules/ssh-agent/index.js.map +1 -0
- package/dist/modules/ssh-agent/session/impl.js +265 -0
- package/dist/modules/ssh-agent/session/impl.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js +20 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface.js +32 -0
- package/dist/modules/ssh-agent/session/interface.js.map +1 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js +204 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js.map +1 -0
- package/dist/modules/ssh-agent/session/prf-flow.js +103 -0
- package/dist/modules/ssh-agent/session/prf-flow.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js +117 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js +2 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js.map +1 -0
- package/dist/scripts/build.js +412 -0
- package/dist/scripts/build.js.map +1 -0
- package/dist/scripts/check-npm-version.js +8 -0
- package/dist/scripts/check-npm-version.js.map +1 -0
- package/dist/scripts/kill-existing-agent.js +21 -0
- package/dist/scripts/kill-existing-agent.js.map +1 -0
- package/dist/scripts/publish.js +46 -0
- package/dist/scripts/publish.js.map +1 -0
- package/package.json +52 -11
- package/src/commands/git/hosts.ts +20 -0
- package/src/commands/git/index.ts +9 -0
- package/src/commands/git/setup.ts +182 -0
- package/src/commands/index.ts +15 -0
- package/src/commands/npm/index.ts +8 -0
- package/src/commands/npm/setup/git-remote.ts +78 -0
- package/src/commands/npm/setup/index.ts +189 -0
- package/src/commands/npm/setup/npm-auth.ts +58 -0
- package/src/commands/npm/setup/package-json-validate.ts +68 -0
- package/src/commands/npm/setup/package-registry.ts +24 -0
- package/src/commands/npm/setup/publish-workflow.yml +35 -0
- package/src/commands/npm/setup/setup.test.ts +117 -0
- package/src/commands/npm/setup/trusted-publishing.ts +106 -0
- package/src/commands/npm/setup/workflow.ts +20 -0
- package/src/commands/ssh/get-sk-credential.ts +110 -0
- package/src/commands/ssh/index.ts +8 -0
- package/src/commands/upgrade.ts +90 -0
- package/src/common/command.ts +16 -0
- package/src/common/constants.ts +26 -0
- package/src/common/effective-modules-extensions.ts +24 -0
- package/src/common/log.ts +3 -0
- package/src/common/render-caller-tree.ts +22 -0
- package/src/entrypoints/ozy-signing-agent.ts +48 -0
- package/src/entrypoints/ozy-ssh-keygen.ts +10 -0
- package/src/entrypoints/ozy-virtual-security-key.ts +21 -0
- package/src/entrypoints/ozy.ts +2 -0
- package/src/index.ts +1 -0
- package/src/internal/thing.ts +3 -0
- package/src/modules/cli/agent-client/impl.ts +152 -0
- package/src/modules/cli/agent-client/interface.ts +17 -0
- package/src/modules/cli/git/impl.ts +133 -0
- package/src/modules/cli/git/interface.ts +23 -0
- package/src/modules/cli/git/submodule/fix.ts +298 -0
- package/src/modules/cli/git/submodule/git-state.ts +217 -0
- package/src/modules/cli/git/submodule/gitmodules.ts +126 -0
- package/src/modules/cli/git/submodule/printer.ts +256 -0
- package/src/modules/cli/git/submodule/resolve-git-dir.ts +14 -0
- package/src/modules/cli/github/impl.ts +169 -0
- package/src/modules/cli/github/interface.ts +29 -0
- package/src/modules/cli/index.ts +16 -0
- package/src/modules/common/crypto/impl.ts +173 -0
- package/src/modules/common/crypto/interface.ts +20 -0
- package/src/modules/common/index.ts +19 -0
- package/src/modules/common/kep-map-store/impl.ts +47 -0
- package/src/modules/common/kep-map-store/interface.ts +9 -0
- package/src/modules/common/os-platform/caller-info.ts +152 -0
- package/src/modules/common/os-platform/impl.ts +86 -0
- package/src/modules/common/os-platform/interface.ts +16 -0
- package/src/modules/common/os-platform/virtual-hid/index.ts +12 -0
- package/src/modules/common/os-platform/virtual-hid/linux.ts +7 -0
- package/src/modules/common/os-platform/virtual-hid/mac.ts +150 -0
- package/src/modules/common/os-platform/virtual-hid/windows.ts +7 -0
- package/src/modules/common/ssh-config/impl.ts +130 -0
- package/src/modules/common/ssh-config/interface.ts +33 -0
- package/src/modules/ssh-agent/index.ts +13 -0
- package/src/modules/ssh-agent/session/agent-notes.md +210 -0
- package/src/modules/ssh-agent/session/design.md +2 -0
- package/src/modules/ssh-agent/session/html-refactor.md +23 -0
- package/src/modules/ssh-agent/session/impl.ts +328 -0
- package/src/modules/ssh-agent/session/interface-rpc.ts +24 -0
- package/src/modules/ssh-agent/session/interface.ts +62 -0
- package/src/modules/ssh-agent/session/passkey-prf-page.ts +224 -0
- package/src/modules/ssh-agent/session/prf-flow.ts +152 -0
- package/src/modules/ssh-agent/ssh-agent/impl.ts +136 -0
- package/src/modules/ssh-agent/ssh-agent/interface.ts +9 -0
- package/src/scripts/build.ts +506 -0
- package/src/scripts/check-npm-version.ts +7 -0
- package/src/scripts/command-launcher.js.ejs +168 -0
- package/src/scripts/kill-existing-agent.ts +22 -0
- package/src/scripts/publish.ts +47 -0
- package/src/scripts/windows-bin-shims.md +1104 -0
- package/multi-call-binary +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { Effect, Option } from "effect";
|
|
5
|
+
import { resolveGitDir } from "./resolve-git-dir";
|
|
6
|
+
|
|
7
|
+
// --- types ---
|
|
8
|
+
|
|
9
|
+
export interface GitConfigSubmodule {
|
|
10
|
+
name: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
path?: string;
|
|
13
|
+
active?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ModuleDirInfo {
|
|
17
|
+
/** Path relative to .git/modules/ */
|
|
18
|
+
relativePath: string;
|
|
19
|
+
/** URL from this module dir's own git config [remote "origin"] */
|
|
20
|
+
remoteUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WorkingTreeGitEntry {
|
|
24
|
+
/** Submodule path relative to repo root */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Whether .git at this path is a gitdir-redirect file, a full directory, or absent */
|
|
27
|
+
type: "file" | "dir" | "missing";
|
|
28
|
+
/** Content of the gitdir: line when type === "file" */
|
|
29
|
+
gitdirTarget?: string;
|
|
30
|
+
/** Result of `git remote get-url origin` run inside the working tree dir */
|
|
31
|
+
remote?: string | { error: string };
|
|
32
|
+
/** Result of `git rev-parse HEAD` run inside the working tree dir */
|
|
33
|
+
headCommit?: string | { error: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IndexGitlink {
|
|
37
|
+
path: string;
|
|
38
|
+
commit: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface GitState {
|
|
42
|
+
/** Submodule stanzas found in .git/config */
|
|
43
|
+
config: GitConfigSubmodule[];
|
|
44
|
+
/** Git dirs found under .git/modules/, each with their remote URL */
|
|
45
|
+
moduleDirs: ModuleDirInfo[];
|
|
46
|
+
/** Dirs under .git/modules/ that contain no HEAD file and aren't prefixes of any known module path */
|
|
47
|
+
orphanModuleDirs: string[];
|
|
48
|
+
/** Gitlink entries recorded in the index (mode 160000) */
|
|
49
|
+
index: IndexGitlink[];
|
|
50
|
+
/** State of .git file/dir in each known submodule working tree path */
|
|
51
|
+
workingTree: WorkingTreeGitEntry[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- parsers ---
|
|
55
|
+
|
|
56
|
+
function parseGitConfig(content: string): GitConfigSubmodule[] {
|
|
57
|
+
const entries: GitConfigSubmodule[] = [];
|
|
58
|
+
let current: Partial<GitConfigSubmodule> | null = null;
|
|
59
|
+
|
|
60
|
+
for (const line of content.split("\n")) {
|
|
61
|
+
const headerMatch = line.match(/^\[submodule "(.+)"\]$/);
|
|
62
|
+
if (headerMatch) {
|
|
63
|
+
if (current?.name) entries.push(current as GitConfigSubmodule);
|
|
64
|
+
current = { name: headerMatch[1] };
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!current) continue;
|
|
68
|
+
const kvMatch = line.match(/^\s+(\w+)\s*=\s*(.+)$/);
|
|
69
|
+
if (!kvMatch) continue;
|
|
70
|
+
const [, key, value] = kvMatch;
|
|
71
|
+
if (key === "url") current.url = value!.trim();
|
|
72
|
+
else if (key === "path") current.path = value!.trim();
|
|
73
|
+
else if (key === "active") current.active = value!.trim() === "true";
|
|
74
|
+
}
|
|
75
|
+
if (current?.name) entries.push(current as GitConfigSubmodule);
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseIndexGitlinks(output: string): IndexGitlink[] {
|
|
80
|
+
return output
|
|
81
|
+
.split("\n")
|
|
82
|
+
.filter((l) => l.startsWith("160000 "))
|
|
83
|
+
.map((line) => {
|
|
84
|
+
const match = line.match(/^160000 ([0-9a-f]{40}) \d\t(.+)$/);
|
|
85
|
+
if (!match) throw new Error(`Unexpected ls-files line: ${line}`);
|
|
86
|
+
return { commit: match[1]!, path: match[2]! };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Recursively scans .git/modules/, returning:
|
|
92
|
+
* - gitDirs: paths of real git dirs (contain a HEAD file)
|
|
93
|
+
* - orphanDirs: paths that are neither a git dir nor a prefix of any known module path
|
|
94
|
+
*/
|
|
95
|
+
function scanModuleDirs(
|
|
96
|
+
root: string,
|
|
97
|
+
modulePaths: Set<string>,
|
|
98
|
+
rel = "",
|
|
99
|
+
): { gitDirs: string[]; orphanDirs: string[] } {
|
|
100
|
+
const gitDirs: string[] = [];
|
|
101
|
+
const orphanDirs: string[] = [];
|
|
102
|
+
if (!existsSync(root)) return { gitDirs, orphanDirs };
|
|
103
|
+
|
|
104
|
+
for (const entry of readdirSync(root)) {
|
|
105
|
+
const abs = join(root, entry);
|
|
106
|
+
if (!statSync(abs).isDirectory()) continue;
|
|
107
|
+
const relPath = rel ? `${rel}/${entry}` : entry;
|
|
108
|
+
|
|
109
|
+
if (existsSync(join(abs, "HEAD"))) {
|
|
110
|
+
gitDirs.push(relPath);
|
|
111
|
+
} else {
|
|
112
|
+
const isPrefix = [...modulePaths].some((p) => p.startsWith(relPath + "/"));
|
|
113
|
+
if (isPrefix) {
|
|
114
|
+
const sub = scanModuleDirs(abs, modulePaths, relPath);
|
|
115
|
+
gitDirs.push(...sub.gitDirs);
|
|
116
|
+
orphanDirs.push(...sub.orphanDirs);
|
|
117
|
+
} else {
|
|
118
|
+
orphanDirs.push(relPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { gitDirs, orphanDirs };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function readModuleDirRemoteUrl(abs: string): Promise<Option.Option<string>> {
|
|
127
|
+
const configPath = join(abs, "config");
|
|
128
|
+
if (!existsSync(configPath)) return Option.none();
|
|
129
|
+
const content = await readFile(configPath, "utf8");
|
|
130
|
+
const match = content.match(/\[remote "origin"\][^\[]*\n\s+url\s*=\s*(.+)/);
|
|
131
|
+
return match?.[1] ? Option.some(match[1].trim()) : Option.none();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function spawnGitInDir(args: string[], cwd: string): Promise<string | { error: string }> {
|
|
135
|
+
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
136
|
+
const [code, out, err] = await Promise.all([
|
|
137
|
+
proc.exited,
|
|
138
|
+
new Response(proc.stdout).text(),
|
|
139
|
+
new Response(proc.stderr).text(),
|
|
140
|
+
]);
|
|
141
|
+
return code === 0 ? out.trim() : { error: err.trim().split("\n")[0]! };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function readWorkingTreeEntry(repoRoot: string, subPath: string): Promise<WorkingTreeGitEntry> {
|
|
145
|
+
const workingTreeAbs = join(repoRoot, subPath);
|
|
146
|
+
const dotGit = join(workingTreeAbs, ".git");
|
|
147
|
+
|
|
148
|
+
let type: "file" | "dir" | "missing";
|
|
149
|
+
let gitdirTarget: string | undefined;
|
|
150
|
+
|
|
151
|
+
if (!existsSync(dotGit)) {
|
|
152
|
+
type = "missing";
|
|
153
|
+
} else if (statSync(dotGit).isDirectory()) {
|
|
154
|
+
type = "dir";
|
|
155
|
+
} else {
|
|
156
|
+
type = "file";
|
|
157
|
+
const content = await readFile(dotGit, "utf8");
|
|
158
|
+
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
159
|
+
gitdirTarget = match?.[1].trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!existsSync(workingTreeAbs)) return { path: subPath, type, gitdirTarget };
|
|
163
|
+
|
|
164
|
+
const [remote, headCommit] = await Promise.all([
|
|
165
|
+
spawnGitInDir(["remote", "get-url", "origin"], workingTreeAbs),
|
|
166
|
+
spawnGitInDir(["rev-parse", "HEAD"], workingTreeAbs),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
return { path: subPath, type, gitdirTarget, remote, headCommit };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- main reader ---
|
|
173
|
+
|
|
174
|
+
export function readGitState(repoRoot: string, extraPaths: string[] = []): Effect.Effect<GitState, string> {
|
|
175
|
+
return Effect.tryPromise({
|
|
176
|
+
try: async () => {
|
|
177
|
+
const gitDir = await resolveGitDir(repoRoot);
|
|
178
|
+
|
|
179
|
+
const [configContent, lsOutput] = await Promise.all([
|
|
180
|
+
readFile(resolve(gitDir, "config"), "utf8"),
|
|
181
|
+
new Response(
|
|
182
|
+
Bun.spawn(["git", "ls-files", "--stage"], { cwd: repoRoot, stdout: "pipe" }).stdout
|
|
183
|
+
).text(),
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
const config = parseGitConfig(configContent);
|
|
187
|
+
const index = parseIndexGitlinks(lsOutput);
|
|
188
|
+
|
|
189
|
+
const modulePaths = new Set(extraPaths);
|
|
190
|
+
const modulesRoot = resolve(gitDir, "modules");
|
|
191
|
+
const { gitDirs: moduleDirPaths, orphanDirs: orphanModuleDirs } = scanModuleDirs(modulesRoot, modulePaths);
|
|
192
|
+
const moduleDirs = await Promise.all(
|
|
193
|
+
moduleDirPaths.map(async (rel) => {
|
|
194
|
+
const remoteUrlOption = await readModuleDirRemoteUrl(join(modulesRoot, rel));
|
|
195
|
+
return {
|
|
196
|
+
relativePath: rel,
|
|
197
|
+
remoteUrl: Option.isSome(remoteUrlOption) ? remoteUrlOption.value : undefined,
|
|
198
|
+
};
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const allPaths = [
|
|
203
|
+
...new Set([
|
|
204
|
+
...index.map((e) => e.path),
|
|
205
|
+
...config.flatMap((e) => (e.path ? [e.path] : [])),
|
|
206
|
+
...extraPaths,
|
|
207
|
+
]),
|
|
208
|
+
];
|
|
209
|
+
const workingTree = await Promise.all(
|
|
210
|
+
allPaths.map((p) => readWorkingTreeEntry(repoRoot, p))
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return { config, moduleDirs, orphanModuleDirs, index, workingTree };
|
|
214
|
+
},
|
|
215
|
+
catch: (e) => e instanceof Error ? e.message : String(e),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import type { EffectGen } from "effective-modules";
|
|
6
|
+
import { STANDARD_REMOTE_PREFIX } from "@/common/constants";
|
|
7
|
+
|
|
8
|
+
export interface Submodule {
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// git@host:org/repo.git or git@host:repo.git
|
|
15
|
+
const SSH_URL_RE = /^git@[^:]+:.+\.git$/;
|
|
16
|
+
const SSH_URL_HOST_RE = /^git@[^:]+:(.+)$/;
|
|
17
|
+
|
|
18
|
+
export class Gitmodules {
|
|
19
|
+
private constructor(
|
|
20
|
+
private readonly path: string,
|
|
21
|
+
private readonly repoRoot: string,
|
|
22
|
+
private readonly modules: Submodule[],
|
|
23
|
+
private readonly rawContent: string,
|
|
24
|
+
) {
|
|
25
|
+
this.path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public static *fromRepoRoot(repoRoot: string): EffectGen<Gitmodules, string> {
|
|
29
|
+
const path = resolve(repoRoot, ".gitmodules");
|
|
30
|
+
|
|
31
|
+
const content = yield* Effect.tryPromise({
|
|
32
|
+
try: () => readFile(path, "utf8"),
|
|
33
|
+
catch: (e) => e instanceof Error ? e.message : String(e),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const modules = Gitmodules
|
|
37
|
+
.parse(content)
|
|
38
|
+
.map(Gitmodules.normalizeRemote)
|
|
39
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
+
|
|
41
|
+
const serialized = modules
|
|
42
|
+
.map((m) => `[submodule "${m.name}"]\n\tpath = ${m.path}\n\turl = ${m.url}`)
|
|
43
|
+
.join("\n") + "\n";
|
|
44
|
+
|
|
45
|
+
const result = new Gitmodules(path, repoRoot, modules, serialized);
|
|
46
|
+
const errors = result.validate();
|
|
47
|
+
if (errors.length) {
|
|
48
|
+
return yield* Effect.fail(`Validation errors in .gitmodules:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (serialized !== content) {
|
|
52
|
+
yield* Effect.tryPromise({
|
|
53
|
+
try: () => writeFile(resolve(repoRoot, '.gitmodules'), serialized, 'utf8'),
|
|
54
|
+
catch: (e) => e instanceof Error ? e.message : String(e),
|
|
55
|
+
});
|
|
56
|
+
yield* Effect.log('Updated .gitmodules.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static parse(content: string): Submodule[] {
|
|
63
|
+
const modules: Submodule[] = [];
|
|
64
|
+
let current: Partial<Submodule> | null = null;
|
|
65
|
+
|
|
66
|
+
for (const line of content.split("\n")) {
|
|
67
|
+
const headerMatch = line.match(/^\[submodule "(.+)"\]$/);
|
|
68
|
+
if (headerMatch) {
|
|
69
|
+
if (current) modules.push(current as Submodule);
|
|
70
|
+
current = { name: headerMatch[1] };
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const kvMatch = line.match(/^\s+(\w+)\s*=\s*(.+)$/);
|
|
74
|
+
if (kvMatch && current) {
|
|
75
|
+
const [, key, value] = kvMatch;
|
|
76
|
+
if (key === "path" || key === "url") current[key] = value.trim();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (current) modules.push(current as Submodule);
|
|
80
|
+
return modules;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static normalizeRemote(m: Submodule): Submodule {
|
|
84
|
+
if (m.url.startsWith(STANDARD_REMOTE_PREFIX)) return m;
|
|
85
|
+
const match = m.url.match(SSH_URL_HOST_RE);
|
|
86
|
+
// TODO: we should probably move this logic to be alongside validate
|
|
87
|
+
if (!match) return m; // malformed URL — left as-is, validate() will report it
|
|
88
|
+
const normalizedUrl = `${STANDARD_REMOTE_PREFIX}${match[1]}`;
|
|
89
|
+
console.warn(`- updating submodule "${m.name}" remote: ${m.url} -> ${normalizedUrl}`);
|
|
90
|
+
return { ...m, url: normalizedUrl };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get list(): readonly Submodule[] {
|
|
94
|
+
return this.modules;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get content(): string {
|
|
98
|
+
return this.rawContent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private validate(): string[] {
|
|
102
|
+
const errors: string[] = [];
|
|
103
|
+
const names = new Set<string>();
|
|
104
|
+
const paths = new Set<string>();
|
|
105
|
+
const urls = new Set<string>();
|
|
106
|
+
|
|
107
|
+
for (const m of this.modules) {
|
|
108
|
+
if (names.has(m.name)) errors.push(`Duplicate name: "${m.name}"`);
|
|
109
|
+
else names.add(m.name);
|
|
110
|
+
|
|
111
|
+
if (paths.has(m.path)) errors.push(`Duplicate path: "${m.path}" (submodule "${m.name}")`);
|
|
112
|
+
else paths.add(m.path);
|
|
113
|
+
|
|
114
|
+
if (urls.has(m.url)) errors.push(`Duplicate url: "${m.url}" (submodule "${m.name}")`);
|
|
115
|
+
else urls.add(m.url);
|
|
116
|
+
|
|
117
|
+
if (!SSH_URL_RE.test(m.url))
|
|
118
|
+
errors.push(`Invalid url for "${m.name}": ${m.url}`);
|
|
119
|
+
|
|
120
|
+
if (!existsSync(resolve(this.repoRoot, m.path)))
|
|
121
|
+
errors.push(`Path does not exist for "${m.name}": ${m.path}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return errors;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { Gitmodules } from "./gitmodules";
|
|
2
|
+
import type { GitState, ModuleDirInfo, IndexGitlink, WorkingTreeGitEntry } from "./git-state";
|
|
3
|
+
import type { Fix } from "./fix";
|
|
4
|
+
|
|
5
|
+
const RED = "\x1b[31m";
|
|
6
|
+
const GREEN = "\x1b[32m";
|
|
7
|
+
const CYAN = "\x1b[36m";
|
|
8
|
+
const DIM = "\x1b[2m";
|
|
9
|
+
const BOLD = "\x1b[1m";
|
|
10
|
+
const RESET = "\x1b[0m";
|
|
11
|
+
|
|
12
|
+
const ok = `${GREEN}✓${RESET}`;
|
|
13
|
+
const fail = (msg: string) => `${RED}✗ ${msg}${RESET}`;
|
|
14
|
+
const fsName = (s: string) => `${CYAN}${s}${RESET}`;
|
|
15
|
+
const key = (s: string) => `${DIM}${s}${RESET}`;
|
|
16
|
+
|
|
17
|
+
interface Line {
|
|
18
|
+
label: string;
|
|
19
|
+
isKey?: boolean; // dim key style instead of cyan fs style
|
|
20
|
+
value?: string;
|
|
21
|
+
status?: string;
|
|
22
|
+
children?: Line[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function render(lines: Line[], prefix = ""): void {
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const last = i === lines.length - 1;
|
|
28
|
+
const branch = last ? "└── " : "├── ";
|
|
29
|
+
const child = last ? " " : "│ ";
|
|
30
|
+
const l = lines[i];
|
|
31
|
+
const label = l.isKey ? key(l.label) : fsName(l.label);
|
|
32
|
+
let out = `${prefix}${branch}${label}`;
|
|
33
|
+
if (l.value !== undefined) out += `: ${l.value}`;
|
|
34
|
+
if (l.status !== undefined) out += ` ${l.status}`;
|
|
35
|
+
console.log(out);
|
|
36
|
+
if (l.children?.length) render(l.children, prefix + child);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildPathTree(parts: string[], children: Line[]): Line {
|
|
41
|
+
if (parts.length === 1) return { label: parts[0], children };
|
|
42
|
+
return { label: parts[0], children: [buildPathTree(parts.slice(1), children)] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resultLine(label: string, result: string | { error: string } | undefined): Line {
|
|
46
|
+
if (result === undefined) return { label, isKey: true, status: fail("unknown") };
|
|
47
|
+
if (typeof result === "string") return { label, isKey: true, value: result };
|
|
48
|
+
return { label, isKey: true, status: fail(result.error) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function moduleHasIssues(
|
|
52
|
+
m: { path: string; url: string },
|
|
53
|
+
cfg: { path?: string; url?: string } | undefined,
|
|
54
|
+
dir: ModuleDirInfo | undefined,
|
|
55
|
+
idx: IndexGitlink | undefined,
|
|
56
|
+
headSha: string | undefined,
|
|
57
|
+
wt: WorkingTreeGitEntry | undefined,
|
|
58
|
+
): boolean {
|
|
59
|
+
if (!cfg) return true;
|
|
60
|
+
if (cfg.path !== m.path || cfg.url !== m.url) return true;
|
|
61
|
+
if (!dir) return true;
|
|
62
|
+
if (!idx) return true;
|
|
63
|
+
if (headSha && idx.commit !== headSha) return true;
|
|
64
|
+
if (!wt || wt.type === "missing" || wt.type === "dir") return true;
|
|
65
|
+
if (typeof wt.headCommit !== "string") return true;
|
|
66
|
+
if (typeof wt.remote !== "string" || wt.remote !== m.url) return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function printSyncState(modules: Gitmodules, state: GitState): void {
|
|
71
|
+
const byName = new Map(state.config.map((c) => [c.name, c]));
|
|
72
|
+
const byPath = new Map(state.moduleDirs.map((d) => [d.relativePath, d]));
|
|
73
|
+
const byIndexPath = new Map(state.index.map((i) => [i.path, i]));
|
|
74
|
+
const byTreePath = new Map(state.workingTree.map((w) => [w.path, w]));
|
|
75
|
+
const modulePaths = new Set(modules.list.map((m) => m.path));
|
|
76
|
+
const moduleNames = new Set(modules.list.map((m) => m.name));
|
|
77
|
+
|
|
78
|
+
console.log("=== syncing submodules ===");
|
|
79
|
+
|
|
80
|
+
for (const m of modules.list) {
|
|
81
|
+
const cfg = byName.get(m.name);
|
|
82
|
+
const dir = byPath.get(m.path);
|
|
83
|
+
const idx = byIndexPath.get(m.path);
|
|
84
|
+
const wt = byTreePath.get(m.path);
|
|
85
|
+
const headSha = typeof wt?.headCommit === "string" ? wt.headCommit : undefined;
|
|
86
|
+
|
|
87
|
+
if (!moduleHasIssues(m, cfg, dir, idx, headSha, wt)) {
|
|
88
|
+
console.log(`${BOLD}[${m.name}]${RESET} ${ok}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`${BOLD}[${m.name}]${RESET}`);
|
|
93
|
+
|
|
94
|
+
// .gitmodules
|
|
95
|
+
const gitmodulesNode: Line = {
|
|
96
|
+
label: ".gitmodules",
|
|
97
|
+
children: [
|
|
98
|
+
{ label: "path", isKey: true, value: m.path },
|
|
99
|
+
{ label: "url", isKey: true, value: m.url },
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// .git/config
|
|
104
|
+
let configNode: Line;
|
|
105
|
+
if (!cfg) {
|
|
106
|
+
configNode = { label: "config", status: fail("missing") };
|
|
107
|
+
} else {
|
|
108
|
+
configNode = {
|
|
109
|
+
label: "config",
|
|
110
|
+
children: [
|
|
111
|
+
{
|
|
112
|
+
label: "path",
|
|
113
|
+
isKey: true,
|
|
114
|
+
value: cfg.path ?? "(missing)",
|
|
115
|
+
status: cfg.path === m.path ? ok : fail(`expected ${m.path}`),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: "url",
|
|
119
|
+
isKey: true,
|
|
120
|
+
value: cfg.url ?? "(missing)",
|
|
121
|
+
status: cfg.url === m.url ? ok : fail(`expected ${m.url}`),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// .git/modules
|
|
128
|
+
const modulesNode: Line = {
|
|
129
|
+
label: "modules",
|
|
130
|
+
children: [{ label: m.path, status: dir ? ok : fail("missing") }],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// .git/index
|
|
134
|
+
let indexNode: Line;
|
|
135
|
+
if (!idx) {
|
|
136
|
+
indexNode = { label: "index", status: fail("missing") };
|
|
137
|
+
} else {
|
|
138
|
+
const idxSha8 = idx.commit.slice(0, 8);
|
|
139
|
+
const matches = headSha && idx.commit === headSha;
|
|
140
|
+
const mismatches = headSha && idx.commit !== headSha;
|
|
141
|
+
indexNode = {
|
|
142
|
+
label: "index",
|
|
143
|
+
value: idxSha8,
|
|
144
|
+
status: matches ? ok : mismatches ? fail(`head is ${headSha.slice(0, 8)}`) : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const dotGitNode: Line = {
|
|
149
|
+
label: ".git",
|
|
150
|
+
children: [configNode, modulesNode, indexNode],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// working tree leaves
|
|
154
|
+
const wtLeaves: Line[] = [];
|
|
155
|
+
|
|
156
|
+
if (!wt || wt.type === "missing") {
|
|
157
|
+
wtLeaves.push({ label: ".git", status: fail("missing") });
|
|
158
|
+
} else if (wt.type === "dir") {
|
|
159
|
+
wtLeaves.push({ label: ".git", status: fail("directory (standalone clone)") });
|
|
160
|
+
} else {
|
|
161
|
+
wtLeaves.push({ label: ".git", value: `gitdir → ${wt.gitdirTarget}` });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (wt) {
|
|
165
|
+
const head = headSha ?? wt.headCommit;
|
|
166
|
+
wtLeaves.push(resultLine("head", typeof head === "string" ? head.slice(0, 8) : head));
|
|
167
|
+
|
|
168
|
+
if (typeof wt.remote === "string") {
|
|
169
|
+
wtLeaves.push({
|
|
170
|
+
label: "remote",
|
|
171
|
+
isKey: true,
|
|
172
|
+
value: wt.remote,
|
|
173
|
+
status: wt.remote === m.url ? ok : fail(`expected ${m.url}`),
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
wtLeaves.push(resultLine("remote", wt.remote));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const workingTreeNode = buildPathTree(m.path.split("/"), wtLeaves);
|
|
181
|
+
|
|
182
|
+
render([gitmodulesNode, dotGitNode, workingTreeNode]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Stale entries
|
|
186
|
+
const staleConfig = state.config.filter((c) => !moduleNames.has(c.name));
|
|
187
|
+
const staleDirs = state.moduleDirs.filter((d) => !modulePaths.has(d.relativePath));
|
|
188
|
+
const staleIndex = state.index.filter((i) => !modulePaths.has(i.path));
|
|
189
|
+
|
|
190
|
+
if (staleConfig.length || staleDirs.length || state.orphanModuleDirs.length || staleIndex.length) {
|
|
191
|
+
console.log(`\n${RED}${BOLD}=== stale (not in .gitmodules) ===${RESET}`);
|
|
192
|
+
const staleNodes: Line[] = [];
|
|
193
|
+
|
|
194
|
+
if (staleConfig.length) {
|
|
195
|
+
staleNodes.push({
|
|
196
|
+
label: ".git/config",
|
|
197
|
+
children: staleConfig.map((c) => ({ label: c.name })),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (staleDirs.length || state.orphanModuleDirs.length) {
|
|
201
|
+
staleNodes.push({
|
|
202
|
+
label: ".git/modules",
|
|
203
|
+
children: [
|
|
204
|
+
...staleDirs.map((d) => ({ label: d.relativePath })),
|
|
205
|
+
...state.orphanModuleDirs.map((d) => ({ label: d })),
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (staleIndex.length) {
|
|
210
|
+
staleNodes.push({
|
|
211
|
+
label: ".git/index",
|
|
212
|
+
children: staleIndex.map((i) => ({ label: i.path, isKey: true, value: i.commit.slice(0, 8) })),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
render(staleNodes);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type FixTree = Map<string, { subtree: FixTree; fixes: Fix[] }>;
|
|
221
|
+
|
|
222
|
+
function addToFixTree(tree: FixTree, fix: Fix, depth = 0): void {
|
|
223
|
+
const seg = fix.location[depth];
|
|
224
|
+
if (!tree.has(seg)) tree.set(seg, { subtree: new Map(), fixes: [] });
|
|
225
|
+
const node = tree.get(seg)!;
|
|
226
|
+
if (depth === fix.location.length - 1) {
|
|
227
|
+
node.fixes.push(fix);
|
|
228
|
+
} else {
|
|
229
|
+
addToFixTree(node.subtree, fix, depth + 1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function fixTreeToLines(tree: FixTree): Line[] {
|
|
234
|
+
const lines: Line[] = [];
|
|
235
|
+
for (const [label, { subtree, fixes }] of tree) {
|
|
236
|
+
const children = fixTreeToLines(subtree);
|
|
237
|
+
if (fixes.length > 0) {
|
|
238
|
+
for (const fix of fixes) {
|
|
239
|
+
const actionStr = fix.action === "deleted" ? `${RED}deleted${RESET}` : `${GREEN}synced${RESET}`;
|
|
240
|
+
const status = fix.detail ? `${actionStr} ${fix.detail}` : actionStr;
|
|
241
|
+
lines.push({ label, status, children: children.length > 0 ? children : undefined });
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
lines.push({ label, children });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return lines;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function printFixes(fixes: Fix[]): void {
|
|
251
|
+
if (fixes.length === 0) return;
|
|
252
|
+
const tree: FixTree = new Map();
|
|
253
|
+
for (const fix of fixes) addToFixTree(tree, fix);
|
|
254
|
+
console.log("\napplied fixes:");
|
|
255
|
+
render(fixTreeToLines(tree));
|
|
256
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
// A submodule's .git is a redirect file ("gitdir: <relative path>"), not a directory —
|
|
6
|
+
// resolve it to the real git dir so callers don't try to treat the file as a directory.
|
|
7
|
+
export async function resolveGitDir(repoRoot: string): Promise<string> {
|
|
8
|
+
const dotGit = resolve(repoRoot, ".git");
|
|
9
|
+
if (statSync(dotGit).isDirectory()) return dotGit;
|
|
10
|
+
const content = await readFile(dotGit, "utf8");
|
|
11
|
+
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
12
|
+
if (!match) throw new Error(`Malformed .git file at ${dotGit}`);
|
|
13
|
+
return resolve(repoRoot, match[1]!.trim());
|
|
14
|
+
}
|