@ozyman42/ozy-cli 0.0.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +862 -273
- 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.0
|
|
2787
|
+
var version = "0.2.0";
|
|
2788
2788
|
|
|
2789
2789
|
// node_modules/commander/esm.mjs
|
|
2790
2790
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -2815,9 +2815,13 @@ function log(...stuff) {
|
|
|
2815
2815
|
// src/common/command.ts
|
|
2816
2816
|
function makeCommand(name, description, action) {
|
|
2817
2817
|
return new Command(name).description(description).action(async () => {
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2818
|
+
try {
|
|
2819
|
+
const result = await action();
|
|
2820
|
+
if (!result.success) {
|
|
2821
|
+
log(`\u2717 ${result.error}: ${result.reason}`);
|
|
2822
|
+
}
|
|
2823
|
+
} catch (err) {
|
|
2824
|
+
log(`\u2717 unexpected error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2821
2825
|
}
|
|
2822
2826
|
process.exit();
|
|
2823
2827
|
});
|
|
@@ -8024,22 +8028,30 @@ function parseIndexGitlinks(output) {
|
|
|
8024
8028
|
return { commit: match2[1], path: match2[2] };
|
|
8025
8029
|
});
|
|
8026
8030
|
}
|
|
8027
|
-
function
|
|
8031
|
+
function scanModuleDirs(root, modulePaths, rel = "") {
|
|
8032
|
+
const gitDirs = [];
|
|
8033
|
+
const orphanDirs = [];
|
|
8028
8034
|
if (!existsSync2(root))
|
|
8029
|
-
return
|
|
8030
|
-
const results = [];
|
|
8035
|
+
return { gitDirs, orphanDirs };
|
|
8031
8036
|
for (const entry of readdirSync(root)) {
|
|
8032
8037
|
const abs = join(root, entry);
|
|
8033
|
-
const relPath = rel ? `${rel}/${entry}` : entry;
|
|
8034
8038
|
if (!statSync(abs).isDirectory())
|
|
8035
8039
|
continue;
|
|
8040
|
+
const relPath = rel ? `${rel}/${entry}` : entry;
|
|
8036
8041
|
if (existsSync2(join(abs, "HEAD"))) {
|
|
8037
|
-
|
|
8042
|
+
gitDirs.push(relPath);
|
|
8038
8043
|
} else {
|
|
8039
|
-
|
|
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
|
+
}
|
|
8040
8052
|
}
|
|
8041
8053
|
}
|
|
8042
|
-
return
|
|
8054
|
+
return { gitDirs, orphanDirs };
|
|
8043
8055
|
}
|
|
8044
8056
|
async function readModuleDirRemoteUrl(abs) {
|
|
8045
8057
|
const configPath = join(abs, "config");
|
|
@@ -8049,15 +8061,38 @@ async function readModuleDirRemoteUrl(abs) {
|
|
|
8049
8061
|
const match2 = content.match(/\[remote "origin"\][^\[]*\n\s+url\s*=\s*(.+)/);
|
|
8050
8062
|
return match2?.[1].trim();
|
|
8051
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
|
+
}
|
|
8052
8074
|
async function readWorkingTreeEntry(repoRoot, subPath) {
|
|
8053
|
-
const
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
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 };
|
|
8061
8096
|
}
|
|
8062
8097
|
async function readGitState(repoRoot, extraPaths = []) {
|
|
8063
8098
|
const gitDir = resolve2(repoRoot, ".git");
|
|
@@ -8067,8 +8102,9 @@ async function readGitState(repoRoot, extraPaths = []) {
|
|
|
8067
8102
|
]);
|
|
8068
8103
|
const config = parseGitConfig(configContent);
|
|
8069
8104
|
const index = parseIndexGitlinks(lsOutput);
|
|
8105
|
+
const modulePaths = new Set(extraPaths);
|
|
8070
8106
|
const modulesRoot = resolve2(gitDir, "modules");
|
|
8071
|
-
const moduleDirPaths =
|
|
8107
|
+
const { gitDirs: moduleDirPaths, orphanDirs: orphanModuleDirs } = scanModuleDirs(modulesRoot, modulePaths);
|
|
8072
8108
|
const moduleDirs = await Promise.all(moduleDirPaths.map(async (rel) => ({
|
|
8073
8109
|
relativePath: rel,
|
|
8074
8110
|
remoteUrl: await readModuleDirRemoteUrl(join(modulesRoot, rel))
|
|
@@ -8081,11 +8117,11 @@ async function readGitState(repoRoot, extraPaths = []) {
|
|
|
8081
8117
|
])
|
|
8082
8118
|
];
|
|
8083
8119
|
const workingTree = await Promise.all(allPaths.map((p) => readWorkingTreeEntry(repoRoot, p)));
|
|
8084
|
-
return { config, moduleDirs, index, workingTree };
|
|
8120
|
+
return { config, moduleDirs, orphanModuleDirs, index, workingTree };
|
|
8085
8121
|
}
|
|
8086
8122
|
|
|
8087
8123
|
// src/commands/git/sync-submodules/fix.ts
|
|
8088
|
-
import { existsSync as existsSync3 } from "fs";
|
|
8124
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
8089
8125
|
import { readFile as readFile4, writeFile, rename, mkdir, rm } from "fs/promises";
|
|
8090
8126
|
import { resolve as resolve3, join as join2, dirname, relative } from "path";
|
|
8091
8127
|
function removeExtraConfigSubmodules(content, validNames) {
|
|
@@ -8114,295 +8150,359 @@ function removeExtraConfigSubmodules(content, validNames) {
|
|
|
8114
8150
|
return { result: output.join(`
|
|
8115
8151
|
`), removed };
|
|
8116
8152
|
}
|
|
8117
|
-
function repoPath(url) {
|
|
8118
|
-
const stripped = url.replace(/\.git$/, "");
|
|
8119
|
-
if (stripped.startsWith("http")) {
|
|
8120
|
-
const parts = stripped.split("/");
|
|
8121
|
-
return parts.slice(-2).join("/");
|
|
8122
|
-
}
|
|
8123
|
-
const colonIdx = stripped.indexOf(":");
|
|
8124
|
-
return colonIdx >= 0 ? stripped.slice(colonIdx + 1) : stripped;
|
|
8125
|
-
}
|
|
8126
8153
|
async function spawnGit(args, cwd) {
|
|
8127
|
-
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "
|
|
8154
|
+
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
8128
8155
|
const code = await proc.exited;
|
|
8129
|
-
if (code !== 0)
|
|
8130
|
-
|
|
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
|
+
}
|
|
8131
8161
|
}
|
|
8132
|
-
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 = [];
|
|
8133
8191
|
const modulesRoot = resolve3(repoRoot, ".git", "modules");
|
|
8134
|
-
const
|
|
8192
|
+
const gmByPath = new Map(modules.map((m) => [m.path, m]));
|
|
8135
8193
|
for (const d of moduleDirs) {
|
|
8136
|
-
const
|
|
8137
|
-
if (!d.remoteUrl) {
|
|
8138
|
-
console.log(`${prefix}: no remote URL, skipping`);
|
|
8139
|
-
continue;
|
|
8140
|
-
}
|
|
8141
|
-
const gm = gmByRepoPath.get(repoPath(d.remoteUrl));
|
|
8194
|
+
const gm = gmByPath.get(d.relativePath);
|
|
8142
8195
|
if (!gm) {
|
|
8143
8196
|
await rm(join2(modulesRoot, d.relativePath), { recursive: true, force: true });
|
|
8144
|
-
|
|
8145
|
-
continue;
|
|
8197
|
+
fixes.push({ location: [".git", "modules", ...d.relativePath.split("/")], action: "deleted" });
|
|
8146
8198
|
}
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
}
|
|
8151
|
-
const targetAbs = join2(modulesRoot, gm.path);
|
|
8152
|
-
if (existsSync3(targetAbs)) {
|
|
8153
|
-
console.log(`${prefix}: target ${gm.path} already exists, skipping`);
|
|
8154
|
-
continue;
|
|
8155
|
-
}
|
|
8156
|
-
await mkdir(dirname(targetAbs), { recursive: true });
|
|
8157
|
-
await rename(join2(modulesRoot, d.relativePath), targetAbs);
|
|
8158
|
-
console.log(`${prefix} \u2192 ${gm.path}`);
|
|
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" });
|
|
8159
8203
|
}
|
|
8160
8204
|
for (const m of modules) {
|
|
8161
8205
|
const workingTreeDir = resolve3(repoRoot, m.path);
|
|
8162
8206
|
const moduleGitDir = join2(modulesRoot, m.path);
|
|
8163
8207
|
const dotGit = join2(workingTreeDir, ".git");
|
|
8164
|
-
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))
|
|
8165
8232
|
continue;
|
|
8166
8233
|
const rel = relative(workingTreeDir, moduleGitDir);
|
|
8167
8234
|
await writeFile(dotGit, `gitdir: ${rel}
|
|
8168
8235
|
`, "utf8");
|
|
8169
|
-
|
|
8236
|
+
fixes.push({ location: [...m.path.split("/"), ".git"], action: "synced" });
|
|
8170
8237
|
}
|
|
8171
|
-
console.log(" running: git submodule sync");
|
|
8172
|
-
await spawnGit(["submodule", "sync"], repoRoot);
|
|
8173
8238
|
for (const m of modules) {
|
|
8174
8239
|
const moduleGitDir = join2(modulesRoot, m.path);
|
|
8175
8240
|
if (!existsSync3(moduleGitDir))
|
|
8176
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
|
+
]);
|
|
8177
8248
|
const worktree = relative(moduleGitDir, resolve3(repoRoot, m.path));
|
|
8178
|
-
|
|
8179
|
-
|
|
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
|
+
}
|
|
8180
8257
|
}
|
|
8258
|
+
return fixes;
|
|
8181
8259
|
}
|
|
8182
|
-
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 = [];
|
|
8183
8263
|
for (const m of modules) {
|
|
8184
8264
|
const workingTree = resolve3(repoRoot, m.path);
|
|
8185
|
-
if (!existsSync3(workingTree))
|
|
8186
|
-
console.log(` ${m.path}: working tree missing, skipping`);
|
|
8265
|
+
if (!existsSync3(workingTree))
|
|
8187
8266
|
continue;
|
|
8188
|
-
}
|
|
8189
|
-
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" });
|
|
8190
8268
|
const sha = (await new Response(shaProc.stdout).text()).trim();
|
|
8191
|
-
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
8192
|
-
console.log(` ${m.path}: could not read HEAD (${sha}), skipping`);
|
|
8269
|
+
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
8193
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)}` });
|
|
8194
8275
|
}
|
|
8195
|
-
await spawnGit(["update-index", "--cacheinfo", `160000,${sha},${m.path}`], repoRoot);
|
|
8196
|
-
console.log(` ${m.path}: ${sha.slice(0, 8)}`);
|
|
8197
|
-
}
|
|
8198
|
-
}
|
|
8199
|
-
async function fixGitConfig(repoRoot, modules) {
|
|
8200
|
-
const configPath = resolve3(repoRoot, ".git", "config");
|
|
8201
|
-
const content = await readFile4(configPath, "utf8");
|
|
8202
|
-
const validNames = new Set(modules.map((m) => m.name));
|
|
8203
|
-
const { result, removed } = removeExtraConfigSubmodules(content, validNames);
|
|
8204
|
-
if (removed.length === 0) {
|
|
8205
|
-
console.log(" .git/config: no extra submodule entries");
|
|
8206
|
-
return;
|
|
8207
|
-
}
|
|
8208
|
-
await writeFile(configPath, result, "utf8");
|
|
8209
|
-
for (const name of removed) {
|
|
8210
|
-
console.log(` .git/config: removed [submodule "${name}"]`);
|
|
8211
8276
|
}
|
|
8277
|
+
return fixes;
|
|
8212
8278
|
}
|
|
8213
8279
|
|
|
8214
8280
|
// src/commands/git/sync-submodules/printer.ts
|
|
8215
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";
|
|
8216
8286
|
var RESET = "\x1B[0m";
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
}
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
const
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
console.log(
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
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);
|
|
8237
8306
|
}
|
|
8238
8307
|
}
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
constructor(label, message) {
|
|
8244
|
-
super();
|
|
8245
|
-
this.label = label;
|
|
8246
|
-
this.message = message;
|
|
8247
|
-
}
|
|
8248
|
-
header() {
|
|
8249
|
-
return this.label;
|
|
8250
|
-
}
|
|
8251
|
-
fields() {
|
|
8252
|
-
return [];
|
|
8253
|
-
}
|
|
8254
|
-
print(indent = " ") {
|
|
8255
|
-
console.log(`${RED}${indent}${this.label} \u2190 ${this.message}${RESET}`);
|
|
8256
|
-
}
|
|
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)] };
|
|
8257
8312
|
}
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
}
|
|
8265
|
-
header() {
|
|
8266
|
-
return `[${this.m.name}]`;
|
|
8267
|
-
}
|
|
8268
|
-
fields() {
|
|
8269
|
-
return [
|
|
8270
|
-
{ key: "path", value: this.m.path },
|
|
8271
|
-
{ key: "url", value: this.m.url }
|
|
8272
|
-
];
|
|
8273
|
-
}
|
|
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) };
|
|
8274
8319
|
}
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
return
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
return "not in .gitmodules";
|
|
8294
|
-
}
|
|
8295
|
-
fields() {
|
|
8296
|
-
const { e, gm } = this;
|
|
8297
|
-
return [
|
|
8298
|
-
{ key: "path", value: e.path ?? "(none)", error: gm && e.path !== gm.path ? `expected ${gm.path}` : undefined },
|
|
8299
|
-
{ key: "url", value: e.url ?? "(none)", error: gm && e.url !== gm.url ? `expected ${gm.url}` : undefined },
|
|
8300
|
-
{ key: "active", value: String(e.active ?? "(unset)") }
|
|
8301
|
-
];
|
|
8302
|
-
}
|
|
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;
|
|
8303
8338
|
}
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
d;
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
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]);
|
|
8318
8433
|
}
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
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);
|
|
8324
8463
|
}
|
|
8325
8464
|
}
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
header() {
|
|
8336
|
-
return this.e.path;
|
|
8337
|
-
}
|
|
8338
|
-
headerError() {
|
|
8339
|
-
return !this.knownPath ? "not in .gitmodules" : undefined;
|
|
8340
|
-
}
|
|
8341
|
-
fields() {
|
|
8342
|
-
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);
|
|
8343
8474
|
}
|
|
8344
8475
|
}
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
if (this.e.type === "dir")
|
|
8359
|
-
return "expected gitdir file, got directory";
|
|
8360
|
-
}
|
|
8361
|
-
fields() {
|
|
8362
|
-
if (this.e.type === "file")
|
|
8363
|
-
return [{ value: `gitdir -> ${this.e.gitdirTarget}` }];
|
|
8364
|
-
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
|
+
}
|
|
8365
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));
|
|
8366
8501
|
}
|
|
8367
8502
|
|
|
8368
8503
|
// src/commands/git/sync-submodules/index.ts
|
|
8369
8504
|
var REPO_ROOT = process.cwd();
|
|
8370
8505
|
var GITMODULES_PATH = resolve4(REPO_ROOT, ".gitmodules");
|
|
8371
|
-
function buildSections(modules, state) {
|
|
8372
|
-
const byName = new Map(modules.map((m) => [m.name, m]));
|
|
8373
|
-
const byPath = new Map(modules.map((m) => [m.path, m]));
|
|
8374
|
-
const gitmodulesItems = modules.map((m) => new GitmodulesEntryItem(m));
|
|
8375
|
-
const seenConfigNames = new Set;
|
|
8376
|
-
const configItems = state.config.map((e) => {
|
|
8377
|
-
const isDuplicate = seenConfigNames.has(e.name);
|
|
8378
|
-
seenConfigNames.add(e.name);
|
|
8379
|
-
return new GitConfigEntryItem(e, byName.get(e.name), isDuplicate);
|
|
8380
|
-
});
|
|
8381
|
-
for (const m of modules) {
|
|
8382
|
-
if (!seenConfigNames.has(m.name))
|
|
8383
|
-
configItems.push(new GhostItem(`[${m.name}]`, "missing from .git/config"));
|
|
8384
|
-
}
|
|
8385
|
-
const seenModulePaths = new Set;
|
|
8386
|
-
const moduleDirItems = state.moduleDirs.map((d) => {
|
|
8387
|
-
seenModulePaths.add(d.relativePath);
|
|
8388
|
-
return new ModuleDirItem(d, byPath.get(d.relativePath));
|
|
8389
|
-
});
|
|
8390
|
-
for (const m of modules) {
|
|
8391
|
-
if (!seenModulePaths.has(m.path))
|
|
8392
|
-
moduleDirItems.push(new GhostItem(m.path, "missing from .git/modules/"));
|
|
8393
|
-
}
|
|
8394
|
-
const seenIndexPaths = new Set;
|
|
8395
|
-
const indexItems = state.index.map((e) => {
|
|
8396
|
-
seenIndexPaths.add(e.path);
|
|
8397
|
-
return new IndexGitlinkItem(e, byPath.has(e.path));
|
|
8398
|
-
});
|
|
8399
|
-
for (const m of modules) {
|
|
8400
|
-
if (!seenIndexPaths.has(m.path))
|
|
8401
|
-
indexItems.push(new GhostItem(m.path, "missing from index"));
|
|
8402
|
-
}
|
|
8403
|
-
const workingTreeItems = state.workingTree.map((e) => new WorkingTreeItem(e));
|
|
8404
|
-
return { gitmodulesItems, configItems, moduleDirItems, indexItems, workingTreeItems };
|
|
8405
|
-
}
|
|
8406
8506
|
async function main() {
|
|
8407
8507
|
const content = await readFile5(GITMODULES_PATH, "utf8");
|
|
8408
8508
|
const modules = parse(content);
|
|
@@ -8414,20 +8514,14 @@ async function main() {
|
|
|
8414
8514
|
process.exit(1);
|
|
8415
8515
|
}
|
|
8416
8516
|
const submodulePaths = modules.map((m) => m.path);
|
|
8417
|
-
console.log("=== fixing ===");
|
|
8418
|
-
await fixGitConfig(REPO_ROOT, modules);
|
|
8419
|
-
const preFixState = await readGitState(REPO_ROOT, submodulePaths);
|
|
8420
|
-
await fixModuleDirs(REPO_ROOT, modules, preFixState.moduleDirs);
|
|
8421
|
-
await fixIndexGitlinks(REPO_ROOT, modules);
|
|
8422
8517
|
const state = await readGitState(REPO_ROOT, submodulePaths);
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
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);
|
|
8431
8525
|
const result = serialize(sorted(modules));
|
|
8432
8526
|
if (result !== content) {
|
|
8433
8527
|
await writeFile2(GITMODULES_PATH, result, "utf8");
|
|
@@ -8446,10 +8540,425 @@ var git = new Command("git").summary("setup git in repo for verified commits");
|
|
|
8446
8540
|
git.addCommand(cmd);
|
|
8447
8541
|
});
|
|
8448
8542
|
|
|
8449
|
-
// src/commands/npm/setup.ts
|
|
8543
|
+
// src/commands/npm/setup/index.ts
|
|
8544
|
+
var {$: $6 } = globalThis.Bun;
|
|
8545
|
+
import { existsSync as existsSync4 } from "fs";
|
|
8546
|
+
|
|
8547
|
+
// src/commands/npm/setup/npm-auth.ts
|
|
8548
|
+
var {$: $3 } = globalThis.Bun;
|
|
8549
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
8550
|
+
import { homedir } from "os";
|
|
8551
|
+
var npmrcPath = `${homedir()}/.npmrc`;
|
|
8552
|
+
var TOKEN_RE = /^\/\/registry\.npmjs\.org\/:_authToken=(\S+)$/m;
|
|
8553
|
+
function readNpmrc() {
|
|
8554
|
+
try {
|
|
8555
|
+
return readFileSync(npmrcPath, "utf8");
|
|
8556
|
+
} catch {
|
|
8557
|
+
return "";
|
|
8558
|
+
}
|
|
8559
|
+
}
|
|
8560
|
+
function stripTokenFromDisk() {
|
|
8561
|
+
const content = readNpmrc();
|
|
8562
|
+
const stripped = content.replace(TOKEN_RE, "").replace(/\n{3,}/g, `
|
|
8563
|
+
|
|
8564
|
+
`).trim();
|
|
8565
|
+
if (!stripped) {
|
|
8566
|
+
try {
|
|
8567
|
+
unlinkSync(npmrcPath);
|
|
8568
|
+
} catch {}
|
|
8569
|
+
} else {
|
|
8570
|
+
writeFileSync(npmrcPath, stripped + `
|
|
8571
|
+
`);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
async function whoamiWithToken(token) {
|
|
8575
|
+
const result = await $3`npm whoami`.env({ ...process.env, NODE_AUTH_TOKEN: token }).quiet().nothrow();
|
|
8576
|
+
if (result.exitCode !== 0)
|
|
8577
|
+
return null;
|
|
8578
|
+
return result.stdout.toString().trim();
|
|
8579
|
+
}
|
|
8580
|
+
async function acquireToken() {
|
|
8581
|
+
const existing = readNpmrc().match(TOKEN_RE)?.[1];
|
|
8582
|
+
if (existing) {
|
|
8583
|
+
const user2 = await whoamiWithToken(existing);
|
|
8584
|
+
if (user2) {
|
|
8585
|
+
stripTokenFromDisk();
|
|
8586
|
+
return { token: existing, user: user2 };
|
|
8587
|
+
}
|
|
8588
|
+
}
|
|
8589
|
+
log(" not logged in \u2014 opening npm login...");
|
|
8590
|
+
await $3`npm login`.nothrow();
|
|
8591
|
+
const token = readNpmrc().match(TOKEN_RE)?.[1];
|
|
8592
|
+
if (!token)
|
|
8593
|
+
throw new Error('npm login did not produce a token \u2014 try running "npm login" manually');
|
|
8594
|
+
const user = await whoamiWithToken(token);
|
|
8595
|
+
if (!user)
|
|
8596
|
+
throw new Error("npm login succeeded but token verification failed");
|
|
8597
|
+
stripTokenFromDisk();
|
|
8598
|
+
return { token, user };
|
|
8599
|
+
}
|
|
8600
|
+
|
|
8601
|
+
// src/commands/npm/setup/package-registry.ts
|
|
8602
|
+
async function fetchPackageInfo(name) {
|
|
8603
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`);
|
|
8604
|
+
if (res.status === 404)
|
|
8605
|
+
return null;
|
|
8606
|
+
if (!res.ok)
|
|
8607
|
+
throw new Error(`registry error ${res.status} for ${name}`);
|
|
8608
|
+
const data = await res.json();
|
|
8609
|
+
return {
|
|
8610
|
+
name: data.name,
|
|
8611
|
+
latestVersion: data["dist-tags"]?.latest,
|
|
8612
|
+
distTags: data["dist-tags"] ?? {}
|
|
8613
|
+
};
|
|
8614
|
+
}
|
|
8615
|
+
|
|
8616
|
+
// src/commands/npm/setup/trusted-publishing.ts
|
|
8617
|
+
var {$: $4 } = globalThis.Bun;
|
|
8618
|
+
import { createInterface } from "readline";
|
|
8619
|
+
function encodePackageName(name) {
|
|
8620
|
+
if (!name.startsWith("@"))
|
|
8621
|
+
return encodeURIComponent(name);
|
|
8622
|
+
const slash = name.indexOf("/");
|
|
8623
|
+
return `@${name.slice(1, slash)}%2F${name.slice(slash + 1)}`;
|
|
8624
|
+
}
|
|
8625
|
+
function promptPasscode() {
|
|
8626
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8627
|
+
return new Promise((resolve5) => {
|
|
8628
|
+
rl.question(" enter the passcode from the browser: ", (answer) => {
|
|
8629
|
+
rl.close();
|
|
8630
|
+
resolve5(answer.trim());
|
|
8631
|
+
});
|
|
8632
|
+
});
|
|
8633
|
+
}
|
|
8634
|
+
async function registryPost(url, body, authToken, { existingOtp, alreadyExistsStatus } = {}) {
|
|
8635
|
+
const headers = {
|
|
8636
|
+
Authorization: `Bearer ${authToken}`,
|
|
8637
|
+
"Content-Type": "application/json"
|
|
8638
|
+
};
|
|
8639
|
+
const post = (otp) => fetch(url, {
|
|
8640
|
+
method: "POST",
|
|
8641
|
+
headers: otp ? { ...headers, "npm-otp": otp } : headers,
|
|
8642
|
+
body
|
|
8643
|
+
});
|
|
8644
|
+
const check = (res2) => {
|
|
8645
|
+
if (res2.ok)
|
|
8646
|
+
return true;
|
|
8647
|
+
if (alreadyExistsStatus && res2.status === alreadyExistsStatus)
|
|
8648
|
+
return true;
|
|
8649
|
+
return false;
|
|
8650
|
+
};
|
|
8651
|
+
if (existingOtp) {
|
|
8652
|
+
const res2 = await post(existingOtp);
|
|
8653
|
+
if (check(res2))
|
|
8654
|
+
return { otp: existingOtp, alreadyExists: !res2.ok };
|
|
8655
|
+
}
|
|
8656
|
+
let res = await post();
|
|
8657
|
+
if (check(res))
|
|
8658
|
+
return { otp: undefined, alreadyExists: !res.ok };
|
|
8659
|
+
if (res.status === 401) {
|
|
8660
|
+
const notice = res.headers.get("npm-notice") ?? "";
|
|
8661
|
+
const loginUrl = notice.match(/https:\/\/www\.npmjs\.com\/login\/[a-f0-9-]+/)?.[0];
|
|
8662
|
+
if (!loginUrl)
|
|
8663
|
+
throw new Error("2FA required but could not parse login URL");
|
|
8664
|
+
await $4`open ${loginUrl}`.quiet().nothrow();
|
|
8665
|
+
const otp = await promptPasscode();
|
|
8666
|
+
res = await post(otp);
|
|
8667
|
+
if (check(res))
|
|
8668
|
+
return { otp, alreadyExists: !res.ok };
|
|
8669
|
+
}
|
|
8670
|
+
throw new Error(`${res.status} ${await res.text()}`);
|
|
8671
|
+
}
|
|
8672
|
+
async function addTrustedPublisher(name, ownerRepo, token, environment = "prod") {
|
|
8673
|
+
const encoded = encodePackageName(name);
|
|
8674
|
+
log(" setting up (may require 2FA)...");
|
|
8675
|
+
const body = JSON.stringify([{
|
|
8676
|
+
type: "github",
|
|
8677
|
+
claims: { repository: ownerRepo, workflow_ref: { file: "publish.yml" }, environment },
|
|
8678
|
+
permissions: ["createPackage"]
|
|
8679
|
+
}]);
|
|
8680
|
+
try {
|
|
8681
|
+
const { otp, alreadyExists } = await registryPost(`https://registry.npmjs.org/-/package/${encoded}/trust`, body, token, { alreadyExistsStatus: 409 });
|
|
8682
|
+
return { alreadyConfigured: alreadyExists, otp };
|
|
8683
|
+
} catch (e) {
|
|
8684
|
+
throw new Error(`trust setup failed: ${e.message}`);
|
|
8685
|
+
}
|
|
8686
|
+
}
|
|
8687
|
+
async function setPackageAccessPolicy(name, token, existingOtp) {
|
|
8688
|
+
const encoded = encodePackageName(name);
|
|
8689
|
+
const body = JSON.stringify({ publish_requires_tfa: true, automation_token_overrides_tfa: false });
|
|
8690
|
+
try {
|
|
8691
|
+
await registryPost(`https://registry.npmjs.org/-/package/${encoded}/access`, body, token, { existingOtp });
|
|
8692
|
+
} catch (e) {
|
|
8693
|
+
throw new Error(`access policy setup failed: ${e.message}`);
|
|
8694
|
+
}
|
|
8695
|
+
}
|
|
8696
|
+
|
|
8697
|
+
// src/commands/npm/setup/git-remote.ts
|
|
8698
|
+
var {$: $5 } = globalThis.Bun;
|
|
8699
|
+
function parseGithubOwnerRepo(url) {
|
|
8700
|
+
const https = url.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
8701
|
+
if (https)
|
|
8702
|
+
return { owner: https[1], repo: https[2] };
|
|
8703
|
+
const alias = url.match(/^[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
8704
|
+
if (alias)
|
|
8705
|
+
return { owner: alias[1], repo: alias[2] };
|
|
8706
|
+
return null;
|
|
8707
|
+
}
|
|
8708
|
+
async function checkGitHubAccess(remoteUrl) {
|
|
8709
|
+
const parsed = parseGithubOwnerRepo(remoteUrl);
|
|
8710
|
+
if (!parsed)
|
|
8711
|
+
return null;
|
|
8712
|
+
const result = await $5`gh api repos/${parsed.owner}/${parsed.repo}`.quiet().nothrow();
|
|
8713
|
+
if (result.exitCode !== 0)
|
|
8714
|
+
return null;
|
|
8715
|
+
let data;
|
|
8716
|
+
try {
|
|
8717
|
+
data = JSON.parse(result.stdout.toString());
|
|
8718
|
+
} catch {
|
|
8719
|
+
return null;
|
|
8720
|
+
}
|
|
8721
|
+
return { ...parsed, canPush: data?.permissions?.push === true };
|
|
8722
|
+
}
|
|
8723
|
+
|
|
8724
|
+
// src/commands/npm/setup/workflow.ts
|
|
8725
|
+
import * as fs3 from "fs/promises";
|
|
8726
|
+
import * as path from "path";
|
|
8727
|
+
|
|
8728
|
+
// src/commands/npm/setup/publish-workflow.yml
|
|
8729
|
+
var publish_workflow_default = `name: Publish to npm
|
|
8730
|
+
|
|
8731
|
+
on:
|
|
8732
|
+
push:
|
|
8733
|
+
branches:
|
|
8734
|
+
- main
|
|
8735
|
+
workflow_dispatch: {}
|
|
8736
|
+
|
|
8737
|
+
jobs:
|
|
8738
|
+
publish:
|
|
8739
|
+
runs-on: ubuntu-latest
|
|
8740
|
+
environment: prod
|
|
8741
|
+
permissions:
|
|
8742
|
+
contents: read
|
|
8743
|
+
id-token: write
|
|
8744
|
+
steps:
|
|
8745
|
+
- name: Checkout
|
|
8746
|
+
uses: actions/checkout@v4
|
|
8747
|
+
|
|
8748
|
+
- uses: oven-sh/setup-bun@v2
|
|
8749
|
+
|
|
8750
|
+
- name: Setup Node
|
|
8751
|
+
uses: actions/setup-node@v4
|
|
8752
|
+
with:
|
|
8753
|
+
node-version: "24"
|
|
8754
|
+
registry-url: 'https://registry.npmjs.org'
|
|
8755
|
+
|
|
8756
|
+
- name: Install dependencies
|
|
8757
|
+
run: bun install
|
|
8758
|
+
|
|
8759
|
+
- name: Build
|
|
8760
|
+
run: bun run build
|
|
8761
|
+
|
|
8762
|
+
- name: Publish
|
|
8763
|
+
run: npm publish dist/ --access public
|
|
8764
|
+
`;
|
|
8765
|
+
|
|
8766
|
+
// src/commands/npm/setup/workflow.ts
|
|
8767
|
+
var WORKFLOW_PATH = ".github/workflows/publish.yml";
|
|
8768
|
+
async function workflowExists(repoRoot) {
|
|
8769
|
+
try {
|
|
8770
|
+
await fs3.access(path.join(repoRoot, WORKFLOW_PATH));
|
|
8771
|
+
return true;
|
|
8772
|
+
} catch {
|
|
8773
|
+
return false;
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
async function createWorkflow(repoRoot) {
|
|
8777
|
+
const fullPath = path.join(repoRoot, WORKFLOW_PATH);
|
|
8778
|
+
await fs3.mkdir(path.dirname(fullPath), { recursive: true });
|
|
8779
|
+
await fs3.writeFile(fullPath, publish_workflow_default, "utf8");
|
|
8780
|
+
}
|
|
8781
|
+
|
|
8782
|
+
// src/commands/npm/setup/package-json-validate.ts
|
|
8783
|
+
var GITHUB_URL_RE = /^git\+https:\/\/github\.com\/[^/]+\/[^/]+\.git$/;
|
|
8784
|
+
function isValidGitHubUrl(url) {
|
|
8785
|
+
return GITHUB_URL_RE.test(url);
|
|
8786
|
+
}
|
|
8787
|
+
function validatePackageJson(pkg) {
|
|
8788
|
+
const issues = [];
|
|
8789
|
+
const repoUrl = typeof pkg.repository === "object" ? pkg.repository?.url : pkg.repository;
|
|
8790
|
+
if (!repoUrl) {
|
|
8791
|
+
issues.push({ field: "repository", issue: "missing \u2014 required for provenance" });
|
|
8792
|
+
} else if (!isValidGitHubUrl(repoUrl)) {
|
|
8793
|
+
issues.push({ field: "repository.url", issue: `not a valid GitHub URL (got "${repoUrl}")` });
|
|
8794
|
+
}
|
|
8795
|
+
if (pkg.publishConfig?.access !== "public") {
|
|
8796
|
+
issues.push({ field: "publishConfig.access", issue: 'should be "public"' });
|
|
8797
|
+
}
|
|
8798
|
+
if (pkg.publishConfig?.provenance !== true) {
|
|
8799
|
+
issues.push({ field: "publishConfig.provenance", issue: "should be true" });
|
|
8800
|
+
}
|
|
8801
|
+
if (!pkg.version) {
|
|
8802
|
+
issues.push({ field: "version", issue: 'missing \u2014 add a "version" field (e.g. "0.0.1")', fatal: true });
|
|
8803
|
+
}
|
|
8804
|
+
if (!pkg.scripts?.build) {
|
|
8805
|
+
issues.push({ field: "scripts.build", issue: 'missing \u2014 add a "build" script before running setup', fatal: true });
|
|
8806
|
+
}
|
|
8807
|
+
if (!pkg.files?.includes("dist")) {
|
|
8808
|
+
issues.push({ field: "files", issue: `"dist" not included \u2014 built output won't be published` });
|
|
8809
|
+
}
|
|
8810
|
+
return issues;
|
|
8811
|
+
}
|
|
8812
|
+
|
|
8813
|
+
// src/commands/npm/setup/index.ts
|
|
8450
8814
|
var setup2 = makeCommand("setup", "configure a new npm package for publishing", async () => {
|
|
8815
|
+
const cwd = process.cwd();
|
|
8816
|
+
log("checking npm...");
|
|
8817
|
+
const npmCheck = await $6`which npm`.quiet().nothrow();
|
|
8818
|
+
if (npmCheck.exitCode !== 0) {
|
|
8819
|
+
return Err("NpmNotFound" /* NpmNotFound */, '"npm" not found in PATH \u2014 install Node.js from nodejs.org');
|
|
8820
|
+
}
|
|
8821
|
+
const npmVersion = (await $6`npm --version`.quiet().nothrow()).stdout.toString().trim();
|
|
8822
|
+
const [major, minor] = npmVersion.split(".").map(Number);
|
|
8823
|
+
if (major < 11 || major === 11 && minor < 10) {
|
|
8824
|
+
return Err("NpmNotFound" /* NpmNotFound */, `npm ${npmVersion} is too old \u2014 "npm trust" requires 11.10.0+ (run "npm install -g npm")`);
|
|
8825
|
+
}
|
|
8826
|
+
let npmToken, npmUser;
|
|
8827
|
+
try {
|
|
8828
|
+
({ token: npmToken, user: npmUser } = await acquireToken());
|
|
8829
|
+
} catch (e) {
|
|
8830
|
+
return Err("NotLoggedIn" /* NotLoggedIn */, `npm authentication failed: ${e.message}`);
|
|
8831
|
+
}
|
|
8832
|
+
log(` \u2713 logged in as ${npmUser}`);
|
|
8833
|
+
log("checking GitHub...");
|
|
8834
|
+
const ghCheck = await $6`which gh`.quiet().nothrow();
|
|
8835
|
+
if (ghCheck.exitCode !== 0) {
|
|
8836
|
+
return Err("GhNotFound" /* GhNotFound */, '"gh" not found in PATH \u2014 install GitHub CLI from cli.github.com');
|
|
8837
|
+
}
|
|
8838
|
+
let ghUser = await $6`gh api user --jq .login`.quiet().nothrow();
|
|
8839
|
+
if (ghUser.exitCode !== 0) {
|
|
8840
|
+
log(" not logged in \u2014 opening gh auth login...");
|
|
8841
|
+
await $6`gh auth login`.nothrow();
|
|
8842
|
+
ghUser = await $6`gh api user --jq .login`.quiet().nothrow();
|
|
8843
|
+
}
|
|
8844
|
+
if (ghUser.exitCode !== 0) {
|
|
8845
|
+
return Err("NotLoggedIn" /* NotLoggedIn */, 'not logged into GitHub \u2014 run "gh auth login"');
|
|
8846
|
+
}
|
|
8847
|
+
log(` \u2713 logged in as ${ghUser.stdout.toString().trim()}`);
|
|
8848
|
+
log("checking git repo...");
|
|
8849
|
+
if (!existsSync4(`${cwd}/.git`)) {
|
|
8850
|
+
return Err("NoGitRepo" /* NoGitRepo */, 'current directory is not a git repository \u2014 run "git init" first');
|
|
8851
|
+
}
|
|
8852
|
+
log(" \u2713 git repo");
|
|
8853
|
+
log("checking package.json...");
|
|
8854
|
+
const pkgFile = Bun.file(`${cwd}/package.json`);
|
|
8855
|
+
if (!await pkgFile.exists()) {
|
|
8856
|
+
return Err("NoPackageJson" /* NoPackageJson */, "no package.json found \u2014 run this command from a package directory");
|
|
8857
|
+
}
|
|
8858
|
+
let pkg;
|
|
8859
|
+
try {
|
|
8860
|
+
pkg = await pkgFile.json();
|
|
8861
|
+
} catch {
|
|
8862
|
+
return Err("InvalidPackageJson" /* InvalidPackageJson */, "package.json contains invalid JSON \u2014 fix it and retry");
|
|
8863
|
+
}
|
|
8864
|
+
const { name } = pkg;
|
|
8865
|
+
if (!name) {
|
|
8866
|
+
return Err("InvalidPackageJson" /* InvalidPackageJson */, 'package.json is missing a "name" field');
|
|
8867
|
+
}
|
|
8868
|
+
log(` name: ${name}`);
|
|
8869
|
+
const fatal = validatePackageJson(pkg).find((i) => i.fatal);
|
|
8870
|
+
if (fatal) {
|
|
8871
|
+
return Err("InvalidPackageJson" /* InvalidPackageJson */, `${fatal.field}: ${fatal.issue}`);
|
|
8872
|
+
}
|
|
8873
|
+
log("checking git remote...");
|
|
8874
|
+
const remote = await getRemoteUrl(cwd);
|
|
8875
|
+
if (!remote) {
|
|
8876
|
+
return Err("NoGitRemote" /* NoGitRemote */, 'no git remote configured \u2014 run "git remote add origin <url>" first');
|
|
8877
|
+
}
|
|
8878
|
+
log(` remote: ${remote}`);
|
|
8879
|
+
const access2 = await checkGitHubAccess(remote);
|
|
8880
|
+
if (!access2) {
|
|
8881
|
+
return Err("RepoNotFound" /* RepoNotFound */, `could not reach GitHub repo at ${remote} \u2014 check "gh auth status"`);
|
|
8882
|
+
}
|
|
8883
|
+
if (!access2.canPush) {
|
|
8884
|
+
return Err("NoWriteAccess" /* NoWriteAccess */, `no push access to ${access2.owner}/${access2.repo} \u2014 you need write permissions`);
|
|
8885
|
+
}
|
|
8886
|
+
const repoHttpsUrl = `https://github.com/${access2.owner}/${access2.repo}`;
|
|
8887
|
+
log(` \u2713 ${access2.owner}/${access2.repo} (write access confirmed)`);
|
|
8888
|
+
const expectedRepoUrl = `git+${repoHttpsUrl}.git`;
|
|
8889
|
+
const currentRepoUrl = typeof pkg.repository === "object" ? pkg.repository?.url : pkg.repository;
|
|
8890
|
+
if (currentRepoUrl !== expectedRepoUrl) {
|
|
8891
|
+
log("syncing package.json repository URL...");
|
|
8892
|
+
log(` was: ${currentRepoUrl ?? "(missing)"}`);
|
|
8893
|
+
pkg = { ...pkg, repository: { type: "git", url: expectedRepoUrl } };
|
|
8894
|
+
await Bun.write(`${cwd}/package.json`, JSON.stringify(pkg, null, 2) + `
|
|
8895
|
+
`);
|
|
8896
|
+
log(` \u2713 set to ${expectedRepoUrl}`);
|
|
8897
|
+
}
|
|
8898
|
+
const nonFatal = validatePackageJson(pkg).filter((i) => !i.fatal);
|
|
8899
|
+
if (nonFatal.length > 0) {
|
|
8900
|
+
log("package.json issues (fix manually):");
|
|
8901
|
+
for (const { field, issue } of nonFatal)
|
|
8902
|
+
log(` ${field}: ${issue}`);
|
|
8903
|
+
}
|
|
8904
|
+
log("checking publish workflow...");
|
|
8905
|
+
if (!await workflowExists(cwd)) {
|
|
8906
|
+
await createWorkflow(cwd);
|
|
8907
|
+
log(" \u2713 created .github/workflows/publish.yml");
|
|
8908
|
+
} else {
|
|
8909
|
+
log(" \u2713 already exists");
|
|
8910
|
+
}
|
|
8911
|
+
log("checking npm registry...");
|
|
8912
|
+
const info = await fetchPackageInfo(name).catch((err) => {
|
|
8913
|
+
throw new Error(`npm registry lookup failed: ${err.message}`);
|
|
8914
|
+
});
|
|
8915
|
+
if (!info) {
|
|
8916
|
+
log(" package not yet published \u2014 building and creating initial release...");
|
|
8917
|
+
const buildResult = await $6`bun run build`.nothrow();
|
|
8918
|
+
if (buildResult.exitCode !== 0) {
|
|
8919
|
+
throw new Error(`build failed: ${buildResult.stderr.toString().trim()}`);
|
|
8920
|
+
}
|
|
8921
|
+
const distPkgFile = Bun.file(`${cwd}/dist/package.json`);
|
|
8922
|
+
if (!await distPkgFile.exists()) {
|
|
8923
|
+
throw new Error("build succeeded but dist/package.json is missing \u2014 build script must produce it");
|
|
8924
|
+
}
|
|
8925
|
+
try {
|
|
8926
|
+
await distPkgFile.json();
|
|
8927
|
+
} catch {
|
|
8928
|
+
throw new Error("dist/package.json exists but contains invalid JSON \u2014 check your build script");
|
|
8929
|
+
}
|
|
8930
|
+
await npmPublish(npmToken);
|
|
8931
|
+
log(` \u2713 published ${name}`);
|
|
8932
|
+
} else {
|
|
8933
|
+
const version2 = info.latestVersion ?? "(unknown)";
|
|
8934
|
+
log(` \u2713 exists (latest v${version2})`);
|
|
8935
|
+
}
|
|
8936
|
+
log("checking trusted publishing...");
|
|
8937
|
+
const ownerRepo = `${access2.owner}/${access2.repo}`;
|
|
8938
|
+
const trust = await addTrustedPublisher(name, ownerRepo, npmToken);
|
|
8939
|
+
log(trust.alreadyConfigured ? " \u2713 already configured" : " \u2713 configured");
|
|
8940
|
+
log("setting package access policy...");
|
|
8941
|
+
await setPackageAccessPolicy(name, npmToken, trust.otp);
|
|
8942
|
+
log(" \u2713 configured");
|
|
8451
8943
|
return Ok(true);
|
|
8452
8944
|
});
|
|
8945
|
+
async function getRemoteUrl(cwd) {
|
|
8946
|
+
const result = await $6`git -C ${cwd} remote get-url origin`.quiet().nothrow();
|
|
8947
|
+
if (result.exitCode !== 0)
|
|
8948
|
+
return;
|
|
8949
|
+
return result.stdout.toString().trim();
|
|
8950
|
+
}
|
|
8951
|
+
async function npmPublish(token) {
|
|
8952
|
+
const proc = Bun.spawn(["npm", "publish", "dist/", "--access", "public", "--no-provenance"], {
|
|
8953
|
+
stdin: "inherit",
|
|
8954
|
+
stdout: "inherit",
|
|
8955
|
+
stderr: "inherit",
|
|
8956
|
+
env: { ...process.env, NODE_AUTH_TOKEN: token }
|
|
8957
|
+
});
|
|
8958
|
+
const exitCode = await proc.exited;
|
|
8959
|
+
if (exitCode !== 0)
|
|
8960
|
+
throw new Error("npm publish failed \u2014 see output above");
|
|
8961
|
+
}
|
|
8453
8962
|
|
|
8454
8963
|
// src/commands/npm/index.ts
|
|
8455
8964
|
var npm = new Command("npm").summary("npm package management utilities");
|
|
@@ -8457,9 +8966,89 @@ var npm = new Command("npm").summary("npm package management utilities");
|
|
|
8457
8966
|
npm.addCommand(cmd);
|
|
8458
8967
|
});
|
|
8459
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", "-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", 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
|
+
|
|
8460
9049
|
// src/commands/index.ts
|
|
8461
9050
|
program.name("ozy").version(version);
|
|
8462
|
-
[git, npm].forEach((cmd) => {
|
|
9051
|
+
[git, npm, upgrade].forEach((cmd) => {
|
|
8463
9052
|
program.addCommand(cmd);
|
|
8464
9053
|
});
|
|
8465
9054
|
program.parse();
|