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.
@@ -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 `/* eslint-disable import-x/no-unresolved, no-undef -- dynamic imports for optional framework plugins */
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
- "eslint-config-prettier",
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 nodePath2 from "path";
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(nodePath2.join(dir, entry.name), depth + 1)) {
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
- getBaseEslintPackages,
814
- getConditionalEslintPackages,
1210
+ reconcile,
815
1211
  SAFEWORD_SCHEMA,
816
- detectProjectType
1212
+ isGitRepo,
1213
+ createProjectContext,
1214
+ info,
1215
+ success,
1216
+ warn,
1217
+ error,
1218
+ header,
1219
+ listItem,
1220
+ keyValue
817
1221
  };
818
- //# sourceMappingURL=chunk-CLSGXTOL.js.map
1222
+ //# sourceMappingURL=chunk-UUKED7KU.js.map