@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/README.md +24 -17
- package/dist/config.js +17 -4
- package/dist/index.js +239 -55
- package/dist/install.d.ts +18 -9
- package/dist/install.js +332 -249
- package/dist/runtime.d.ts +39 -0
- package/dist/runtime.js +279 -0
- package/dist/updateCheck.d.ts +2 -2
- package/dist/updateCheck.js +67 -32
- package/dist/windowsInstall.d.ts +16 -0
- package/dist/windowsInstall.js +175 -0
- package/package.json +12 -14
package/dist/install.js
CHANGED
|
@@ -1,63 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
17
|
+
function setStatus(log, msg) {
|
|
18
|
+
if (log.status)
|
|
19
|
+
log.status(msg);
|
|
20
|
+
else
|
|
21
|
+
log.info(msg);
|
|
41
22
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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(
|
|
162
|
+
ui.success(`added ${c.bold(String(count))} package${count === 1 ? "" : "s"} in ${elapsed}s`);
|
|
188
163
|
}
|
|
189
164
|
else {
|
|
190
|
-
ui.info(`added ${
|
|
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(
|
|
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
|
-
|
|
208
|
-
const
|
|
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
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
222
|
-
*
|
|
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(
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
const
|
|
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 ${
|
|
242
|
+
ui.success(`installed ${mapped.join(", ")} globally`);
|
|
262
243
|
else
|
|
263
|
-
ui.info(`installed ${
|
|
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
|
|
267
|
-
|
|
268
|
-
return
|
|
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
|
-
|
|
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
|
-
?
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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 (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
: `removed global ${targets.join(", ")}`);
|
|
338
|
+
if (didWindowsUninstall && otherTargets.length === 0) {
|
|
339
|
+
// uninstallWindowsExe already reported success
|
|
340
|
+
return;
|
|
334
341
|
}
|
|
335
|
-
|
|
336
|
-
ui.
|
|
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
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
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 });
|