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
|
@@ -4,18 +4,20 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
SAFEWORD_SCHEMA,
|
|
6
6
|
createProjectContext,
|
|
7
|
+
reconcile
|
|
8
|
+
} from "./chunk-ZE6QJHZD.js";
|
|
9
|
+
import {
|
|
10
|
+
VERSION
|
|
11
|
+
} from "./chunk-ORQHKDT2.js";
|
|
12
|
+
import {
|
|
7
13
|
exists,
|
|
8
14
|
header,
|
|
9
15
|
info,
|
|
10
16
|
keyValue,
|
|
11
17
|
readFileSafe,
|
|
12
|
-
reconcile,
|
|
13
18
|
success,
|
|
14
19
|
warn
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import {
|
|
17
|
-
VERSION
|
|
18
|
-
} from "./chunk-ORQHKDT2.js";
|
|
20
|
+
} from "./chunk-DYLHQBW3.js";
|
|
19
21
|
|
|
20
22
|
// src/commands/check.ts
|
|
21
23
|
import nodePath from "path";
|
|
@@ -78,9 +80,12 @@ async function checkHealth(cwd) {
|
|
|
78
80
|
const projectVersion = readFileSafe(versionPath)?.trim() ?? void 0;
|
|
79
81
|
const ctx = createProjectContext(cwd);
|
|
80
82
|
const result = await reconcile(SAFEWORD_SCHEMA, "upgrade", ctx, { dryRun: true });
|
|
83
|
+
const actionsWithPath = result.actions.filter(
|
|
84
|
+
(a) => a.type !== "chmod" && a.type !== "json-merge" && a.type !== "json-unmerge"
|
|
85
|
+
);
|
|
81
86
|
const issues = [
|
|
82
|
-
...findMissingFiles(cwd,
|
|
83
|
-
...findMissingPatches(cwd,
|
|
87
|
+
...findMissingFiles(cwd, actionsWithPath),
|
|
88
|
+
...findMissingPatches(cwd, actionsWithPath)
|
|
84
89
|
];
|
|
85
90
|
if (!exists(nodePath.join(cwd, ".claude", "settings.json"))) {
|
|
86
91
|
issues.push("Missing: .claude/settings.json");
|
|
@@ -162,4 +167,4 @@ async function check(options) {
|
|
|
162
167
|
export {
|
|
163
168
|
check
|
|
164
169
|
};
|
|
165
|
-
//# sourceMappingURL=check-
|
|
170
|
+
//# sourceMappingURL=check-6SBEN4FB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["/**\n * Check command - Verify project health and configuration\n *\n * Uses reconcile() with dryRun to detect missing files and configuration issues.\n */\n\nimport nodePath from 'node:path';\n\nimport { reconcile } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { header, info, keyValue, success, warn } from '../utils/output.js';\nimport { isNewerVersion } from '../utils/version.js';\nimport { VERSION } from '../version.js';\n\nexport interface CheckOptions {\n offline?: boolean;\n}\n\n/**\n * Check for missing files from write actions\n * @param cwd\n * @param actions\n */\nfunction findMissingFiles(cwd: string, actions: { type: string; path: string }[]): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type === 'write' && !exists(nodePath.join(cwd, action.path))) {\n issues.push(`Missing: ${action.path}`);\n }\n }\n return issues;\n}\n\n/**\n * Check for missing text patch markers\n * @param cwd\n * @param actions\n */\nfunction findMissingPatches(\n cwd: string,\n actions: { type: string; path: string; definition?: { marker: string } }[],\n): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type !== 'text-patch') continue;\n\n const fullPath = nodePath.join(cwd, action.path);\n if (exists(fullPath)) {\n const content = readFileSafe(fullPath) ?? '';\n if (action.definition && !content.includes(action.definition.marker)) {\n issues.push(`${action.path} missing safeword link`);\n }\n } else {\n issues.push(`${action.path} file missing`);\n }\n }\n return issues;\n}\n\ninterface HealthStatus {\n configured: boolean;\n projectVersion: string | undefined;\n cliVersion: string;\n updateAvailable: boolean;\n latestVersion: string | undefined;\n issues: string[];\n missingPackages: string[];\n}\n\n/**\n * Check for latest version from npm (with timeout)\n * @param timeout\n */\nasync function checkLatestVersion(timeout = 3000): Promise<string | undefined> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: string };\n return data.version ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check project configuration health using reconcile dryRun\n * @param cwd\n */\nasync function checkHealth(cwd: string): Promise<HealthStatus> {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDirectory)) {\n return {\n configured: false,\n projectVersion: undefined,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues: [],\n missingPackages: [],\n };\n }\n\n // Read project version\n const versionPath = nodePath.join(safewordDirectory, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? undefined;\n\n // Use reconcile with dryRun to detect issues\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'upgrade', ctx, { dryRun: true });\n\n // Collect issues from write actions and text patches\n // Filter out chmod (paths[] instead of path) and json-merge/unmerge (incompatible definition)\n const actionsWithPath = result.actions.filter(\n (\n a,\n ): a is Exclude<\n (typeof result.actions)[number],\n { type: 'chmod' } | { type: 'json-merge' } | { type: 'json-unmerge' }\n > => a.type !== 'chmod' && a.type !== 'json-merge' && a.type !== 'json-unmerge',\n );\n const issues: string[] = [\n ...findMissingFiles(cwd, actionsWithPath),\n ...findMissingPatches(cwd, actionsWithPath),\n ];\n\n // Check for missing .claude/settings.json\n if (!exists(nodePath.join(cwd, '.claude', 'settings.json'))) {\n issues.push('Missing: .claude/settings.json');\n }\n\n return {\n configured: true,\n projectVersion,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues,\n missingPackages: result.packagesToInstall,\n };\n}\n\n/**\n * Check for CLI updates and report status\n * @param health\n */\nasync function reportUpdateStatus(health: HealthStatus): Promise<void> {\n info('\\nChecking for updates...');\n const latestVersion = await checkLatestVersion();\n\n if (!latestVersion) {\n warn(\"Couldn't check for updates (offline?)\");\n return;\n }\n\n health.latestVersion = latestVersion;\n health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);\n\n if (health.updateAvailable) {\n warn(`Update available: v${latestVersion}`);\n info('Run `npm install -g safeword` to upgrade');\n } else {\n success('CLI is up to date');\n }\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus): void {\n if (!health.projectVersion) return;\n\n if (isNewerVersion(health.cliVersion, health.projectVersion)) {\n warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);\n info('Consider upgrading the CLI');\n } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {\n info(`\\nUpgrade available for project config`);\n info(\n `Run \\`safeword upgrade\\` to update from v${health.projectVersion} to v${health.cliVersion}`,\n );\n }\n}\n\n/**\n * Report issues or success\n * @param health\n */\nfunction reportHealthSummary(health: HealthStatus): void {\n if (health.issues.length > 0) {\n header('Issues Found');\n for (const issue of health.issues) {\n warn(issue);\n }\n info('\\nRun `safeword upgrade` to repair configuration');\n return;\n }\n\n if (health.missingPackages.length > 0) {\n header('Missing Packages');\n info(`${health.missingPackages.length} linting packages not installed`);\n info('Run `safeword upgrade` to install missing packages');\n return;\n }\n\n success('\\nConfiguration is healthy');\n}\n\n/**\n *\n * @param options\n */\nexport async function check(options: CheckOptions): Promise<void> {\n const cwd = process.cwd();\n\n header('Safeword Health Check');\n\n const health = await checkHealth(cwd);\n\n // Not configured\n if (!health.configured) {\n info('Not configured. Run `safeword setup` to initialize.');\n return;\n }\n\n // Show versions\n keyValue('Safeword CLI', `v${health.cliVersion}`);\n keyValue('Project config', health.projectVersion ? `v${health.projectVersion}` : 'unknown');\n\n // Check for updates (unless offline)\n if (options.offline) {\n info('\\nSkipped update check (offline mode)');\n } else {\n await reportUpdateStatus(health);\n }\n\n reportVersionMismatch(health);\n reportHealthSummary(health);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAMA,OAAO,cAAc;AAmBrB,SAAS,iBAAiB,KAAa,SAAqD;AAC1F,QAAM,SAAmB,CAAC;AAC1B,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,SAAS,WAAW,CAAC,OAAO,SAAS,KAAK,KAAK,OAAO,IAAI,CAAC,GAAG;AACvE,aAAO,KAAK,YAAY,OAAO,IAAI,EAAE;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAOA,SAAS,mBACP,KACA,SACU;AACV,QAAM,SAAmB,CAAC;AAC1B,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,SAAS,aAAc;AAElC,UAAM,WAAW,SAAS,KAAK,KAAK,OAAO,IAAI;AAC/C,QAAI,OAAO,QAAQ,GAAG;AACpB,YAAM,UAAU,aAAa,QAAQ,KAAK;AAC1C,UAAI,OAAO,cAAc,CAAC,QAAQ,SAAS,OAAO,WAAW,MAAM,GAAG;AACpE,eAAO,KAAK,GAAG,OAAO,IAAI,wBAAwB;AAAA,MACpD;AAAA,IACF,OAAO;AACL,aAAO,KAAK,GAAG,OAAO,IAAI,eAAe;AAAA,IAC3C;AAAA,EACF;AACA,SAAO;AACT;AAgBA,eAAe,mBAAmB,UAAU,KAAmC;AAC7E,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,WAAW,MAAM;AACjC,iBAAW,MAAM;AAAA,IACnB,GAAG,OAAO;AAEV,UAAM,WAAW,MAAM,MAAM,8CAA8C;AAAA,MACzE,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,SAAS;AAEtB,QAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,KAAK,WAAW;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAe,YAAY,KAAoC;AAC7D,QAAM,oBAAoB,SAAS,KAAK,KAAK,WAAW;AAGxD,MAAI,CAAC,OAAO,iBAAiB,GAAG;AAC9B,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,YAAY;AAAA,MACZ,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf,QAAQ,CAAC;AAAA,MACT,iBAAiB,CAAC;AAAA,IACpB;AAAA,EACF;AAGA,QAAM,cAAc,SAAS,KAAK,mBAAmB,SAAS;AAC9D,QAAM,iBAAiB,aAAa,WAAW,GAAG,KAAK,KAAK;AAG5D,QAAM,MAAM,qBAAqB,GAAG;AACpC,QAAM,SAAS,MAAM,UAAU,iBAAiB,WAAW,KAAK,EAAE,QAAQ,KAAK,CAAC;AAIhF,QAAM,kBAAkB,OAAO,QAAQ;AAAA,IACrC,CACE,MAIG,EAAE,SAAS,WAAW,EAAE,SAAS,gBAAgB,EAAE,SAAS;AAAA,EACnE;AACA,QAAM,SAAmB;AAAA,IACvB,GAAG,iBAAiB,KAAK,eAAe;AAAA,IACxC,GAAG,mBAAmB,KAAK,eAAe;AAAA,EAC5C;AAGA,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,WAAW,eAAe,CAAC,GAAG;AAC3D,WAAO,KAAK,gCAAgC;AAAA,EAC9C;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,IACZ;AAAA,IACA,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf;AAAA,IACA,iBAAiB,OAAO;AAAA,EAC1B;AACF;AAMA,eAAe,mBAAmB,QAAqC;AACrE,OAAK,2BAA2B;AAChC,QAAM,gBAAgB,MAAM,mBAAmB;AAE/C,MAAI,CAAC,eAAe;AAClB,SAAK,uCAAuC;AAC5C;AAAA,EACF;AAEA,SAAO,gBAAgB;AACvB,SAAO,kBAAkB,eAAe,OAAO,YAAY,aAAa;AAExE,MAAI,OAAO,iBAAiB;AAC1B,SAAK,sBAAsB,aAAa,EAAE;AAC1C,SAAK,0CAA0C;AAAA,EACjD,OAAO;AACL,YAAQ,mBAAmB;AAAA,EAC7B;AACF;AAMA,SAAS,sBAAsB,QAA4B;AACzD,MAAI,CAAC,OAAO,eAAgB;AAE5B,MAAI,eAAe,OAAO,YAAY,OAAO,cAAc,GAAG;AAC5D,SAAK,oBAAoB,OAAO,cAAc,yBAAyB,OAAO,UAAU,GAAG;AAC3F,SAAK,4BAA4B;AAAA,EACnC,WAAW,eAAe,OAAO,gBAAgB,OAAO,UAAU,GAAG;AACnE,SAAK;AAAA,qCAAwC;AAC7C;AAAA,MACE,4CAA4C,OAAO,cAAc,QAAQ,OAAO,UAAU;AAAA,IAC5F;AAAA,EACF;AACF;AAMA,SAAS,oBAAoB,QAA4B;AACvD,MAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,WAAO,cAAc;AACrB,eAAW,SAAS,OAAO,QAAQ;AACjC,WAAK,KAAK;AAAA,IACZ;AACA,SAAK,kDAAkD;AACvD;AAAA,EACF;AAEA,MAAI,OAAO,gBAAgB,SAAS,GAAG;AACrC,WAAO,kBAAkB;AACzB,SAAK,GAAG,OAAO,gBAAgB,MAAM,iCAAiC;AACtE,SAAK,oDAAoD;AACzD;AAAA,EACF;AAEA,UAAQ,4BAA4B;AACtC;AAMA,eAAsB,MAAM,SAAsC;AAChE,QAAM,MAAM,QAAQ,IAAI;AAExB,SAAO,uBAAuB;AAE9B,QAAM,SAAS,MAAM,YAAY,GAAG;AAGpC,MAAI,CAAC,OAAO,YAAY;AACtB,SAAK,qDAAqD;AAC1D;AAAA,EACF;AAGA,WAAS,gBAAgB,IAAI,OAAO,UAAU,EAAE;AAChD,WAAS,kBAAkB,OAAO,iBAAiB,IAAI,OAAO,cAAc,KAAK,SAAS;AAG1F,MAAI,QAAQ,SAAS;AACnB,SAAK,uCAAuC;AAAA,EAC9C,OAAO;AACL,UAAM,mBAAmB,MAAM;AAAA,EACjC;AAEA,wBAAsB,MAAM;AAC5B,sBAAoB,MAAM;AAC5B;","names":[]}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
error,
|
|
3
|
+
exists,
|
|
4
|
+
info,
|
|
5
|
+
readJson,
|
|
6
|
+
success
|
|
7
|
+
} from "./chunk-DYLHQBW3.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/sync-config.ts
|
|
10
|
+
import { writeFileSync } from "fs";
|
|
11
|
+
import nodePath3 from "path";
|
|
12
|
+
|
|
13
|
+
// src/utils/boundaries.ts
|
|
14
|
+
import { readdirSync } from "fs";
|
|
15
|
+
import nodePath from "path";
|
|
16
|
+
var ARCHITECTURE_LAYERS = [
|
|
17
|
+
// Layer 0: Pure types (no imports)
|
|
18
|
+
{ layer: "types", dirs: ["types", "interfaces", "schemas"] },
|
|
19
|
+
// Layer 1: Utilities (only types)
|
|
20
|
+
{ layer: "utils", dirs: ["utils", "helpers", "shared", "common", "core"] },
|
|
21
|
+
// Layer 2: Libraries (types, utils)
|
|
22
|
+
{ layer: "lib", dirs: ["lib", "libraries"] },
|
|
23
|
+
// Layer 3: State & logic (types, utils, lib)
|
|
24
|
+
{ layer: "hooks", dirs: ["hooks", "composables"] },
|
|
25
|
+
{ layer: "services", dirs: ["services", "api", "stores", "state"] },
|
|
26
|
+
// Layer 4: UI components (all above)
|
|
27
|
+
{ layer: "components", dirs: ["components", "ui"] },
|
|
28
|
+
// Layer 5: Features (all above)
|
|
29
|
+
{ layer: "features", dirs: ["features", "modules", "domains"] },
|
|
30
|
+
// Layer 6: Entry points (can import everything)
|
|
31
|
+
{ layer: "app", dirs: ["app", "pages", "views", "routes", "commands"] }
|
|
32
|
+
];
|
|
33
|
+
function findMonorepoPackages(projectDirectory) {
|
|
34
|
+
const packages = [];
|
|
35
|
+
const monorepoRoots = ["packages", "apps", "libs", "modules"];
|
|
36
|
+
for (const root of monorepoRoots) {
|
|
37
|
+
const rootPath = nodePath.join(projectDirectory, root);
|
|
38
|
+
if (!exists(rootPath)) continue;
|
|
39
|
+
try {
|
|
40
|
+
const entries = readdirSync(rootPath, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
43
|
+
packages.push(nodePath.join(root, entry.name));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return packages;
|
|
50
|
+
}
|
|
51
|
+
function hasLayerForPrefix(elements, layer, pathPrefix) {
|
|
52
|
+
return elements.some(
|
|
53
|
+
(element) => element.layer === layer && element.pattern.startsWith(pathPrefix)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function scanSearchPath(projectDirectory, searchPath, pathPrefix, elements) {
|
|
57
|
+
for (const layerDefinition of ARCHITECTURE_LAYERS) {
|
|
58
|
+
for (const dirName of layerDefinition.dirs) {
|
|
59
|
+
const fullPath = nodePath.join(projectDirectory, searchPath, dirName);
|
|
60
|
+
if (exists(fullPath) && !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)) {
|
|
61
|
+
elements.push({
|
|
62
|
+
layer: layerDefinition.layer,
|
|
63
|
+
pattern: `${pathPrefix}${dirName}/**`,
|
|
64
|
+
location: `${pathPrefix}${dirName}`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function scanForLayers(projectDirectory, basePath) {
|
|
71
|
+
const elements = [];
|
|
72
|
+
const prefix = basePath ? `${basePath}/` : "";
|
|
73
|
+
scanSearchPath(projectDirectory, nodePath.join(basePath, "src"), `${prefix}src/`, elements);
|
|
74
|
+
scanSearchPath(projectDirectory, basePath, prefix, elements);
|
|
75
|
+
return elements;
|
|
76
|
+
}
|
|
77
|
+
function detectArchitecture(projectDirectory) {
|
|
78
|
+
const elements = [];
|
|
79
|
+
const packages = findMonorepoPackages(projectDirectory);
|
|
80
|
+
const isMonorepo = packages.length > 0;
|
|
81
|
+
if (isMonorepo) {
|
|
82
|
+
for (const pkg of packages) {
|
|
83
|
+
elements.push(...scanForLayers(projectDirectory, pkg));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
elements.push(...scanForLayers(projectDirectory, ""));
|
|
87
|
+
const seen = /* @__PURE__ */ new Set();
|
|
88
|
+
const uniqueElements = elements.filter((element) => {
|
|
89
|
+
if (seen.has(element.pattern)) return false;
|
|
90
|
+
seen.add(element.pattern);
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
return { elements: uniqueElements, isMonorepo };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/utils/depcruise-config.ts
|
|
97
|
+
import nodePath2 from "path";
|
|
98
|
+
function detectWorkspaces(cwd) {
|
|
99
|
+
const packageJsonPath = nodePath2.join(cwd, "package.json");
|
|
100
|
+
const packageJson = readJson(packageJsonPath);
|
|
101
|
+
if (!packageJson?.workspaces) return void 0;
|
|
102
|
+
const workspaces = Array.isArray(packageJson.workspaces) ? packageJson.workspaces : packageJson.workspaces.packages;
|
|
103
|
+
return workspaces && workspaces.length > 0 ? workspaces : void 0;
|
|
104
|
+
}
|
|
105
|
+
function generateMonorepoRules(workspaces) {
|
|
106
|
+
const rules = [];
|
|
107
|
+
const hasLibs = workspaces.some((w) => w.startsWith("libs"));
|
|
108
|
+
const hasPackages = workspaces.some((w) => w.startsWith("packages"));
|
|
109
|
+
const hasApps = workspaces.some((w) => w.startsWith("apps"));
|
|
110
|
+
if (hasLibs && (hasPackages || hasApps)) {
|
|
111
|
+
rules.push(` {
|
|
112
|
+
name: 'libs-cannot-import-packages-or-apps',
|
|
113
|
+
severity: 'error',
|
|
114
|
+
from: { path: '^libs/' },
|
|
115
|
+
to: { path: '^(packages|apps)/' },
|
|
116
|
+
}`);
|
|
117
|
+
}
|
|
118
|
+
if (hasPackages && hasApps) {
|
|
119
|
+
rules.push(` {
|
|
120
|
+
name: 'packages-cannot-import-apps',
|
|
121
|
+
severity: 'error',
|
|
122
|
+
from: { path: '^packages/' },
|
|
123
|
+
to: { path: '^apps/' },
|
|
124
|
+
}`);
|
|
125
|
+
}
|
|
126
|
+
return rules.join(",\n");
|
|
127
|
+
}
|
|
128
|
+
function generateDepCruiseConfigFile(arch) {
|
|
129
|
+
const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : "";
|
|
130
|
+
const hasMonorepoRules = monorepoRules.length > 0;
|
|
131
|
+
return String.raw`module.exports = {
|
|
132
|
+
forbidden: [
|
|
133
|
+
// ERROR RULES (block on violations)
|
|
134
|
+
{
|
|
135
|
+
name: 'no-circular',
|
|
136
|
+
severity: 'error',
|
|
137
|
+
from: {},
|
|
138
|
+
to: { circular: true },
|
|
139
|
+
},${hasMonorepoRules ? `
|
|
140
|
+
${monorepoRules},` : ""}
|
|
141
|
+
// INFO RULES (reported in /audit, not errors)
|
|
142
|
+
{
|
|
143
|
+
name: 'no-orphans',
|
|
144
|
+
severity: 'info',
|
|
145
|
+
from: { orphan: true, pathNot: ['\\.test\\.', 'index\\.ts$', 'main\\.ts$'] },
|
|
146
|
+
to: {},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
options: {
|
|
150
|
+
doNotFollow: { path: ['node_modules', '.safeword'] },
|
|
151
|
+
tsConfig: { fileName: 'tsconfig.json' },
|
|
152
|
+
enhancedResolveOptions: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
function generateDepCruiseMainConfig() {
|
|
158
|
+
return `/**
|
|
159
|
+
* Dependency Cruiser Configuration
|
|
160
|
+
*
|
|
161
|
+
* Imports auto-generated rules from .safeword/depcruise-config.js
|
|
162
|
+
* ADD YOUR CUSTOM RULES BELOW the spread operator.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
const generated = require('./.safeword/depcruise-config.js');
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
forbidden: [
|
|
169
|
+
...generated.forbidden,
|
|
170
|
+
// ADD YOUR CUSTOM RULES BELOW:
|
|
171
|
+
// { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },
|
|
172
|
+
],
|
|
173
|
+
options: {
|
|
174
|
+
...generated.options,
|
|
175
|
+
// Your overrides here
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/commands/sync-config.ts
|
|
182
|
+
function syncConfigCore(cwd, arch) {
|
|
183
|
+
const safewordDirectory = nodePath3.join(cwd, ".safeword");
|
|
184
|
+
const result = { generatedConfig: false, createdMainConfig: false };
|
|
185
|
+
const generatedConfigPath = nodePath3.join(safewordDirectory, "depcruise-config.js");
|
|
186
|
+
const generatedConfig = generateDepCruiseConfigFile(arch);
|
|
187
|
+
writeFileSync(generatedConfigPath, generatedConfig);
|
|
188
|
+
result.generatedConfig = true;
|
|
189
|
+
const mainConfigPath = nodePath3.join(cwd, ".dependency-cruiser.js");
|
|
190
|
+
if (!exists(mainConfigPath)) {
|
|
191
|
+
const mainConfig = generateDepCruiseMainConfig();
|
|
192
|
+
writeFileSync(mainConfigPath, mainConfig);
|
|
193
|
+
result.createdMainConfig = true;
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
function buildArchitecture(cwd) {
|
|
198
|
+
const arch = detectArchitecture(cwd);
|
|
199
|
+
const workspaces = detectWorkspaces(cwd);
|
|
200
|
+
return { ...arch, workspaces };
|
|
201
|
+
}
|
|
202
|
+
function hasArchitectureDetected(arch) {
|
|
203
|
+
return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;
|
|
204
|
+
}
|
|
205
|
+
async function syncConfig() {
|
|
206
|
+
const cwd = process.cwd();
|
|
207
|
+
const safewordDirectory = nodePath3.join(cwd, ".safeword");
|
|
208
|
+
if (!exists(safewordDirectory)) {
|
|
209
|
+
error("Not configured. Run `safeword setup` first.");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const arch = buildArchitecture(cwd);
|
|
213
|
+
const result = syncConfigCore(cwd, arch);
|
|
214
|
+
if (result.generatedConfig) {
|
|
215
|
+
info("Generated .safeword/depcruise-config.js");
|
|
216
|
+
}
|
|
217
|
+
if (result.createdMainConfig) {
|
|
218
|
+
info("Created .dependency-cruiser.js");
|
|
219
|
+
}
|
|
220
|
+
success("Config synced");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
syncConfigCore,
|
|
225
|
+
buildArchitecture,
|
|
226
|
+
hasArchitectureDetected,
|
|
227
|
+
syncConfig
|
|
228
|
+
};
|
|
229
|
+
//# sourceMappingURL=chunk-3R26BJXN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/sync-config.ts","../src/utils/boundaries.ts","../src/utils/depcruise-config.ts"],"sourcesContent":["/**\n * Sync Config command - Regenerate depcruise config from current project structure\n *\n * Used by `/audit` slash command to refresh config before running checks.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { detectArchitecture } from '../utils/boundaries.js';\nimport {\n type DepCruiseArchitecture,\n detectWorkspaces,\n generateDepCruiseConfigFile,\n generateDepCruiseMainConfig,\n} from '../utils/depcruise-config.js';\nimport { exists } from '../utils/fs.js';\nimport { error, info, success } from '../utils/output.js';\n\nexport interface SyncConfigResult {\n generatedConfig: boolean;\n createdMainConfig: boolean;\n}\n\n/**\n * Core sync logic - writes depcruise configs to disk\n * Can be called from setup or as standalone command\n */\nexport function syncConfigCore(cwd: string, arch: DepCruiseArchitecture): SyncConfigResult {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n const result: SyncConfigResult = { generatedConfig: false, createdMainConfig: false };\n\n // Generate and write .safeword/depcruise-config.js\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.js');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.js');\n if (!exists(mainConfigPath)) {\n const mainConfig = generateDepCruiseMainConfig();\n writeFileSync(mainConfigPath, mainConfig);\n result.createdMainConfig = true;\n }\n\n return result;\n}\n\n/**\n * Build full architecture info by combining detected layers with workspaces\n */\nexport function buildArchitecture(cwd: string): DepCruiseArchitecture {\n const arch = detectArchitecture(cwd);\n const workspaces = detectWorkspaces(cwd);\n return { ...arch, workspaces };\n}\n\n/**\n * Check if architecture was detected (layers, monorepo structure, or workspaces)\n */\nexport function hasArchitectureDetected(arch: DepCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * CLI command: Sync depcruise config with current project structure\n */\nexport async function syncConfig(): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if .safeword exists\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Detect current architecture and workspaces\n const arch = buildArchitecture(cwd);\n const result = syncConfigCore(cwd, arch);\n\n if (result.generatedConfig) {\n info('Generated .safeword/depcruise-config.js');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.js');\n }\n\n success('Config synced');\n}\n","/**\n * Architecture boundaries detection and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<Layer, Layer[]> = {\n types: [],\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\n\nexport interface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n * @param projectDirectory\n */\nfunction findMonorepoPackages(projectDirectory: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = nodePath.join(projectDirectory, root);\n if (!exists(rootPath)) continue;\n\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(nodePath.join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n * @param elements\n * @param layer\n * @param pathPrefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(\n element => element.layer === layer && element.pattern.startsWith(pathPrefix),\n );\n}\n\n/**\n * Scan a single search path for architecture layers\n * @param projectDirectory\n * @param searchPath\n * @param pathPrefix\n * @param elements\n */\nfunction scanSearchPath(\n projectDirectory: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDefinition of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDefinition.dirs) {\n const fullPath = nodePath.join(projectDirectory, searchPath, dirName);\n if (exists(fullPath) && !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n\n/**\n * Format a single element for the config\n * @param el\n */\nfunction formatElement(element: DetectedElement): string {\n return ` { type: '${element.layer}', pattern: '${element.pattern}', mode: 'full' }`;\n}\n\n/**\n * Format allowed imports for a rule\n * @param allowed\n */\nfunction formatAllowedImports(allowed: Layer[]): string {\n return allowed.map(d => `'${d}'`).join(', ');\n}\n\n/**\n * Generate a single rule for what a layer can import\n * @param layer\n * @param detectedLayers\n */\nfunction generateRule(layer: Layer, detectedLayers: Set<Layer>): string | undefined {\n const allowedLayers = HIERARCHY[layer];\n if (allowedLayers.length === 0) return undefined;\n\n const allowed = allowedLayers.filter(dep => detectedLayers.has(dep));\n if (allowed.length === 0) return undefined;\n\n return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;\n}\n\n/**\n * Build description of what was detected\n * @param arch\n */\nfunction buildDetectedInfo(arch: DetectedArchitecture): string {\n if (arch.elements.length === 0) {\n return 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n }\n const locations = arch.elements.map(element => element.location).join(', ');\n const monorepoNote = arch.isMonorepo ? ' (monorepo)' : '';\n return `Detected: ${locations}${monorepoNote}`;\n}\n\n/**\n *\n * @param arch\n */\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const hasElements = arch.elements.length > 0;\n\n // Generate element definitions\n const elementsContent = arch.elements.map(element => formatElement(element)).join(',\\n');\n\n // Generate rules (what each layer can import)\n const detectedLayers = new Set(arch.elements.map(element => element.layer));\n const rules = [...detectedLayers]\n .map(layer => generateRule(layer, detectedLayers))\n .filter((rule): rule is string => rule !== undefined);\n const rulesContent = rules.join(',\\n');\n\n const detectedInfo = buildDetectedInfo(arch);\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'error' severity - LLMs ignore warnings, errors force compliance\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {${\n hasElements\n ? `\n 'boundaries/element-types': ['error', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],`\n : ''\n }\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DepCruiseArchitecture extends DetectedArchitecture {\n workspaces?: string[];\n}\n\ninterface PackageJson {\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Detect workspaces from package.json\n * Supports both array format and object format (yarn workspaces)\n */\nexport function detectWorkspaces(cwd: string): string[] | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n\n if (!packageJson?.workspaces) return undefined;\n\n // Handle both formats: string[] or { packages: string[] }\n const workspaces = Array.isArray(packageJson.workspaces)\n ? packageJson.workspaces\n : packageJson.workspaces.packages;\n\n return workspaces && workspaces.length > 0 ? workspaces : undefined;\n}\n\n/**\n * Generate monorepo hierarchy rules based on workspace patterns\n */\nfunction generateMonorepoRules(workspaces: string[]): string {\n const rules: string[] = [];\n\n const hasLibs = workspaces.some(w => w.startsWith('libs'));\n const hasPackages = workspaces.some(w => w.startsWith('packages'));\n const hasApps = workspaces.some(w => w.startsWith('apps'));\n\n // libs cannot import packages or apps\n if (hasLibs && (hasPackages || hasApps)) {\n rules.push(` {\n name: 'libs-cannot-import-packages-or-apps',\n severity: 'error',\n from: { path: '^libs/' },\n to: { path: '^(packages|apps)/' },\n }`);\n }\n\n // packages cannot import apps\n if (hasPackages && hasApps) {\n rules.push(` {\n name: 'packages-cannot-import-apps',\n severity: 'error',\n from: { path: '^packages/' },\n to: { path: '^apps/' },\n }`);\n }\n\n return rules.join(',\\n');\n}\n\n/**\n * Generate .safeword/depcruise-config.js content (forbidden rules + options)\n */\nexport function generateDepCruiseConfigFile(arch: DepCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // ERROR RULES (block on violations)\n {\n name: 'no-circular',\n severity: 'error',\n from: {},\n to: { circular: true },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n // INFO RULES (reported in /audit, not errors)\n {\n name: 'no-orphans',\n severity: 'info',\n from: { orphan: true, pathNot: ['\\\\.test\\\\.', 'index\\\\.ts$', 'main\\\\.ts$'] },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },\n },\n};\n`;\n}\n\n/**\n * Generate .dependency-cruiser.js (main config that imports generated)\n */\nexport function generateDepCruiseMainConfig(): string {\n return `/**\n * Dependency Cruiser Configuration\n *\n * Imports auto-generated rules from .safeword/depcruise-config.js\n * ADD YOUR CUSTOM RULES BELOW the spread operator.\n */\n\nconst generated = require('./.safeword/depcruise-config.js');\n\nmodule.exports = {\n forbidden: [\n ...generated.forbidden,\n // ADD YOUR CUSTOM RULES BELOW:\n // { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },\n ],\n options: {\n ...generated.options,\n // Your overrides here\n },\n};\n`;\n}\n"],"mappings":"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","names":["nodePath","nodePath","nodePath","nodePath"]}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/utils/fs.ts
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync
|
|
11
|
+
} from "fs";
|
|
12
|
+
import nodePath from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
function getTemplatesDirectory() {
|
|
16
|
+
const knownTemplateFile = "SAFEWORD.md";
|
|
17
|
+
const candidates = [
|
|
18
|
+
nodePath.join(__dirname, "..", "templates"),
|
|
19
|
+
// From dist/ (flat bundled)
|
|
20
|
+
nodePath.join(__dirname, "..", "..", "templates"),
|
|
21
|
+
// From src/utils/ or dist/utils/
|
|
22
|
+
nodePath.join(__dirname, "templates")
|
|
23
|
+
// Direct sibling (unlikely but safe)
|
|
24
|
+
];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
if (existsSync(nodePath.join(candidate, knownTemplateFile))) {
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error("Templates directory not found");
|
|
31
|
+
}
|
|
32
|
+
function exists(path) {
|
|
33
|
+
return existsSync(path);
|
|
34
|
+
}
|
|
35
|
+
function ensureDirectory(path) {
|
|
36
|
+
if (!existsSync(path)) {
|
|
37
|
+
mkdirSync(path, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function readFile(path) {
|
|
41
|
+
return readFileSync(path, "utf8");
|
|
42
|
+
}
|
|
43
|
+
function readFileSafe(path) {
|
|
44
|
+
if (!existsSync(path)) return void 0;
|
|
45
|
+
return readFileSync(path, "utf8");
|
|
46
|
+
}
|
|
47
|
+
function writeFile(path, content) {
|
|
48
|
+
ensureDirectory(nodePath.dirname(path));
|
|
49
|
+
writeFileSync(path, content);
|
|
50
|
+
}
|
|
51
|
+
function remove(path) {
|
|
52
|
+
if (existsSync(path)) {
|
|
53
|
+
rmSync(path, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function removeIfEmpty(path) {
|
|
57
|
+
if (!existsSync(path)) return false;
|
|
58
|
+
try {
|
|
59
|
+
rmdirSync(path);
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function makeScriptsExecutable(dirPath) {
|
|
66
|
+
if (!existsSync(dirPath)) return;
|
|
67
|
+
for (const file of readdirSync(dirPath)) {
|
|
68
|
+
if (file.endsWith(".sh")) {
|
|
69
|
+
chmodSync(nodePath.join(dirPath, file), 493);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function readJson(path) {
|
|
74
|
+
const content = readFileSafe(path);
|
|
75
|
+
if (!content) return void 0;
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(content);
|
|
78
|
+
} catch {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function writeJson(path, data) {
|
|
83
|
+
writeFile(path, `${JSON.stringify(data, void 0, 2)}
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/utils/output.ts
|
|
88
|
+
function info(message) {
|
|
89
|
+
console.log(message);
|
|
90
|
+
}
|
|
91
|
+
function success(message) {
|
|
92
|
+
console.log(`\u2713 ${message}`);
|
|
93
|
+
}
|
|
94
|
+
function warn(message) {
|
|
95
|
+
console.warn(`\u26A0 ${message}`);
|
|
96
|
+
}
|
|
97
|
+
function error(message) {
|
|
98
|
+
console.error(`\u2717 ${message}`);
|
|
99
|
+
}
|
|
100
|
+
function header(title) {
|
|
101
|
+
console.log(`
|
|
102
|
+
${title}`);
|
|
103
|
+
console.log("\u2500".repeat(title.length));
|
|
104
|
+
}
|
|
105
|
+
function listItem(item, indent = 2) {
|
|
106
|
+
console.log(`${" ".repeat(indent)}\u2022 ${item}`);
|
|
107
|
+
}
|
|
108
|
+
function keyValue(key, value) {
|
|
109
|
+
console.log(` ${key}: ${value}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
getTemplatesDirectory,
|
|
114
|
+
exists,
|
|
115
|
+
ensureDirectory,
|
|
116
|
+
readFile,
|
|
117
|
+
readFileSafe,
|
|
118
|
+
writeFile,
|
|
119
|
+
remove,
|
|
120
|
+
removeIfEmpty,
|
|
121
|
+
makeScriptsExecutable,
|
|
122
|
+
readJson,
|
|
123
|
+
writeJson,
|
|
124
|
+
info,
|
|
125
|
+
success,
|
|
126
|
+
warn,
|
|
127
|
+
error,
|
|
128
|
+
header,
|
|
129
|
+
listItem,
|
|
130
|
+
keyValue
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=chunk-DYLHQBW3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/fs.ts","../src/utils/output.ts"],"sourcesContent":["/**\n * File system utilities for CLI operations\n */\n\nimport {\n chmodSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport nodePath from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// Get the directory of this module (for locating templates)\nconst __dirname = nodePath.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Get path to bundled templates directory.\n * Works in both development (src/) and production (dist/) contexts.\n *\n * Note: We check for SAFEWORD.md to distinguish from src/templates/ which\n * contains TypeScript source files (config.ts, content.ts).\n *\n * Path resolution (bundled with tsup):\n * - From dist/chunk-*.js: __dirname = packages/cli/dist/ → ../templates\n */\nexport function getTemplatesDirectory(): string {\n const knownTemplateFile = 'SAFEWORD.md';\n\n // Try different relative paths - the bundled code ends up in dist/ directly (flat)\n // while source is in src/utils/\n const candidates = [\n nodePath.join(__dirname, '..', 'templates'), // From dist/ (flat bundled)\n nodePath.join(__dirname, '..', '..', 'templates'), // From src/utils/ or dist/utils/\n nodePath.join(__dirname, 'templates'), // Direct sibling (unlikely but safe)\n ];\n\n for (const candidate of candidates) {\n if (existsSync(nodePath.join(candidate, knownTemplateFile))) {\n return candidate;\n }\n }\n\n throw new Error('Templates directory not found');\n}\n\n/**\n * Check if a path exists\n * @param path\n */\nexport function exists(path: string): boolean {\n return existsSync(path);\n}\n\n/**\n * Create directory recursively\n * @param path\n */\nexport function ensureDirectory(path: string): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true });\n }\n}\n\n/**\n * Read file as string\n * @param path\n */\nexport function readFile(path: string): string {\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Read file as string, return null if not exists\n * @param path\n */\nexport function readFileSafe(path: string): string | undefined {\n if (!existsSync(path)) return undefined;\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Write file, creating parent directories if needed\n * @param path\n * @param content\n */\nexport function writeFile(path: string, content: string): void {\n ensureDirectory(nodePath.dirname(path));\n writeFileSync(path, content);\n}\n\n/**\n * Remove file or directory recursively\n * @param path\n */\nexport function remove(path: string): void {\n if (existsSync(path)) {\n rmSync(path, { recursive: true, force: true });\n }\n}\n\n/**\n * Remove directory only if empty, returns true if removed\n * @param path\n */\nexport function removeIfEmpty(path: string): boolean {\n if (!existsSync(path)) return false;\n try {\n rmdirSync(path); // Non-recursive, throws if not empty\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Make all shell scripts in a directory executable\n * @param dirPath\n */\nexport function makeScriptsExecutable(dirPath: string): void {\n if (!existsSync(dirPath)) return;\n for (const file of readdirSync(dirPath)) {\n if (file.endsWith('.sh')) {\n chmodSync(nodePath.join(dirPath, file), 0o755);\n }\n }\n}\n\n/**\n * Read JSON file\n * @param path\n */\nexport function readJson(path: string): unknown {\n const content = readFileSafe(path);\n if (!content) return undefined;\n try {\n return JSON.parse(content) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Write JSON file with formatting\n * @param path\n * @param data\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFile(path, `${JSON.stringify(data, undefined, 2)}\\n`);\n}\n","/**\n * Console output utilities for consistent CLI messaging\n */\n\n/**\n * Print info message\n * @param message\n */\nexport function info(message: string): void {\n console.log(message);\n}\n\n/**\n * Print success message\n * @param message\n */\nexport function success(message: string): void {\n console.log(`✓ ${message}`);\n}\n\n/**\n * Print warning message\n * @param message\n */\nexport function warn(message: string): void {\n console.warn(`⚠ ${message}`);\n}\n\n/**\n * Print error message to stderr\n * @param message\n */\nexport function error(message: string): void {\n console.error(`✗ ${message}`);\n}\n\n/**\n * Print a blank line\n */\nexport function blank(): void {\n console.log('');\n}\n\n/**\n * Print a section header\n * @param title\n */\nexport function header(title: string): void {\n console.log(`\\n${title}`);\n console.log('─'.repeat(title.length));\n}\n\n/**\n * Print a list item\n * @param item\n * @param indent\n */\nexport function listItem(item: string, indent = 2): void {\n console.log(`${' '.repeat(indent)}• ${item}`);\n}\n\n/**\n * Print key-value pair\n * @param key\n * @param value\n */\nexport function keyValue(key: string, value: string): void {\n console.log(` ${key}: ${value}`);\n}\n"],"mappings":"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","names":[]}
|