@ozyman42/ozy-cli 0.1.0 → 0.2.1
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/index.js +439 -269
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2784,7 +2784,7 @@ var require_ssh_config2 = __commonJS((exports) => {
|
|
|
2784
2784
|
exports.default = ssh_config_1.default;
|
|
2785
2785
|
});
|
|
2786
2786
|
// package.json
|
|
2787
|
-
var version = "0.1
|
|
2787
|
+
var version = "0.2.1";
|
|
2788
2788
|
|
|
2789
2789
|
// node_modules/commander/esm.mjs
|
|
2790
2790
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -8028,22 +8028,30 @@ function parseIndexGitlinks(output) {
|
|
|
8028
8028
|
return { commit: match2[1], path: match2[2] };
|
|
8029
8029
|
});
|
|
8030
8030
|
}
|
|
8031
|
-
function
|
|
8031
|
+
function scanModuleDirs(root, modulePaths, rel = "") {
|
|
8032
|
+
const gitDirs = [];
|
|
8033
|
+
const orphanDirs = [];
|
|
8032
8034
|
if (!existsSync2(root))
|
|
8033
|
-
return
|
|
8034
|
-
const results = [];
|
|
8035
|
+
return { gitDirs, orphanDirs };
|
|
8035
8036
|
for (const entry of readdirSync(root)) {
|
|
8036
8037
|
const abs = join(root, entry);
|
|
8037
|
-
const relPath = rel ? `${rel}/${entry}` : entry;
|
|
8038
8038
|
if (!statSync(abs).isDirectory())
|
|
8039
8039
|
continue;
|
|
8040
|
+
const relPath = rel ? `${rel}/${entry}` : entry;
|
|
8040
8041
|
if (existsSync2(join(abs, "HEAD"))) {
|
|
8041
|
-
|
|
8042
|
+
gitDirs.push(relPath);
|
|
8042
8043
|
} else {
|
|
8043
|
-
|
|
8044
|
+
const isPrefix = [...modulePaths].some((p) => p.startsWith(relPath + "/"));
|
|
8045
|
+
if (isPrefix) {
|
|
8046
|
+
const sub = scanModuleDirs(abs, modulePaths, relPath);
|
|
8047
|
+
gitDirs.push(...sub.gitDirs);
|
|
8048
|
+
orphanDirs.push(...sub.orphanDirs);
|
|
8049
|
+
} else {
|
|
8050
|
+
orphanDirs.push(relPath);
|
|
8051
|
+
}
|
|
8044
8052
|
}
|
|
8045
8053
|
}
|
|
8046
|
-
return
|
|
8054
|
+
return { gitDirs, orphanDirs };
|
|
8047
8055
|
}
|
|
8048
8056
|
async function readModuleDirRemoteUrl(abs) {
|
|
8049
8057
|
const configPath = join(abs, "config");
|
|
@@ -8053,15 +8061,38 @@ async function readModuleDirRemoteUrl(abs) {
|
|
|
8053
8061
|
const match2 = content.match(/\[remote "origin"\][^\[]*\n\s+url\s*=\s*(.+)/);
|
|
8054
8062
|
return match2?.[1].trim();
|
|
8055
8063
|
}
|
|
8064
|
+
async function spawnGitInDir(args, cwd) {
|
|
8065
|
+
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
8066
|
+
const [code, out, err] = await Promise.all([
|
|
8067
|
+
proc.exited,
|
|
8068
|
+
new Response(proc.stdout).text(),
|
|
8069
|
+
new Response(proc.stderr).text()
|
|
8070
|
+
]);
|
|
8071
|
+
return code === 0 ? out.trim() : { error: err.trim().split(`
|
|
8072
|
+
`)[0] };
|
|
8073
|
+
}
|
|
8056
8074
|
async function readWorkingTreeEntry(repoRoot, subPath) {
|
|
8057
|
-
const
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8075
|
+
const workingTreeAbs = join(repoRoot, subPath);
|
|
8076
|
+
const dotGit = join(workingTreeAbs, ".git");
|
|
8077
|
+
let type;
|
|
8078
|
+
let gitdirTarget;
|
|
8079
|
+
if (!existsSync2(dotGit)) {
|
|
8080
|
+
type = "missing";
|
|
8081
|
+
} else if (statSync(dotGit).isDirectory()) {
|
|
8082
|
+
type = "dir";
|
|
8083
|
+
} else {
|
|
8084
|
+
type = "file";
|
|
8085
|
+
const content = await readFile3(dotGit, "utf8");
|
|
8086
|
+
const match2 = content.match(/^gitdir:\s*(.+)$/m);
|
|
8087
|
+
gitdirTarget = match2?.[1].trim();
|
|
8088
|
+
}
|
|
8089
|
+
if (!existsSync2(workingTreeAbs))
|
|
8090
|
+
return { path: subPath, type, gitdirTarget };
|
|
8091
|
+
const [remote, headCommit] = await Promise.all([
|
|
8092
|
+
spawnGitInDir(["remote", "get-url", "origin"], workingTreeAbs),
|
|
8093
|
+
spawnGitInDir(["rev-parse", "HEAD"], workingTreeAbs)
|
|
8094
|
+
]);
|
|
8095
|
+
return { path: subPath, type, gitdirTarget, remote, headCommit };
|
|
8065
8096
|
}
|
|
8066
8097
|
async function readGitState(repoRoot, extraPaths = []) {
|
|
8067
8098
|
const gitDir = resolve2(repoRoot, ".git");
|
|
@@ -8071,8 +8102,9 @@ async function readGitState(repoRoot, extraPaths = []) {
|
|
|
8071
8102
|
]);
|
|
8072
8103
|
const config = parseGitConfig(configContent);
|
|
8073
8104
|
const index = parseIndexGitlinks(lsOutput);
|
|
8105
|
+
const modulePaths = new Set(extraPaths);
|
|
8074
8106
|
const modulesRoot = resolve2(gitDir, "modules");
|
|
8075
|
-
const moduleDirPaths =
|
|
8107
|
+
const { gitDirs: moduleDirPaths, orphanDirs: orphanModuleDirs } = scanModuleDirs(modulesRoot, modulePaths);
|
|
8076
8108
|
const moduleDirs = await Promise.all(moduleDirPaths.map(async (rel) => ({
|
|
8077
8109
|
relativePath: rel,
|
|
8078
8110
|
remoteUrl: await readModuleDirRemoteUrl(join(modulesRoot, rel))
|
|
@@ -8085,11 +8117,11 @@ async function readGitState(repoRoot, extraPaths = []) {
|
|
|
8085
8117
|
])
|
|
8086
8118
|
];
|
|
8087
8119
|
const workingTree = await Promise.all(allPaths.map((p) => readWorkingTreeEntry(repoRoot, p)));
|
|
8088
|
-
return { config, moduleDirs, index, workingTree };
|
|
8120
|
+
return { config, moduleDirs, orphanModuleDirs, index, workingTree };
|
|
8089
8121
|
}
|
|
8090
8122
|
|
|
8091
8123
|
// src/commands/git/sync-submodules/fix.ts
|
|
8092
|
-
import { existsSync as existsSync3 } from "fs";
|
|
8124
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
8093
8125
|
import { readFile as readFile4, writeFile, rename, mkdir, rm } from "fs/promises";
|
|
8094
8126
|
import { resolve as resolve3, join as join2, dirname, relative } from "path";
|
|
8095
8127
|
function removeExtraConfigSubmodules(content, validNames) {
|
|
@@ -8118,295 +8150,359 @@ function removeExtraConfigSubmodules(content, validNames) {
|
|
|
8118
8150
|
return { result: output.join(`
|
|
8119
8151
|
`), removed };
|
|
8120
8152
|
}
|
|
8121
|
-
function repoPath(url) {
|
|
8122
|
-
const stripped = url.replace(/\.git$/, "");
|
|
8123
|
-
if (stripped.startsWith("http")) {
|
|
8124
|
-
const parts = stripped.split("/");
|
|
8125
|
-
return parts.slice(-2).join("/");
|
|
8126
|
-
}
|
|
8127
|
-
const colonIdx = stripped.indexOf(":");
|
|
8128
|
-
return colonIdx >= 0 ? stripped.slice(colonIdx + 1) : stripped;
|
|
8129
|
-
}
|
|
8130
8153
|
async function spawnGit(args, cwd) {
|
|
8131
|
-
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "
|
|
8154
|
+
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
8132
8155
|
const code = await proc.exited;
|
|
8133
|
-
if (code !== 0)
|
|
8134
|
-
|
|
8156
|
+
if (code !== 0) {
|
|
8157
|
+
const err = await new Response(proc.stderr).text();
|
|
8158
|
+
throw new Error(`git ${args.join(" ")} exited with code ${code}
|
|
8159
|
+
${err.trim()}`);
|
|
8160
|
+
}
|
|
8135
8161
|
}
|
|
8136
|
-
async function
|
|
8162
|
+
async function fixGitConfig(repoRoot, modules) {
|
|
8163
|
+
const configPath = resolve3(repoRoot, ".git", "config");
|
|
8164
|
+
const content = await readFile4(configPath, "utf8");
|
|
8165
|
+
const validNames = new Set(modules.map((m) => m.name));
|
|
8166
|
+
const { result: afterRemoval, removed } = removeExtraConfigSubmodules(content, validNames);
|
|
8167
|
+
const existingNames = new Set([...afterRemoval.matchAll(/^\[submodule "(.+)"\]$/gm)].map((m) => m[1]));
|
|
8168
|
+
const missing = modules.filter((m) => !existingNames.has(m.name));
|
|
8169
|
+
let result = afterRemoval;
|
|
8170
|
+
for (const m of missing) {
|
|
8171
|
+
if (!result.endsWith(`
|
|
8172
|
+
`))
|
|
8173
|
+
result += `
|
|
8174
|
+
`;
|
|
8175
|
+
result += `[submodule "${m.name}"]
|
|
8176
|
+
active = true
|
|
8177
|
+
url = ${m.url}
|
|
8178
|
+
path = ${m.path}
|
|
8179
|
+
`;
|
|
8180
|
+
}
|
|
8181
|
+
if (removed.length === 0 && missing.length === 0)
|
|
8182
|
+
return [];
|
|
8183
|
+
await writeFile(configPath, result, "utf8");
|
|
8184
|
+
return [
|
|
8185
|
+
...removed.map((name) => ({ location: [".git", "config", name], action: "deleted" })),
|
|
8186
|
+
...missing.map((m) => ({ location: [".git", "config", m.name], action: "synced" }))
|
|
8187
|
+
];
|
|
8188
|
+
}
|
|
8189
|
+
async function fixModuleDirs(repoRoot, modules, moduleDirs, orphanModuleDirs) {
|
|
8190
|
+
const fixes = [];
|
|
8137
8191
|
const modulesRoot = resolve3(repoRoot, ".git", "modules");
|
|
8138
|
-
const
|
|
8192
|
+
const gmByPath = new Map(modules.map((m) => [m.path, m]));
|
|
8139
8193
|
for (const d of moduleDirs) {
|
|
8140
|
-
const
|
|
8141
|
-
if (!d.remoteUrl) {
|
|
8142
|
-
console.log(`${prefix}: no remote URL, skipping`);
|
|
8143
|
-
continue;
|
|
8144
|
-
}
|
|
8145
|
-
const gm = gmByRepoPath.get(repoPath(d.remoteUrl));
|
|
8194
|
+
const gm = gmByPath.get(d.relativePath);
|
|
8146
8195
|
if (!gm) {
|
|
8147
8196
|
await rm(join2(modulesRoot, d.relativePath), { recursive: true, force: true });
|
|
8148
|
-
|
|
8149
|
-
continue;
|
|
8150
|
-
}
|
|
8151
|
-
if (d.relativePath === gm.path) {
|
|
8152
|
-
console.log(`${prefix}: already correct`);
|
|
8153
|
-
continue;
|
|
8154
|
-
}
|
|
8155
|
-
const targetAbs = join2(modulesRoot, gm.path);
|
|
8156
|
-
if (existsSync3(targetAbs)) {
|
|
8157
|
-
console.log(`${prefix}: target ${gm.path} already exists, skipping`);
|
|
8158
|
-
continue;
|
|
8197
|
+
fixes.push({ location: [".git", "modules", ...d.relativePath.split("/")], action: "deleted" });
|
|
8159
8198
|
}
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8199
|
+
}
|
|
8200
|
+
for (const rel of orphanModuleDirs) {
|
|
8201
|
+
await rm(join2(modulesRoot, rel), { recursive: true, force: true });
|
|
8202
|
+
fixes.push({ location: [".git", "modules", ...rel.split("/")], action: "deleted" });
|
|
8163
8203
|
}
|
|
8164
8204
|
for (const m of modules) {
|
|
8165
8205
|
const workingTreeDir = resolve3(repoRoot, m.path);
|
|
8166
8206
|
const moduleGitDir = join2(modulesRoot, m.path);
|
|
8167
8207
|
const dotGit = join2(workingTreeDir, ".git");
|
|
8168
|
-
if (!existsSync3(workingTreeDir)
|
|
8208
|
+
if (!existsSync3(workingTreeDir))
|
|
8209
|
+
continue;
|
|
8210
|
+
if (existsSync3(dotGit) && statSync2(dotGit).isDirectory()) {
|
|
8211
|
+
if (existsSync3(moduleGitDir))
|
|
8212
|
+
continue;
|
|
8213
|
+
await mkdir(dirname(moduleGitDir), { recursive: true });
|
|
8214
|
+
await rename(dotGit, moduleGitDir);
|
|
8215
|
+
const rel2 = relative(workingTreeDir, moduleGitDir);
|
|
8216
|
+
await writeFile(dotGit, `gitdir: ${rel2}
|
|
8217
|
+
`, "utf8");
|
|
8218
|
+
fixes.push({ location: [...m.path.split("/"), ".git"], action: "synced", detail: "migrated standalone .git" });
|
|
8219
|
+
continue;
|
|
8220
|
+
}
|
|
8221
|
+
if (!existsSync3(moduleGitDir)) {
|
|
8222
|
+
await mkdir(dirname(moduleGitDir), { recursive: true });
|
|
8223
|
+
await spawnGit(["clone", "--bare", m.url, moduleGitDir], repoRoot);
|
|
8224
|
+
const configFile = `.git/modules/${m.path}/config`;
|
|
8225
|
+
const worktree = relative(moduleGitDir, workingTreeDir);
|
|
8226
|
+
await spawnGit(["config", "-f", configFile, "core.bare", "false"], repoRoot);
|
|
8227
|
+
await spawnGit(["config", "-f", configFile, "core.worktree", worktree], repoRoot);
|
|
8228
|
+
fixes.push({ location: [".git", "modules", ...m.path.split("/")], action: "synced", detail: "initialized" });
|
|
8229
|
+
continue;
|
|
8230
|
+
}
|
|
8231
|
+
if (existsSync3(dotGit))
|
|
8169
8232
|
continue;
|
|
8170
8233
|
const rel = relative(workingTreeDir, moduleGitDir);
|
|
8171
8234
|
await writeFile(dotGit, `gitdir: ${rel}
|
|
8172
8235
|
`, "utf8");
|
|
8173
|
-
|
|
8236
|
+
fixes.push({ location: [...m.path.split("/"), ".git"], action: "synced" });
|
|
8174
8237
|
}
|
|
8175
|
-
console.log(" running: git submodule sync");
|
|
8176
|
-
await spawnGit(["submodule", "sync"], repoRoot);
|
|
8177
8238
|
for (const m of modules) {
|
|
8178
8239
|
const moduleGitDir = join2(modulesRoot, m.path);
|
|
8179
8240
|
if (!existsSync3(moduleGitDir))
|
|
8180
8241
|
continue;
|
|
8242
|
+
const configFile = `.git/modules/${m.path}/config`;
|
|
8243
|
+
const readConfig = (key) => Bun.spawn(["git", "config", "-f", configFile, key], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" });
|
|
8244
|
+
const [currentUrl, currentWorktree] = await Promise.all([
|
|
8245
|
+
new Response(readConfig("remote.origin.url").stdout).text().then((t) => t.trim()),
|
|
8246
|
+
new Response(readConfig("core.worktree").stdout).text().then((t) => t.trim())
|
|
8247
|
+
]);
|
|
8181
8248
|
const worktree = relative(moduleGitDir, resolve3(repoRoot, m.path));
|
|
8182
|
-
|
|
8183
|
-
|
|
8249
|
+
if (currentUrl !== m.url) {
|
|
8250
|
+
await spawnGit(["config", "-f", configFile, "remote.origin.url", m.url], repoRoot);
|
|
8251
|
+
fixes.push({ location: [".git", "modules", ...m.path.split("/"), "config"], action: "synced", detail: "remote.origin.url" });
|
|
8252
|
+
}
|
|
8253
|
+
if (currentWorktree !== worktree) {
|
|
8254
|
+
await spawnGit(["config", "-f", configFile, "core.worktree", worktree], repoRoot);
|
|
8255
|
+
fixes.push({ location: [".git", "modules", ...m.path.split("/"), "config"], action: "synced", detail: "core.worktree" });
|
|
8256
|
+
}
|
|
8184
8257
|
}
|
|
8258
|
+
return fixes;
|
|
8185
8259
|
}
|
|
8186
|
-
async function fixIndexGitlinks(repoRoot, modules) {
|
|
8260
|
+
async function fixIndexGitlinks(repoRoot, modules, currentIndex) {
|
|
8261
|
+
const currentShas = new Map(currentIndex.map((e) => [e.path, e.commit]));
|
|
8262
|
+
const fixes = [];
|
|
8187
8263
|
for (const m of modules) {
|
|
8188
8264
|
const workingTree = resolve3(repoRoot, m.path);
|
|
8189
|
-
if (!existsSync3(workingTree))
|
|
8190
|
-
console.log(` ${m.path}: working tree missing, skipping`);
|
|
8265
|
+
if (!existsSync3(workingTree))
|
|
8191
8266
|
continue;
|
|
8192
|
-
}
|
|
8193
|
-
const shaProc = Bun.spawn(["git", "rev-parse", "HEAD"], { cwd: workingTree, stdout: "pipe" });
|
|
8267
|
+
const shaProc = Bun.spawn(["git", "rev-parse", "HEAD"], { cwd: workingTree, stdout: "pipe", stderr: "pipe" });
|
|
8194
8268
|
const sha = (await new Response(shaProc.stdout).text()).trim();
|
|
8195
|
-
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
8196
|
-
console.log(` ${m.path}: could not read HEAD (${sha}), skipping`);
|
|
8269
|
+
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
8197
8270
|
continue;
|
|
8271
|
+
const current = currentShas.get(m.path);
|
|
8272
|
+
await spawnGit(["update-index", "--add", "--cacheinfo", `160000,${sha},${m.path}`], repoRoot);
|
|
8273
|
+
if (current !== sha) {
|
|
8274
|
+
fixes.push({ location: [".git", "index"], action: "synced", detail: `${m.path}: ${sha.slice(0, 8)}` });
|
|
8198
8275
|
}
|
|
8199
|
-
await spawnGit(["update-index", "--cacheinfo", `160000,${sha},${m.path}`], repoRoot);
|
|
8200
|
-
console.log(` ${m.path}: ${sha.slice(0, 8)}`);
|
|
8201
|
-
}
|
|
8202
|
-
}
|
|
8203
|
-
async function fixGitConfig(repoRoot, modules) {
|
|
8204
|
-
const configPath = resolve3(repoRoot, ".git", "config");
|
|
8205
|
-
const content = await readFile4(configPath, "utf8");
|
|
8206
|
-
const validNames = new Set(modules.map((m) => m.name));
|
|
8207
|
-
const { result, removed } = removeExtraConfigSubmodules(content, validNames);
|
|
8208
|
-
if (removed.length === 0) {
|
|
8209
|
-
console.log(" .git/config: no extra submodule entries");
|
|
8210
|
-
return;
|
|
8211
|
-
}
|
|
8212
|
-
await writeFile(configPath, result, "utf8");
|
|
8213
|
-
for (const name of removed) {
|
|
8214
|
-
console.log(` .git/config: removed [submodule "${name}"]`);
|
|
8215
8276
|
}
|
|
8277
|
+
return fixes;
|
|
8216
8278
|
}
|
|
8217
8279
|
|
|
8218
8280
|
// src/commands/git/sync-submodules/printer.ts
|
|
8219
8281
|
var RED = "\x1B[31m";
|
|
8282
|
+
var GREEN = "\x1B[32m";
|
|
8283
|
+
var CYAN = "\x1B[36m";
|
|
8284
|
+
var DIM = "\x1B[2m";
|
|
8285
|
+
var BOLD = "\x1B[1m";
|
|
8220
8286
|
var RESET = "\x1B[0m";
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
}
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
const
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
console.log(
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
item.print();
|
|
8287
|
+
var ok = `${GREEN}\u2713${RESET}`;
|
|
8288
|
+
var fail = (msg) => `${RED}\u2717 ${msg}${RESET}`;
|
|
8289
|
+
var fsName = (s) => `${CYAN}${s}${RESET}`;
|
|
8290
|
+
var key = (s) => `${DIM}${s}${RESET}`;
|
|
8291
|
+
function render(lines, prefix = "") {
|
|
8292
|
+
for (let i = 0;i < lines.length; i++) {
|
|
8293
|
+
const last = i === lines.length - 1;
|
|
8294
|
+
const branch = last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
8295
|
+
const child = last ? " " : "\u2502 ";
|
|
8296
|
+
const l = lines[i];
|
|
8297
|
+
const label = l.isKey ? key(l.label) : fsName(l.label);
|
|
8298
|
+
let out = `${prefix}${branch}${label}`;
|
|
8299
|
+
if (l.value !== undefined)
|
|
8300
|
+
out += `: ${l.value}`;
|
|
8301
|
+
if (l.status !== undefined)
|
|
8302
|
+
out += ` ${l.status}`;
|
|
8303
|
+
console.log(out);
|
|
8304
|
+
if (l.children?.length)
|
|
8305
|
+
render(l.children, prefix + child);
|
|
8241
8306
|
}
|
|
8242
8307
|
}
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
constructor(label, message) {
|
|
8248
|
-
super();
|
|
8249
|
-
this.label = label;
|
|
8250
|
-
this.message = message;
|
|
8251
|
-
}
|
|
8252
|
-
header() {
|
|
8253
|
-
return this.label;
|
|
8254
|
-
}
|
|
8255
|
-
fields() {
|
|
8256
|
-
return [];
|
|
8257
|
-
}
|
|
8258
|
-
print(indent = " ") {
|
|
8259
|
-
console.log(`${RED}${indent}${this.label} \u2190 ${this.message}${RESET}`);
|
|
8260
|
-
}
|
|
8308
|
+
function buildPathTree(parts, children) {
|
|
8309
|
+
if (parts.length === 1)
|
|
8310
|
+
return { label: parts[0], children };
|
|
8311
|
+
return { label: parts[0], children: [buildPathTree(parts.slice(1), children)] };
|
|
8261
8312
|
}
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
}
|
|
8269
|
-
header() {
|
|
8270
|
-
return `[${this.m.name}]`;
|
|
8271
|
-
}
|
|
8272
|
-
fields() {
|
|
8273
|
-
return [
|
|
8274
|
-
{ key: "path", value: this.m.path },
|
|
8275
|
-
{ key: "url", value: this.m.url }
|
|
8276
|
-
];
|
|
8277
|
-
}
|
|
8313
|
+
function resultLine(label, result) {
|
|
8314
|
+
if (result === undefined)
|
|
8315
|
+
return { label, isKey: true, status: fail("unknown") };
|
|
8316
|
+
if (typeof result === "string")
|
|
8317
|
+
return { label, isKey: true, value: result };
|
|
8318
|
+
return { label, isKey: true, status: fail(result.error) };
|
|
8278
8319
|
}
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
return
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
return "not in .gitmodules";
|
|
8298
|
-
}
|
|
8299
|
-
fields() {
|
|
8300
|
-
const { e, gm } = this;
|
|
8301
|
-
return [
|
|
8302
|
-
{ key: "path", value: e.path ?? "(none)", error: gm && e.path !== gm.path ? `expected ${gm.path}` : undefined },
|
|
8303
|
-
{ key: "url", value: e.url ?? "(none)", error: gm && e.url !== gm.url ? `expected ${gm.url}` : undefined },
|
|
8304
|
-
{ key: "active", value: String(e.active ?? "(unset)") }
|
|
8305
|
-
];
|
|
8306
|
-
}
|
|
8320
|
+
function moduleHasIssues(m, cfg, dir, idx, headSha, wt) {
|
|
8321
|
+
if (!cfg)
|
|
8322
|
+
return true;
|
|
8323
|
+
if (cfg.path !== m.path || cfg.url !== m.url)
|
|
8324
|
+
return true;
|
|
8325
|
+
if (!dir)
|
|
8326
|
+
return true;
|
|
8327
|
+
if (!idx)
|
|
8328
|
+
return true;
|
|
8329
|
+
if (headSha && idx.commit !== headSha)
|
|
8330
|
+
return true;
|
|
8331
|
+
if (!wt || wt.type === "missing" || wt.type === "dir")
|
|
8332
|
+
return true;
|
|
8333
|
+
if (typeof wt.headCommit !== "string")
|
|
8334
|
+
return true;
|
|
8335
|
+
if (typeof wt.remote !== "string" || wt.remote !== m.url)
|
|
8336
|
+
return true;
|
|
8337
|
+
return false;
|
|
8307
8338
|
}
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
d;
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8339
|
+
function printSyncState(modules, state) {
|
|
8340
|
+
const byName = new Map(state.config.map((c) => [c.name, c]));
|
|
8341
|
+
const byPath = new Map(state.moduleDirs.map((d) => [d.relativePath, d]));
|
|
8342
|
+
const byIndexPath = new Map(state.index.map((i) => [i.path, i]));
|
|
8343
|
+
const byTreePath = new Map(state.workingTree.map((w) => [w.path, w]));
|
|
8344
|
+
const modulePaths = new Set(modules.map((m) => m.path));
|
|
8345
|
+
const moduleNames = new Set(modules.map((m) => m.name));
|
|
8346
|
+
console.log("=== submodules ===");
|
|
8347
|
+
for (const m of modules) {
|
|
8348
|
+
const cfg = byName.get(m.name);
|
|
8349
|
+
const dir = byPath.get(m.path);
|
|
8350
|
+
const idx = byIndexPath.get(m.path);
|
|
8351
|
+
const wt = byTreePath.get(m.path);
|
|
8352
|
+
const headSha = typeof wt?.headCommit === "string" ? wt.headCommit : undefined;
|
|
8353
|
+
console.log(`
|
|
8354
|
+
${BOLD}[${m.name}]${RESET}`);
|
|
8355
|
+
if (!moduleHasIssues(m, cfg, dir, idx, headSha, wt)) {
|
|
8356
|
+
console.log(`\u2514\u2500\u2500 ${ok}`);
|
|
8357
|
+
continue;
|
|
8358
|
+
}
|
|
8359
|
+
const gitmodulesNode = {
|
|
8360
|
+
label: ".gitmodules",
|
|
8361
|
+
children: [
|
|
8362
|
+
{ label: "path", isKey: true, value: m.path },
|
|
8363
|
+
{ label: "url", isKey: true, value: m.url }
|
|
8364
|
+
]
|
|
8365
|
+
};
|
|
8366
|
+
let configNode;
|
|
8367
|
+
if (!cfg) {
|
|
8368
|
+
configNode = { label: "config", status: fail("missing") };
|
|
8369
|
+
} else {
|
|
8370
|
+
configNode = {
|
|
8371
|
+
label: "config",
|
|
8372
|
+
children: [
|
|
8373
|
+
{
|
|
8374
|
+
label: "path",
|
|
8375
|
+
isKey: true,
|
|
8376
|
+
value: cfg.path ?? "(missing)",
|
|
8377
|
+
status: cfg.path === m.path ? ok : fail(`expected ${m.path}`)
|
|
8378
|
+
},
|
|
8379
|
+
{
|
|
8380
|
+
label: "url",
|
|
8381
|
+
isKey: true,
|
|
8382
|
+
value: cfg.url ?? "(missing)",
|
|
8383
|
+
status: cfg.url === m.url ? ok : fail(`expected ${m.url}`)
|
|
8384
|
+
}
|
|
8385
|
+
]
|
|
8386
|
+
};
|
|
8387
|
+
}
|
|
8388
|
+
const modulesNode = {
|
|
8389
|
+
label: "modules",
|
|
8390
|
+
children: [{ label: m.path, status: dir ? ok : fail("missing") }]
|
|
8391
|
+
};
|
|
8392
|
+
let indexNode;
|
|
8393
|
+
if (!idx) {
|
|
8394
|
+
indexNode = { label: "index", status: fail("missing") };
|
|
8395
|
+
} else {
|
|
8396
|
+
const idxSha8 = idx.commit.slice(0, 8);
|
|
8397
|
+
const matches = headSha && idx.commit === headSha;
|
|
8398
|
+
const mismatches = headSha && idx.commit !== headSha;
|
|
8399
|
+
indexNode = {
|
|
8400
|
+
label: "index",
|
|
8401
|
+
value: idxSha8,
|
|
8402
|
+
status: matches ? ok : mismatches ? fail(`head is ${headSha.slice(0, 8)}`) : undefined
|
|
8403
|
+
};
|
|
8404
|
+
}
|
|
8405
|
+
const dotGitNode = {
|
|
8406
|
+
label: ".git",
|
|
8407
|
+
children: [configNode, modulesNode, indexNode]
|
|
8408
|
+
};
|
|
8409
|
+
const wtLeaves = [];
|
|
8410
|
+
if (!wt || wt.type === "missing") {
|
|
8411
|
+
wtLeaves.push({ label: ".git", status: fail("missing") });
|
|
8412
|
+
} else if (wt.type === "dir") {
|
|
8413
|
+
wtLeaves.push({ label: ".git", status: fail("directory (standalone clone)") });
|
|
8414
|
+
} else {
|
|
8415
|
+
wtLeaves.push({ label: ".git", value: `gitdir \u2192 ${wt.gitdirTarget}` });
|
|
8416
|
+
}
|
|
8417
|
+
if (wt) {
|
|
8418
|
+
const head = headSha ?? wt.headCommit;
|
|
8419
|
+
wtLeaves.push(resultLine("head", typeof head === "string" ? head.slice(0, 8) : head));
|
|
8420
|
+
if (typeof wt.remote === "string") {
|
|
8421
|
+
wtLeaves.push({
|
|
8422
|
+
label: "remote",
|
|
8423
|
+
isKey: true,
|
|
8424
|
+
value: wt.remote,
|
|
8425
|
+
status: wt.remote === m.url ? ok : fail(`expected ${m.url}`)
|
|
8426
|
+
});
|
|
8427
|
+
} else {
|
|
8428
|
+
wtLeaves.push(resultLine("remote", wt.remote));
|
|
8429
|
+
}
|
|
8430
|
+
}
|
|
8431
|
+
const workingTreeNode = buildPathTree(m.path.split("/"), wtLeaves);
|
|
8432
|
+
render([gitmodulesNode, dotGitNode, workingTreeNode]);
|
|
8322
8433
|
}
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8434
|
+
const staleConfig = state.config.filter((c) => !moduleNames.has(c.name));
|
|
8435
|
+
const staleDirs = state.moduleDirs.filter((d) => !modulePaths.has(d.relativePath));
|
|
8436
|
+
const staleIndex = state.index.filter((i) => !modulePaths.has(i.path));
|
|
8437
|
+
if (staleConfig.length || staleDirs.length || state.orphanModuleDirs.length || staleIndex.length) {
|
|
8438
|
+
console.log(`
|
|
8439
|
+
${RED}${BOLD}=== stale (not in .gitmodules) ===${RESET}`);
|
|
8440
|
+
const staleNodes = [];
|
|
8441
|
+
if (staleConfig.length) {
|
|
8442
|
+
staleNodes.push({
|
|
8443
|
+
label: ".git/config",
|
|
8444
|
+
children: staleConfig.map((c) => ({ label: c.name }))
|
|
8445
|
+
});
|
|
8446
|
+
}
|
|
8447
|
+
if (staleDirs.length || state.orphanModuleDirs.length) {
|
|
8448
|
+
staleNodes.push({
|
|
8449
|
+
label: ".git/modules",
|
|
8450
|
+
children: [
|
|
8451
|
+
...staleDirs.map((d) => ({ label: d.relativePath })),
|
|
8452
|
+
...state.orphanModuleDirs.map((d) => ({ label: d }))
|
|
8453
|
+
]
|
|
8454
|
+
});
|
|
8455
|
+
}
|
|
8456
|
+
if (staleIndex.length) {
|
|
8457
|
+
staleNodes.push({
|
|
8458
|
+
label: ".git/index",
|
|
8459
|
+
children: staleIndex.map((i) => ({ label: i.path, isKey: true, value: i.commit.slice(0, 8) }))
|
|
8460
|
+
});
|
|
8461
|
+
}
|
|
8462
|
+
render(staleNodes);
|
|
8328
8463
|
}
|
|
8329
8464
|
}
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
header() {
|
|
8340
|
-
return this.e.path;
|
|
8341
|
-
}
|
|
8342
|
-
headerError() {
|
|
8343
|
-
return !this.knownPath ? "not in .gitmodules" : undefined;
|
|
8344
|
-
}
|
|
8345
|
-
fields() {
|
|
8346
|
-
return [{ key: "commit", value: this.e.commit.slice(0, 8) }];
|
|
8465
|
+
function addToFixTree(tree, fix, depth = 0) {
|
|
8466
|
+
const seg = fix.location[depth];
|
|
8467
|
+
if (!tree.has(seg))
|
|
8468
|
+
tree.set(seg, { subtree: new Map, fixes: [] });
|
|
8469
|
+
const node = tree.get(seg);
|
|
8470
|
+
if (depth === fix.location.length - 1) {
|
|
8471
|
+
node.fixes.push(fix);
|
|
8472
|
+
} else {
|
|
8473
|
+
addToFixTree(node.subtree, fix, depth + 1);
|
|
8347
8474
|
}
|
|
8348
8475
|
}
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
if (this.e.type === "dir")
|
|
8363
|
-
return "expected gitdir file, got directory";
|
|
8364
|
-
}
|
|
8365
|
-
fields() {
|
|
8366
|
-
if (this.e.type === "file")
|
|
8367
|
-
return [{ value: `gitdir -> ${this.e.gitdirTarget}` }];
|
|
8368
|
-
return [];
|
|
8476
|
+
function fixTreeToLines(tree) {
|
|
8477
|
+
const lines = [];
|
|
8478
|
+
for (const [label, { subtree, fixes }] of tree) {
|
|
8479
|
+
const children = fixTreeToLines(subtree);
|
|
8480
|
+
if (fixes.length > 0) {
|
|
8481
|
+
for (const fix of fixes) {
|
|
8482
|
+
const actionStr = fix.action === "deleted" ? `${RED}deleted${RESET}` : `${GREEN}synced${RESET}`;
|
|
8483
|
+
const status = fix.detail ? `${actionStr} ${fix.detail}` : actionStr;
|
|
8484
|
+
lines.push({ label, status, children: children.length > 0 ? children : undefined });
|
|
8485
|
+
}
|
|
8486
|
+
} else {
|
|
8487
|
+
lines.push({ label, children });
|
|
8488
|
+
}
|
|
8369
8489
|
}
|
|
8490
|
+
return lines;
|
|
8491
|
+
}
|
|
8492
|
+
function printFixes(fixes) {
|
|
8493
|
+
if (fixes.length === 0)
|
|
8494
|
+
return;
|
|
8495
|
+
const tree = new Map;
|
|
8496
|
+
for (const fix of fixes)
|
|
8497
|
+
addToFixTree(tree, fix);
|
|
8498
|
+
console.log(`
|
|
8499
|
+
applied fixes:`);
|
|
8500
|
+
render(fixTreeToLines(tree));
|
|
8370
8501
|
}
|
|
8371
8502
|
|
|
8372
8503
|
// src/commands/git/sync-submodules/index.ts
|
|
8373
8504
|
var REPO_ROOT = process.cwd();
|
|
8374
8505
|
var GITMODULES_PATH = resolve4(REPO_ROOT, ".gitmodules");
|
|
8375
|
-
function buildSections(modules, state) {
|
|
8376
|
-
const byName = new Map(modules.map((m) => [m.name, m]));
|
|
8377
|
-
const byPath = new Map(modules.map((m) => [m.path, m]));
|
|
8378
|
-
const gitmodulesItems = modules.map((m) => new GitmodulesEntryItem(m));
|
|
8379
|
-
const seenConfigNames = new Set;
|
|
8380
|
-
const configItems = state.config.map((e) => {
|
|
8381
|
-
const isDuplicate = seenConfigNames.has(e.name);
|
|
8382
|
-
seenConfigNames.add(e.name);
|
|
8383
|
-
return new GitConfigEntryItem(e, byName.get(e.name), isDuplicate);
|
|
8384
|
-
});
|
|
8385
|
-
for (const m of modules) {
|
|
8386
|
-
if (!seenConfigNames.has(m.name))
|
|
8387
|
-
configItems.push(new GhostItem(`[${m.name}]`, "missing from .git/config"));
|
|
8388
|
-
}
|
|
8389
|
-
const seenModulePaths = new Set;
|
|
8390
|
-
const moduleDirItems = state.moduleDirs.map((d) => {
|
|
8391
|
-
seenModulePaths.add(d.relativePath);
|
|
8392
|
-
return new ModuleDirItem(d, byPath.get(d.relativePath));
|
|
8393
|
-
});
|
|
8394
|
-
for (const m of modules) {
|
|
8395
|
-
if (!seenModulePaths.has(m.path))
|
|
8396
|
-
moduleDirItems.push(new GhostItem(m.path, "missing from .git/modules/"));
|
|
8397
|
-
}
|
|
8398
|
-
const seenIndexPaths = new Set;
|
|
8399
|
-
const indexItems = state.index.map((e) => {
|
|
8400
|
-
seenIndexPaths.add(e.path);
|
|
8401
|
-
return new IndexGitlinkItem(e, byPath.has(e.path));
|
|
8402
|
-
});
|
|
8403
|
-
for (const m of modules) {
|
|
8404
|
-
if (!seenIndexPaths.has(m.path))
|
|
8405
|
-
indexItems.push(new GhostItem(m.path, "missing from index"));
|
|
8406
|
-
}
|
|
8407
|
-
const workingTreeItems = state.workingTree.map((e) => new WorkingTreeItem(e));
|
|
8408
|
-
return { gitmodulesItems, configItems, moduleDirItems, indexItems, workingTreeItems };
|
|
8409
|
-
}
|
|
8410
8506
|
async function main() {
|
|
8411
8507
|
const content = await readFile5(GITMODULES_PATH, "utf8");
|
|
8412
8508
|
const modules = parse(content);
|
|
@@ -8418,20 +8514,14 @@ async function main() {
|
|
|
8418
8514
|
process.exit(1);
|
|
8419
8515
|
}
|
|
8420
8516
|
const submodulePaths = modules.map((m) => m.path);
|
|
8421
|
-
console.log("=== fixing ===");
|
|
8422
|
-
await fixGitConfig(REPO_ROOT, modules);
|
|
8423
|
-
const preFixState = await readGitState(REPO_ROOT, submodulePaths);
|
|
8424
|
-
await fixModuleDirs(REPO_ROOT, modules, preFixState.moduleDirs);
|
|
8425
|
-
await fixIndexGitlinks(REPO_ROOT, modules);
|
|
8426
8517
|
const state = await readGitState(REPO_ROOT, submodulePaths);
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
PrintableItem.printGroup("working tree .git entries", workingTreeItems);
|
|
8518
|
+
printSyncState(modules, state);
|
|
8519
|
+
const appliedFixes = [
|
|
8520
|
+
...await fixGitConfig(REPO_ROOT, modules),
|
|
8521
|
+
...await fixModuleDirs(REPO_ROOT, modules, state.moduleDirs, state.orphanModuleDirs),
|
|
8522
|
+
...await fixIndexGitlinks(REPO_ROOT, modules, state.index)
|
|
8523
|
+
];
|
|
8524
|
+
printFixes(appliedFixes);
|
|
8435
8525
|
const result = serialize(sorted(modules));
|
|
8436
8526
|
if (result !== content) {
|
|
8437
8527
|
await writeFile2(GITMODULES_PATH, result, "utf8");
|
|
@@ -8876,9 +8966,89 @@ var npm = new Command("npm").summary("npm package management utilities");
|
|
|
8876
8966
|
npm.addCommand(cmd);
|
|
8877
8967
|
});
|
|
8878
8968
|
|
|
8969
|
+
// src/commands/upgrade.ts
|
|
8970
|
+
import { existsSync as existsSync5 } from "fs";
|
|
8971
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
8972
|
+
function findLockFile(dir) {
|
|
8973
|
+
let current = dir;
|
|
8974
|
+
while (true) {
|
|
8975
|
+
if (existsSync5(join4(current, "bun.lock")) || existsSync5(join4(current, "bun.lockb")))
|
|
8976
|
+
return "bun";
|
|
8977
|
+
if (existsSync5(join4(current, "pnpm-lock.yaml")))
|
|
8978
|
+
return "pnpm";
|
|
8979
|
+
if (existsSync5(join4(current, "package-lock.json")))
|
|
8980
|
+
return "npm";
|
|
8981
|
+
const parent = dirname3(current);
|
|
8982
|
+
if (parent === current)
|
|
8983
|
+
return;
|
|
8984
|
+
current = parent;
|
|
8985
|
+
}
|
|
8986
|
+
}
|
|
8987
|
+
function detectInstall(binaryPath) {
|
|
8988
|
+
if (binaryPath.includes("/.bun/bin/")) {
|
|
8989
|
+
return { pm: "bun", scope: "global" };
|
|
8990
|
+
}
|
|
8991
|
+
if (binaryPath.includes("/pnpm/")) {
|
|
8992
|
+
return { pm: "pnpm", scope: "global" };
|
|
8993
|
+
}
|
|
8994
|
+
const localMatch = binaryPath.match(/^(.+\/node_modules)\/.bin\//);
|
|
8995
|
+
if (localMatch) {
|
|
8996
|
+
const projectRoot = dirname3(localMatch[1]);
|
|
8997
|
+
const pm = findLockFile(projectRoot) ?? "npm";
|
|
8998
|
+
return { pm, scope: "local", cwd: projectRoot };
|
|
8999
|
+
}
|
|
9000
|
+
return;
|
|
9001
|
+
}
|
|
9002
|
+
function buildUpgradeCommand(info) {
|
|
9003
|
+
const pkg = "@ozyman42/ozy-cli@latest";
|
|
9004
|
+
if (info.scope === "global") {
|
|
9005
|
+
if (info.pm === "bun")
|
|
9006
|
+
return ["bun", "add", "--no-cache", "-g", pkg];
|
|
9007
|
+
if (info.pm === "pnpm")
|
|
9008
|
+
return ["pnpm", "add", "-g", pkg];
|
|
9009
|
+
return ["npm", "install", "-g", pkg];
|
|
9010
|
+
}
|
|
9011
|
+
if (info.pm === "bun")
|
|
9012
|
+
return ["bun", "add", "--no-cache", pkg];
|
|
9013
|
+
if (info.pm === "pnpm")
|
|
9014
|
+
return ["pnpm", "add", pkg];
|
|
9015
|
+
return ["npm", "install", pkg];
|
|
9016
|
+
}
|
|
9017
|
+
async function whichOzy() {
|
|
9018
|
+
const proc = Bun.spawn(["which", "ozy"], { stdout: "pipe", stderr: "pipe" });
|
|
9019
|
+
const code = await proc.exited;
|
|
9020
|
+
if (code !== 0)
|
|
9021
|
+
return;
|
|
9022
|
+
return (await new Response(proc.stdout).text()).trim();
|
|
9023
|
+
}
|
|
9024
|
+
var upgrade = makeCommand("upgrade", "upgrade ozy-cli to the latest version", async () => {
|
|
9025
|
+
const binaryPath = await whichOzy();
|
|
9026
|
+
if (!binaryPath) {
|
|
9027
|
+
return Err("not-installed", "ozy binary not found \u2014 install with: bun add -g @ozyman42/ozy-cli");
|
|
9028
|
+
}
|
|
9029
|
+
const info = detectInstall(binaryPath);
|
|
9030
|
+
if (!info) {
|
|
9031
|
+
return Err("unknown-install", `could not detect package manager from binary path: ${binaryPath}
|
|
9032
|
+
Upgrade manually with your package manager.`);
|
|
9033
|
+
}
|
|
9034
|
+
const cmd = buildUpgradeCommand(info);
|
|
9035
|
+
console.log(`Detected: ${info.pm} (${info.scope})`);
|
|
9036
|
+
console.log(`Running: ${cmd.join(" ")}`);
|
|
9037
|
+
const proc = Bun.spawn(cmd, {
|
|
9038
|
+
cwd: info.cwd ?? process.cwd(),
|
|
9039
|
+
stdout: "inherit",
|
|
9040
|
+
stderr: "inherit"
|
|
9041
|
+
});
|
|
9042
|
+
const code = await proc.exited;
|
|
9043
|
+
if (code !== 0) {
|
|
9044
|
+
return Err("install-failed", `${cmd[0]} exited with code ${code}`);
|
|
9045
|
+
}
|
|
9046
|
+
return Ok(true);
|
|
9047
|
+
});
|
|
9048
|
+
|
|
8879
9049
|
// src/commands/index.ts
|
|
8880
9050
|
program.name("ozy").version(version);
|
|
8881
|
-
[git, npm].forEach((cmd) => {
|
|
9051
|
+
[git, npm, upgrade].forEach((cmd) => {
|
|
8882
9052
|
program.addCommand(cmd);
|
|
8883
9053
|
});
|
|
8884
9054
|
program.parse();
|