@lovrozagar/grunt 0.6.2 → 0.8.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.
package/cli/grunt.mjs CHANGED
@@ -4,6 +4,15 @@ import path from "node:path"
4
4
  import { fileURLToPath } from "node:url"
5
5
  import { destAlreadyInited, init, RESERVED_SKILLS, toGruntScriptName } from "./init.mjs"
6
6
  import { confirm, isInteractive, select, spinner } from "./prompt.mjs"
7
+ import {
8
+ PACKAGE_MANAGER_ASK,
9
+ PACKAGE_MANAGER_OPTIONS,
10
+ UNKNOWN_PACKAGE_MANAGER,
11
+ detectPackageManager,
12
+ isPackageManager,
13
+ runScriptArgs,
14
+ } from "../scripts/package-manager.mjs"
15
+ import { mapCommand } from "../scripts/folder-map.mjs"
7
16
 
8
17
  const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
9
18
 
@@ -12,14 +21,15 @@ const USAGE = `Usage: grunt [command]
12
21
  Default (no command): TTY menu; else init — full setup
13
22
 
14
23
  Commands:
15
- init Full setup: merge SoT, npm install, grunt:rulesync:generate, grunt:sync:globals:apply, grunt:rulesync:check
16
- generate npm run grunt:rulesync:generate
17
- check npm run grunt:rulesync:check
18
- sync-globals npm run grunt:sync:globals (dry-run; --apply to write)
19
- purge-mcps npm run grunt:purge:global-mcps (dry-run; --apply to write)
20
- doctor npm run grunt:doctor
21
- setup npm run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
24
+ init Full setup: merge SoT, install, grunt:rulesync:generate, grunt:sync:globals:apply, grunt:rulesync:check
25
+ generate run grunt:rulesync:generate
26
+ check run grunt:rulesync:check
27
+ sync-globals run grunt:sync:globals (dry-run; --apply to write)
28
+ purge-mcps run grunt:purge:global-mcps (dry-run; --apply to write)
29
+ doctor run grunt:doctor
30
+ setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
22
31
  upgrade Re-init: copy owned files, prune retired grunt-owned names, print reserved skills
32
+ map [dir] Code-only folder map (git-tracked, no files); dir for depth
23
33
  help Show this help
24
34
  version Print package version
25
35
 
@@ -29,6 +39,7 @@ Flags:
29
39
  --non-interactive Same as --yes
30
40
  --apply Write for sync-globals / purge-mcps
31
41
  --host <id> sync-globals host
42
+ --pm <name> npm | yarn | pnpm | bun (else lockfile, then how grunt was launched, then ask)
32
43
  `
33
44
 
