@drops-it/dpm 1.0.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/api.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { DpmConfig } from "./config.js";
2
+ export declare class DpmApi {
3
+ private config;
4
+ constructor(config: DpmConfig);
5
+ private headers;
6
+ private url;
7
+ whoami(): Promise<{
8
+ username: string;
9
+ id: string;
10
+ scopes: string[];
11
+ }>;
12
+ search(q: string): Promise<{
13
+ packages: Array<Record<string, unknown>>;
14
+ }>;
15
+ getPackage(name: string): Promise<{
16
+ name: string;
17
+ description?: string;
18
+ latest?: string | null;
19
+ source?: string;
20
+ versions: Array<{
21
+ version: string;
22
+ dependencies?: Record<string, string>;
23
+ dist: {
24
+ tarball: string;
25
+ integrity: string;
26
+ };
27
+ }>;
28
+ "dist-tags": Record<string, string>;
29
+ } | null>;
30
+ publish(body: {
31
+ manifest: Record<string, unknown>;
32
+ tarballBase64: string;
33
+ access?: "public" | "private";
34
+ }): Promise<any>;
35
+ startDeviceLogin(): Promise<{
36
+ deviceCode: string;
37
+ userCode: string;
38
+ verificationUrl: string;
39
+ expiresIn: number;
40
+ }>;
41
+ pollDeviceLogin(deviceCode: string): Promise<{
42
+ status: "pending" | "approved";
43
+ token?: string;
44
+ }>;
45
+ downloadTarball(tarballUrl: string): Promise<Buffer>;
46
+ }
package/dist/api.js ADDED
@@ -0,0 +1,82 @@
1
+ export class DpmApi {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ headers(json = true) {
7
+ const h = {
8
+ "user-agent": "dpm-cli/1.0",
9
+ accept: "application/json",
10
+ };
11
+ if (json)
12
+ h["content-type"] = "application/json";
13
+ if (this.config.token)
14
+ h.authorization = `Bearer ${this.config.token}`;
15
+ return h;
16
+ }
17
+ url(p) {
18
+ return `${this.config.registry.replace(/\/$/, "")}${p}`;
19
+ }
20
+ async whoami() {
21
+ const res = await fetch(this.url("/dpm/v1/whoami"), { headers: this.headers() });
22
+ if (!res.ok)
23
+ throw new Error(`whoami failed (${res.status})`);
24
+ return res.json();
25
+ }
26
+ async search(q) {
27
+ const res = await fetch(this.url(`/dpm/v1/search?q=${encodeURIComponent(q)}`), {
28
+ headers: this.headers(),
29
+ });
30
+ if (!res.ok)
31
+ throw new Error(`search failed (${res.status})`);
32
+ return res.json();
33
+ }
34
+ async getPackage(name) {
35
+ const res = await fetch(this.url(`/dpm/v1/packages/${encodeURIComponent(name)}`), { headers: this.headers() });
36
+ if (res.status === 404)
37
+ return null;
38
+ if (!res.ok)
39
+ throw new Error(`view failed (${res.status})`);
40
+ return res.json();
41
+ }
42
+ async publish(body) {
43
+ const res = await fetch(this.url("/dpm/v1/publish"), {
44
+ method: "PUT",
45
+ headers: this.headers(),
46
+ body: JSON.stringify(body),
47
+ });
48
+ if (!res.ok) {
49
+ const err = await res.text();
50
+ throw new Error(`publish failed (${res.status}): ${err}`);
51
+ }
52
+ return res.json();
53
+ }
54
+ async startDeviceLogin() {
55
+ const res = await fetch(this.url("/api/v1/auth/cli/device"), {
56
+ method: "POST",
57
+ headers: this.headers(),
58
+ });
59
+ if (!res.ok)
60
+ throw new Error(`device login failed (${res.status})`);
61
+ return res.json();
62
+ }
63
+ async pollDeviceLogin(deviceCode) {
64
+ const res = await fetch(this.url("/api/v1/auth/cli/poll"), {
65
+ method: "POST",
66
+ headers: this.headers(),
67
+ body: JSON.stringify({ deviceCode }),
68
+ });
69
+ if (!res.ok)
70
+ throw new Error(`poll failed (${res.status})`);
71
+ return res.json();
72
+ }
73
+ async downloadTarball(tarballUrl) {
74
+ const url = tarballUrl.startsWith("http")
75
+ ? tarballUrl
76
+ : this.url(tarballUrl);
77
+ const res = await fetch(url, { headers: this.headers(false) });
78
+ if (!res.ok)
79
+ throw new Error(`tarball download failed (${res.status})`);
80
+ return Buffer.from(await res.arrayBuffer());
81
+ }
82
+ }
@@ -0,0 +1,33 @@
1
+ import type { DpmApi } from "./api.js";
2
+ import type { Lockfile } from "./config.js";
3
+ export type VulnSeverity = "info" | "low" | "moderate" | "high" | "critical";
4
+ export type AdvisoryHit = {
5
+ name: string;
6
+ version: string;
7
+ title: string;
8
+ severity: VulnSeverity;
9
+ url?: string;
10
+ vulnerable_versions?: string;
11
+ };
12
+ export type ProposedFix = {
13
+ name: string;
14
+ from: string;
15
+ to: string;
16
+ direct: boolean;
17
+ /** True when the fix requires widening a top-level range (--force). */
18
+ breaking: boolean;
19
+ };
20
+ /**
21
+ * Queries the public npm advisory bulk API for known vulnerabilities
22
+ * in the given installed package versions.
23
+ */
24
+ export declare function auditPackages(packages: Record<string, {
25
+ version: string;
26
+ }>): Promise<AdvisoryHit[]>;
27
+ /**
28
+ * Proposes non-vulnerable versions for each vulnerable package,
29
+ * preferring the smallest bump that still satisfies parent ranges.
30
+ */
31
+ export declare function proposeFixes(api: DpmApi, lock: Lockfile, hits: AdvisoryHit[], topLevel: Record<string, string>, force?: boolean): Promise<ProposedFix[]>;
32
+ /** Formats an npm-style colored vulnerability summary for the console. */
33
+ export declare function formatAuditReport(hits: AdvisoryHit[], fixes?: ProposedFix[]): string;
package/dist/audit.js ADDED
@@ -0,0 +1,210 @@
1
+ import semver from "semver";
2
+ import { c, severityColor } from "./colors.js";
3
+ const SEVERITY_ORDER = ["info", "low", "moderate", "high", "critical"];
4
+ function asSeverity(raw) {
5
+ const s = (raw || "moderate").toLowerCase();
6
+ if (SEVERITY_ORDER.includes(s))
7
+ return s;
8
+ return "moderate";
9
+ }
10
+ /**
11
+ * Queries the public npm advisory bulk API for known vulnerabilities
12
+ * in the given installed package versions.
13
+ */
14
+ export async function auditPackages(packages) {
15
+ const names = Object.keys(packages);
16
+ if (names.length === 0)
17
+ return [];
18
+ const body = {};
19
+ for (const name of names) {
20
+ body[name] = [packages[name].version];
21
+ }
22
+ let res;
23
+ try {
24
+ res = await fetch("https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", {
25
+ method: "POST",
26
+ headers: {
27
+ "content-type": "application/json",
28
+ accept: "application/json",
29
+ "user-agent": "dpm-cli/1.0",
30
+ },
31
+ body: JSON.stringify(body),
32
+ });
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ if (!res.ok)
38
+ return [];
39
+ let data;
40
+ try {
41
+ data = (await res.json());
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ const hits = [];
47
+ for (const [name, advisories] of Object.entries(data)) {
48
+ if (!Array.isArray(advisories) || !packages[name])
49
+ continue;
50
+ for (const adv of advisories) {
51
+ hits.push({
52
+ name,
53
+ version: packages[name].version,
54
+ title: adv.title || "Known vulnerability",
55
+ severity: asSeverity(adv.severity),
56
+ url: adv.url,
57
+ vulnerable_versions: adv.vulnerable_versions,
58
+ });
59
+ }
60
+ }
61
+ hits.sort((a, b) => SEVERITY_ORDER.indexOf(b.severity) - SEVERITY_ORDER.indexOf(a.severity));
62
+ return hits;
63
+ }
64
+ function versionIsVulnerable(version, hits) {
65
+ return hits.some((h) => {
66
+ if (!h.vulnerable_versions)
67
+ return false;
68
+ try {
69
+ return semver.satisfies(version, h.vulnerable_versions, { includePrerelease: true });
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ });
75
+ }
76
+ /** Collect parent dependency ranges that constrain a package in the lockfile. */
77
+ function parentRanges(name, lock, topLevel) {
78
+ const ranges = [];
79
+ if (topLevel[name])
80
+ ranges.push(topLevel[name]);
81
+ for (const pkg of Object.values(lock.packages)) {
82
+ const r = pkg.dependencies?.[name];
83
+ if (r)
84
+ ranges.push(r);
85
+ }
86
+ return ranges;
87
+ }
88
+ /**
89
+ * Proposes non-vulnerable versions for each vulnerable package,
90
+ * preferring the smallest bump that still satisfies parent ranges.
91
+ */
92
+ export async function proposeFixes(api, lock, hits, topLevel, force = false) {
93
+ const byName = new Map();
94
+ for (const h of hits) {
95
+ const list = byName.get(h.name) || [];
96
+ list.push(h);
97
+ byName.set(h.name, list);
98
+ }
99
+ const fixes = [];
100
+ for (const [name, pkgHits] of byName) {
101
+ const current = lock.packages[name]?.version;
102
+ if (!current)
103
+ continue;
104
+ const meta = await api.getPackage(name);
105
+ if (!meta)
106
+ continue;
107
+ const versions = meta.versions
108
+ .map((v) => v.version)
109
+ .filter((v) => semver.valid(v))
110
+ .sort(semver.compare);
111
+ const safe = versions.filter((v) => !versionIsVulnerable(v, pkgHits));
112
+ if (safe.length === 0)
113
+ continue;
114
+ const ranges = parentRanges(name, lock, topLevel);
115
+ const direct = Boolean(topLevel[name]);
116
+ const inRange = (v) => ranges.length === 0 ||
117
+ ranges.every((r) => {
118
+ try {
119
+ return semver.satisfies(v, r);
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ });
125
+ let target = safe.find((v) => semver.gte(v, current) && inRange(v)) ??
126
+ safe.filter((v) => inRange(v)).at(-1);
127
+ let breaking = false;
128
+ if (!target && force) {
129
+ target = safe.filter((v) => semver.gte(v, current)).at(0) ?? safe.at(-1);
130
+ breaking = true;
131
+ }
132
+ if (!target || target === current)
133
+ continue;
134
+ fixes.push({ name, from: current, to: target, direct, breaking });
135
+ }
136
+ return fixes.sort((a, b) => a.name.localeCompare(b.name));
137
+ }
138
+ function countBySeverity(hits) {
139
+ const counts = {
140
+ info: 0,
141
+ low: 0,
142
+ moderate: 0,
143
+ high: 0,
144
+ critical: 0,
145
+ };
146
+ for (const h of hits)
147
+ counts[h.severity] += 1;
148
+ return counts;
149
+ }
150
+ /** Formats an npm-style colored vulnerability summary for the console. */
151
+ export function formatAuditReport(hits, fixes = []) {
152
+ if (hits.length === 0) {
153
+ return `${c.green("found 0 vulnerabilities")}`;
154
+ }
155
+ const counts = countBySeverity(hits);
156
+ const parts = SEVERITY_ORDER.filter((s) => counts[s] > 0).map((s) => {
157
+ const color = severityColor(s);
158
+ return color(`${counts[s]} ${s}`);
159
+ });
160
+ const byPkg = new Map();
161
+ for (const h of hits) {
162
+ let agg = byPkg.get(h.name);
163
+ if (!agg) {
164
+ agg = {
165
+ version: h.version,
166
+ max: h.severity,
167
+ counts: { info: 0, low: 0, moderate: 0, high: 0, critical: 0 },
168
+ };
169
+ byPkg.set(h.name, agg);
170
+ }
171
+ agg.counts[h.severity] += 1;
172
+ if (SEVERITY_ORDER.indexOf(h.severity) > SEVERITY_ORDER.indexOf(agg.max)) {
173
+ agg.max = h.severity;
174
+ }
175
+ }
176
+ const fixByName = new Map(fixes.map((f) => [f.name, f]));
177
+ const pkgLines = [...byPkg.entries()]
178
+ .sort((a, b) => SEVERITY_ORDER.indexOf(b[1].max) - SEVERITY_ORDER.indexOf(a[1].max))
179
+ .slice(0, 8)
180
+ .map(([name, info]) => {
181
+ const detail = SEVERITY_ORDER.filter((s) => info.counts[s] > 0)
182
+ .map((s) => severityColor(s)(`${info.counts[s]} ${s}`))
183
+ .join(", ");
184
+ const fix = fixByName.get(name);
185
+ const fixHint = fix
186
+ ? c.green(` → ${fix.to}${fix.breaking ? " (breaking)" : ""}`)
187
+ : c.dim(" (no fix available)");
188
+ return ` ${c.bold(name)}@${c.dim(info.version)} ${detail}${fixHint}`;
189
+ });
190
+ const extra = byPkg.size > 8 ? `\n ${c.dim(`… and ${byPkg.size - 8} more packages`)}` : "";
191
+ const fixable = fixes.filter((f) => !f.breaking).length;
192
+ const forced = fixes.filter((f) => f.breaking).length;
193
+ const footer = [""];
194
+ if (fixable > 0) {
195
+ footer.push(c.cyan(` ${fixable} vulnerabilit${fixable === 1 ? "y" : "ies"} fixable with:`));
196
+ footer.push(` ${c.bold("dpm audit fix")}`);
197
+ }
198
+ if (forced > 0) {
199
+ footer.push(c.yellow(` ${forced} require a breaking update:`));
200
+ footer.push(` ${c.bold("dpm audit fix --force")}`);
201
+ }
202
+ if (fixable === 0 && forced === 0) {
203
+ footer.push(c.dim(" No automatic fixes available for these advisories."));
204
+ }
205
+ return [
206
+ "",
207
+ `${c.bold(`found ${hits.length} vulnerabilit${hits.length === 1 ? "y" : "ies"}`)} (${parts.join(", ")})`,
208
+ ...pkgLines,
209
+ ].join("\n") + extra + footer.join("\n");
210
+ }
@@ -0,0 +1,15 @@
1
+ /** ANSI styling helpers that stay silent when stdout is not a TTY or NO_COLOR is set. */
2
+ export declare const c: {
3
+ reset: string;
4
+ dim: (text: string) => string;
5
+ bold: (text: string) => string;
6
+ green: (text: string) => string;
7
+ yellow: (text: string) => string;
8
+ red: (text: string) => string;
9
+ cyan: (text: string) => string;
10
+ magenta: (text: string) => string;
11
+ blue: (text: string) => string;
12
+ white: (text: string) => string;
13
+ gray: (text: string) => string;
14
+ };
15
+ export declare function severityColor(severity: string): (text: string) => string;
package/dist/colors.js ADDED
@@ -0,0 +1,30 @@
1
+ /** ANSI styling helpers that stay silent when stdout is not a TTY or NO_COLOR is set. */
2
+ const enabled = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0";
3
+ const wrap = (open, close = "\x1b[0m") => (text) => enabled() ? `${open}${text}${close}` : text;
4
+ export const c = {
5
+ reset: "\x1b[0m",
6
+ dim: wrap("\x1b[2m"),
7
+ bold: wrap("\x1b[1m"),
8
+ green: wrap("\x1b[32m"),
9
+ yellow: wrap("\x1b[33m"),
10
+ red: wrap("\x1b[31m"),
11
+ cyan: wrap("\x1b[36m"),
12
+ magenta: wrap("\x1b[35m"),
13
+ blue: wrap("\x1b[34m"),
14
+ white: wrap("\x1b[37m"),
15
+ gray: wrap("\x1b[90m"),
16
+ };
17
+ export function severityColor(severity) {
18
+ switch (severity) {
19
+ case "critical":
20
+ return (t) => c.bold(c.red(t));
21
+ case "high":
22
+ return c.red;
23
+ case "moderate":
24
+ return c.yellow;
25
+ case "low":
26
+ return c.cyan;
27
+ default:
28
+ return c.gray;
29
+ }
30
+ }
@@ -0,0 +1,23 @@
1
+ export type DpmConfig = {
2
+ registry: string;
3
+ token?: string;
4
+ };
5
+ export type Lockfile = {
6
+ lockfileVersion: 1;
7
+ packages: Record<string, {
8
+ version: string;
9
+ resolved: string;
10
+ integrity: string;
11
+ source: "dpm" | "npm";
12
+ dependencies?: Record<string, string>;
13
+ }>;
14
+ };
15
+ declare const CONFIG_PATH: string;
16
+ export declare function loadConfig(): Promise<DpmConfig>;
17
+ export declare function saveConfig(config: DpmConfig): Promise<void>;
18
+ export declare function loadLockfile(cwd?: string): Promise<Lockfile>;
19
+ export declare function saveLockfile(lock: Lockfile, cwd?: string): Promise<void>;
20
+ export declare function readPackageJson(cwd?: string): Promise<Record<string, unknown>>;
21
+ export declare function writePackageJson(data: Record<string, unknown>, cwd?: string): Promise<void>;
22
+ export declare function ensureDir(dir: string): Promise<void>;
23
+ export { CONFIG_PATH };
package/dist/config.js ADDED
@@ -0,0 +1,43 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import YAML from "yaml";
5
+ const CONFIG_PATH = path.join(homedir(), ".dpmrc");
6
+ export async function loadConfig() {
7
+ try {
8
+ const raw = await readFile(CONFIG_PATH, "utf8");
9
+ return JSON.parse(raw);
10
+ }
11
+ catch {
12
+ return {
13
+ registry: process.env.DPM_REGISTRY || "https://dpm.drops-it.com",
14
+ token: process.env.DPM_TOKEN,
15
+ };
16
+ }
17
+ }
18
+ export async function saveConfig(config) {
19
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
20
+ }
21
+ export async function loadLockfile(cwd = process.cwd()) {
22
+ try {
23
+ const raw = await readFile(path.join(cwd, "dpm-lock.yaml"), "utf8");
24
+ return YAML.parse(raw);
25
+ }
26
+ catch {
27
+ return { lockfileVersion: 1, packages: {} };
28
+ }
29
+ }
30
+ export async function saveLockfile(lock, cwd = process.cwd()) {
31
+ await writeFile(path.join(cwd, "dpm-lock.yaml"), YAML.stringify(lock));
32
+ }
33
+ export async function readPackageJson(cwd = process.cwd()) {
34
+ const raw = await readFile(path.join(cwd, "package.json"), "utf8");
35
+ return JSON.parse(raw);
36
+ }
37
+ export async function writePackageJson(data, cwd = process.cwd()) {
38
+ await writeFile(path.join(cwd, "package.json"), `${JSON.stringify(data, null, 2)}\n`);
39
+ }
40
+ export async function ensureDir(dir) {
41
+ await mkdir(dir, { recursive: true });
42
+ }
43
+ export { CONFIG_PATH };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ import { exec } from "node:child_process";
3
+ import { Command } from "commander";
4
+ import { DpmApi } from "./api.js";
5
+ import { loadConfig, saveConfig, writePackageJson } from "./config.js";
6
+ import { addPackages, auditFix, installAll, installGlobalPackages, packTarball, removePackage, runAudit, } from "./install.js";
7
+ import { createUi } from "./ui.js";
8
+ import { CLI_VERSION, maybeNotifyUpdate } from "./updateCheck.js";
9
+ const program = new Command();
10
+ program.name("dpm").description("Drops Package Manager").version(CLI_VERSION);
11
+ program.hook("preAction", async () => {
12
+ await maybeNotifyUpdate();
13
+ });
14
+ program
15
+ .command("config")
16
+ .argument("[key]")
17
+ .argument("[value]")
18
+ .description("Get or set CLI config")
19
+ .action(async (key, value) => {
20
+ const config = await loadConfig();
21
+ if (!key) {
22
+ console.log(JSON.stringify({ ...config, token: config.token ? "***" : undefined }, null, 2));
23
+ return;
24
+ }
25
+ if (value === undefined) {
26
+ console.log(config[key]);
27
+ return;
28
+ }
29
+ if (key === "registry")
30
+ config.registry = value;
31
+ if (key === "token")
32
+ config.token = value;
33
+ await saveConfig(config);
34
+ console.log("Saved.");
35
+ });
36
+ program
37
+ .command("login")
38
+ .description("Log in via Drops-IT (browser device flow)")
39
+ .action(async () => {
40
+ const config = await loadConfig();
41
+ const api = new DpmApi(config);
42
+ const device = await api.startDeviceLogin();
43
+ const site = config.registry.includes("127.0.0.1") || config.registry.includes("localhost")
44
+ ? "http://127.0.0.1:25121"
45
+ : "https://dpm.drops-it.com";
46
+ const url = `${site}${device.verificationUrl}?code=${device.userCode}`;
47
+ console.log(`Open ${url}`);
48
+ console.log(`User code: ${device.userCode}`);
49
+ exec(`${process.platform === "win32" ? "start" : "xdg-open"} "" "${url}"`);
50
+ const started = Date.now();
51
+ while (Date.now() - started < device.expiresIn * 1000) {
52
+ await new Promise((r) => setTimeout(r, 2000));
53
+ const poll = await api.pollDeviceLogin(device.deviceCode);
54
+ if (poll.status === "approved" && poll.token) {
55
+ config.token = poll.token;
56
+ await saveConfig(config);
57
+ const who = await new DpmApi(config).whoami();
58
+ console.log(`Logged in as ${who.username}`);
59
+ return;
60
+ }
61
+ }
62
+ console.error("Login timed out.");
63
+ process.exit(1);
64
+ });
65
+ program
66
+ .command("logout")
67
+ .description("Remove local DPM token")
68
+ .action(async () => {
69
+ const config = await loadConfig();
70
+ delete config.token;
71
+ await saveConfig(config);
72
+ console.log("Logged out.");
73
+ });
74
+ program
75
+ .command("whoami")
76
+ .description("Show current DPM user")
77
+ .action(async () => {
78
+ const api = new DpmApi(await loadConfig());
79
+ const who = await api.whoami();
80
+ console.log(who.username);
81
+ });
82
+ program
83
+ .command("init")
84
+ .description("Create a package.json")
85
+ .action(async () => {
86
+ await writePackageJson({
87
+ name: "my-package",
88
+ version: "1.0.0",
89
+ description: "",
90
+ main: "index.js",
91
+ license: "MIT",
92
+ dependencies: {},
93
+ });
94
+ console.log("Created package.json");
95
+ });
96
+ program
97
+ .command("search")
98
+ .argument("<query>")
99
+ .action(async (query) => {
100
+ const api = new DpmApi(await loadConfig());
101
+ const { packages } = await api.search(query);
102
+ for (const p of packages) {
103
+ console.log(`${p.name}@${p.version ?? "?"} — ${p.description || ""} [${p.source || "dpm"}]`);
104
+ }
105
+ });
106
+ program
107
+ .command("view")
108
+ .argument("<name>")
109
+ .action(async (name) => {
110
+ const api = new DpmApi(await loadConfig());
111
+ const meta = await api.getPackage(name);
112
+ if (!meta) {
113
+ console.error("Not found");
114
+ process.exit(1);
115
+ }
116
+ console.log(JSON.stringify(meta, null, 2));
117
+ });
118
+ program
119
+ .command("install")
120
+ .alias("i")
121
+ .argument("[packages...]", "optional packages to add (like npm i lodash)")
122
+ .option("-g, --global", "install into the npm global prefix")
123
+ .description("Install package.json deps, or add packages then install")
124
+ .action(async (packages, opts) => {
125
+ const api = new DpmApi(await loadConfig());
126
+ const ui = createUi();
127
+ if (opts.global) {
128
+ await installGlobalPackages(api, packages, ui);
129
+ return;
130
+ }
131
+ if (packages.length > 0) {
132
+ await addPackages(api, packages, process.cwd(), ui);
133
+ return;
134
+ }
135
+ await installAll(api, process.cwd(), ui);
136
+ });
137
+ program
138
+ .command("add")
139
+ .argument("<packages...>")
140
+ .option("-g, --global", "install into the npm global prefix")
141
+ .description("Add dependencies to package.json and install")
142
+ .action(async (packages, opts) => {
143
+ const api = new DpmApi(await loadConfig());
144
+ const ui = createUi();
145
+ if (opts.global) {
146
+ await installGlobalPackages(api, packages, ui);
147
+ return;
148
+ }
149
+ await addPackages(api, packages, process.cwd(), ui);
150
+ });
151
+ program
152
+ .command("update")
153
+ .description("Re-resolve and install dependencies")
154
+ .action(async () => {
155
+ const api = new DpmApi(await loadConfig());
156
+ await installAll(api, process.cwd(), createUi());
157
+ });
158
+ program
159
+ .command("remove")
160
+ .alias("rm")
161
+ .argument("<name>")
162
+ .action(async (name) => {
163
+ await removePackage(name, process.cwd(), createUi());
164
+ });
165
+ const auditCmd = program
166
+ .command("audit")
167
+ .description("Report known vulnerabilities (like npm audit)")
168
+ .action(async () => {
169
+ const api = new DpmApi(await loadConfig());
170
+ await runAudit(api, process.cwd(), createUi());
171
+ });
172
+ auditCmd
173
+ .command("fix")
174
+ .description("Apply available vulnerability fixes")
175
+ .option("--force", "allow breaking version bumps")
176
+ .action(async (opts) => {
177
+ const api = new DpmApi(await loadConfig());
178
+ await auditFix(api, process.cwd(), createUi(), Boolean(opts.force));
179
+ });
180
+ program
181
+ .command("publish")
182
+ .option("--access <access>", "public or private")
183
+ .action(async (opts) => {
184
+ const api = new DpmApi(await loadConfig());
185
+ const packed = await packTarball();
186
+ const result = await api.publish({
187
+ manifest: packed.manifest,
188
+ tarballBase64: packed.tarballBase64,
189
+ access: opts.access,
190
+ });
191
+ console.log(`Published ${result.name}@${result.version} (${result.access})`);
192
+ });
193
+ program
194
+ .command("link")
195
+ .description("Reserved for local workspace linking")
196
+ .action(() => {
197
+ console.log("Use file: dependencies or workspaces for now.");
198
+ });
199
+ program
200
+ .command("unlink")
201
+ .description("Reserved for local workspace unlinking")
202
+ .action(() => {
203
+ console.log("Unlink placeholder — remove file: links from package.json.");
204
+ });
205
+ program.parseAsync(process.argv).catch((err) => {
206
+ console.error(err instanceof Error ? err.message : err);
207
+ process.exit(1);
208
+ });
@@ -0,0 +1,48 @@
1
+ import { DpmApi } from "./api.js";
2
+ import { auditPackages, type ProposedFix } from "./audit.js";
3
+ import { type Lockfile } from "./config.js";
4
+ export type InstallLog = {
5
+ info: (msg: string) => void;
6
+ warn: (msg: string) => void;
7
+ success?: (msg: string) => void;
8
+ status?: (msg: string) => void;
9
+ clearStatus?: () => void;
10
+ };
11
+ export type InstallOptions = {
12
+ /** Prefer these exact versions when they satisfy the requested range. */
13
+ preferredVersions?: Record<string, string>;
14
+ /** Skip the post-install vulnerability check (used by nested audit-fix reinstall). */
15
+ skipAudit?: boolean;
16
+ };
17
+ /** Parse `name`, `name@1.2.3`, `name@^1`, `@scope/name`, `@scope/name@1.0.0`. */
18
+ export declare function parsePackageSpec(spec: string): {
19
+ name: string;
20
+ range: string;
21
+ };
22
+ export declare function installAll(api: DpmApi, cwd?: string, log?: InstallLog, options?: InstallOptions): Promise<Lockfile>;
23
+ export declare function addPackages(api: DpmApi, specs: string[], cwd?: string, log?: InstallLog): Promise<Lockfile>;
24
+ /**
25
+ * Installs packages into the npm global prefix (bins linked by npm).
26
+ * Used for CLI self-updates: `dpm i -g @drops-it/dpm`.
27
+ */
28
+ export declare function installGlobalPackages(api: DpmApi, specs: string[], log?: InstallLog): Promise<void>;
29
+ export declare function addPackage(api: DpmApi, name: string, range?: string, cwd?: string, log?: InstallLog): Promise<Lockfile>;
30
+ export declare function removePackage(name: string, cwd?: string, log?: InstallLog): Promise<void>;
31
+ /**
32
+ * Runs a vulnerability audit against the current lockfile (or installs first).
33
+ */
34
+ export declare function runAudit(api: DpmApi, cwd?: string, log?: InstallLog): Promise<{
35
+ hits: Awaited<ReturnType<typeof auditPackages>>;
36
+ fixes: ProposedFix[];
37
+ }>;
38
+ /**
39
+ * Applies available advisory fixes (like `npm audit fix`), then reinstalls.
40
+ */
41
+ export declare function auditFix(api: DpmApi, cwd?: string, log?: InstallLog, force?: boolean): Promise<{
42
+ fixed: ProposedFix[];
43
+ lock: Lockfile;
44
+ }>;
45
+ export declare function packTarball(cwd?: string): Promise<{
46
+ manifest: Record<string, unknown>;
47
+ tarballBase64: string;
48
+ }>;
@@ -0,0 +1,354 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import * as tar from "tar";
7
+ import semver from "semver";
8
+ import { auditPackages, formatAuditReport, proposeFixes, } from "./audit.js";
9
+ import { c } from "./colors.js";
10
+ import { loadLockfile, readPackageJson, saveLockfile, writePackageJson, } from "./config.js";
11
+ import { createUi } from "./ui.js";
12
+ const execFileAsync = promisify(execFile);
13
+ function asUi(log) {
14
+ if ("status" in log && typeof log.status === "function" && "clearStatus" in log) {
15
+ return log;
16
+ }
17
+ return createUi();
18
+ }
19
+ function integrityOf(buf) {
20
+ return `sha512-${createHash("sha512").update(buf).digest("base64")}`;
21
+ }
22
+ async function extractToNodeModules(name, tarball, cwd) {
23
+ const target = path.join(cwd, "node_modules", ...name.split("/"));
24
+ await rm(target, { recursive: true, force: true });
25
+ await mkdir(target, { recursive: true });
26
+ const tmp = path.join(cwd, "node_modules", `.dpm-tmp-${Date.now()}-${Math.random().toString(16).slice(2)}.tgz`);
27
+ await writeFile(tmp, tarball);
28
+ await tar.x({ file: tmp, cwd: target, strip: 1 });
29
+ await rm(tmp, { force: true });
30
+ }
31
+ function pickVersion(versions, range, distTags, preferred) {
32
+ // Audit-fix preferred versions always win when present in the registry.
33
+ if (preferred && versions.includes(preferred))
34
+ return preferred;
35
+ if (range === "latest" || range === "*" || range === "") {
36
+ return distTags.latest ?? semver.maxSatisfying(versions, "*");
37
+ }
38
+ if (semver.valid(range))
39
+ return versions.includes(range) ? range : null;
40
+ return semver.maxSatisfying(versions, range);
41
+ }
42
+ /** Parse `name`, `name@1.2.3`, `name@^1`, `@scope/name`, `@scope/name@1.0.0`. */
43
+ export function parsePackageSpec(spec) {
44
+ if (spec.startsWith("@")) {
45
+ const at = spec.indexOf("@", 1);
46
+ if (at === -1)
47
+ return { name: spec, range: "latest" };
48
+ return { name: spec.slice(0, at), range: spec.slice(at + 1) || "latest" };
49
+ }
50
+ const at = spec.lastIndexOf("@");
51
+ if (at <= 0)
52
+ return { name: spec, range: "latest" };
53
+ return { name: spec.slice(0, at), range: spec.slice(at + 1) || "latest" };
54
+ }
55
+ async function ensurePackageJson(cwd, log) {
56
+ try {
57
+ await access(path.join(cwd, "package.json"));
58
+ return readPackageJson(cwd);
59
+ }
60
+ catch {
61
+ log.warn("no package.json found — creating one");
62
+ const pkg = {
63
+ name: path.basename(cwd) || "my-package",
64
+ version: "1.0.0",
65
+ private: true,
66
+ dependencies: {},
67
+ };
68
+ await writePackageJson(pkg, cwd);
69
+ return pkg;
70
+ }
71
+ }
72
+ async function resolveRange(api, name, range) {
73
+ const meta = await api.getPackage(name);
74
+ if (!meta)
75
+ throw new Error(`Package not found: ${name}`);
76
+ if (range === "latest" || range === "*") {
77
+ const latest = meta.latest || meta["dist-tags"]?.latest;
78
+ if (!latest)
79
+ throw new Error(`No versions for ${name}`);
80
+ return `^${latest}`;
81
+ }
82
+ if (semver.valid(range))
83
+ return range;
84
+ return range;
85
+ }
86
+ function setStatus(log, msg) {
87
+ if (log.status)
88
+ log.status(msg);
89
+ else
90
+ log.info(msg);
91
+ }
92
+ function topLevelDeps(pkg) {
93
+ return {
94
+ ...(pkg.dependencies || {}),
95
+ ...(pkg.devDependencies || {}),
96
+ };
97
+ }
98
+ function readOverrides(pkg) {
99
+ const raw = pkg.overrides || {};
100
+ const out = {};
101
+ for (const [name, value] of Object.entries(raw)) {
102
+ if (typeof value === "string")
103
+ out[name] = value;
104
+ }
105
+ return out;
106
+ }
107
+ export async function installAll(api, cwd = process.cwd(), log = createUi(), options = {}) {
108
+ const ui = asUi(log);
109
+ const pkg = await ensurePackageJson(cwd, ui);
110
+ const deps = topLevelDeps(pkg);
111
+ const entries = Object.entries(deps);
112
+ if (entries.length === 0) {
113
+ ui.warn("package.json has no dependencies");
114
+ await saveLockfile({ lockfileVersion: 1, packages: {} }, cwd);
115
+ return { lockfileVersion: 1, packages: {} };
116
+ }
117
+ ui.info(`${c.cyan("→")} resolving ${c.bold(String(entries.length))} top-level dependenc${entries.length === 1 ? "y" : "ies"}`);
118
+ const lock = { lockfileVersion: 1, packages: {} };
119
+ const queue = [...entries];
120
+ let installed = 0;
121
+ const started = Date.now();
122
+ const preferred = {
123
+ ...readOverrides(pkg),
124
+ ...(options.preferredVersions || {}),
125
+ };
126
+ while (queue.length) {
127
+ const [name, range] = queue.pop();
128
+ if (lock.packages[name])
129
+ continue;
130
+ const progress = () => {
131
+ const pending = queue.length + 1;
132
+ return `${c.dim(`${installed} done · ${pending} left`)}`;
133
+ };
134
+ setStatus(ui, `resolve ${c.bold(name)}@${range} ${progress()}`);
135
+ const meta = await api.getPackage(name);
136
+ if (!meta)
137
+ throw new Error(`Package not found: ${name}`);
138
+ const versions = meta.versions.map((v) => v.version);
139
+ const version = pickVersion(versions, range, meta["dist-tags"] || {}, preferred[name]);
140
+ if (!version)
141
+ throw new Error(`No version of ${name} matches ${range}`);
142
+ const ver = meta.versions.find((v) => v.version === version);
143
+ const source = meta.source || "dpm";
144
+ const sourceTag = source === "npm" ? c.magenta("[npm]") : c.blue("[dpm]");
145
+ setStatus(ui, `download ${c.bold(name)}@${c.green(version)} ${sourceTag} ${progress()}`);
146
+ const tarball = await api.downloadTarball(ver.dist.tarball);
147
+ const integrity = ver.dist.integrity || integrityOf(tarball);
148
+ setStatus(ui, `extract ${c.bold(name)}@${c.green(version)} ${progress()}`);
149
+ await extractToNodeModules(name, tarball, cwd);
150
+ installed += 1;
151
+ lock.packages[name] = {
152
+ version,
153
+ resolved: ver.dist.tarball,
154
+ integrity,
155
+ source,
156
+ dependencies: ver.dependencies,
157
+ };
158
+ for (const [dep, depRange] of Object.entries(ver.dependencies || {})) {
159
+ if (!lock.packages[dep])
160
+ queue.push([dep, depRange]);
161
+ }
162
+ }
163
+ ui.clearStatus();
164
+ await saveLockfile(lock, cwd);
165
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1);
166
+ if (ui.success) {
167
+ ui.success(`added ${c.bold(String(installed))} package${installed === 1 ? "" : "s"} in ${elapsed}s`);
168
+ }
169
+ else {
170
+ ui.info(`added ${installed} package${installed === 1 ? "" : "s"} in ${elapsed}s`);
171
+ }
172
+ if (!options.skipAudit) {
173
+ setStatus(ui, "checking vulnerabilities…");
174
+ const hits = await auditPackages(lock.packages);
175
+ const fixes = hits.length
176
+ ? await proposeFixes(api, lock, hits, deps, true)
177
+ : [];
178
+ ui.clearStatus();
179
+ ui.info(formatAuditReport(hits, fixes));
180
+ }
181
+ return lock;
182
+ }
183
+ export async function addPackages(api, specs, cwd = process.cwd(), log = createUi()) {
184
+ if (specs.length === 0)
185
+ return installAll(api, cwd, log);
186
+ const ui = asUi(log);
187
+ const pkg = await ensurePackageJson(cwd, ui);
188
+ const deps = pkg.dependencies || {};
189
+ for (const spec of specs) {
190
+ const { name, range } = parsePackageSpec(spec);
191
+ setStatus(ui, `add ${c.bold(name)}`);
192
+ const savedRange = await resolveRange(api, name, range);
193
+ deps[name] = savedRange;
194
+ ui.info(`${c.green("+")} ${c.bold(name)}@${c.cyan(savedRange)}`);
195
+ }
196
+ pkg.dependencies = deps;
197
+ await writePackageJson(pkg, cwd);
198
+ return installAll(api, cwd, ui);
199
+ }
200
+ /**
201
+ * Installs packages into the npm global prefix (bins linked by npm).
202
+ * Used for CLI self-updates: `dpm i -g @drops-it/dpm`.
203
+ */
204
+ export async function installGlobalPackages(api, specs, log = createUi()) {
205
+ if (specs.length === 0) {
206
+ throw new Error("global install requires at least one package name");
207
+ }
208
+ const ui = asUi(log);
209
+ for (const spec of specs) {
210
+ const { name, range } = parsePackageSpec(spec);
211
+ setStatus(ui, `resolve ${c.bold(name)}`);
212
+ const meta = await api.getPackage(name);
213
+ if (!meta)
214
+ throw new Error(`Package not found: ${name}`);
215
+ const versions = meta.versions.map((v) => v.version);
216
+ const version = pickVersion(versions, range, meta["dist-tags"] || {});
217
+ if (!version)
218
+ throw new Error(`No version of ${name} matches ${range}`);
219
+ const ver = meta.versions.find((v) => v.version === version);
220
+ 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`);
223
+ await writeFile(tmp, tarball);
224
+ try {
225
+ 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
+ });
230
+ }
231
+ finally {
232
+ await rm(tmp, { force: true });
233
+ }
234
+ ui.clearStatus();
235
+ if (ui.success)
236
+ ui.success(`installed ${c.bold(name)}@${c.green(version)} globally`);
237
+ else
238
+ ui.info(`installed ${name}@${version} globally`);
239
+ }
240
+ }
241
+ export async function addPackage(api, name, range = "latest", cwd = process.cwd(), log = createUi()) {
242
+ const spec = range === "latest" ? name : `${name}@${range}`;
243
+ return addPackages(api, [spec], cwd, log);
244
+ }
245
+ export async function removePackage(name, cwd = process.cwd(), log = createUi()) {
246
+ const ui = asUi(log);
247
+ const pkg = await readPackageJson(cwd);
248
+ const deps = pkg.dependencies || {};
249
+ const dev = pkg.devDependencies || {};
250
+ if (!deps[name] && !dev[name]) {
251
+ ui.warn(`${name} is not in package.json`);
252
+ }
253
+ delete deps[name];
254
+ delete dev[name];
255
+ pkg.dependencies = deps;
256
+ pkg.devDependencies = dev;
257
+ await writePackageJson(pkg, cwd);
258
+ await rm(path.join(cwd, "node_modules", ...name.split("/")), {
259
+ recursive: true,
260
+ force: true,
261
+ });
262
+ const lock = await loadLockfile(cwd);
263
+ delete lock.packages[name];
264
+ await saveLockfile(lock, cwd);
265
+ if (ui.success)
266
+ ui.success(`removed ${c.bold(name)}`);
267
+ else
268
+ ui.info(`removed ${name}`);
269
+ }
270
+ /**
271
+ * Runs a vulnerability audit against the current lockfile (or installs first).
272
+ */
273
+ export async function runAudit(api, cwd = process.cwd(), log = createUi()) {
274
+ const ui = asUi(log);
275
+ let lock = await loadLockfile(cwd);
276
+ if (Object.keys(lock.packages).length === 0) {
277
+ lock = await installAll(api, cwd, ui, { skipAudit: true });
278
+ }
279
+ setStatus(ui, "checking vulnerabilities…");
280
+ const hits = await auditPackages(lock.packages);
281
+ const pkg = await readPackageJson(cwd);
282
+ const deps = topLevelDeps(pkg);
283
+ const fixes = hits.length ? await proposeFixes(api, lock, hits, deps, true) : [];
284
+ ui.clearStatus();
285
+ ui.info(formatAuditReport(hits, fixes));
286
+ return { hits, fixes: fixes.filter((f) => !f.breaking) };
287
+ }
288
+ /**
289
+ * Applies available advisory fixes (like `npm audit fix`), then reinstalls.
290
+ */
291
+ export async function auditFix(api, cwd = process.cwd(), log = createUi(), force = false) {
292
+ const ui = asUi(log);
293
+ let lock = await loadLockfile(cwd);
294
+ if (Object.keys(lock.packages).length === 0) {
295
+ lock = await installAll(api, cwd, ui, { skipAudit: true });
296
+ }
297
+ setStatus(ui, "checking vulnerabilities…");
298
+ const hits = await auditPackages(lock.packages);
299
+ const pkg = await readPackageJson(cwd);
300
+ const deps = topLevelDeps(pkg);
301
+ const fixes = await proposeFixes(api, lock, hits, deps, force);
302
+ ui.clearStatus();
303
+ const applicable = force ? fixes : fixes.filter((f) => !f.breaking);
304
+ if (applicable.length === 0) {
305
+ ui.info(formatAuditReport(hits, await proposeFixes(api, lock, hits, deps, true)));
306
+ ui.warn(force ? "no fixes available" : "no non-breaking fixes — try dpm audit fix --force");
307
+ return { fixed: [], lock };
308
+ }
309
+ const preferred = {};
310
+ const dependencies = pkg.dependencies || {};
311
+ const devDependencies = pkg.devDependencies || {};
312
+ const overrides = readOverrides(pkg);
313
+ for (const fix of applicable) {
314
+ preferred[fix.name] = fix.to;
315
+ ui.info(`${c.green("fix")} ${c.bold(fix.name)} ${c.dim(fix.from)} ${c.cyan("→")} ${c.green(fix.to)}${fix.breaking ? c.yellow(" (breaking)") : ""}`);
316
+ if (dependencies[fix.name]) {
317
+ dependencies[fix.name] = `^${fix.to}`;
318
+ }
319
+ else if (devDependencies[fix.name]) {
320
+ devDependencies[fix.name] = `^${fix.to}`;
321
+ }
322
+ else {
323
+ // Persist transitive fixes so the next plain install keeps them.
324
+ overrides[fix.name] = fix.to;
325
+ }
326
+ }
327
+ pkg.dependencies = dependencies;
328
+ pkg.devDependencies = devDependencies;
329
+ if (Object.keys(overrides).length > 0)
330
+ pkg.overrides = overrides;
331
+ await writePackageJson(pkg, cwd);
332
+ const next = await installAll(api, cwd, ui, {
333
+ preferredVersions: preferred,
334
+ skipAudit: false,
335
+ });
336
+ return { fixed: applicable, lock: next };
337
+ }
338
+ export async function packTarball(cwd = process.cwd()) {
339
+ const manifest = await readPackageJson(cwd);
340
+ const out = path.join(cwd, `.dpm-pack-${Date.now()}.tgz`);
341
+ await tar.c({
342
+ gzip: true,
343
+ file: out,
344
+ cwd,
345
+ prefix: "package",
346
+ filter: (p) => !p.includes("node_modules") &&
347
+ !p.startsWith(".git") &&
348
+ !p.endsWith(".tgz") &&
349
+ p !== "dpm-lock.yaml",
350
+ }, ["."]);
351
+ const buf = await readFile(out);
352
+ await rm(out, { force: true });
353
+ return { manifest, tarballBase64: buf.toString("base64") };
354
+ }
package/dist/ui.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /** Clears the ephemeral status line and writes permanent output. */
2
+ export type Ui = {
3
+ info: (msg: string) => void;
4
+ warn: (msg: string) => void;
5
+ success: (msg: string) => void;
6
+ /** Overwrites the current in-progress line (cleared when the next status or commit runs). */
7
+ status: (msg: string) => void;
8
+ /** Ends the status line and prints a lasting message. */
9
+ commit: (msg: string) => void;
10
+ /** Clears any active status line without printing. */
11
+ clearStatus: () => void;
12
+ };
13
+ /**
14
+ * Builds a terminal UI with one live animated status line that replaces itself
15
+ * as each package is fetched, plus permanent info/warn lines.
16
+ */
17
+ export declare function createUi(): Ui;
package/dist/ui.js ADDED
@@ -0,0 +1,76 @@
1
+ import { c } from "./colors.js";
2
+ const isTty = () => Boolean(process.stdout.isTTY);
3
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
+ /**
5
+ * Builds a terminal UI with one live animated status line that replaces itself
6
+ * as each package is fetched, plus permanent info/warn lines.
7
+ */
8
+ export function createUi() {
9
+ let active = false;
10
+ let label = "";
11
+ let frame = 0;
12
+ let timer = null;
13
+ const paint = () => {
14
+ if (!isTty() || !active)
15
+ return;
16
+ const spin = c.cyan(FRAMES[frame % FRAMES.length]);
17
+ const cols = process.stdout.columns || 80;
18
+ const prefix = `${spin} `;
19
+ const max = Math.max(20, cols - 3);
20
+ const text = label.length > max ? `${label.slice(0, max - 1)}…` : label;
21
+ process.stdout.write(`\r\x1b[2K${prefix}${text}`);
22
+ };
23
+ const stopSpin = () => {
24
+ if (timer) {
25
+ clearInterval(timer);
26
+ timer = null;
27
+ }
28
+ };
29
+ const clearLine = () => {
30
+ stopSpin();
31
+ if (!active)
32
+ return;
33
+ if (isTty())
34
+ process.stdout.write(`\r\x1b[2K`);
35
+ active = false;
36
+ label = "";
37
+ };
38
+ const status = (msg) => {
39
+ if (!isTty())
40
+ return;
41
+ label = msg;
42
+ active = true;
43
+ if (!timer) {
44
+ frame = 0;
45
+ paint();
46
+ timer = setInterval(() => {
47
+ frame += 1;
48
+ paint();
49
+ }, 80);
50
+ }
51
+ else {
52
+ paint();
53
+ }
54
+ };
55
+ const commit = (msg) => {
56
+ clearLine();
57
+ console.log(msg);
58
+ };
59
+ return {
60
+ status,
61
+ commit,
62
+ clearStatus: clearLine,
63
+ info: (msg) => {
64
+ clearLine();
65
+ console.log(msg);
66
+ },
67
+ success: (msg) => {
68
+ clearLine();
69
+ console.log(`${c.green("✔")} ${msg}`);
70
+ },
71
+ warn: (msg) => {
72
+ clearLine();
73
+ console.warn(`${c.yellow("warn")} ${msg}`);
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,7 @@
1
+ export declare const CLI_NAME: string;
2
+ export declare const CLI_VERSION: string;
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.
6
+ */
7
+ export declare function maybeNotifyUpdate(): Promise<void>;
@@ -0,0 +1,105 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import semver from "semver";
6
+ import { c } from "./colors.js";
7
+ 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;
12
+ /** Package names checked for a newer CLI release. */
13
+ const UPDATE_CANDIDATES = ["dpm", "@drops-it/dpm"];
14
+ const STATE_DIR = path.join(homedir(), ".dpm");
15
+ 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}`;
22
+ }
23
+ async function loadState() {
24
+ try {
25
+ return JSON.parse(await readFile(STATE_PATH, "utf8"));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ async function saveState(state) {
32
+ await mkdir(STATE_DIR, { recursive: true });
33
+ await writeFile(STATE_PATH, JSON.stringify(state), { mode: 0o600 });
34
+ }
35
+ async function fetchLatestFromDpm(name, registry) {
36
+ const url = `${registry.replace(/\/$/, "")}/dpm/v1/packages/${encodeURIComponent(name)}`;
37
+ const res = await fetch(url, {
38
+ headers: { accept: "application/json", "user-agent": `dpm-cli/${CLI_VERSION}` },
39
+ signal: AbortSignal.timeout(2500),
40
+ });
41
+ if (!res.ok)
42
+ return null;
43
+ const data = (await res.json());
44
+ return data.latest || data["dist-tags"]?.latest || null;
45
+ }
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
+ /**
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.
79
+ */
80
+ export async function maybeNotifyUpdate() {
81
+ const day = todayKey();
82
+ const prev = await loadState();
83
+ if (prev?.day === day)
84
+ return;
85
+ // Mark the day early so a slow/failed check does not retry every command.
86
+ await saveState({ day, latest: prev?.latest });
87
+ try {
88
+ const latest = await resolveNewestAvailable();
89
+ if (!latest || !semver.valid(CLI_VERSION))
90
+ return;
91
+ await saveState({ day, latest });
92
+ if (!semver.gt(latest, CLI_VERSION))
93
+ return;
94
+ const updatePkg = CLI_NAME.startsWith("@") ? CLI_NAME : "@drops-it/dpm";
95
+ console.error([
96
+ "",
97
+ `${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}`)}`,
99
+ "",
100
+ ].join("\n"));
101
+ }
102
+ catch {
103
+ // Ignore network / parse errors — update checks must never break commands.
104
+ }
105
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@drops-it/dpm",
3
+ "version": "1.0.1",
4
+ "description": "Drops Package Manager CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "dpm": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=22"
14
+ },
15
+ "scripts": {
16
+ "dev": "tsx src/index.ts",
17
+ "build": "tsc -p tsconfig.json",
18
+ "build:windows": "node scripts/build-windows.mjs",
19
+ "sign:windows": "powershell -ExecutionPolicy Bypass -File scripts/sign-windows.ps1",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "commander": "^12.1.0",
27
+ "semver": "^7.6.3",
28
+ "tar": "^7.4.3",
29
+ "yaml": "^2.7.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.10.5",
33
+ "@types/semver": "^7.5.8",
34
+ "esbuild": "^0.25.0",
35
+ "tsx": "^4.19.2",
36
+ "typescript": "^5.7.3"
37
+ },
38
+ "keywords": [
39
+ "dpm",
40
+ "package-manager",
41
+ "drops-it",
42
+ "cli"
43
+ ],
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/Drops-IT/DPM.git"
48
+ }
49
+ }