roblox-guard 0.5.0 → 0.6.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/package.json +1 -1
- package/scripts/roblox-guard.mjs +162 -3
package/package.json
CHANGED
package/scripts/roblox-guard.mjs
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* rules have fallen out of the model's context.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
-
import { join, resolve, sep } from "node:path";
|
|
10
|
-
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
|
|
12
|
+
/** The installed package, so `install` can read the standards it ships with. */
|
|
13
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
14
|
|
|
12
15
|
/** Proof that a source file is Roblox rather than another Lua dialect. */
|
|
13
16
|
const ROBLOX_MARKERS =
|
|
@@ -64,10 +67,51 @@ const SKIP_REASON = {
|
|
|
64
67
|
unreadable: "could not be read as a file",
|
|
65
68
|
};
|
|
66
69
|
|
|
70
|
+
/** Marks a file this tool wrote, so it may be replaced without asking. */
|
|
71
|
+
export const GENERATED = "<!-- Generated from AGENTS.md. Edit that file. -->";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Where each agent reads its instructions, the front matter that host expects above the shared
|
|
75
|
+
* body, and the directory whose presence says the host is in use here. An agent that reads
|
|
76
|
+
* plain Markdown takes no front matter; one with no marker of its own is written on request.
|
|
77
|
+
*/
|
|
78
|
+
export const RULE_TARGETS = [
|
|
79
|
+
{
|
|
80
|
+
path: ".cursor/rules/roblox-optimum.mdc",
|
|
81
|
+
marker: ".cursor",
|
|
82
|
+
agent: "Cursor",
|
|
83
|
+
frontMatter: `---
|
|
84
|
+
description: Roblox and Luau coding standards
|
|
85
|
+
globs: ["**/*.luau", "**/*.lua"]
|
|
86
|
+
alwaysApply: false
|
|
87
|
+
---
|
|
88
|
+
`,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
path: ".kiro/steering/roblox-optimum.md",
|
|
92
|
+
marker: ".kiro",
|
|
93
|
+
agent: "Kiro",
|
|
94
|
+
frontMatter: `---
|
|
95
|
+
inclusion: fileMatch
|
|
96
|
+
fileMatchPattern: ["**/*.luau", "**/*.lua"]
|
|
97
|
+
---
|
|
98
|
+
`,
|
|
99
|
+
},
|
|
100
|
+
{ path: "rules/roblox-optimum.md", marker: "rules", agent: "a plugin rules directory", frontMatter: "" },
|
|
101
|
+
{ path: ".windsurf/rules/roblox-optimum.md", marker: ".windsurf", agent: "Windsurf", frontMatter: "" },
|
|
102
|
+
{ path: ".clinerules/roblox-optimum.md", marker: ".clinerules", agent: "Cline", frontMatter: "" },
|
|
103
|
+
{ path: ".qoder/rules/roblox-optimum.md", marker: ".qoder", agent: "Qoder", frontMatter: "" },
|
|
104
|
+
{ path: ".agents/rules/roblox-optimum.md", marker: ".agents", agent: "Antigravity", frontMatter: "" },
|
|
105
|
+
{ path: ".github/copilot-instructions.md", marker: ".github", agent: "GitHub Copilot", frontMatter: "" },
|
|
106
|
+
{ path: "QWEN.md", marker: null, agent: "Qwen Code", frontMatter: "" },
|
|
107
|
+
];
|
|
108
|
+
|
|
67
109
|
const USAGE = `roblox-guard - deterministic Roblox and Luau standards checks
|
|
68
110
|
|
|
69
111
|
Usage:
|
|
70
112
|
roblox-guard --check <file...> Check files. Exit 1 when a file has findings.
|
|
113
|
+
roblox-guard install [--all] Write the standards into this project, for every agent it
|
|
114
|
+
finds. --all writes every agent's file regardless.
|
|
71
115
|
roblox-guard --selftest Run the built-in assertions.
|
|
72
116
|
roblox-guard Read a post-write hook payload on stdin. Exit 2 reports
|
|
73
117
|
findings back to the agent.
|
|
@@ -346,6 +390,109 @@ function runCheck(paths) {
|
|
|
346
390
|
return 0;
|
|
347
391
|
}
|
|
348
392
|
|
|
393
|
+
/** The commit hook, which is the one setup that works whoever wrote the file. */
|
|
394
|
+
const PRE_COMMIT = `#!/bin/sh
|
|
395
|
+
# Installed by roblox-guard. Delete this file to remove it.
|
|
396
|
+
files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.luau?$')
|
|
397
|
+
[ -z "$files" ] || npx roblox-guard --check $files
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Whether a path may be written: either it is absent, or this tool wrote what is there.
|
|
402
|
+
* Anyone else's file is left alone, because a standards tool that overwrites work silently
|
|
403
|
+
* has already cost more than it saves.
|
|
404
|
+
*
|
|
405
|
+
* @param path string -- Absolute path to consider.
|
|
406
|
+
* @param mark string -- The line this tool stamps into what it writes.
|
|
407
|
+
* @return boolean
|
|
408
|
+
*/
|
|
409
|
+
function writable(path, mark) {
|
|
410
|
+
if (!existsSync(path)) return true;
|
|
411
|
+
try {
|
|
412
|
+
return readFileSync(path, "utf8").includes(mark);
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Writes the standards into the current project for every agent it can see, and reports the
|
|
420
|
+
* installs it cannot perform itself.
|
|
421
|
+
*
|
|
422
|
+
* @param all boolean -- Write every agent's file, not just the ones with a marker present.
|
|
423
|
+
* @return number -- Process exit code.
|
|
424
|
+
*/
|
|
425
|
+
function runInstall(all) {
|
|
426
|
+
const source = join(PACKAGE_ROOT, "AGENTS.md");
|
|
427
|
+
if (!existsSync(source)) {
|
|
428
|
+
process.stderr.write(`roblox-guard install: AGENTS.md is missing from the package.\n`);
|
|
429
|
+
return 2;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const body = readFileSync(source, "utf8").replace(/\r\n/g, "\n");
|
|
433
|
+
const cwd = process.cwd();
|
|
434
|
+
const written = [];
|
|
435
|
+
const kept = [];
|
|
436
|
+
|
|
437
|
+
for (const target of [{ path: "AGENTS.md", marker: null, frontMatter: "" }, ...RULE_TARGETS]) {
|
|
438
|
+
if (!all && target.marker && !existsSync(join(cwd, target.marker))) continue;
|
|
439
|
+
if (!all && target.path === "QWEN.md") continue;
|
|
440
|
+
|
|
441
|
+
const full = join(cwd, target.path);
|
|
442
|
+
if (!writable(full, GENERATED) && target.path !== "AGENTS.md") {
|
|
443
|
+
kept.push(target.path);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (target.path === "AGENTS.md" && existsSync(full) && !writable(full, GENERATED)) {
|
|
447
|
+
kept.push(target.path);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
452
|
+
writeFileSync(full, `${target.frontMatter}${GENERATED}\n\n${body}`);
|
|
453
|
+
written.push(target.path);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const hook = join(cwd, ".git", "hooks", "pre-commit");
|
|
457
|
+
let hookNote = "";
|
|
458
|
+
if (existsSync(join(cwd, ".git"))) {
|
|
459
|
+
if (writable(hook, "roblox-guard")) {
|
|
460
|
+
mkdirSync(dirname(hook), { recursive: true });
|
|
461
|
+
writeFileSync(hook, PRE_COMMIT, { mode: 0o755 });
|
|
462
|
+
written.push(".git/hooks/pre-commit");
|
|
463
|
+
} else {
|
|
464
|
+
hookNote =
|
|
465
|
+
"\nA pre-commit hook already exists, so it was left alone. To add the check to it:\n" +
|
|
466
|
+
" npx roblox-guard --check $(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.luau?$')\n";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
process.stdout.write(
|
|
471
|
+
(written.length > 0
|
|
472
|
+
? `roblox-guard installed ${written.length} file(s):\n` + written.map((p) => ` ${p}\n`).join("")
|
|
473
|
+
: "roblox-guard wrote nothing new.\n") +
|
|
474
|
+
(kept.length > 0
|
|
475
|
+
? `\nLeft alone, because something else wrote them:\n` + kept.map((p) => ` ${p}\n`).join("")
|
|
476
|
+
: "") +
|
|
477
|
+
hookNote +
|
|
478
|
+
`
|
|
479
|
+
Agents that install themselves, run whichever you use:
|
|
480
|
+
Claude Code /plugin marketplace add andrian-syh/roblox-optimum
|
|
481
|
+
/plugin install roblox-optimum@andrian-syh
|
|
482
|
+
Codex codex plugin marketplace add andrian-syh/roblox-optimum
|
|
483
|
+
codex plugin add roblox-optimum@andrian-syh
|
|
484
|
+
Copilot CLI copilot plugin marketplace add andrian-syh/roblox-optimum
|
|
485
|
+
copilot plugin install roblox-optimum@andrian-syh
|
|
486
|
+
Qwen Code qwen extensions install ${HOME_PAGE}
|
|
487
|
+
Kiro install a power from ${HOME_PAGE}
|
|
488
|
+
|
|
489
|
+
Standards: ${HOME_PAGE}
|
|
490
|
+
`,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
349
496
|
/**
|
|
350
497
|
* Tells the agent to re-read the skill once a summary has dropped the rules, and stays
|
|
351
498
|
* silent outside a Roblox project so unrelated sessions are not disturbed.
|
|
@@ -497,6 +644,17 @@ Players.PlayerAdded:Connect(greet)
|
|
|
497
644
|
"LoadAnimation on an Animator is not reported",
|
|
498
645
|
);
|
|
499
646
|
|
|
647
|
+
ok(
|
|
648
|
+
RULE_TARGETS.every((t) => typeof t.path === "string" && typeof t.frontMatter === "string"),
|
|
649
|
+
"every rule target names a path and its front matter",
|
|
650
|
+
);
|
|
651
|
+
ok(
|
|
652
|
+
new Set(RULE_TARGETS.map((t) => t.path)).size === RULE_TARGETS.length,
|
|
653
|
+
"no two rule targets claim the same path",
|
|
654
|
+
);
|
|
655
|
+
ok(writable(join(ROOT_ABSENT, "nothing.md"), GENERATED), "an absent file may be written");
|
|
656
|
+
ok(!writable("package.json", GENERATED), "a file this tool did not write is left alone");
|
|
657
|
+
|
|
500
658
|
ok(checkFile("notes.txt").status === "not-luau", "a non-Luau path is reported as skipped");
|
|
501
659
|
ok(
|
|
502
660
|
checkFile(join("proj", "Packages", "Cmdr.luau")).status === "vendored",
|
|
@@ -519,6 +677,7 @@ if (invokedDirectly) {
|
|
|
519
677
|
if (mode === "--selftest") selftest();
|
|
520
678
|
else if (mode === "--compact") process.exit(await runCompactReminder());
|
|
521
679
|
else if (mode === "--check") process.exit(runCheck(rest));
|
|
680
|
+
else if (mode === "install") process.exit(runInstall(rest.includes("--all")));
|
|
522
681
|
else if (mode === "--help" || mode === "-h") process.stdout.write(USAGE);
|
|
523
682
|
else if (mode === undefined) process.exit(await runPostToolUse());
|
|
524
683
|
else {
|