@drops-it/dpm 1.1.1 → 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/config.js +10 -3
- package/dist/index.js +3 -1
- package/dist/install.d.ts +7 -3
- package/dist/install.js +152 -40
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +67 -22
- 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 -10
package/dist/config.js
CHANGED
|
@@ -14,9 +14,16 @@ export async function loadConfig() {
|
|
|
14
14
|
registry: "https://dpm.drops-it.com",
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
// Env
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// Env can override the API host — but public npm is an install fallback only,
|
|
18
|
+
// never a DPM registry API (publish/login/search would 404 there).
|
|
19
|
+
if (process.env.DPM_REGISTRY) {
|
|
20
|
+
const raw = process.env.DPM_REGISTRY.trim();
|
|
21
|
+
const isPublicNpm = raw === "npmjs" ||
|
|
22
|
+
/registry\.npmjs\.org/i.test(raw) ||
|
|
23
|
+
/^https?:\/\/npmjs\.org\/?$/i.test(raw);
|
|
24
|
+
if (!isPublicNpm)
|
|
25
|
+
config.registry = raw;
|
|
26
|
+
}
|
|
20
27
|
if (process.env.DPM_TOKEN)
|
|
21
28
|
config.token = process.env.DPM_TOKEN;
|
|
22
29
|
return config;
|
package/dist/index.js
CHANGED
|
@@ -47,8 +47,10 @@ function shouldForwardToRuntime(argv) {
|
|
|
47
47
|
if (args.length === 0)
|
|
48
48
|
return false;
|
|
49
49
|
const first = args[0];
|
|
50
|
+
// `-v` must stay on the CLI (Commander); do not forward to the runtime (Bun).
|
|
50
51
|
if (first === "-h" ||
|
|
51
52
|
first === "--help" ||
|
|
53
|
+
first === "-v" ||
|
|
52
54
|
first === "-V" ||
|
|
53
55
|
first === "--version") {
|
|
54
56
|
return false;
|
|
@@ -78,7 +80,7 @@ const program = new Command();
|
|
|
78
80
|
program
|
|
79
81
|
.name("dpm")
|
|
80
82
|
.description("Drops Package Manager — same commands as a full JS toolkit, plus DPM registry auth/publish")
|
|
81
|
-
.version(CLI_VERSION)
|
|
83
|
+
.version(CLI_VERSION, "-v, --version", "output the DPM CLI version")
|
|
82
84
|
.addHelpText("after", `
|
|
83
85
|
Runtime (same surface as a modern JS toolkit; binary is managed by DPM):
|
|
84
86
|
dpm --watch <file> Run a file with reload
|
package/dist/install.d.ts
CHANGED
|
@@ -27,16 +27,20 @@ export declare function installAll(_api: DpmApi, cwd?: string, log?: InstallLog,
|
|
|
27
27
|
export declare function addPackages(api: DpmApi, specs: string[], cwd?: string, log?: InstallLog): Promise<Lockfile>;
|
|
28
28
|
/**
|
|
29
29
|
* Installs packages into the global prefix via the managed DPM runtime.
|
|
30
|
-
*
|
|
30
|
+
* Self-update of the Windows exe uses `/download/dpm.exe` instead of bun -g.
|
|
31
31
|
*/
|
|
32
32
|
export declare function installGlobalPackages(_api: DpmApi, specs: string[], log?: InstallLog): Promise<void>;
|
|
33
33
|
/**
|
|
34
34
|
* Uninstalls global packages (and cleans dpm bin shims when removing the CLI).
|
|
35
|
-
* `dpm rm -g` with no names removes the dpm CLI itself.
|
|
35
|
+
* `dpm rm -g` with no names removes the dpm CLI itself (Windows exe + bun/npm globals).
|
|
36
36
|
*/
|
|
37
37
|
export declare function removeGlobalPackages(specs: string[], log?: InstallLog): Promise<void>;
|
|
38
38
|
export declare function addPackage(api: DpmApi, name: string, range?: string, cwd?: string, log?: InstallLog): Promise<Lockfile>;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Removes a local dependency via the managed runtime.
|
|
41
|
+
* If Bun fails (e.g. ENOENT on an unrelated cached tarball), falls back to
|
|
42
|
+
* editing package.json + reinstall.
|
|
43
|
+
*/
|
|
40
44
|
export declare function removePackage(name: string, cwd?: string, log?: InstallLog): Promise<void>;
|
|
41
45
|
/**
|
|
42
46
|
* Runs a vulnerability audit against the current lockfile (or installs first).
|
package/dist/install.js
CHANGED
|
@@ -7,6 +7,7 @@ import { c } from "./colors.js";
|
|
|
7
7
|
import { loadConfig, loadLockfile, readPackageJson, saveLockfile, writePackageJson, } from "./config.js";
|
|
8
8
|
import { ensureInstallConfig, runtimeInstallEnv, spawnRuntime, } from "./runtime.js";
|
|
9
9
|
import { createUi } from "./ui.js";
|
|
10
|
+
import { hasWindowsExeInstall, uninstallWindowsExe, updateWindowsExe, } from "./windowsInstall.js";
|
|
10
11
|
function asUi(log) {
|
|
11
12
|
if ("status" in log && typeof log.status === "function" && "clearStatus" in log) {
|
|
12
13
|
return log;
|
|
@@ -126,7 +127,7 @@ export async function syncLockFromNodeModules(cwd) {
|
|
|
126
127
|
async function prepareRuntimeInstall(cwd) {
|
|
127
128
|
const config = await loadConfig();
|
|
128
129
|
await ensureInstallConfig(config, cwd);
|
|
129
|
-
return { config, env: runtimeInstallEnv(config) };
|
|
130
|
+
return { config, env: runtimeInstallEnv(config, cwd) };
|
|
130
131
|
}
|
|
131
132
|
/** Installs package.json dependencies via the managed DPM runtime. */
|
|
132
133
|
export async function installAll(_api, cwd = process.cwd(), log = createUi(), options = {}) {
|
|
@@ -142,7 +143,8 @@ export async function installAll(_api, cwd = process.cwd(), log = createUi(), op
|
|
|
142
143
|
await writePackageJson(pkg, cwd);
|
|
143
144
|
}
|
|
144
145
|
const { env } = await prepareRuntimeInstall(cwd);
|
|
145
|
-
|
|
146
|
+
ui.clearStatus();
|
|
147
|
+
ui.info(`${c.dim("→")} installing dependencies…`);
|
|
146
148
|
const started = Date.now();
|
|
147
149
|
const code = await spawnRuntime({
|
|
148
150
|
cwd,
|
|
@@ -150,7 +152,6 @@ export async function installAll(_api, cwd = process.cwd(), log = createUi(), op
|
|
|
150
152
|
env,
|
|
151
153
|
brandOutput: true,
|
|
152
154
|
});
|
|
153
|
-
ui.clearStatus();
|
|
154
155
|
if (code !== 0) {
|
|
155
156
|
throw new Error(`install failed (exit ${code})`);
|
|
156
157
|
}
|
|
@@ -183,14 +184,14 @@ export async function addPackages(api, specs, cwd = process.cwd(), log = createU
|
|
|
183
184
|
const ui = asUi(log);
|
|
184
185
|
await ensurePackageJson(cwd, ui);
|
|
185
186
|
const { env } = await prepareRuntimeInstall(cwd);
|
|
186
|
-
|
|
187
|
+
ui.clearStatus();
|
|
188
|
+
ui.info(`${c.dim("→")} add ${specs.join(", ")}`);
|
|
187
189
|
const code = await spawnRuntime({
|
|
188
190
|
cwd,
|
|
189
191
|
args: ["add", ...specs],
|
|
190
192
|
env,
|
|
191
193
|
brandOutput: true,
|
|
192
194
|
});
|
|
193
|
-
ui.clearStatus();
|
|
194
195
|
if (code !== 0)
|
|
195
196
|
throw new Error(`add failed (exit ${code})`);
|
|
196
197
|
for (const spec of specs) {
|
|
@@ -199,30 +200,62 @@ export async function addPackages(api, specs, cwd = process.cwd(), log = createU
|
|
|
199
200
|
}
|
|
200
201
|
return syncLockFromNodeModules(cwd);
|
|
201
202
|
}
|
|
203
|
+
const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
|
|
204
|
+
function isCliSelfName(name) {
|
|
205
|
+
return CLI_SELF_NAMES.includes(name);
|
|
206
|
+
}
|
|
202
207
|
/**
|
|
203
208
|
* Installs packages into the global prefix via the managed DPM runtime.
|
|
204
|
-
*
|
|
209
|
+
* Self-update of the Windows exe uses `/download/dpm.exe` instead of bun -g.
|
|
205
210
|
*/
|
|
206
211
|
export async function installGlobalPackages(_api, specs, log = createUi()) {
|
|
207
212
|
if (specs.length === 0) {
|
|
208
213
|
throw new Error("global install requires at least one package name");
|
|
209
214
|
}
|
|
210
215
|
const ui = asUi(log);
|
|
211
|
-
const
|
|
216
|
+
const parsed = specs.map((spec) => {
|
|
212
217
|
const { name, range } = parsePackageSpec(spec);
|
|
213
218
|
const real = name === "@drops-it/dpm" ? "dpm" : name;
|
|
214
|
-
return
|
|
219
|
+
return { real, range, original: spec };
|
|
215
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);
|
|
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})`);
|
|
241
|
+
if (ui.success)
|
|
242
|
+
ui.success(`installed ${mapped.join(", ")} globally`);
|
|
243
|
+
else
|
|
244
|
+
ui.info(`installed ${mapped.join(", ")} globally`);
|
|
245
|
+
}
|
|
246
|
+
if (otherSpecs.length === 0)
|
|
247
|
+
return;
|
|
248
|
+
const mapped = otherSpecs.map((p) => p.range === "latest" ? p.real : `${p.real}@${p.range}`);
|
|
216
249
|
const config = await loadConfig();
|
|
217
250
|
await ensureInstallConfig(config, process.cwd());
|
|
218
251
|
const env = runtimeInstallEnv(config);
|
|
219
|
-
|
|
252
|
+
ui.clearStatus();
|
|
253
|
+
ui.info(`${c.dim("→")} install -g ${mapped.join(", ")}`);
|
|
220
254
|
const code = await spawnRuntime({
|
|
221
255
|
args: ["add", "-g", ...mapped],
|
|
222
256
|
env,
|
|
223
257
|
brandOutput: true,
|
|
224
258
|
});
|
|
225
|
-
ui.clearStatus();
|
|
226
259
|
if (code !== 0)
|
|
227
260
|
throw new Error(`global install failed (exit ${code})`);
|
|
228
261
|
if (ui.success)
|
|
@@ -230,12 +263,8 @@ export async function installGlobalPackages(_api, specs, log = createUi()) {
|
|
|
230
263
|
else
|
|
231
264
|
ui.info(`installed ${mapped.join(", ")} globally`);
|
|
232
265
|
}
|
|
233
|
-
const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
|
|
234
266
|
async function runtimeGlobalPrefix() {
|
|
235
267
|
// Best-effort: common user global paths when removing the CLI.
|
|
236
|
-
if (process.platform === "win32") {
|
|
237
|
-
return path.join(homedir(), ".dpm", "global");
|
|
238
|
-
}
|
|
239
268
|
return path.join(homedir(), ".dpm", "global");
|
|
240
269
|
}
|
|
241
270
|
/** Removes leftover Windows/Unix bin shims for a command name. */
|
|
@@ -251,20 +280,51 @@ async function removeGlobalBinShims(binName, prefix) {
|
|
|
251
280
|
];
|
|
252
281
|
await Promise.all(candidates.map((p) => rm(p, { force: true })));
|
|
253
282
|
}
|
|
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
|
+
}
|
|
254
297
|
/**
|
|
255
298
|
* Uninstalls global packages (and cleans dpm bin shims when removing the CLI).
|
|
256
|
-
* `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).
|
|
257
300
|
*/
|
|
258
301
|
export async function removeGlobalPackages(specs, log = createUi()) {
|
|
259
302
|
const ui = asUi(log);
|
|
260
303
|
const targets = specs.length > 0
|
|
261
|
-
?
|
|
262
|
-
|
|
304
|
+
? [
|
|
305
|
+
...new Set(specs
|
|
306
|
+
.map((s) => parsePackageSpec(s).name)
|
|
307
|
+
.map((n) => (n === "@drops-it/dpm" ? "dpm" : n))),
|
|
308
|
+
]
|
|
309
|
+
: ["dpm"];
|
|
263
310
|
const config = await loadConfig();
|
|
264
311
|
const env = runtimeInstallEnv(config);
|
|
265
|
-
const removingSelf = targets.some(
|
|
266
|
-
|
|
267
|
-
|
|
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;
|
|
318
|
+
}
|
|
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)}`);
|
|
268
328
|
await spawnRuntime({
|
|
269
329
|
args: ["remove", "-g", name],
|
|
270
330
|
env,
|
|
@@ -272,44 +332,96 @@ export async function removeGlobalPackages(specs, log = createUi()) {
|
|
|
272
332
|
});
|
|
273
333
|
}
|
|
274
334
|
if (removingSelf) {
|
|
275
|
-
|
|
276
|
-
await removeGlobalBinShims("dpm", prefix);
|
|
277
|
-
const roaming = path.join(homedir(), "AppData", "Roaming", "npm");
|
|
278
|
-
await removeGlobalBinShims("dpm", roaming);
|
|
279
|
-
await rm(path.join(prefix, "node_modules", "dpm"), { recursive: true, force: true });
|
|
280
|
-
await rm(path.join(homedir(), ".bun", "install", "global", "node_modules", "dpm"), {
|
|
281
|
-
recursive: true,
|
|
282
|
-
force: true,
|
|
283
|
-
}).catch(() => undefined);
|
|
335
|
+
await scrubCliGlobalLeftovers();
|
|
284
336
|
}
|
|
285
337
|
ui.clearStatus();
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
: `removed global ${targets.join(", ")}`);
|
|
338
|
+
if (didWindowsUninstall && otherTargets.length === 0) {
|
|
339
|
+
// uninstallWindowsExe already reported success
|
|
340
|
+
return;
|
|
290
341
|
}
|
|
291
|
-
|
|
292
|
-
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;
|
|
293
348
|
}
|
|
349
|
+
if (ui.success)
|
|
350
|
+
ui.success(`removed global ${targets.join(", ")}`);
|
|
351
|
+
else
|
|
352
|
+
ui.info(`removed global ${targets.join(", ")}`);
|
|
294
353
|
}
|
|
295
354
|
export async function addPackage(api, name, range = "latest", cwd = process.cwd(), log = createUi()) {
|
|
296
355
|
const spec = range === "latest" ? name : `${name}@${range}`;
|
|
297
356
|
return addPackages(api, [spec], cwd, log);
|
|
298
357
|
}
|
|
299
|
-
/**
|
|
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
|
+
*/
|
|
300
391
|
export async function removePackage(name, cwd = process.cwd(), log = createUi()) {
|
|
301
392
|
const ui = asUi(log);
|
|
302
393
|
const { env } = await prepareRuntimeInstall(cwd);
|
|
303
|
-
|
|
394
|
+
ui.clearStatus();
|
|
395
|
+
ui.info(`${c.dim("→")} remove ${c.bold(name)}`);
|
|
304
396
|
const code = await spawnRuntime({
|
|
305
397
|
cwd,
|
|
306
398
|
args: ["remove", name],
|
|
307
399
|
env,
|
|
308
400
|
brandOutput: true,
|
|
309
401
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
}
|
|
313
425
|
await syncLockFromNodeModules(cwd);
|
|
314
426
|
if (ui.success)
|
|
315
427
|
ui.success(`removed ${c.bold(name)}`);
|
package/dist/runtime.d.ts
CHANGED
|
@@ -16,22 +16,24 @@ export type SpawnRuntimeOptions = {
|
|
|
16
16
|
env?: NodeJS.ProcessEnv;
|
|
17
17
|
/** Extra args after the runtime binary (e.g. ["install"], ["run", "start"]). */
|
|
18
18
|
args: string[];
|
|
19
|
-
/** When true, rewrite stderr
|
|
19
|
+
/** When true, pipe + rewrite stdout/stderr so bun never leaks into the UI. */
|
|
20
20
|
brandOutput?: boolean;
|
|
21
21
|
};
|
|
22
22
|
/**
|
|
23
23
|
* Spawns the managed runtime with inherited stdio and returns the exit code.
|
|
24
|
+
* With `brandOutput`, both stdout and stderr are piped, branded, and each
|
|
25
|
+
* write starts on a clean line so it never glues onto a DPM spinner.
|
|
24
26
|
*/
|
|
25
27
|
export declare function spawnRuntime(opts: SpawnRuntimeOptions): Promise<number>;
|
|
26
28
|
/**
|
|
27
29
|
* Resolves the install registry URL.
|
|
28
|
-
* DPM hosts use the `/npm` facade;
|
|
30
|
+
* DPM hosts use the `/npm` facade; bootstrap / this app monorepo / npmjs → public npm.
|
|
29
31
|
*/
|
|
30
|
-
export declare function resolveInstallRegistryUrl(registry: string): string;
|
|
32
|
+
export declare function resolveInstallRegistryUrl(registry: string, cwd?: string): string;
|
|
31
33
|
/**
|
|
32
34
|
* Writes project-local registry config so the runtime installer hits the DPM npm facade
|
|
33
|
-
* (or
|
|
35
|
+
* (or public npm when bootstrapping / installing this app monorepo).
|
|
34
36
|
*/
|
|
35
37
|
export declare function ensureInstallConfig(config: DpmConfig, cwd?: string): Promise<void>;
|
|
36
38
|
/** Builds npm_config_* env for a one-shot install without relying only on files. */
|
|
37
|
-
export declare function runtimeInstallEnv(config: DpmConfig): NodeJS.ProcessEnv;
|
|
39
|
+
export declare function runtimeInstallEnv(config: DpmConfig, cwd?: string): NodeJS.ProcessEnv;
|
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { access, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { homedir, platform, tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
/** Public npm — used when this app must install while its own registry is offline. */
|
|
7
|
+
const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
6
8
|
/** Pinned managed runtime version (oven-sh/bun release tag without prefix). */
|
|
7
9
|
export const RUNTIME_VERSION = "1.2.19";
|
|
8
10
|
const RUNTIME_NAME = process.platform === "win32" ? "dpm-runtime.exe" : "dpm-runtime";
|
|
@@ -155,14 +157,16 @@ export async function resolveRuntime() {
|
|
|
155
157
|
throw new Error(`DPM runtime is not available (${msg}). Re-run the DPM installer or set DPM_RUNTIME.`);
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
/** Rewrites managed-runtime banners so users only see DPM branding. */
|
|
161
|
+
function brandText(text) {
|
|
162
|
+
return text
|
|
161
163
|
.replace(/\bBun\b/g, "DPM")
|
|
162
|
-
.replace(/\bbun\b/g, "dpm")
|
|
164
|
+
.replace(/\bbun\b/g, "dpm");
|
|
163
165
|
}
|
|
164
166
|
/**
|
|
165
167
|
* Spawns the managed runtime with inherited stdio and returns the exit code.
|
|
168
|
+
* With `brandOutput`, both stdout and stderr are piped, branded, and each
|
|
169
|
+
* write starts on a clean line so it never glues onto a DPM spinner.
|
|
166
170
|
*/
|
|
167
171
|
export async function spawnRuntime(opts) {
|
|
168
172
|
const bin = await resolveRuntime();
|
|
@@ -173,58 +177,99 @@ export async function spawnRuntime(opts) {
|
|
|
173
177
|
npm_config_registry: opts.env?.npm_config_registry ?? process.env.npm_config_registry,
|
|
174
178
|
};
|
|
175
179
|
return new Promise((resolve, reject) => {
|
|
180
|
+
const brand = Boolean(opts.brandOutput);
|
|
176
181
|
const child = spawn(bin, opts.args, {
|
|
177
182
|
cwd: opts.cwd ?? process.cwd(),
|
|
178
183
|
env,
|
|
179
|
-
stdio:
|
|
184
|
+
stdio: brand ? ["inherit", "pipe", "pipe"] : "inherit",
|
|
180
185
|
windowsHide: true,
|
|
181
186
|
});
|
|
182
|
-
|
|
187
|
+
let needClear = brand;
|
|
188
|
+
const writeBranded = (stream, chunk) => {
|
|
189
|
+
let text = brandText(chunk.toString("utf8"));
|
|
190
|
+
if (!text)
|
|
191
|
+
return;
|
|
192
|
+
if (needClear) {
|
|
193
|
+
// Drop any leftover `\r`-style status line before the first child byte.
|
|
194
|
+
stream.write("\r\x1b[2K");
|
|
195
|
+
needClear = false;
|
|
196
|
+
}
|
|
197
|
+
// If a chunk starts mid-line after a previous incomplete write, keep as-is;
|
|
198
|
+
// only force a newline when the previous paint left the cursor mid-status.
|
|
199
|
+
stream.write(text);
|
|
200
|
+
};
|
|
201
|
+
if (brand && child.stdout) {
|
|
202
|
+
child.stdout.on("data", (chunk) => {
|
|
203
|
+
writeBranded(process.stdout, chunk);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (brand && child.stderr) {
|
|
183
207
|
child.stderr.on("data", (chunk) => {
|
|
184
|
-
process.stderr
|
|
208
|
+
writeBranded(process.stderr, chunk);
|
|
185
209
|
});
|
|
186
210
|
}
|
|
187
211
|
child.on("error", reject);
|
|
188
212
|
child.on("close", (code) => resolve(code ?? 1));
|
|
189
213
|
});
|
|
190
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* True for the DPM registry app monorepo (`package.json` name dpm + private).
|
|
217
|
+
* That tree must install from public npm — its own /npm façade is down during restart.
|
|
218
|
+
*/
|
|
219
|
+
function isDpmAppMonorepo(cwd) {
|
|
220
|
+
try {
|
|
221
|
+
const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
222
|
+
return pkg.name === "dpm" && pkg.private === true;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
191
228
|
/**
|
|
192
229
|
* Resolves the install registry URL.
|
|
193
|
-
* DPM hosts use the `/npm` facade;
|
|
230
|
+
* DPM hosts use the `/npm` facade; bootstrap / this app monorepo / npmjs → public npm.
|
|
194
231
|
*/
|
|
195
|
-
export function resolveInstallRegistryUrl(registry) {
|
|
232
|
+
export function resolveInstallRegistryUrl(registry, cwd = process.cwd()) {
|
|
233
|
+
if (isDpmAppMonorepo(cwd))
|
|
234
|
+
return PUBLIC_NPM_REGISTRY;
|
|
196
235
|
const base = registry.replace(/\/$/, "").replace(/\/npm$/i, "");
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
return
|
|
236
|
+
const usePublicNpm = process.env.DPM_BOOTSTRAP === "1" ||
|
|
237
|
+
process.env.DPM_USE_NPM_UPSTREAM === "1" ||
|
|
238
|
+
/registry\.npmjs\.org/i.test(base);
|
|
239
|
+
if (usePublicNpm)
|
|
240
|
+
return PUBLIC_NPM_REGISTRY;
|
|
202
241
|
return `${base}/npm/`;
|
|
203
242
|
}
|
|
204
243
|
/**
|
|
205
244
|
* Writes project-local registry config so the runtime installer hits the DPM npm facade
|
|
206
|
-
* (or
|
|
245
|
+
* (or public npm when bootstrapping / installing this app monorepo).
|
|
207
246
|
*/
|
|
208
247
|
export async function ensureInstallConfig(config, cwd = process.cwd()) {
|
|
209
|
-
const registryUrl = resolveInstallRegistryUrl(config.registry);
|
|
248
|
+
const registryUrl = resolveInstallRegistryUrl(config.registry, cwd);
|
|
210
249
|
const host = new URL(registryUrl).host;
|
|
211
250
|
const pathPrefix = registryUrl.includes("/npm") ? "/npm" : "";
|
|
251
|
+
const token = registryUrl.includes("registry.npmjs.org")
|
|
252
|
+
? ""
|
|
253
|
+
: (config.token ?? "");
|
|
212
254
|
const npmrcLines = [
|
|
213
255
|
`registry=${registryUrl}`,
|
|
214
|
-
`//${host}${pathPrefix}/:_authToken=${
|
|
256
|
+
`//${host}${pathPrefix}/:_authToken=${token}`,
|
|
215
257
|
];
|
|
216
|
-
await writeFile(path.join(cwd, ".npmrc"), `${npmrcLines.join("\n")}\n`, {
|
|
258
|
+
await writeFile(path.join(cwd, ".npmrc"), `${npmrcLines.join("\n")}\n`, {
|
|
259
|
+
mode: 0o600,
|
|
260
|
+
});
|
|
217
261
|
const bunfig = `[install]
|
|
218
262
|
registry = "${registryUrl}"
|
|
219
263
|
`;
|
|
220
264
|
await writeFile(path.join(cwd, "bunfig.toml"), bunfig);
|
|
221
265
|
}
|
|
222
266
|
/** Builds npm_config_* env for a one-shot install without relying only on files. */
|
|
223
|
-
export function runtimeInstallEnv(config) {
|
|
224
|
-
const registryUrl = resolveInstallRegistryUrl(config.registry);
|
|
267
|
+
export function runtimeInstallEnv(config, cwd = process.cwd()) {
|
|
268
|
+
const registryUrl = resolveInstallRegistryUrl(config.registry, cwd);
|
|
269
|
+
const useToken = !registryUrl.includes("registry.npmjs.org") && config.token;
|
|
225
270
|
return {
|
|
226
271
|
npm_config_registry: registryUrl,
|
|
227
|
-
...(
|
|
272
|
+
...(useToken
|
|
228
273
|
? {
|
|
229
274
|
npm_config__authToken: config.token,
|
|
230
275
|
BUN_CONFIG_TOKEN: config.token,
|
package/dist/updateCheck.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export declare const CLI_NAME
|
|
1
|
+
export declare const CLI_NAME = "@drops-it/dpm";
|
|
2
2
|
export declare const CLI_VERSION: string;
|
|
3
3
|
/**
|
|
4
4
|
* On the first dpm command in a new terminal session, checks the DPM registry
|
|
5
|
-
* for a newer CLI and prints
|
|
5
|
+
* for a newer CLI and prints a one-line hint (non-blocking on failure).
|
|
6
6
|
*/
|
|
7
7
|
export declare function maybeNotifyUpdate(): Promise<void>;
|
package/dist/updateCheck.js
CHANGED
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import semver from "semver";
|
|
6
8
|
import { c } from "./colors.js";
|
|
7
9
|
import { loadConfig } from "./config.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Resolves CLI version from esbuild inject, nearby package.json (npm layout),
|
|
12
|
+
* or a safe fallback — never throws at import time.
|
|
13
|
+
*/
|
|
14
|
+
function resolveCliVersion() {
|
|
15
|
+
// Windows single-file bundle: esbuild define replaces this identifier.
|
|
16
|
+
try {
|
|
17
|
+
const injected = new Function("return typeof __DPM_CLI_VERSION__ !== 'undefined' ? __DPM_CLI_VERSION__ : undefined")();
|
|
18
|
+
if (typeof injected === "string" && injected.length > 0)
|
|
19
|
+
return injected;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* not injected (tsc / npm dist) */
|
|
23
|
+
}
|
|
24
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
for (const file of [
|
|
26
|
+
path.join(here, "..", "package.json"),
|
|
27
|
+
path.join(here, "package.json"),
|
|
28
|
+
]) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
31
|
+
if (pkg.version)
|
|
32
|
+
return pkg.version;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* try next */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const req = createRequire(import.meta.url);
|
|
40
|
+
const pkg = req("../package.json");
|
|
41
|
+
if (pkg.version)
|
|
42
|
+
return pkg.version;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* bundled exe has no sibling package.json */
|
|
46
|
+
}
|
|
47
|
+
return "1.1.5";
|
|
48
|
+
}
|
|
49
|
+
export const CLI_NAME = "@drops-it/dpm";
|
|
50
|
+
export const CLI_VERSION = resolveCliVersion();
|
|
12
51
|
const STATE_DIR = path.join(homedir(), ".dpm");
|
|
13
52
|
const STATE_PATH = path.join(STATE_DIR, "update-check.json");
|
|
14
|
-
/** Identifies the current terminal so we only notify once per new shell. */
|
|
15
53
|
function sessionKey() {
|
|
16
54
|
const parts = [`ppid:${process.ppid}`];
|
|
17
55
|
if (process.env.WT_SESSION)
|
|
@@ -22,11 +60,12 @@ function sessionKey() {
|
|
|
22
60
|
}
|
|
23
61
|
async function loadState() {
|
|
24
62
|
try {
|
|
25
|
-
const raw =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
const raw = await readFile(STATE_PATH, "utf8");
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
return {
|
|
66
|
+
sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
|
|
67
|
+
latest: parsed.latest,
|
|
68
|
+
};
|
|
30
69
|
}
|
|
31
70
|
catch {
|
|
32
71
|
return { sessions: [] };
|
|
@@ -34,48 +73,44 @@ async function loadState() {
|
|
|
34
73
|
}
|
|
35
74
|
async function saveState(state) {
|
|
36
75
|
await mkdir(STATE_DIR, { recursive: true });
|
|
37
|
-
await writeFile(STATE_PATH, JSON.stringify(
|
|
76
|
+
await writeFile(STATE_PATH, JSON.stringify(state), { mode: 0o600 });
|
|
38
77
|
}
|
|
39
78
|
async function fetchLatestFromDpm(registry) {
|
|
40
79
|
const url = `${registry.replace(/\/$/, "")}/dpm/v1/packages/dpm`;
|
|
41
80
|
const res = await fetch(url, {
|
|
42
|
-
headers: {
|
|
43
|
-
|
|
81
|
+
headers: {
|
|
82
|
+
accept: "application/json",
|
|
83
|
+
"user-agent": `dpm-cli/${CLI_VERSION}`,
|
|
84
|
+
},
|
|
44
85
|
});
|
|
45
86
|
if (!res.ok)
|
|
46
87
|
return null;
|
|
47
|
-
const
|
|
48
|
-
return
|
|
88
|
+
const body = (await res.json());
|
|
89
|
+
return body.latest ?? body["dist-tags"]?.latest ?? null;
|
|
49
90
|
}
|
|
50
91
|
/**
|
|
51
92
|
* On the first dpm command in a new terminal session, checks the DPM registry
|
|
52
|
-
* for a newer CLI and prints
|
|
93
|
+
* for a newer CLI and prints a one-line hint (non-blocking on failure).
|
|
53
94
|
*/
|
|
54
95
|
export async function maybeNotifyUpdate() {
|
|
55
|
-
const session = sessionKey();
|
|
56
|
-
const prev = await loadState();
|
|
57
|
-
if (prev.sessions.includes(session))
|
|
58
|
-
return;
|
|
59
|
-
await saveState({ sessions: [...prev.sessions, session], latest: prev.latest });
|
|
60
96
|
try {
|
|
97
|
+
const key = sessionKey();
|
|
98
|
+
const state = await loadState();
|
|
99
|
+
if (state.sessions.includes(key))
|
|
100
|
+
return;
|
|
61
101
|
const config = await loadConfig();
|
|
62
102
|
const latest = await fetchLatestFromDpm(config.registry);
|
|
103
|
+
state.sessions = [...state.sessions.filter((s) => s !== key), key].slice(-20);
|
|
104
|
+
if (latest)
|
|
105
|
+
state.latest = latest;
|
|
106
|
+
await saveState(state);
|
|
63
107
|
if (!latest || !semver.valid(latest) || !semver.valid(CLI_VERSION))
|
|
64
108
|
return;
|
|
65
|
-
await saveState({
|
|
66
|
-
sessions: [...prev.sessions, session],
|
|
67
|
-
latest,
|
|
68
|
-
});
|
|
69
109
|
if (!semver.gt(latest, CLI_VERSION))
|
|
70
110
|
return;
|
|
71
|
-
console.error(
|
|
72
|
-
"",
|
|
73
|
-
`${c.yellow("▲")} ${c.bold("dpm")} update available: ${c.dim(CLI_VERSION)} ${c.cyan("→")} ${c.green(latest)}`,
|
|
74
|
-
` Update with: ${c.bold("dpm i -g dpm")}`,
|
|
75
|
-
"",
|
|
76
|
-
].join("\n"));
|
|
111
|
+
console.error(`${c.yellow("▲")} ${c.bold("dpm")} update available: ${c.dim(CLI_VERSION)} ${c.cyan("→")} ${c.green(latest)}`);
|
|
77
112
|
}
|
|
78
113
|
catch {
|
|
79
|
-
|
|
114
|
+
/* never block the command */
|
|
80
115
|
}
|
|
81
116
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Ui } from "./ui.js";
|
|
2
|
+
/** `%LOCALAPPDATA%\Programs\DPM` — where `dpm.exe` setup installs. */
|
|
3
|
+
export declare function windowsExeInstallDir(): string;
|
|
4
|
+
export declare function windowsExePath(): string;
|
|
5
|
+
/** True when the Windows launcher install is present. */
|
|
6
|
+
export declare function hasWindowsExeInstall(): Promise<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Uninstalls the Windows `dpm.exe` layout (PATH, shortcut, cache, install dir).
|
|
9
|
+
* Schedules delete of the running tree after this process exits.
|
|
10
|
+
*/
|
|
11
|
+
export declare function uninstallWindowsExe(ui: Ui): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Replaces `%LOCALAPPDATA%\\Programs\\DPM\\dpm.exe` from the registry site's
|
|
14
|
+
* `/download/dpm.exe` (see latest.json). Used for `dpm i -g dpm` on Windows exe installs.
|
|
15
|
+
*/
|
|
16
|
+
export declare function updateWindowsExe(ui: Ui): Promise<void>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { access, mkdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { CLI_VERSION } from "./updateCheck.js";
|
|
10
|
+
async function exists(p) {
|
|
11
|
+
try {
|
|
12
|
+
await access(p);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** `%LOCALAPPDATA%\Programs\DPM` — where `dpm.exe` setup installs. */
|
|
20
|
+
export function windowsExeInstallDir() {
|
|
21
|
+
const base = process.env.LOCALAPPDATA || path.join(homedir(), "AppData", "Local");
|
|
22
|
+
return path.join(base, "Programs", "DPM");
|
|
23
|
+
}
|
|
24
|
+
export function windowsExePath() {
|
|
25
|
+
return path.join(windowsExeInstallDir(), "dpm.exe");
|
|
26
|
+
}
|
|
27
|
+
/** True when the Windows launcher install is present. */
|
|
28
|
+
export async function hasWindowsExeInstall() {
|
|
29
|
+
return process.platform === "win32" && exists(windowsExePath());
|
|
30
|
+
}
|
|
31
|
+
function startMenuShortcutPath() {
|
|
32
|
+
const appData = process.env.APPDATA || path.join(homedir(), "AppData", "Roaming");
|
|
33
|
+
return path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "DPM.lnk");
|
|
34
|
+
}
|
|
35
|
+
function cliCacheRoot() {
|
|
36
|
+
const base = process.env.LOCALAPPDATA || path.join(homedir(), "AppData", "Local");
|
|
37
|
+
return path.join(base, "dpm", "cli");
|
|
38
|
+
}
|
|
39
|
+
/** Removes `dir` from the user PATH (HKCU Environment). */
|
|
40
|
+
function removeFromUserPath(dir) {
|
|
41
|
+
const ps = `
|
|
42
|
+
$dir = [IO.Path]::GetFullPath('${dir.replace(/'/g, "''")}')
|
|
43
|
+
$key = 'HKCU:\\Environment'
|
|
44
|
+
$cur = (Get-ItemProperty -Path $key -Name Path -ErrorAction SilentlyContinue).Path
|
|
45
|
+
if (-not $cur) { return }
|
|
46
|
+
$parts = $cur -split ';' | Where-Object {
|
|
47
|
+
$_.Trim() -ne '' -and [IO.Path]::GetFullPath($_.Trim()) -ne $dir
|
|
48
|
+
}
|
|
49
|
+
$next = ($parts -join ';').Trim(';')
|
|
50
|
+
Set-ItemProperty -Path $key -Name Path -Value $next
|
|
51
|
+
Add-Type -Namespace Win32 -Name Native -MemberDefinition '[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);'
|
|
52
|
+
$hwnd = [IntPtr]0xffff; $r = [UIntPtr]::Zero
|
|
53
|
+
[Win32.Native]::SendMessageTimeout($hwnd, 0x1A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$r) | Out-Null
|
|
54
|
+
`;
|
|
55
|
+
spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { windowsHide: true, stdio: "ignore" });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Uninstalls the Windows `dpm.exe` layout (PATH, shortcut, cache, install dir).
|
|
59
|
+
* Schedules delete of the running tree after this process exits.
|
|
60
|
+
*/
|
|
61
|
+
export async function uninstallWindowsExe(ui) {
|
|
62
|
+
const installDir = windowsExeInstallDir();
|
|
63
|
+
const exe = windowsExePath();
|
|
64
|
+
if (!(await exists(exe))) {
|
|
65
|
+
ui.warn("No Windows dpm.exe install found under Local\\Programs\\DPM");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
ui.info("Removing Windows DPM install…");
|
|
69
|
+
removeFromUserPath(installDir);
|
|
70
|
+
await rm(startMenuShortcutPath(), { force: true });
|
|
71
|
+
await rm(cliCacheRoot(), { recursive: true, force: true });
|
|
72
|
+
// Cannot delete the running exe reliably — cmd deletes the folder after exit.
|
|
73
|
+
const bat = path.join(process.env.TEMP || installDir, `dpm-uninstall-${Date.now()}.cmd`);
|
|
74
|
+
const batBody = [
|
|
75
|
+
"@echo off",
|
|
76
|
+
"timeout /t 2 /nobreak >nul",
|
|
77
|
+
`rmdir /s /q "${installDir}"`,
|
|
78
|
+
`del /f /q "%~f0"`,
|
|
79
|
+
"",
|
|
80
|
+
].join("\r\n");
|
|
81
|
+
await writeFile(bat, batBody, "utf8");
|
|
82
|
+
const child = spawn("cmd.exe", ["/c", "start", "/b", "", bat], {
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
stdio: "ignore",
|
|
85
|
+
detached: true,
|
|
86
|
+
});
|
|
87
|
+
child.unref();
|
|
88
|
+
ui.success("Uninstalled DPM from PATH and Start Menu. Install folder will be removed shortly — open a new terminal.");
|
|
89
|
+
}
|
|
90
|
+
function siteBaseFromRegistry(registry) {
|
|
91
|
+
return registry.replace(/\/$/, "").replace(/\/npm$/i, "");
|
|
92
|
+
}
|
|
93
|
+
async function fetchLatestMeta(site) {
|
|
94
|
+
const url = `${site}/download/latest.json`;
|
|
95
|
+
const res = await fetch(url, {
|
|
96
|
+
headers: { accept: "application/json", "user-agent": `dpm-cli/${CLI_VERSION}` },
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
throw new Error(`Could not fetch ${url} (${res.status})`);
|
|
100
|
+
}
|
|
101
|
+
return (await res.json());
|
|
102
|
+
}
|
|
103
|
+
async function downloadToFile(url, dest) {
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
headers: { "user-agent": `dpm-cli/${CLI_VERSION}` },
|
|
106
|
+
redirect: "follow",
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok || !res.body) {
|
|
109
|
+
throw new Error(`Download failed (${res.status}): ${url}`);
|
|
110
|
+
}
|
|
111
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
112
|
+
const tmp = `${dest}.download`;
|
|
113
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(tmp));
|
|
114
|
+
await rename(tmp, dest);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Replaces `%LOCALAPPDATA%\\Programs\\DPM\\dpm.exe` from the registry site's
|
|
118
|
+
* `/download/dpm.exe` (see latest.json). Used for `dpm i -g dpm` on Windows exe installs.
|
|
119
|
+
*/
|
|
120
|
+
export async function updateWindowsExe(ui) {
|
|
121
|
+
const installDir = windowsExeInstallDir();
|
|
122
|
+
const exe = windowsExePath();
|
|
123
|
+
if (!(await exists(exe))) {
|
|
124
|
+
throw new Error("No Windows dpm.exe install found. Run the downloaded dpm.exe installer first.");
|
|
125
|
+
}
|
|
126
|
+
const config = await loadConfig();
|
|
127
|
+
const site = siteBaseFromRegistry(config.registry);
|
|
128
|
+
ui.info(`Checking ${site}/download/latest.json…`);
|
|
129
|
+
const meta = await fetchLatestMeta(site);
|
|
130
|
+
const remoteVer = meta.version || "unknown";
|
|
131
|
+
if (remoteVer === CLI_VERSION) {
|
|
132
|
+
ui.success(`Already up to date (${CLI_VERSION})`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const downloadPath = meta.downloadPath || "/download/dpm.exe";
|
|
136
|
+
const url = `${site}${downloadPath.startsWith("/") ? "" : "/"}${downloadPath}`;
|
|
137
|
+
const staging = path.join(installDir, "dpm.exe.new");
|
|
138
|
+
ui.info(`Downloading DPM ${remoteVer}…`);
|
|
139
|
+
await downloadToFile(url, staging);
|
|
140
|
+
const backup = path.join(installDir, "dpm.exe.old");
|
|
141
|
+
await rm(backup, { force: true });
|
|
142
|
+
try {
|
|
143
|
+
await rename(exe, backup);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Running from another copy — overwrite in place via cmd after exit
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await rename(staging, exe);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Locked: schedule replace
|
|
153
|
+
const bat = path.join(process.env.TEMP || installDir, `dpm-update-${Date.now()}.cmd`);
|
|
154
|
+
await writeFile(bat, [
|
|
155
|
+
"@echo off",
|
|
156
|
+
"timeout /t 2 /nobreak >nul",
|
|
157
|
+
`move /y "${staging}" "${exe}"`,
|
|
158
|
+
`del /f /q "${backup}" 2>nul`,
|
|
159
|
+
`del /f /q "%~f0"`,
|
|
160
|
+
"",
|
|
161
|
+
].join("\r\n"), "utf8");
|
|
162
|
+
const child = spawn("cmd.exe", ["/c", "start", "/b", "", bat], {
|
|
163
|
+
windowsHide: true,
|
|
164
|
+
stdio: "ignore",
|
|
165
|
+
detached: true,
|
|
166
|
+
});
|
|
167
|
+
child.unref();
|
|
168
|
+
ui.success(`DPM ${remoteVer} downloaded — restart the terminal (or wait a second) to use it.`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await rm(backup, { force: true });
|
|
172
|
+
// Drop old embedded CLI cache so next launch unpacks the new bundle.
|
|
173
|
+
await rm(cliCacheRoot(), { recursive: true, force: true });
|
|
174
|
+
ui.success(`Updated Windows DPM ${CLI_VERSION} → ${remoteVer}. Open a new terminal if \`dpm -v\` still shows the old version.`);
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drops-it/dpm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Drops Package Manager CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,20 +17,22 @@
|
|
|
17
17
|
"prepublishOnly": "npm run build"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
20
|
-
"access": "public"
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"commander": "^12.1.0",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"magmastream": "^2.10.4",
|
|
26
|
+
"semver": "^7.8.5",
|
|
27
|
+
"tar": "^7.5.22",
|
|
28
|
+
"yaml": "^2.9.1"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@types/node": "^22.
|
|
30
|
-
"@types/semver": "^7.
|
|
31
|
-
"esbuild": "^0.25.
|
|
32
|
-
"tsx": "^4.
|
|
33
|
-
"typescript": "^5.
|
|
31
|
+
"@types/node": "^22.20.5",
|
|
32
|
+
"@types/semver": "^7.8.0",
|
|
33
|
+
"esbuild": "^0.25.12",
|
|
34
|
+
"tsx": "^4.23.15",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
34
36
|
},
|
|
35
37
|
"keywords": [
|
|
36
38
|
"dpm",
|