vibecoding-devtools 1.0.1

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/hooks/full-suite.mjs +156 -0
  4. package/hooks/lib/io.mjs +135 -0
  5. package/hooks/maestri-protocol.mjs +48 -0
  6. package/hooks/secret-scan.mjs +90 -0
  7. package/installer/cli.mjs +263 -0
  8. package/installer/clis.mjs +46 -0
  9. package/installer/convert.mjs +91 -0
  10. package/installer/detect.mjs +389 -0
  11. package/installer/execute.mjs +386 -0
  12. package/installer/hooks.mjs +67 -0
  13. package/installer/json-merge.mjs +211 -0
  14. package/installer/panel.mjs +99 -0
  15. package/installer/plan.mjs +255 -0
  16. package/installer/state.mjs +105 -0
  17. package/installer/third-party.json +16 -0
  18. package/installer/third-party.mjs +204 -0
  19. package/npm-shrinkwrap.json +80 -0
  20. package/package.json +43 -0
  21. package/rules/base.md +25 -0
  22. package/rules/context-on-demand.md +25 -0
  23. package/rules/maestri-messaging.md +10 -0
  24. package/rules/maestri-usage.md +12 -0
  25. package/rules/model-tiers.md +17 -0
  26. package/rules/risk-process.md +69 -0
  27. package/rules/testing.md +53 -0
  28. package/skills/maestri-agent-cli/SKILL.md +150 -0
  29. package/skills/maestri-agent-cli/references/_template.md +62 -0
  30. package/skills/maestri-agent-cli/references/antigravity.md +206 -0
  31. package/skills/maestri-agent-cli/references/claude-code.md +203 -0
  32. package/skills/maestri-agent-cli/references/codex.md +190 -0
  33. package/skills/maestri-budget/SKILL.md +118 -0
  34. package/skills/maestri-protocol/SKILL.md +72 -0
  35. package/skills/risk-triage/SKILL.md +44 -0
  36. package/skills/security-surfaces/SKILL.md +31 -0
  37. package/skills/security-surfaces/surfaces/accounts-and-data.md +41 -0
  38. package/skills/security-surfaces/surfaces/inputs-and-services.md +47 -0
  39. package/skills/security-surfaces/surfaces/money.md +57 -0
  40. package/skills/security-surfaces/surfaces/secrets-and-infra.md +43 -0
  41. package/templates/AGENTS.md +68 -0
  42. package/templates/CLAUDE.md +1 -0
  43. package/templates/coderabbit.yaml +127 -0
