safeword 0.10.1 → 0.11.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.
- package/dist/{check-IBMCNPDE.js → check-6SBEN4FB.js} +13 -8
- package/dist/check-6SBEN4FB.js.map +1 -0
- package/dist/chunk-3R26BJXN.js +229 -0
- package/dist/chunk-3R26BJXN.js.map +1 -0
- package/dist/chunk-DYLHQBW3.js +132 -0
- package/dist/chunk-DYLHQBW3.js.map +1 -0
- package/dist/{chunk-H7PCVPAC.js → chunk-ZE6QJHZD.js} +226 -228
- package/dist/chunk-ZE6QJHZD.js.map +1 -0
- package/dist/cli.js +9 -5
- package/dist/cli.js.map +1 -1
- package/dist/{diff-7AXUOVBF.js → diff-YLENBSAH.js} +8 -6
- package/dist/{diff-7AXUOVBF.js.map → diff-YLENBSAH.js.map} +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/{reset-AEEUFODC.js → reset-IKMRI6W4.js} +6 -4
- package/dist/{reset-AEEUFODC.js.map → reset-IKMRI6W4.js.map} +1 -1
- package/dist/{setup-R5IC3GHJ.js → setup-FOEXCAJV.js} +41 -13
- package/dist/setup-FOEXCAJV.js.map +1 -0
- package/dist/sync-config-PPTR3JPA.js +14 -0
- package/dist/sync-config-PPTR3JPA.js.map +1 -0
- package/dist/{upgrade-QXP3ONNQ.js → upgrade-AKVIMR5M.js} +8 -6
- package/dist/{upgrade-QXP3ONNQ.js.map → upgrade-AKVIMR5M.js.map} +1 -1
- package/package.json +4 -3
- package/templates/commands/audit.md +41 -0
- package/templates/skills/safeword-debugging/SKILL.md +15 -15
- package/dist/check-IBMCNPDE.js.map +0 -1
- package/dist/chunk-H7PCVPAC.js.map +0 -1
- package/dist/setup-R5IC3GHJ.js.map +0 -1
|
@@ -1,96 +1,48 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
3
|
} from "./chunk-ORQHKDT2.js";
|
|
4
|
-
|
|
5
|
-
// src/utils/fs.ts
|
|
6
4
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
ensureDirectory,
|
|
6
|
+
exists,
|
|
7
|
+
getTemplatesDirectory,
|
|
8
|
+
makeScriptsExecutable,
|
|
9
|
+
readFile,
|
|
10
|
+
readFileSafe,
|
|
11
|
+
readJson,
|
|
12
|
+
remove,
|
|
13
|
+
removeIfEmpty,
|
|
14
|
+
writeFile,
|
|
15
|
+
writeJson
|
|
16
|
+
} from "./chunk-DYLHQBW3.js";
|
|
17
|
+
|
|
18
|
+
// src/reconcile.ts
|
|
16
19
|
import nodePath from "path";
|
|
17
|
-
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
var HUSKY_DIR = ".husky";
|
|
21
|
+
var PRETTIER_PACKAGES = /* @__PURE__ */ new Set([
|
|
22
|
+
"prettier",
|
|
23
|
+
"prettier-plugin-astro",
|
|
24
|
+
"prettier-plugin-tailwindcss",
|
|
25
|
+
"prettier-plugin-sh"
|
|
26
|
+
]);
|
|
27
|
+
function getConditionalPackages(conditionalPackages, projectType) {
|
|
28
|
+
const packages = [];
|
|
29
|
+
for (const [key, deps] of Object.entries(conditionalPackages)) {
|
|
30
|
+
if (key === "standard") {
|
|
31
|
+
if (!projectType.existingFormatter) {
|
|
32
|
+
packages.push(...deps);
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
32
35
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
function ensureDirectory(path) {
|
|
40
|
-
if (!existsSync(path)) {
|
|
41
|
-
mkdirSync(path, { recursive: true });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function readFile(path) {
|
|
45
|
-
return readFileSync(path, "utf8");
|
|
46
|
-
}
|
|
47
|
-
function readFileSafe(path) {
|
|
48
|
-
if (!existsSync(path)) return void 0;
|
|
49
|
-
return readFileSync(path, "utf8");
|
|
50
|
-
}
|
|
51
|
-
function writeFile(path, content) {
|
|
52
|
-
ensureDirectory(nodePath.dirname(path));
|
|
53
|
-
writeFileSync(path, content);
|
|
54
|
-
}
|
|
55
|
-
function remove(path) {
|
|
56
|
-
if (existsSync(path)) {
|
|
57
|
-
rmSync(path, { recursive: true, force: true });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function removeIfEmpty(path) {
|
|
61
|
-
if (!existsSync(path)) return false;
|
|
62
|
-
try {
|
|
63
|
-
rmdirSync(path);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function makeScriptsExecutable(dirPath) {
|
|
70
|
-
if (!existsSync(dirPath)) return;
|
|
71
|
-
for (const file of readdirSync(dirPath)) {
|
|
72
|
-
if (file.endsWith(".sh")) {
|
|
73
|
-
chmodSync(nodePath.join(dirPath, file), 493);
|
|
36
|
+
if (projectType[key]) {
|
|
37
|
+
if (projectType.existingFormatter) {
|
|
38
|
+
packages.push(...deps.filter((pkg) => !PRETTIER_PACKAGES.has(pkg)));
|
|
39
|
+
} else {
|
|
40
|
+
packages.push(...deps);
|
|
41
|
+
}
|
|
74
42
|
}
|
|
75
43
|
}
|
|
44
|
+
return packages;
|
|
76
45
|
}
|
|
77
|
-
function readJson(path) {
|
|
78
|
-
const content = readFileSafe(path);
|
|
79
|
-
if (!content) return void 0;
|
|
80
|
-
try {
|
|
81
|
-
return JSON.parse(content);
|
|
82
|
-
} catch {
|
|
83
|
-
return void 0;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function writeJson(path, data) {
|
|
87
|
-
writeFile(path, `${JSON.stringify(data, void 0, 2)}
|
|
88
|
-
`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// src/reconcile.ts
|
|
92
|
-
import nodePath2 from "path";
|
|
93
|
-
var HUSKY_DIR = ".husky";
|
|
94
46
|
function shouldSkipForNonGit(path, isGitRepo2) {
|
|
95
47
|
return path.startsWith(HUSKY_DIR) && !isGitRepo2;
|
|
96
48
|
}
|
|
@@ -99,7 +51,7 @@ function planMissingDirectories(directories, cwd, isGitRepo2) {
|
|
|
99
51
|
const created = [];
|
|
100
52
|
for (const dir of directories) {
|
|
101
53
|
if (shouldSkipForNonGit(dir, isGitRepo2)) continue;
|
|
102
|
-
if (!exists(
|
|
54
|
+
if (!exists(nodePath.join(cwd, dir))) {
|
|
103
55
|
actions.push({ type: "mkdir", path: dir });
|
|
104
56
|
created.push(dir);
|
|
105
57
|
}
|
|
@@ -110,7 +62,7 @@ function planTextPatches(patches, cwd, isGitRepo2) {
|
|
|
110
62
|
const actions = [];
|
|
111
63
|
for (const [filePath, definition] of Object.entries(patches)) {
|
|
112
64
|
if (shouldSkipForNonGit(filePath, isGitRepo2)) continue;
|
|
113
|
-
const content = readFileSafe(
|
|
65
|
+
const content = readFileSafe(nodePath.join(cwd, filePath)) ?? "";
|
|
114
66
|
if (!content.includes(definition.marker)) {
|
|
115
67
|
actions.push({ type: "text-patch", path: filePath, definition });
|
|
116
68
|
}
|
|
@@ -132,7 +84,7 @@ function planManagedFileWrites(files, ctx) {
|
|
|
132
84
|
const actions = [];
|
|
133
85
|
const created = [];
|
|
134
86
|
for (const [filePath, definition] of Object.entries(files)) {
|
|
135
|
-
if (exists(
|
|
87
|
+
if (exists(nodePath.join(ctx.cwd, filePath))) continue;
|
|
136
88
|
const content = resolveFileContent(definition, ctx);
|
|
137
89
|
actions.push({ type: "write", path: filePath, content });
|
|
138
90
|
created.push(filePath);
|
|
@@ -145,7 +97,7 @@ function planTextPatchesWithCreation(patches, ctx) {
|
|
|
145
97
|
for (const [filePath, definition] of Object.entries(patches)) {
|
|
146
98
|
if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
|
|
147
99
|
actions.push({ type: "text-patch", path: filePath, definition });
|
|
148
|
-
if (definition.createIfMissing && !exists(
|
|
100
|
+
if (definition.createIfMissing && !exists(nodePath.join(ctx.cwd, filePath))) {
|
|
149
101
|
created.push(filePath);
|
|
150
102
|
}
|
|
151
103
|
}
|
|
@@ -155,7 +107,7 @@ function planExistingDirectoriesRemoval(directories, cwd) {
|
|
|
155
107
|
const actions = [];
|
|
156
108
|
const removed = [];
|
|
157
109
|
for (const dir of directories) {
|
|
158
|
-
if (exists(
|
|
110
|
+
if (exists(nodePath.join(cwd, dir))) {
|
|
159
111
|
actions.push({ type: "rmdir", path: dir });
|
|
160
112
|
removed.push(dir);
|
|
161
113
|
}
|
|
@@ -166,7 +118,7 @@ function planExistingFilesRemoval(files, cwd) {
|
|
|
166
118
|
const actions = [];
|
|
167
119
|
const removed = [];
|
|
168
120
|
for (const filePath of files) {
|
|
169
|
-
if (exists(
|
|
121
|
+
if (exists(nodePath.join(cwd, filePath))) {
|
|
170
122
|
actions.push({ type: "rm", path: filePath });
|
|
171
123
|
removed.push(filePath);
|
|
172
124
|
}
|
|
@@ -210,7 +162,7 @@ function planDeprecatedFilesRemoval(deprecatedFiles, cwd) {
|
|
|
210
162
|
const actions = [];
|
|
211
163
|
const removed = [];
|
|
212
164
|
for (const filePath of deprecatedFiles) {
|
|
213
|
-
if (exists(
|
|
165
|
+
if (exists(nodePath.join(cwd, filePath))) {
|
|
214
166
|
actions.push({ type: "rm", path: filePath });
|
|
215
167
|
removed.push(filePath);
|
|
216
168
|
}
|
|
@@ -241,16 +193,21 @@ function computeInstallPlan(schema, ctx) {
|
|
|
241
193
|
const actions = [];
|
|
242
194
|
const wouldCreate = [];
|
|
243
195
|
const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
|
|
244
|
-
const
|
|
245
|
-
actions.push(...
|
|
246
|
-
wouldCreate.push(...
|
|
196
|
+
const directories = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);
|
|
197
|
+
actions.push(...directories.actions);
|
|
198
|
+
wouldCreate.push(...directories.created);
|
|
247
199
|
const owned = planOwnedFileWrites(schema.ownedFiles, ctx);
|
|
248
200
|
actions.push(...owned.actions);
|
|
249
201
|
wouldCreate.push(...owned.created);
|
|
250
202
|
const managed = planManagedFileWrites(schema.managedFiles, ctx);
|
|
251
203
|
actions.push(...managed.actions);
|
|
252
204
|
wouldCreate.push(...managed.created);
|
|
253
|
-
const chmodPaths = [
|
|
205
|
+
const chmodPaths = [
|
|
206
|
+
".safeword/hooks",
|
|
207
|
+
".safeword/hooks/cursor",
|
|
208
|
+
".safeword/lib",
|
|
209
|
+
".safeword/scripts"
|
|
210
|
+
];
|
|
254
211
|
if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);
|
|
255
212
|
actions.push({ type: "chmod", paths: chmodPaths });
|
|
256
213
|
for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
|
|
@@ -259,8 +216,20 @@ function computeInstallPlan(schema, ctx) {
|
|
|
259
216
|
const patches = planTextPatchesWithCreation(schema.textPatches, ctx);
|
|
260
217
|
actions.push(...patches.actions);
|
|
261
218
|
wouldCreate.push(...patches.created);
|
|
262
|
-
const packagesToInstall = computePackagesToInstall(
|
|
263
|
-
|
|
219
|
+
const packagesToInstall = computePackagesToInstall(
|
|
220
|
+
schema,
|
|
221
|
+
ctx.projectType,
|
|
222
|
+
ctx.developmentDeps,
|
|
223
|
+
ctx.isGitRepo
|
|
224
|
+
);
|
|
225
|
+
return {
|
|
226
|
+
actions,
|
|
227
|
+
wouldCreate,
|
|
228
|
+
wouldUpdate: [],
|
|
229
|
+
wouldRemove: [],
|
|
230
|
+
packagesToInstall,
|
|
231
|
+
packagesToRemove: []
|
|
232
|
+
};
|
|
264
233
|
}
|
|
265
234
|
function computeUpgradePlan(schema, ctx) {
|
|
266
235
|
const actions = [];
|
|
@@ -272,7 +241,7 @@ function computeUpgradePlan(schema, ctx) {
|
|
|
272
241
|
wouldCreate.push(...missingDirectories.created);
|
|
273
242
|
for (const [filePath, definition] of Object.entries(schema.ownedFiles)) {
|
|
274
243
|
if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
|
|
275
|
-
const fullPath =
|
|
244
|
+
const fullPath = nodePath.join(ctx.cwd, filePath);
|
|
276
245
|
const newContent = resolveFileContent(definition, ctx);
|
|
277
246
|
if (!fileNeedsUpdate(fullPath, newContent)) continue;
|
|
278
247
|
actions.push({ type: "write", path: filePath, content: newContent });
|
|
@@ -283,7 +252,7 @@ function computeUpgradePlan(schema, ctx) {
|
|
|
283
252
|
}
|
|
284
253
|
}
|
|
285
254
|
for (const [filePath, definition] of Object.entries(schema.managedFiles)) {
|
|
286
|
-
const fullPath =
|
|
255
|
+
const fullPath = nodePath.join(ctx.cwd, filePath);
|
|
287
256
|
const newContent = resolveFileContent(definition, ctx);
|
|
288
257
|
if (!exists(fullPath)) {
|
|
289
258
|
actions.push({ type: "write", path: filePath, content: newContent });
|
|
@@ -341,7 +310,7 @@ function computeUninstallPlan(schema, ctx, full) {
|
|
|
341
310
|
actions.push({ type: "json-unmerge", path: filePath, definition });
|
|
342
311
|
}
|
|
343
312
|
for (const [filePath, definition] of Object.entries(schema.textPatches)) {
|
|
344
|
-
const fullPath =
|
|
313
|
+
const fullPath = nodePath.join(ctx.cwd, filePath);
|
|
345
314
|
if (exists(fullPath)) {
|
|
346
315
|
const content = readFileSafe(fullPath) ?? "";
|
|
347
316
|
if (content.includes(definition.marker)) {
|
|
@@ -382,48 +351,57 @@ function executePlan(plan, ctx) {
|
|
|
382
351
|
}
|
|
383
352
|
function executeChmod(cwd, paths) {
|
|
384
353
|
for (const path of paths) {
|
|
385
|
-
const fullPath =
|
|
354
|
+
const fullPath = nodePath.join(cwd, path);
|
|
386
355
|
if (exists(fullPath)) makeScriptsExecutable(fullPath);
|
|
387
356
|
}
|
|
388
357
|
}
|
|
389
358
|
function executeRmdir(cwd, path, result) {
|
|
390
|
-
if (removeIfEmpty(
|
|
359
|
+
if (removeIfEmpty(nodePath.join(cwd, path))) result.removed.push(path);
|
|
391
360
|
}
|
|
392
361
|
function executeAction(action, ctx, result) {
|
|
393
362
|
switch (action.type) {
|
|
394
|
-
case "mkdir":
|
|
395
|
-
ensureDirectory(
|
|
363
|
+
case "mkdir": {
|
|
364
|
+
ensureDirectory(nodePath.join(ctx.cwd, action.path));
|
|
396
365
|
result.created.push(action.path);
|
|
397
366
|
break;
|
|
398
|
-
|
|
367
|
+
}
|
|
368
|
+
case "rmdir": {
|
|
399
369
|
executeRmdir(ctx.cwd, action.path, result);
|
|
400
370
|
break;
|
|
401
|
-
|
|
371
|
+
}
|
|
372
|
+
case "write": {
|
|
402
373
|
executeWrite(ctx.cwd, action.path, action.content, result);
|
|
403
374
|
break;
|
|
404
|
-
|
|
405
|
-
|
|
375
|
+
}
|
|
376
|
+
case "rm": {
|
|
377
|
+
remove(nodePath.join(ctx.cwd, action.path));
|
|
406
378
|
result.removed.push(action.path);
|
|
407
379
|
break;
|
|
408
|
-
|
|
380
|
+
}
|
|
381
|
+
case "chmod": {
|
|
409
382
|
executeChmod(ctx.cwd, action.paths);
|
|
410
383
|
break;
|
|
411
|
-
|
|
384
|
+
}
|
|
385
|
+
case "json-merge": {
|
|
412
386
|
executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);
|
|
413
387
|
break;
|
|
414
|
-
|
|
388
|
+
}
|
|
389
|
+
case "json-unmerge": {
|
|
415
390
|
executeJsonUnmerge(ctx.cwd, action.path, action.definition);
|
|
416
391
|
break;
|
|
417
|
-
|
|
392
|
+
}
|
|
393
|
+
case "text-patch": {
|
|
418
394
|
executeTextPatch(ctx.cwd, action.path, action.definition);
|
|
419
395
|
break;
|
|
420
|
-
|
|
396
|
+
}
|
|
397
|
+
case "text-unpatch": {
|
|
421
398
|
executeTextUnpatch(ctx.cwd, action.path, action.definition);
|
|
422
399
|
break;
|
|
400
|
+
}
|
|
423
401
|
}
|
|
424
402
|
}
|
|
425
403
|
function executeWrite(cwd, path, content, result) {
|
|
426
|
-
const fullPath =
|
|
404
|
+
const fullPath = nodePath.join(cwd, path);
|
|
427
405
|
const existed = exists(fullPath);
|
|
428
406
|
writeFile(fullPath, content);
|
|
429
407
|
(existed ? result.updated : result.created).push(path);
|
|
@@ -431,7 +409,7 @@ function executeWrite(cwd, path, content, result) {
|
|
|
431
409
|
function resolveFileContent(definition, ctx) {
|
|
432
410
|
if (definition.template) {
|
|
433
411
|
const templatesDirectory = getTemplatesDirectory();
|
|
434
|
-
return readFile(
|
|
412
|
+
return readFile(nodePath.join(templatesDirectory, definition.template));
|
|
435
413
|
}
|
|
436
414
|
if (definition.content) {
|
|
437
415
|
return typeof definition.content === "function" ? definition.content() : definition.content;
|
|
@@ -452,31 +430,25 @@ function computePackagesToInstall(schema, projectType, installedDevelopmentDeps,
|
|
|
452
430
|
if (!isGitRepo2) {
|
|
453
431
|
needed = needed.filter((pkg) => !GIT_ONLY_PACKAGES.has(pkg));
|
|
454
432
|
}
|
|
455
|
-
|
|
456
|
-
if (projectType[key]) {
|
|
457
|
-
needed.push(...deps);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
433
|
+
needed.push(...getConditionalPackages(schema.packages.conditional, projectType));
|
|
460
434
|
return needed.filter((pkg) => !(pkg in installedDevelopmentDeps));
|
|
461
435
|
}
|
|
462
436
|
function computePackagesToRemove(schema, projectType, installedDevelopmentDeps) {
|
|
463
|
-
const safewordPackages = [
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
}
|
|
437
|
+
const safewordPackages = [
|
|
438
|
+
...schema.packages.base,
|
|
439
|
+
...getConditionalPackages(schema.packages.conditional, projectType)
|
|
440
|
+
];
|
|
469
441
|
return safewordPackages.filter((pkg) => pkg in installedDevelopmentDeps);
|
|
470
442
|
}
|
|
471
443
|
function executeJsonMerge(cwd, path, definition, ctx) {
|
|
472
|
-
const fullPath =
|
|
444
|
+
const fullPath = nodePath.join(cwd, path);
|
|
473
445
|
const existing = readJson(fullPath) ?? {};
|
|
474
446
|
const merged = definition.merge(existing, ctx);
|
|
475
447
|
if (JSON.stringify(existing) === JSON.stringify(merged)) return;
|
|
476
448
|
writeJson(fullPath, merged);
|
|
477
449
|
}
|
|
478
450
|
function executeJsonUnmerge(cwd, path, definition) {
|
|
479
|
-
const fullPath =
|
|
451
|
+
const fullPath = nodePath.join(cwd, path);
|
|
480
452
|
if (!exists(fullPath)) return;
|
|
481
453
|
const existing = readJson(fullPath);
|
|
482
454
|
if (!existing) return;
|
|
@@ -491,14 +463,14 @@ function executeJsonUnmerge(cwd, path, definition) {
|
|
|
491
463
|
writeJson(fullPath, unmerged);
|
|
492
464
|
}
|
|
493
465
|
function executeTextPatch(cwd, path, definition) {
|
|
494
|
-
const fullPath =
|
|
466
|
+
const fullPath = nodePath.join(cwd, path);
|
|
495
467
|
let content = readFileSafe(fullPath) ?? "";
|
|
496
468
|
if (content.includes(definition.marker)) return;
|
|
497
469
|
content = definition.operation === "prepend" ? definition.content + content : content + definition.content;
|
|
498
470
|
writeFile(fullPath, content);
|
|
499
471
|
}
|
|
500
472
|
function executeTextUnpatch(cwd, path, definition) {
|
|
501
|
-
const fullPath =
|
|
473
|
+
const fullPath = nodePath.join(cwd, path);
|
|
502
474
|
const content = readFileSafe(fullPath);
|
|
503
475
|
if (!content) return;
|
|
504
476
|
let unpatched = content.replace(definition.content, "");
|
|
@@ -511,56 +483,74 @@ function executeTextUnpatch(cwd, path, definition) {
|
|
|
511
483
|
}
|
|
512
484
|
|
|
513
485
|
// src/templates/config.ts
|
|
514
|
-
function getEslintConfig() {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
486
|
+
function getEslintConfig(hasExistingFormatter2 = false) {
|
|
487
|
+
if (hasExistingFormatter2) {
|
|
488
|
+
return getFormatterAgnosticEslintConfig();
|
|
489
|
+
}
|
|
490
|
+
return getStandardEslintConfig();
|
|
491
|
+
}
|
|
492
|
+
function getStandardEslintConfig() {
|
|
493
|
+
return `import { dirname } from "node:path";
|
|
494
|
+
import { fileURLToPath } from "node:url";
|
|
518
495
|
import safeword from "eslint-plugin-safeword";
|
|
519
496
|
import eslintConfigPrettier from "eslint-config-prettier";
|
|
520
497
|
|
|
521
|
-
|
|
498
|
+
const { detect, configs } = safeword;
|
|
522
499
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
// Build dynamic ignores based on detected frameworks
|
|
527
|
-
const ignores = ["**/node_modules/", "**/dist/", "**/build/", "**/coverage/"];
|
|
528
|
-
if (deps["next"]) ignores.push(".next/");
|
|
529
|
-
if (deps["astro"]) ignores.push(".astro/");
|
|
500
|
+
const deps = detect.collectAllDeps(__dirname);
|
|
501
|
+
const framework = detect.detectFramework(deps);
|
|
530
502
|
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
baseConfig = safeword.configs.recommendedTypeScript;
|
|
542
|
-
} else {
|
|
543
|
-
baseConfig = safeword.configs.recommended;
|
|
544
|
-
}
|
|
503
|
+
// Map framework to base config
|
|
504
|
+
// Note: Astro config only lints .astro files, so we combine it with TypeScript config
|
|
505
|
+
// to also lint .ts files in Astro projects
|
|
506
|
+
const baseConfigs = {
|
|
507
|
+
next: configs.recommendedTypeScriptNext,
|
|
508
|
+
react: configs.recommendedTypeScriptReact,
|
|
509
|
+
astro: [...configs.recommendedTypeScript, ...configs.astro],
|
|
510
|
+
typescript: configs.recommendedTypeScript,
|
|
511
|
+
javascript: configs.recommended,
|
|
512
|
+
};
|
|
545
513
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
...
|
|
514
|
+
export default [
|
|
515
|
+
{ ignores: detect.getIgnores(deps) },
|
|
516
|
+
...baseConfigs[framework],
|
|
517
|
+
...(detect.hasVitest(deps) ? configs.vitest : []),
|
|
518
|
+
...(detect.hasPlaywright(deps) ? configs.playwright : []),
|
|
519
|
+
...(detect.hasTailwind(deps) ? configs.tailwind : []),
|
|
520
|
+
...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
|
|
521
|
+
eslintConfigPrettier,
|
|
550
522
|
];
|
|
551
|
-
|
|
552
|
-
// Add test configs if testing frameworks detected
|
|
553
|
-
if (deps["vitest"]) {
|
|
554
|
-
configs.push(...safeword.configs.vitest);
|
|
555
|
-
}
|
|
556
|
-
if (deps["playwright"] || deps["@playwright/test"]) {
|
|
557
|
-
configs.push(...safeword.configs.playwright);
|
|
523
|
+
`;
|
|
558
524
|
}
|
|
525
|
+
function getFormatterAgnosticEslintConfig() {
|
|
526
|
+
return `import { dirname } from "node:path";
|
|
527
|
+
import { fileURLToPath } from "node:url";
|
|
528
|
+
import safeword from "eslint-plugin-safeword";
|
|
559
529
|
|
|
560
|
-
|
|
561
|
-
|
|
530
|
+
const { detect, configs } = safeword;
|
|
531
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
532
|
+
const deps = detect.collectAllDeps(__dirname);
|
|
533
|
+
const framework = detect.detectFramework(deps);
|
|
562
534
|
|
|
563
|
-
|
|
535
|
+
// Map framework to base config
|
|
536
|
+
// Note: Astro config only lints .astro files, so we combine it with TypeScript config
|
|
537
|
+
// to also lint .ts files in Astro projects
|
|
538
|
+
const baseConfigs = {
|
|
539
|
+
next: configs.recommendedTypeScriptNext,
|
|
540
|
+
react: configs.recommendedTypeScriptReact,
|
|
541
|
+
astro: [...configs.recommendedTypeScript, ...configs.astro],
|
|
542
|
+
typescript: configs.recommendedTypeScript,
|
|
543
|
+
javascript: configs.recommended,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
export default [
|
|
547
|
+
{ ignores: detect.getIgnores(deps) },
|
|
548
|
+
...baseConfigs[framework],
|
|
549
|
+
...(detect.hasVitest(deps) ? configs.vitest : []),
|
|
550
|
+
...(detect.hasPlaywright(deps) ? configs.playwright : []),
|
|
551
|
+
...(detect.hasTailwind(deps) ? configs.tailwind : []),
|
|
552
|
+
...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
|
|
553
|
+
];
|
|
564
554
|
`;
|
|
565
555
|
}
|
|
566
556
|
var CURSOR_HOOKS = {
|
|
@@ -645,7 +635,7 @@ Read it BEFORE working on any task in this project.
|
|
|
645
635
|
|
|
646
636
|
// src/utils/hooks.ts
|
|
647
637
|
function isHookEntry(h) {
|
|
648
|
-
return typeof h === "object" && h !==
|
|
638
|
+
return typeof h === "object" && h !== null && "hooks" in h && Array.isArray(h.hooks);
|
|
649
639
|
}
|
|
650
640
|
function isSafewordHook(h) {
|
|
651
641
|
if (!isHookEntry(h)) return false;
|
|
@@ -822,6 +812,7 @@ var SAFEWORD_SCHEMA = {
|
|
|
822
812
|
template: "skills/safeword-writing-plans/SKILL.md"
|
|
823
813
|
},
|
|
824
814
|
".claude/commands/architecture.md": { template: "commands/architecture.md" },
|
|
815
|
+
".claude/commands/audit.md": { template: "commands/audit.md" },
|
|
825
816
|
".claude/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
|
|
826
817
|
".claude/commands/lint.md": { template: "commands/lint.md" },
|
|
827
818
|
".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
|
|
@@ -845,8 +836,9 @@ var SAFEWORD_SCHEMA = {
|
|
|
845
836
|
".cursor/rules/safeword-writing-plans.mdc": {
|
|
846
837
|
template: "cursor/rules/safeword-writing-plans.mdc"
|
|
847
838
|
},
|
|
848
|
-
// Cursor commands (
|
|
839
|
+
// Cursor commands (5 files - same as Claude)
|
|
849
840
|
".cursor/commands/architecture.md": { template: "commands/architecture.md" },
|
|
841
|
+
".cursor/commands/audit.md": { template: "commands/audit.md" },
|
|
850
842
|
".cursor/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
|
|
851
843
|
".cursor/commands/lint.md": { template: "commands/lint.md" },
|
|
852
844
|
".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
|
|
@@ -857,7 +849,7 @@ var SAFEWORD_SCHEMA = {
|
|
|
857
849
|
// Files created if missing, updated only if content matches current template
|
|
858
850
|
managedFiles: {
|
|
859
851
|
"eslint.config.mjs": {
|
|
860
|
-
generator: () => getEslintConfig()
|
|
852
|
+
generator: (ctx) => getEslintConfig(ctx.projectType.existingFormatter)
|
|
861
853
|
},
|
|
862
854
|
// Minimal tsconfig for ESLint type-checked linting (only if missing)
|
|
863
855
|
"tsconfig.json": {
|
|
@@ -883,6 +875,17 @@ var SAFEWORD_SCHEMA = {
|
|
|
883
875
|
2
|
|
884
876
|
);
|
|
885
877
|
}
|
|
878
|
+
},
|
|
879
|
+
// Knip config for dead code detection (used by /audit)
|
|
880
|
+
"knip.json": {
|
|
881
|
+
generator: () => JSON.stringify(
|
|
882
|
+
{
|
|
883
|
+
ignore: [".safeword/**"],
|
|
884
|
+
ignoreDependencies: ["eslint-plugin-safeword"]
|
|
885
|
+
},
|
|
886
|
+
void 0,
|
|
887
|
+
2
|
|
888
|
+
)
|
|
886
889
|
}
|
|
887
890
|
},
|
|
888
891
|
// JSON files where we merge specific keys
|
|
@@ -890,15 +893,23 @@ var SAFEWORD_SCHEMA = {
|
|
|
890
893
|
"package.json": {
|
|
891
894
|
keys: ["scripts.lint", "scripts.format", "scripts.format:check", "scripts.knip"],
|
|
892
895
|
conditionalKeys: {
|
|
896
|
+
existingLinter: ["scripts.lint:eslint"],
|
|
897
|
+
// Projects with existing linter get separate ESLint script
|
|
893
898
|
publishableLibrary: ["scripts.publint"],
|
|
894
899
|
shell: ["scripts.lint:sh"]
|
|
895
900
|
},
|
|
896
901
|
merge: (existing, ctx) => {
|
|
897
902
|
const scripts = { ...existing.scripts };
|
|
898
903
|
const result = { ...existing };
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
|
|
904
|
+
if (ctx.projectType.existingLinter) {
|
|
905
|
+
if (!scripts["lint:eslint"]) scripts["lint:eslint"] = "eslint .";
|
|
906
|
+
} else {
|
|
907
|
+
if (!scripts.lint) scripts.lint = "eslint .";
|
|
908
|
+
}
|
|
909
|
+
if (!ctx.projectType.existingFormatter) {
|
|
910
|
+
if (!scripts.format) scripts.format = "prettier --write .";
|
|
911
|
+
if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
|
|
912
|
+
}
|
|
902
913
|
if (!scripts.knip) scripts.knip = "knip";
|
|
903
914
|
if (ctx.projectType.publishableLibrary && !scripts.publint) {
|
|
904
915
|
scripts.publint = "publint";
|
|
@@ -912,6 +923,7 @@ var SAFEWORD_SCHEMA = {
|
|
|
912
923
|
unmerge: (existing) => {
|
|
913
924
|
const result = { ...existing };
|
|
914
925
|
const scripts = { ...existing.scripts };
|
|
926
|
+
delete scripts["lint:eslint"];
|
|
915
927
|
delete scripts["lint:sh"];
|
|
916
928
|
delete scripts["format:check"];
|
|
917
929
|
delete scripts.knip;
|
|
@@ -1087,48 +1099,62 @@ var SAFEWORD_SCHEMA = {
|
|
|
1087
1099
|
// NPM packages to install
|
|
1088
1100
|
packages: {
|
|
1089
1101
|
base: [
|
|
1090
|
-
// Core tools
|
|
1102
|
+
// Core tools (always needed)
|
|
1091
1103
|
"eslint",
|
|
1092
|
-
"prettier",
|
|
1093
1104
|
// Safeword plugin (bundles eslint-config-prettier + all ESLint plugins)
|
|
1094
1105
|
"eslint-plugin-safeword",
|
|
1095
|
-
//
|
|
1106
|
+
// Architecture and dead code tools (used by /audit)
|
|
1107
|
+
"dependency-cruiser",
|
|
1096
1108
|
"knip"
|
|
1097
1109
|
],
|
|
1098
1110
|
conditional: {
|
|
1099
|
-
// Prettier
|
|
1111
|
+
// Prettier (only for projects without existing formatter)
|
|
1112
|
+
standard: ["prettier"],
|
|
1113
|
+
// "standard" = !existingFormatter
|
|
1114
|
+
// Prettier plugins (only for projects without existing formatter that need them)
|
|
1100
1115
|
astro: ["prettier-plugin-astro"],
|
|
1101
1116
|
tailwind: ["prettier-plugin-tailwindcss"],
|
|
1117
|
+
shell: ["prettier-plugin-sh"],
|
|
1102
1118
|
// Non-ESLint tools
|
|
1103
1119
|
publishableLibrary: ["publint"],
|
|
1104
|
-
|
|
1120
|
+
shellcheck: ["shellcheck"]
|
|
1121
|
+
// Renamed from shell to avoid conflict with prettier-plugin-sh
|
|
1105
1122
|
}
|
|
1106
1123
|
}
|
|
1107
1124
|
};
|
|
1108
1125
|
|
|
1109
1126
|
// src/utils/git.ts
|
|
1110
|
-
import
|
|
1127
|
+
import nodePath2 from "path";
|
|
1111
1128
|
function isGitRepo(cwd) {
|
|
1112
|
-
return exists(
|
|
1129
|
+
return exists(nodePath2.join(cwd, ".git"));
|
|
1113
1130
|
}
|
|
1114
1131
|
|
|
1115
1132
|
// src/utils/context.ts
|
|
1116
|
-
import
|
|
1133
|
+
import nodePath4 from "path";
|
|
1117
1134
|
|
|
1118
1135
|
// src/utils/project-detector.ts
|
|
1119
|
-
import { readdirSync
|
|
1120
|
-
import
|
|
1136
|
+
import { readdirSync } from "fs";
|
|
1137
|
+
import nodePath3 from "path";
|
|
1138
|
+
import { detect } from "eslint-plugin-safeword";
|
|
1139
|
+
var {
|
|
1140
|
+
TAILWIND_PACKAGES,
|
|
1141
|
+
TANSTACK_QUERY_PACKAGES,
|
|
1142
|
+
PLAYWRIGHT_PACKAGES,
|
|
1143
|
+
FORMATTER_CONFIG_FILES,
|
|
1144
|
+
hasExistingLinter,
|
|
1145
|
+
hasExistingFormatter
|
|
1146
|
+
} = detect;
|
|
1121
1147
|
function hasShellScripts(cwd, maxDepth = 4) {
|
|
1122
1148
|
const excludeDirectories = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
|
|
1123
1149
|
function scan(dir, depth) {
|
|
1124
1150
|
if (depth > maxDepth) return false;
|
|
1125
1151
|
try {
|
|
1126
|
-
const entries =
|
|
1152
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1127
1153
|
for (const entry of entries) {
|
|
1128
1154
|
if (entry.isFile() && entry.name.endsWith(".sh")) {
|
|
1129
1155
|
return true;
|
|
1130
1156
|
}
|
|
1131
|
-
if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(
|
|
1157
|
+
if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(nodePath3.join(dir, entry.name), depth + 1)) {
|
|
1132
1158
|
return true;
|
|
1133
1159
|
}
|
|
1134
1160
|
}
|
|
@@ -1142,16 +1168,20 @@ function detectProjectType(packageJson, cwd) {
|
|
|
1142
1168
|
const deps = packageJson.dependencies || {};
|
|
1143
1169
|
const developmentDeps = packageJson.devDependencies || {};
|
|
1144
1170
|
const allDeps = { ...deps, ...developmentDeps };
|
|
1171
|
+
const scripts = packageJson.scripts || {};
|
|
1145
1172
|
const hasTypescript = "typescript" in allDeps;
|
|
1146
1173
|
const hasReact = "react" in deps || "react" in developmentDeps;
|
|
1147
1174
|
const hasNextJs = "next" in deps;
|
|
1148
1175
|
const hasAstro = "astro" in deps || "astro" in developmentDeps;
|
|
1149
1176
|
const hasVitest = "vitest" in developmentDeps;
|
|
1150
1177
|
const hasPlaywright = "@playwright/test" in developmentDeps;
|
|
1151
|
-
const hasTailwind =
|
|
1178
|
+
const hasTailwind = TAILWIND_PACKAGES.some((pkg) => pkg in allDeps);
|
|
1179
|
+
const hasTanstackQuery = TANSTACK_QUERY_PACKAGES.some((pkg) => pkg in allDeps);
|
|
1152
1180
|
const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
|
|
1153
1181
|
const isPublishable = hasEntryPoints && packageJson.private !== true;
|
|
1154
1182
|
const hasShell = cwd ? hasShellScripts(cwd) : false;
|
|
1183
|
+
const hasLinter = hasExistingLinter(scripts);
|
|
1184
|
+
const hasFormatter = cwd ? hasExistingFormatter(cwd, scripts) : "format" in scripts;
|
|
1155
1185
|
return {
|
|
1156
1186
|
typescript: hasTypescript,
|
|
1157
1187
|
react: hasReact || hasNextJs,
|
|
@@ -1161,14 +1191,17 @@ function detectProjectType(packageJson, cwd) {
|
|
|
1161
1191
|
vitest: hasVitest,
|
|
1162
1192
|
playwright: hasPlaywright,
|
|
1163
1193
|
tailwind: hasTailwind,
|
|
1194
|
+
tanstackQuery: hasTanstackQuery,
|
|
1164
1195
|
publishableLibrary: isPublishable,
|
|
1165
|
-
shell: hasShell
|
|
1196
|
+
shell: hasShell,
|
|
1197
|
+
existingLinter: hasLinter,
|
|
1198
|
+
existingFormatter: hasFormatter
|
|
1166
1199
|
};
|
|
1167
1200
|
}
|
|
1168
1201
|
|
|
1169
1202
|
// src/utils/context.ts
|
|
1170
1203
|
function createProjectContext(cwd) {
|
|
1171
|
-
const packageJson = readJson(
|
|
1204
|
+
const packageJson = readJson(nodePath4.join(cwd, "package.json"));
|
|
1172
1205
|
return {
|
|
1173
1206
|
cwd,
|
|
1174
1207
|
projectType: detectProjectType(packageJson ?? {}, cwd),
|
|
@@ -1177,45 +1210,10 @@ function createProjectContext(cwd) {
|
|
|
1177
1210
|
};
|
|
1178
1211
|
}
|
|
1179
1212
|
|
|
1180
|
-
// src/utils/output.ts
|
|
1181
|
-
function info(message) {
|
|
1182
|
-
console.log(message);
|
|
1183
|
-
}
|
|
1184
|
-
function success(message) {
|
|
1185
|
-
console.log(`\u2713 ${message}`);
|
|
1186
|
-
}
|
|
1187
|
-
function warn(message) {
|
|
1188
|
-
console.warn(`\u26A0 ${message}`);
|
|
1189
|
-
}
|
|
1190
|
-
function error(message) {
|
|
1191
|
-
console.error(`\u2717 ${message}`);
|
|
1192
|
-
}
|
|
1193
|
-
function header(title) {
|
|
1194
|
-
console.log(`
|
|
1195
|
-
${title}`);
|
|
1196
|
-
console.log("\u2500".repeat(title.length));
|
|
1197
|
-
}
|
|
1198
|
-
function listItem(item, indent = 2) {
|
|
1199
|
-
console.log(`${" ".repeat(indent)}\u2022 ${item}`);
|
|
1200
|
-
}
|
|
1201
|
-
function keyValue(key, value) {
|
|
1202
|
-
console.log(` ${key}: ${value}`);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
1213
|
export {
|
|
1206
|
-
exists,
|
|
1207
|
-
readFileSafe,
|
|
1208
|
-
writeJson,
|
|
1209
1214
|
reconcile,
|
|
1210
1215
|
SAFEWORD_SCHEMA,
|
|
1211
1216
|
isGitRepo,
|
|
1212
|
-
createProjectContext
|
|
1213
|
-
info,
|
|
1214
|
-
success,
|
|
1215
|
-
warn,
|
|
1216
|
-
error,
|
|
1217
|
-
header,
|
|
1218
|
-
listItem,
|
|
1219
|
-
keyValue
|
|
1217
|
+
createProjectContext
|
|
1220
1218
|
};
|
|
1221
|
-
//# sourceMappingURL=chunk-
|
|
1219
|
+
//# sourceMappingURL=chunk-ZE6QJHZD.js.map
|