safeword 0.8.6 → 0.8.9
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/dist/{check-I2J6THGQ.js → check-QMAGWUOA.js} +24 -22
- package/dist/check-QMAGWUOA.js.map +1 -0
- package/dist/{chunk-DES5CSPH.js → chunk-4URRFBUS.js} +10 -10
- package/dist/chunk-4URRFBUS.js.map +1 -0
- package/dist/{chunk-DXT6TWW4.js → chunk-CLSGXTOL.js} +232 -435
- package/dist/chunk-CLSGXTOL.js.map +1 -0
- package/dist/{chunk-W66Z3C5H.js → chunk-FJYRWU2V.js} +5 -5
- package/dist/chunk-FJYRWU2V.js.map +1 -0
- package/dist/{chunk-VXKJ5ZIV.js → chunk-KQ6BLN6W.js} +172 -155
- package/dist/chunk-KQ6BLN6W.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/cli.js.map +1 -1
- package/dist/{diff-4YFDNEZB.js → diff-2T7UDES7.js} +12 -12
- package/dist/diff-2T7UDES7.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/{reset-QVERBAQJ.js → reset-QRXG7KZZ.js} +8 -8
- package/dist/reset-QRXG7KZZ.js.map +1 -0
- package/dist/{setup-ZSMZ7HZG.js → setup-QUUJ7SH3.js} +8 -8
- package/dist/setup-QUUJ7SH3.js.map +1 -0
- package/dist/sync-ISBJ7X2T.js +9 -0
- package/dist/{upgrade-WILVVHUY.js → upgrade-FALAUUKE.js} +22 -10
- package/dist/upgrade-FALAUUKE.js.map +1 -0
- package/package.json +15 -14
- package/templates/SAFEWORD.md +4 -2
- package/templates/commands/cleanup-zombies.md +48 -0
- package/templates/guides/zombie-process-cleanup.md +40 -24
- package/templates/scripts/cleanup-zombies.sh +222 -0
- package/templates/scripts/lint-md.sh +0 -0
- package/dist/check-I2J6THGQ.js.map +0 -1
- package/dist/chunk-DES5CSPH.js.map +0 -1
- package/dist/chunk-DXT6TWW4.js.map +0 -1
- package/dist/chunk-VXKJ5ZIV.js.map +0 -1
- package/dist/chunk-W66Z3C5H.js.map +0 -1
- package/dist/diff-4YFDNEZB.js.map +0 -1
- package/dist/reset-QVERBAQJ.js.map +0 -1
- package/dist/setup-ZSMZ7HZG.js.map +0 -1
- package/dist/sync-VQW5DSTV.js +0 -9
- package/dist/upgrade-WILVVHUY.js.map +0 -1
- /package/dist/{sync-VQW5DSTV.js.map → sync-ISBJ7X2T.js.map} +0 -0
|
@@ -4,30 +4,30 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// src/utils/fs.ts
|
|
6
6
|
import {
|
|
7
|
+
chmodSync,
|
|
7
8
|
existsSync,
|
|
8
9
|
mkdirSync,
|
|
10
|
+
readdirSync,
|
|
9
11
|
readFileSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
rmSync,
|
|
12
12
|
rmdirSync,
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync
|
|
15
15
|
} from "fs";
|
|
16
|
-
import
|
|
16
|
+
import nodePath from "path";
|
|
17
17
|
import { fileURLToPath } from "url";
|
|
18
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
function
|
|
18
|
+
var __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
function getTemplatesDirectory() {
|
|
20
20
|
const knownTemplateFile = "SAFEWORD.md";
|
|
21
21
|
const candidates = [
|
|
22
|
-
join(__dirname, "..", "templates"),
|
|
22
|
+
nodePath.join(__dirname, "..", "templates"),
|
|
23
23
|
// From dist/ (flat bundled)
|
|
24
|
-
join(__dirname, "..", "..", "templates"),
|
|
24
|
+
nodePath.join(__dirname, "..", "..", "templates"),
|
|
25
25
|
// From src/utils/ or dist/utils/
|
|
26
|
-
join(__dirname, "templates")
|
|
26
|
+
nodePath.join(__dirname, "templates")
|
|
27
27
|
// Direct sibling (unlikely but safe)
|
|
28
28
|
];
|
|
29
29
|
for (const candidate of candidates) {
|
|
30
|
-
if (existsSync(join(candidate, knownTemplateFile))) {
|
|
30
|
+
if (existsSync(nodePath.join(candidate, knownTemplateFile))) {
|
|
31
31
|
return candidate;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -36,20 +36,20 @@ function getTemplatesDir() {
|
|
|
36
36
|
function exists(path) {
|
|
37
37
|
return existsSync(path);
|
|
38
38
|
}
|
|
39
|
-
function
|
|
39
|
+
function ensureDirectory(path) {
|
|
40
40
|
if (!existsSync(path)) {
|
|
41
41
|
mkdirSync(path, { recursive: true });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
function readFile(path) {
|
|
45
|
-
return readFileSync(path, "
|
|
45
|
+
return readFileSync(path, "utf8");
|
|
46
46
|
}
|
|
47
47
|
function readFileSafe(path) {
|
|
48
|
-
if (!existsSync(path)) return
|
|
49
|
-
return readFileSync(path, "
|
|
48
|
+
if (!existsSync(path)) return void 0;
|
|
49
|
+
return readFileSync(path, "utf8");
|
|
50
50
|
}
|
|
51
51
|
function writeFile(path, content) {
|
|
52
|
-
|
|
52
|
+
ensureDirectory(nodePath.dirname(path));
|
|
53
53
|
writeFileSync(path, content);
|
|
54
54
|
}
|
|
55
55
|
function remove(path) {
|
|
@@ -70,146 +70,34 @@ function makeScriptsExecutable(dirPath) {
|
|
|
70
70
|
if (!existsSync(dirPath)) return;
|
|
71
71
|
for (const file of readdirSync(dirPath)) {
|
|
72
72
|
if (file.endsWith(".sh")) {
|
|
73
|
-
chmodSync(join(dirPath, file), 493);
|
|
73
|
+
chmodSync(nodePath.join(dirPath, file), 493);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
function readJson(path) {
|
|
78
78
|
const content = readFileSafe(path);
|
|
79
|
-
if (!content) return
|
|
79
|
+
if (!content) return void 0;
|
|
80
80
|
try {
|
|
81
81
|
return JSON.parse(content);
|
|
82
82
|
} catch {
|
|
83
|
-
return
|
|
83
|
+
return void 0;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
function writeJson(path, data) {
|
|
87
|
-
writeFile(path, JSON.stringify(data,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// src/utils/project-detector.ts
|
|
91
|
-
import { readdirSync as readdirSync2 } from "fs";
|
|
92
|
-
import { join as join2 } from "path";
|
|
93
|
-
function hasShellScripts(cwd, maxDepth = 4) {
|
|
94
|
-
const excludeDirs = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
|
|
95
|
-
function scan(dir, depth) {
|
|
96
|
-
if (depth > maxDepth) return false;
|
|
97
|
-
try {
|
|
98
|
-
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
99
|
-
for (const entry of entries) {
|
|
100
|
-
if (entry.isFile() && entry.name.endsWith(".sh")) {
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
if (entry.isDirectory() && !excludeDirs.has(entry.name)) {
|
|
104
|
-
if (scan(join2(dir, entry.name), depth + 1)) {
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
return scan(cwd, 0);
|
|
114
|
-
}
|
|
115
|
-
function detectProjectType(packageJson, cwd) {
|
|
116
|
-
const deps = packageJson.dependencies || {};
|
|
117
|
-
const devDeps = packageJson.devDependencies || {};
|
|
118
|
-
const allDeps = { ...deps, ...devDeps };
|
|
119
|
-
const hasTypescript = "typescript" in allDeps;
|
|
120
|
-
const hasReact = "react" in deps || "react" in devDeps;
|
|
121
|
-
const hasNextJs = "next" in deps;
|
|
122
|
-
const hasAstro = "astro" in deps || "astro" in devDeps;
|
|
123
|
-
const hasVue = "vue" in deps || "vue" in devDeps;
|
|
124
|
-
const hasNuxt = "nuxt" in deps;
|
|
125
|
-
const hasSvelte = "svelte" in deps || "svelte" in devDeps;
|
|
126
|
-
const hasSvelteKit = "@sveltejs/kit" in deps || "@sveltejs/kit" in devDeps;
|
|
127
|
-
const hasElectron = "electron" in deps || "electron" in devDeps;
|
|
128
|
-
const hasVitest = "vitest" in devDeps;
|
|
129
|
-
const hasPlaywright = "@playwright/test" in devDeps;
|
|
130
|
-
const hasTailwind = "tailwindcss" in allDeps;
|
|
131
|
-
const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
|
|
132
|
-
const isPublishable = hasEntryPoints && packageJson.private !== true;
|
|
133
|
-
const hasShell = cwd ? hasShellScripts(cwd) : false;
|
|
134
|
-
return {
|
|
135
|
-
typescript: hasTypescript,
|
|
136
|
-
react: hasReact || hasNextJs,
|
|
137
|
-
// Next.js implies React
|
|
138
|
-
nextjs: hasNextJs,
|
|
139
|
-
astro: hasAstro,
|
|
140
|
-
vue: hasVue || hasNuxt,
|
|
141
|
-
// Nuxt implies Vue
|
|
142
|
-
nuxt: hasNuxt,
|
|
143
|
-
svelte: hasSvelte || hasSvelteKit,
|
|
144
|
-
// SvelteKit implies Svelte
|
|
145
|
-
sveltekit: hasSvelteKit,
|
|
146
|
-
electron: hasElectron,
|
|
147
|
-
vitest: hasVitest,
|
|
148
|
-
playwright: hasPlaywright,
|
|
149
|
-
tailwind: hasTailwind,
|
|
150
|
-
publishableLibrary: isPublishable,
|
|
151
|
-
shell: hasShell
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// src/templates/content.ts
|
|
156
|
-
var AGENTS_MD_LINK = `**\u26A0\uFE0F ALWAYS READ FIRST:** \`.safeword/SAFEWORD.md\`
|
|
157
|
-
|
|
158
|
-
The SAFEWORD.md file contains core development patterns, workflows, and conventions.
|
|
159
|
-
Read it BEFORE working on any task in this project.
|
|
160
|
-
|
|
161
|
-
---`;
|
|
162
|
-
function getPrettierConfig(projectType) {
|
|
163
|
-
const config = {
|
|
164
|
-
semi: true,
|
|
165
|
-
singleQuote: true,
|
|
166
|
-
tabWidth: 2,
|
|
167
|
-
trailingComma: "es5",
|
|
168
|
-
printWidth: 100,
|
|
169
|
-
endOfLine: "lf"
|
|
170
|
-
};
|
|
171
|
-
const plugins = [];
|
|
172
|
-
if (projectType.astro) plugins.push("prettier-plugin-astro");
|
|
173
|
-
if (projectType.svelte) plugins.push("prettier-plugin-svelte");
|
|
174
|
-
if (projectType.shell) plugins.push("prettier-plugin-sh");
|
|
175
|
-
if (projectType.tailwind) plugins.push("prettier-plugin-tailwindcss");
|
|
176
|
-
if (plugins.length > 0) {
|
|
177
|
-
config.plugins = plugins;
|
|
178
|
-
}
|
|
179
|
-
return JSON.stringify(config, null, 2) + "\n";
|
|
180
|
-
}
|
|
181
|
-
function getLintStagedConfig(projectType) {
|
|
182
|
-
const config = {
|
|
183
|
-
"*.{js,jsx,ts,tsx,mjs,mts,cjs,cts}": ["eslint --fix", "prettier --write"],
|
|
184
|
-
"*.{vue,svelte,astro}": ["eslint --fix", "prettier --write"],
|
|
185
|
-
"*.{json,css,scss,html,yaml,yml,graphql}": ["prettier --write"],
|
|
186
|
-
"*.md": ["markdownlint-cli2 --fix", "prettier --write"]
|
|
187
|
-
};
|
|
188
|
-
if (projectType.shell) {
|
|
189
|
-
config["*.sh"] = ["shellcheck", "prettier --write"];
|
|
190
|
-
}
|
|
191
|
-
return config;
|
|
87
|
+
writeFile(path, `${JSON.stringify(data, void 0, 2)}
|
|
88
|
+
`);
|
|
192
89
|
}
|
|
193
90
|
|
|
194
91
|
// src/templates/config.ts
|
|
195
|
-
function getEslintConfig(
|
|
196
|
-
return `/* eslint-disable import-x/no-unresolved, no-undef -- dynamic imports for optional framework plugins
|
|
92
|
+
function getEslintConfig() {
|
|
93
|
+
return `/* eslint-disable import-x/no-unresolved, no-undef -- dynamic imports for optional framework plugins */
|
|
197
94
|
import { readFileSync } from "fs";
|
|
198
95
|
import { dirname, join } from "path";
|
|
199
96
|
import { fileURLToPath } from "url";
|
|
200
|
-
import
|
|
201
|
-
import js from "@eslint/js";
|
|
202
|
-
import { importX } from "eslint-plugin-import-x";
|
|
203
|
-
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
|
204
|
-
import sonarjs from "eslint-plugin-sonarjs";
|
|
205
|
-
import sdl from "@microsoft/eslint-plugin-sdl";
|
|
206
|
-
import playwright from "eslint-plugin-playwright";
|
|
207
|
-
import unicorn from "eslint-plugin-unicorn";
|
|
97
|
+
import safeword from "eslint-plugin-safeword";
|
|
208
98
|
import eslintConfigPrettier from "eslint-config-prettier";
|
|
209
|
-
${options.boundaries ? 'import boundariesConfig from "./.safeword/eslint-boundaries.config.mjs";' : ""}
|
|
210
99
|
|
|
211
100
|
// Read package.json relative to this config file (not CWD)
|
|
212
|
-
// This ensures correct detection in monorepos where lint-staged may run from subdirectories
|
|
213
101
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
214
102
|
const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
|
215
103
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -236,122 +124,55 @@ if (deps["astro"]) ignores.push(".astro/");
|
|
|
236
124
|
if (deps["vue"] || deps["nuxt"]) ignores.push(".nuxt/");
|
|
237
125
|
if (deps["svelte"] || deps["@sveltejs/kit"]) ignores.push(".svelte-kit/");
|
|
238
126
|
|
|
239
|
-
//
|
|
127
|
+
// Select appropriate safeword config based on detected framework
|
|
128
|
+
// Order matters: most specific first
|
|
129
|
+
let baseConfig;
|
|
130
|
+
if (deps["next"]) {
|
|
131
|
+
baseConfig = safeword.configs.recommendedTypeScriptNext;
|
|
132
|
+
} else if (deps["react"]) {
|
|
133
|
+
baseConfig = safeword.configs.recommendedTypeScriptReact;
|
|
134
|
+
} else if (deps["astro"]) {
|
|
135
|
+
baseConfig = safeword.configs.astro;
|
|
136
|
+
} else if (deps["typescript"] || deps["typescript-eslint"]) {
|
|
137
|
+
baseConfig = safeword.configs.recommendedTypeScript;
|
|
138
|
+
} else {
|
|
139
|
+
baseConfig = safeword.configs.recommended;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Start with ignores + safeword config
|
|
240
143
|
const configs = [
|
|
241
144
|
{ ignores },
|
|
242
|
-
|
|
243
|
-
importX.flatConfigs.recommended,
|
|
244
|
-
{
|
|
245
|
-
settings: {
|
|
246
|
-
"import-x/resolver-next": [createTypeScriptImportResolver()],
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
sonarjs.configs.recommended,
|
|
250
|
-
...sdl.configs.recommended,
|
|
251
|
-
unicorn.configs["flat/recommended"],
|
|
252
|
-
{
|
|
253
|
-
// Unicorn overrides for LLM-generated code
|
|
254
|
-
// Keep modern JS enforcement, disable overly pedantic rules
|
|
255
|
-
rules: {
|
|
256
|
-
"unicorn/prevent-abbreviations": "off", // ctx, dir, pkg, err are standard
|
|
257
|
-
"unicorn/no-null": "off", // null is valid JS
|
|
258
|
-
"unicorn/no-process-exit": "off", // CLI apps use process.exit
|
|
259
|
-
"unicorn/import-style": "off", // Named imports are fine
|
|
260
|
-
"unicorn/numeric-separators-style": "off", // Style preference
|
|
261
|
-
"unicorn/text-encoding-identifier-case": "off", // utf-8 vs utf8
|
|
262
|
-
"unicorn/switch-case-braces": "warn", // Good practice, not critical
|
|
263
|
-
"unicorn/catch-error-name": "warn", // Reasonable, auto-fixable
|
|
264
|
-
"unicorn/no-negated-condition": "off", // Sometimes clearer
|
|
265
|
-
"unicorn/no-array-reduce": "off", // Reduce is fine when readable
|
|
266
|
-
"unicorn/no-array-for-each": "off", // forEach is fine
|
|
267
|
-
"unicorn/prefer-module": "off", // CJS still valid
|
|
268
|
-
},
|
|
269
|
-
},
|
|
145
|
+
...baseConfig,
|
|
270
146
|
];
|
|
271
147
|
|
|
272
|
-
//
|
|
273
|
-
if (deps["
|
|
274
|
-
|
|
275
|
-
configs.push(importX.flatConfigs.typescript);
|
|
276
|
-
configs.push(...tseslint.default.configs.recommended);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// React/Next.js support
|
|
280
|
-
if (deps["react"] || deps["next"]) {
|
|
281
|
-
const react = await tryImport("eslint-plugin-react", "React");
|
|
282
|
-
const reactHooks = await tryImport("eslint-plugin-react-hooks", "React");
|
|
283
|
-
const jsxA11y = await tryImport("eslint-plugin-jsx-a11y", "React");
|
|
284
|
-
configs.push(react.default.configs.flat.recommended);
|
|
285
|
-
configs.push(react.default.configs.flat["jsx-runtime"]);
|
|
286
|
-
configs.push({
|
|
287
|
-
name: "react-hooks",
|
|
288
|
-
plugins: { "react-hooks": reactHooks.default },
|
|
289
|
-
rules: reactHooks.default.configs.recommended.rules,
|
|
290
|
-
});
|
|
291
|
-
configs.push(jsxA11y.default.flatConfigs.recommended);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Next.js plugin
|
|
295
|
-
if (deps["next"]) {
|
|
296
|
-
const nextPlugin = await tryImport("@next/eslint-plugin-next", "Next.js");
|
|
297
|
-
configs.push({
|
|
298
|
-
name: "nextjs",
|
|
299
|
-
plugins: { "@next/next": nextPlugin.default },
|
|
300
|
-
rules: nextPlugin.default.configs.recommended.rules,
|
|
301
|
-
});
|
|
148
|
+
// Add test configs if testing frameworks detected
|
|
149
|
+
if (deps["vitest"]) {
|
|
150
|
+
configs.push(...safeword.configs.vitest);
|
|
302
151
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (deps["astro"]) {
|
|
306
|
-
const astro = await tryImport("eslint-plugin-astro", "Astro");
|
|
307
|
-
configs.push(...astro.default.configs.recommended);
|
|
152
|
+
if (deps["playwright"] || deps["@playwright/test"]) {
|
|
153
|
+
configs.push(...safeword.configs.playwright);
|
|
308
154
|
}
|
|
309
155
|
|
|
310
|
-
//
|
|
156
|
+
// Frameworks NOT in eslint-plugin-safeword (dynamic imports)
|
|
311
157
|
if (deps["vue"] || deps["nuxt"]) {
|
|
312
158
|
const vue = await tryImport("eslint-plugin-vue", "Vue");
|
|
313
159
|
configs.push(...vue.default.configs["flat/recommended"]);
|
|
314
160
|
}
|
|
315
161
|
|
|
316
|
-
// Svelte support
|
|
317
162
|
if (deps["svelte"] || deps["@sveltejs/kit"]) {
|
|
318
163
|
const svelte = await tryImport("eslint-plugin-svelte", "Svelte");
|
|
319
164
|
configs.push(...svelte.default.configs.recommended);
|
|
320
165
|
}
|
|
321
166
|
|
|
322
|
-
// Electron support
|
|
323
167
|
if (deps["electron"]) {
|
|
324
168
|
const electron = await tryImport("@electron-toolkit/eslint-config", "Electron");
|
|
325
169
|
configs.push(electron.default);
|
|
326
170
|
}
|
|
327
171
|
|
|
328
|
-
// Vitest support (scoped to test files)
|
|
329
|
-
if (deps["vitest"]) {
|
|
330
|
-
const vitest = await tryImport("@vitest/eslint-plugin", "Vitest");
|
|
331
|
-
configs.push({
|
|
332
|
-
name: "vitest",
|
|
333
|
-
files: ["**/*.test.{js,ts,jsx,tsx}", "**/*.spec.{js,ts,jsx,tsx}", "**/tests/**"],
|
|
334
|
-
plugins: { vitest: vitest.default },
|
|
335
|
-
languageOptions: {
|
|
336
|
-
globals: { ...vitest.default.environments.env.globals },
|
|
337
|
-
},
|
|
338
|
-
rules: { ...vitest.default.configs.recommended.rules },
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Playwright for e2e tests (always included - safeword sets up Playwright)
|
|
343
|
-
configs.push({
|
|
344
|
-
name: "playwright",
|
|
345
|
-
files: ["**/e2e/**", "**/*.e2e.{js,ts,jsx,tsx}", "**/playwright/**"],
|
|
346
|
-
...playwright.configs["flat/recommended"],
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// Architecture boundaries${options.boundaries ? "\nconfigs.push(boundariesConfig);" : ""}
|
|
350
|
-
|
|
351
172
|
// eslint-config-prettier must be last to disable conflicting rules
|
|
352
173
|
configs.push(eslintConfigPrettier);
|
|
353
174
|
|
|
354
|
-
export default
|
|
175
|
+
export default configs;
|
|
355
176
|
`;
|
|
356
177
|
}
|
|
357
178
|
var CURSOR_HOOKS = {
|
|
@@ -426,166 +247,50 @@ var SETTINGS_HOOKS = {
|
|
|
426
247
|
]
|
|
427
248
|
};
|
|
428
249
|
|
|
429
|
-
// src/
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
];
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
components: ["hooks", "services", "lib", "utils", "types"],
|
|
456
|
-
features: ["components", "hooks", "services", "lib", "utils", "types"],
|
|
457
|
-
app: ["features", "components", "hooks", "services", "lib", "utils", "types"]
|
|
458
|
-
};
|
|
459
|
-
function findMonorepoPackages(projectDir) {
|
|
460
|
-
const packages = [];
|
|
461
|
-
const monorepoRoots = ["packages", "apps", "libs", "modules"];
|
|
462
|
-
for (const root of monorepoRoots) {
|
|
463
|
-
const rootPath = join3(projectDir, root);
|
|
464
|
-
if (exists(rootPath)) {
|
|
465
|
-
try {
|
|
466
|
-
const entries = readdirSync3(rootPath, { withFileTypes: true });
|
|
467
|
-
for (const entry of entries) {
|
|
468
|
-
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
469
|
-
packages.push(join3(root, entry.name));
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
} catch {
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return packages;
|
|
477
|
-
}
|
|
478
|
-
function hasLayerForPrefix(elements, layer, pathPrefix) {
|
|
479
|
-
return elements.some((e) => e.layer === layer && e.pattern.startsWith(pathPrefix));
|
|
480
|
-
}
|
|
481
|
-
function scanSearchPath(projectDir, searchPath, pathPrefix, elements) {
|
|
482
|
-
for (const layerDef of ARCHITECTURE_LAYERS) {
|
|
483
|
-
for (const dirName of layerDef.dirs) {
|
|
484
|
-
const fullPath = join3(projectDir, searchPath, dirName);
|
|
485
|
-
if (exists(fullPath) && !hasLayerForPrefix(elements, layerDef.layer, pathPrefix)) {
|
|
486
|
-
elements.push({
|
|
487
|
-
layer: layerDef.layer,
|
|
488
|
-
pattern: `${pathPrefix}${dirName}/**`,
|
|
489
|
-
location: `${pathPrefix}${dirName}`
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
function scanForLayers(projectDir, basePath) {
|
|
496
|
-
const elements = [];
|
|
497
|
-
const prefix = basePath ? `${basePath}/` : "";
|
|
498
|
-
scanSearchPath(projectDir, join3(basePath, "src"), `${prefix}src/`, elements);
|
|
499
|
-
scanSearchPath(projectDir, basePath, prefix, elements);
|
|
500
|
-
return elements;
|
|
501
|
-
}
|
|
502
|
-
function detectArchitecture(projectDir) {
|
|
503
|
-
const elements = [];
|
|
504
|
-
const packages = findMonorepoPackages(projectDir);
|
|
505
|
-
const isMonorepo = packages.length > 0;
|
|
506
|
-
if (isMonorepo) {
|
|
507
|
-
for (const pkg of packages) {
|
|
508
|
-
elements.push(...scanForLayers(projectDir, pkg));
|
|
509
|
-
}
|
|
250
|
+
// src/templates/content.ts
|
|
251
|
+
var AGENTS_MD_LINK = `**\u26A0\uFE0F ALWAYS READ FIRST:** \`.safeword/SAFEWORD.md\`
|
|
252
|
+
|
|
253
|
+
The SAFEWORD.md file contains core development patterns, workflows, and conventions.
|
|
254
|
+
Read it BEFORE working on any task in this project.
|
|
255
|
+
|
|
256
|
+
---`;
|
|
257
|
+
function getPrettierConfig(projectType) {
|
|
258
|
+
const config = {
|
|
259
|
+
semi: true,
|
|
260
|
+
singleQuote: true,
|
|
261
|
+
tabWidth: 2,
|
|
262
|
+
trailingComma: "all",
|
|
263
|
+
printWidth: 100,
|
|
264
|
+
endOfLine: "lf",
|
|
265
|
+
useTabs: false,
|
|
266
|
+
bracketSpacing: true,
|
|
267
|
+
arrowParens: "avoid"
|
|
268
|
+
};
|
|
269
|
+
const plugins = [];
|
|
270
|
+
if (projectType.astro) plugins.push("prettier-plugin-astro");
|
|
271
|
+
if (projectType.svelte) plugins.push("prettier-plugin-svelte");
|
|
272
|
+
if (projectType.shell) plugins.push("prettier-plugin-sh");
|
|
273
|
+
if (projectType.tailwind) plugins.push("prettier-plugin-tailwindcss");
|
|
274
|
+
if (plugins.length > 0) {
|
|
275
|
+
config.plugins = plugins;
|
|
510
276
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const uniqueElements = elements.filter((e) => {
|
|
514
|
-
if (seen.has(e.pattern)) return false;
|
|
515
|
-
seen.add(e.pattern);
|
|
516
|
-
return true;
|
|
517
|
-
});
|
|
518
|
-
return { elements: uniqueElements, isMonorepo };
|
|
519
|
-
}
|
|
520
|
-
function formatElement(el) {
|
|
521
|
-
return ` { type: '${el.layer}', pattern: '${el.pattern}', mode: 'full' }`;
|
|
522
|
-
}
|
|
523
|
-
function formatAllowedImports(allowed) {
|
|
524
|
-
return allowed.map((d) => `'${d}'`).join(", ");
|
|
277
|
+
return `${JSON.stringify(config, void 0, 2)}
|
|
278
|
+
`;
|
|
525
279
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (allowed.length === 0) return null;
|
|
531
|
-
return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;
|
|
280
|
+
|
|
281
|
+
// src/utils/hooks.ts
|
|
282
|
+
function isHookEntry(h) {
|
|
283
|
+
return typeof h === "object" && h !== void 0 && "hooks" in h && Array.isArray(h.hooks);
|
|
532
284
|
}
|
|
533
|
-
function
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
const locations = arch.elements.map((e) => e.location).join(", ");
|
|
538
|
-
const monorepoNote = arch.isMonorepo ? " (monorepo)" : "";
|
|
539
|
-
return `Detected: ${locations}${monorepoNote}`;
|
|
285
|
+
function isSafewordHook(h) {
|
|
286
|
+
if (!isHookEntry(h)) return false;
|
|
287
|
+
return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
|
|
540
288
|
}
|
|
541
|
-
function
|
|
542
|
-
|
|
543
|
-
const elementsContent = arch.elements.map((el) => formatElement(el)).join(",\n");
|
|
544
|
-
const detectedLayers = new Set(arch.elements.map((e) => e.layer));
|
|
545
|
-
const rules = [...detectedLayers].map((layer) => generateRule(layer, detectedLayers)).filter((rule) => rule !== null);
|
|
546
|
-
const rulesContent = rules.join(",\n");
|
|
547
|
-
const detectedInfo = buildDetectedInfo(arch);
|
|
548
|
-
return `/**
|
|
549
|
-
* Architecture Boundaries Configuration (AUTO-GENERATED)
|
|
550
|
-
*
|
|
551
|
-
* ${detectedInfo}
|
|
552
|
-
*
|
|
553
|
-
* This enforces import boundaries between architectural layers:
|
|
554
|
-
* - Lower layers (types, utils) cannot import from higher layers (components, features)
|
|
555
|
-
* - Uses 'warn' severity - informative, not blocking
|
|
556
|
-
*
|
|
557
|
-
* Recognized directories (in hierarchy order):
|
|
558
|
-
* types \u2192 utils \u2192 lib \u2192 hooks/services \u2192 components \u2192 features/modules \u2192 app
|
|
559
|
-
*
|
|
560
|
-
* To customize, override in your eslint.config.mjs:
|
|
561
|
-
* rules: { 'boundaries/element-types': ['error', { ... }] }
|
|
562
|
-
*/
|
|
563
|
-
|
|
564
|
-
import boundaries from 'eslint-plugin-boundaries';
|
|
565
|
-
|
|
566
|
-
export default {
|
|
567
|
-
plugins: { boundaries },
|
|
568
|
-
settings: {
|
|
569
|
-
'boundaries/elements': [
|
|
570
|
-
${elementsContent}
|
|
571
|
-
],
|
|
572
|
-
},
|
|
573
|
-
rules: {${hasElements ? `
|
|
574
|
-
'boundaries/element-types': ['warn', {
|
|
575
|
-
default: 'disallow',
|
|
576
|
-
rules: [
|
|
577
|
-
${rulesContent}
|
|
578
|
-
],
|
|
579
|
-
}],` : ""}
|
|
580
|
-
'boundaries/no-unknown': 'off', // Allow files outside defined elements
|
|
581
|
-
'boundaries/no-unknown-files': 'off', // Allow non-matching files
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
`;
|
|
289
|
+
function filterOutSafewordHooks(hooks) {
|
|
290
|
+
return hooks.filter((h) => !isSafewordHook(h));
|
|
585
291
|
}
|
|
586
292
|
|
|
587
293
|
// src/utils/install.ts
|
|
588
|
-
var HUSKY_PRE_COMMIT_CONTENT = "npx safeword sync --quiet --stage\nnpx lint-staged\n";
|
|
589
294
|
var MCP_SERVERS = {
|
|
590
295
|
context7: {
|
|
591
296
|
command: "npx",
|
|
@@ -597,18 +302,6 @@ var MCP_SERVERS = {
|
|
|
597
302
|
}
|
|
598
303
|
};
|
|
599
304
|
|
|
600
|
-
// src/utils/hooks.ts
|
|
601
|
-
function isHookEntry(h) {
|
|
602
|
-
return typeof h === "object" && h !== null && "hooks" in h && Array.isArray(h.hooks);
|
|
603
|
-
}
|
|
604
|
-
function isSafewordHook(h) {
|
|
605
|
-
if (!isHookEntry(h)) return false;
|
|
606
|
-
return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
|
|
607
|
-
}
|
|
608
|
-
function filterOutSafewordHooks(hooks) {
|
|
609
|
-
return hooks.filter((h) => !isSafewordHook(h));
|
|
610
|
-
}
|
|
611
|
-
|
|
612
305
|
// src/schema.ts
|
|
613
306
|
function isEslintPackage(pkg) {
|
|
614
307
|
return pkg.startsWith("eslint") || pkg.startsWith("@eslint/") || pkg.startsWith("@microsoft/eslint") || pkg.startsWith("@next/eslint") || pkg.startsWith("@vitest/eslint") || pkg.startsWith("@electron-toolkit/eslint") || pkg === "typescript-eslint";
|
|
@@ -638,7 +331,6 @@ var SAFEWORD_SCHEMA = {
|
|
|
638
331
|
".safeword/planning/issues",
|
|
639
332
|
".safeword/planning/plans",
|
|
640
333
|
".safeword/scripts",
|
|
641
|
-
".husky",
|
|
642
334
|
".cursor",
|
|
643
335
|
".cursor/rules",
|
|
644
336
|
".cursor/commands"
|
|
@@ -659,16 +351,47 @@ var SAFEWORD_SCHEMA = {
|
|
|
659
351
|
".safeword/guides/development-workflow.md",
|
|
660
352
|
".safeword/guides/tdd-best-practices.md",
|
|
661
353
|
".safeword/guides/user-story-guide.md",
|
|
662
|
-
".safeword/guides/test-definitions-guide.md"
|
|
354
|
+
".safeword/guides/test-definitions-guide.md",
|
|
355
|
+
// Boundaries config now project-specific (v0.9.0)
|
|
356
|
+
".safeword/eslint-boundaries.config.mjs"
|
|
357
|
+
],
|
|
358
|
+
// Packages to uninstall on upgrade (consolidated into eslint-plugin-safeword v0.9.0)
|
|
359
|
+
deprecatedPackages: [
|
|
360
|
+
// Individual ESLint plugins now bundled in eslint-plugin-safeword
|
|
361
|
+
"@eslint/js",
|
|
362
|
+
"eslint-plugin-import-x",
|
|
363
|
+
"eslint-import-resolver-typescript",
|
|
364
|
+
"eslint-plugin-sonarjs",
|
|
365
|
+
"eslint-plugin-unicorn",
|
|
366
|
+
"eslint-plugin-boundaries",
|
|
367
|
+
"eslint-plugin-playwright",
|
|
368
|
+
"eslint-plugin-promise",
|
|
369
|
+
"eslint-plugin-regexp",
|
|
370
|
+
"eslint-plugin-jsdoc",
|
|
371
|
+
"eslint-plugin-simple-import-sort",
|
|
372
|
+
"eslint-plugin-security",
|
|
373
|
+
// Conditional ESLint plugins now in safeword
|
|
374
|
+
"typescript-eslint",
|
|
375
|
+
"eslint-plugin-react",
|
|
376
|
+
"eslint-plugin-react-hooks",
|
|
377
|
+
"eslint-plugin-jsx-a11y",
|
|
378
|
+
"@next/eslint-plugin-next",
|
|
379
|
+
"eslint-plugin-astro",
|
|
380
|
+
"@vitest/eslint-plugin",
|
|
381
|
+
// Pre-commit hooks no longer managed by safeword
|
|
382
|
+
"husky",
|
|
383
|
+
"lint-staged"
|
|
384
|
+
],
|
|
385
|
+
// Directories to delete on upgrade (no longer managed by safeword)
|
|
386
|
+
deprecatedDirs: [
|
|
387
|
+
".husky"
|
|
388
|
+
// Pre-commit hooks no longer managed by safeword
|
|
663
389
|
],
|
|
664
390
|
// Files owned by safeword (overwritten on upgrade if content changed)
|
|
665
391
|
ownedFiles: {
|
|
666
392
|
// Core files
|
|
667
393
|
".safeword/SAFEWORD.md": { template: "SAFEWORD.md" },
|
|
668
394
|
".safeword/version": { content: () => VERSION },
|
|
669
|
-
".safeword/eslint-boundaries.config.mjs": {
|
|
670
|
-
generator: (ctx) => generateBoundariesConfig(detectArchitecture(ctx.cwd))
|
|
671
|
-
},
|
|
672
395
|
// Hooks (7 files)
|
|
673
396
|
".safeword/hooks/session-verify-agents.sh": { template: "hooks/session-verify-agents.sh" },
|
|
674
397
|
".safeword/hooks/session-version.sh": { template: "hooks/session-version.sh" },
|
|
@@ -715,11 +438,12 @@ var SAFEWORD_SCHEMA = {
|
|
|
715
438
|
// Prompts (2 files)
|
|
716
439
|
".safeword/prompts/architecture.md": { template: "prompts/architecture.md" },
|
|
717
440
|
".safeword/prompts/quality-review.md": { template: "prompts/quality-review.md" },
|
|
718
|
-
// Scripts (
|
|
441
|
+
// Scripts (4 files)
|
|
719
442
|
".safeword/scripts/bisect-test-pollution.sh": { template: "scripts/bisect-test-pollution.sh" },
|
|
720
443
|
".safeword/scripts/bisect-zombie-processes.sh": {
|
|
721
444
|
template: "scripts/bisect-zombie-processes.sh"
|
|
722
445
|
},
|
|
446
|
+
".safeword/scripts/cleanup-zombies.sh": { template: "scripts/cleanup-zombies.sh" },
|
|
723
447
|
".safeword/scripts/lint-md.sh": { template: "scripts/lint-md.sh" },
|
|
724
448
|
// Claude skills and commands (9 files)
|
|
725
449
|
".claude/skills/safeword-brainstorming/SKILL.md": {
|
|
@@ -741,10 +465,9 @@ var SAFEWORD_SCHEMA = {
|
|
|
741
465
|
template: "skills/safeword-writing-plans/SKILL.md"
|
|
742
466
|
},
|
|
743
467
|
".claude/commands/architecture.md": { template: "commands/architecture.md" },
|
|
468
|
+
".claude/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
|
|
744
469
|
".claude/commands/lint.md": { template: "commands/lint.md" },
|
|
745
470
|
".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
|
|
746
|
-
// Husky (1 file)
|
|
747
|
-
".husky/pre-commit": { content: HUSKY_PRE_COMMIT_CONTENT },
|
|
748
471
|
// Cursor rules (7 files)
|
|
749
472
|
".cursor/rules/safeword-core.mdc": { template: "cursor/rules/safeword-core.mdc" },
|
|
750
473
|
".cursor/rules/safeword-brainstorming.mdc": {
|
|
@@ -765,10 +488,11 @@ var SAFEWORD_SCHEMA = {
|
|
|
765
488
|
".cursor/rules/safeword-writing-plans.mdc": {
|
|
766
489
|
template: "cursor/rules/safeword-writing-plans.mdc"
|
|
767
490
|
},
|
|
768
|
-
// Cursor commands (
|
|
491
|
+
// Cursor commands (4 files - same as Claude)
|
|
492
|
+
".cursor/commands/architecture.md": { template: "commands/architecture.md" },
|
|
493
|
+
".cursor/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
|
|
769
494
|
".cursor/commands/lint.md": { template: "commands/lint.md" },
|
|
770
495
|
".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
|
|
771
|
-
".cursor/commands/architecture.md": { template: "commands/architecture.md" },
|
|
772
496
|
// Cursor hooks adapters (2 files)
|
|
773
497
|
".safeword/hooks/cursor/after-file-edit.sh": { template: "hooks/cursor/after-file-edit.sh" },
|
|
774
498
|
".safeword/hooks/cursor/stop.sh": { template: "hooks/cursor/stop.sh" }
|
|
@@ -776,10 +500,35 @@ var SAFEWORD_SCHEMA = {
|
|
|
776
500
|
// Files created if missing, updated only if content matches current template
|
|
777
501
|
managedFiles: {
|
|
778
502
|
"eslint.config.mjs": {
|
|
779
|
-
generator: () => getEslintConfig(
|
|
503
|
+
generator: () => getEslintConfig()
|
|
780
504
|
},
|
|
781
505
|
".prettierrc": { generator: (ctx) => getPrettierConfig(ctx.projectType) },
|
|
782
|
-
".markdownlint-cli2.jsonc": { template: "markdownlint-cli2.jsonc" }
|
|
506
|
+
".markdownlint-cli2.jsonc": { template: "markdownlint-cli2.jsonc" },
|
|
507
|
+
// Minimal tsconfig for ESLint type-checked linting (only if missing)
|
|
508
|
+
"tsconfig.json": {
|
|
509
|
+
generator: (ctx) => {
|
|
510
|
+
if (!ctx.developmentDeps.typescript && !ctx.developmentDeps["typescript-eslint"]) {
|
|
511
|
+
return "";
|
|
512
|
+
}
|
|
513
|
+
return JSON.stringify(
|
|
514
|
+
{
|
|
515
|
+
compilerOptions: {
|
|
516
|
+
target: "ES2022",
|
|
517
|
+
module: "NodeNext",
|
|
518
|
+
moduleResolution: "NodeNext",
|
|
519
|
+
strict: true,
|
|
520
|
+
esModuleInterop: true,
|
|
521
|
+
skipLibCheck: true,
|
|
522
|
+
noEmit: true
|
|
523
|
+
},
|
|
524
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
525
|
+
exclude: ["node_modules", "dist", "build"]
|
|
526
|
+
},
|
|
527
|
+
void 0,
|
|
528
|
+
2
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
783
532
|
},
|
|
784
533
|
// JSON files where we merge specific keys
|
|
785
534
|
jsonMerges: {
|
|
@@ -789,23 +538,20 @@ var SAFEWORD_SCHEMA = {
|
|
|
789
538
|
"scripts.lint:md",
|
|
790
539
|
"scripts.format",
|
|
791
540
|
"scripts.format:check",
|
|
792
|
-
"scripts.knip"
|
|
793
|
-
"scripts.prepare",
|
|
794
|
-
"lint-staged"
|
|
541
|
+
"scripts.knip"
|
|
795
542
|
],
|
|
796
543
|
conditionalKeys: {
|
|
797
544
|
publishableLibrary: ["scripts.publint"],
|
|
798
545
|
shell: ["scripts.lint:sh"]
|
|
799
546
|
},
|
|
800
547
|
merge: (existing, ctx) => {
|
|
801
|
-
const scripts = existing.scripts
|
|
548
|
+
const scripts = { ...existing.scripts };
|
|
802
549
|
const result = { ...existing };
|
|
803
550
|
if (!scripts.lint) scripts.lint = "eslint .";
|
|
804
551
|
if (!scripts["lint:md"]) scripts["lint:md"] = 'markdownlint-cli2 "**/*.md" "#node_modules"';
|
|
805
552
|
if (!scripts.format) scripts.format = "prettier --write .";
|
|
806
553
|
if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
|
|
807
554
|
if (!scripts.knip) scripts.knip = "knip";
|
|
808
|
-
if (!scripts.prepare) scripts.prepare = "husky || true";
|
|
809
555
|
if (ctx.projectType.publishableLibrary && !scripts.publint) {
|
|
810
556
|
scripts.publint = "publint";
|
|
811
557
|
}
|
|
@@ -813,9 +559,6 @@ var SAFEWORD_SCHEMA = {
|
|
|
813
559
|
scripts["lint:sh"] = "shellcheck **/*.sh";
|
|
814
560
|
}
|
|
815
561
|
result.scripts = scripts;
|
|
816
|
-
if (!existing["lint-staged"]) {
|
|
817
|
-
result["lint-staged"] = getLintStagedConfig(ctx.projectType);
|
|
818
|
-
}
|
|
819
562
|
return result;
|
|
820
563
|
},
|
|
821
564
|
unmerge: (existing) => {
|
|
@@ -825,14 +568,12 @@ var SAFEWORD_SCHEMA = {
|
|
|
825
568
|
delete scripts["lint:sh"];
|
|
826
569
|
delete scripts["format:check"];
|
|
827
570
|
delete scripts.knip;
|
|
828
|
-
delete scripts.prepare;
|
|
829
571
|
delete scripts.publint;
|
|
830
572
|
if (Object.keys(scripts).length > 0) {
|
|
831
573
|
result.scripts = scripts;
|
|
832
574
|
} else {
|
|
833
575
|
delete result.scripts;
|
|
834
576
|
}
|
|
835
|
-
delete result["lint-staged"];
|
|
836
577
|
return result;
|
|
837
578
|
}
|
|
838
579
|
},
|
|
@@ -969,42 +710,98 @@ var SAFEWORD_SCHEMA = {
|
|
|
969
710
|
// NPM packages to install
|
|
970
711
|
packages: {
|
|
971
712
|
base: [
|
|
713
|
+
// Core tools
|
|
972
714
|
"eslint",
|
|
973
715
|
"prettier",
|
|
974
|
-
"@eslint/js",
|
|
975
|
-
"eslint-plugin-import-x",
|
|
976
|
-
"eslint-import-resolver-typescript",
|
|
977
|
-
"eslint-plugin-sonarjs",
|
|
978
|
-
"eslint-plugin-unicorn",
|
|
979
|
-
"eslint-plugin-boundaries",
|
|
980
|
-
"eslint-plugin-playwright",
|
|
981
|
-
"@microsoft/eslint-plugin-sdl",
|
|
982
716
|
"eslint-config-prettier",
|
|
717
|
+
// Safeword plugin (bundles all ESLint plugins)
|
|
718
|
+
"eslint-plugin-safeword",
|
|
719
|
+
// Non-ESLint tools
|
|
983
720
|
"markdownlint-cli2",
|
|
984
|
-
"knip"
|
|
985
|
-
"husky",
|
|
986
|
-
"lint-staged"
|
|
721
|
+
"knip"
|
|
987
722
|
],
|
|
988
723
|
conditional: {
|
|
989
|
-
|
|
990
|
-
react: ["eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-jsx-a11y"],
|
|
991
|
-
nextjs: ["@next/eslint-plugin-next"],
|
|
992
|
-
astro: ["eslint-plugin-astro", "prettier-plugin-astro"],
|
|
724
|
+
// Frameworks NOT in eslint-plugin-safeword
|
|
993
725
|
vue: ["eslint-plugin-vue"],
|
|
994
726
|
svelte: ["eslint-plugin-svelte", "prettier-plugin-svelte"],
|
|
995
727
|
electron: ["@electron-toolkit/eslint-config"],
|
|
996
|
-
|
|
728
|
+
// Prettier plugins
|
|
729
|
+
astro: ["prettier-plugin-astro"],
|
|
997
730
|
tailwind: ["prettier-plugin-tailwindcss"],
|
|
731
|
+
// Non-ESLint tools
|
|
998
732
|
publishableLibrary: ["publint"],
|
|
999
733
|
shell: ["shellcheck", "prettier-plugin-sh"]
|
|
1000
734
|
}
|
|
1001
735
|
}
|
|
1002
736
|
};
|
|
1003
737
|
|
|
738
|
+
// src/utils/project-detector.ts
|
|
739
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
740
|
+
import nodePath2 from "path";
|
|
741
|
+
function hasShellScripts(cwd, maxDepth = 4) {
|
|
742
|
+
const excludeDirectories = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
|
|
743
|
+
function scan(dir, depth) {
|
|
744
|
+
if (depth > maxDepth) return false;
|
|
745
|
+
try {
|
|
746
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
747
|
+
for (const entry of entries) {
|
|
748
|
+
if (entry.isFile() && entry.name.endsWith(".sh")) {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(nodePath2.join(dir, entry.name), depth + 1)) {
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
return scan(cwd, 0);
|
|
760
|
+
}
|
|
761
|
+
function detectProjectType(packageJson, cwd) {
|
|
762
|
+
const deps = packageJson.dependencies || {};
|
|
763
|
+
const developmentDeps = packageJson.devDependencies || {};
|
|
764
|
+
const allDeps = { ...deps, ...developmentDeps };
|
|
765
|
+
const hasTypescript = "typescript" in allDeps;
|
|
766
|
+
const hasReact = "react" in deps || "react" in developmentDeps;
|
|
767
|
+
const hasNextJs = "next" in deps;
|
|
768
|
+
const hasAstro = "astro" in deps || "astro" in developmentDeps;
|
|
769
|
+
const hasVue = "vue" in deps || "vue" in developmentDeps;
|
|
770
|
+
const hasNuxt = "nuxt" in deps;
|
|
771
|
+
const hasSvelte = "svelte" in deps || "svelte" in developmentDeps;
|
|
772
|
+
const hasSvelteKit = "@sveltejs/kit" in deps || "@sveltejs/kit" in developmentDeps;
|
|
773
|
+
const hasElectron = "electron" in deps || "electron" in developmentDeps;
|
|
774
|
+
const hasVitest = "vitest" in developmentDeps;
|
|
775
|
+
const hasPlaywright = "@playwright/test" in developmentDeps;
|
|
776
|
+
const hasTailwind = "tailwindcss" in allDeps;
|
|
777
|
+
const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
|
|
778
|
+
const isPublishable = hasEntryPoints && packageJson.private !== true;
|
|
779
|
+
const hasShell = cwd ? hasShellScripts(cwd) : false;
|
|
780
|
+
return {
|
|
781
|
+
typescript: hasTypescript,
|
|
782
|
+
react: hasReact || hasNextJs,
|
|
783
|
+
// Next.js implies React
|
|
784
|
+
nextjs: hasNextJs,
|
|
785
|
+
astro: hasAstro,
|
|
786
|
+
vue: hasVue || hasNuxt,
|
|
787
|
+
// Nuxt implies Vue
|
|
788
|
+
nuxt: hasNuxt,
|
|
789
|
+
svelte: hasSvelte || hasSvelteKit,
|
|
790
|
+
// SvelteKit implies Svelte
|
|
791
|
+
sveltekit: hasSvelteKit,
|
|
792
|
+
electron: hasElectron,
|
|
793
|
+
vitest: hasVitest,
|
|
794
|
+
playwright: hasPlaywright,
|
|
795
|
+
tailwind: hasTailwind,
|
|
796
|
+
publishableLibrary: isPublishable,
|
|
797
|
+
shell: hasShell
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
1004
801
|
export {
|
|
1005
|
-
|
|
802
|
+
getTemplatesDirectory,
|
|
1006
803
|
exists,
|
|
1007
|
-
|
|
804
|
+
ensureDirectory,
|
|
1008
805
|
readFile,
|
|
1009
806
|
readFileSafe,
|
|
1010
807
|
writeFile,
|
|
@@ -1013,9 +810,9 @@ export {
|
|
|
1013
810
|
makeScriptsExecutable,
|
|
1014
811
|
readJson,
|
|
1015
812
|
writeJson,
|
|
1016
|
-
detectProjectType,
|
|
1017
813
|
getBaseEslintPackages,
|
|
1018
814
|
getConditionalEslintPackages,
|
|
1019
|
-
SAFEWORD_SCHEMA
|
|
815
|
+
SAFEWORD_SCHEMA,
|
|
816
|
+
detectProjectType
|
|
1020
817
|
};
|
|
1021
|
-
//# sourceMappingURL=chunk-
|
|
818
|
+
//# sourceMappingURL=chunk-CLSGXTOL.js.map
|