@lovrozagar/grunt 0.6.2 → 0.7.0

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.
@@ -11,6 +11,11 @@ import {
11
11
  findSkillContentConflicts,
12
12
  formatSkillConflictWarn,
13
13
  } from "./skill-conflicts.mjs";
14
+ import {
15
+ PACKAGE_MANAGERS,
16
+ detectPackageManager,
17
+ runScriptLine,
18
+ } from "./package-manager.mjs";
14
19
 
15
20
  export const CHROMIUM_BINS = [
16
21
  "chromium",
@@ -28,8 +33,7 @@ const RTK_CURL =
28
33
  "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh";
29
34
  const RTK_DOCS = "https://www.rtk-ai.app/docs/getting-started/installation/";
30
35
  const LP_INSTALL_SH = "curl -fsSL https://pkg.lightpanda.io/install.sh | bash";
31
- const RULESYNC_SCHEMA = "rulesync schema doctor: npm run rulesync:doctor";
32
- const REQUIRED = ["node", "npm", "git", "rtk", "rulesync", "lightpanda", "chromium"];
36
+ const REQUIRED = ["node", "git", "rtk", "rulesync", "lightpanda", "chromium"];
33
37
  export const REQUIRED_MAP_FILES = [
34
38
  { name: "INDEX.md", rel: ".rulesync/reference/INDEX.md" },
35
39
  { name: "skills-map.md", rel: ".rulesync/reference/skills-map.md" },
@@ -127,7 +131,7 @@ function lookupChromium(pathEnv, platform) {
127
131
 
128
132
  export function installHints(id, platform = process.platform) {
129
133
  if (id === "all") {
130
- const keys = ["node", "npm", "git", "rtk", "rulesync", "lightpanda", "chromium"];
134
+ const keys = ["node", "package-manager", "git", "rtk", "rulesync", "lightpanda", "chromium"];
131
135
  return keys.flatMap((k) => installHints(k, platform));
132
136
  }
133
137
  if (id === "node") {
@@ -139,6 +143,22 @@ export function installHints(id, platform = process.platform) {
139
143
  if (id === "npm") {
140
144
  return ["npm ships with Node ≥22", NODE_URL];
141
145
  }
146
+ if (id === "yarn") {
147
+ return ["corepack enable", "npm i -g yarn", "https://yarnpkg.com/getting-started/install"];
148
+ }
149
+ if (id === "pnpm") {
150
+ return ["corepack enable", "npm i -g pnpm", "https://pnpm.io/installation"];
151
+ }
152
+ if (id === "bun") {
153
+ return ["curl -fsSL https://bun.sh/install | bash", "https://bun.sh"];
154
+ }
155
+ if (id === "package-manager") {
156
+ return [
157
+ "npm ships with Node ≥22",
158
+ "corepack enable (yarn / pnpm)",
159
+ "curl -fsSL https://bun.sh/install | bash",
160
+ ];
161
+ }
142
162
  if (id === "git") {
143
163
  if (platform === "win32") return ["winget install Git.Git"];
144
164
  if (platform === "darwin") return ["brew install git"];
@@ -151,7 +171,13 @@ export function installHints(id, platform = process.platform) {
151
171
  return [RTK_CURL, "brew install rtk"];
152
172
  }
153
173
  if (id === "rulesync") {
154
- return ["npm i -D rulesync", "npx"];
174
+ return [
175
+ "npm i -D rulesync",
176
+ "yarn add -D rulesync",
177
+ "pnpm add -D rulesync",
178
+ "bun add -D rulesync",
179
+ "npx",
180
+ ];
155
181
  }
156
182
  if (id === "lightpanda") {
157
183
  if (platform === "win32") {
@@ -296,7 +322,17 @@ export function runDoctor({
296
322
  const major = nodeMajor(ver);
297
323
  const nodeOk = Boolean(nodeBin) && major >= 22;
298
324
 
299
- const npm = whichBin("npm", pathEnv, platform);
325
+ const pmBins = Object.fromEntries(
326
+ PACKAGE_MANAGERS.map((name) => [name, whichBin(name, pathEnv, platform)]),
327
+ );
328
+ const detectedPm = detectPackageManager({ cwd, env });
329
+ const wantedPm = detectedPm.manager
330
+ ? [detectedPm.manager]
331
+ : detectedPm.candidates?.length
332
+ ? detectedPm.candidates
333
+ : PACKAGE_MANAGERS;
334
+ const presentPm = wantedPm.filter((name) => pmBins[name]);
335
+ const pmOk = presentPm.length > 0;
300
336
  const git = whichBin("git", pathEnv, platform);
301
337
  const rtk = whichBin("rtk", nmPath, platform);
302
338
  const rulesync = whichBin("rulesync", nmPath, platform);
@@ -310,7 +346,6 @@ export function runDoctor({
310
346
 
311
347
  const found = {
312
348
  node: nodeOk ? nodeBin : "",
313
- npm,
314
349
  git,
315
350
  rtk,
316
351
  rulesync,
@@ -326,7 +361,17 @@ export function runDoctor({
326
361
  } else {
327
362
  lines.push(row("node", "missing"));
328
363
  }
329
- lines.push(npm ? row("npm", "ok", npm) : row("npm", "missing"));
364
+ if (pmOk) {
365
+ lines.push(
366
+ row(
367
+ "package-manager",
368
+ "ok",
369
+ presentPm.map((name) => `${name} (${pmBins[name]})`).join(", "),
370
+ ),
371
+ );
372
+ } else {
373
+ lines.push(row("package-manager", "missing", wantedPm.join(" | ")));
374
+ }
330
375
  lines.push(git ? row("git", "ok", git) : row("git", "missing"));
331
376
  lines.push(rtk ? row("rtk", "ok", rtk) : row("rtk", "missing"));
332
377
  lines.push(rulesync ? row("rulesync", "ok", rulesync) : row("rulesync", "missing"));
@@ -348,15 +393,20 @@ export function runDoctor({
348
393
  lines.push(speak.ok ? row("speak", "ok", speak.extra) : row("speak", "missing (optional)"));
349
394
 
350
395
  const missingRequired = REQUIRED.filter((k) => (k === "node" ? !nodeOk : !found[k]));
396
+ if (!pmOk) missingRequired.push("package-manager");
351
397
  if (missingRequired.length) {
352
398
  lines.push("");
353
399
  lines.push("install (print-only; not run):");
354
400
  const seen = new Set();
355
401
  for (const k of missingRequired) {
356
- for (const h of installHints(k, platform)) {
357
- if (seen.has(h)) continue;
358
- seen.add(h);
359
- lines.push(h);
402
+ const hintIds =
403
+ k === "package-manager" && wantedPm.length === 1 ? wantedPm : [k];
404
+ for (const id of hintIds) {
405
+ for (const h of installHints(id, platform)) {
406
+ if (seen.has(h)) continue;
407
+ seen.add(h);
408
+ lines.push(h);
409
+ }
360
410
  }
361
411
  }
362
412
  }
@@ -389,7 +439,7 @@ export function runDoctor({
389
439
  }
390
440
 
391
441
  lines.push("");
392
- lines.push(RULESYNC_SCHEMA);
442
+ lines.push(`rulesync schema doctor: ${runScriptLine(detectedPm.manager, "rulesync:doctor")}`);
393
443
  const stdout = `${lines.join("\n")}\n`;
394
444
  return { code: missingRequired.length || missingMaps.length ? 1 : 0, stdout, stderr: "" };
395
445
  }
@@ -0,0 +1,150 @@
1
+ /** Detect npm | yarn | pnpm | bun. Never default to npm. */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export const PACKAGE_MANAGERS = ["npm", "yarn", "pnpm", "bun"];
6
+
7
+ export const UNKNOWN_PACKAGE_MANAGER =
8
+ 'Cannot tell which package manager to use (npm, yarn, pnpm, bun). Add a lockfile or package.json "packageManager" field, or pass --pm <name>.';
9
+
10
+ export const PACKAGE_MANAGER_ASK = "Which package manager?";
11
+
12
+ export const PACKAGE_MANAGER_OPTIONS = PACKAGE_MANAGERS.map((value) => ({
13
+ value,
14
+ label: value,
15
+ }));
16
+
17
+ const LOCKFILES = [
18
+ ["pnpm-lock.yaml", "pnpm"],
19
+ ["yarn.lock", "yarn"],
20
+ ["bun.lockb", "bun"],
21
+ ["bun.lock", "bun"],
22
+ ["package-lock.json", "npm"],
23
+ ["npm-shrinkwrap.json", "npm"],
24
+ ];
25
+
26
+ function isFile(abs) {
27
+ try {
28
+ return fs.statSync(abs).isFile();
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function readJson(abs) {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(abs, "utf8"));
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export function isPackageManager(name) {
43
+ return PACKAGE_MANAGERS.includes(String(name || "").toLowerCase());
44
+ }
45
+
46
+ export function parsePackageManagerField(raw) {
47
+ if (raw && typeof raw === "object" && typeof raw.name === "string") {
48
+ raw = raw.name;
49
+ }
50
+ if (typeof raw !== "string") return null;
51
+ const name = raw.trim().split("@")[0].toLowerCase();
52
+ return isPackageManager(name) ? name : null;
53
+ }
54
+
55
+ function fieldFromPkg(pkg) {
56
+ if (!pkg || typeof pkg !== "object") return null;
57
+ const fromField = parsePackageManagerField(pkg.packageManager);
58
+ if (fromField) return fromField;
59
+ return parsePackageManagerField(pkg.devEngines?.packageManager);
60
+ }
61
+
62
+ export function detectFromRepo(cwd) {
63
+ const lockfiles = [];
64
+ const managers = [];
65
+ const seen = new Set();
66
+ const add = (name, lock) => {
67
+ if (!isPackageManager(name)) return;
68
+ if (lock) lockfiles.push(lock);
69
+ if (seen.has(name)) return;
70
+ seen.add(name);
71
+ managers.push(name);
72
+ };
73
+ const pkg = readJson(path.join(cwd || "", "package.json"));
74
+ const fromField = fieldFromPkg(pkg);
75
+ if (fromField) add(fromField, null);
76
+ for (const [file, name] of LOCKFILES) {
77
+ if (isFile(path.join(cwd || "", file))) add(name, file);
78
+ }
79
+ return { managers, lockfiles };
80
+ }
81
+
82
+ export function detectFromInvocation(env = process.env, { versions = process.versions } = {}) {
83
+ const ua = String(env?.npm_config_user_agent || "").trim();
84
+ if (ua) {
85
+ const first = ua.split(/[\s/]/)[0].toLowerCase();
86
+ if (isPackageManager(first)) return first;
87
+ }
88
+ const execPath = String(env?.npm_execpath || "").replace(/\\/g, "/");
89
+ const base = execPath.split("/").pop() || "";
90
+ const lower = base.toLowerCase();
91
+ for (const pm of PACKAGE_MANAGERS) {
92
+ if (lower === pm || lower.startsWith(`${pm}.`) || lower.startsWith(`${pm}-`)) return pm;
93
+ }
94
+ if (versions && versions.bun) return "bun";
95
+ return null;
96
+ }
97
+
98
+ export function detectPackageManager({
99
+ cwd = "",
100
+ env = process.env,
101
+ override,
102
+ versions = process.versions,
103
+ } = {}) {
104
+ if (override != null && override !== "") {
105
+ const name = String(override).toLowerCase();
106
+ if (!isPackageManager(name)) {
107
+ return { manager: null, source: null, error: "invalid", override: name };
108
+ }
109
+ return { manager: name, source: "flag" };
110
+ }
111
+ const repo = detectFromRepo(cwd);
112
+ if (repo.managers.length === 1) {
113
+ return { manager: repo.managers[0], source: "repo", lockfiles: repo.lockfiles };
114
+ }
115
+ const invoked = detectFromInvocation(env, { versions });
116
+ if (invoked) {
117
+ if (repo.managers.length === 0) {
118
+ return { manager: invoked, source: "invocation", lockfiles: repo.lockfiles };
119
+ }
120
+ if (repo.managers.includes(invoked)) {
121
+ return {
122
+ manager: invoked,
123
+ source: "invocation",
124
+ candidates: repo.managers,
125
+ lockfiles: repo.lockfiles,
126
+ };
127
+ }
128
+ }
129
+ return {
130
+ manager: null,
131
+ source: null,
132
+ candidates: repo.managers,
133
+ lockfiles: repo.lockfiles,
134
+ };
135
+ }
136
+
137
+ export function installArgs(_pm) {
138
+ return ["install"];
139
+ }
140
+
141
+ export function runScriptArgs(pm, script, extra = []) {
142
+ if (!extra.length) return ["run", script];
143
+ if (pm === "npm") return ["run", script, "--", ...extra];
144
+ return ["run", script, ...extra];
145
+ }
146
+
147
+ export function runScriptLine(pm, script) {
148
+ if (pm) return `${pm} run ${script}`;
149
+ return `npm|yarn|pnpm|bun run ${script}`;
150
+ }