34
45
  const MENU_OPTIONS = [
@@ -45,26 +56,41 @@ const MENU_OPTIONS = [
45
56
  ]
46
57
 
47
58
  const YES_FLAGS = new Set(["--yes", "-y", "--non-interactive"])
59
+ const NEEDS_PM = new Set([
60
+ "init",
61
+ "generate",
62
+ "check",
63
+ "sync-globals",
64
+ "purge-mcps",
65
+ "doctor",
66
+ "setup",
67
+ "upgrade",
68
+ ])
48
69
 
49
70
  function pkgVersion() {
50
71
  const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"))
51
72
  return pkg.version
52
73
  }
53
74
 
54
- function npmRun(script, extra = []) {
55
- const args = extra.length ? ["run", script, "--", ...extra] : ["run", script]
56
- execFileSync("npm", args, { cwd: process.cwd(), stdio: "inherit" })
75
+ function runScript(pm, script, extra = []) {
76
+ execFileSync(pm, runScriptArgs(pm, script, extra), { cwd: process.cwd(), stdio: "inherit" })
57
77
  }
58
78
 
59
79
  function hostValueOk(v) {
60
80
  return v != null && v !== "" && !String(v).startsWith("-")
61
81
  }
62
82
 
83
+ function pmValueOk(v) {
84
+ return v != null && v !== "" && isPackageManager(v)
85
+ }
86
+
63
87
  export function parseArgv(argv) {
64
88
  let skipGlobals = false
65
89
  let apply = false
66
90
  let host
67
91
  let hostError = false
92
+ let pm
93
+ let pmError = false
68
94
  const positionals = []
69
95
  for (let i = 0; i < argv.length; i++) {
70
96
  const a = argv[i]
@@ -92,9 +118,25 @@ export function parseArgv(argv) {
92
118
  if (!hostValueOk(host)) hostError = true
93
119
  continue
94
120
  }
121
+ if (a === "--pm") {
122
+ const v = argv[i + 1]
123
+ if (!pmValueOk(v)) {
124
+ pmError = true
125
+ continue
126
+ }
127
+ pm = String(v).toLowerCase()
128
+ i += 1
129
+ continue
130
+ }
131
+ if (typeof a === "string" && a.startsWith("--pm=")) {
132
+ pm = a.slice("--pm=".length)
133
+ if (!pmValueOk(pm)) pmError = true
134
+ else pm = pm.toLowerCase()
135
+ continue
136
+ }
95
137
  positionals.push(a)
96
138
  }
97
- return { cmd: positionals[0], args: positionals.slice(1), skipGlobals, apply, host, hostError }
139
+ return { cmd: positionals[0], args: positionals.slice(1), skipGlobals, apply, host, hostError, pm, pmError }
98
140
  }
99
141
 
100
142
  function hostExtra(host) {
@@ -111,9 +153,23 @@ function bindSpinner() {
111
153
 
112
154
  export const APPLY_GLOBALS_CONFIRM = "Apply global prompt optimizations? (recommended)"
113
155
 
114
- async function runInit(cwd, { skipGlobals, interactive }) {
156
+ export async function resolveCliPackageManager({ cwd, env, override, interactive }) {
157
+ const detected = detectPackageManager({ cwd, env, override })
158
+ if (detected.manager) return detected.manager
159
+ if (interactive) {
160
+ return await select({
161
+ message: PACKAGE_MANAGER_ASK,
162
+ options: PACKAGE_MANAGER_OPTIONS,
163
+ })
164
+ }
165
+ process.stdout.write(`${UNKNOWN_PACKAGE_MANAGER}\n`)
166
+ process.exitCode = 1
167
+ return null
168
+ }
169
+
170
+ async function runInit(cwd, { skipGlobals, interactive, packageManager }) {
115
171
  if (!interactive) {
116
- init(cwd, { skipGlobals })
172
+ init(cwd, { skipGlobals, packageManager })
117
173
  return
118
174
  }
119
175
  if (destAlreadyInited(cwd)) {
@@ -131,10 +187,11 @@ async function runInit(cwd, { skipGlobals, interactive }) {
131
187
  skipGlobals: !applyGlobals,
132
188
  applyGlobals,
133
189
  onPhase: bindSpinner(),
190
+ packageManager,
134
191
  })
135
192
  }
136
193
 
137
- async function dispatch(cmd, flags, interactive) {
194
+ async function dispatch(cmd, flags, interactive, pm) {
138
195
  if (cmd === "help" || cmd === "--help" || cmd === "-h") {
139
196
  process.stdout.write(USAGE)
140
197
  return
@@ -147,38 +204,44 @@ async function dispatch(cmd, flags, interactive) {
147
204
  await runInit(process.cwd(), {
148
205
  skipGlobals: flags.skipGlobals,
149
206
  interactive: interactive && (!cmd || cmd === "init"),
207
+ packageManager: pm,
150
208
  })
151
209
  return
152
210
  }
153
211
  if (cmd === "generate") {
154
- npmRun(toGruntScriptName("rulesync:generate"))
212
+ runScript(pm, toGruntScriptName("rulesync:generate"))
155
213
  return
156
214
  }
157
215
  if (cmd === "check") {
158
- npmRun(toGruntScriptName("rulesync:check"))
216
+ runScript(pm, toGruntScriptName("rulesync:check"))
159
217
  return
160
218
  }
161
219
  if (cmd === "sync-globals") {
162
220
  const script = flags.apply ? toGruntScriptName("sync:globals:apply") : toGruntScriptName("sync:globals")
163
- npmRun(script, hostExtra(flags.host))
221
+ runScript(pm, script, hostExtra(flags.host))
164
222
  return
165
223
  }
166
224
  if (cmd === "purge-mcps") {
167
- npmRun(flags.apply ? toGruntScriptName("purge:global-mcps:apply") : toGruntScriptName("purge:global-mcps"))
225
+ runScript(pm, flags.apply ? toGruntScriptName("purge:global-mcps:apply") : toGruntScriptName("purge:global-mcps"))
168
226
  return
169
227
  }
170
228
  if (cmd === "doctor") {
171
- npmRun(toGruntScriptName("doctor"))
229
+ runScript(pm, toGruntScriptName("doctor"))
172
230
  return
173
231
  }
174
232
  if (cmd === "setup") {
175
- npmRun(toGruntScriptName("setup"), flags.args)
233
+ runScript(pm, toGruntScriptName("setup"), flags.args)
234
+ return
235
+ }
236
+ if (cmd === "map") {
237
+ process.stdout.write(mapCommand(process.cwd(), flags.args[0] || ""))
176
238
  return
177
239
  }
178
240
  if (cmd === "upgrade") {
179
241
  await runInit(process.cwd(), {
180
242
  skipGlobals: flags.skipGlobals,
181
243
  interactive: false,
244
+ packageManager: pm,
182
245
  })
183
246
  process.stdout.write(`reserved: ${RESERVED_SKILLS.join(" ")}\n`)
184
247
  return
@@ -187,9 +250,13 @@ async function dispatch(cmd, flags, interactive) {
187
250
  process.exitCode = 1
188
251
  }
189
252
 
253
+ function commandNeedsPackageManager(cmd) {
254
+ return !cmd || NEEDS_PM.has(cmd)
255
+ }
256
+
190
257
  export async function start() {
191
258
  const flags = parseArgv(process.argv.slice(2))
192
- if (flags.hostError) {
259
+ if (flags.hostError || flags.pmError) {
193
260
  process.stdout.write(USAGE)
194
261
  process.exitCode = 1
195
262
  return
@@ -205,5 +272,15 @@ export async function start() {
205
272
  if (choice === "quit") return
206
273
  cmd = choice
207
274
  }
208
- await dispatch(cmd, flags, interactive)
275
+ let pm
276
+ if (commandNeedsPackageManager(cmd)) {
277
+ pm = await resolveCliPackageManager({
278
+ cwd: process.cwd(),
279
+ env: process.env,
280
+ override: flags.pm,
281
+ interactive,
282
+ })
283
+ if (!pm) return
284
+ }
285
+ await dispatch(cmd, flags, interactive, pm)
209
286
  }
package/cli/grunt.test.ts CHANGED
@@ -10,6 +10,8 @@ const toGruntScriptName = vi.hoisted(
10
10
  () => (k) => (String(k).startsWith("grunt:") ? k : `grunt:${k}`),
11
11
  );
12
12
  const execFileSync = vi.hoisted(() => vi.fn());
13
+ const detectPackageManager = vi.hoisted(() => vi.fn());
14
+ const actualDetect = vi.hoisted(() => ({ fn: null as null | ((...args: unknown[]) => unknown) }));
13
15
  const isInteractive = vi.hoisted(() => vi.fn(() => false));
14
16
  const select = vi.hoisted(() => vi.fn());
15
17
  const confirm = vi.hoisted(() => vi.fn());
@@ -17,6 +19,7 @@ const spinner = vi.hoisted(() =>
17
19
  vi.fn(() => ({ start: vi.fn(), stop: vi.fn() })),
18
20
  );
19
21
  const bailIfCancel = vi.hoisted(() => vi.fn((v) => v));
22
+ const mapCommand = vi.hoisted(() => vi.fn((_cwd: string, dir: string) => `map:${dir}\n`));
20
23
 
21
24
  vi.mock("./init.mjs", () => ({
22
25
  init,
@@ -27,6 +30,17 @@ vi.mock("./init.mjs", () => ({
27
30
  GRUNT_NPM_PREFIX: "grunt:",
28
31
  }));
29
32
  vi.mock("node:child_process", () => ({ execFileSync }));
33
+ vi.mock("../scripts/folder-map.mjs", () => ({ mapCommand }));
34
+ vi.mock("../scripts/package-manager.mjs", async (importOriginal) => {
35
+ const actual = await importOriginal<typeof import("../scripts/package-manager.mjs")>();
36
+ actualDetect.fn = actual.detectPackageManager;
37
+ detectPackageManager.mockImplementation((...args: unknown[]) =>
38
+ actual.detectPackageManager(
39
+ ...(args as Parameters<typeof actual.detectPackageManager>),
40
+ ),
41
+ );
42
+ return { ...actual, detectPackageManager };
43
+ });
30
44
  vi.mock("./prompt.mjs", () => ({
31
45
  isInteractive,
32
46
  select,
@@ -35,6 +49,10 @@ vi.mock("./prompt.mjs", () => ({
35
49
  bailIfCancel,
36
50
  }));
37
51
 
52
+ import {
53
+ PACKAGE_MANAGER_ASK,
54
+ UNKNOWN_PACKAGE_MANAGER,
55
+ } from "../scripts/package-manager.mjs";
38
56
  import { APPLY_GLOBALS_CONFIRM, parseArgv, start } from "./grunt.mjs";
39
57
 
40
58
  const USAGE = `Usage: grunt [command]
@@ -42,14 +60,15 @@ const USAGE = `Usage: grunt [command]
42
60
  Default (no command): TTY menu; else init — full setup
43
61
 
44
62
  Commands:
45
- init Full setup: merge SoT, npm install, grunt:rulesync:generate, grunt:sync:globals:apply, grunt:rulesync:check
46
- generate npm run grunt:rulesync:generate
47
- check npm run grunt:rulesync:check
48
- sync-globals npm run grunt:sync:globals (dry-run; --apply to write)
49
- purge-mcps npm run grunt:purge:global-mcps (dry-run; --apply to write)
50
- doctor npm run grunt:doctor
51
- setup npm run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
63
+ init Full setup: merge SoT, install, grunt:rulesync:generate, grunt:sync:globals:apply, grunt:rulesync:check
64
+ generate run grunt:rulesync:generate
65
+ check run grunt:rulesync:check
66
+ sync-globals run grunt:sync:globals (dry-run; --apply to write)
67
+ purge-mcps run grunt:purge:global-mcps (dry-run; --apply to write)
68
+ doctor run grunt:doctor
69
+ setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
52
70
  upgrade Re-init: copy owned files, prune retired grunt-owned names, print reserved skills
71
+ map [dir] Code-only folder map (git-tracked, no files); dir for depth
53
72
  help Show this help
54
73
  version Print package version
55
74
 
@@ -59,6 +78,7 @@ Flags:
59
78
  --non-interactive Same as --yes
60
79
  --apply Write for sync-globals / purge-mcps
61
80
  --host <id> sync-globals host
81
+ --pm <name> npm | yarn | pnpm | bun (else lockfile, then how grunt was launched, then ask)
62
82
  `;
63
83
 
64
84
  const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -88,6 +108,10 @@ describe("start", () => {
88
108
  shouldAutoSkipGlobals.mockReset();
89
109
  shouldAutoSkipGlobals.mockReturnValue(false);
90
110
  execFileSync.mockReset();
111
+ detectPackageManager.mockReset();
112
+ detectPackageManager.mockImplementation((...args: unknown[]) =>
113
+ (actualDetect.fn as (...a: unknown[]) => unknown)(...args),
114
+ );
91
115
  isInteractive.mockReset();
92
116
  isInteractive.mockReturnValue(false);
93
117
  select.mockReset();
@@ -125,7 +149,10 @@ describe("start", () => {
125
149
  process.argv = ["node", "grunt"];
126
150
  await start();
127
151
  expect(init).toHaveBeenCalledOnce();
128
- expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
152
+ expect(init).toHaveBeenCalledWith(process.cwd(), {
153
+ skipGlobals: false,
154
+ packageManager: "npm",
155
+ });
129
156
  expect(execFileSync).not.toHaveBeenCalled();
130
157
  expect(select).not.toHaveBeenCalled();
131
158
  });
@@ -134,7 +161,10 @@ describe("start", () => {
134
161
  process.argv = ["node", "grunt", "init"];
135
162
  await start();
136
163
  expect(init).toHaveBeenCalledOnce();
137
- expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
164
+ expect(init).toHaveBeenCalledWith(process.cwd(), {
165
+ skipGlobals: false,
166
+ packageManager: "npm",
167
+ });
138
168
  });
139
169
 
140
170
  it.each([
@@ -145,7 +175,10 @@ describe("start", () => {
145
175
  process.argv = argv;
146
176
  await start();
147
177
  expect(init).toHaveBeenCalledOnce();
148
- expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: true });
178
+ expect(init).toHaveBeenCalledWith(process.cwd(), {
179
+ skipGlobals: true,
180
+ packageManager: "npm",
181
+ });
149
182
  });
150
183
 
151
184
  it.each(["--yes", "-y", "--non-interactive"])(
@@ -156,7 +189,10 @@ describe("start", () => {
156
189
  expect(isInteractive).toHaveBeenCalledOnce();
157
190
  expect(select).not.toHaveBeenCalled();
158
191
  expect(init).toHaveBeenCalledOnce();
159
- expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
192
+ expect(init).toHaveBeenCalledWith(process.cwd(), {
193
+ skipGlobals: false,
194
+ packageManager: "npm",
195
+ });
160
196
  },
161
197
  );
162
198
 
@@ -280,11 +316,30 @@ describe("start", () => {
280
316
  process.argv = ["node", "grunt", "upgrade"];
281
317
  await start();
282
318
  expect(init).toHaveBeenCalledOnce();
283
- expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
319
+ expect(init).toHaveBeenCalledWith(process.cwd(), {
320
+ skipGlobals: false,
321
+ packageManager: "npm",
322
+ });
284
323
  expect(chunks.join("")).toBe("reserved: browser tmp write-plan\n");
285
324
  expect(select).not.toHaveBeenCalled();
286
325
  });
287
326
 
327
+ it("map prints the folder map without a package manager", async () => {
328
+ process.argv = ["node", "grunt", "map"];
329
+ await start();
330
+ expect(mapCommand).toHaveBeenLastCalledWith(process.cwd(), "");
331
+ expect(chunks.join("")).toBe("map:\n");
332
+ expect(detectPackageManager).not.toHaveBeenCalled();
333
+ expect(execFileSync).not.toHaveBeenCalled();
334
+ });
335
+
336
+ it("map <dir> scopes the folder map", async () => {
337
+ process.argv = ["node", "grunt", "map", "packages/web"];
338
+ await start();
339
+ expect(mapCommand).toHaveBeenLastCalledWith(process.cwd(), "packages/web");
340
+ expect(chunks.join("")).toBe("map:packages/web\n");
341
+ });
342
+
288
343
  it("unknown writes usage and exitCode 1", async () => {
289
344
  process.argv = ["node", "grunt", "nope"];
290
345
  await start();
@@ -408,6 +463,7 @@ describe("start", () => {
408
463
  expect.objectContaining({
409
464
  skipGlobals: false,
410
465
  applyGlobals: true,
466
+ packageManager: "npm",
411
467
  onPhase: expect.any(Function),
412
468
  }),
413
469
  );
@@ -418,6 +474,84 @@ describe("start", () => {
418
474
  expect(spin.stop).toHaveBeenCalledWith("install");
419
475
  });
420
476
 
477
+ it("init --pm pnpm passes packageManager", async () => {
478
+ process.argv = ["node", "grunt", "init", "--pm", "pnpm"];
479
+ await start();
480
+ expect(init).toHaveBeenCalledWith(process.cwd(), {
481
+ skipGlobals: false,
482
+ packageManager: "pnpm",
483
+ });
484
+ });
485
+
486
+ it("generate --pm pnpm runs pnpm", async () => {
487
+ process.argv = ["node", "grunt", "generate", "--pm", "pnpm"];
488
+ await start();
489
+ expect(execFileSync).toHaveBeenCalledWith("pnpm", ["run", "grunt:rulesync:generate"], {
490
+ cwd: process.cwd(),
491
+ stdio: "inherit",
492
+ });
493
+ });
494
+
495
+ it("setup --pm=yarn extra args omit npm --", async () => {
496
+ process.argv = ["node", "grunt", "setup", "--pm=yarn", "speak"];
497
+ await start();
498
+ expect(execFileSync).toHaveBeenCalledWith("yarn", ["run", "grunt:setup", "speak"], {
499
+ cwd: process.cwd(),
500
+ stdio: "inherit",
501
+ });
502
+ });
503
+
504
+ it("invalid --pm writes usage exit 1", async () => {
505
+ process.argv = ["node", "grunt", "generate", "--pm", "deno"];
506
+ await start();
507
+ expect(chunks.join("")).toBe(USAGE);
508
+ expect(process.exitCode).toBe(1);
509
+ expect(execFileSync).not.toHaveBeenCalled();
510
+ });
511
+
512
+ it("TTY menu then unknown manager asks", async () => {
513
+ detectPackageManager.mockReturnValue({ manager: null, source: null });
514
+ isInteractive.mockReturnValue(true);
515
+ select.mockResolvedValueOnce("generate").mockResolvedValueOnce("pnpm");
516
+ process.argv = ["node", "grunt"];
517
+ await start();
518
+ expect(select).toHaveBeenCalledTimes(2);
519
+ expect(execFileSync).toHaveBeenCalledWith("pnpm", ["run", "grunt:rulesync:generate"], {
520
+ cwd: process.cwd(),
521
+ stdio: "inherit",
522
+ });
523
+ });
524
+
525
+ it("TTY unknown manager asks then runs the choice", async () => {
526
+ detectPackageManager.mockReturnValue({ manager: null, source: null });
527
+ isInteractive.mockReturnValue(true);
528
+ select.mockResolvedValue("bun");
529
+ process.argv = ["node", "grunt", "generate"];
530
+ await start();
531
+ expect(select).toHaveBeenCalledWith({
532
+ message: PACKAGE_MANAGER_ASK,
533
+ options: [
534
+ { value: "npm", label: "npm" },
535
+ { value: "yarn", label: "yarn" },
536
+ { value: "pnpm", label: "pnpm" },
537
+ { value: "bun", label: "bun" },
538
+ ],
539
+ });
540
+ expect(execFileSync).toHaveBeenCalledWith("bun", ["run", "grunt:rulesync:generate"], {
541
+ cwd: process.cwd(),
542
+ stdio: "inherit",
543
+ });
544
+ });
545
+
546
+ it("non-interactive unknown manager writes hint and skips run", async () => {
547
+ detectPackageManager.mockReturnValue({ manager: null, source: null });
548
+ process.argv = ["node", "grunt", "generate"];
549
+ await start();
550
+ expect(chunks.join("")).toBe(`${UNKNOWN_PACKAGE_MANAGER}\n`);
551
+ expect(process.exitCode).toBe(1);
552
+ expect(execFileSync).not.toHaveBeenCalled();
553
+ });
554
+
421
555
  it("TTY init --skip-globals default confirm false maps to skip", async () => {
422
556
  isInteractive.mockReturnValue(true);
423
557
  confirm.mockResolvedValue(false);
@@ -432,6 +566,7 @@ describe("start", () => {
432
566
  expect.objectContaining({
433
567
  skipGlobals: true,
434
568
  applyGlobals: false,
569
+ packageManager: "npm",
435
570
  onPhase: expect.any(Function),
436
571
  }),
437
572
  );
@@ -462,3 +597,24 @@ describe("parseArgv --host", () => {
462
597
  });
463
598
  });
464
599
  });
600
+
601
+ describe("parseArgv --pm", () => {
602
+ it("pair and equals", () => {
603
+ expect(parseArgv(["generate", "--pm", "PNPM"])).toMatchObject({
604
+ cmd: "generate",
605
+ pm: "pnpm",
606
+ pmError: false,
607
+ });
608
+ expect(parseArgv(["generate", "--pm=Yarn"])).toMatchObject({
609
+ pm: "yarn",
610
+ pmError: false,
611
+ });
612
+ });
613
+
614
+ it("missing, empty, flag, and unknown are pmError", () => {
615
+ expect(parseArgv(["generate", "--pm"])).toMatchObject({ pmError: true });
616
+ expect(parseArgv(["generate", "--pm="])).toMatchObject({ pmError: true });
617
+ expect(parseArgv(["generate", "--pm", "--yes"])).toMatchObject({ pmError: true });
618
+ expect(parseArgv(["generate", "--pm", "deno"])).toMatchObject({ pmError: true });
619
+ });
620
+ });