@@ -0,0 +1,389 @@
1
+ // Installer detection reads machine and project state without writing files.
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { CLIS, NODE_MINIMUM } from "./clis.mjs";
8
+ import { readState } from "./state.mjs";
9
+
10
+ const isWindows = (platform) => platform === "win32";
11
+
12
+ // Expand home and environment variables in directory paths.
13
+ export function expand(directory, { env = process.env, home = os.homedir() } = {}) {
14
+ return directory
15
+ .replace(/^~(?=$|[\\/])/, home)
16
+ .replace(/%([^%]+)%/g, (_, name) => env[name] ?? env[name.toUpperCase()] ?? `%${name}%`);
17
+ }
18
+
19
+ // Find executables in directories, using PATHEXT on Windows.
20
+ export function findIn(bin, directories, { env = process.env, platform = process.platform } = {}) {
21
+ const exts = isWindows(platform)
22
+ ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean).map((e) => e.toLowerCase())
23
+ : [""];
24
+ for (const directory of directories) {
25
+ for (const ext of exts) {
26
+ const filePath = path.join(directory, bin + ext);
27
+ try {
28
+ const st = fs.statSync(filePath);
29
+ if (!st.isFile()) continue;
30
+ if (!isWindows(platform)) fs.accessSync(filePath, fs.constants.X_OK);
31
+ return filePath;
32
+ } catch (error) {
33
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") {
34
+ error.filePath = filePath;
35
+ throw error;
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export function pathDirectories(env = process.env) {
44
+ return (env.PATH ?? env.Path ?? "")
45
+ .split(path.delimiter)
46
+ .map((p) => p.replace(/^"(.*)"$/, "$1"))
47
+ .filter(Boolean);
48
+ }
49
+
50
+ // Extract the first x.y.z version from --version output.
51
+ export function extractVersion(output) {
52
+ return output?.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
53
+ }
54
+
55
+ function run(filePath, args, env) {
56
+ // Windows cmd and bat files require a shell; paths come from disk and arguments are fixed.
57
+ // Installer arguments never contain user input.
58
+ const options = { encoding: "utf8", timeout: 15000, windowsHide: true, env };
59
+ const r = /\.(cmd|bat)$/i.test(filePath)
60
+ ? spawnSync(`"${filePath}" ${args.join(" ")}`, { ...options, shell: true })
61
+ : spawnSync(filePath, args, options);
62
+ if (r.error) return { ok: false, error: r.error.message };
63
+ return { ok: r.status === 0, output: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
64
+ }
65
+
66
+ // Run --version: an existing executable must also respond.
67
+ export function versionOf(filePath, { env = process.env } = {}) {
68
+ const r = run(filePath, ["--version"], env);
69
+ if (!r.ok) return { ok: false, error: r.error ?? (r.output || "--version failed") };
70
+ return { ok: true, version: extractVersion(r.output) };
71
+ }
72
+
73
+ // Detect CLIs on PATH or in known installation directories outside PATH.
74
+ export function detectCli(cli, { env = process.env, platform = process.platform,
75
+ home = env.HOME ?? env.USERPROFILE ?? os.homedir() } = {}) {
76
+ env = { ...env, HOME: home, USERPROFILE: home };
77
+ let onPath;
78
+ let filePath;
79
+ try {
80
+ onPath = findIn(cli.bin, pathDirectories(env), { env, platform });
81
+ const directories = (isWindows(platform) ? cli.directories.win : cli.directories.unix).map((p) => expand(p, { env, home }));
82
+ filePath = onPath ?? findIn(cli.bin, directories, { env, platform });
83
+ } catch (error) {
84
+ return { id: cli.id, name: cli.name, state: "error", filePath: error.filePath, error: error.message };
85
+ }
86
+ const install = isWindows(platform) ? cli.install.win : cli.install.unix;
87
+ if (!filePath) return { id: cli.id, name: cli.name, state: "missing", install };
88
+ const v = versionOf(filePath, { env });
89
+ const base = { id: cli.id, name: cli.name, filePath, directory: path.dirname(filePath) };
90
+ if (!v.ok) return { ...base, state: "error", error: v.error };
91
+ return { ...base, state: onPath ? "ok" : "off-path", version: v.version };
92
+ }
93
+
94
+ export function checkNode(version = process.versions.node) {
95
+ const major = Number(version.split(".")[0]);
96
+ return { version, ok: major >= NODE_MINIMUM, minimum: NODE_MINIMUM };
97
+ }
98
+
99
+ // Command-line tools must be on PATH and respond to --version, such as git and gitleaks.
100
+ export function checkTool(bin, { env = process.env, platform = process.platform } = {}) {
101
+ let filePath;
102
+ try { filePath = findIn(bin, pathDirectories(env), { env, platform }); }
103
+ catch (error) { return { ok: false, state: "error", filePath: error.filePath, error: error.message }; }
104
+ if (!filePath) return { ok: false, error: "not found in PATH" };
105
+ const v = versionOf(filePath, { env });
106
+ return v.ok ? { ok: true, filePath, version: v.version } : { ok: false, filePath, error: v.error };
107
+ }
108
+
109
+ // Suggest ECC rule packages from project files.
110
+ const BY_FILE = [
111
+ [/^(package\.json|tsconfig\.json)$/, "typescript"],
112
+ [/^(pyproject\.toml|requirements.*\.txt|setup\.py|Pipfile)$/, "python"],
113
+ [/^go\.mod$/, "golang"],
114
+ [/^Cargo\.toml$/, "rust"],
115
+ [/^(pom\.xml|build\.gradle)$/, "java"],
116
+ [/^build\.gradle\.kts$/, "kotlin"],
117
+ [/^composer\.json$/, "php"],
118
+ [/^Gemfile$/, "ruby"],
119
+ [/^Package\.swift$/, "swift"],
120
+ [/\.(csproj|sln)$/, "csharp"],
121
+ [/\.fsproj$/, "fsharp"],
122
+ [/^pubspec\.yaml$/, "dart"],
123
+ [/^CMakeLists\.txt$/, "cpp"],
124
+ [/^(cpanfile|Makefile\.PL)$/, "perl"],
125
+ [/^oh-package\.json5$/, "arkts"],
126
+ ];
127
+ const BY_DEPENDENCY = [
128
+ ["react-native", "react-native"],
129
+ ["react", "react"],
130
+ ["next", "react"],
131
+ ["vue", "vue"],
132
+ ["nuxt", "nuxt"],
133
+ ["@angular/core", "angular"],
134
+ ];
135
+
136
+ export function detectStacks({ files = [], dependencies = [] }) {
137
+ const stacks = new Set();
138
+ for (const name of files) for (const [re, packageName] of BY_FILE) if (re.test(name)) stacks.add(packageName);
139
+ for (const [dep, packageName] of BY_DEPENDENCY) if (dependencies.includes(dep)) stacks.add(packageName);
140
+ return [...stacks].sort();
141
+ }
142
+
143
+ const IGNORED = new Set(["node_modules", ".git", "vendor", "dist", "build", "target", ".venv", "venv"]);
144
+
145
+ // Scan the root and two levels of monorepo directories, skipping generated directories.
146
+ function projectFiles(root, depth = 2) {
147
+ const files = [];
148
+ const dependencies = new Set();
149
+ const visit = (directory, level) => {
150
+ let entries;
151
+ try {
152
+ entries = fs.readdirSync(directory, { withFileTypes: true });
153
+ } catch {
154
+ return;
155
+ }
156
+ for (const e of entries) {
157
+ if (e.isDirectory()) {
158
+ if (level < depth && !IGNORED.has(e.name) && !e.name.startsWith(".")) {
159
+ visit(path.join(directory, e.name), level + 1);
160
+ }
161
+ } else {
162
+ files.push(e.name);
163
+ if (e.name === "package.json") {
164
+ try {
165
+ const pkg = JSON.parse(fs.readFileSync(path.join(directory, e.name), "utf8"));
166
+ for (const d of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) dependencies.add(d);
167
+ } catch {
168
+ // Ignore dependencies in invalid package.json files.
169
+ }
170
+ }
171
+ }
172
+ }
173
+ };
174
+ visit(root, 0);
175
+ return { files, dependencies: [...dependencies] };
176
+ }
177
+
178
+ export function detectProject(cwd = process.cwd(), { env = process.env } = {}) {
179
+ const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, env, encoding: "utf8", windowsHide: true });
180
+ if (r.error) return { inRepository: false, reason: "git cannot run" };
181
+ if (r.status !== 0) {
182
+ // Git status 128 can also mean repository refusal, such as dubious ownership; report the reason.
183
+ const error = (r.stderr ?? "").trim().split("\n")[0];
184
+ const outside = /not a git repository/i.test(error);
185
+ return { inRepository: false, reason: outside ? "outside a git repository" : error || "git failed" };
186
+ }
187
+ const root = path.resolve(r.stdout.trim());
188
+ return { inRepository: true, root, stacks: detectStacks(projectFiles(root)) };
189
+ }
190
+
191
+ function normalizePlugin(cli, item) {
192
+ const { installedSection, ...fields } = item;
193
+ const name = item.name ?? item.id;
194
+ if (typeof name !== "string" || !name.trim()) throw new Error("Plugin has no recognized name");
195
+ const status = typeof (item.status ?? item.state) === "string"
196
+ ? (item.status ?? item.state).replace(/^[^a-z]+/i, "").trim() : null;
197
+ let installed = null; // Missing status proves neither installation nor absence.
198
+ if (typeof item.installed === "boolean") installed = item.installed;
199
+ else if (/^installed\b/i.test(status ?? "")) installed = true;
200
+ else if (/^(?:not[\s_-]+installed|uninstalled)\b/i.test(status ?? "")) installed = false;
201
+ else if (cli !== "codex" && /\b(?:loaded|enabled|disabled|installed)\b/i.test(status ?? "")) installed = true;
202
+ else if (cli !== "codex" && typeof installedSection === "boolean") installed = installedSection;
203
+ else if (cli !== "codex" && status === null && typeof item.enabled === "boolean") installed = true;
204
+ return { ...fields, name: name.trim(), version: item.version ?? null, status, installed };
205
+ }
206
+
207
+ export function parsePlugins(cli, output) {
208
+ const text = output.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").trim();
209
+ if (cli === "agy" || /^[\[{]/.test(text)) {
210
+ const json = JSON.parse(text);
211
+ const items = Array.isArray(json) ? json : json?.plugins;
212
+ if (!Array.isArray(items)) throw new Error("Plugin response has no recognized list");
213
+ return items.map((item) => {
214
+ if (!item || typeof item !== "object") throw new Error("Plugin item has no recognized fields");
215
+ return normalizePlugin(cli, item);
216
+ });
217
+ }
218
+ if (!text) throw new Error("Empty plugin response");
219
+ const usableLines = text.split(/\r?\n/).map((line) => line.trim())
220
+ .filter((line) => line && !/^warning\b/i.test(line));
221
+ const usableText = usableLines.join(" ").replace(/\s+/g, " ").trim();
222
+ // Official empty plugin-list responses may be preceded by PATH alias warnings.
223
+ // Keep warnings separate from the actual response.
224
+ if (cli === "codex") {
225
+ if (/^no marketplace plugins found\.?$/i.test(usableText)) return [];
226
+ }
227
+ if (cli === "claude" && /^no plugins installed\. use (?:\x60claude plugin install\x60|claude plugin install) to install a plugin\.$/i.test(usableText)) return [];
228
+ const items = [];
229
+ let installedInSection;
230
+ let current;
231
+ let awaitingMarketplaceTable = false;
232
+ for (const line of text.split(/\r?\n/)) {
233
+ const clean = line.trim();
234
+ if (!clean) continue;
235
+ if (/^warning\b/i.test(clean)) continue;
236
+ if (/^Marketplace\s+`[^`]+`$/i.test(clean)) {
237
+ awaitingMarketplaceTable = true;
238
+ current = undefined;
239
+ continue;
240
+ }
241
+ if (/^PLUGIN\s+STATUS\s+VERSION\s+SOURCE$/i.test(clean)) {
242
+ awaitingMarketplaceTable = false;
243
+ continue;
244
+ }
245
+ if (awaitingMarketplaceTable) {
246
+ if (/^Remote catalog$/i.test(clean)) continue;
247
+ if ((path.win32.isAbsolute(clean) || path.posix.isAbsolute(clean)) && /[/\\]marketplace\.json$/i.test(clean)) continue;
248
+ throw new Error("Unrecognized plugin response format");
249
+ }
250
+ if (/^installed plugins\s*:\s*$/i.test(clean)) { installedInSection = true; current = undefined; continue; }
251
+ if (/^(?:available|catalog) plugins\s*:\s*$/i.test(clean)) { installedInSection = false; current = undefined; continue; }
252
+ const field = cli === "claude" && clean.match(/^(Version|Scope|Status|Path):\s*(.*)$/i);
253
+ if (field) {
254
+ if (!current || !field[2]) throw new Error("Incomplete plugin response");
255
+ if (field[1].toLowerCase() !== "path") current[field[1].toLowerCase()] = field[2];
256
+ continue;
257
+ }
258
+ if (cli === "claude" && /:$/.test(clean)) {
259
+ // Synced and future section headings are metadata, not plugin entries.
260
+ installedInSection = undefined;
261
+ current = undefined;
262
+ continue;
263
+ }
264
+ // Claude lists installed plugins in blocks with version, scope and status below each name.
265
+ const claudeName = cli === "claude" && clean.match(/^(?:[❯→➜›>•*-]\s*)?([a-z\d._/-]+@[a-z\d._/-]+)$/i);
266
+ if (claudeName) {
267
+ current = { name: claudeName[1] };
268
+ if (installedInSection !== undefined) current.installedSection = installedInSection;
269
+ items.push(current);
270
+ continue;
271
+ }
272
+ if (/^[|:\s+─━┌┬┐└┴┘├┼┤-]+$/.test(clean)) continue;
273
+ const cells = clean.includes("|") ? clean.split("|").map((c) => c.trim()).filter(Boolean) : null;
274
+ const m = cells ? null : clean.match(/^([a-z\d_.@/-]+)\s+(.+)$/i);
275
+ const name = cells?.[0] ?? m?.[1];
276
+ if (!name) throw new Error("Unrecognized plugin response format");
277
+ if (/^(?:name|plugin|plugins)$/i.test(name) && /\bstatus\b/i.test(clean)) continue;
278
+ const rest = cells ? cells.slice(1).join(" ") : m[2];
279
+ const status = cells?.slice(1).find((c) => /^(?:not\s+)?installed\b/i.test(c))
280
+ ?? rest.match(/^(?:\d+\.\d+\.\d+\s+)?((?:not\s+)?installed\b(?:,\s*(?:enabled|disabled))?)/i)?.[1]
281
+ ?? (cli !== "codex" ? rest.match(/\b(enabled|disabled)\b/i)?.[1] : null);
282
+ const version = extractVersion(rest);
283
+ if (!/^[a-z\d_.@/-]+$/i.test(name) || (!status && !version)) throw new Error("Unrecognized plugin response format");
284
+ current = { name, status: status ?? null, version, original: line };
285
+ if (cli === "claude" && installedInSection !== undefined) current.installedSection = installedInSection;
286
+ items.push(current);
287
+ }
288
+ // Headers and table scaffolding alone never establish absence.
289
+ if (!items.length || awaitingMarketplaceTable) throw new Error("Unrecognized plugin response format");
290
+ return items.map((item) => normalizePlugin(cli, item));
291
+ }
292
+
293
+ // executePlugins matches spawnSync; tests never invoke actual CLIs.
294
+ export function detectInstalled({ home, project = null, clis = [], executePlugins = spawnSync } = {}) {
295
+ if (!home) throw new Error("detectInstalled requires home");
296
+ const state = readState({ home });
297
+ const names = fs.readdirSync(new URL("../skills/", import.meta.url), { withFileTypes: true })
298
+ .filter((e) => e.isDirectory()).map((e) => e.name).sort();
299
+ const skills = [];
300
+ const directories = {
301
+ claude: [["global", path.join(home, ".claude", "skills")]],
302
+ codex: [["global", path.join(home, ".agents", "skills")]],
303
+ agy: [["global", path.join(home, ".gemini", "config", "skills")]],
304
+ };
305
+ if (project) {
306
+ directories.claude.push(["project", path.join(project, ".claude", "skills")]);
307
+ for (const cli of ["codex", "agy"]) directories[cli].push(["project", path.join(project, ".agents", "skills")]);
308
+ }
309
+ for (const [cli, locations] of Object.entries(directories)) {
310
+ for (const [scope, directory] of locations) {
311
+ for (const name of names) {
312
+ const filePath = path.join(directory, name);
313
+ try {
314
+ fs.lstatSync(filePath);
315
+ skills.push({ cli, scope, name, filePath });
316
+ } catch (error) {
317
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
318
+ }
319
+ }
320
+ }
321
+ }
322
+ const plugins = {};
323
+ for (const { id } of CLIS) {
324
+ const cli = clis.find((c) => c.id === id);
325
+ plugins[id] = { state: "unknown", items: null, error: "CLI not detected" };
326
+ if (!cli?.filePath || cli.state === "missing") continue;
327
+ try {
328
+ if (id === "codex" || id === "claude") {
329
+ const configDir = path.join(home, id === "codex" ? ".codex" : ".claude");
330
+ try {
331
+ if (!fs.statSync(configDir).isDirectory()) {
332
+ throw new Error(`Configuration path is not a directory: ${configDir}`);
333
+ }
334
+ } catch (error) {
335
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
336
+ plugins[id] = { state: "known", items: [], output: "", reason: "configuration directory missing" };
337
+ continue;
338
+ }
339
+ }
340
+ const args = ["plugin", "list", ...(id === "agy" ? ["--json"] : [])];
341
+ const env = { ...process.env };
342
+ for (const [name, value] of Object.entries({ HOME: home, USERPROFILE: home,
343
+ CODEX_HOME: path.join(home, ".codex"), CLAUDE_CONFIG_DIR: path.join(home, ".claude") })) {
344
+ // Avoid differently capitalized duplicate environment keys in Windows subprocesses.
345
+ if (process.platform === "win32") for (const existing of Object.keys(env)) {
346
+ if (existing.toUpperCase() === name) delete env[existing];
347
+ }
348
+ env[name] = value;
349
+ }
350
+ // Codex remote catalogs may exceed 2 MB and take longer than three seconds.
351
+ const options = { encoding: "utf8", timeout: 30000, maxBuffer: 64 * 1024 * 1024, windowsHide: true,
352
+ cwd: project ?? home, env };
353
+ let command = cli.filePath;
354
+ let argumentsList = args;
355
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
356
+ if (/["%!\r\n\0]/.test(command)) throw new Error("Invalid CLI path for cmd.exe");
357
+ argumentsList = ["/d", "/s", "/c", `"\"${command}\" ${args.join(" ")}"`];
358
+ command = "cmd.exe";
359
+ options.windowsVerbatimArguments = true;
360
+ }
361
+ const r = executePlugins(command, argumentsList, options);
362
+ if (r.error) throw r.error;
363
+ if (r.status !== 0) throw new Error(`plugin list failed (${r.status ?? r.signal ?? "no status"})`);
364
+ const output = String(r.stdout ?? "").trim();
365
+ const items = parsePlugins(id, output);
366
+ plugins[id] = { state: "known", items, output };
367
+ } catch (error) {
368
+ plugins[id] = { state: "unknown", items: null, error: error.message };
369
+ }
370
+ }
371
+ return { state, skills, plugins };
372
+ }
373
+
374
+ export function detectAll({ cwd = process.cwd(), env = process.env, platform = process.platform,
375
+ home = env.HOME ?? env.USERPROFILE ?? os.homedir(), executePlugins = spawnSync } = {}) {
376
+ env = { ...env, HOME: home, USERPROFILE: home };
377
+ const clis = CLIS.map((cli) => detectCli(cli, { env, platform, home }));
378
+ const project = detectProject(cwd, { env });
379
+ return {
380
+ platform,
381
+ node: checkNode(),
382
+ git: checkTool("git", { env, platform }),
383
+ gitleaks: checkTool("gitleaks", { env, platform }),
384
+ clis,
385
+ project,
386
+ installed: detectInstalled({ home, project: project.inRepository ? project.root : null,
387
+ clis, executePlugins }),
388
+ };
389
+ }