@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.
Files changed (2) hide show
  1. package/dist/index.js +862 -273
  2. 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.5";
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
- const result = await action();
2819
- if (!result.success) {
2820
- log(`${result.error}: ${result.reason}`);
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 findGitDirs(root, rel = "") {
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
- results.push(relPath);
8042
+ gitDirs.push(relPath);
8038
8043
  } else {
8039
- results.push(...findGitDirs(abs, relPath));
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 results;
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 dotGit = join(repoRoot, subPath, ".git");
8054
- if (!existsSync2(dotGit))
8055
- return { path: subPath, type: "missing" };
8056
- if (statSync(dotGit).isDirectory())
8057
- return { path: subPath, type: "dir" };
8058
- const content = await readFile3(dotGit, "utf8");
8059
- const match2 = content.match(/^gitdir:\s*(.+)$/m);
8060
- return { path: subPath, type: "file", gitdirTarget: match2?.[1].trim() };
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 = findGitDirs(modulesRoot);
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: "inherit", stderr: "inherit" });
8154
+ const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
8128
8155
  const code = await proc.exited;
8129
- if (code !== 0)
8130
- throw new Error(`git ${args.join(" ")} exited with code ${code}`);
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 fixModuleDirs(repoRoot, modules, moduleDirs) {
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 gmByRepoPath = new Map(modules.map((m) => [repoPath(m.url), m]));
8192
+ const gmByPath = new Map(modules.map((m) => [m.path, m]));
8135
8193
  for (const d of moduleDirs) {
8136
- const prefix = ` .git/modules/${d.relativePath}`;
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
- console.log(`${prefix}: deleted (orphaned, no matching submodule)`);
8145
- continue;
8197
+ fixes.push({ location: [".git", "modules", ...d.relativePath.split("/")], action: "deleted" });
8146
8198
  }
8147
- if (d.relativePath === gm.path) {
8148
- console.log(`${prefix}: already correct`);
8149
- continue;
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) || !existsSync3(moduleGitDir) || existsSync3(dotGit))
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
- console.log(` wrote ${m.path}/.git \u2192 ${rel}`);
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
- await spawnGit(["config", "-f", `.git/modules/${m.path}/config`, "core.worktree", worktree], repoRoot);
8179
- console.log(` set core.worktree for ${m.path}: ${worktree}`);
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
- function renderField(f, indent) {
8218
- const text = f.key !== undefined ? `${indent} ${f.key}=${f.value}` : `${indent} ${f.value}`;
8219
- return f.error ? `${text} ${RED}\u2190 ${f.error}${RESET}` : text;
8220
- }
8221
-
8222
- class PrintableItem {
8223
- headerError() {
8224
- return;
8225
- }
8226
- print(indent = " ") {
8227
- const e = this.headerError();
8228
- console.log(e ? `${indent}${this.header()} ${RED}\u2190 ${e}${RESET}` : `${indent}${this.header()}`);
8229
- for (const f of this.fields())
8230
- console.log(renderField(f, indent));
8231
- }
8232
- static printGroup(header, items) {
8233
- console.log(`
8234
- === ${header} ===`);
8235
- for (const item of items)
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
- class GhostItem extends PrintableItem {
8241
- label;
8242
- message;
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
- class GitmodulesEntryItem extends PrintableItem {
8260
- m;
8261
- constructor(m) {
8262
- super();
8263
- this.m = m;
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
- class GitConfigEntryItem extends PrintableItem {
8277
- e;
8278
- gm;
8279
- isDuplicate;
8280
- constructor(e, gm, isDuplicate) {
8281
- super();
8282
- this.e = e;
8283
- this.gm = gm;
8284
- this.isDuplicate = isDuplicate;
8285
- }
8286
- header() {
8287
- return `[${this.e.name}]`;
8288
- }
8289
- headerError() {
8290
- if (this.isDuplicate)
8291
- return "duplicate";
8292
- if (!this.gm)
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
- class ModuleDirItem extends PrintableItem {
8306
- d;
8307
- gm;
8308
- constructor(d, gm) {
8309
- super();
8310
- this.d = d;
8311
- this.gm = gm;
8312
- }
8313
- header() {
8314
- return this.d.relativePath;
8315
- }
8316
- headerError() {
8317
- return !this.gm ? "not in .gitmodules" : undefined;
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
- fields() {
8320
- const { d, gm } = this;
8321
- return [
8322
- { key: "remote", value: d.remoteUrl ?? "(none)", error: gm && d.remoteUrl && d.remoteUrl !== gm.url ? `expected ${gm.url}` : undefined }
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
- class IndexGitlinkItem extends PrintableItem {
8328
- e;
8329
- knownPath;
8330
- constructor(e, knownPath) {
8331
- super();
8332
- this.e = e;
8333
- this.knownPath = knownPath;
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
- class WorkingTreeItem extends PrintableItem {
8347
- e;
8348
- constructor(e) {
8349
- super();
8350
- this.e = e;
8351
- }
8352
- header() {
8353
- return this.e.path;
8354
- }
8355
- headerError() {
8356
- if (this.e.type === "missing")
8357
- return "missing";
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
- const { gitmodulesItems, configItems, moduleDirItems, indexItems, workingTreeItems } = buildSections(modules, state);
8424
- console.log("=== .gitmodules ===");
8425
- for (const item of gitmodulesItems)
8426
- item.print();
8427
- PrintableItem.printGroup(".git/config submodule entries", configItems);
8428
- PrintableItem.printGroup(".git/modules/ git dirs", moduleDirItems);
8429
- PrintableItem.printGroup("index gitlinks", indexItems);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ozyman42/ozy-cli",
3
- "version": "0.0.5",
3
+ "version": "0.2.0",
4
4
  "description": "Ozymandias' personal tools",
5
5
  "type": "module",
6
6
  "bin": {