@drops-it/dpm 1.0.6 → 1.1.5

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/install.js CHANGED
@@ -1,63 +1,39 @@
1
- import { createHash } from "node:crypto";
2
- import { execFile, exec } from "node:child_process";
3
- import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
- import { homedir, tmpdir } from "node:os";
1
+ import { access, readdir, readFile, rm, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
5
3
  import path from "node:path";
6
- import { promisify } from "node:util";
7
4
  import * as tar from "tar";
8
- import semver from "semver";
9
5
  import { auditPackages, formatAuditReport, proposeFixes, } from "./audit.js";
10
6
  import { c } from "./colors.js";
11
- import { loadLockfile, readPackageJson, saveLockfile, writePackageJson, } from "./config.js";
7
+ import { loadConfig, loadLockfile, readPackageJson, saveLockfile, writePackageJson, } from "./config.js";
8
+ import { ensureInstallConfig, runtimeInstallEnv, spawnRuntime, } from "./runtime.js";
12
9
  import { createUi } from "./ui.js";
13
- const execFileAsync = promisify(execFile);
14
- const execAsync = promisify(exec);
15
- function quoteNpmArg(value) {
16
- if (!/[ \t"&<>|^]/.test(value))
17
- return value;
18
- return `"${value.replace(/"/g, '\\"')}"`;
19
- }
20
- async function runNpm(args) {
21
- if (process.platform === "win32") {
22
- const cmdline = ["npm", ...args.map(quoteNpmArg)].join(" ");
23
- return execAsync(cmdline, {
24
- windowsHide: true,
25
- maxBuffer: 10 * 1024 * 1024,
26
- });
27
- }
28
- return execFileAsync("npm", args, {
29
- windowsHide: true,
30
- maxBuffer: 10 * 1024 * 1024,
31
- });
32
- }
10
+ import { hasWindowsExeInstall, uninstallWindowsExe, updateWindowsExe, } from "./windowsInstall.js";
33
11
  function asUi(log) {
34
12
  if ("status" in log && typeof log.status === "function" && "clearStatus" in log) {
35
13
  return log;
36
14
  }
37
15
  return createUi();
38
16
  }
39
- function integrityOf(buf) {
40
- return `sha512-${createHash("sha512").update(buf).digest("base64")}`;
17
+ function setStatus(log, msg) {
18
+ if (log.status)
19
+ log.status(msg);
20
+ else
21
+ log.info(msg);
41
22
  }
42
- async function extractToNodeModules(name, tarball, cwd) {
43
- const target = path.join(cwd, "node_modules", ...name.split("/"));
44
- await rm(target, { recursive: true, force: true });
45
- await mkdir(target, { recursive: true });
46
- const tmp = path.join(cwd, "node_modules", `.dpm-tmp-${Date.now()}-${Math.random().toString(16).slice(2)}.tgz`);
47
- await writeFile(tmp, tarball);
48
- await tar.x({ file: tmp, cwd: target, strip: 1 });
49
- await rm(tmp, { force: true });
23
+ function topLevelDeps(pkg) {
24
+ return {
25
+ ...(pkg.dependencies || {}),
26
+ ...(pkg.devDependencies || {}),
27
+ };
50
28
  }
51
- function pickVersion(versions, range, distTags, preferred) {
52
- // Audit-fix preferred versions always win when present in the registry.
53
- if (preferred && versions.includes(preferred))
54
- return preferred;
55
- if (range === "latest" || range === "*" || range === "") {
56
- return distTags.latest ?? semver.maxSatisfying(versions, "*");
57
- }
58
- if (semver.valid(range))
59
- return versions.includes(range) ? range : null;
60
- return semver.maxSatisfying(versions, range);
29
+ function readOverrides(pkg) {
30
+ const raw = pkg.overrides || {};
31
+ const out = {};
32
+ for (const [name, value] of Object.entries(raw)) {
33
+ if (typeof value === "string")
34
+ out[name] = value;
35
+ }
36
+ return out;
61
37
  }
62
38
  /** Parse `name`, `name@1.2.3`, `name@^1`, `@scope/name`, `@scope/name@1.0.0`. */
63
39
  export function parsePackageSpec(spec) {
@@ -89,196 +65,207 @@ async function ensurePackageJson(cwd, log) {
89
65
  return pkg;
90
66
  }
91
67
  }
92
- async function resolveRange(api, name, range) {
93
- const meta = await api.getPackage(name);
94
- if (!meta)
95
- throw new Error(`Package not found: ${name}`);
96
- if (range === "latest" || range === "*") {
97
- const latest = meta.latest || meta["dist-tags"]?.latest;
98
- if (!latest)
99
- throw new Error(`No versions for ${name}`);
100
- return `^${latest}`;
101
- }
102
- if (semver.valid(range))
103
- return range;
104
- return range;
105
- }
106
- function setStatus(log, msg) {
107
- if (log.status)
108
- log.status(msg);
109
- else
110
- log.info(msg);
111
- }
112
- function topLevelDeps(pkg) {
113
- return {
114
- ...(pkg.dependencies || {}),
115
- ...(pkg.devDependencies || {}),
116
- };
117
- }
118
- function readOverrides(pkg) {
119
- const raw = pkg.overrides || {};
120
- const out = {};
121
- for (const [name, value] of Object.entries(raw)) {
122
- if (typeof value === "string")
123
- out[name] = value;
68
+ /**
69
+ * Builds a DPM lock snapshot from installed node_modules for audit tooling.
70
+ */
71
+ export async function syncLockFromNodeModules(cwd) {
72
+ const lock = { lockfileVersion: 1, packages: {} };
73
+ const root = path.join(cwd, "node_modules");
74
+ try {
75
+ await access(root);
124
76
  }
125
- return out;
77
+ catch {
78
+ await saveLockfile(lock, cwd);
79
+ return lock;
80
+ }
81
+ async function visit(dir, scope) {
82
+ let entries;
83
+ try {
84
+ entries = await readdir(dir);
85
+ }
86
+ catch {
87
+ return;
88
+ }
89
+ for (const entry of entries) {
90
+ if (entry === ".bin" || entry.startsWith("."))
91
+ continue;
92
+ const full = path.join(dir, entry);
93
+ let st;
94
+ try {
95
+ st = await stat(full);
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ if (!st.isDirectory())
101
+ continue;
102
+ if (entry.startsWith("@") && !scope) {
103
+ await visit(full, entry);
104
+ continue;
105
+ }
106
+ const name = scope ? `${scope}/${entry}` : entry;
107
+ try {
108
+ const raw = await readFile(path.join(full, "package.json"), "utf8");
109
+ const meta = JSON.parse(raw);
110
+ lock.packages[name] = {
111
+ version: meta.version || "0.0.0",
112
+ resolved: name,
113
+ integrity: "",
114
+ source: "dpm",
115
+ dependencies: meta.dependencies,
116
+ };
117
+ }
118
+ catch {
119
+ /* skip broken packages */
120
+ }
121
+ }
122
+ }
123
+ await visit(root);
124
+ await saveLockfile(lock, cwd);
125
+ return lock;
126
+ }
127
+ async function prepareRuntimeInstall(cwd) {
128
+ const config = await loadConfig();
129
+ await ensureInstallConfig(config, cwd);
130
+ return { config, env: runtimeInstallEnv(config, cwd) };
126
131
  }
127
- export async function installAll(api, cwd = process.cwd(), log = createUi(), options = {}) {
132
+ /** Installs package.json dependencies via the managed DPM runtime. */
133
+ export async function installAll(_api, cwd = process.cwd(), log = createUi(), options = {}) {
128
134
  const ui = asUi(log);
129
- const pkg = await ensurePackageJson(cwd, ui);
130
- const deps = topLevelDeps(pkg);
131
- const entries = Object.entries(deps);
132
- if (entries.length === 0) {
133
- ui.warn("package.json has no dependencies");
134
- await saveLockfile({ lockfileVersion: 1, packages: {} }, cwd);
135
- return { lockfileVersion: 1, packages: {} };
136
- }
137
- ui.info(`${c.cyan("→")} resolving ${c.bold(String(entries.length))} top-level dependenc${entries.length === 1 ? "y" : "ies"}`);
138
- const lock = { lockfileVersion: 1, packages: {} };
139
- const queue = [...entries];
140
- let installed = 0;
141
- const started = Date.now();
142
- const preferred = {
143
- ...readOverrides(pkg),
144
- ...(options.preferredVersions || {}),
145
- };
146
- while (queue.length) {
147
- const [name, range] = queue.pop();
148
- if (lock.packages[name])
149
- continue;
150
- const progress = () => {
151
- const pending = queue.length + 1;
152
- return `${c.dim(`${installed} done · ${pending} left`)}`;
135
+ await ensurePackageJson(cwd, ui);
136
+ if (options.preferredVersions && Object.keys(options.preferredVersions).length > 0) {
137
+ const pkg = await readPackageJson(cwd);
138
+ const overrides = {
139
+ ...readOverrides(pkg),
140
+ ...options.preferredVersions,
153
141
  };
154
- setStatus(ui, `resolve ${c.bold(name)}@${range} ${progress()}`);
155
- const meta = await api.getPackage(name);
156
- if (!meta)
157
- throw new Error(`Package not found: ${name}`);
158
- const versions = meta.versions.map((v) => v.version);
159
- const version = pickVersion(versions, range, meta["dist-tags"] || {}, preferred[name]);
160
- if (!version)
161
- throw new Error(`No version of ${name} matches ${range}`);
162
- const ver = meta.versions.find((v) => v.version === version);
163
- const source = meta.source || "dpm";
164
- const sourceTag = source === "npm" ? c.magenta("[npm]") : c.blue("[dpm]");
165
- setStatus(ui, `download ${c.bold(name)}@${c.green(version)} ${sourceTag} ${progress()}`);
166
- const tarball = await api.downloadTarball(ver.dist.tarball);
167
- const integrity = ver.dist.integrity || integrityOf(tarball);
168
- setStatus(ui, `extract ${c.bold(name)}@${c.green(version)} ${progress()}`);
169
- await extractToNodeModules(name, tarball, cwd);
170
- installed += 1;
171
- lock.packages[name] = {
172
- version,
173
- resolved: ver.dist.tarball,
174
- integrity,
175
- source,
176
- dependencies: ver.dependencies,
177
- };
178
- for (const [dep, depRange] of Object.entries(ver.dependencies || {})) {
179
- if (!lock.packages[dep])
180
- queue.push([dep, depRange]);
181
- }
142
+ pkg.overrides = overrides;
143
+ await writePackageJson(pkg, cwd);
182
144
  }
145
+ const { env } = await prepareRuntimeInstall(cwd);
183
146
  ui.clearStatus();
184
- await saveLockfile(lock, cwd);
147
+ ui.info(`${c.dim("→")} installing dependencies…`);
148
+ const started = Date.now();
149
+ const code = await spawnRuntime({
150
+ cwd,
151
+ args: ["install"],
152
+ env,
153
+ brandOutput: true,
154
+ });
155
+ if (code !== 0) {
156
+ throw new Error(`install failed (exit ${code})`);
157
+ }
158
+ const lock = await syncLockFromNodeModules(cwd);
185
159
  const elapsed = ((Date.now() - started) / 1000).toFixed(1);
160
+ const count = Object.keys(lock.packages).length;
186
161
  if (ui.success) {
187
- ui.success(`added ${c.bold(String(installed))} package${installed === 1 ? "" : "s"} in ${elapsed}s`);
162
+ ui.success(`added ${c.bold(String(count))} package${count === 1 ? "" : "s"} in ${elapsed}s`);
188
163
  }
189
164
  else {
190
- ui.info(`added ${installed} package${installed === 1 ? "" : "s"} in ${elapsed}s`);
165
+ ui.info(`added ${count} package${count === 1 ? "" : "s"} in ${elapsed}s`);
191
166
  }
192
167
  if (!options.skipAudit) {
193
168
  setStatus(ui, "checking vulnerabilities…");
194
169
  const hits = await auditPackages(lock.packages);
170
+ const pkg = await readPackageJson(cwd);
171
+ const deps = topLevelDeps(pkg);
195
172
  const fixes = hits.length
196
- ? await proposeFixes(api, lock, hits, deps, true)
173
+ ? await proposeFixes(_api, lock, hits, deps, true)
197
174
  : [];
198
175
  ui.clearStatus();
199
176
  ui.info(formatAuditReport(hits, fixes));
200
177
  }
201
178
  return lock;
202
179
  }
180
+ /** Adds packages then installs via the managed DPM runtime. */
203
181
  export async function addPackages(api, specs, cwd = process.cwd(), log = createUi()) {
204
182
  if (specs.length === 0)
205
183
  return installAll(api, cwd, log);
206
184
  const ui = asUi(log);
207
- const pkg = await ensurePackageJson(cwd, ui);
208
- const deps = pkg.dependencies || {};
185
+ await ensurePackageJson(cwd, ui);
186
+ const { env } = await prepareRuntimeInstall(cwd);
187
+ ui.clearStatus();
188
+ ui.info(`${c.dim("→")} add ${specs.join(", ")}`);
189
+ const code = await spawnRuntime({
190
+ cwd,
191
+ args: ["add", ...specs],
192
+ env,
193
+ brandOutput: true,
194
+ });
195
+ if (code !== 0)
196
+ throw new Error(`add failed (exit ${code})`);
209
197
  for (const spec of specs) {
210
- const { name, range } = parsePackageSpec(spec);
211
- setStatus(ui, `add ${c.bold(name)}`);
212
- const savedRange = await resolveRange(api, name, range);
213
- deps[name] = savedRange;
214
- ui.info(`${c.green("+")} ${c.bold(name)}@${c.cyan(savedRange)}`);
198
+ const { name } = parsePackageSpec(spec);
199
+ ui.info(`${c.green("+")} ${c.bold(name)}`);
215
200
  }
216
- pkg.dependencies = deps;
217
- await writePackageJson(pkg, cwd);
218
- return installAll(api, cwd, ui);
201
+ return syncLockFromNodeModules(cwd);
202
+ }
203
+ const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
204
+ function isCliSelfName(name) {
205
+ return CLI_SELF_NAMES.includes(name);
219
206
  }
220
207
  /**
221
- * Installs packages into the npm global prefix (bins linked by npm).
222
- * Downloads through the DPM registry so installs count as DPM downloads.
223
- * Used for CLI self-updates: `dpm i -g dpm` / `dpm i -g @drops-it/dpm`.
208
+ * Installs packages into the global prefix via the managed DPM runtime.
209
+ * Self-update of the Windows exe uses `/download/dpm.exe` instead of bun -g.
224
210
  */
225
- export async function installGlobalPackages(api, specs, log = createUi()) {
211
+ export async function installGlobalPackages(_api, specs, log = createUi()) {
226
212
  if (specs.length === 0) {
227
213
  throw new Error("global install requires at least one package name");
228
214
  }
229
215
  const ui = asUi(log);
230
- for (const spec of specs) {
231
- let { name, range } = parsePackageSpec(spec);
232
- // CLI lives on DPM as unscoped `dpm` — map the npm name away.
233
- if (name === "@drops-it/dpm")
234
- name = "dpm";
235
- setStatus(ui, `resolve ${c.bold(name)}`);
236
- const meta = await api.getPackage(name);
237
- if (!meta)
238
- throw new Error(`Package not found: ${name}`);
239
- const versions = meta.versions.map((v) => v.version);
240
- const version = pickVersion(versions, range, meta["dist-tags"] || {});
241
- if (!version)
242
- throw new Error(`No version of ${name} matches ${range}`);
243
- const ver = meta.versions.find((v) => v.version === version);
244
- // Prefer the DPM tarball URL so the registry records the download.
245
- const tarballUrl = ver.dist.tarball.includes("/dpm/v1/packages/")
246
- ? ver.dist.tarball
247
- : `/dpm/v1/packages/${encodeURIComponent(name)}/-/${encodeURIComponent(name)}-${version}.tgz`;
248
- setStatus(ui, `download ${c.bold(name)}@${c.green(version)}`);
249
- const tarball = await api.downloadTarball(tarballUrl);
250
- const tmp = path.join(tmpdir(), `dpm-global-${Date.now()}-${Math.random().toString(16).slice(2)}.tgz`);
251
- await writeFile(tmp, tarball);
252
- try {
253
- setStatus(ui, `install ${c.bold(name)}@${c.green(version)} ${c.dim("[global]")}`);
254
- await runNpm(["install", "-g", tmp]);
255
- }
256
- finally {
257
- await rm(tmp, { force: true });
258
- }
216
+ const parsed = specs.map((spec) => {
217
+ const { name, range } = parsePackageSpec(spec);
218
+ const real = name === "@drops-it/dpm" ? "dpm" : name;
219
+ return { real, range, original: spec };
220
+ });
221
+ const selfSpecs = parsed.filter((p) => isCliSelfName(p.real));
222
+ const otherSpecs = parsed.filter((p) => !isCliSelfName(p.real));
223
+ if (selfSpecs.length > 0 && (await hasWindowsExeInstall())) {
224
+ await updateWindowsExe(ui);
225
+ }
226
+ else if (selfSpecs.length > 0) {
227
+ // npm/bun global CLI install
228
+ const mapped = selfSpecs.map((p) => p.range === "latest" ? p.real : `${p.real}@${p.range}`);
229
+ const config = await loadConfig();
230
+ await ensureInstallConfig(config, process.cwd());
231
+ const env = runtimeInstallEnv(config);
259
232
  ui.clearStatus();
233
+ ui.info(`${c.dim("→")} install -g ${mapped.join(", ")}`);
234
+ const code = await spawnRuntime({
235
+ args: ["add", "-g", ...mapped],
236
+ env,
237
+ brandOutput: true,
238
+ });
239
+ if (code !== 0)
240
+ throw new Error(`global install failed (exit ${code})`);
260
241
  if (ui.success)
261
- ui.success(`installed ${c.bold(name)}@${c.green(version)} globally`);
242
+ ui.success(`installed ${mapped.join(", ")} globally`);
262
243
  else
263
- ui.info(`installed ${name}@${version} globally`);
244
+ ui.info(`installed ${mapped.join(", ")} globally`);
264
245
  }
246
+ if (otherSpecs.length === 0)
247
+ return;
248
+ const mapped = otherSpecs.map((p) => p.range === "latest" ? p.real : `${p.real}@${p.range}`);
249
+ const config = await loadConfig();
250
+ await ensureInstallConfig(config, process.cwd());
251
+ const env = runtimeInstallEnv(config);
252
+ ui.clearStatus();
253
+ ui.info(`${c.dim("→")} install -g ${mapped.join(", ")}`);
254
+ const code = await spawnRuntime({
255
+ args: ["add", "-g", ...mapped],
256
+ env,
257
+ brandOutput: true,
258
+ });
259
+ if (code !== 0)
260
+ throw new Error(`global install failed (exit ${code})`);
261
+ if (ui.success)
262
+ ui.success(`installed ${mapped.join(", ")} globally`);
263
+ else
264
+ ui.info(`installed ${mapped.join(", ")} globally`);
265
265
  }
266
- async function npmGlobalPrefix() {
267
- const { stdout } = await runNpm(["prefix", "-g"]);
268
- return stdout.toString().trim();
269
- }
270
- async function tryNpmUninstallGlobal(name) {
271
- try {
272
- await runNpm(["uninstall", "-g", name]);
273
- }
274
- catch {
275
- try {
276
- await runNpm(["unlink", "-g", name]);
277
- }
278
- catch {
279
- // Package may already be gone.
280
- }
281
- }
266
+ async function runtimeGlobalPrefix() {
267
+ // Best-effort: common user global paths when removing the CLI.
268
+ return path.join(homedir(), ".dpm", "global");
282
269
  }
283
270
  /** Removes leftover Windows/Unix bin shims for a command name. */
284
271
  async function removeGlobalBinShims(binName, prefix) {
@@ -286,80 +273,156 @@ async function removeGlobalBinShims(binName, prefix) {
286
273
  path.join(prefix, binName),
287
274
  path.join(prefix, `${binName}.cmd`),
288
275
  path.join(prefix, `${binName}.ps1`),
276
+ path.join(prefix, "bin", binName),
289
277
  path.join(prefix, "node_modules", ".bin", binName),
290
278
  path.join(prefix, "node_modules", ".bin", `${binName}.cmd`),
291
279
  path.join(prefix, "node_modules", ".bin", `${binName}.ps1`),
292
280
  ];
293
281
  await Promise.all(candidates.map((p) => rm(p, { force: true })));
294
282
  }
295
- const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
283
+ /** Deletes leftover global package trees for the CLI (npm/bun prefixes). */
284
+ async function scrubCliGlobalLeftovers() {
285
+ const prefix = await runtimeGlobalPrefix();
286
+ await removeGlobalBinShims("dpm", prefix);
287
+ const roaming = path.join(homedir(), "AppData", "Roaming", "npm");
288
+ await removeGlobalBinShims("dpm", roaming);
289
+ await rm(path.join(prefix, "node_modules", "dpm"), {
290
+ recursive: true,
291
+ force: true,
292
+ });
293
+ await rm(path.join(homedir(), ".bun", "install", "global", "node_modules", "dpm"), { recursive: true, force: true }).catch(() => undefined);
294
+ await rm(path.join(homedir(), "AppData", "Roaming", "npm", "node_modules", "@drops-it", "dpm"), { recursive: true, force: true }).catch(() => undefined);
295
+ await rm(path.join(homedir(), "AppData", "Roaming", "npm", "node_modules", "dpm"), { recursive: true, force: true }).catch(() => undefined);
296
+ }
296
297
  /**
297
298
  * Uninstalls global packages (and cleans dpm bin shims when removing the CLI).
298
- * `dpm rm -g` with no names removes the dpm CLI itself.
299
+ * `dpm rm -g` with no names removes the dpm CLI itself (Windows exe + bun/npm globals).
299
300
  */
300
301
  export async function removeGlobalPackages(specs, log = createUi()) {
301
302
  const ui = asUi(log);
302
303
  const targets = specs.length > 0
303
- ? specs.map((s) => parsePackageSpec(s).name)
304
- : [...CLI_SELF_NAMES];
305
- const prefix = await npmGlobalPrefix();
306
- const removingSelf = targets.some((n) => CLI_SELF_NAMES.includes(n));
307
- for (const name of targets) {
308
- setStatus(ui, `uninstall -g ${c.bold(name)}`);
309
- await tryNpmUninstallGlobal(name);
310
- // Scoped packages may leave the unscoped alias around.
311
- if (name === "@drops-it/dpm")
312
- await tryNpmUninstallGlobal("dpm");
313
- if (name === "dpm")
314
- await tryNpmUninstallGlobal("@drops-it/dpm");
304
+ ? [
305
+ ...new Set(specs
306
+ .map((s) => parsePackageSpec(s).name)
307
+ .map((n) => (n === "@drops-it/dpm" ? "dpm" : n))),
308
+ ]
309
+ : ["dpm"];
310
+ const config = await loadConfig();
311
+ const env = runtimeInstallEnv(config);
312
+ const removingSelf = targets.some(isCliSelfName);
313
+ const otherTargets = targets.filter((n) => !isCliSelfName(n));
314
+ let didWindowsUninstall = false;
315
+ if (removingSelf && (await hasWindowsExeInstall())) {
316
+ await uninstallWindowsExe(ui);
317
+ didWindowsUninstall = true;
315
318
  }
316
- if (removingSelf) {
317
- await removeGlobalBinShims("dpm", prefix);
318
- // Also clear a linked/local leftover under the user npm folder on Windows.
319
- const roaming = path.join(homedir(), "AppData", "Roaming", "npm");
320
- if (roaming.toLowerCase() !== prefix.toLowerCase()) {
321
- await removeGlobalBinShims("dpm", roaming);
322
- }
323
- await rm(path.join(prefix, "node_modules", "dpm"), { recursive: true, force: true });
324
- await rm(path.join(prefix, "node_modules", "@drops-it", "dpm"), {
325
- recursive: true,
326
- force: true,
319
+ // bun -g only for non-exe packages (and for self when not an exe install).
320
+ const bunTargets = didWindowsUninstall
321
+ ? otherTargets
322
+ : removingSelf
323
+ ? [...new Set([...otherTargets, "dpm"])]
324
+ : otherTargets;
325
+ for (const name of bunTargets) {
326
+ ui.clearStatus();
327
+ ui.info(`${c.dim("→")} uninstall -g ${c.bold(name)}`);
328
+ await spawnRuntime({
329
+ args: ["remove", "-g", name],
330
+ env,
331
+ brandOutput: true,
327
332
  });
328
333
  }
334
+ if (removingSelf) {
335
+ await scrubCliGlobalLeftovers();
336
+ }
329
337
  ui.clearStatus();
330
- if (ui.success) {
331
- ui.success(removingSelf && specs.length === 0
332
- ? "removed global dpm (bins + package)"
333
- : `removed global ${targets.join(", ")}`);
338
+ if (didWindowsUninstall && otherTargets.length === 0) {
339
+ // uninstallWindowsExe already reported success
340
+ return;
334
341
  }
335
- else {
336
- ui.info(`removed global ${targets.join(", ")}`);
342
+ if (removingSelf && specs.length === 0) {
343
+ if (ui.success)
344
+ ui.success("removed global dpm");
345
+ else
346
+ ui.info("removed global dpm");
347
+ return;
337
348
  }
349
+ if (ui.success)
350
+ ui.success(`removed global ${targets.join(", ")}`);
351
+ else
352
+ ui.info(`removed global ${targets.join(", ")}`);
338
353
  }
339
354
  export async function addPackage(api, name, range = "latest", cwd = process.cwd(), log = createUi()) {
340
355
  const spec = range === "latest" ? name : `${name}@${range}`;
341
356
  return addPackages(api, [spec], cwd, log);
342
357
  }
358
+ /** Drops a package name from package.json dependency maps. */
359
+ async function stripFromPackageJson(name, cwd) {
360
+ const pkg = await readPackageJson(cwd);
361
+ let changed = false;
362
+ for (const field of [
363
+ "dependencies",
364
+ "devDependencies",
365
+ "optionalDependencies",
366
+ "peerDependencies",
367
+ ]) {
368
+ const bag = pkg[field];
369
+ if (bag && typeof bag === "object" && !Array.isArray(bag) && name in bag) {
370
+ delete bag[name];
371
+ changed = true;
372
+ }
373
+ }
374
+ if (changed)
375
+ await writePackageJson(pkg, cwd);
376
+ return changed;
377
+ }
378
+ /** Module folder under node_modules for a bare or scoped package name. */
379
+ function nodeModulesPath(cwd, name) {
380
+ if (name.startsWith("@")) {
381
+ const [scope, pkg] = name.split("/");
382
+ return path.join(cwd, "node_modules", scope, pkg);
383
+ }
384
+ return path.join(cwd, "node_modules", name);
385
+ }
386
+ /**
387
+ * Removes a local dependency via the managed runtime.
388
+ * If Bun fails (e.g. ENOENT on an unrelated cached tarball), falls back to
389
+ * editing package.json + reinstall.
390
+ */
343
391
  export async function removePackage(name, cwd = process.cwd(), log = createUi()) {
344
392
  const ui = asUi(log);
345
- const pkg = await readPackageJson(cwd);
346
- const deps = pkg.dependencies || {};
347
- const dev = pkg.devDependencies || {};
348
- if (!deps[name] && !dev[name]) {
349
- ui.warn(`${name} is not in package.json`);
350
- }
351
- delete deps[name];
352
- delete dev[name];
353
- pkg.dependencies = deps;
354
- pkg.devDependencies = dev;
355
- await writePackageJson(pkg, cwd);
356
- await rm(path.join(cwd, "node_modules", ...name.split("/")), {
357
- recursive: true,
358
- force: true,
393
+ const { env } = await prepareRuntimeInstall(cwd);
394
+ ui.clearStatus();
395
+ ui.info(`${c.dim("→")} remove ${c.bold(name)}`);
396
+ const code = await spawnRuntime({
397
+ cwd,
398
+ args: ["remove", name],
399
+ env,
400
+ brandOutput: true,
359
401
  });
360
- const lock = await loadLockfile(cwd);
361
- delete lock.packages[name];
362
- await saveLockfile(lock, cwd);
402
+ if (code === 0) {
403
+ await syncLockFromNodeModules(cwd);
404
+ if (ui.success)
405
+ ui.success(`removed ${c.bold(name)}`);
406
+ else
407
+ ui.info(`removed ${name}`);
408
+ return;
409
+ }
410
+ ui.warn("runtime remove failed (often a missing cache tarball) — updating package.json and reinstalling");
411
+ const stripped = await stripFromPackageJson(name, cwd);
412
+ if (!stripped) {
413
+ throw new Error(`remove failed: ${name} is not listed in package.json`);
414
+ }
415
+ await rm(nodeModulesPath(cwd, name), { recursive: true, force: true });
416
+ const installCode = await spawnRuntime({
417
+ cwd,
418
+ args: ["install"],
419
+ env,
420
+ brandOutput: true,
421
+ });
422
+ if (installCode !== 0) {
423
+ throw new Error(`remove failed (exit ${code}); reinstall also failed. Try deleting node_modules and the lockfile, then dpm i`);
424
+ }
425
+ await syncLockFromNodeModules(cwd);
363
426
  if (ui.success)
364
427
  ui.success(`removed ${c.bold(name)}`);
365
428
  else
@@ -384,7 +447,7 @@ export async function runAudit(api, cwd = process.cwd(), log = createUi()) {
384
447
  return { hits, fixes: fixes.filter((f) => !f.breaking) };
385
448
  }
386
449
  /**
387
- * Applies available advisory fixes (like `npm audit fix`), then reinstalls.
450
+ * Applies available advisory fixes, then reinstalls via the DPM runtime.
388
451
  */
389
452
  export async function auditFix(api, cwd = process.cwd(), log = createUi(), force = false) {
390
453
  const ui = asUi(log);
@@ -418,7 +481,6 @@ export async function auditFix(api, cwd = process.cwd(), log = createUi(), force
418
481
  devDependencies[fix.name] = `^${fix.to}`;
419
482
  }
420
483
  else {
421
- // Persist transitive fixes so the next plain install keeps them.
422
484
  overrides[fix.name] = fix.to;
423
485
  }
424
486
  }
@@ -433,8 +495,26 @@ export async function auditFix(api, cwd = process.cwd(), log = createUi(), force
433
495
  });
434
496
  return { fixed: applicable, lock: next };
435
497
  }
498
+ async function readLocalReadme(cwd) {
499
+ for (const name of ["README.md", "readme.md", "README", "readme.txt", "README.txt"]) {
500
+ try {
501
+ const text = await readFile(path.join(cwd, name), "utf8");
502
+ if (text.trim())
503
+ return text;
504
+ }
505
+ catch {
506
+ /* try next */
507
+ }
508
+ }
509
+ return "";
510
+ }
511
+ /** Packs the current package into a gzipped tarball for publish. */
436
512
  export async function packTarball(cwd = process.cwd()) {
437
513
  const manifest = await readPackageJson(cwd);
514
+ const readme = await readLocalReadme(cwd);
515
+ if (readme && !String(manifest.readme ?? "").trim()) {
516
+ manifest.readme = readme;
517
+ }
438
518
  const out = path.join(cwd, `.dpm-pack-${Date.now()}.tgz`);
439
519
  await tar.c({
440
520
  gzip: true,
@@ -444,7 +524,10 @@ export async function packTarball(cwd = process.cwd()) {
444
524
  filter: (p) => !p.includes("node_modules") &&
445
525
  !p.startsWith(".git") &&
446
526
  !p.endsWith(".tgz") &&
447
- p !== "dpm-lock.yaml",
527
+ p !== "dpm-lock.yaml" &&
528
+ p !== "bun.lock" &&
529
+ p !== "bun.lockb" &&
530
+ p !== "bunfig.toml",
448
531
  }, ["."]);
449
532
  const buf = await readFile(out);
450
533
  await rm(out, { force: true });