@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 CHANGED
@@ -14,9 +14,16 @@ export async function loadConfig() {
14
14
  registry: "https://dpm.drops-it.com",
15
15
  };
16
16
  }
17
- // Env wins (Pterodactyl bootstrap sets DPM_REGISTRY=npmjs).
18
- if (process.env.DPM_REGISTRY)
19
- config.registry = process.env.DPM_REGISTRY;
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
- * Used for CLI self-updates: `dpm i -g dpm` / `dpm i -g @drops-it/dpm`.
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
- /** Removes a local dependency via the managed DPM runtime. */
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
- setStatus(ui, "installing dependencies…");
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
- setStatus(ui, `add ${specs.join(", ")}`);
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
- * Used for CLI self-updates: `dpm i -g dpm` / `dpm i -g @drops-it/dpm`.
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 mapped = specs.map((spec) => {
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 range === "latest" ? real : `${real}@${range}`;
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
- setStatus(ui, `install -g ${mapped.join(", ")}`);
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
- ? specs.map((s) => parsePackageSpec(s).name).map((n) => (n === "@drops-it/dpm" ? "dpm" : n))
262
- : [...CLI_SELF_NAMES];
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((n) => CLI_SELF_NAMES.includes(n) || n === "dpm");
266
- for (const name of targets) {
267
- setStatus(ui, `uninstall -g ${c.bold(name)}`);
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
- const prefix = await runtimeGlobalPrefix();
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 (ui.success) {
287
- ui.success(removingSelf && specs.length === 0
288
- ? "removed global dpm (bins + package)"
289
- : `removed global ${targets.join(", ")}`);
338
+ if (didWindowsUninstall && otherTargets.length === 0) {
339
+ // uninstallWindowsExe already reported success
340
+ return;
290
341
  }
291
- else {
292
- 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;
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
- /** Removes a local dependency via the managed DPM runtime. */
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
- setStatus(ui, `remove ${c.bold(name)}`);
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
- ui.clearStatus();
311
- if (code !== 0)
312
- throw new Error(`remove failed (exit ${code})`);
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 banners that say bun → dpm. */
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; plain npmjs (or DPM_BOOTSTRAP=1) uses the URL as-is.
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 npmjs when bootstrapping / pointed at registry.npmjs.org).
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
- function brandStderr(chunk) {
159
- return Buffer.from(chunk
160
- .toString("utf8")
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: opts.brandOutput ? ["inherit", "inherit", "pipe"] : "inherit",
184
+ stdio: brand ? ["inherit", "pipe", "pipe"] : "inherit",
180
185
  windowsHide: true,
181
186
  });
182
- if (opts.brandOutput && child.stderr) {
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.write(brandStderr(chunk));
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; plain npmjs (or DPM_BOOTSTRAP=1) uses the URL as-is.
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 bootstrap = process.env.DPM_BOOTSTRAP === "1" ||
198
- /registry\.npmjs\.org/i.test(base) ||
199
- process.env.DPM_USE_NPM_UPSTREAM === "1";
200
- if (bootstrap)
201
- return `${base}/`;
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 npmjs when bootstrapping / pointed at registry.npmjs.org).
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=${config.token ?? ""}`,
256
+ `//${host}${pathPrefix}/:_authToken=${token}`,
215
257
  ];
216
- await writeFile(path.join(cwd, ".npmrc"), `${npmrcLines.join("\n")}\n`, { mode: 0o600 });
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
- ...(config.token
272
+ ...(useToken
228
273
  ? {
229
274
  npm_config__authToken: config.token,
230
275
  BUN_CONFIG_TOKEN: config.token,
@@ -1,7 +1,7 @@
1
- export declare const CLI_NAME: string;
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 how to update with dpm. Never blocks on failure.
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>;
@@ -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
- const require = createRequire(import.meta.url);
9
- const pkg = require("../package.json");
10
- export const CLI_NAME = pkg.name;
11
- export const CLI_VERSION = pkg.version;
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 = JSON.parse(await readFile(STATE_PATH, "utf8"));
26
- if (Array.isArray(raw.sessions)) {
27
- return { sessions: raw.sessions.slice(-40), latest: raw.latest };
28
- }
29
- return { sessions: [], latest: raw.latest };
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({ sessions: state.sessions.slice(-40), latest: state.latest }), { mode: 0o600 });
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: { accept: "application/json", "user-agent": `dpm-cli/${CLI_VERSION}` },
43
- signal: AbortSignal.timeout(2500),
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 data = (await res.json());
48
- return data.latest || data["dist-tags"]?.latest || null;
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 how to update with dpm. Never blocks on failure.
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
- // Ignore network / parse errors — update checks must never break commands.
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.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
- "semver": "^7.6.3",
25
- "tar": "^7.4.3",
26
- "yaml": "^2.7.0"
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.10.5",
30
- "@types/semver": "^7.5.8",
31
- "esbuild": "^0.25.0",
32
- "tsx": "^4.19.2",
33
- "typescript": "^5.7.3"
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",