safeword 0.8.7 → 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-PPVIEF3Q.js → check-QMAGWUOA.js} +24 -22
- package/dist/check-QMAGWUOA.js.map +1 -0
- package/dist/{chunk-BFBUEJDH.js → chunk-4URRFBUS.js} +10 -10
- package/dist/chunk-4URRFBUS.js.map +1 -0
- package/dist/{chunk-34PU3QZI.js → chunk-CLSGXTOL.js} +232 -461
- 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-3OK3NQEW.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-S3ICSYQY.js → diff-2T7UDES7.js} +12 -12
- package/dist/diff-2T7UDES7.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/{reset-ZST2SGZ2.js → reset-QRXG7KZZ.js} +8 -8
- package/dist/reset-QRXG7KZZ.js.map +1 -0
- package/dist/{setup-ANAIEP3D.js → setup-QUUJ7SH3.js} +8 -8
- package/dist/setup-QUUJ7SH3.js.map +1 -0
- package/dist/sync-ISBJ7X2T.js +9 -0
- package/dist/{upgrade-QFIGWZ5I.js → upgrade-FALAUUKE.js} +22 -10
- package/dist/upgrade-FALAUUKE.js.map +1 -0
- package/package.json +2 -2
- 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/dist/check-PPVIEF3Q.js.map +0 -1
- package/dist/chunk-34PU3QZI.js.map +0 -1
- package/dist/chunk-3OK3NQEW.js.map +0 -1
- package/dist/chunk-BFBUEJDH.js.map +0 -1
- package/dist/chunk-W66Z3C5H.js.map +0 -1
- package/dist/diff-S3ICSYQY.js.map +0 -1
- package/dist/reset-ZST2SGZ2.js.map +0 -1
- package/dist/setup-ANAIEP3D.js.map +0 -1
- package/dist/sync-V6D7QTMO.js +0 -9
- package/dist/upgrade-QFIGWZ5I.js.map +0 -1
- /package/dist/{sync-V6D7QTMO.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,148 +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
|
-
|
|
274
|
-
|
|
275
|
-
const tseslint = await tryImport("typescript-eslint", "TypeScript");
|
|
276
|
-
const { existsSync } = await import("fs");
|
|
277
|
-
const hasTsconfig = existsSync(join(__dirname, "tsconfig.json"));
|
|
278
|
-
|
|
279
|
-
configs.push(importX.flatConfigs.typescript);
|
|
280
|
-
|
|
281
|
-
if (hasTsconfig) {
|
|
282
|
-
// Type-aware linting (recommended + stylistic)
|
|
283
|
-
configs.push(...tseslint.default.configs.recommendedTypeChecked);
|
|
284
|
-
configs.push(...tseslint.default.configs.stylisticTypeChecked);
|
|
285
|
-
configs.push({
|
|
286
|
-
languageOptions: {
|
|
287
|
-
parserOptions: {
|
|
288
|
-
projectService: true,
|
|
289
|
-
tsconfigRootDir: __dirname,
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
// Disable type-checked rules for JS files (no type info available)
|
|
294
|
-
configs.push({
|
|
295
|
-
files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
|
|
296
|
-
extends: [tseslint.default.configs.disableTypeChecked],
|
|
297
|
-
});
|
|
298
|
-
} else {
|
|
299
|
-
// Fall back to non-type-aware rules when no tsconfig exists
|
|
300
|
-
configs.push(...tseslint.default.configs.recommended);
|
|
301
|
-
configs.push(...tseslint.default.configs.stylistic);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// React/Next.js support
|
|
306
|
-
if (deps["react"] || deps["next"]) {
|
|
307
|
-
const react = await tryImport("eslint-plugin-react", "React");
|
|
308
|
-
const reactHooks = await tryImport("eslint-plugin-react-hooks", "React");
|
|
309
|
-
const jsxA11y = await tryImport("eslint-plugin-jsx-a11y", "React");
|
|
310
|
-
configs.push(react.default.configs.flat.recommended);
|
|
311
|
-
configs.push(react.default.configs.flat["jsx-runtime"]);
|
|
312
|
-
configs.push({
|
|
313
|
-
name: "react-hooks",
|
|
314
|
-
plugins: { "react-hooks": reactHooks.default },
|
|
315
|
-
rules: reactHooks.default.configs.recommended.rules,
|
|
316
|
-
});
|
|
317
|
-
configs.push(jsxA11y.default.flatConfigs.recommended);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Next.js plugin
|
|
321
|
-
if (deps["next"]) {
|
|
322
|
-
const nextPlugin = await tryImport("@next/eslint-plugin-next", "Next.js");
|
|
323
|
-
configs.push({
|
|
324
|
-
name: "nextjs",
|
|
325
|
-
plugins: { "@next/next": nextPlugin.default },
|
|
326
|
-
rules: nextPlugin.default.configs.recommended.rules,
|
|
327
|
-
});
|
|
148
|
+
// Add test configs if testing frameworks detected
|
|
149
|
+
if (deps["vitest"]) {
|
|
150
|
+
configs.push(...safeword.configs.vitest);
|
|
328
151
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (deps["astro"]) {
|
|
332
|
-
const astro = await tryImport("eslint-plugin-astro", "Astro");
|
|
333
|
-
configs.push(...astro.default.configs.recommended);
|
|
152
|
+
if (deps["playwright"] || deps["@playwright/test"]) {
|
|
153
|
+
configs.push(...safeword.configs.playwright);
|
|
334
154
|
}
|
|
335
155
|
|
|
336
|
-
//
|
|
156
|
+
// Frameworks NOT in eslint-plugin-safeword (dynamic imports)
|
|
337
157
|
if (deps["vue"] || deps["nuxt"]) {
|
|
338
158
|
const vue = await tryImport("eslint-plugin-vue", "Vue");
|
|
339
159
|
configs.push(...vue.default.configs["flat/recommended"]);
|
|
340
160
|
}
|
|
341
161
|
|
|
342
|
-
// Svelte support
|
|
343
162
|
if (deps["svelte"] || deps["@sveltejs/kit"]) {
|
|
344
163
|
const svelte = await tryImport("eslint-plugin-svelte", "Svelte");
|
|
345
164
|
configs.push(...svelte.default.configs.recommended);
|
|
346
165
|
}
|
|
347
166
|
|
|
348
|
-
// Electron support
|
|
349
167
|
if (deps["electron"]) {
|
|
350
168
|
const electron = await tryImport("@electron-toolkit/eslint-config", "Electron");
|
|
351
169
|
configs.push(electron.default);
|
|
352
170
|
}
|
|
353
171
|
|
|
354
|
-
// Vitest support (scoped to test files)
|
|
355
|
-
if (deps["vitest"]) {
|
|
356
|
-
const vitest = await tryImport("@vitest/eslint-plugin", "Vitest");
|
|
357
|
-
configs.push({
|
|
358
|
-
name: "vitest",
|
|
359
|
-
files: ["**/*.test.{js,ts,jsx,tsx}", "**/*.spec.{js,ts,jsx,tsx}", "**/tests/**"],
|
|
360
|
-
plugins: { vitest: vitest.default },
|
|
361
|
-
languageOptions: {
|
|
362
|
-
globals: { ...vitest.default.environments.env.globals },
|
|
363
|
-
},
|
|
364
|
-
rules: { ...vitest.default.configs.recommended.rules },
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Playwright for e2e tests (always included - safeword sets up Playwright)
|
|
369
|
-
configs.push({
|
|
370
|
-
name: "playwright",
|
|
371
|
-
files: ["**/e2e/**", "**/*.e2e.{js,ts,jsx,tsx}", "**/playwright/**"],
|
|
372
|
-
...playwright.configs["flat/recommended"],
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// Architecture boundaries${options.boundaries ? "\nconfigs.push(boundariesConfig);" : ""}
|
|
376
|
-
|
|
377
172
|
// eslint-config-prettier must be last to disable conflicting rules
|
|
378
173
|
configs.push(eslintConfigPrettier);
|
|
379
174
|
|
|
380
|
-
export default
|
|
175
|
+
export default configs;
|
|
381
176
|
`;
|
|
382
177
|
}
|
|
383
178
|
var CURSOR_HOOKS = {
|
|
@@ -452,166 +247,50 @@ var SETTINGS_HOOKS = {
|
|
|
452
247
|
]
|
|
453
248
|
};
|
|
454
249
|
|
|
455
|
-
// src/
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
];
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
components: ["hooks", "services", "lib", "utils", "types"],
|
|
482
|
-
features: ["components", "hooks", "services", "lib", "utils", "types"],
|
|
483
|
-
app: ["features", "components", "hooks", "services", "lib", "utils", "types"]
|
|
484
|
-
};
|
|
485
|
-
function findMonorepoPackages(projectDir) {
|
|
486
|
-
const packages = [];
|
|
487
|
-
const monorepoRoots = ["packages", "apps", "libs", "modules"];
|
|
488
|
-
for (const root of monorepoRoots) {
|
|
489
|
-
const rootPath = join3(projectDir, root);
|
|
490
|
-
if (exists(rootPath)) {
|
|
491
|
-
try {
|
|
492
|
-
const entries = readdirSync3(rootPath, { withFileTypes: true });
|
|
493
|
-
for (const entry of entries) {
|
|
494
|
-
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
495
|
-
packages.push(join3(root, entry.name));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
} catch {
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
return packages;
|
|
503
|
-
}
|
|
504
|
-
function hasLayerForPrefix(elements, layer, pathPrefix) {
|
|
505
|
-
return elements.some((e) => e.layer === layer && e.pattern.startsWith(pathPrefix));
|
|
506
|
-
}
|
|
507
|
-
function scanSearchPath(projectDir, searchPath, pathPrefix, elements) {
|
|
508
|
-
for (const layerDef of ARCHITECTURE_LAYERS) {
|
|
509
|
-
for (const dirName of layerDef.dirs) {
|
|
510
|
-
const fullPath = join3(projectDir, searchPath, dirName);
|
|
511
|
-
if (exists(fullPath) && !hasLayerForPrefix(elements, layerDef.layer, pathPrefix)) {
|
|
512
|
-
elements.push({
|
|
513
|
-
layer: layerDef.layer,
|
|
514
|
-
pattern: `${pathPrefix}${dirName}/**`,
|
|
515
|
-
location: `${pathPrefix}${dirName}`
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
function scanForLayers(projectDir, basePath) {
|
|
522
|
-
const elements = [];
|
|
523
|
-
const prefix = basePath ? `${basePath}/` : "";
|
|
524
|
-
scanSearchPath(projectDir, join3(basePath, "src"), `${prefix}src/`, elements);
|
|
525
|
-
scanSearchPath(projectDir, basePath, prefix, elements);
|
|
526
|
-
return elements;
|
|
527
|
-
}
|
|
528
|
-
function detectArchitecture(projectDir) {
|
|
529
|
-
const elements = [];
|
|
530
|
-
const packages = findMonorepoPackages(projectDir);
|
|
531
|
-
const isMonorepo = packages.length > 0;
|
|
532
|
-
if (isMonorepo) {
|
|
533
|
-
for (const pkg of packages) {
|
|
534
|
-
elements.push(...scanForLayers(projectDir, pkg));
|
|
535
|
-
}
|
|
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;
|
|
536
276
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const uniqueElements = elements.filter((e) => {
|
|
540
|
-
if (seen.has(e.pattern)) return false;
|
|
541
|
-
seen.add(e.pattern);
|
|
542
|
-
return true;
|
|
543
|
-
});
|
|
544
|
-
return { elements: uniqueElements, isMonorepo };
|
|
545
|
-
}
|
|
546
|
-
function formatElement(el) {
|
|
547
|
-
return ` { type: '${el.layer}', pattern: '${el.pattern}', mode: 'full' }`;
|
|
548
|
-
}
|
|
549
|
-
function formatAllowedImports(allowed) {
|
|
550
|
-
return allowed.map((d) => `'${d}'`).join(", ");
|
|
277
|
+
return `${JSON.stringify(config, void 0, 2)}
|
|
278
|
+
`;
|
|
551
279
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (allowed.length === 0) return null;
|
|
557
|
-
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);
|
|
558
284
|
}
|
|
559
|
-
function
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
const locations = arch.elements.map((e) => e.location).join(", ");
|
|
564
|
-
const monorepoNote = arch.isMonorepo ? " (monorepo)" : "";
|
|
565
|
-
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"));
|
|
566
288
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
const elementsContent = arch.elements.map((el) => formatElement(el)).join(",\n");
|
|
570
|
-
const detectedLayers = new Set(arch.elements.map((e) => e.layer));
|
|
571
|
-
const rules = [...detectedLayers].map((layer) => generateRule(layer, detectedLayers)).filter((rule) => rule !== null);
|
|
572
|
-
const rulesContent = rules.join(",\n");
|
|
573
|
-
const detectedInfo = buildDetectedInfo(arch);
|
|
574
|
-
return `/**
|
|
575
|
-
* Architecture Boundaries Configuration (AUTO-GENERATED)
|
|
576
|
-
*
|
|
577
|
-
* ${detectedInfo}
|
|
578
|
-
*
|
|
579
|
-
* This enforces import boundaries between architectural layers:
|
|
580
|
-
* - Lower layers (types, utils) cannot import from higher layers (components, features)
|
|
581
|
-
* - Uses 'warn' severity - informative, not blocking
|
|
582
|
-
*
|
|
583
|
-
* Recognized directories (in hierarchy order):
|
|
584
|
-
* types \u2192 utils \u2192 lib \u2192 hooks/services \u2192 components \u2192 features/modules \u2192 app
|
|
585
|
-
*
|
|
586
|
-
* To customize, override in your eslint.config.mjs:
|
|
587
|
-
* rules: { 'boundaries/element-types': ['error', { ... }] }
|
|
588
|
-
*/
|
|
589
|
-
|
|
590
|
-
import boundaries from 'eslint-plugin-boundaries';
|
|
591
|
-
|
|
592
|
-
export default {
|
|
593
|
-
plugins: { boundaries },
|
|
594
|
-
settings: {
|
|
595
|
-
'boundaries/elements': [
|
|
596
|
-
${elementsContent}
|
|
597
|
-
],
|
|
598
|
-
},
|
|
599
|
-
rules: {${hasElements ? `
|
|
600
|
-
'boundaries/element-types': ['warn', {
|
|
601
|
-
default: 'disallow',
|
|
602
|
-
rules: [
|
|
603
|
-
${rulesContent}
|
|
604
|
-
],
|
|
605
|
-
}],` : ""}
|
|
606
|
-
'boundaries/no-unknown': 'off', // Allow files outside defined elements
|
|
607
|
-
'boundaries/no-unknown-files': 'off', // Allow non-matching files
|
|
608
|
-
},
|
|
609
|
-
};
|
|
610
|
-
`;
|
|
289
|
+
function filterOutSafewordHooks(hooks) {
|
|
290
|
+
return hooks.filter((h) => !isSafewordHook(h));
|
|
611
291
|
}
|
|
612
292
|
|
|
613
293
|
// src/utils/install.ts
|
|
614
|
-
var HUSKY_PRE_COMMIT_CONTENT = "npx safeword sync --quiet --stage\nnpx lint-staged\n";
|
|
615
294
|
var MCP_SERVERS = {
|
|
616
295
|
context7: {
|
|
617
296
|
command: "npx",
|
|
@@ -623,18 +302,6 @@ var MCP_SERVERS = {
|
|
|
623
302
|
}
|
|
624
303
|
};
|
|
625
304
|
|
|
626
|
-
// src/utils/hooks.ts
|
|
627
|
-
function isHookEntry(h) {
|
|
628
|
-
return typeof h === "object" && h !== null && "hooks" in h && Array.isArray(h.hooks);
|
|
629
|
-
}
|
|
630
|
-
function isSafewordHook(h) {
|
|
631
|
-
if (!isHookEntry(h)) return false;
|
|
632
|
-
return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
|
|
633
|
-
}
|
|
634
|
-
function filterOutSafewordHooks(hooks) {
|
|
635
|
-
return hooks.filter((h) => !isSafewordHook(h));
|
|
636
|
-
}
|
|
637
|
-
|
|
638
305
|
// src/schema.ts
|
|
639
306
|
function isEslintPackage(pkg) {
|
|
640
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";
|
|
@@ -664,7 +331,6 @@ var SAFEWORD_SCHEMA = {
|
|
|
664
331
|
".safeword/planning/issues",
|
|
665
332
|
".safeword/planning/plans",
|
|
666
333
|
".safeword/scripts",
|
|
667
|
-
".husky",
|
|
668
334
|
".cursor",
|
|
669
335
|
".cursor/rules",
|
|
670
336
|
".cursor/commands"
|
|
@@ -685,16 +351,47 @@ var SAFEWORD_SCHEMA = {
|
|
|
685
351
|
".safeword/guides/development-workflow.md",
|
|
686
352
|
".safeword/guides/tdd-best-practices.md",
|
|
687
353
|
".safeword/guides/user-story-guide.md",
|
|
688
|
-
".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
|
|
689
389
|
],
|
|
690
390
|
// Files owned by safeword (overwritten on upgrade if content changed)
|
|
691
391
|
ownedFiles: {
|
|
692
392
|
// Core files
|
|
693
393
|
".safeword/SAFEWORD.md": { template: "SAFEWORD.md" },
|
|
694
394
|
".safeword/version": { content: () => VERSION },
|
|
695
|
-
".safeword/eslint-boundaries.config.mjs": {
|
|
696
|
-
generator: (ctx) => generateBoundariesConfig(detectArchitecture(ctx.cwd))
|
|
697
|
-
},
|
|
698
395
|
// Hooks (7 files)
|
|
699
396
|
".safeword/hooks/session-verify-agents.sh": { template: "hooks/session-verify-agents.sh" },
|
|
700
397
|
".safeword/hooks/session-version.sh": { template: "hooks/session-version.sh" },
|
|
@@ -741,11 +438,12 @@ var SAFEWORD_SCHEMA = {
|
|
|
741
438
|
// Prompts (2 files)
|
|
742
439
|
".safeword/prompts/architecture.md": { template: "prompts/architecture.md" },
|
|
743
440
|
".safeword/prompts/quality-review.md": { template: "prompts/quality-review.md" },
|
|
744
|
-
// Scripts (
|
|
441
|
+
// Scripts (4 files)
|
|
745
442
|
".safeword/scripts/bisect-test-pollution.sh": { template: "scripts/bisect-test-pollution.sh" },
|
|
746
443
|
".safeword/scripts/bisect-zombie-processes.sh": {
|
|
747
444
|
template: "scripts/bisect-zombie-processes.sh"
|
|
748
445
|
},
|
|
446
|
+
".safeword/scripts/cleanup-zombies.sh": { template: "scripts/cleanup-zombies.sh" },
|
|
749
447
|
".safeword/scripts/lint-md.sh": { template: "scripts/lint-md.sh" },
|
|
750
448
|
// Claude skills and commands (9 files)
|
|
751
449
|
".claude/skills/safeword-brainstorming/SKILL.md": {
|
|
@@ -767,10 +465,9 @@ var SAFEWORD_SCHEMA = {
|
|
|
767
465
|
template: "skills/safeword-writing-plans/SKILL.md"
|
|
768
466
|
},
|
|
769
467
|
".claude/commands/architecture.md": { template: "commands/architecture.md" },
|
|
468
|
+
".claude/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
|
|
770
469
|
".claude/commands/lint.md": { template: "commands/lint.md" },
|
|
771
470
|
".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
|
|
772
|
-
// Husky (1 file)
|
|
773
|
-
".husky/pre-commit": { content: HUSKY_PRE_COMMIT_CONTENT },
|
|
774
471
|
// Cursor rules (7 files)
|
|
775
472
|
".cursor/rules/safeword-core.mdc": { template: "cursor/rules/safeword-core.mdc" },
|
|
776
473
|
".cursor/rules/safeword-brainstorming.mdc": {
|
|
@@ -791,10 +488,11 @@ var SAFEWORD_SCHEMA = {
|
|
|
791
488
|
".cursor/rules/safeword-writing-plans.mdc": {
|
|
792
489
|
template: "cursor/rules/safeword-writing-plans.mdc"
|
|
793
490
|
},
|
|
794
|
-
// 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" },
|
|
795
494
|
".cursor/commands/lint.md": { template: "commands/lint.md" },
|
|
796
495
|
".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
|
|
797
|
-
".cursor/commands/architecture.md": { template: "commands/architecture.md" },
|
|
798
496
|
// Cursor hooks adapters (2 files)
|
|
799
497
|
".safeword/hooks/cursor/after-file-edit.sh": { template: "hooks/cursor/after-file-edit.sh" },
|
|
800
498
|
".safeword/hooks/cursor/stop.sh": { template: "hooks/cursor/stop.sh" }
|
|
@@ -802,10 +500,35 @@ var SAFEWORD_SCHEMA = {
|
|
|
802
500
|
// Files created if missing, updated only if content matches current template
|
|
803
501
|
managedFiles: {
|
|
804
502
|
"eslint.config.mjs": {
|
|
805
|
-
generator: () => getEslintConfig(
|
|
503
|
+
generator: () => getEslintConfig()
|
|
806
504
|
},
|
|
807
505
|
".prettierrc": { generator: (ctx) => getPrettierConfig(ctx.projectType) },
|
|
808
|
-
".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
|
+
}
|
|
809
532
|
},
|
|
810
533
|
// JSON files where we merge specific keys
|
|
811
534
|
jsonMerges: {
|
|
@@ -815,23 +538,20 @@ var SAFEWORD_SCHEMA = {
|
|
|
815
538
|
"scripts.lint:md",
|
|
816
539
|
"scripts.format",
|
|
817
540
|
"scripts.format:check",
|
|
818
|
-
"scripts.knip"
|
|
819
|
-
"scripts.prepare",
|
|
820
|
-
"lint-staged"
|
|
541
|
+
"scripts.knip"
|
|
821
542
|
],
|
|
822
543
|
conditionalKeys: {
|
|
823
544
|
publishableLibrary: ["scripts.publint"],
|
|
824
545
|
shell: ["scripts.lint:sh"]
|
|
825
546
|
},
|
|
826
547
|
merge: (existing, ctx) => {
|
|
827
|
-
const scripts = existing.scripts
|
|
548
|
+
const scripts = { ...existing.scripts };
|
|
828
549
|
const result = { ...existing };
|
|
829
550
|
if (!scripts.lint) scripts.lint = "eslint .";
|
|
830
551
|
if (!scripts["lint:md"]) scripts["lint:md"] = 'markdownlint-cli2 "**/*.md" "#node_modules"';
|
|
831
552
|
if (!scripts.format) scripts.format = "prettier --write .";
|
|
832
553
|
if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
|
|
833
554
|
if (!scripts.knip) scripts.knip = "knip";
|
|
834
|
-
if (!scripts.prepare) scripts.prepare = "husky || true";
|
|
835
555
|
if (ctx.projectType.publishableLibrary && !scripts.publint) {
|
|
836
556
|
scripts.publint = "publint";
|
|
837
557
|
}
|
|
@@ -839,9 +559,6 @@ var SAFEWORD_SCHEMA = {
|
|
|
839
559
|
scripts["lint:sh"] = "shellcheck **/*.sh";
|
|
840
560
|
}
|
|
841
561
|
result.scripts = scripts;
|
|
842
|
-
if (!existing["lint-staged"]) {
|
|
843
|
-
result["lint-staged"] = getLintStagedConfig(ctx.projectType);
|
|
844
|
-
}
|
|
845
562
|
return result;
|
|
846
563
|
},
|
|
847
564
|
unmerge: (existing) => {
|
|
@@ -851,14 +568,12 @@ var SAFEWORD_SCHEMA = {
|
|
|
851
568
|
delete scripts["lint:sh"];
|
|
852
569
|
delete scripts["format:check"];
|
|
853
570
|
delete scripts.knip;
|
|
854
|
-
delete scripts.prepare;
|
|
855
571
|
delete scripts.publint;
|
|
856
572
|
if (Object.keys(scripts).length > 0) {
|
|
857
573
|
result.scripts = scripts;
|
|
858
574
|
} else {
|
|
859
575
|
delete result.scripts;
|
|
860
576
|
}
|
|
861
|
-
delete result["lint-staged"];
|
|
862
577
|
return result;
|
|
863
578
|
}
|
|
864
579
|
},
|
|
@@ -995,42 +710,98 @@ var SAFEWORD_SCHEMA = {
|
|
|
995
710
|
// NPM packages to install
|
|
996
711
|
packages: {
|
|
997
712
|
base: [
|
|
713
|
+
// Core tools
|
|
998
714
|
"eslint",
|
|
999
715
|
"prettier",
|
|
1000
|
-
"@eslint/js",
|
|
1001
|
-
"eslint-plugin-import-x",
|
|
1002
|
-
"eslint-import-resolver-typescript",
|
|
1003
|
-
"eslint-plugin-sonarjs",
|
|
1004
|
-
"eslint-plugin-unicorn",
|
|
1005
|
-
"eslint-plugin-boundaries",
|
|
1006
|
-
"eslint-plugin-playwright",
|
|
1007
|
-
"@microsoft/eslint-plugin-sdl",
|
|
1008
716
|
"eslint-config-prettier",
|
|
717
|
+
// Safeword plugin (bundles all ESLint plugins)
|
|
718
|
+
"eslint-plugin-safeword",
|
|
719
|
+
// Non-ESLint tools
|
|
1009
720
|
"markdownlint-cli2",
|
|
1010
|
-
"knip"
|
|
1011
|
-
"husky",
|
|
1012
|
-
"lint-staged"
|
|
721
|
+
"knip"
|
|
1013
722
|
],
|
|
1014
723
|
conditional: {
|
|
1015
|
-
|
|
1016
|
-
react: ["eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-jsx-a11y"],
|
|
1017
|
-
nextjs: ["@next/eslint-plugin-next"],
|
|
1018
|
-
astro: ["eslint-plugin-astro", "prettier-plugin-astro"],
|
|
724
|
+
// Frameworks NOT in eslint-plugin-safeword
|
|
1019
725
|
vue: ["eslint-plugin-vue"],
|
|
1020
726
|
svelte: ["eslint-plugin-svelte", "prettier-plugin-svelte"],
|
|
1021
727
|
electron: ["@electron-toolkit/eslint-config"],
|
|
1022
|
-
|
|
728
|
+
// Prettier plugins
|
|
729
|
+
astro: ["prettier-plugin-astro"],
|
|
1023
730
|
tailwind: ["prettier-plugin-tailwindcss"],
|
|
731
|
+
// Non-ESLint tools
|
|
1024
732
|
publishableLibrary: ["publint"],
|
|
1025
733
|
shell: ["shellcheck", "prettier-plugin-sh"]
|
|
1026
734
|
}
|
|
1027
735
|
}
|
|
1028
736
|
};
|
|
1029
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
|
+
|
|
1030
801
|
export {
|
|
1031
|
-
|
|
802
|
+
getTemplatesDirectory,
|
|
1032
803
|
exists,
|
|
1033
|
-
|
|
804
|
+
ensureDirectory,
|
|
1034
805
|
readFile,
|
|
1035
806
|
readFileSafe,
|
|
1036
807
|
writeFile,
|
|
@@ -1039,9 +810,9 @@ export {
|
|
|
1039
810
|
makeScriptsExecutable,
|
|
1040
811
|
readJson,
|
|
1041
812
|
writeJson,
|
|
1042
|
-
detectProjectType,
|
|
1043
813
|
getBaseEslintPackages,
|
|
1044
814
|
getConditionalEslintPackages,
|
|
1045
|
-
SAFEWORD_SCHEMA
|
|
815
|
+
SAFEWORD_SCHEMA,
|
|
816
|
+
detectProjectType
|
|
1046
817
|
};
|
|
1047
|
-
//# sourceMappingURL=chunk-
|
|
818
|
+
//# sourceMappingURL=chunk-CLSGXTOL.js.map
|