@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.
- package/CHANGELOG.md +13 -0
- package/README.md +32 -22
- package/cli/grunt.mjs +93 -22
- package/cli/grunt.test.ts +149 -12
- package/cli/init.mjs +93 -17
- package/cli/init.test.ts +251 -2
- package/package.json +2 -1
- package/scripts/check-globals.mjs +13 -5
- package/scripts/doctor.mjs +62 -12
- package/scripts/package-manager.mjs +150 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Package manager detection for init/generate/check/doctor/setup: lockfile or `package.json` `"packageManager"`, then how grunt was launched, then TTY ask. `--pm npm|yarn|pnpm|bun` overrides. Non-interactive with no signal fails instead of assuming npm.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Init no longer forces `npm install` / `npm run`
|
|
19
|
+
- Init keeps `@lovrozagar/grunt` in `devDependencies` (moves it out of `dependencies` when present; adds it when missing)
|
|
20
|
+
- Init INDEX/skills-map/refs-map are generated from the dest tree (grunt refs plus consumer extras) instead of copying the package maps
|
|
21
|
+
- Init writes root `.mcp.json` when dest is missing it
|
|
22
|
+
|
|
10
23
|
## [0.6.2] - 2026-09-17
|
|
11
24
|
|
|
12
25
|
### Changed
|
package/README.md
CHANGED
|
@@ -23,23 +23,26 @@ OSS drop-in; merges existing configs; switches default provider flow to Grunt.
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
25
|
- Node.js 22+
|
|
26
|
-
- Consumer:
|
|
26
|
+
- Consumer (devDependency; init moves it there if it landed in `dependencies`):
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
npm i -D @lovrozagar/grunt
|
|
30
|
-
|
|
29
|
+
npm i -D @lovrozagar/grunt && npm exec grunt
|
|
30
|
+
pnpm add -D @lovrozagar/grunt && pnpm exec grunt
|
|
31
|
+
yarn add -D @lovrozagar/grunt && yarn grunt
|
|
32
|
+
bun add -D @lovrozagar/grunt && bunx grunt
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
- Same as `
|
|
34
|
-
- Do not
|
|
35
|
-
- Package: `@lovrozagar/grunt`
|
|
35
|
+
- Same as `grunt init` when no command
|
|
36
|
+
- Do not run this package's test script as a consumer
|
|
37
|
+
- Package: `@lovrozagar/grunt` MIT · https://github.com/lovrozagar/grunt
|
|
38
|
+
- Package manager: lockfile or `package.json` `"packageManager"`, then how grunt was launched, then TTY ask. `--pm npm|yarn|pnpm|bun` overrides. Non-interactive with no signal fails (does not assume npm).
|
|
36
39
|
|
|
37
40
|
## Prerequisites
|
|
38
41
|
|
|
39
42
|
All OS. Print-only. Never auto-install.
|
|
40
43
|
|
|
41
44
|
```
|
|
42
|
-
|
|
45
|
+
grunt doctor
|
|
43
46
|
node scripts/doctor.mjs
|
|
44
47
|
```
|
|
45
48
|
|
|
@@ -47,7 +50,8 @@ Exit 1 if any required missing; 0 if all required ok. Optional tools are reporte
|
|
|
47
50
|
|
|
48
51
|
| tool | required | install |
|
|
49
52
|
| --- | --- | --- |
|
|
50
|
-
| node ≥22
|
|
53
|
+
| node ≥22 | yes | https://nodejs.org (≥22) · nvm / OS pkg · win `winget install OpenJS.NodeJS.LTS` |
|
|
54
|
+
| package manager | yes | npm (ships with Node ≥22) · yarn/pnpm `corepack enable` · bun https://bun.sh · detected from the repo, else how grunt was launched |
|
|
51
55
|
| git | yes | linux `sudo apt install git` · mac `brew install git` · win `winget install Git.Git` |
|
|
52
56
|
| rtk | yes | linux/mac `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh \| sh` or `brew install rtk` · win release zip `rtk.exe` on PATH or WSL curl ([docs](https://www.rtk-ai.app/docs/getting-started/installation/)) |
|
|
53
57
|
| rulesync | yes | `npm i -D rulesync` / npx (PATH or npx-able) |
|
|
@@ -61,7 +65,7 @@ Exit 1 if any required missing; 0 if all required ok. Optional tools are reporte
|
|
|
61
65
|
| whisper-cli | no | local STT for `/listen` · mac `brew install whisper-cpp` · first listen downloads `ggml-base.en.bin` · setup in `.rulesync/reference/listen.md` |
|
|
62
66
|
| listen | no | `node scripts/listen.mjs` · STT input · `grunt setup` / `node scripts/setup.mjs listen` · local whisper.cpp then OpenAI Whisper |
|
|
63
67
|
|
|
64
|
-
Rulesync schema doctor is separate: `
|
|
68
|
+
Rulesync schema doctor is separate: `grunt:rulesync:doctor` (or SoT `rulesync:doctor`).
|
|
65
69
|
|
|
66
70
|
## Usage
|
|
67
71
|
|
|
@@ -73,13 +77,13 @@ Rulesync schema doctor is separate: `npm run grunt:rulesync:doctor`.
|
|
|
73
77
|
|
|
74
78
|
### Commands
|
|
75
79
|
|
|
76
|
-
- `init` → `init()` — merge SoT,
|
|
77
|
-
- `generate` →
|
|
78
|
-
- `check` →
|
|
79
|
-
- `sync-globals` →
|
|
80
|
-
- `purge-mcps` →
|
|
81
|
-
- `doctor` →
|
|
82
|
-
- `setup` →
|
|
80
|
+
- `init` → `init()` — merge SoT, install, `grunt:rulesync:generate`, `grunt:sync:globals:apply`, `grunt:rulesync:check`
|
|
81
|
+
- `generate` → run `grunt:rulesync:generate`
|
|
82
|
+
- `check` → run `grunt:rulesync:check`
|
|
83
|
+
- `sync-globals` → run `grunt:sync:globals` (dry-run); `--apply` → `grunt:sync:globals:apply`
|
|
84
|
+
- `purge-mcps` → run `grunt:purge:global-mcps` (dry-run); `--apply` → `grunt:purge:global-mcps:apply`
|
|
85
|
+
- `doctor` → run `grunt:doctor` (`grunt doctor` / `node scripts/doctor.mjs` stay). Rulesync schema: `grunt:rulesync:doctor`
|
|
86
|
+
- `setup` → run `grunt:setup` (`node scripts/setup.mjs`) — handheld speak / listen / google-workspace / browser. TTY walks each; flags for non-TTY keys/`--creds`
|
|
83
87
|
- `upgrade` → same merge as init for an already-inited repo: copy owned trees/scripts, prune retired grunt-owned names, print reserved skill names
|
|
84
88
|
- `help`
|
|
85
89
|
- `version`
|
|
@@ -90,25 +94,31 @@ Rulesync schema doctor is separate: `npm run grunt:rulesync:doctor`.
|
|
|
90
94
|
- `--yes` / `-y` / `--non-interactive` — no TTY menu; default command still `init`. Not `--apply`
|
|
91
95
|
- `--apply` — write for `sync-globals` / `purge-mcps`
|
|
92
96
|
- `--host <id>` — `sync-globals --host <id>`
|
|
97
|
+
- `--pm <name>` — `npm` | `yarn` | `pnpm` | `bun`
|
|
93
98
|
|
|
94
99
|
## Init
|
|
95
100
|
|
|
96
|
-
- Merge SoT
|
|
101
|
+
- Merge SoT, install with the detected package manager, `grunt:rulesync:generate` `grunt:sync:globals:apply` `grunt:rulesync:check`
|
|
102
|
+
- `@lovrozagar/grunt` is a consumer devDependency. Init moves it out of `dependencies` when it is there, and adds it when missing.
|
|
103
|
+
- INDEX / skills-map / refs-map are generated from the dest `.rulesync` tree (grunt files plus consumer extras). Package copies of those maps are not stamped over dest.
|
|
104
|
+
- Root `.mcp.json` is created when missing; generate still merges from `.rulesync/mcp-policy.jsonc`.
|
|
97
105
|
- `--skip-globals` skips apply
|
|
98
106
|
- TTY init asks `Apply global prompt optimizations? (recommended)` (default Yes; `--skip-globals` defaults No)
|
|
99
107
|
- Non-interactive re-init auto-skips globals when `<!-- grunt:begin -->` in `AGENTS.md`/`CLAUDE.md`
|
|
100
108
|
- First init (no sentinel) applies globals unless flagged
|
|
101
109
|
- Owned trees/scripts refresh; extra `.rulesync` files kept; patches to grunt-owned files lost
|
|
102
110
|
- `cp` cannot delete dest extras. Init/upgrade then prune: retired skills `parent` `solo` `cascade`, agents `implementer` `thinker` (plus `.grok/roles/{implementer,thinker}.toml` and `.gemini/agents/{name}/`), scripts `telemetry.mjs` `grunt-config.mjs`, paths `.grok/parent.md` `.grok/skills/shared` `.rulesync/grunt.config.jsonc` plus local overlay and example, and reserved skill dirs this package no longer ships. Consumer extras stay.
|
|
103
|
-
- Breaking: consumer
|
|
111
|
+
- Breaking: consumer scripts are `grunt:<SoT-key>` (`grunt:rulesync:generate`, `grunt:doctor`). Re-init migrates `package.json` (owned unprefixed keys + suffixes; `npm|yarn|pnpm|bun run` refs in other dest scripts). CI/husky/`run rulesync:*` / `run doctor` must switch. No aliases. SoT repo scripts stay unprefixed (`rulesync:generate`).
|
|
104
112
|
|
|
105
113
|
## Version bump
|
|
106
114
|
|
|
107
115
|
Already-inited consumer (0.5.x → 0.6, and later):
|
|
108
116
|
|
|
109
117
|
```
|
|
110
|
-
npm i -D @lovrozagar/grunt@latest
|
|
111
|
-
|
|
118
|
+
npm i -D @lovrozagar/grunt@latest && npm exec grunt upgrade
|
|
119
|
+
pnpm add -D @lovrozagar/grunt@latest && pnpm exec grunt upgrade
|
|
120
|
+
yarn add -D @lovrozagar/grunt@latest && yarn grunt upgrade
|
|
121
|
+
bun add -D @lovrozagar/grunt@latest && bunx grunt upgrade
|
|
112
122
|
```
|
|
113
123
|
|
|
114
124
|
`upgrade` is init plus a reserved-names print. Same merge, prune, and globals-skip rules as Init. New skills/scripts appear because they are in the package copy list. Dropped grunt-owned files disappear only if they are on the retired lists (or a reserved skill this package no longer ships). Do not expect a blind dest-dir mirror-delete; that would wipe consumer extras.
|
|
@@ -155,7 +165,7 @@ Pipeline (no `-t geminicli`):
|
|
|
155
165
|
3. `emit-gemini.mjs` — `GEMINI.md` `.gemini/agents/{id}/agent.md` MCP `.gemini/settings.json`
|
|
156
166
|
4. `emit-agent-shell-tools` — Claude grunt body `Bash`; other hosts `run_terminal_command` (hooks-union)
|
|
157
167
|
|
|
158
|
-
`check` = rulesync check. `doctor` = unified prereqs. Schema: `
|
|
168
|
+
`check` = rulesync check. `doctor` = unified prereqs. Schema: `rulesync:doctor`.
|
|
159
169
|
|
|
160
170
|
Emit writes other-CLI trees from `.rulesync` for the **next** process of that CLI. Not a live hop into another host.
|
|
161
171
|
|
|
@@ -170,7 +180,7 @@ Emit writes other-CLI trees from `.rulesync` for the **next** process of that CL
|
|
|
170
180
|
|
|
171
181
|
Not git. Not `sync-globals`.
|
|
172
182
|
|
|
173
|
-
First-hand: `
|
|
183
|
+
First-hand: `grunt setup` (TTY). Per target: `node scripts/setup.mjs speak|listen|google-workspace|browser`. Spec: `.rulesync/reference/setup.md`.
|
|
174
184
|
|
|
175
185
|
| | where |
|
|
176
186
|
| --- | --- |
|
package/cli/grunt.mjs
CHANGED
|
@@ -4,6 +4,14 @@ 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"
|
|
7
15
|
|
|
8
16
|
const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
9
17
|
|
|
@@ -12,13 +20,13 @@ const USAGE = `Usage: grunt [command]
|
|
|
12
20
|
Default (no command): TTY menu; else init — full setup
|
|
13
21
|
|
|
14
22
|
Commands:
|
|
15
|
-
init Full setup: merge SoT,
|
|
16
|
-
generate
|
|
17
|
-
check
|
|
18
|
-
sync-globals
|
|
19
|
-
purge-mcps
|
|
20
|
-
doctor
|
|
21
|
-
setup
|
|
23
|
+
init Full setup: merge SoT, install, grunt:rulesync:generate, grunt:sync:globals:apply, grunt:rulesync:check
|
|
24
|
+
generate run grunt:rulesync:generate
|
|
25
|
+
check run grunt:rulesync:check
|
|
26
|
+
sync-globals run grunt:sync:globals (dry-run; --apply to write)
|
|
27
|
+
purge-mcps run grunt:purge:global-mcps (dry-run; --apply to write)
|
|
28
|
+
doctor run grunt:doctor
|
|
29
|
+
setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
|
|
22
30
|
upgrade Re-init: copy owned files, prune retired grunt-owned names, print reserved skills
|
|
23
31
|
help Show this help
|
|
24
32
|
version Print package version
|
|
@@ -29,6 +37,7 @@ Flags:
|
|
|
29
37
|
--non-interactive Same as --yes
|
|
30
38
|
--apply Write for sync-globals / purge-mcps
|
|
31
39
|
--host <id> sync-globals host
|
|
40
|
+
--pm <name> npm | yarn | pnpm | bun (else lockfile, then how grunt was launched, then ask)
|
|
32
41
|
`
|
|
33
42
|
|
|
34
43
|
const MENU_OPTIONS = [
|
|
@@ -45,26 +54,41 @@ const MENU_OPTIONS = [
|
|
|
45
54
|
]
|
|
46
55
|
|
|
47
56
|
const YES_FLAGS = new Set(["--yes", "-y", "--non-interactive"])
|
|
57
|
+
const NEEDS_PM = new Set([
|
|
58
|
+
"init",
|
|
59
|
+
"generate",
|
|
60
|
+
"check",
|
|
61
|
+
"sync-globals",
|
|
62
|
+
"purge-mcps",
|
|
63
|
+
"doctor",
|
|
64
|
+
"setup",
|
|
65
|
+
"upgrade",
|
|
66
|
+
])
|
|
48
67
|
|
|
49
68
|
function pkgVersion() {
|
|
50
69
|
const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"))
|
|
51
70
|
return pkg.version
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
execFileSync("npm", args, { cwd: process.cwd(), stdio: "inherit" })
|
|
73
|
+
function runScript(pm, script, extra = []) {
|
|
74
|
+
execFileSync(pm, runScriptArgs(pm, script, extra), { cwd: process.cwd(), stdio: "inherit" })
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
function hostValueOk(v) {
|
|
60
78
|
return v != null && v !== "" && !String(v).startsWith("-")
|
|
61
79
|
}
|
|
62
80
|
|
|
81
|
+
function pmValueOk(v) {
|
|
82
|
+
return v != null && v !== "" && isPackageManager(v)
|
|
83
|
+
}
|
|
84
|
+
|
|
63
85
|
export function parseArgv(argv) {
|
|
64
86
|
let skipGlobals = false
|
|
65
87
|
let apply = false
|
|
66
88
|
let host
|
|
67
89
|
let hostError = false
|
|
90
|
+
let pm
|
|
91
|
+
let pmError = false
|
|
68
92
|
const positionals = []
|
|
69
93
|
for (let i = 0; i < argv.length; i++) {
|
|
70
94
|
const a = argv[i]
|
|
@@ -92,9 +116,25 @@ export function parseArgv(argv) {
|
|
|
92
116
|
if (!hostValueOk(host)) hostError = true
|
|
93
117
|
continue
|
|
94
118
|
}
|
|
119
|
+
if (a === "--pm") {
|
|
120
|
+
const v = argv[i + 1]
|
|
121
|
+
if (!pmValueOk(v)) {
|
|
122
|
+
pmError = true
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
pm = String(v).toLowerCase()
|
|
126
|
+
i += 1
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (typeof a === "string" && a.startsWith("--pm=")) {
|
|
130
|
+
pm = a.slice("--pm=".length)
|
|
131
|
+
if (!pmValueOk(pm)) pmError = true
|
|
132
|
+
else pm = pm.toLowerCase()
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
95
135
|
positionals.push(a)
|
|
96
136
|
}
|
|
97
|
-
return { cmd: positionals[0], args: positionals.slice(1), skipGlobals, apply, host, hostError }
|
|
137
|
+
return { cmd: positionals[0], args: positionals.slice(1), skipGlobals, apply, host, hostError, pm, pmError }
|
|
98
138
|
}
|
|
99
139
|
|
|
100
140
|
function hostExtra(host) {
|
|
@@ -111,9 +151,23 @@ function bindSpinner() {
|
|
|
111
151
|
|
|
112
152
|
export const APPLY_GLOBALS_CONFIRM = "Apply global prompt optimizations? (recommended)"
|
|
113
153
|
|
|
114
|
-
async function
|
|
154
|
+
export async function resolveCliPackageManager({ cwd, env, override, interactive }) {
|
|
155
|
+
const detected = detectPackageManager({ cwd, env, override })
|
|
156
|
+
if (detected.manager) return detected.manager
|
|
157
|
+
if (interactive) {
|
|
158
|
+
return await select({
|
|
159
|
+
message: PACKAGE_MANAGER_ASK,
|
|
160
|
+
options: PACKAGE_MANAGER_OPTIONS,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
process.stdout.write(`${UNKNOWN_PACKAGE_MANAGER}\n`)
|
|
164
|
+
process.exitCode = 1
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function runInit(cwd, { skipGlobals, interactive, packageManager }) {
|
|
115
169
|
if (!interactive) {
|
|
116
|
-
init(cwd, { skipGlobals })
|
|
170
|
+
init(cwd, { skipGlobals, packageManager })
|
|
117
171
|
return
|
|
118
172
|
}
|
|
119
173
|
if (destAlreadyInited(cwd)) {
|
|
@@ -131,10 +185,11 @@ async function runInit(cwd, { skipGlobals, interactive }) {
|
|
|
131
185
|
skipGlobals: !applyGlobals,
|
|
132
186
|
applyGlobals,
|
|
133
187
|
onPhase: bindSpinner(),
|
|
188
|
+
packageManager,
|
|
134
189
|
})
|
|
135
190
|
}
|
|
136
191
|
|
|
137
|
-
async function dispatch(cmd, flags, interactive) {
|
|
192
|
+
async function dispatch(cmd, flags, interactive, pm) {
|
|
138
193
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
139
194
|
process.stdout.write(USAGE)
|
|
140
195
|
return
|
|
@@ -147,38 +202,40 @@ async function dispatch(cmd, flags, interactive) {
|
|
|
147
202
|
await runInit(process.cwd(), {
|
|
148
203
|
skipGlobals: flags.skipGlobals,
|
|
149
204
|
interactive: interactive && (!cmd || cmd === "init"),
|
|
205
|
+
packageManager: pm,
|
|
150
206
|
})
|
|
151
207
|
return
|
|
152
208
|
}
|
|
153
209
|
if (cmd === "generate") {
|
|
154
|
-
|
|
210
|
+
runScript(pm, toGruntScriptName("rulesync:generate"))
|
|
155
211
|
return
|
|
156
212
|
}
|
|
157
213
|
if (cmd === "check") {
|
|
158
|
-
|
|
214
|
+
runScript(pm, toGruntScriptName("rulesync:check"))
|
|
159
215
|
return
|
|
160
216
|
}
|
|
161
217
|
if (cmd === "sync-globals") {
|
|
162
218
|
const script = flags.apply ? toGruntScriptName("sync:globals:apply") : toGruntScriptName("sync:globals")
|
|
163
|
-
|
|
219
|
+
runScript(pm, script, hostExtra(flags.host))
|
|
164
220
|
return
|
|
165
221
|
}
|
|
166
222
|
if (cmd === "purge-mcps") {
|
|
167
|
-
|
|
223
|
+
runScript(pm, flags.apply ? toGruntScriptName("purge:global-mcps:apply") : toGruntScriptName("purge:global-mcps"))
|
|
168
224
|
return
|
|
169
225
|
}
|
|
170
226
|
if (cmd === "doctor") {
|
|
171
|
-
|
|
227
|
+
runScript(pm, toGruntScriptName("doctor"))
|
|
172
228
|
return
|
|
173
229
|
}
|
|
174
230
|
if (cmd === "setup") {
|
|
175
|
-
|
|
231
|
+
runScript(pm, toGruntScriptName("setup"), flags.args)
|
|
176
232
|
return
|
|
177
233
|
}
|
|
178
234
|
if (cmd === "upgrade") {
|
|
179
235
|
await runInit(process.cwd(), {
|
|
180
236
|
skipGlobals: flags.skipGlobals,
|
|
181
237
|
interactive: false,
|
|
238
|
+
packageManager: pm,
|
|
182
239
|
})
|
|
183
240
|
process.stdout.write(`reserved: ${RESERVED_SKILLS.join(" ")}\n`)
|
|
184
241
|
return
|
|
@@ -187,9 +244,13 @@ async function dispatch(cmd, flags, interactive) {
|
|
|
187
244
|
process.exitCode = 1
|
|
188
245
|
}
|
|
189
246
|
|
|
247
|
+
function commandNeedsPackageManager(cmd) {
|
|
248
|
+
return !cmd || NEEDS_PM.has(cmd)
|
|
249
|
+
}
|
|
250
|
+
|
|
190
251
|
export async function start() {
|
|
191
252
|
const flags = parseArgv(process.argv.slice(2))
|
|
192
|
-
if (flags.hostError) {
|
|
253
|
+
if (flags.hostError || flags.pmError) {
|
|
193
254
|
process.stdout.write(USAGE)
|
|
194
255
|
process.exitCode = 1
|
|
195
256
|
return
|
|
@@ -205,5 +266,15 @@ export async function start() {
|
|
|
205
266
|
if (choice === "quit") return
|
|
206
267
|
cmd = choice
|
|
207
268
|
}
|
|
208
|
-
|
|
269
|
+
let pm
|
|
270
|
+
if (commandNeedsPackageManager(cmd)) {
|
|
271
|
+
pm = await resolveCliPackageManager({
|
|
272
|
+
cwd: process.cwd(),
|
|
273
|
+
env: process.env,
|
|
274
|
+
override: flags.pm,
|
|
275
|
+
interactive,
|
|
276
|
+
})
|
|
277
|
+
if (!pm) return
|
|
278
|
+
}
|
|
279
|
+
await dispatch(cmd, flags, interactive, pm)
|
|
209
280
|
}
|
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());
|
|
@@ -27,6 +29,16 @@ vi.mock("./init.mjs", () => ({
|
|
|
27
29
|
GRUNT_NPM_PREFIX: "grunt:",
|
|
28
30
|
}));
|
|
29
31
|
vi.mock("node:child_process", () => ({ execFileSync }));
|
|
32
|
+
vi.mock("../scripts/package-manager.mjs", async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal<typeof import("../scripts/package-manager.mjs")>();
|
|
34
|
+
actualDetect.fn = actual.detectPackageManager;
|
|
35
|
+
detectPackageManager.mockImplementation((...args: unknown[]) =>
|
|
36
|
+
actual.detectPackageManager(
|
|
37
|
+
...(args as Parameters<typeof actual.detectPackageManager>),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
return { ...actual, detectPackageManager };
|
|
41
|
+
});
|
|
30
42
|
vi.mock("./prompt.mjs", () => ({
|
|
31
43
|
isInteractive,
|
|
32
44
|
select,
|
|
@@ -35,6 +47,10 @@ vi.mock("./prompt.mjs", () => ({
|
|
|
35
47
|
bailIfCancel,
|
|
36
48
|
}));
|
|
37
49
|
|
|
50
|
+
import {
|
|
51
|
+
PACKAGE_MANAGER_ASK,
|
|
52
|
+
UNKNOWN_PACKAGE_MANAGER,
|
|
53
|
+
} from "../scripts/package-manager.mjs";
|
|
38
54
|
import { APPLY_GLOBALS_CONFIRM, parseArgv, start } from "./grunt.mjs";
|
|
39
55
|
|
|
40
56
|
const USAGE = `Usage: grunt [command]
|
|
@@ -42,13 +58,13 @@ const USAGE = `Usage: grunt [command]
|
|
|
42
58
|
Default (no command): TTY menu; else init — full setup
|
|
43
59
|
|
|
44
60
|
Commands:
|
|
45
|
-
init Full setup: merge SoT,
|
|
46
|
-
generate
|
|
47
|
-
check
|
|
48
|
-
sync-globals
|
|
49
|
-
purge-mcps
|
|
50
|
-
doctor
|
|
51
|
-
setup
|
|
61
|
+
init Full setup: merge SoT, install, grunt:rulesync:generate, grunt:sync:globals:apply, grunt:rulesync:check
|
|
62
|
+
generate run grunt:rulesync:generate
|
|
63
|
+
check run grunt:rulesync:check
|
|
64
|
+
sync-globals run grunt:sync:globals (dry-run; --apply to write)
|
|
65
|
+
purge-mcps run grunt:purge:global-mcps (dry-run; --apply to write)
|
|
66
|
+
doctor run grunt:doctor
|
|
67
|
+
setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
|
|
52
68
|
upgrade Re-init: copy owned files, prune retired grunt-owned names, print reserved skills
|
|
53
69
|
help Show this help
|
|
54
70
|
version Print package version
|
|
@@ -59,6 +75,7 @@ Flags:
|
|
|
59
75
|
--non-interactive Same as --yes
|
|
60
76
|
--apply Write for sync-globals / purge-mcps
|
|
61
77
|
--host <id> sync-globals host
|
|
78
|
+
--pm <name> npm | yarn | pnpm | bun (else lockfile, then how grunt was launched, then ask)
|
|
62
79
|
`;
|
|
63
80
|
|
|
64
81
|
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -88,6 +105,10 @@ describe("start", () => {
|
|
|
88
105
|
shouldAutoSkipGlobals.mockReset();
|
|
89
106
|
shouldAutoSkipGlobals.mockReturnValue(false);
|
|
90
107
|
execFileSync.mockReset();
|
|
108
|
+
detectPackageManager.mockReset();
|
|
109
|
+
detectPackageManager.mockImplementation((...args: unknown[]) =>
|
|
110
|
+
(actualDetect.fn as (...a: unknown[]) => unknown)(...args),
|
|
111
|
+
);
|
|
91
112
|
isInteractive.mockReset();
|
|
92
113
|
isInteractive.mockReturnValue(false);
|
|
93
114
|
select.mockReset();
|
|
@@ -125,7 +146,10 @@ describe("start", () => {
|
|
|
125
146
|
process.argv = ["node", "grunt"];
|
|
126
147
|
await start();
|
|
127
148
|
expect(init).toHaveBeenCalledOnce();
|
|
128
|
-
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
149
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
150
|
+
skipGlobals: false,
|
|
151
|
+
packageManager: "npm",
|
|
152
|
+
});
|
|
129
153
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
130
154
|
expect(select).not.toHaveBeenCalled();
|
|
131
155
|
});
|
|
@@ -134,7 +158,10 @@ describe("start", () => {
|
|
|
134
158
|
process.argv = ["node", "grunt", "init"];
|
|
135
159
|
await start();
|
|
136
160
|
expect(init).toHaveBeenCalledOnce();
|
|
137
|
-
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
161
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
162
|
+
skipGlobals: false,
|
|
163
|
+
packageManager: "npm",
|
|
164
|
+
});
|
|
138
165
|
});
|
|
139
166
|
|
|
140
167
|
it.each([
|
|
@@ -145,7 +172,10 @@ describe("start", () => {
|
|
|
145
172
|
process.argv = argv;
|
|
146
173
|
await start();
|
|
147
174
|
expect(init).toHaveBeenCalledOnce();
|
|
148
|
-
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
175
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
176
|
+
skipGlobals: true,
|
|
177
|
+
packageManager: "npm",
|
|
178
|
+
});
|
|
149
179
|
});
|
|
150
180
|
|
|
151
181
|
it.each(["--yes", "-y", "--non-interactive"])(
|
|
@@ -156,7 +186,10 @@ describe("start", () => {
|
|
|
156
186
|
expect(isInteractive).toHaveBeenCalledOnce();
|
|
157
187
|
expect(select).not.toHaveBeenCalled();
|
|
158
188
|
expect(init).toHaveBeenCalledOnce();
|
|
159
|
-
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
189
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
190
|
+
skipGlobals: false,
|
|
191
|
+
packageManager: "npm",
|
|
192
|
+
});
|
|
160
193
|
},
|
|
161
194
|
);
|
|
162
195
|
|
|
@@ -280,7 +313,10 @@ describe("start", () => {
|
|
|
280
313
|
process.argv = ["node", "grunt", "upgrade"];
|
|
281
314
|
await start();
|
|
282
315
|
expect(init).toHaveBeenCalledOnce();
|
|
283
|
-
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
316
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
317
|
+
skipGlobals: false,
|
|
318
|
+
packageManager: "npm",
|
|
319
|
+
});
|
|
284
320
|
expect(chunks.join("")).toBe("reserved: browser tmp write-plan\n");
|
|
285
321
|
expect(select).not.toHaveBeenCalled();
|
|
286
322
|
});
|
|
@@ -408,6 +444,7 @@ describe("start", () => {
|
|
|
408
444
|
expect.objectContaining({
|
|
409
445
|
skipGlobals: false,
|
|
410
446
|
applyGlobals: true,
|
|
447
|
+
packageManager: "npm",
|
|
411
448
|
onPhase: expect.any(Function),
|
|
412
449
|
}),
|
|
413
450
|
);
|
|
@@ -418,6 +455,84 @@ describe("start", () => {
|
|
|
418
455
|
expect(spin.stop).toHaveBeenCalledWith("install");
|
|
419
456
|
});
|
|
420
457
|
|
|
458
|
+
it("init --pm pnpm passes packageManager", async () => {
|
|
459
|
+
process.argv = ["node", "grunt", "init", "--pm", "pnpm"];
|
|
460
|
+
await start();
|
|
461
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
462
|
+
skipGlobals: false,
|
|
463
|
+
packageManager: "pnpm",
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("generate --pm pnpm runs pnpm", async () => {
|
|
468
|
+
process.argv = ["node", "grunt", "generate", "--pm", "pnpm"];
|
|
469
|
+
await start();
|
|
470
|
+
expect(execFileSync).toHaveBeenCalledWith("pnpm", ["run", "grunt:rulesync:generate"], {
|
|
471
|
+
cwd: process.cwd(),
|
|
472
|
+
stdio: "inherit",
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("setup --pm=yarn extra args omit npm --", async () => {
|
|
477
|
+
process.argv = ["node", "grunt", "setup", "--pm=yarn", "speak"];
|
|
478
|
+
await start();
|
|
479
|
+
expect(execFileSync).toHaveBeenCalledWith("yarn", ["run", "grunt:setup", "speak"], {
|
|
480
|
+
cwd: process.cwd(),
|
|
481
|
+
stdio: "inherit",
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("invalid --pm writes usage exit 1", async () => {
|
|
486
|
+
process.argv = ["node", "grunt", "generate", "--pm", "deno"];
|
|
487
|
+
await start();
|
|
488
|
+
expect(chunks.join("")).toBe(USAGE);
|
|
489
|
+
expect(process.exitCode).toBe(1);
|
|
490
|
+
expect(execFileSync).not.toHaveBeenCalled();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("TTY menu then unknown manager asks", async () => {
|
|
494
|
+
detectPackageManager.mockReturnValue({ manager: null, source: null });
|
|
495
|
+
isInteractive.mockReturnValue(true);
|
|
496
|
+
select.mockResolvedValueOnce("generate").mockResolvedValueOnce("pnpm");
|
|
497
|
+
process.argv = ["node", "grunt"];
|
|
498
|
+
await start();
|
|
499
|
+
expect(select).toHaveBeenCalledTimes(2);
|
|
500
|
+
expect(execFileSync).toHaveBeenCalledWith("pnpm", ["run", "grunt:rulesync:generate"], {
|
|
501
|
+
cwd: process.cwd(),
|
|
502
|
+
stdio: "inherit",
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("TTY unknown manager asks then runs the choice", async () => {
|
|
507
|
+
detectPackageManager.mockReturnValue({ manager: null, source: null });
|
|
508
|
+
isInteractive.mockReturnValue(true);
|
|
509
|
+
select.mockResolvedValue("bun");
|
|
510
|
+
process.argv = ["node", "grunt", "generate"];
|
|
511
|
+
await start();
|
|
512
|
+
expect(select).toHaveBeenCalledWith({
|
|
513
|
+
message: PACKAGE_MANAGER_ASK,
|
|
514
|
+
options: [
|
|
515
|
+
{ value: "npm", label: "npm" },
|
|
516
|
+
{ value: "yarn", label: "yarn" },
|
|
517
|
+
{ value: "pnpm", label: "pnpm" },
|
|
518
|
+
{ value: "bun", label: "bun" },
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
expect(execFileSync).toHaveBeenCalledWith("bun", ["run", "grunt:rulesync:generate"], {
|
|
522
|
+
cwd: process.cwd(),
|
|
523
|
+
stdio: "inherit",
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("non-interactive unknown manager writes hint and skips run", async () => {
|
|
528
|
+
detectPackageManager.mockReturnValue({ manager: null, source: null });
|
|
529
|
+
process.argv = ["node", "grunt", "generate"];
|
|
530
|
+
await start();
|
|
531
|
+
expect(chunks.join("")).toBe(`${UNKNOWN_PACKAGE_MANAGER}\n`);
|
|
532
|
+
expect(process.exitCode).toBe(1);
|
|
533
|
+
expect(execFileSync).not.toHaveBeenCalled();
|
|
534
|
+
});
|
|
535
|
+
|
|
421
536
|
it("TTY init --skip-globals default confirm false maps to skip", async () => {
|
|
422
537
|
isInteractive.mockReturnValue(true);
|
|
423
538
|
confirm.mockResolvedValue(false);
|
|
@@ -432,6 +547,7 @@ describe("start", () => {
|
|
|
432
547
|
expect.objectContaining({
|
|
433
548
|
skipGlobals: true,
|
|
434
549
|
applyGlobals: false,
|
|
550
|
+
packageManager: "npm",
|
|
435
551
|
onPhase: expect.any(Function),
|
|
436
552
|
}),
|
|
437
553
|
);
|
|
@@ -462,3 +578,24 @@ describe("parseArgv --host", () => {
|
|
|
462
578
|
});
|
|
463
579
|
});
|
|
464
580
|
});
|
|
581
|
+
|
|
582
|
+
describe("parseArgv --pm", () => {
|
|
583
|
+
it("pair and equals", () => {
|
|
584
|
+
expect(parseArgv(["generate", "--pm", "PNPM"])).toMatchObject({
|
|
585
|
+
cmd: "generate",
|
|
586
|
+
pm: "pnpm",
|
|
587
|
+
pmError: false,
|
|
588
|
+
});
|
|
589
|
+
expect(parseArgv(["generate", "--pm=Yarn"])).toMatchObject({
|
|
590
|
+
pm: "yarn",
|
|
591
|
+
pmError: false,
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("missing, empty, flag, and unknown are pmError", () => {
|
|
596
|
+
expect(parseArgv(["generate", "--pm"])).toMatchObject({ pmError: true });
|
|
597
|
+
expect(parseArgv(["generate", "--pm="])).toMatchObject({ pmError: true });
|
|
598
|
+
expect(parseArgv(["generate", "--pm", "--yes"])).toMatchObject({ pmError: true });
|
|
599
|
+
expect(parseArgv(["generate", "--pm", "deno"])).toMatchObject({ pmError: true });
|
|
600
|
+
});
|
|
601
|
+
});
|