safeword 0.8.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{check-QMAGWUOA.js → check-MLYBKA5Z.js} +6 -8
- package/dist/{check-QMAGWUOA.js.map → check-MLYBKA5Z.js.map} +1 -1
- package/dist/{chunk-CLSGXTOL.js → chunk-UUKED7KU.js} +482 -78
- package/dist/chunk-UUKED7KU.js.map +1 -0
- package/dist/cli.js +5 -9
- package/dist/cli.js.map +1 -1
- package/dist/{diff-2T7UDES7.js → diff-Q44YDA2W.js} +5 -7
- package/dist/{diff-2T7UDES7.js.map → diff-Q44YDA2W.js.map} +1 -1
- package/dist/index.d.ts +0 -5
- package/dist/reset-EEFVJ5GB.js +73 -0
- package/dist/reset-EEFVJ5GB.js.map +1 -0
- package/dist/setup-WZ3F25KX.js +91 -0
- package/dist/setup-WZ3F25KX.js.map +1 -0
- package/dist/{upgrade-FALAUUKE.js → upgrade-WB7N3GWG.js} +31 -43
- package/dist/upgrade-WB7N3GWG.js.map +1 -0
- package/package.json +1 -1
- package/templates/SAFEWORD.md +1 -0
- package/templates/guides/cli-reference.md +9 -11
- package/dist/chunk-4URRFBUS.js +0 -88
- package/dist/chunk-4URRFBUS.js.map +0 -1
- package/dist/chunk-CLSGXTOL.js.map +0 -1
- package/dist/chunk-KQ6BLN6W.js +0 -493
- package/dist/chunk-KQ6BLN6W.js.map +0 -1
- package/dist/reset-QRXG7KZZ.js +0 -74
- package/dist/reset-QRXG7KZZ.js.map +0 -1
- package/dist/setup-QUUJ7SH3.js +0 -100
- package/dist/setup-QUUJ7SH3.js.map +0 -1
- package/dist/sync-ISBJ7X2T.js +0 -9
- package/dist/sync-ISBJ7X2T.js.map +0 -1
- package/dist/upgrade-FALAUUKE.js.map +0 -1
|
@@ -88,10 +88,431 @@ function writeJson(path, data) {
|
|
|
88
88
|
`);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// src/reconcile.ts
|
|
92
|
+
import nodePath2 from "path";
|
|
93
|
+
var HUSKY_DIR = ".husky";
|
|
94
|
+
function shouldSkipForNonGit(path, isGitRepo2) {
|
|
95
|
+
return path.startsWith(HUSKY_DIR) && !isGitRepo2;
|
|
96
|
+
}
|
|
97
|
+
function planMissingDirectories(directories, cwd, isGitRepo2) {
|
|
98
|
+
const actions = [];
|
|
99
|
+
const created = [];
|
|
100
|
+
for (const dir of directories) {
|
|
101
|
+
if (shouldSkipForNonGit(dir, isGitRepo2)) continue;
|
|
102
|
+
if (!exists(nodePath2.join(cwd, dir))) {
|
|
103
|
+
actions.push({ type: "mkdir", path: dir });
|
|
104
|
+
created.push(dir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { actions, created };
|
|
108
|
+
}
|
|
109
|
+
function planTextPatches(patches, cwd, isGitRepo2) {
|
|
110
|
+
const actions = [];
|
|
111
|
+
for (const [filePath, definition] of Object.entries(patches)) {
|
|
112
|
+
if (shouldSkipForNonGit(filePath, isGitRepo2)) continue;
|
|
113
|
+
const content = readFileSafe(nodePath2.join(cwd, filePath)) ?? "";
|
|
114
|
+
if (!content.includes(definition.marker)) {
|
|
115
|
+
actions.push({ type: "text-patch", path: filePath, definition });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return actions;
|
|
119
|
+
}
|
|
120
|
+
function planOwnedFileWrites(files, ctx) {
|
|
121
|
+
const actions = [];
|
|
122
|
+
const created = [];
|
|
123
|
+
for (const [filePath, definition] of Object.entries(files)) {
|
|
124
|
+
if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
|
|
125
|
+
const content = resolveFileContent(definition, ctx);
|
|
126
|
+
actions.push({ type: "write", path: filePath, content });
|
|
127
|
+
created.push(filePath);
|
|
128
|
+
}
|
|
129
|
+
return { actions, created };
|
|
130
|
+
}
|
|
131
|
+
function planManagedFileWrites(files, ctx) {
|
|
132
|
+
const actions = [];
|
|
133
|
+
const created = [];
|
|
134
|
+
for (const [filePath, definition] of Object.entries(files)) {
|
|
135
|
+
if (exists(nodePath2.join(ctx.cwd, filePath))) continue;
|
|
136
|
+
const content = resolveFileContent(definition, ctx);
|
|
137
|
+
actions.push({ type: "write", path: filePath, content });
|
|
138
|
+
created.push(filePath);
|
|
139
|
+
}
|
|
140
|
+
return { actions, created };
|
|
141
|
+
}
|
|
142
|
+
function planTextPatchesWithCreation(patches, ctx) {
|
|
143
|
+
const actions = [];
|
|
144
|
+
const created = [];
|
|
145
|
+
for (const [filePath, definition] of Object.entries(patches)) {
|
|
146
|
+
if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
|
|
147
|
+
actions.push({ type: "text-patch", path: filePath, definition });
|
|
148
|
+
if (definition.createIfMissing && !exists(nodePath2.join(ctx.cwd, filePath))) {
|
|
149
|
+
created.push(filePath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { actions, created };
|
|
153
|
+
}
|
|
154
|
+
function planExistingDirectoriesRemoval(directories, cwd) {
|
|
155
|
+
const actions = [];
|
|
156
|
+
const removed = [];
|
|
157
|
+
for (const dir of directories) {
|
|
158
|
+
if (exists(nodePath2.join(cwd, dir))) {
|
|
159
|
+
actions.push({ type: "rmdir", path: dir });
|
|
160
|
+
removed.push(dir);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { actions, removed };
|
|
164
|
+
}
|
|
165
|
+
function planExistingFilesRemoval(files, cwd) {
|
|
166
|
+
const actions = [];
|
|
167
|
+
const removed = [];
|
|
168
|
+
for (const filePath of files) {
|
|
169
|
+
if (exists(nodePath2.join(cwd, filePath))) {
|
|
170
|
+
actions.push({ type: "rm", path: filePath });
|
|
171
|
+
removed.push(filePath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { actions, removed };
|
|
175
|
+
}
|
|
176
|
+
function getClaudeParentDirectoryForCleanup(filePath) {
|
|
177
|
+
if (!filePath.startsWith(".claude/")) return void 0;
|
|
178
|
+
const parentDirectory = filePath.slice(0, Math.max(0, filePath.lastIndexOf("/")));
|
|
179
|
+
if (!parentDirectory || parentDirectory === ".claude" || parentDirectory === ".claude/skills" || parentDirectory === ".claude/commands") {
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
return parentDirectory;
|
|
183
|
+
}
|
|
184
|
+
async function reconcile(schema, mode, ctx, options) {
|
|
185
|
+
const dryRun = options?.dryRun ?? false;
|
|
186
|
+
const plan = computePlan(schema, mode, ctx);
|
|
187
|
+
if (dryRun) {
|
|
188
|
+
return {
|
|
189
|
+
actions: plan.actions,
|
|
190
|
+
applied: false,
|
|
191
|
+
created: plan.wouldCreate,
|
|
192
|
+
updated: plan.wouldUpdate,
|
|
193
|
+
removed: plan.wouldRemove,
|
|
194
|
+
packagesToInstall: plan.packagesToInstall,
|
|
195
|
+
packagesToRemove: plan.packagesToRemove
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const result = executePlan(plan, ctx);
|
|
199
|
+
return {
|
|
200
|
+
actions: plan.actions,
|
|
201
|
+
applied: true,
|
|
202
|
+
created: result.created,
|
|
203
|
+
updated: result.updated,
|
|
204
|
+
removed: result.removed,
|
|
205
|
+
packagesToInstall: plan.packagesToInstall,
|
|
206
|
+
packagesToRemove: plan.packagesToRemove
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function planDeprecatedFilesRemoval(deprecatedFiles, cwd) {
|
|
210
|
+
const actions = [];
|
|
211
|
+
const removed = [];
|
|
212
|
+
for (const filePath of deprecatedFiles) {
|
|
213
|
+
if (exists(nodePath2.join(cwd, filePath))) {
|
|
214
|
+
actions.push({ type: "rm", path: filePath });
|
|
215
|
+
removed.push(filePath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { actions, removed };
|
|
219
|
+
}
|
|
220
|
+
function computePlan(schema, mode, ctx) {
|
|
221
|
+
switch (mode) {
|
|
222
|
+
case "install": {
|
|
223
|
+
return computeInstallPlan(schema, ctx);
|
|
224
|
+
}
|
|
225
|
+
case "upgrade": {
|
|
226
|
+
return computeUpgradePlan(schema, ctx);
|
|
227
|
+
}
|
|
228
|
+
case "uninstall": {
|
|
229
|
+
return computeUninstallPlan(schema, ctx, false);
|
|
230
|
+
}
|
|
231
|
+
case "uninstall-full": {
|
|
232
|
+
return computeUninstallPlan(schema, ctx, true);
|
|
233
|
+
}
|
|
234
|
+
default: {
|
|
235
|
+
const _exhaustiveCheck = mode;
|
|
236
|
+
return _exhaustiveCheck;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function computeInstallPlan(schema, ctx) {
|
|
241
|
+
const actions = [];
|
|
242
|
+
const wouldCreate = [];
|
|
243
|
+
const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
|
|
244
|
+
const dirs = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);
|
|
245
|
+
actions.push(...dirs.actions);
|
|
246
|
+
wouldCreate.push(...dirs.created);
|
|
247
|
+
const owned = planOwnedFileWrites(schema.ownedFiles, ctx);
|
|
248
|
+
actions.push(...owned.actions);
|
|
249
|
+
wouldCreate.push(...owned.created);
|
|
250
|
+
const managed = planManagedFileWrites(schema.managedFiles, ctx);
|
|
251
|
+
actions.push(...managed.actions);
|
|
252
|
+
wouldCreate.push(...managed.created);
|
|
253
|
+
const chmodPaths = [".safeword/hooks", ".safeword/hooks/cursor", ".safeword/lib", ".safeword/scripts"];
|
|
254
|
+
if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);
|
|
255
|
+
actions.push({ type: "chmod", paths: chmodPaths });
|
|
256
|
+
for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
|
|
257
|
+
actions.push({ type: "json-merge", path: filePath, definition });
|
|
258
|
+
}
|
|
259
|
+
const patches = planTextPatchesWithCreation(schema.textPatches, ctx);
|
|
260
|
+
actions.push(...patches.actions);
|
|
261
|
+
wouldCreate.push(...patches.created);
|
|
262
|
+
const packagesToInstall = computePackagesToInstall(schema, ctx.projectType, ctx.developmentDeps, ctx.isGitRepo);
|
|
263
|
+
return { actions, wouldCreate, wouldUpdate: [], wouldRemove: [], packagesToInstall, packagesToRemove: [] };
|
|
264
|
+
}
|
|
265
|
+
function computeUpgradePlan(schema, ctx) {
|
|
266
|
+
const actions = [];
|
|
267
|
+
const wouldCreate = [];
|
|
268
|
+
const wouldUpdate = [];
|
|
269
|
+
const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
|
|
270
|
+
const missingDirectories = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);
|
|
271
|
+
actions.push(...missingDirectories.actions);
|
|
272
|
+
wouldCreate.push(...missingDirectories.created);
|
|
273
|
+
for (const [filePath, definition] of Object.entries(schema.ownedFiles)) {
|
|
274
|
+
if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
|
|
275
|
+
const fullPath = nodePath2.join(ctx.cwd, filePath);
|
|
276
|
+
const newContent = resolveFileContent(definition, ctx);
|
|
277
|
+
if (!fileNeedsUpdate(fullPath, newContent)) continue;
|
|
278
|
+
actions.push({ type: "write", path: filePath, content: newContent });
|
|
279
|
+
if (exists(fullPath)) {
|
|
280
|
+
wouldUpdate.push(filePath);
|
|
281
|
+
} else {
|
|
282
|
+
wouldCreate.push(filePath);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const [filePath, definition] of Object.entries(schema.managedFiles)) {
|
|
286
|
+
const fullPath = nodePath2.join(ctx.cwd, filePath);
|
|
287
|
+
const newContent = resolveFileContent(definition, ctx);
|
|
288
|
+
if (!exists(fullPath)) {
|
|
289
|
+
actions.push({ type: "write", path: filePath, content: newContent });
|
|
290
|
+
wouldCreate.push(filePath);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const deprecatedFiles = planDeprecatedFilesRemoval(schema.deprecatedFiles, ctx.cwd);
|
|
294
|
+
actions.push(...deprecatedFiles.actions);
|
|
295
|
+
const wouldRemove = deprecatedFiles.removed;
|
|
296
|
+
const deprecatedDirectories = planExistingDirectoriesRemoval(schema.deprecatedDirs, ctx.cwd);
|
|
297
|
+
actions.push(...deprecatedDirectories.actions);
|
|
298
|
+
wouldRemove.push(...deprecatedDirectories.removed);
|
|
299
|
+
const chmodPathsUpgrade = [
|
|
300
|
+
".safeword/hooks",
|
|
301
|
+
".safeword/hooks/cursor",
|
|
302
|
+
".safeword/lib",
|
|
303
|
+
".safeword/scripts"
|
|
304
|
+
];
|
|
305
|
+
actions.push({ type: "chmod", paths: chmodPathsUpgrade });
|
|
306
|
+
for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
|
|
307
|
+
actions.push({ type: "json-merge", path: filePath, definition });
|
|
308
|
+
}
|
|
309
|
+
actions.push(...planTextPatches(schema.textPatches, ctx.cwd, ctx.isGitRepo));
|
|
310
|
+
const packagesToInstall = computePackagesToInstall(
|
|
311
|
+
schema,
|
|
312
|
+
ctx.projectType,
|
|
313
|
+
ctx.developmentDeps,
|
|
314
|
+
ctx.isGitRepo
|
|
315
|
+
);
|
|
316
|
+
const packagesToRemove = schema.deprecatedPackages.filter((pkg) => pkg in ctx.developmentDeps);
|
|
317
|
+
return {
|
|
318
|
+
actions,
|
|
319
|
+
wouldCreate,
|
|
320
|
+
wouldUpdate,
|
|
321
|
+
wouldRemove,
|
|
322
|
+
packagesToInstall,
|
|
323
|
+
packagesToRemove
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function computeUninstallPlan(schema, ctx, full) {
|
|
327
|
+
const actions = [];
|
|
328
|
+
const wouldRemove = [];
|
|
329
|
+
const ownedFiles = planExistingFilesRemoval(Object.keys(schema.ownedFiles), ctx.cwd);
|
|
330
|
+
actions.push(...ownedFiles.actions);
|
|
331
|
+
wouldRemove.push(...ownedFiles.removed);
|
|
332
|
+
const directoriesToCleanup = /* @__PURE__ */ new Set();
|
|
333
|
+
for (const filePath of ownedFiles.removed) {
|
|
334
|
+
const parentDirectory = getClaudeParentDirectoryForCleanup(filePath);
|
|
335
|
+
if (parentDirectory) directoriesToCleanup.add(parentDirectory);
|
|
336
|
+
}
|
|
337
|
+
const cleanupDirectories = planExistingDirectoriesRemoval([...directoriesToCleanup], ctx.cwd);
|
|
338
|
+
actions.push(...cleanupDirectories.actions);
|
|
339
|
+
wouldRemove.push(...cleanupDirectories.removed);
|
|
340
|
+
for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
|
|
341
|
+
actions.push({ type: "json-unmerge", path: filePath, definition });
|
|
342
|
+
}
|
|
343
|
+
for (const [filePath, definition] of Object.entries(schema.textPatches)) {
|
|
344
|
+
const fullPath = nodePath2.join(ctx.cwd, filePath);
|
|
345
|
+
if (exists(fullPath)) {
|
|
346
|
+
const content = readFileSafe(fullPath) ?? "";
|
|
347
|
+
if (content.includes(definition.marker)) {
|
|
348
|
+
actions.push({ type: "text-unpatch", path: filePath, definition });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const preserved = planExistingDirectoriesRemoval(schema.preservedDirs.toReversed(), ctx.cwd);
|
|
353
|
+
actions.push(...preserved.actions);
|
|
354
|
+
wouldRemove.push(...preserved.removed);
|
|
355
|
+
const owned = planExistingDirectoriesRemoval(schema.ownedDirs.toReversed(), ctx.cwd);
|
|
356
|
+
actions.push(...owned.actions);
|
|
357
|
+
wouldRemove.push(...owned.removed);
|
|
358
|
+
if (full) {
|
|
359
|
+
const managed = planExistingFilesRemoval(Object.keys(schema.managedFiles), ctx.cwd);
|
|
360
|
+
actions.push(...managed.actions);
|
|
361
|
+
wouldRemove.push(...managed.removed);
|
|
362
|
+
}
|
|
363
|
+
const packagesToRemove = full ? computePackagesToRemove(schema, ctx.projectType, ctx.developmentDeps) : [];
|
|
364
|
+
return {
|
|
365
|
+
actions,
|
|
366
|
+
wouldCreate: [],
|
|
367
|
+
wouldUpdate: [],
|
|
368
|
+
wouldRemove,
|
|
369
|
+
packagesToInstall: [],
|
|
370
|
+
packagesToRemove
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function executePlan(plan, ctx) {
|
|
374
|
+
const created = [];
|
|
375
|
+
const updated = [];
|
|
376
|
+
const removed = [];
|
|
377
|
+
const result = { created, updated, removed };
|
|
378
|
+
for (const action of plan.actions) {
|
|
379
|
+
executeAction(action, ctx, result);
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
function executeChmod(cwd, paths) {
|
|
384
|
+
for (const path of paths) {
|
|
385
|
+
const fullPath = nodePath2.join(cwd, path);
|
|
386
|
+
if (exists(fullPath)) makeScriptsExecutable(fullPath);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function executeRmdir(cwd, path, result) {
|
|
390
|
+
if (removeIfEmpty(nodePath2.join(cwd, path))) result.removed.push(path);
|
|
391
|
+
}
|
|
392
|
+
function executeAction(action, ctx, result) {
|
|
393
|
+
switch (action.type) {
|
|
394
|
+
case "mkdir":
|
|
395
|
+
ensureDirectory(nodePath2.join(ctx.cwd, action.path));
|
|
396
|
+
result.created.push(action.path);
|
|
397
|
+
break;
|
|
398
|
+
case "rmdir":
|
|
399
|
+
executeRmdir(ctx.cwd, action.path, result);
|
|
400
|
+
break;
|
|
401
|
+
case "write":
|
|
402
|
+
executeWrite(ctx.cwd, action.path, action.content, result);
|
|
403
|
+
break;
|
|
404
|
+
case "rm":
|
|
405
|
+
remove(nodePath2.join(ctx.cwd, action.path));
|
|
406
|
+
result.removed.push(action.path);
|
|
407
|
+
break;
|
|
408
|
+
case "chmod":
|
|
409
|
+
executeChmod(ctx.cwd, action.paths);
|
|
410
|
+
break;
|
|
411
|
+
case "json-merge":
|
|
412
|
+
executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);
|
|
413
|
+
break;
|
|
414
|
+
case "json-unmerge":
|
|
415
|
+
executeJsonUnmerge(ctx.cwd, action.path, action.definition);
|
|
416
|
+
break;
|
|
417
|
+
case "text-patch":
|
|
418
|
+
executeTextPatch(ctx.cwd, action.path, action.definition);
|
|
419
|
+
break;
|
|
420
|
+
case "text-unpatch":
|
|
421
|
+
executeTextUnpatch(ctx.cwd, action.path, action.definition);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function executeWrite(cwd, path, content, result) {
|
|
426
|
+
const fullPath = nodePath2.join(cwd, path);
|
|
427
|
+
const existed = exists(fullPath);
|
|
428
|
+
writeFile(fullPath, content);
|
|
429
|
+
(existed ? result.updated : result.created).push(path);
|
|
430
|
+
}
|
|
431
|
+
function resolveFileContent(definition, ctx) {
|
|
432
|
+
if (definition.template) {
|
|
433
|
+
const templatesDirectory = getTemplatesDirectory();
|
|
434
|
+
return readFile(nodePath2.join(templatesDirectory, definition.template));
|
|
435
|
+
}
|
|
436
|
+
if (definition.content) {
|
|
437
|
+
return typeof definition.content === "function" ? definition.content() : definition.content;
|
|
438
|
+
}
|
|
439
|
+
if (definition.generator) {
|
|
440
|
+
return definition.generator(ctx);
|
|
441
|
+
}
|
|
442
|
+
throw new Error("FileDefinition must have template, content, or generator");
|
|
443
|
+
}
|
|
444
|
+
function fileNeedsUpdate(installedPath, newContent) {
|
|
445
|
+
if (!exists(installedPath)) return true;
|
|
446
|
+
const currentContent = readFileSafe(installedPath);
|
|
447
|
+
return currentContent?.trim() !== newContent.trim();
|
|
448
|
+
}
|
|
449
|
+
var GIT_ONLY_PACKAGES = /* @__PURE__ */ new Set(["husky", "lint-staged"]);
|
|
450
|
+
function computePackagesToInstall(schema, projectType, installedDevelopmentDeps, isGitRepo2 = true) {
|
|
451
|
+
let needed = [...schema.packages.base];
|
|
452
|
+
if (!isGitRepo2) {
|
|
453
|
+
needed = needed.filter((pkg) => !GIT_ONLY_PACKAGES.has(pkg));
|
|
454
|
+
}
|
|
455
|
+
for (const [key, deps] of Object.entries(schema.packages.conditional)) {
|
|
456
|
+
if (projectType[key]) {
|
|
457
|
+
needed.push(...deps);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return needed.filter((pkg) => !(pkg in installedDevelopmentDeps));
|
|
461
|
+
}
|
|
462
|
+
function computePackagesToRemove(schema, projectType, installedDevelopmentDeps) {
|
|
463
|
+
const safewordPackages = [...schema.packages.base];
|
|
464
|
+
for (const [key, deps] of Object.entries(schema.packages.conditional)) {
|
|
465
|
+
if (projectType[key]) {
|
|
466
|
+
safewordPackages.push(...deps);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return safewordPackages.filter((pkg) => pkg in installedDevelopmentDeps);
|
|
470
|
+
}
|
|
471
|
+
function executeJsonMerge(cwd, path, definition, ctx) {
|
|
472
|
+
const fullPath = nodePath2.join(cwd, path);
|
|
473
|
+
const existing = readJson(fullPath) ?? {};
|
|
474
|
+
const merged = definition.merge(existing, ctx);
|
|
475
|
+
if (JSON.stringify(existing) === JSON.stringify(merged)) return;
|
|
476
|
+
writeJson(fullPath, merged);
|
|
477
|
+
}
|
|
478
|
+
function executeJsonUnmerge(cwd, path, definition) {
|
|
479
|
+
const fullPath = nodePath2.join(cwd, path);
|
|
480
|
+
if (!exists(fullPath)) return;
|
|
481
|
+
const existing = readJson(fullPath);
|
|
482
|
+
if (!existing) return;
|
|
483
|
+
const unmerged = definition.unmerge(existing);
|
|
484
|
+
if (definition.removeFileIfEmpty) {
|
|
485
|
+
const remainingKeys = Object.keys(unmerged).filter((k) => unmerged[k] !== void 0);
|
|
486
|
+
if (remainingKeys.length === 0) {
|
|
487
|
+
remove(fullPath);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
writeJson(fullPath, unmerged);
|
|
492
|
+
}
|
|
493
|
+
function executeTextPatch(cwd, path, definition) {
|
|
494
|
+
const fullPath = nodePath2.join(cwd, path);
|
|
495
|
+
let content = readFileSafe(fullPath) ?? "";
|
|
496
|
+
if (content.includes(definition.marker)) return;
|
|
497
|
+
content = definition.operation === "prepend" ? definition.content + content : content + definition.content;
|
|
498
|
+
writeFile(fullPath, content);
|
|
499
|
+
}
|
|
500
|
+
function executeTextUnpatch(cwd, path, definition) {
|
|
501
|
+
const fullPath = nodePath2.join(cwd, path);
|
|
502
|
+
const content = readFileSafe(fullPath);
|
|
503
|
+
if (!content) return;
|
|
504
|
+
let unpatched = content.replace(definition.content, "");
|
|
505
|
+
if (unpatched === content && content.includes(definition.marker)) {
|
|
506
|
+
const lines = content.split("\n");
|
|
507
|
+
const filtered = lines.filter((line) => !line.includes(definition.marker));
|
|
508
|
+
unpatched = filtered.join("\n").replace(/^\n+/, "");
|
|
509
|
+
}
|
|
510
|
+
writeFile(fullPath, unpatched);
|
|
511
|
+
}
|
|
512
|
+
|
|
91
513
|
// src/templates/config.ts
|
|
92
514
|
function getEslintConfig() {
|
|
93
|
-
return
|
|
94
|
-
import { readFileSync } from "fs";
|
|
515
|
+
return `import { readFileSync } from "fs";
|
|
95
516
|
import { dirname, join } from "path";
|
|
96
517
|
import { fileURLToPath } from "url";
|
|
97
518
|
import safeword from "eslint-plugin-safeword";
|
|
@@ -102,27 +523,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
102
523
|
const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
|
103
524
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
104
525
|
|
|
105
|
-
// Helper for dynamic imports with actionable error messages
|
|
106
|
-
async function tryImport(pkgName, frameworkName) {
|
|
107
|
-
try {
|
|
108
|
-
return await import(pkgName);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
if (err.code === "ERR_MODULE_NOT_FOUND") {
|
|
111
|
-
console.error(\`\\n\u2717 Missing ESLint plugin for \${frameworkName}\\n\`);
|
|
112
|
-
console.error(\`Your package.json has \${frameworkName} but the ESLint plugin is not installed.\`);
|
|
113
|
-
console.error(\`Run: npm install -D \${pkgName}\\n\`);
|
|
114
|
-
console.error(\`Or run: npx safeword sync\\n\`);
|
|
115
|
-
}
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
526
|
// Build dynamic ignores based on detected frameworks
|
|
121
527
|
const ignores = ["**/node_modules/", "**/dist/", "**/build/", "**/coverage/"];
|
|
122
528
|
if (deps["next"]) ignores.push(".next/");
|
|
123
529
|
if (deps["astro"]) ignores.push(".astro/");
|
|
124
|
-
if (deps["vue"] || deps["nuxt"]) ignores.push(".nuxt/");
|
|
125
|
-
if (deps["svelte"] || deps["@sveltejs/kit"]) ignores.push(".svelte-kit/");
|
|
126
530
|
|
|
127
531
|
// Select appropriate safeword config based on detected framework
|
|
128
532
|
// Order matters: most specific first
|
|
@@ -153,22 +557,6 @@ if (deps["playwright"] || deps["@playwright/test"]) {
|
|
|
153
557
|
configs.push(...safeword.configs.playwright);
|
|
154
558
|
}
|
|
155
559
|
|
|
156
|
-
// Frameworks NOT in eslint-plugin-safeword (dynamic imports)
|
|
157
|
-
if (deps["vue"] || deps["nuxt"]) {
|
|
158
|
-
const vue = await tryImport("eslint-plugin-vue", "Vue");
|
|
159
|
-
configs.push(...vue.default.configs["flat/recommended"]);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (deps["svelte"] || deps["@sveltejs/kit"]) {
|
|
163
|
-
const svelte = await tryImport("eslint-plugin-svelte", "Svelte");
|
|
164
|
-
configs.push(...svelte.default.configs.recommended);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (deps["electron"]) {
|
|
168
|
-
const electron = await tryImport("@electron-toolkit/eslint-config", "Electron");
|
|
169
|
-
configs.push(electron.default);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
560
|
// eslint-config-prettier must be last to disable conflicting rules
|
|
173
561
|
configs.push(eslintConfigPrettier);
|
|
174
562
|
|
|
@@ -268,7 +656,6 @@ function getPrettierConfig(projectType) {
|
|
|
268
656
|
};
|
|
269
657
|
const plugins = [];
|
|
270
658
|
if (projectType.astro) plugins.push("prettier-plugin-astro");
|
|
271
|
-
if (projectType.svelte) plugins.push("prettier-plugin-svelte");
|
|
272
659
|
if (projectType.shell) plugins.push("prettier-plugin-sh");
|
|
273
660
|
if (projectType.tailwind) plugins.push("prettier-plugin-tailwindcss");
|
|
274
661
|
if (plugins.length > 0) {
|
|
@@ -303,16 +690,6 @@ var MCP_SERVERS = {
|
|
|
303
690
|
};
|
|
304
691
|
|
|
305
692
|
// src/schema.ts
|
|
306
|
-
function isEslintPackage(pkg) {
|
|
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";
|
|
308
|
-
}
|
|
309
|
-
function getBaseEslintPackages() {
|
|
310
|
-
return SAFEWORD_SCHEMA.packages.base.filter((pkg) => isEslintPackage(pkg));
|
|
311
|
-
}
|
|
312
|
-
function getConditionalEslintPackages(key) {
|
|
313
|
-
const deps = SAFEWORD_SCHEMA.packages.conditional[key];
|
|
314
|
-
return deps ? deps.filter((pkg) => isEslintPackage(pkg)) : [];
|
|
315
|
-
}
|
|
316
693
|
var SAFEWORD_SCHEMA = {
|
|
317
694
|
version: VERSION,
|
|
318
695
|
// Directories fully owned by safeword (created on setup, deleted on reset)
|
|
@@ -713,18 +1090,13 @@ var SAFEWORD_SCHEMA = {
|
|
|
713
1090
|
// Core tools
|
|
714
1091
|
"eslint",
|
|
715
1092
|
"prettier",
|
|
716
|
-
|
|
717
|
-
// Safeword plugin (bundles all ESLint plugins)
|
|
1093
|
+
// Safeword plugin (bundles eslint-config-prettier + all ESLint plugins)
|
|
718
1094
|
"eslint-plugin-safeword",
|
|
719
1095
|
// Non-ESLint tools
|
|
720
1096
|
"markdownlint-cli2",
|
|
721
1097
|
"knip"
|
|
722
1098
|
],
|
|
723
1099
|
conditional: {
|
|
724
|
-
// Frameworks NOT in eslint-plugin-safeword
|
|
725
|
-
vue: ["eslint-plugin-vue"],
|
|
726
|
-
svelte: ["eslint-plugin-svelte", "prettier-plugin-svelte"],
|
|
727
|
-
electron: ["@electron-toolkit/eslint-config"],
|
|
728
1100
|
// Prettier plugins
|
|
729
1101
|
astro: ["prettier-plugin-astro"],
|
|
730
1102
|
tailwind: ["prettier-plugin-tailwindcss"],
|
|
@@ -735,9 +1107,18 @@ var SAFEWORD_SCHEMA = {
|
|
|
735
1107
|
}
|
|
736
1108
|
};
|
|
737
1109
|
|
|
1110
|
+
// src/utils/git.ts
|
|
1111
|
+
import nodePath3 from "path";
|
|
1112
|
+
function isGitRepo(cwd) {
|
|
1113
|
+
return exists(nodePath3.join(cwd, ".git"));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/utils/context.ts
|
|
1117
|
+
import nodePath5 from "path";
|
|
1118
|
+
|
|
738
1119
|
// src/utils/project-detector.ts
|
|
739
1120
|
import { readdirSync as readdirSync2 } from "fs";
|
|
740
|
-
import
|
|
1121
|
+
import nodePath4 from "path";
|
|
741
1122
|
function hasShellScripts(cwd, maxDepth = 4) {
|
|
742
1123
|
const excludeDirectories = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
|
|
743
1124
|
function scan(dir, depth) {
|
|
@@ -748,7 +1129,7 @@ function hasShellScripts(cwd, maxDepth = 4) {
|
|
|
748
1129
|
if (entry.isFile() && entry.name.endsWith(".sh")) {
|
|
749
1130
|
return true;
|
|
750
1131
|
}
|
|
751
|
-
if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(
|
|
1132
|
+
if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(nodePath4.join(dir, entry.name), depth + 1)) {
|
|
752
1133
|
return true;
|
|
753
1134
|
}
|
|
754
1135
|
}
|
|
@@ -766,11 +1147,6 @@ function detectProjectType(packageJson, cwd) {
|
|
|
766
1147
|
const hasReact = "react" in deps || "react" in developmentDeps;
|
|
767
1148
|
const hasNextJs = "next" in deps;
|
|
768
1149
|
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
1150
|
const hasVitest = "vitest" in developmentDeps;
|
|
775
1151
|
const hasPlaywright = "@playwright/test" in developmentDeps;
|
|
776
1152
|
const hasTailwind = "tailwindcss" in allDeps;
|
|
@@ -783,13 +1159,6 @@ function detectProjectType(packageJson, cwd) {
|
|
|
783
1159
|
// Next.js implies React
|
|
784
1160
|
nextjs: hasNextJs,
|
|
785
1161
|
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
1162
|
vitest: hasVitest,
|
|
794
1163
|
playwright: hasPlaywright,
|
|
795
1164
|
tailwind: hasTailwind,
|
|
@@ -798,21 +1167,56 @@ function detectProjectType(packageJson, cwd) {
|
|
|
798
1167
|
};
|
|
799
1168
|
}
|
|
800
1169
|
|
|
1170
|
+
// src/utils/context.ts
|
|
1171
|
+
function createProjectContext(cwd) {
|
|
1172
|
+
const packageJson = readJson(nodePath5.join(cwd, "package.json"));
|
|
1173
|
+
return {
|
|
1174
|
+
cwd,
|
|
1175
|
+
projectType: detectProjectType(packageJson ?? {}, cwd),
|
|
1176
|
+
developmentDeps: packageJson?.devDependencies ?? {},
|
|
1177
|
+
isGitRepo: isGitRepo(cwd)
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/utils/output.ts
|
|
1182
|
+
function info(message) {
|
|
1183
|
+
console.log(message);
|
|
1184
|
+
}
|
|
1185
|
+
function success(message) {
|
|
1186
|
+
console.log(`\u2713 ${message}`);
|
|
1187
|
+
}
|
|
1188
|
+
function warn(message) {
|
|
1189
|
+
console.warn(`\u26A0 ${message}`);
|
|
1190
|
+
}
|
|
1191
|
+
function error(message) {
|
|
1192
|
+
console.error(`\u2717 ${message}`);
|
|
1193
|
+
}
|
|
1194
|
+
function header(title) {
|
|
1195
|
+
console.log(`
|
|
1196
|
+
${title}`);
|
|
1197
|
+
console.log("\u2500".repeat(title.length));
|
|
1198
|
+
}
|
|
1199
|
+
function listItem(item, indent = 2) {
|
|
1200
|
+
console.log(`${" ".repeat(indent)}\u2022 ${item}`);
|
|
1201
|
+
}
|
|
1202
|
+
function keyValue(key, value) {
|
|
1203
|
+
console.log(` ${key}: ${value}`);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
801
1206
|
export {
|
|
802
|
-
getTemplatesDirectory,
|
|
803
1207
|
exists,
|
|
804
|
-
ensureDirectory,
|
|
805
|
-
readFile,
|
|
806
1208
|
readFileSafe,
|
|
807
|
-
writeFile,
|
|
808
|
-
remove,
|
|
809
|
-
removeIfEmpty,
|
|
810
|
-
makeScriptsExecutable,
|
|
811
|
-
readJson,
|
|
812
1209
|
writeJson,
|
|
813
|
-
|
|
814
|
-
getConditionalEslintPackages,
|
|
1210
|
+
reconcile,
|
|
815
1211
|
SAFEWORD_SCHEMA,
|
|
816
|
-
|
|
1212
|
+
isGitRepo,
|
|
1213
|
+
createProjectContext,
|
|
1214
|
+
info,
|
|
1215
|
+
success,
|
|
1216
|
+
warn,
|
|
1217
|
+
error,
|
|
1218
|
+
header,
|
|
1219
|
+
listItem,
|
|
1220
|
+
keyValue
|
|
817
1221
|
};
|
|
818
|
-
//# sourceMappingURL=chunk-
|
|
1222
|
+
//# sourceMappingURL=chunk-UUKED7KU.js.map
|