@drops-it/dpm 1.0.1 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # dpm
2
+
3
+ **Drops Package Manager** — a fast, privacy-minded CLI for installing, publishing, and auditing Node packages on [dpm.drops-it.com](https://dpm.drops-it.com), with automatic npm fallback for anything not on the registry.
4
+
5
+ ```text
6
+ ┌─────────────────────────────────────────┐
7
+ │ dpm i lodash │
8
+ │ → resolving 1 top-level dependency │
9
+ │ ✔ added 1 package in 0.4s │
10
+ │ found 0 vulnerabilities │
11
+ └─────────────────────────────────────────┘
12
+ ```
13
+
14
+ Requires **Node.js 22+**.
15
+
16
+ ---
17
+
18
+ ## Install
19
+
20
+ **From DPM (recommended)**
21
+
22
+ ```bash
23
+ # after you already have any dpm binary once:
24
+ dpm i -g dpm
25
+ ```
26
+
27
+ **From npm**
28
+
29
+ ```bash
30
+ npm i -g @drops-it/dpm
31
+ ```
32
+
33
+ **Windows launcher**
34
+
35
+ Download [`dpm.exe`](https://dpm.drops-it.com/download/dpm.exe) — it boots the CLI if Node is installed.
36
+
37
+ Verify:
38
+
39
+ ```bash
40
+ dpm --version
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Quick start
46
+
47
+ ```bash
48
+ dpm login # browser device flow (Drops-IT)
49
+ dpm init # create package.json
50
+ dpm i lodash # add + install
51
+ dpm i # install from package.json
52
+ dpm publish --access public
53
+ ```
54
+
55
+ Config lives in `~/.dpmrc` (registry + token). Defaults to `https://dpm.drops-it.com`.
56
+
57
+ ```bash
58
+ dpm config registry https://dpm.drops-it.com
59
+ dpm whoami
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Commands
65
+
66
+ | Command | What it does |
67
+ | --- | --- |
68
+ | `dpm login` / `logout` / `whoami` | Auth via Drops-IT |
69
+ | `dpm init` | Scaffold `package.json` |
70
+ | `dpm i` / `dpm install` | Install deps (or add packages then install) |
71
+ | `dpm i -g <pkg>` | Global install (counts as a DPM download) |
72
+ | `dpm add <pkg…>` | Add to `package.json` and install |
73
+ | `dpm rm` / `remove` / `uninstall` | Remove a local dependency |
74
+ | `dpm rm -g` | Remove the global **dpm** CLI + bin shims |
75
+ | `dpm rm -g <pkg>` | Uninstall a global package |
76
+ | `dpm update` | Re-resolve and reinstall |
77
+ | `dpm search <q>` | Search the registry |
78
+ | `dpm view <name>` | Show package metadata |
79
+ | `dpm publish [--access public\|private]` | Publish the current package |
80
+ | `dpm audit` | Report known vulnerabilities |
81
+ | `dpm audit fix` | Apply non-breaking fixes |
82
+ | `dpm audit fix --force` | Allow breaking upgrades |
83
+ | `dpm config [key] [value]` | Read/write CLI config |
84
+
85
+ ---
86
+
87
+ ## Install experience
88
+
89
+ - One live status line with a spinner while packages resolve / download / extract
90
+ - Colorized success, warnings, and severity
91
+ - Post-install **advisory report** (npm-compatible bulk advisories)
92
+ - Fix hints: `dpm audit fix` / `dpm audit fix --force`
93
+ - Transitive fixes can land in `package.json` `overrides` so they stick
94
+
95
+ ```bash
96
+ dpm i lodash@4.17.20
97
+ dpm audit
98
+ dpm audit fix
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Updates
104
+
105
+ On the **first `dpm` command in a new terminal**, dpm checks the DPM registry for a newer CLI.
106
+
107
+ ```text
108
+ ▲ dpm update available: 1.0.3 → 1.0.4
109
+ Update with: dpm i -g dpm
110
+ ```
111
+
112
+ No daily nag. No npm noise — updates are driven by the DPM package `dpm`.
113
+
114
+ ---
115
+
116
+ ## Registry behavior
117
+
118
+ 1. Resolve on **DPM** first (`dpm` packages you publish, public or private).
119
+ 2. Missing public packages fall through to **npm** and are cached by the registry.
120
+ 3. Private scoped packages are free on DPM — use your `@username` or org scope after login.
121
+
122
+ ```bash
123
+ dpm publish --access private # scoped private packages
124
+ dpm publish --access public
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Global CLI lifecycle
130
+
131
+ ```bash
132
+ dpm i -g dpm # install / update the CLI from DPM
133
+ dpm rm -g # remove dpm + Windows/Unix shims
134
+ ```
135
+
136
+ `dpm i -g @drops-it/dpm` is accepted and mapped to the DPM package `dpm`.
137
+
138
+ ---
139
+
140
+ ## Development
141
+
142
+ ```bash
143
+ cd cli
144
+ npm install
145
+ npm run build
146
+ npm link # puts `dpm` on your PATH from this checkout
147
+ npm run dev -- --help
148
+ ```
149
+
150
+ Windows native binary:
151
+
152
+ ```bash
153
+ npm run build:windows --prefix cli
154
+ # optional Authenticode:
155
+ # npm run sign:windows --prefix cli
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Links
161
+
162
+ - Website / registry: [dpm.drops-it.com](https://dpm.drops-it.com)
163
+ - npm package: [`@drops-it/dpm`](https://www.npmjs.com/package/@drops-it/dpm)
164
+ - DPM package: `dpm`
165
+
166
+ ---
167
+
168
+ ## License
169
+
170
+ MIT © Drops-IT
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { exec } from "node:child_process";
3
3
  import { Command } from "commander";
4
4
  import { DpmApi } from "./api.js";
5
5
  import { loadConfig, saveConfig, writePackageJson } from "./config.js";
6
- import { addPackages, auditFix, installAll, installGlobalPackages, packTarball, removePackage, runAudit, } from "./install.js";
6
+ import { addPackages, auditFix, installAll, installGlobalPackages, packTarball, removeGlobalPackages, removePackage, runAudit, } from "./install.js";
7
7
  import { createUi } from "./ui.js";
8
8
  import { CLI_VERSION, maybeNotifyUpdate } from "./updateCheck.js";
9
9
  const program = new Command();
@@ -158,9 +158,23 @@ program
158
158
  program
159
159
  .command("remove")
160
160
  .alias("rm")
161
- .argument("<name>")
162
- .action(async (name) => {
163
- await removePackage(name, process.cwd(), createUi());
161
+ .alias("uninstall")
162
+ .argument("[packages...]", "package to remove (omit with -g to remove the dpm CLI)")
163
+ .option("-g, --global", "remove from the npm global prefix")
164
+ .description("Remove a dependency, or uninstall the global dpm CLI")
165
+ .action(async (packages, opts) => {
166
+ const ui = createUi();
167
+ if (opts.global) {
168
+ await removeGlobalPackages(packages, ui);
169
+ return;
170
+ }
171
+ if (packages.length === 0) {
172
+ console.error("Usage: dpm rm <name> or dpm rm -g");
173
+ process.exit(1);
174
+ }
175
+ for (const name of packages) {
176
+ await removePackage(name, process.cwd(), ui);
177
+ }
164
178
  });
165
179
  const auditCmd = program
166
180
  .command("audit")
package/dist/install.d.ts CHANGED
@@ -23,9 +23,15 @@ export declare function installAll(api: DpmApi, cwd?: string, log?: InstallLog,
23
23
  export declare function addPackages(api: DpmApi, specs: string[], cwd?: string, log?: InstallLog): Promise<Lockfile>;
24
24
  /**
25
25
  * Installs packages into the npm global prefix (bins linked by npm).
26
- * Used for CLI self-updates: `dpm i -g @drops-it/dpm`.
26
+ * Downloads through the DPM registry so installs count as DPM downloads.
27
+ * Used for CLI self-updates: `dpm i -g dpm` / `dpm i -g @drops-it/dpm`.
27
28
  */
28
29
  export declare function installGlobalPackages(api: DpmApi, specs: string[], log?: InstallLog): Promise<void>;
30
+ /**
31
+ * Uninstalls global packages (and cleans dpm bin shims when removing the CLI).
32
+ * `dpm rm -g` with no names removes the dpm CLI itself.
33
+ */
34
+ export declare function removeGlobalPackages(specs: string[], log?: InstallLog): Promise<void>;
29
35
  export declare function addPackage(api: DpmApi, name: string, range?: string, cwd?: string, log?: InstallLog): Promise<Lockfile>;
30
36
  export declare function removePackage(name: string, cwd?: string, log?: InstallLog): Promise<void>;
31
37
  /**
package/dist/install.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { execFile } from "node:child_process";
2
+ import { execFile, exec } from "node:child_process";
3
3
  import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
4
5
  import path from "node:path";
5
6
  import { promisify } from "node:util";
6
7
  import * as tar from "tar";
@@ -10,6 +11,25 @@ import { c } from "./colors.js";
10
11
  import { loadLockfile, readPackageJson, saveLockfile, writePackageJson, } from "./config.js";
11
12
  import { createUi } from "./ui.js";
12
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
+ }
13
33
  function asUi(log) {
14
34
  if ("status" in log && typeof log.status === "function" && "clearStatus" in log) {
15
35
  return log;
@@ -199,7 +219,8 @@ export async function addPackages(api, specs, cwd = process.cwd(), log = createU
199
219
  }
200
220
  /**
201
221
  * Installs packages into the npm global prefix (bins linked by npm).
202
- * Used for CLI self-updates: `dpm i -g @drops-it/dpm`.
222
+ * Downloads through the DPM registry so installs count as DPM downloads.
223
+ * Used for CLI self-updates: `dpm i -g dpm` / `dpm i -g @drops-it/dpm`.
203
224
  */
204
225
  export async function installGlobalPackages(api, specs, log = createUi()) {
205
226
  if (specs.length === 0) {
@@ -207,7 +228,10 @@ export async function installGlobalPackages(api, specs, log = createUi()) {
207
228
  }
208
229
  const ui = asUi(log);
209
230
  for (const spec of specs) {
210
- const { name, range } = parsePackageSpec(spec);
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";
211
235
  setStatus(ui, `resolve ${c.bold(name)}`);
212
236
  const meta = await api.getPackage(name);
213
237
  if (!meta)
@@ -217,16 +241,17 @@ export async function installGlobalPackages(api, specs, log = createUi()) {
217
241
  if (!version)
218
242
  throw new Error(`No version of ${name} matches ${range}`);
219
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`;
220
248
  setStatus(ui, `download ${c.bold(name)}@${c.green(version)}`);
221
- const tarball = await api.downloadTarball(ver.dist.tarball);
222
- const tmp = path.join(process.cwd(), `.dpm-global-${Date.now()}-${Math.random().toString(16).slice(2)}.tgz`);
249
+ const tarball = await api.downloadTarball(tarballUrl);
250
+ const tmp = path.join(tmpdir(), `dpm-global-${Date.now()}-${Math.random().toString(16).slice(2)}.tgz`);
223
251
  await writeFile(tmp, tarball);
224
252
  try {
225
253
  setStatus(ui, `npm install -g ${c.bold(name)}@${c.green(version)}`);
226
- await execFileAsync("npm", ["install", "-g", tmp], {
227
- windowsHide: true,
228
- maxBuffer: 10 * 1024 * 1024,
229
- });
254
+ await runNpm(["install", "-g", tmp]);
230
255
  }
231
256
  finally {
232
257
  await rm(tmp, { force: true });
@@ -238,6 +263,79 @@ export async function installGlobalPackages(api, specs, log = createUi()) {
238
263
  ui.info(`installed ${name}@${version} globally`);
239
264
  }
240
265
  }
266
+ async function npmGlobalPrefix() {
267
+ const { stdout } = await runNpm(["prefix", "-g"]);
268
+ return stdout.toString().trim();
269
+ }
270
+ async function tryNpmUninstallGlobal(name) {
271
+ try {
272
+ await runNpm(["uninstall", "-g", name]);
273
+ }
274
+ catch {
275
+ try {
276
+ await runNpm(["unlink", "-g", name]);
277
+ }
278
+ catch {
279
+ // Package may already be gone.
280
+ }
281
+ }
282
+ }
283
+ /** Removes leftover Windows/Unix bin shims for a command name. */
284
+ async function removeGlobalBinShims(binName, prefix) {
285
+ const candidates = [
286
+ path.join(prefix, binName),
287
+ path.join(prefix, `${binName}.cmd`),
288
+ path.join(prefix, `${binName}.ps1`),
289
+ path.join(prefix, "node_modules", ".bin", binName),
290
+ path.join(prefix, "node_modules", ".bin", `${binName}.cmd`),
291
+ path.join(prefix, "node_modules", ".bin", `${binName}.ps1`),
292
+ ];
293
+ await Promise.all(candidates.map((p) => rm(p, { force: true })));
294
+ }
295
+ const CLI_SELF_NAMES = ["@drops-it/dpm", "dpm"];
296
+ /**
297
+ * Uninstalls global packages (and cleans dpm bin shims when removing the CLI).
298
+ * `dpm rm -g` with no names removes the dpm CLI itself.
299
+ */
300
+ export async function removeGlobalPackages(specs, log = createUi()) {
301
+ const ui = asUi(log);
302
+ const targets = specs.length > 0
303
+ ? specs.map((s) => parsePackageSpec(s).name)
304
+ : [...CLI_SELF_NAMES];
305
+ const prefix = await npmGlobalPrefix();
306
+ const removingSelf = targets.some((n) => CLI_SELF_NAMES.includes(n));
307
+ for (const name of targets) {
308
+ setStatus(ui, `uninstall -g ${c.bold(name)}`);
309
+ await tryNpmUninstallGlobal(name);
310
+ // Scoped packages may leave the unscoped alias around.
311
+ if (name === "@drops-it/dpm")
312
+ await tryNpmUninstallGlobal("dpm");
313
+ if (name === "dpm")
314
+ await tryNpmUninstallGlobal("@drops-it/dpm");
315
+ }
316
+ if (removingSelf) {
317
+ await removeGlobalBinShims("dpm", prefix);
318
+ // Also clear a linked/local leftover under the user npm folder on Windows.
319
+ const roaming = path.join(homedir(), "AppData", "Roaming", "npm");
320
+ if (roaming.toLowerCase() !== prefix.toLowerCase()) {
321
+ await removeGlobalBinShims("dpm", roaming);
322
+ }
323
+ await rm(path.join(prefix, "node_modules", "dpm"), { recursive: true, force: true });
324
+ await rm(path.join(prefix, "node_modules", "@drops-it", "dpm"), {
325
+ recursive: true,
326
+ force: true,
327
+ });
328
+ }
329
+ ui.clearStatus();
330
+ if (ui.success) {
331
+ ui.success(removingSelf && specs.length === 0
332
+ ? "removed global dpm (bins + package)"
333
+ : `removed global ${targets.join(", ")}`);
334
+ }
335
+ else {
336
+ ui.info(`removed global ${targets.join(", ")}`);
337
+ }
338
+ }
241
339
  export async function addPackage(api, name, range = "latest", cwd = process.cwd(), log = createUi()) {
242
340
  const spec = range === "latest" ? name : `${name}@${range}`;
243
341
  return addPackages(api, [spec], cwd, log);
@@ -1,7 +1,7 @@
1
1
  export declare const CLI_NAME: string;
2
2
  export declare const CLI_VERSION: string;
3
3
  /**
4
- * On the first dpm command of each day, checks DPM + npm for a newer CLI
5
- * and prints how to update with dpm when one exists. Never blocks on failure.
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.
6
6
  */
7
7
  export declare function maybeNotifyUpdate(): Promise<void>;
@@ -9,31 +9,35 @@ const require = createRequire(import.meta.url);
9
9
  const pkg = require("../package.json");
10
10
  export const CLI_NAME = pkg.name;
11
11
  export const CLI_VERSION = pkg.version;
12
- /** Package names checked for a newer CLI release. */
13
- const UPDATE_CANDIDATES = ["dpm", "@drops-it/dpm"];
14
12
  const STATE_DIR = path.join(homedir(), ".dpm");
15
13
  const STATE_PATH = path.join(STATE_DIR, "update-check.json");
16
- function todayKey() {
17
- const d = new Date();
18
- const y = d.getFullYear();
19
- const m = String(d.getMonth() + 1).padStart(2, "0");
20
- const day = String(d.getDate()).padStart(2, "0");
21
- return `${y}-${m}-${day}`;
14
+ /** Identifies the current terminal so we only notify once per new shell. */
15
+ function sessionKey() {
16
+ const parts = [`ppid:${process.ppid}`];
17
+ if (process.env.WT_SESSION)
18
+ parts.push(`wt:${process.env.WT_SESSION}`);
19
+ if (process.env.TERM_SESSION_ID)
20
+ parts.push(`term:${process.env.TERM_SESSION_ID}`);
21
+ return parts.join("|");
22
22
  }
23
23
  async function loadState() {
24
24
  try {
25
- return JSON.parse(await readFile(STATE_PATH, "utf8"));
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 };
26
30
  }
27
31
  catch {
28
- return null;
32
+ return { sessions: [] };
29
33
  }
30
34
  }
31
35
  async function saveState(state) {
32
36
  await mkdir(STATE_DIR, { recursive: true });
33
- await writeFile(STATE_PATH, JSON.stringify(state), { mode: 0o600 });
37
+ await writeFile(STATE_PATH, JSON.stringify({ sessions: state.sessions.slice(-40), latest: state.latest }), { mode: 0o600 });
34
38
  }
35
- async function fetchLatestFromDpm(name, registry) {
36
- const url = `${registry.replace(/\/$/, "")}/dpm/v1/packages/${encodeURIComponent(name)}`;
39
+ async function fetchLatestFromDpm(registry) {
40
+ const url = `${registry.replace(/\/$/, "")}/dpm/v1/packages/dpm`;
37
41
  const res = await fetch(url, {
38
42
  headers: { accept: "application/json", "user-agent": `dpm-cli/${CLI_VERSION}` },
39
43
  signal: AbortSignal.timeout(2500),
@@ -43,59 +47,31 @@ async function fetchLatestFromDpm(name, registry) {
43
47
  const data = (await res.json());
44
48
  return data.latest || data["dist-tags"]?.latest || null;
45
49
  }
46
- async function fetchLatestFromNpm(name) {
47
- const scopedUrl = name.startsWith("@")
48
- ? `https://registry.npmjs.org/${name.replace("/", "%2F")}`
49
- : `https://registry.npmjs.org/${encodeURIComponent(name)}`;
50
- const res = await fetch(scopedUrl, {
51
- headers: { accept: "application/json", "user-agent": `dpm-cli/${CLI_VERSION}` },
52
- signal: AbortSignal.timeout(2500),
53
- });
54
- if (!res.ok)
55
- return null;
56
- const data = (await res.json());
57
- return data["dist-tags"]?.latest || null;
58
- }
59
- async function resolveNewestAvailable() {
60
- const config = await loadConfig();
61
- const found = [];
62
- await Promise.all(UPDATE_CANDIDATES.map(async (name) => {
63
- const [dpm, npm] = await Promise.all([
64
- fetchLatestFromDpm(name, config.registry).catch(() => null),
65
- fetchLatestFromNpm(name).catch(() => null),
66
- ]);
67
- if (dpm && semver.valid(dpm))
68
- found.push(dpm);
69
- if (npm && semver.valid(npm))
70
- found.push(npm);
71
- }));
72
- if (found.length === 0)
73
- return null;
74
- return found.sort(semver.rcompare)[0] ?? null;
75
- }
76
50
  /**
77
- * On the first dpm command of each day, checks DPM + npm for a newer CLI
78
- * and prints how to update with dpm when one exists. Never blocks on failure.
51
+ * 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.
79
53
  */
80
54
  export async function maybeNotifyUpdate() {
81
- const day = todayKey();
55
+ const session = sessionKey();
82
56
  const prev = await loadState();
83
- if (prev?.day === day)
57
+ if (prev.sessions.includes(session))
84
58
  return;
85
- // Mark the day early so a slow/failed check does not retry every command.
86
- await saveState({ day, latest: prev?.latest });
59
+ await saveState({ sessions: [...prev.sessions, session], latest: prev.latest });
87
60
  try {
88
- const latest = await resolveNewestAvailable();
89
- if (!latest || !semver.valid(CLI_VERSION))
61
+ const config = await loadConfig();
62
+ const latest = await fetchLatestFromDpm(config.registry);
63
+ if (!latest || !semver.valid(latest) || !semver.valid(CLI_VERSION))
90
64
  return;
91
- await saveState({ day, latest });
65
+ await saveState({
66
+ sessions: [...prev.sessions, session],
67
+ latest,
68
+ });
92
69
  if (!semver.gt(latest, CLI_VERSION))
93
70
  return;
94
- const updatePkg = CLI_NAME.startsWith("@") ? CLI_NAME : "@drops-it/dpm";
95
71
  console.error([
96
72
  "",
97
73
  `${c.yellow("▲")} ${c.bold("dpm")} update available: ${c.dim(CLI_VERSION)} ${c.cyan("→")} ${c.green(latest)}`,
98
- ` Update with: ${c.bold(`dpm i -g ${updatePkg}`)}`,
74
+ ` Update with: ${c.bold("dpm i -g dpm")}`,
99
75
  "",
100
76
  ].join("\n"));
101
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drops-it/dpm",
3
- "version": "1.0.1",
3
+ "version": "1.0.5",
4
4
  "description": "Drops Package Manager CLI",
5
5
  "type": "module",
6
6
  "bin": {