@lovrozagar/grunt 0.6.0 → 0.6.2
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/.rulesync/reference/map.md +1 -0
- package/CHANGELOG.md +22 -0
- package/README.md +3 -2
- package/cli/grunt.mjs +6 -5
- package/cli/grunt.test.ts +16 -4
- package/cli/init.mjs +31 -224
- package/cli/init.test.ts +99 -28
- package/cli/prompt.mjs +9 -44
- package/package.json +4 -1
- package/scripts/browser.mjs +30 -2
- package/scripts/check-globals.mjs +5 -2
- package/scripts/doctor.mjs +22 -9
- package/scripts/emit-gemini.mjs +1 -1
- package/scripts/emit-mcp-policy.mjs +13 -7
- package/scripts/guarded-md.mjs +230 -0
- package/scripts/guarded-roots.mjs +1 -1
- package/scripts/interactive.mjs +15 -0
- package/scripts/prompt.mjs +32 -0
- package/scripts/setup.mjs +2 -2
|
@@ -36,6 +36,7 @@ Cheap outline. Not a file dump.
|
|
|
36
36
|
|
|
37
37
|
## Scripts
|
|
38
38
|
- `scripts/pipeline.mjs` — inner generate/check/watch chain (rulesync + emit-* + hooks-union / check-globals). Called by `guarded-roots`; not a public npm script (`rulesync:generate` `rulesync:check` `rulesync:watch` only)
|
|
39
|
+
- `scripts/guarded-md.mjs` — sentinel merge for `AGENTS.md` `CLAUDE.md` `GEMINI.md`; copied into consumers
|
|
39
40
|
- `scripts/guarded-roots.mjs` — snapshot/remerge `AGENTS.md` `CLAUDE.md` `GEMINI.md` around pipeline; check interiors wrapper
|
|
40
41
|
- `scripts/check-globals.mjs` — `$HOME/.grok/config.toml` `[agent].name==orchestrator` + `[features].two_pass_compaction==true`
|
|
41
42
|
- `scripts/gate-fat-tools.mjs`
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.2] - 2026-09-17
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- TTY init defaults `Apply global prompt optimizations? (recommended)` to Yes (`--skip-globals` still defaults No)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Init in `$HOME` no longer fails `emit-mcp-policy --check` after globals write `[features]` into the same `~/.grok/config.toml`
|
|
19
|
+
- TTY init spinner `stop` keeps the phase name (no empty `◇`)
|
|
20
|
+
|
|
21
|
+
## [0.6.1] - 2026-09-17
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- TTY init asks `Apply global prompt optimizations? (recommended)` instead of `Apply globals?`
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Windows tests: host `whichBin` PATHEXT/`F_OK`, cmd shims, and `path.resolve` scratch paths
|
|
30
|
+
- Consumer `init` generate: product scripts imported `../cli/` which is not copied. Sentinel helpers live in `scripts/guarded-md.mjs`; setup prompts live in `scripts/prompt.mjs` / `scripts/interactive.mjs`. After init, tests spawn `node ./scripts/guarded-roots.mjs generate` and require every copied script's relative imports to exist on dest
|
|
31
|
+
|
|
10
32
|
## [0.6.0] - 2026-09-17
|
|
11
33
|
|
|
12
34
|
### Added
|
package/README.md
CHANGED
|
@@ -95,7 +95,8 @@ Rulesync schema doctor is separate: `npm run grunt:rulesync:doctor`.
|
|
|
95
95
|
|
|
96
96
|
- Merge SoT `npm install` `grunt:rulesync:generate` `grunt:sync:globals:apply` `grunt:rulesync:check`
|
|
97
97
|
- `--skip-globals` skips apply
|
|
98
|
-
-
|
|
98
|
+
- TTY init asks `Apply global prompt optimizations? (recommended)` (default Yes; `--skip-globals` defaults No)
|
|
99
|
+
- Non-interactive re-init auto-skips globals when `<!-- grunt:begin -->` in `AGENTS.md`/`CLAUDE.md`
|
|
99
100
|
- First init (no sentinel) applies globals unless flagged
|
|
100
101
|
- Owned trees/scripts refresh; extra `.rulesync` files kept; patches to grunt-owned files lost
|
|
101
102
|
- `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.
|
|
@@ -346,7 +347,7 @@ Repo-relative (repository root):
|
|
|
346
347
|
|
|
347
348
|
## Layout
|
|
348
349
|
|
|
349
|
-
Published (`package.json` `files`): `bin/grunt.js` `cli` `scripts/check-globals.mjs` `scripts/emit-agent-shell-tools.mjs` `scripts/emit-gemini.mjs` `scripts/guarded-roots.mjs` `scripts/emit-mcp-policy.mjs` `scripts/gate-fat-tools.mjs` `scripts/hooks-union.mjs` `scripts/pipeline.mjs` `scripts/grunt-job.mjs` `scripts/parse-need.mjs` `scripts/persist-handoff.mjs` `scripts/persist-implementation.mjs` `scripts/persist-tmp.mjs` `scripts/persist-plan.mjs` `scripts/purge-global-mcps.mjs` `scripts/scrub-spawn-prompt.mjs` `scripts/scrub-text-lib.mjs` `scripts/sync-global-settings.mjs` `scripts/browser.mjs` `scripts/speak.mjs` `scripts/listen.mjs` `scripts/google-workspace.mjs` `scripts/setup.mjs` `scripts/doctor.mjs` `scripts/scrub-text` `.rulesync` `.grok` `.codex` `.claude` `.agents` `AGENTS.md` `CLAUDE.md` `.mcp.json` `README.md` `LICENSE` `CHANGELOG.md`
|
|
350
|
+
Published (`package.json` `files`): `bin/grunt.js` `cli` `scripts/check-globals.mjs` `scripts/emit-agent-shell-tools.mjs` `scripts/emit-gemini.mjs` `scripts/emit-maps.mjs` `scripts/guarded-md.mjs` `scripts/guarded-roots.mjs` `scripts/emit-mcp-policy.mjs` `scripts/gate-fat-tools.mjs` `scripts/hooks-union.mjs` `scripts/pipeline.mjs` `scripts/grunt-job.mjs` `scripts/parse-need.mjs` `scripts/persist-handoff.mjs` `scripts/persist-implementation.mjs` `scripts/persist-tmp.mjs` `scripts/persist-plan.mjs` `scripts/purge-global-mcps.mjs` `scripts/scrub-spawn-prompt.mjs` `scripts/scrub-text-lib.mjs` `scripts/sync-global-settings.mjs` `scripts/browser.mjs` `scripts/speak.mjs` `scripts/listen.mjs` `scripts/google-workspace.mjs` `scripts/interactive.mjs` `scripts/prompt.mjs` `scripts/setup.mjs` `scripts/doctor.mjs` `scripts/skill-conflicts.mjs` `scripts/scrub-text` `.rulesync` `.grok` `.codex` `.claude` `.agents` `AGENTS.md` `CLAUDE.md` `.mcp.json` `README.md` `LICENSE` `CHANGELOG.md`
|
|
350
351
|
|
|
351
352
|
No `scripts/*.test.ts` `scripts/fixtures/` `docs/` `coverage/` `vitest.config.ts` in `files`. `cli` dir ships whole (includes `cli/*.test.ts`).
|
|
352
353
|
|
package/cli/grunt.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process"
|
|
|
2
2
|
import { readFileSync } from "node:fs"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { fileURLToPath } from "node:url"
|
|
5
|
-
import { destAlreadyInited, init, RESERVED_SKILLS,
|
|
5
|
+
import { destAlreadyInited, init, RESERVED_SKILLS, toGruntScriptName } from "./init.mjs"
|
|
6
6
|
import { confirm, isInteractive, select, spinner } from "./prompt.mjs"
|
|
7
7
|
|
|
8
8
|
const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
@@ -105,10 +105,12 @@ function bindSpinner() {
|
|
|
105
105
|
const spin = spinner()
|
|
106
106
|
return (name, action) => {
|
|
107
107
|
if (action === "start") spin.start(name)
|
|
108
|
-
else if (action === "stop") spin.stop()
|
|
108
|
+
else if (action === "stop") spin.stop(name)
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
export const APPLY_GLOBALS_CONFIRM = "Apply global prompt optimizations? (recommended)"
|
|
113
|
+
|
|
112
114
|
async function runInit(cwd, { skipGlobals, interactive }) {
|
|
113
115
|
if (!interactive) {
|
|
114
116
|
init(cwd, { skipGlobals })
|
|
@@ -121,10 +123,9 @@ async function runInit(cwd, { skipGlobals, interactive }) {
|
|
|
121
123
|
})
|
|
122
124
|
if (!again) return
|
|
123
125
|
}
|
|
124
|
-
const autoSkip = skipGlobals || shouldAutoSkipGlobals(cwd)
|
|
125
126
|
const applyGlobals = await confirm({
|
|
126
|
-
message:
|
|
127
|
-
initialValue: !
|
|
127
|
+
message: APPLY_GLOBALS_CONFIRM,
|
|
128
|
+
initialValue: !skipGlobals,
|
|
128
129
|
})
|
|
129
130
|
init(cwd, {
|
|
130
131
|
skipGlobals: !applyGlobals,
|
package/cli/grunt.test.ts
CHANGED
|
@@ -35,7 +35,7 @@ vi.mock("./prompt.mjs", () => ({
|
|
|
35
35
|
bailIfCancel,
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
|
-
import { parseArgv, start } from "./grunt.mjs";
|
|
38
|
+
import { APPLY_GLOBALS_CONFIRM, parseArgv, start } from "./grunt.mjs";
|
|
39
39
|
|
|
40
40
|
const USAGE = `Usage: grunt [command]
|
|
41
41
|
|
|
@@ -320,6 +320,10 @@ describe("start", () => {
|
|
|
320
320
|
]);
|
|
321
321
|
expect(init).toHaveBeenCalled();
|
|
322
322
|
expect(isInteractive).toHaveBeenCalledOnce();
|
|
323
|
+
expect(confirm).toHaveBeenCalledWith({
|
|
324
|
+
message: APPLY_GLOBALS_CONFIRM,
|
|
325
|
+
initialValue: true,
|
|
326
|
+
});
|
|
323
327
|
});
|
|
324
328
|
|
|
325
329
|
it("TTY menu generate", async () => {
|
|
@@ -393,9 +397,12 @@ describe("start", () => {
|
|
|
393
397
|
await start();
|
|
394
398
|
expect(confirm).toHaveBeenCalled();
|
|
395
399
|
const globalsConfirm = confirm.mock.calls.find(
|
|
396
|
-
(c) =>
|
|
400
|
+
(c) => (c[0] as { message?: string }).message === APPLY_GLOBALS_CONFIRM,
|
|
397
401
|
);
|
|
398
|
-
expect(globalsConfirm?.[0]).toMatchObject({
|
|
402
|
+
expect(globalsConfirm?.[0]).toMatchObject({
|
|
403
|
+
message: APPLY_GLOBALS_CONFIRM,
|
|
404
|
+
initialValue: true,
|
|
405
|
+
});
|
|
399
406
|
expect(init).toHaveBeenCalledWith(
|
|
400
407
|
process.cwd(),
|
|
401
408
|
expect.objectContaining({
|
|
@@ -406,8 +413,9 @@ describe("start", () => {
|
|
|
406
413
|
);
|
|
407
414
|
expect(isInteractive).toHaveBeenCalledOnce();
|
|
408
415
|
expect(spin.start).toHaveBeenCalledWith("merge");
|
|
409
|
-
expect(spin.stop).
|
|
416
|
+
expect(spin.stop).toHaveBeenCalledWith("merge");
|
|
410
417
|
expect(spin.start).toHaveBeenCalledWith("install");
|
|
418
|
+
expect(spin.stop).toHaveBeenCalledWith("install");
|
|
411
419
|
});
|
|
412
420
|
|
|
413
421
|
it("TTY init --skip-globals default confirm false maps to skip", async () => {
|
|
@@ -415,6 +423,10 @@ describe("start", () => {
|
|
|
415
423
|
confirm.mockResolvedValue(false);
|
|
416
424
|
process.argv = ["node", "grunt", "init", "--skip-globals"];
|
|
417
425
|
await start();
|
|
426
|
+
expect(confirm).toHaveBeenCalledWith({
|
|
427
|
+
message: APPLY_GLOBALS_CONFIRM,
|
|
428
|
+
initialValue: false,
|
|
429
|
+
});
|
|
418
430
|
expect(init).toHaveBeenCalledWith(
|
|
419
431
|
process.cwd(),
|
|
420
432
|
expect.objectContaining({
|
package/cli/init.mjs
CHANGED
|
@@ -8,14 +8,40 @@ import {
|
|
|
8
8
|
formatSkillConflictWarn,
|
|
9
9
|
listSkillDirNames,
|
|
10
10
|
} from "../scripts/skill-conflicts.mjs"
|
|
11
|
+
import {
|
|
12
|
+
SENTINEL_BEGIN,
|
|
13
|
+
extractGruntBody,
|
|
14
|
+
writeMergedGuardedFile,
|
|
15
|
+
snapshotGuardedRoots,
|
|
16
|
+
remergeGuardedRoots,
|
|
17
|
+
withGuardedCheckInteriors,
|
|
18
|
+
} from "../scripts/guarded-md.mjs"
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
GUARDED_ROOT_FILES,
|
|
22
|
+
SENTINEL_BEGIN,
|
|
23
|
+
SENTINEL_END,
|
|
24
|
+
MAX_GUARDED_MARKDOWN_BYTES,
|
|
25
|
+
extractUserMarkdown,
|
|
26
|
+
extractGruntBody,
|
|
27
|
+
composeGuardedMarkdown,
|
|
28
|
+
mergeGuardedContent,
|
|
29
|
+
writeMergedGuardedFile,
|
|
30
|
+
guardedMarkdownDrift,
|
|
31
|
+
snapshotGuardedRoots,
|
|
32
|
+
remergeGuardedRoots,
|
|
33
|
+
healGuardedRootFile,
|
|
34
|
+
withGuardedCheckInteriors,
|
|
35
|
+
} from "../scripts/guarded-md.mjs"
|
|
11
36
|
|
|
12
37
|
const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
13
38
|
|
|
14
|
-
const PRODUCT_SCRIPTS = [
|
|
39
|
+
export const PRODUCT_SCRIPTS = [
|
|
15
40
|
"check-globals.mjs",
|
|
16
41
|
"emit-agent-shell-tools.mjs",
|
|
17
42
|
"emit-gemini.mjs",
|
|
18
43
|
"emit-maps.mjs",
|
|
44
|
+
"guarded-md.mjs",
|
|
19
45
|
"guarded-roots.mjs",
|
|
20
46
|
"emit-mcp-policy.mjs",
|
|
21
47
|
"gate-fat-tools.mjs",
|
|
@@ -35,6 +61,8 @@ const PRODUCT_SCRIPTS = [
|
|
|
35
61
|
"speak.mjs",
|
|
36
62
|
"listen.mjs",
|
|
37
63
|
"google-workspace.mjs",
|
|
64
|
+
"interactive.mjs",
|
|
65
|
+
"prompt.mjs",
|
|
38
66
|
"setup.mjs",
|
|
39
67
|
"doctor.mjs",
|
|
40
68
|
"skill-conflicts.mjs",
|
|
@@ -88,7 +116,6 @@ const AGENT_MIRROR_DIRS = [
|
|
|
88
116
|
[".codex", "agents"],
|
|
89
117
|
]
|
|
90
118
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"]
|
|
91
|
-
export const GUARDED_ROOT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]
|
|
92
119
|
// .mcp.json is intentionally not copied here: scripts/emit-mcp-policy.mjs
|
|
93
120
|
// (run via consumer `grunt:rulesync:generate`, see mergeClaudeSettings ordering) owns and
|
|
94
121
|
// merges it, so a plain-copy would just be clobbered/redundant.
|
|
@@ -104,16 +131,6 @@ export const LAUNCH_SCRIPTS = {
|
|
|
104
131
|
grok: "grok --yolo",
|
|
105
132
|
}
|
|
106
133
|
const OWNED_HOOK_FILES = ["scrub-spawn-prompt.mjs", "gate-fat-tools.mjs", "orchestrate-parent.js"]
|
|
107
|
-
export const SENTINEL_BEGIN = "<!-- grunt:begin -->"
|
|
108
|
-
export const SENTINEL_END = "<!-- grunt:end -->"
|
|
109
|
-
export const MAX_GUARDED_MARKDOWN_BYTES = 2 * 1024 * 1024
|
|
110
|
-
const GRUNT_REGION_RE =
|
|
111
|
-
/(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:begin -->[ \t]*\r?\n?[\s\S]*?(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:end -->[ \t]*\r?\n?/g
|
|
112
|
-
const GRUNT_INTERIOR_RE =
|
|
113
|
-
/(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:begin -->[ \t]*\r?\n?([\s\S]*?)(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:end -->/g
|
|
114
|
-
const ORPHAN_END_RE = /(?:^|\r?\n)[ \t]*<!-- grunt:end -->[ \t]*(?=\r?\n|$)|<!-- grunt:end -->/g
|
|
115
|
-
const BEGIN_LINE_RE = /^[ \t]*<!-- grunt:begin -->[ \t]*$/
|
|
116
|
-
const END_LINE_RE = /^[ \t]*<!-- grunt:end -->[ \t]*$/
|
|
117
134
|
|
|
118
135
|
function sortKeys(obj) {
|
|
119
136
|
return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]))
|
|
@@ -199,120 +216,6 @@ export function shouldAutoSkipGlobals(dest) {
|
|
|
199
216
|
return destHasGruntSentinel(dest)
|
|
200
217
|
}
|
|
201
218
|
|
|
202
|
-
function detectNewline(text) {
|
|
203
|
-
return String(text).includes("\r\n") ? "\r\n" : "\n"
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function toNewline(text, nl) {
|
|
207
|
-
return String(text).replace(/\r\n/g, "\n").replace(/\n/g, nl)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function trimTrailingNewlines(text) {
|
|
211
|
-
return String(text).replace(/(?:\r?\n)+$/, "")
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function normEq(a, b) {
|
|
215
|
-
return trimTrailingNewlines(String(a).replace(/\r\n/g, "\n")) ===
|
|
216
|
-
trimTrailingNewlines(String(b).replace(/\r\n/g, "\n"))
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function cloneRe(re) {
|
|
220
|
-
return new RegExp(re.source, re.flags)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function inspectGuardedBuffer(buf) {
|
|
224
|
-
if (buf.length > MAX_GUARDED_MARKDOWN_BYTES) return "huge"
|
|
225
|
-
if (buf.includes(0)) return "binary"
|
|
226
|
-
return null
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function extractUserMarkdown(text) {
|
|
230
|
-
let user = String(text).replace(cloneRe(GRUNT_REGION_RE), "")
|
|
231
|
-
const i = user.indexOf(SENTINEL_BEGIN)
|
|
232
|
-
if (i >= 0) user = user.slice(0, i)
|
|
233
|
-
user = user.replace(cloneRe(ORPHAN_END_RE), "")
|
|
234
|
-
return userRemainderIsBlank(user) ? "" : user
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function unwrapStackedMarkers(text) {
|
|
238
|
-
const lines = String(text).split(/\r?\n/)
|
|
239
|
-
let start = 0
|
|
240
|
-
let end = lines.length
|
|
241
|
-
while (start < end && BEGIN_LINE_RE.test(lines[start])) start++
|
|
242
|
-
while (end > start && lines[end - 1] === "") end--
|
|
243
|
-
while (end > start && END_LINE_RE.test(lines[end - 1])) {
|
|
244
|
-
end--
|
|
245
|
-
while (end > start && lines[end - 1] === "") end--
|
|
246
|
-
}
|
|
247
|
-
return lines.slice(start, end).join("\n")
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function extractGruntBody(text) {
|
|
251
|
-
const src = String(text)
|
|
252
|
-
const parts = []
|
|
253
|
-
const re = cloneRe(GRUNT_INTERIOR_RE)
|
|
254
|
-
let m
|
|
255
|
-
while ((m = re.exec(src))) {
|
|
256
|
-
parts.push(trimTrailingNewlines(unwrapStackedMarkers(m[1])))
|
|
257
|
-
}
|
|
258
|
-
if (parts.length) return parts.join("\n")
|
|
259
|
-
const i = src.indexOf(SENTINEL_BEGIN)
|
|
260
|
-
if (i >= 0) {
|
|
261
|
-
return trimTrailingNewlines(
|
|
262
|
-
unwrapStackedMarkers(src.slice(i + SENTINEL_BEGIN.length).replace(/^[ \t]*\r?\n/, "")),
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
if (!src.includes(SENTINEL_END)) return null
|
|
266
|
-
return trimTrailingNewlines(unwrapStackedMarkers(src))
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function userRemainderIsBlank(user) {
|
|
270
|
-
return String(user).replace(/\r\n/g, "\n").replace(/\n/g, "") === ""
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export function composeGuardedMarkdown(gruntBody, userRemainder, nl = "\n") {
|
|
274
|
-
const raw = extractGruntBody(gruntBody) ?? gruntBody
|
|
275
|
-
const body = toNewline(trimTrailingNewlines(raw), nl)
|
|
276
|
-
const block = `${SENTINEL_BEGIN}${nl}${body}${nl}${SENTINEL_END}${nl}`
|
|
277
|
-
const user = extractUserMarkdown(userRemainder == null ? "" : String(userRemainder))
|
|
278
|
-
if (!user || userRemainderIsBlank(user)) return block
|
|
279
|
-
return block + toNewline(user, nl)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function mergeGuardedContent(existingText, gruntBody) {
|
|
283
|
-
const existing = existingText == null ? "" : String(existingText)
|
|
284
|
-
const nl = existing.includes("\r\n") || String(gruntBody).includes("\r\n")
|
|
285
|
-
? detectNewline(existing || gruntBody)
|
|
286
|
-
: "\n"
|
|
287
|
-
if (!existing) return composeGuardedMarkdown(gruntBody, "", nl)
|
|
288
|
-
const hadSentinel = existing.includes(SENTINEL_BEGIN) && existing.includes(SENTINEL_END)
|
|
289
|
-
const user = extractUserMarkdown(existing)
|
|
290
|
-
if (!hadSentinel && normEq(existing, gruntBody)) {
|
|
291
|
-
return composeGuardedMarkdown(gruntBody, "", nl)
|
|
292
|
-
}
|
|
293
|
-
return composeGuardedMarkdown(gruntBody, user, nl)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function writeMergedGuardedFile(destPath, gruntBody) {
|
|
297
|
-
if (!fs.existsSync(destPath)) {
|
|
298
|
-
fs.writeFileSync(destPath, mergeGuardedContent("", gruntBody))
|
|
299
|
-
return
|
|
300
|
-
}
|
|
301
|
-
const buf = fs.readFileSync(destPath)
|
|
302
|
-
if (inspectGuardedBuffer(buf)) return "aborted-unsafe"
|
|
303
|
-
fs.writeFileSync(destPath, mergeGuardedContent(buf.toString("utf8"), gruntBody))
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function guardedMarkdownDrift(destPath, gruntBody) {
|
|
307
|
-
if (!fs.existsSync(destPath)) return true
|
|
308
|
-
const buf = fs.readFileSync(destPath)
|
|
309
|
-
if (inspectGuardedBuffer(buf)) return true
|
|
310
|
-
const text = buf.toString("utf8")
|
|
311
|
-
const interior = extractGruntBody(text)
|
|
312
|
-
if (interior == null) return !normEq(text, gruntBody)
|
|
313
|
-
return !normEq(interior, gruntBody)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
219
|
export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = false } = {}) {
|
|
317
220
|
void alreadyInited
|
|
318
221
|
const src = path.join(pkgRoot, file)
|
|
@@ -322,104 +225,6 @@ export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = fals
|
|
|
322
225
|
return writeMergedGuardedFile(d, extractGruntBody(srcText) ?? srcText)
|
|
323
226
|
}
|
|
324
227
|
|
|
325
|
-
export function snapshotGuardedRoots(root) {
|
|
326
|
-
const snap = {}
|
|
327
|
-
for (const file of GUARDED_ROOT_FILES) {
|
|
328
|
-
const p = path.join(root, file)
|
|
329
|
-
if (!fs.existsSync(p)) continue
|
|
330
|
-
const buf = fs.readFileSync(p)
|
|
331
|
-
const unsafe = inspectGuardedBuffer(buf)
|
|
332
|
-
if (unsafe) {
|
|
333
|
-
snap[file] = { unsafe, rawBuf: buf }
|
|
334
|
-
continue
|
|
335
|
-
}
|
|
336
|
-
const raw = buf.toString("utf8")
|
|
337
|
-
snap[file] = { raw, user: extractUserMarkdown(raw) }
|
|
338
|
-
}
|
|
339
|
-
return snap
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export function remergeGuardedRoots(root, snap) {
|
|
343
|
-
for (const file of GUARDED_ROOT_FILES) {
|
|
344
|
-
const p = path.join(root, file)
|
|
345
|
-
const rec = snap[file]
|
|
346
|
-
if (rec?.unsafe) {
|
|
347
|
-
fs.writeFileSync(p, rec.rawBuf)
|
|
348
|
-
continue
|
|
349
|
-
}
|
|
350
|
-
if (!fs.existsSync(p)) {
|
|
351
|
-
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
352
|
-
continue
|
|
353
|
-
}
|
|
354
|
-
const buf = fs.readFileSync(p)
|
|
355
|
-
if (inspectGuardedBuffer(buf)) {
|
|
356
|
-
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
357
|
-
continue
|
|
358
|
-
}
|
|
359
|
-
const current = buf.toString("utf8")
|
|
360
|
-
if (rec && current === rec.raw) continue
|
|
361
|
-
const gruntBody = extractGruntBody(current) ?? current
|
|
362
|
-
const nl = detectNewline((rec && rec.raw) || current)
|
|
363
|
-
let user = rec ? rec.user : extractUserMarkdown(current)
|
|
364
|
-
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = ""
|
|
365
|
-
if (!rec && extractGruntBody(current) == null && normEq(current, gruntBody)) user = ""
|
|
366
|
-
fs.writeFileSync(p, composeGuardedMarkdown(gruntBody, user, nl))
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export function healGuardedRootFile(root, file, snap) {
|
|
371
|
-
if (!GUARDED_ROOT_FILES.includes(file)) return
|
|
372
|
-
const p = path.join(root, file)
|
|
373
|
-
const rec = snap[file]
|
|
374
|
-
if (!fs.existsSync(p)) return
|
|
375
|
-
if (rec?.unsafe) {
|
|
376
|
-
fs.writeFileSync(p, rec.rawBuf)
|
|
377
|
-
return
|
|
378
|
-
}
|
|
379
|
-
const buf = fs.readFileSync(p)
|
|
380
|
-
if (inspectGuardedBuffer(buf)) {
|
|
381
|
-
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
const current = buf.toString("utf8")
|
|
385
|
-
const interior = extractGruntBody(current)
|
|
386
|
-
if (interior != null) {
|
|
387
|
-
snap[file] = { raw: current, user: extractUserMarkdown(current) }
|
|
388
|
-
return
|
|
389
|
-
}
|
|
390
|
-
if (rec && current === rec.raw) return
|
|
391
|
-
const gruntBody = current
|
|
392
|
-
const nl = detectNewline((rec && rec.raw) || current)
|
|
393
|
-
let user = rec ? rec.user : extractUserMarkdown(current)
|
|
394
|
-
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = ""
|
|
395
|
-
if (!rec && normEq(current, gruntBody)) user = ""
|
|
396
|
-
const out = composeGuardedMarkdown(gruntBody, user, nl)
|
|
397
|
-
fs.writeFileSync(p, out)
|
|
398
|
-
snap[file] = { raw: out, user }
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export function withGuardedCheckInteriors(root, fn) {
|
|
402
|
-
const snap = snapshotGuardedRoots(root)
|
|
403
|
-
try {
|
|
404
|
-
for (const file of GUARDED_ROOT_FILES) {
|
|
405
|
-
const rec = snap[file]
|
|
406
|
-
if (!rec || rec.unsafe) continue
|
|
407
|
-
const body = extractGruntBody(rec.raw)
|
|
408
|
-
if (body == null) continue
|
|
409
|
-
const nl = detectNewline(rec.raw)
|
|
410
|
-
fs.writeFileSync(path.join(root, file), `${toNewline(trimTrailingNewlines(body), nl)}${nl}`)
|
|
411
|
-
}
|
|
412
|
-
return fn()
|
|
413
|
-
} finally {
|
|
414
|
-
for (const file of GUARDED_ROOT_FILES) {
|
|
415
|
-
const rec = snap[file]
|
|
416
|
-
if (!rec) continue
|
|
417
|
-
if (rec.unsafe) fs.writeFileSync(path.join(root, file), rec.rawBuf)
|
|
418
|
-
else fs.writeFileSync(path.join(root, file), rec.raw)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
228
|
export function mergeClaudeSettings(destRoot, pkgRoot) {
|
|
424
229
|
const srcPath = path.join(pkgRoot, ".claude", "settings.json")
|
|
425
230
|
const destPath = path.join(destRoot, ".claude", "settings.json")
|
|
@@ -576,6 +381,8 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
576
381
|
destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
|
|
577
382
|
destPkg.devDependencies["smol-toml"] = srcPkg.devDependencies["smol-toml"]
|
|
578
383
|
destPkg.devDependencies["rulesync"] = srcPkg.devDependencies["rulesync"]
|
|
384
|
+
const clack = srcPkg.dependencies?.["@clack/prompts"]
|
|
385
|
+
if (clack) destPkg.devDependencies["@clack/prompts"] = clack
|
|
579
386
|
destPkg.scripts = sortKeys(destPkg.scripts)
|
|
580
387
|
destPkg.devDependencies = sortKeys(destPkg.devDependencies)
|
|
581
388
|
fs.writeFileSync(destPath, `${JSON.stringify(destPkg, null, 2)}\n`)
|
package/cli/init.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -5,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
7
|
import {
|
|
7
8
|
MAX_GUARDED_MARKDOWN_BYTES,
|
|
9
|
+
PRODUCT_SCRIPTS,
|
|
8
10
|
composeGuardedMarkdown,
|
|
9
11
|
destAlreadyInited,
|
|
10
12
|
extractGruntBody,
|
|
@@ -52,34 +54,8 @@ function tmp(prefix: string) {
|
|
|
52
54
|
|
|
53
55
|
const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"];
|
|
54
56
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
55
|
-
const PRODUCT_FILES =
|
|
56
|
-
|
|
57
|
-
"emit-agent-shell-tools.mjs",
|
|
58
|
-
"emit-gemini.mjs",
|
|
59
|
-
"emit-maps.mjs",
|
|
60
|
-
"guarded-roots.mjs",
|
|
61
|
-
"emit-mcp-policy.mjs",
|
|
62
|
-
"gate-fat-tools.mjs",
|
|
63
|
-
"hooks-union.mjs",
|
|
64
|
-
"pipeline.mjs",
|
|
65
|
-
"grunt-job.mjs",
|
|
66
|
-
"parse-need.mjs",
|
|
67
|
-
"persist-handoff.mjs",
|
|
68
|
-
"persist-implementation.mjs",
|
|
69
|
-
"persist-tmp.mjs",
|
|
70
|
-
"persist-plan.mjs",
|
|
71
|
-
"purge-global-mcps.mjs",
|
|
72
|
-
"scrub-spawn-prompt.mjs",
|
|
73
|
-
"scrub-text-lib.mjs",
|
|
74
|
-
"sync-global-settings.mjs",
|
|
75
|
-
"browser.mjs",
|
|
76
|
-
"speak.mjs",
|
|
77
|
-
"listen.mjs",
|
|
78
|
-
"google-workspace.mjs",
|
|
79
|
-
"setup.mjs",
|
|
80
|
-
"doctor.mjs",
|
|
81
|
-
"skill-conflicts.mjs",
|
|
82
|
-
];
|
|
57
|
+
const PRODUCT_FILES = PRODUCT_SCRIPTS.filter((name) => name !== "scrub-text");
|
|
58
|
+
const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
83
59
|
|
|
84
60
|
const SRC_CLAUDE_SETTINGS = {
|
|
85
61
|
permissions: {
|
|
@@ -1996,3 +1972,98 @@ describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
|
|
|
1996
1972
|
});
|
|
1997
1973
|
});
|
|
1998
1974
|
|
|
1975
|
+
function spawnDest(dest: string, args: string[], env: NodeJS.ProcessEnv = process.env) {
|
|
1976
|
+
return spawnSync(process.execPath, args, {
|
|
1977
|
+
cwd: dest,
|
|
1978
|
+
encoding: "utf8",
|
|
1979
|
+
env,
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
const PARENT_IMPORT_RE =
|
|
1984
|
+
/(?:from\s+|import\s*\(\s*)["'`](\.\.[^"'`]*)["'`]/g;
|
|
1985
|
+
const RELATIVE_IMPORT_RE =
|
|
1986
|
+
/(?:from\s+|import\s*\(\s*)["'`](\.[^"'`]+)["'`]/g;
|
|
1987
|
+
|
|
1988
|
+
function relativeSpecs(text: string, re: RegExp) {
|
|
1989
|
+
const out: string[] = [];
|
|
1990
|
+
const copy = new RegExp(re.source, "g");
|
|
1991
|
+
let m: RegExpExecArray | null;
|
|
1992
|
+
while ((m = copy.exec(text))) out.push(m[1]);
|
|
1993
|
+
return out;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
describe("product scripts are consumer-safe", () => {
|
|
1997
|
+
it("package.json files lists every PRODUCT_SCRIPTS entry", () => {
|
|
1998
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"));
|
|
1999
|
+
for (const name of PRODUCT_SCRIPTS) {
|
|
2000
|
+
expect(pkg.files).toContain(`scripts/${name}`);
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
it("do not parent-import (from or import()) — init copies scripts/ only", () => {
|
|
2005
|
+
for (const name of PRODUCT_FILES) {
|
|
2006
|
+
const text = fs.readFileSync(path.join(PKG_ROOT, "scripts", name), "utf8");
|
|
2007
|
+
expect(relativeSpecs(text, PARENT_IMPORT_RE), name).toEqual([]);
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
it("repro: dest node ./scripts/guarded-roots.mjs generate misses ../cli/init.mjs", () => {
|
|
2012
|
+
const dest = tmp("repro-cli-miss-");
|
|
2013
|
+
fs.mkdirSync(path.join(dest, "scripts"));
|
|
2014
|
+
fs.writeFileSync(
|
|
2015
|
+
path.join(dest, "scripts", "broken.mjs"),
|
|
2016
|
+
'import { snapshotGuardedRoots } from "../cli/init.mjs";\n',
|
|
2017
|
+
);
|
|
2018
|
+
const r = spawnDest(dest, ["./scripts/broken.mjs", "generate"]);
|
|
2019
|
+
expect(r.status).not.toBe(0);
|
|
2020
|
+
expect(r.stderr).toMatch(/ERR_MODULE_NOT_FOUND/);
|
|
2021
|
+
expect(r.stderr).toMatch(/cli\/init\.mjs/);
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
it("after init, copied scripts' relative imports exist and generate loads", () => {
|
|
2025
|
+
const dest = tmp("init-generate-");
|
|
2026
|
+
init(dest, {
|
|
2027
|
+
pkgRoot: PKG_ROOT,
|
|
2028
|
+
execFileSync: vi.fn(),
|
|
2029
|
+
skipGlobals: true,
|
|
2030
|
+
applyGlobals: false,
|
|
2031
|
+
});
|
|
2032
|
+
expect(fs.existsSync(path.join(dest, "cli"))).toBe(false);
|
|
2033
|
+
expect(fs.existsSync(path.join(dest, "scripts", "guarded-roots.mjs"))).toBe(true);
|
|
2034
|
+
|
|
2035
|
+
for (const name of PRODUCT_FILES) {
|
|
2036
|
+
const file = path.join(dest, "scripts", name);
|
|
2037
|
+
expect(fs.existsSync(file), name).toBe(true);
|
|
2038
|
+
const text = fs.readFileSync(file, "utf8");
|
|
2039
|
+
for (const spec of relativeSpecs(text, RELATIVE_IMPORT_RE)) {
|
|
2040
|
+
const resolved = path.resolve(path.dirname(file), spec);
|
|
2041
|
+
expect(fs.existsSync(resolved), `${name} -> ${spec}`).toBe(true);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const loaded = spawnDest(dest, ["./scripts/guarded-roots.mjs"]);
|
|
2046
|
+
expect(loaded.status).toBe(1);
|
|
2047
|
+
expect(loaded.stderr).toBe("usage: guarded-roots.mjs generate|check|watch\n");
|
|
2048
|
+
expect(loaded.stderr).not.toMatch(/ERR_MODULE_NOT_FOUND/);
|
|
2049
|
+
|
|
2050
|
+
const gen = spawnDest(dest, ["./scripts/guarded-roots.mjs", "generate"], {
|
|
2051
|
+
...process.env,
|
|
2052
|
+
PATH: ["/usr/bin", "/bin"].join(path.delimiter),
|
|
2053
|
+
});
|
|
2054
|
+
expect(gen.stderr).not.toMatch(/ERR_MODULE_NOT_FOUND/);
|
|
2055
|
+
expect(gen.stderr).not.toMatch(/cli\/init\.mjs/);
|
|
2056
|
+
expect(`${gen.stderr}${gen.stdout}`).toMatch(/rulesync|spawn|not found|ENOENT/i);
|
|
2057
|
+
|
|
2058
|
+
for (const name of ["guarded-roots.mjs", "emit-gemini.mjs", "setup.mjs"]) {
|
|
2059
|
+
const r = spawnDest(dest, [
|
|
2060
|
+
"--input-type=module",
|
|
2061
|
+
"-e",
|
|
2062
|
+
`import(${JSON.stringify(`./scripts/${name}`)})`,
|
|
2063
|
+
]);
|
|
2064
|
+
expect(r.status, `${name} load\n${r.stderr}`).toBe(0);
|
|
2065
|
+
expect(r.stderr, name).not.toMatch(/ERR_MODULE_NOT_FOUND/);
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
});
|
|
2069
|
+
|
package/cli/prompt.mjs
CHANGED
|
@@ -1,44 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const ci = env.CI
|
|
11
|
-
if (ci != null && ci !== "" && ci !== "0" && ci !== "false") return false
|
|
12
|
-
for (const a of argv) {
|
|
13
|
-
if (a === "--yes" || a === "-y" || a === "--non-interactive") return false
|
|
14
|
-
}
|
|
15
|
-
return true
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function bailIfCancel(value) {
|
|
19
|
-
if (clack.isCancel(value)) {
|
|
20
|
-
clack.cancel("Aborted")
|
|
21
|
-
process.exit(0)
|
|
22
|
-
}
|
|
23
|
-
return value
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function select(opts) {
|
|
27
|
-
return bailIfCancel(await clack.select(opts))
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function confirm(opts) {
|
|
31
|
-
return bailIfCancel(await clack.confirm(opts))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function text(opts) {
|
|
35
|
-
return bailIfCancel(await clack.text(opts))
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function password(opts) {
|
|
39
|
-
return bailIfCancel(await clack.password(opts))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function spinner() {
|
|
43
|
-
return clack.spinner()
|
|
44
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
bailIfCancel,
|
|
3
|
+
confirm,
|
|
4
|
+
isInteractive,
|
|
5
|
+
password,
|
|
6
|
+
select,
|
|
7
|
+
spinner,
|
|
8
|
+
text,
|
|
9
|
+
} from "../scripts/prompt.mjs"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"scripts/emit-agent-shell-tools.mjs",
|
|
29
29
|
"scripts/emit-gemini.mjs",
|
|
30
30
|
"scripts/emit-maps.mjs",
|
|
31
|
+
"scripts/guarded-md.mjs",
|
|
31
32
|
"scripts/guarded-roots.mjs",
|
|
32
33
|
"scripts/emit-mcp-policy.mjs",
|
|
33
34
|
"scripts/gate-fat-tools.mjs",
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
"scripts/speak.mjs",
|
|
48
49
|
"scripts/listen.mjs",
|
|
49
50
|
"scripts/google-workspace.mjs",
|
|
51
|
+
"scripts/interactive.mjs",
|
|
52
|
+
"scripts/prompt.mjs",
|
|
50
53
|
"scripts/setup.mjs",
|
|
51
54
|
"scripts/doctor.mjs",
|
|
52
55
|
"scripts/skill-conflicts.mjs",
|
package/scripts/browser.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Session browser rail: nav|snap|click|fill|scroll|wait|hover|select|shot|pdf|stop|doctor|ensure. Lightpanda default. */
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import http from "node:http";
|
|
@@ -137,6 +137,21 @@ function alive(pid) {
|
|
|
137
137
|
|
|
138
138
|
function reap(pid) {
|
|
139
139
|
if (!pid) return;
|
|
140
|
+
if (process.platform === "win32") {
|
|
141
|
+
try {
|
|
142
|
+
process.kill(pid);
|
|
143
|
+
} catch {
|
|
144
|
+
/* ignore */
|
|
145
|
+
}
|
|
146
|
+
if (alive(pid)) {
|
|
147
|
+
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
148
|
+
stdio: "ignore",
|
|
149
|
+
timeout: 1500,
|
|
150
|
+
windowsHide: true,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
140
155
|
try {
|
|
141
156
|
process.kill(-pid, "SIGTERM");
|
|
142
157
|
} catch {
|
|
@@ -480,6 +495,19 @@ function engineBin(engine, bins) {
|
|
|
480
495
|
return bins.chromium;
|
|
481
496
|
}
|
|
482
497
|
|
|
498
|
+
function spawnEngine(bin, args, opts) {
|
|
499
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(bin)) {
|
|
500
|
+
const mjs = `${String(bin).replace(/\.(cmd|bat)$/i, "")}.mjs`;
|
|
501
|
+
try {
|
|
502
|
+
fs.accessSync(mjs, fs.constants.F_OK);
|
|
503
|
+
return spawn(process.execPath, [mjs, ...args], opts);
|
|
504
|
+
} catch {
|
|
505
|
+
return spawn(bin, args, { ...opts, shell: true });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return spawn(bin, args, opts);
|
|
509
|
+
}
|
|
510
|
+
|
|
483
511
|
function spawnArgs(engine, port, profile) {
|
|
484
512
|
if (engine === "lightpanda") return ["serve", "--host", "127.0.0.1", "--port", String(port)];
|
|
485
513
|
return [
|
|
@@ -501,7 +529,7 @@ async function launchEngine(engine, ctx) {
|
|
|
501
529
|
const profile = path.join(dir, "profile");
|
|
502
530
|
fs.mkdirSync(profile, { recursive: true });
|
|
503
531
|
const port = await freePort();
|
|
504
|
-
const child =
|
|
532
|
+
const child = spawnEngine(bin, spawnArgs(engine, port, profile), {
|
|
505
533
|
detached: true,
|
|
506
534
|
stdio: ["ignore", "pipe", "pipe"],
|
|
507
535
|
env: ctx.env,
|
|
@@ -36,7 +36,7 @@ export function checkGlobals({ home, workspaceRoot } = {}) {
|
|
|
36
36
|
if (!homeDir) {
|
|
37
37
|
return { ok: false, error: `HOME required; ${APPLY_HINT}` };
|
|
38
38
|
}
|
|
39
|
-
const homeConfig = path.
|
|
39
|
+
const homeConfig = path.resolve(homeDir, HOME_REL);
|
|
40
40
|
const homeParsed = parseTomlFile(homeConfig, "home ~/.grok/config.toml");
|
|
41
41
|
if (!homeParsed.ok) {
|
|
42
42
|
if (homeParsed.missing) {
|
|
@@ -54,7 +54,10 @@ export function checkGlobals({ home, workspaceRoot } = {}) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const projectConfig = path.
|
|
57
|
+
const projectConfig = path.resolve(ws, PROJECT_REL);
|
|
58
|
+
if (projectConfig === homeConfig) {
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
58
61
|
const projectParsed = parseTomlFile(projectConfig, "project .grok/config.toml");
|
|
59
62
|
if (!projectParsed.ok) {
|
|
60
63
|
if (projectParsed.missing) {
|
package/scripts/doctor.mjs
CHANGED
|
@@ -42,20 +42,32 @@ export function nodeMajor(version) {
|
|
|
42
42
|
return m ? Number(m[1]) : 0;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function winShim(bin) {
|
|
46
|
+
return process.platform === "win32" && /\.(cmd|bat)$/i.test(String(bin || ""));
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
export function whichBin(name, pathEnv = process.env.PATH, platform = process.platform) {
|
|
46
50
|
if (!name) return "";
|
|
51
|
+
const hostWin = process.platform === "win32";
|
|
47
52
|
const delim =
|
|
48
|
-
platform === "win32" && String(pathEnv || "").includes(";")
|
|
53
|
+
(platform === "win32" || hostWin) && String(pathEnv || "").includes(";")
|
|
54
|
+
? ";"
|
|
55
|
+
: path.delimiter;
|
|
49
56
|
const dirs = String(pathEnv || "").split(delim);
|
|
50
|
-
const names = [
|
|
51
|
-
if (platform === "win32" && !path.extname(name)) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const names = [];
|
|
58
|
+
if ((hostWin || platform === "win32") && !path.extname(name)) {
|
|
59
|
+
if (hostWin) {
|
|
60
|
+
const pathext = String(process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM");
|
|
61
|
+
for (const raw of pathext.split(";")) {
|
|
62
|
+
if (!raw) continue;
|
|
63
|
+
names.push(name + raw.toLowerCase());
|
|
64
|
+
names.push(name + raw);
|
|
65
|
+
}
|
|
57
66
|
}
|
|
58
67
|
names.push(`${name}.exe`);
|
|
68
|
+
names.push(name);
|
|
69
|
+
} else {
|
|
70
|
+
names.push(name);
|
|
59
71
|
}
|
|
60
72
|
const uniq = [...new Set(names)];
|
|
61
73
|
for (const dir of dirs) {
|
|
@@ -66,7 +78,7 @@ export function whichBin(name, pathEnv = process.env.PATH, platform = process.pl
|
|
|
66
78
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
67
79
|
return candidate;
|
|
68
80
|
} catch {
|
|
69
|
-
if (
|
|
81
|
+
if (hostWin) {
|
|
70
82
|
try {
|
|
71
83
|
fs.accessSync(candidate, fs.constants.F_OK);
|
|
72
84
|
return candidate;
|
|
@@ -97,6 +109,7 @@ function readNodeVersion(bin) {
|
|
|
97
109
|
encoding: "utf8",
|
|
98
110
|
timeout: 8000,
|
|
99
111
|
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
shell: winShim(bin),
|
|
100
113
|
}),
|
|
101
114
|
).trim();
|
|
102
115
|
} catch {
|
package/scripts/emit-gemini.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { guardedMarkdownDrift, writeMergedGuardedFile } from "
|
|
6
|
+
import { guardedMarkdownDrift, writeMergedGuardedFile } from "./guarded-md.mjs";
|
|
7
7
|
|
|
8
8
|
export const AGENT_IDS = ["orchestrator", "grunt"];
|
|
9
9
|
const SSOT_REL = ".rulesync/subagents";
|
|
@@ -175,9 +175,14 @@ function isMcpToolRule(s) {
|
|
|
175
175
|
return s === "MCPTool" || s.startsWith("MCPTool(");
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
function
|
|
178
|
+
export function isHomeGrokConfig(workspaceRoot, home) {
|
|
179
|
+
if (!workspaceRoot || !home) return false;
|
|
180
|
+
return path.resolve(workspaceRoot, GROK_REL) === path.resolve(home, GROK_REL);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function desiredGrok(disk, policy, { keepHomeTables = false } = {}) {
|
|
179
184
|
const obj = isPlainObject(disk) ? { ...disk } : {};
|
|
180
|
-
delete obj.features;
|
|
185
|
+
if (!keepHomeTables) delete obj.features;
|
|
181
186
|
delete obj.permissions;
|
|
182
187
|
|
|
183
188
|
const plugins = isPlainObject(obj.plugins) ? { ...obj.plugins } : {};
|
|
@@ -288,9 +293,9 @@ function geminiCheckOk(disk, desired) {
|
|
|
288
293
|
return deepEqual(disk.mcpServers, desired.mcpServers);
|
|
289
294
|
}
|
|
290
295
|
|
|
291
|
-
function grokCheckOk(disk, desired, policy) {
|
|
296
|
+
function grokCheckOk(disk, desired, policy, { keepHomeTables = false } = {}) {
|
|
292
297
|
if (!isPlainObject(disk)) return false;
|
|
293
|
-
if (Object.prototype.hasOwnProperty.call(disk, "features")) return false;
|
|
298
|
+
if (!keepHomeTables && Object.prototype.hasOwnProperty.call(disk, "features")) return false;
|
|
294
299
|
if (Array.isArray(disk.permissions)) return false;
|
|
295
300
|
if (isPlainObject(disk.plugins) && Object.prototype.hasOwnProperty.call(disk.plugins, "deny_default")) {
|
|
296
301
|
return false;
|
|
@@ -336,13 +341,14 @@ function writeOrCheck({ check, abs, nextText, diskRaw }) {
|
|
|
336
341
|
return true;
|
|
337
342
|
}
|
|
338
343
|
|
|
339
|
-
export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
|
|
344
|
+
export function emitMcpPolicy({ workspaceRoot, check = false, home } = {}) {
|
|
340
345
|
const ws = workspaceRoot || process.cwd();
|
|
341
346
|
const policy = loadPolicy(ws);
|
|
342
347
|
const valid = validatePolicy(policy);
|
|
343
348
|
if (!valid.ok) {
|
|
344
349
|
return { ok: false, check, error: valid.error };
|
|
345
350
|
}
|
|
351
|
+
const keepHomeTables = isHomeGrokConfig(ws, home || process.env.HOME);
|
|
346
352
|
|
|
347
353
|
const grokAbs = path.join(ws, GROK_REL);
|
|
348
354
|
const claudeAbs = path.join(ws, CLAUDE_SETTINGS_REL);
|
|
@@ -365,7 +371,7 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
|
|
|
365
371
|
const agentsDisk = agentsRaw == null ? null : parseJsonFile(agentsRaw, {});
|
|
366
372
|
const geminiDisk = geminiRaw == null ? null : parseJsonFile(geminiRaw, {});
|
|
367
373
|
|
|
368
|
-
const grokDesired = desiredGrok(grokDisk, policy);
|
|
374
|
+
const grokDesired = desiredGrok(grokDisk, policy, { keepHomeTables });
|
|
369
375
|
const claudeDesired = desiredClaudeSettings(claudeDisk, policy);
|
|
370
376
|
const mcpDesired = desiredMcpJson(policy);
|
|
371
377
|
const codexDesired = desiredCodex(codexDisk, policy);
|
|
@@ -374,7 +380,7 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
|
|
|
374
380
|
|
|
375
381
|
if (check) {
|
|
376
382
|
const drift = [];
|
|
377
|
-
if (grokRaw == null || !grokCheckOk(grokDisk, grokDesired, policy)) drift.push(GROK_REL);
|
|
383
|
+
if (grokRaw == null || !grokCheckOk(grokDisk, grokDesired, policy, { keepHomeTables })) drift.push(GROK_REL);
|
|
378
384
|
if (claudeRaw == null || !claudeCheckOk(claudeDisk, claudeDesired)) drift.push(CLAUDE_SETTINGS_REL);
|
|
379
385
|
if (mcpDisk == null || !deepEqual(mcpDisk, mcpDesired)) drift.push(MCP_JSON_REL);
|
|
380
386
|
const codexOk =
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/** Sentinel merge for AGENTS.md CLAUDE.md GEMINI.md. Copied into consumers. */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const GUARDED_ROOT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"];
|
|
6
|
+
export const SENTINEL_BEGIN = "<!-- grunt:begin -->";
|
|
7
|
+
export const SENTINEL_END = "<!-- grunt:end -->";
|
|
8
|
+
export const MAX_GUARDED_MARKDOWN_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
const GRUNT_REGION_RE =
|
|
10
|
+
/(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:begin -->[ \t]*\r?\n?[\s\S]*?(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:end -->[ \t]*\r?\n?/g;
|
|
11
|
+
const GRUNT_INTERIOR_RE =
|
|
12
|
+
/(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:begin -->[ \t]*\r?\n?([\s\S]*?)(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:end -->/g;
|
|
13
|
+
const ORPHAN_END_RE = /(?:^|\r?\n)[ \t]*<!-- grunt:end -->[ \t]*(?=\r?\n|$)|<!-- grunt:end -->/g;
|
|
14
|
+
const BEGIN_LINE_RE = /^[ \t]*<!-- grunt:begin -->[ \t]*$/;
|
|
15
|
+
const END_LINE_RE = /^[ \t]*<!-- grunt:end -->[ \t]*$/;
|
|
16
|
+
|
|
17
|
+
function detectNewline(text) {
|
|
18
|
+
return String(text).includes("\r\n") ? "\r\n" : "\n";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toNewline(text, nl) {
|
|
22
|
+
return String(text).replace(/\r\n/g, "\n").replace(/\n/g, nl);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function trimTrailingNewlines(text) {
|
|
26
|
+
return String(text).replace(/(?:\r?\n)+$/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normEq(a, b) {
|
|
30
|
+
return (
|
|
31
|
+
trimTrailingNewlines(String(a).replace(/\r\n/g, "\n")) ===
|
|
32
|
+
trimTrailingNewlines(String(b).replace(/\r\n/g, "\n"))
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cloneRe(re) {
|
|
37
|
+
return new RegExp(re.source, re.flags);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function inspectGuardedBuffer(buf) {
|
|
41
|
+
if (buf.length > MAX_GUARDED_MARKDOWN_BYTES) return "huge";
|
|
42
|
+
if (buf.includes(0)) return "binary";
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function extractUserMarkdown(text) {
|
|
47
|
+
let user = String(text).replace(cloneRe(GRUNT_REGION_RE), "");
|
|
48
|
+
const i = user.indexOf(SENTINEL_BEGIN);
|
|
49
|
+
if (i >= 0) user = user.slice(0, i);
|
|
50
|
+
user = user.replace(cloneRe(ORPHAN_END_RE), "");
|
|
51
|
+
return userRemainderIsBlank(user) ? "" : user;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function unwrapStackedMarkers(text) {
|
|
55
|
+
const lines = String(text).split(/\r?\n/);
|
|
56
|
+
let start = 0;
|
|
57
|
+
let end = lines.length;
|
|
58
|
+
while (start < end && BEGIN_LINE_RE.test(lines[start])) start++;
|
|
59
|
+
while (end > start && lines[end - 1] === "") end--;
|
|
60
|
+
while (end > start && END_LINE_RE.test(lines[end - 1])) {
|
|
61
|
+
end--;
|
|
62
|
+
while (end > start && lines[end - 1] === "") end--;
|
|
63
|
+
}
|
|
64
|
+
return lines.slice(start, end).join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function extractGruntBody(text) {
|
|
68
|
+
const src = String(text);
|
|
69
|
+
const parts = [];
|
|
70
|
+
const re = cloneRe(GRUNT_INTERIOR_RE);
|
|
71
|
+
let m;
|
|
72
|
+
while ((m = re.exec(src))) {
|
|
73
|
+
parts.push(trimTrailingNewlines(unwrapStackedMarkers(m[1])));
|
|
74
|
+
}
|
|
75
|
+
if (parts.length) return parts.join("\n");
|
|
76
|
+
const i = src.indexOf(SENTINEL_BEGIN);
|
|
77
|
+
if (i >= 0) {
|
|
78
|
+
return trimTrailingNewlines(
|
|
79
|
+
unwrapStackedMarkers(src.slice(i + SENTINEL_BEGIN.length).replace(/^[ \t]*\r?\n/, "")),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!src.includes(SENTINEL_END)) return null;
|
|
83
|
+
return trimTrailingNewlines(unwrapStackedMarkers(src));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function userRemainderIsBlank(user) {
|
|
87
|
+
return String(user).replace(/\r\n/g, "\n").replace(/\n/g, "") === "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function composeGuardedMarkdown(gruntBody, userRemainder, nl = "\n") {
|
|
91
|
+
const raw = extractGruntBody(gruntBody) ?? gruntBody;
|
|
92
|
+
const body = toNewline(trimTrailingNewlines(raw), nl);
|
|
93
|
+
const block = `${SENTINEL_BEGIN}${nl}${body}${nl}${SENTINEL_END}${nl}`;
|
|
94
|
+
const user = extractUserMarkdown(userRemainder == null ? "" : String(userRemainder));
|
|
95
|
+
if (!user || userRemainderIsBlank(user)) return block;
|
|
96
|
+
return block + toNewline(user, nl);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function mergeGuardedContent(existingText, gruntBody) {
|
|
100
|
+
const existing = existingText == null ? "" : String(existingText);
|
|
101
|
+
const nl =
|
|
102
|
+
existing.includes("\r\n") || String(gruntBody).includes("\r\n")
|
|
103
|
+
? detectNewline(existing || gruntBody)
|
|
104
|
+
: "\n";
|
|
105
|
+
if (!existing) return composeGuardedMarkdown(gruntBody, "", nl);
|
|
106
|
+
const hadSentinel = existing.includes(SENTINEL_BEGIN) && existing.includes(SENTINEL_END);
|
|
107
|
+
const user = extractUserMarkdown(existing);
|
|
108
|
+
if (!hadSentinel && normEq(existing, gruntBody)) {
|
|
109
|
+
return composeGuardedMarkdown(gruntBody, "", nl);
|
|
110
|
+
}
|
|
111
|
+
return composeGuardedMarkdown(gruntBody, user, nl);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function writeMergedGuardedFile(destPath, gruntBody) {
|
|
115
|
+
if (!fs.existsSync(destPath)) {
|
|
116
|
+
fs.writeFileSync(destPath, mergeGuardedContent("", gruntBody));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const buf = fs.readFileSync(destPath);
|
|
120
|
+
if (inspectGuardedBuffer(buf)) return "aborted-unsafe";
|
|
121
|
+
fs.writeFileSync(destPath, mergeGuardedContent(buf.toString("utf8"), gruntBody));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function guardedMarkdownDrift(destPath, gruntBody) {
|
|
125
|
+
if (!fs.existsSync(destPath)) return true;
|
|
126
|
+
const buf = fs.readFileSync(destPath);
|
|
127
|
+
if (inspectGuardedBuffer(buf)) return true;
|
|
128
|
+
const text = buf.toString("utf8");
|
|
129
|
+
const interior = extractGruntBody(text);
|
|
130
|
+
if (interior == null) return !normEq(text, gruntBody);
|
|
131
|
+
return !normEq(interior, gruntBody);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function snapshotGuardedRoots(root) {
|
|
135
|
+
const snap = {};
|
|
136
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
137
|
+
const p = path.join(root, file);
|
|
138
|
+
if (!fs.existsSync(p)) continue;
|
|
139
|
+
const buf = fs.readFileSync(p);
|
|
140
|
+
const unsafe = inspectGuardedBuffer(buf);
|
|
141
|
+
if (unsafe) {
|
|
142
|
+
snap[file] = { unsafe, rawBuf: buf };
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const raw = buf.toString("utf8");
|
|
146
|
+
snap[file] = { raw, user: extractUserMarkdown(raw) };
|
|
147
|
+
}
|
|
148
|
+
return snap;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function remergeGuardedRoots(root, snap) {
|
|
152
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
153
|
+
const p = path.join(root, file);
|
|
154
|
+
const rec = snap[file];
|
|
155
|
+
if (rec?.unsafe) {
|
|
156
|
+
fs.writeFileSync(p, rec.rawBuf);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!fs.existsSync(p)) {
|
|
160
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const buf = fs.readFileSync(p);
|
|
164
|
+
if (inspectGuardedBuffer(buf)) {
|
|
165
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const current = buf.toString("utf8");
|
|
169
|
+
if (rec && current === rec.raw) continue;
|
|
170
|
+
const gruntBody = extractGruntBody(current) ?? current;
|
|
171
|
+
const nl = detectNewline((rec && rec.raw) || current);
|
|
172
|
+
let user = rec ? rec.user : extractUserMarkdown(current);
|
|
173
|
+
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = "";
|
|
174
|
+
if (!rec && extractGruntBody(current) == null && normEq(current, gruntBody)) user = "";
|
|
175
|
+
fs.writeFileSync(p, composeGuardedMarkdown(gruntBody, user, nl));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function healGuardedRootFile(root, file, snap) {
|
|
180
|
+
if (!GUARDED_ROOT_FILES.includes(file)) return;
|
|
181
|
+
const p = path.join(root, file);
|
|
182
|
+
const rec = snap[file];
|
|
183
|
+
if (!fs.existsSync(p)) return;
|
|
184
|
+
if (rec?.unsafe) {
|
|
185
|
+
fs.writeFileSync(p, rec.rawBuf);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const buf = fs.readFileSync(p);
|
|
189
|
+
if (inspectGuardedBuffer(buf)) {
|
|
190
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const current = buf.toString("utf8");
|
|
194
|
+
const interior = extractGruntBody(current);
|
|
195
|
+
if (interior != null) {
|
|
196
|
+
snap[file] = { raw: current, user: extractUserMarkdown(current) };
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (rec && current === rec.raw) return;
|
|
200
|
+
const gruntBody = current;
|
|
201
|
+
const nl = detectNewline((rec && rec.raw) || current);
|
|
202
|
+
let user = rec ? rec.user : extractUserMarkdown(current);
|
|
203
|
+
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = "";
|
|
204
|
+
if (!rec && normEq(current, gruntBody)) user = "";
|
|
205
|
+
const out = composeGuardedMarkdown(gruntBody, user, nl);
|
|
206
|
+
fs.writeFileSync(p, out);
|
|
207
|
+
snap[file] = { raw: out, user };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function withGuardedCheckInteriors(root, fn) {
|
|
211
|
+
const snap = snapshotGuardedRoots(root);
|
|
212
|
+
try {
|
|
213
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
214
|
+
const rec = snap[file];
|
|
215
|
+
if (!rec || rec.unsafe) continue;
|
|
216
|
+
const body = extractGruntBody(rec.raw);
|
|
217
|
+
if (body == null) continue;
|
|
218
|
+
const nl = detectNewline(rec.raw);
|
|
219
|
+
fs.writeFileSync(path.join(root, file), `${toNewline(trimTrailingNewlines(body), nl)}${nl}`);
|
|
220
|
+
}
|
|
221
|
+
return fn();
|
|
222
|
+
} finally {
|
|
223
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
224
|
+
const rec = snap[file];
|
|
225
|
+
if (!rec) continue;
|
|
226
|
+
if (rec.unsafe) fs.writeFileSync(path.join(root, file), rec.rawBuf);
|
|
227
|
+
else fs.writeFileSync(path.join(root, file), rec.raw);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** TTY / CI / --yes probe. Copied into consumers; no clack. */
|
|
2
|
+
export function isInteractive({
|
|
3
|
+
argv = process.argv.slice(2),
|
|
4
|
+
env = process.env,
|
|
5
|
+
stdin = process.stdin,
|
|
6
|
+
stdout = process.stdout,
|
|
7
|
+
} = {}) {
|
|
8
|
+
if (!stdin?.isTTY || !stdout?.isTTY) return false;
|
|
9
|
+
const ci = env.CI;
|
|
10
|
+
if (ci != null && ci !== "" && ci !== "0" && ci !== "false") return false;
|
|
11
|
+
for (const a of argv) {
|
|
12
|
+
if (a === "--yes" || a === "-y" || a === "--non-interactive") return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** TTY prompts. Copied into consumers. */
|
|
2
|
+
import * as clack from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
export { isInteractive } from "./interactive.mjs";
|
|
5
|
+
|
|
6
|
+
export function bailIfCancel(value) {
|
|
7
|
+
if (clack.isCancel(value)) {
|
|
8
|
+
clack.cancel("Aborted");
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function select(opts) {
|
|
15
|
+
return bailIfCancel(await clack.select(opts));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function confirm(opts) {
|
|
19
|
+
return bailIfCancel(await clack.confirm(opts));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function text(opts) {
|
|
23
|
+
return bailIfCancel(await clack.text(opts));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function password(opts) {
|
|
27
|
+
return bailIfCancel(await clack.password(opts));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function spinner() {
|
|
31
|
+
return clack.spinner();
|
|
32
|
+
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { isInteractive as ttyInteractive } from "
|
|
7
|
+
import { isInteractive as ttyInteractive } from "./interactive.mjs";
|
|
8
8
|
import {
|
|
9
9
|
CHROMIUM_BINS,
|
|
10
10
|
installHints,
|
|
@@ -343,7 +343,7 @@ function errLine(io, s) {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
async function defaultIo() {
|
|
346
|
-
const { confirm, password, select, text } = await import("
|
|
346
|
+
const { confirm, password, select, text } = await import("./prompt.mjs");
|
|
347
347
|
return {
|
|
348
348
|
confirm,
|
|
349
349
|
password,
|