@drops-it/dpm 1.0.6 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/install.js CHANGED
@@ -1,63 +1,38 @@
1
- import { createHash } from "node:crypto";
2
- import { execFile, exec } from "node:child_process";
3
- import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
- import { homedir, tmpdir } from "node:os";
1
+ import { access, readdir, readFile, rm, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
5
3
  import path from "node:path";
6
- import { promisify } from "node:util";
7
4
  import * as tar from "tar";
8
- import semver from "semver";
9
5
  import { auditPackages, formatAuditReport, proposeFixes, } from "./audit.js";
10
6
  import { c } from "./colors.js";
11
- import { loadLockfile, readPackageJson, saveLockfile, writePackageJson, } from "./config.js";
7
+ import { loadConfig, loadLockfile, readPackageJson, saveLockfile, writePackageJson, } from "./config.js";
8
+ import { ensureInstallConfig, runtimeInstallEnv, spawnRuntime, } from "./runtime.js";
12
9
  import { createUi } from "./ui.js";
13
- const execFileAsync = promisify(execFile);
14
- const execAsync = promisify(exec);
15
- function quoteNpmArg(value) {
16
- if (!/[ \t"&<>|^]/.test(value))
17
- return value;
18
- return `"${value.replace(/"/g, '\\"')}"`;
19
- }
20
- async function runNpm(args) {
21
- if (process.platform === "win32") {
22
- const cmdline = ["npm", ...args.map(quoteNpmArg)].join(" ");
23
- return execAsync(cmdline, {
24
- windowsHide: true,
25
- maxBuffer: 10 * 1024 * 1024,
26
- });
27
- }
28
- return execFileAsync("npm", args, {
29
- windowsHide: true,
30
- maxBuffer: 10 * 1024 * 1024,
31
- });
32
- }
33
10
  function asUi(log) {
34
11
  if ("status" in log && typeof log.status === "function" && "clearStatus" in log) {
35
12
  return log;
36
13
  }
37
14
  return createUi();
38
15
  }
39
- function integrityOf(buf) {
40
- return `sha512-${createHash("sha512").update(buf).digest("base64")}`;
16
+ function setStatus(log, msg) {
17
+ if (log.status)
18
+ log.status(msg);
19
+ else
20
+ log.info(msg);
41
21
  }
42
- async function extractToNodeModules(name, tarball, cwd) {
43
- const target = path.join(cwd, "node_modules", ...name.split("/"));
44
- await rm(target, { recursive: true, force: true });
45
- await mkdir(target, { recursive: true });
46
- const tmp = path.join(cwd, "node_modules", `.dpm-tmp-${Date.now()}-${Math.random().toString(16).slice(2)}.tgz`);
47
- await writeFile(tmp, tarball);
48
- await tar.x({ file: tmp, cwd: target, strip: 1 });
49
- await rm(tmp, { force: true });
22
+ function topLevelDeps(pkg) {
23
+ return {
24
+ ...(pkg.dependencies || {}),
25
+ ...(pkg.devDependencies || {}),
26
+ };
50
27
  }
51
- function pickVersion(versions, range, distTags, preferred) {
52
- // Audit-fix preferred versions always win when present in the registry.
53
- if (preferred && versions.includes(preferred))
54
- return preferred;
55
- if (range === "latest" || range === "*" || range === "") {
56
- return distTags.latest ?? semver.maxSatisfying(versions, "*");
28
+ function readOverrides(pkg) {
29
+ const raw = pkg.overrides || {};
30
+ const out = {};
31
+ for (const [name, value] of Object.entries(raw)) {
32
+ if (typeof value === "string")
33
+ out[name] = value;
57
34
  }
58
- if (semver.valid(range))
59
- return versions.includes(range) ? range : null;
60
- return semver.maxSatisfying(versions, range);
35
+ return out;
61
36
  }
62
37
  /** Parse `name`, `name@1.2.3`, `name@^1`, `@scope/name`, `@scope/name@1.0.0`. */
63
38
  export function parsePackageSpec(spec) {
@@ -89,196 +64,179 @@ async function ensurePackageJson(cwd, log) {
89
64
  return pkg;
90
65
  }
91
66
  }
92
- async function resolveRange(api, name, range) {
93
- const meta = await api.getPackage(name);
94
- if (!meta)
95
- throw new Error(`Package not found: ${name}`);
96
- if (range === "latest" || range === "*") {
97
- const latest = meta.latest || meta["dist-tags"]?.latest;
98
- if (!latest)
99
- throw new Error(`No versions for ${name}`);
100
- return `^${latest}`;
67
+ /**
68
+ * Builds a DPM lock snapshot from installed node_modules for audit tooling.
69
+ */
70
+ export async function syncLockFromNodeModules(cwd) {
71
+ const lock = { lockfileVersion: 1, packages: {} };
72
+ const root = path.join(cwd, "node_modules");
73
+ try {
74
+ await access(root);
101
75
  }
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;
76
+ catch {
77
+ await saveLockfile(lock, cwd);
78
+ return lock;
124
79
  }
125
- return out;
80
+ async function visit(dir, scope) {
81
+ let entries;
82
+ try {
83
+ entries = await readdir(dir);
84
+ }
85
+ catch {
86
+ return;
87
+ }
88
+ for (const entry of entries) {
89
+ if (entry === ".bin" || entry.startsWith("."))
90
+ continue;
91
+ const full = path.join(dir, entry);
92
+ let st;
93
+ try {
94
+ st = await stat(full);
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ if (!st.isDirectory())
100
+ continue;
101
+ if (entry.startsWith("@") && !scope) {
102
+ await visit(full, entry);
103
+ continue;
104
+ }
105
+ const name = scope ? `${scope}/${entry}` : entry;
106
+ try {
107
+ const raw = await readFile(path.join(full, "package.json"), "utf8");
108
+ const meta = JSON.parse(raw);
109
+ lock.packages[name] = {
110
+ version: meta.version || "0.0.0",
111
+ resolved: name,
112
+ integrity: "",
113
+ source: "dpm",
114
+ dependencies: meta.dependencies,
115
+ };
116
+ }
117
+ catch {
118
+ /* skip broken packages */
119
+ }
120
+ }
121
+ }
122
+ await visit(root);
123
+ await saveLockfile(lock, cwd);
124
+ return lock;
125
+ }
126
+ async function prepareRuntimeInstall(cwd) {
127
+ const config = await loadConfig();
128
+ await ensureInstallConfig(config, cwd);
129
+ return { config, env: runtimeInstallEnv(config) };
126
130
  }
127
- export async function installAll(api, cwd = process.cwd(), log = createUi(), options = {}) {
131
+ /** Installs package.json dependencies via the managed DPM runtime. */
132
+ export async function installAll(_api, cwd = process.cwd(), log = createUi(), options = {}) {
128
133
  const ui = asUi(log);
129
- const pkg = await ensurePackageJson(cwd, ui);
130
- const deps = topLevelDeps(pkg);
131
- const entries = Object.entries(deps);
132
- if (entries.length === 0) {
133
- ui.warn("package.json has no dependencies");
134
- await saveLockfile({ lockfileVersion: 1, packages: {} }, cwd);
135
- return { lockfileVersion: 1, packages: {} };
136
- }
137
- ui.info(`${c.cyan("→")} resolving ${c.bold(String(entries.length))} top-level dependenc${entries.length === 1 ? "y" : "ies"}`);
138
- const lock = { lockfileVersion: 1, packages: {} };
139
- const queue = [...entries];
140
- let installed = 0;
141
- const started = Date.now();
142
- const preferred = {
143
- ...readOverrides(pkg),
144
- ...(options.preferredVersions || {}),
145
- };
146
- while (queue.length) {
147
- const [name, range] = queue.pop();
148
- if (lock.packages[name])
149
- continue;
150
- const progress = () => {
151
- const pending = queue.length + 1;
152
- return `${c.dim(`${installed} done · ${pending} left`)}`;
134
+ await ensurePackageJson(cwd, ui);
135
+ if (options.preferredVersions && Object.keys(options.preferredVersions).length > 0) {
136
+ const pkg = await readPackageJson(cwd);
137
+ const overrides = {
138
+ ...readOverrides(pkg),
139
+ ...options.preferredVersions,
153
140
  };
154
- setStatus(ui, `resolve ${c.bold(name)}@${range} ${progress()}`);
155
- const meta = await api.getPackage(name);
156
- if (!meta)
157
- throw new Error(`Package not found: ${name}`);
158
- const versions = meta.versions.map((v) => v.version);
159
- const version = pickVersion(versions, range, meta["dist-tags"] || {}, preferred[name]);
160
- if (!version)
161
- throw new Error(`No version of ${name} matches ${range}`);
162
- const ver = meta.versions.find((v) => v.version === version);
163
- const source = meta.source || "dpm";
164
- const sourceTag = source === "npm" ? c.magenta("[npm]") : c.blue("[dpm]");
165
- setStatus(ui, `download ${c.bold(name)}@${c.green(version)} ${sourceTag} ${progress()}`);
166
- const tarball = await api.downloadTarball(ver.dist.tarball);
167
- const integrity = ver.dist.integrity || integrityOf(tarball);
168
- setStatus(ui, `extract ${c.bold(name)}@${c.green(version)} ${progress()}`);
169
- await extractToNodeModules(name, tarball, cwd);
170
- installed += 1;
171
- lock.packages[name] = {
172
- version,
173
- resolved: ver.dist.tarball,
174
- integrity,
175
- source,
176
- dependencies: ver.dependencies,
177
- };
178
- for (const [dep, depRange] of Object.entries(ver.dependencies || {})) {
179
- if (!lock.packages[dep])
180
- queue.push([dep, depRange]);
181
- }
141
+ pkg.overrides = overrides;
142
+ await writePackageJson(pkg, cwd);
182
143
  }
144
+ const { env } = await prepareRuntimeInstall(cwd);
145
+ setStatus(ui, "installing dependencies…");
146
+ const started = Date.now();
147
+ const code = await spawnRuntime({
148
+ cwd,
149
+ args: ["install"],
150
+ env,
151
+ brandOutput: true,
152
+ });
183
153
  ui.clearStatus();
184
- await saveLockfile(lock, cwd);
154
+ if (code !== 0) {
155
+ throw new Error(`install failed (exit ${code})`);
156
+ }
157
+ const lock = await syncLockFromNodeModules(cwd);
185
158
  const elapsed = ((Date.now() - started) / 1000).toFixed(1);
159
+ const count = Object.keys(lock.packages).length;
186
160
  if (ui.success) {
187
- ui.success(`added ${c.bold(String(installed))} package${installed === 1 ? "" : "s"} in ${elapsed}s`);
161
+ ui.success(`added ${c.bold(String(count))} package${count === 1 ? "" : "s"} in ${elapsed}s`);
188
162
  }
189
163
  else {
190
- ui.info(`added ${installed} package${installed === 1 ? "" : "s"} in ${elapsed}s`);
164
+ ui.info(`added ${count} package${count === 1 ? "" : "s"} in ${elapsed}s`);
191
165
  }
192
166
  if (!options.skipAudit) {
193
167
  setStatus(ui, "checking vulnerabilities…");
194
168
  const hits = await auditPackages(lock.packages);
169
+ const pkg = await readPackageJson(cwd);
170
+ const deps = topLevelDeps(pkg);
195
171
  const fixes = hits.length
196
- ? await proposeFixes(api, lock, hits, deps, true)
172
+ ? await proposeFixes(_api, lock, hits, deps, true)
197
173
  : [];
198
174
  ui.clearStatus();
199
175
  ui.info(formatAuditReport(hits, fixes));
200
176
  }
201
177
  return lock;
202
178
  }
179
+ /** Adds packages then installs via the managed DPM runtime. */
203
180
  export async function addPackages(api, specs, cwd = process.cwd(), log = createUi()) {
204
181
  if (specs.length === 0)
205
182
  return installAll(api, cwd, log);
206
183
  const ui = asUi(log);
207
- const pkg = await ensurePackageJson(cwd, ui);
208
- const deps = pkg.dependencies || {};
184
+ await ensurePackageJson(cwd, ui);
185
+ const { env } = await prepareRuntimeInstall(cwd);
186
+ setStatus(ui, `add ${specs.join(", ")}`);
187
+ const code = await spawnRuntime({
188
+ cwd,
189
+ args: ["add", ...specs],
190
+ env,
191
+ brandOutput: true,
192
+ });
193
+ ui.clearStatus();
194
+ if (code !== 0)
195
+ throw new Error(`add failed (exit ${code})`);
209
196
  for (const spec of specs) {
210
- const { name, range } = parsePackageSpec(spec);
211
- setStatus(ui, `add ${c.bold(name)}`);
212
- const savedRange = await resolveRange(api, name, range);
213
- deps[name] = savedRange;
214
- ui.info(`${c.green("+")} ${c.bold(name)}@${c.cyan(savedRange)}`);
197
+ const { name } = parsePackageSpec(spec);
198
+ ui.info(`${c.green("+")} ${c.bold(name)}`);
215
199
  }
216
- pkg.dependencies = deps;
217
- await writePackageJson(pkg, cwd);
218
- return installAll(api, cwd, ui);
200
+ return syncLockFromNodeModules(cwd);
219
201
  }
220
202
  /**
221
- * Installs packages into the npm global prefix (bins linked by npm).
222
- * Downloads through the DPM registry so installs count as DPM downloads.
203
+ * Installs packages into the global prefix via the managed DPM runtime.
223
204
  * Used for CLI self-updates: `dpm i -g dpm` / `dpm i -g @drops-it/dpm`.
224
205
  */
225
- export async function installGlobalPackages(api, specs, log = createUi()) {
206
+ export async function installGlobalPackages(_api, specs, log = createUi()) {
226
207
  if (specs.length === 0) {
227
208
  throw new Error("global install requires at least one package name");
228
209
  }
229
210
  const ui = asUi(log);
230
- for (const spec of specs) {
231
- let { name, range } = parsePackageSpec(spec);
232
- // CLI lives on DPM as unscoped `dpm` — map the npm name away.
233
- if (name === "@drops-it/dpm")
234
- name = "dpm";
235
- setStatus(ui, `resolve ${c.bold(name)}`);
236
- const meta = await api.getPackage(name);
237
- if (!meta)
238
- throw new Error(`Package not found: ${name}`);
239
- const versions = meta.versions.map((v) => v.version);
240
- const version = pickVersion(versions, range, meta["dist-tags"] || {});
241
- if (!version)
242
- throw new Error(`No version of ${name} matches ${range}`);
243
- const ver = meta.versions.find((v) => v.version === version);
244
- // Prefer the DPM tarball URL so the registry records the download.
245
- const tarballUrl = ver.dist.tarball.includes("/dpm/v1/packages/")
246
- ? ver.dist.tarball
247
- : `/dpm/v1/packages/${encodeURIComponent(name)}/-/${encodeURIComponent(name)}-${version}.tgz`;
248
- setStatus(ui, `download ${c.bold(name)}@${c.green(version)}`);
249
- const tarball = await api.downloadTarball(tarballUrl);
250
- const tmp = path.join(tmpdir(), `dpm-global-${Date.now()}-${Math.random().toString(16).slice(2)}.tgz`);
251
- await writeFile(tmp, tarball);
252
- try {
253
- setStatus(ui, `install ${c.bold(name)}@${c.green(version)} ${c.dim("[global]")}`);
254
- await runNpm(["install", "-g", tmp]);
255
- }
256
- finally {
257
- await rm(tmp, { force: true });
258
- }
259
- ui.clearStatus();
260
- if (ui.success)
261
- ui.success(`installed ${c.bold(name)}@${c.green(version)} globally`);
262
- else
263
- ui.info(`installed ${name}@${version} globally`);
264
- }
265
- }
266
- async function npmGlobalPrefix() {
267
- const { stdout } = await runNpm(["prefix", "-g"]);
268
- return stdout.toString().trim();
211
+ const mapped = specs.map((spec) => {
212
+ const { name, range } = parsePackageSpec(spec);
213
+ const real = name === "@drops-it/dpm" ? "dpm" : name;
214
+ return range === "latest" ? real : `${real}@${range}`;
215
+ });
216
+ const config = await loadConfig();
217
+ await ensureInstallConfig(config, process.cwd());
218
+ const env = runtimeInstallEnv(config);
219
+ setStatus(ui, `install -g ${mapped.join(", ")}`);
220
+ const code = await spawnRuntime({
221
+ args: ["add", "-g", ...mapped],
222
+ env,
223
+ brandOutput: true,
224
+ });
225
+ ui.clearStatus();
226
+ if (code !== 0)
227
+ throw new Error(`global install failed (exit ${code})`);
228
+ if (ui.success)
229
+ ui.success(`installed ${mapped.join(", ")} globally`);
230
+ else
231
+ ui.info(`installed ${mapped.join(", ")} globally`);
269
232
  }
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
- }
233
+ const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
234
+ async function runtimeGlobalPrefix() {
235
+ // Best-effort: common user global paths when removing the CLI.
236
+ if (process.platform === "win32") {
237
+ return path.join(homedir(), ".dpm", "global");
281
238
  }
239
+ return path.join(homedir(), ".dpm", "global");
282
240
  }
283
241
  /** Removes leftover Windows/Unix bin shims for a command name. */
284
242
  async function removeGlobalBinShims(binName, prefix) {
@@ -286,13 +244,13 @@ async function removeGlobalBinShims(binName, prefix) {
286
244
  path.join(prefix, binName),
287
245
  path.join(prefix, `${binName}.cmd`),
288
246
  path.join(prefix, `${binName}.ps1`),
247
+ path.join(prefix, "bin", binName),
289
248
  path.join(prefix, "node_modules", ".bin", binName),
290
249
  path.join(prefix, "node_modules", ".bin", `${binName}.cmd`),
291
250
  path.join(prefix, "node_modules", ".bin", `${binName}.ps1`),
292
251
  ];
293
252
  await Promise.all(candidates.map((p) => rm(p, { force: true })));
294
253
  }
295
- const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
296
254
  /**
297
255
  * Uninstalls global packages (and cleans dpm bin shims when removing the CLI).
298
256
  * `dpm rm -g` with no names removes the dpm CLI itself.
@@ -300,31 +258,29 @@ const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
300
258
  export async function removeGlobalPackages(specs, log = createUi()) {
301
259
  const ui = asUi(log);
302
260
  const targets = specs.length > 0
303
- ? specs.map((s) => parsePackageSpec(s).name)
261
+ ? specs.map((s) => parsePackageSpec(s).name).map((n) => (n === "@drops-it/dpm" ? "dpm" : n))
304
262
  : [...CLI_SELF_NAMES];
305
- const prefix = await npmGlobalPrefix();
306
- const removingSelf = targets.some((n) => CLI_SELF_NAMES.includes(n));
263
+ const config = await loadConfig();
264
+ const env = runtimeInstallEnv(config);
265
+ const removingSelf = targets.some((n) => CLI_SELF_NAMES.includes(n) || n === "dpm");
307
266
  for (const name of targets) {
308
267
  setStatus(ui, `uninstall -g ${c.bold(name)}`);
309
- await tryNpmUninstallGlobal(name);
310
- // Scoped packages may leave the unscoped alias around.
311
- if (name === "@drops-it/dpm")
312
- await tryNpmUninstallGlobal("dpm");
313
- if (name === "dpm")
314
- await tryNpmUninstallGlobal("@drops-it/dpm");
268
+ await spawnRuntime({
269
+ args: ["remove", "-g", name],
270
+ env,
271
+ brandOutput: true,
272
+ });
315
273
  }
316
274
  if (removingSelf) {
275
+ const prefix = await runtimeGlobalPrefix();
317
276
  await removeGlobalBinShims("dpm", prefix);
318
- // Also clear a linked/local leftover under the user npm folder on Windows.
319
277
  const roaming = path.join(homedir(), "AppData", "Roaming", "npm");
320
- if (roaming.toLowerCase() !== prefix.toLowerCase()) {
321
- await removeGlobalBinShims("dpm", roaming);
322
- }
278
+ await removeGlobalBinShims("dpm", roaming);
323
279
  await rm(path.join(prefix, "node_modules", "dpm"), { recursive: true, force: true });
324
- await rm(path.join(prefix, "node_modules", "@drops-it", "dpm"), {
280
+ await rm(path.join(homedir(), ".bun", "install", "global", "node_modules", "dpm"), {
325
281
  recursive: true,
326
282
  force: true,
327
- });
283
+ }).catch(() => undefined);
328
284
  }
329
285
  ui.clearStatus();
330
286
  if (ui.success) {
@@ -340,26 +296,21 @@ export async function addPackage(api, name, range = "latest", cwd = process.cwd(
340
296
  const spec = range === "latest" ? name : `${name}@${range}`;
341
297
  return addPackages(api, [spec], cwd, log);
342
298
  }
299
+ /** Removes a local dependency via the managed DPM runtime. */
343
300
  export async function removePackage(name, cwd = process.cwd(), log = createUi()) {
344
301
  const ui = asUi(log);
345
- const pkg = await readPackageJson(cwd);
346
- const deps = pkg.dependencies || {};
347
- const dev = pkg.devDependencies || {};
348
- if (!deps[name] && !dev[name]) {
349
- ui.warn(`${name} is not in package.json`);
350
- }
351
- delete deps[name];
352
- delete dev[name];
353
- pkg.dependencies = deps;
354
- pkg.devDependencies = dev;
355
- await writePackageJson(pkg, cwd);
356
- await rm(path.join(cwd, "node_modules", ...name.split("/")), {
357
- recursive: true,
358
- force: true,
302
+ const { env } = await prepareRuntimeInstall(cwd);
303
+ setStatus(ui, `remove ${c.bold(name)}`);
304
+ const code = await spawnRuntime({
305
+ cwd,
306
+ args: ["remove", name],
307
+ env,
308
+ brandOutput: true,
359
309
  });
360
- const lock = await loadLockfile(cwd);
361
- delete lock.packages[name];
362
- await saveLockfile(lock, cwd);
310
+ ui.clearStatus();
311
+ if (code !== 0)
312
+ throw new Error(`remove failed (exit ${code})`);
313
+ await syncLockFromNodeModules(cwd);
363
314
  if (ui.success)
364
315
  ui.success(`removed ${c.bold(name)}`);
365
316
  else
@@ -384,7 +335,7 @@ export async function runAudit(api, cwd = process.cwd(), log = createUi()) {
384
335
  return { hits, fixes: fixes.filter((f) => !f.breaking) };
385
336
  }
386
337
  /**
387
- * Applies available advisory fixes (like `npm audit fix`), then reinstalls.
338
+ * Applies available advisory fixes, then reinstalls via the DPM runtime.
388
339
  */
389
340
  export async function auditFix(api, cwd = process.cwd(), log = createUi(), force = false) {
390
341
  const ui = asUi(log);
@@ -418,7 +369,6 @@ export async function auditFix(api, cwd = process.cwd(), log = createUi(), force
418
369
  devDependencies[fix.name] = `^${fix.to}`;
419
370
  }
420
371
  else {
421
- // Persist transitive fixes so the next plain install keeps them.
422
372
  overrides[fix.name] = fix.to;
423
373
  }
424
374
  }
@@ -433,8 +383,26 @@ export async function auditFix(api, cwd = process.cwd(), log = createUi(), force
433
383
  });
434
384
  return { fixed: applicable, lock: next };
435
385
  }
386
+ async function readLocalReadme(cwd) {
387
+ for (const name of ["README.md", "readme.md", "README", "readme.txt", "README.txt"]) {
388
+ try {
389
+ const text = await readFile(path.join(cwd, name), "utf8");
390
+ if (text.trim())
391
+ return text;
392
+ }
393
+ catch {
394
+ /* try next */
395
+ }
396
+ }
397
+ return "";
398
+ }
399
+ /** Packs the current package into a gzipped tarball for publish. */
436
400
  export async function packTarball(cwd = process.cwd()) {
437
401
  const manifest = await readPackageJson(cwd);
402
+ const readme = await readLocalReadme(cwd);
403
+ if (readme && !String(manifest.readme ?? "").trim()) {
404
+ manifest.readme = readme;
405
+ }
438
406
  const out = path.join(cwd, `.dpm-pack-${Date.now()}.tgz`);
439
407
  await tar.c({
440
408
  gzip: true,
@@ -444,7 +412,10 @@ export async function packTarball(cwd = process.cwd()) {
444
412
  filter: (p) => !p.includes("node_modules") &&
445
413
  !p.startsWith(".git") &&
446
414
  !p.endsWith(".tgz") &&
447
- p !== "dpm-lock.yaml",
415
+ p !== "dpm-lock.yaml" &&
416
+ p !== "bun.lock" &&
417
+ p !== "bun.lockb" &&
418
+ p !== "bunfig.toml",
448
419
  }, ["."]);
449
420
  const buf = await readFile(out);
450
421
  await rm(out, { force: true });
@@ -0,0 +1,37 @@
1
+ import type { DpmConfig } from "./config.js";
2
+ /** Pinned managed runtime version (oven-sh/bun release tag without prefix). */
3
+ export declare const RUNTIME_VERSION = "1.2.19";
4
+ export declare function managedRuntimePath(): string;
5
+ /**
6
+ * Ensures the managed DPM runtime binary exists at a stable path.
7
+ * Downloads a pinned release from GitHub when missing or version-mismatched.
8
+ */
9
+ export declare function ensureRuntime(): Promise<string>;
10
+ /**
11
+ * Resolves a runnable runtime path: managed binary, else PATH fallback branded as DPM.
12
+ */
13
+ export declare function resolveRuntime(): Promise<string>;
14
+ export type SpawnRuntimeOptions = {
15
+ cwd?: string;
16
+ env?: NodeJS.ProcessEnv;
17
+ /** Extra args after the runtime binary (e.g. ["install"], ["run", "start"]). */
18
+ args: string[];
19
+ /** When true, rewrite stderr banners that say bun → dpm. */
20
+ brandOutput?: boolean;
21
+ };
22
+ /**
23
+ * Spawns the managed runtime with inherited stdio and returns the exit code.
24
+ */
25
+ export declare function spawnRuntime(opts: SpawnRuntimeOptions): Promise<number>;
26
+ /**
27
+ * Resolves the install registry URL.
28
+ * DPM hosts use the `/npm` facade; plain npmjs (or DPM_BOOTSTRAP=1) uses the URL as-is.
29
+ */
30
+ export declare function resolveInstallRegistryUrl(registry: string): string;
31
+ /**
32
+ * Writes project-local registry config so the runtime installer hits the DPM npm facade
33
+ * (or npmjs when bootstrapping / pointed at registry.npmjs.org).
34
+ */
35
+ export declare function ensureInstallConfig(config: DpmConfig, cwd?: string): Promise<void>;
36
+ /** Builds npm_config_* env for a one-shot install without relying only on files. */
37
+ export declare function runtimeInstallEnv(config: DpmConfig): NodeJS.ProcessEnv;