@dawudesign/node-hexa-cli 0.1.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/index.d.ts +1 -0
- package/dist/index.js +248 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/license.ts
|
|
8
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var SIGNING_SECRET = "2dc6d0a8b5c755f95a2e9fbca8c8e6c45226763dc2040ae5335bc86f480337bb";
|
|
13
|
+
var CONFIG_DIR = join(homedir(), ".config", "node-hexa");
|
|
14
|
+
var LICENSE_FILE = join(CONFIG_DIR, "license");
|
|
15
|
+
function parseLicenseKey(key) {
|
|
16
|
+
const parts = key.split("-");
|
|
17
|
+
if (parts.length < 3 || parts[0] !== "NODEHEXA") return null;
|
|
18
|
+
const sig = parts.at(-1);
|
|
19
|
+
if (!sig) return null;
|
|
20
|
+
const encoded = parts.slice(1, -1).join("-");
|
|
21
|
+
let payload;
|
|
22
|
+
try {
|
|
23
|
+
payload = Buffer.from(encoded, "base64url").toString("utf8");
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const expectedSig = createHmac("sha256", SIGNING_SECRET).update(payload).digest("hex").slice(0, 16);
|
|
28
|
+
const sigOk = sig.length === expectedSig.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig));
|
|
29
|
+
if (!sigOk) return null;
|
|
30
|
+
const colonIdx = payload.indexOf(":");
|
|
31
|
+
if (colonIdx === -1) return null;
|
|
32
|
+
return {
|
|
33
|
+
email: payload.slice(0, colonIdx),
|
|
34
|
+
expiresAt: payload.slice(colonIdx + 1)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function activateLicense(key) {
|
|
38
|
+
const parsed = parseLicenseKey(key.trim());
|
|
39
|
+
if (!parsed) {
|
|
40
|
+
console.error("\u2717 Invalid license key.");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
44
|
+
if (parsed.expiresAt !== "lifetime" && parsed.expiresAt < today) {
|
|
45
|
+
console.error(`\u2717 This license key expired on ${parsed.expiresAt}.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
49
|
+
writeFileSync(LICENSE_FILE, key.trim(), "utf8");
|
|
50
|
+
console.log(`\u2713 License activated for ${parsed.email} (valid until: ${parsed.expiresAt})`);
|
|
51
|
+
}
|
|
52
|
+
function checkLicense() {
|
|
53
|
+
if (!existsSync(LICENSE_FILE)) {
|
|
54
|
+
console.error(
|
|
55
|
+
"\u2717 No license found. Purchase a license at https://your-website.com and run:\n node-hexa activate <your-license-key>"
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const key = readFileSync(LICENSE_FILE, "utf8").trim();
|
|
60
|
+
const parsed = parseLicenseKey(key);
|
|
61
|
+
if (!parsed) {
|
|
62
|
+
console.error(
|
|
63
|
+
"\u2717 License file is corrupted. Re-activate with:\n node-hexa activate <your-license-key>"
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
68
|
+
if (parsed.expiresAt !== "lifetime" && parsed.expiresAt < today) {
|
|
69
|
+
console.error(
|
|
70
|
+
`\u2717 Your license expired on ${parsed.expiresAt}. Renew at https://your-website.com`
|
|
71
|
+
);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/index.ts
|
|
77
|
+
import {
|
|
78
|
+
analyzeProject,
|
|
79
|
+
generateMermaidGraph,
|
|
80
|
+
generateDocs,
|
|
81
|
+
generateGraphFile,
|
|
82
|
+
detectContexts,
|
|
83
|
+
generateProject,
|
|
84
|
+
generateContext,
|
|
85
|
+
generateUseCase,
|
|
86
|
+
generateAggregate,
|
|
87
|
+
listContexts
|
|
88
|
+
} from "@node-hexa/core";
|
|
89
|
+
function severityBadge(severity) {
|
|
90
|
+
if (severity === "critical") return "CRITICAL";
|
|
91
|
+
if (severity === "high") return "HIGH";
|
|
92
|
+
return "MEDIUM";
|
|
93
|
+
}
|
|
94
|
+
function printLayers(nodes) {
|
|
95
|
+
const layers = ["domain", "application", "infrastructure", "adapter-in", "adapter-out"];
|
|
96
|
+
for (const layer of layers) {
|
|
97
|
+
const layerNodes = nodes.filter((n) => n.layer === layer);
|
|
98
|
+
if (!layerNodes.length) continue;
|
|
99
|
+
console.log(layer.toUpperCase());
|
|
100
|
+
layerNodes.forEach((n) => console.log(` \u2713 ${n.name} (${n.kind})`));
|
|
101
|
+
console.log("");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function printViolations(violations) {
|
|
105
|
+
console.log("Violations\n");
|
|
106
|
+
if (!violations.length) {
|
|
107
|
+
console.log(" \u2713 No architecture violations found\n");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const v of violations) {
|
|
111
|
+
console.log(` \u2717 [${severityBadge(v.severity)}] ${v.message} \u2192 ${v.node}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function printContexts(nodes) {
|
|
115
|
+
const contexts = detectContexts(nodes);
|
|
116
|
+
console.log("\nBounded Contexts\n");
|
|
117
|
+
for (const [name, ctxNodes] of Object.entries(contexts)) {
|
|
118
|
+
console.log(name.toUpperCase());
|
|
119
|
+
ctxNodes.forEach((n) => console.log(` - ${n.name} (${n.kind})`));
|
|
120
|
+
console.log("");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function die(message, code = 1) {
|
|
124
|
+
console.error(`\u2717 ${message}`);
|
|
125
|
+
process.exit(code);
|
|
126
|
+
}
|
|
127
|
+
function handleError(err) {
|
|
128
|
+
die(err instanceof Error ? err.message : String(err), 1);
|
|
129
|
+
}
|
|
130
|
+
var program = new Command();
|
|
131
|
+
program.name("node-hexa").description("Architecture analyzer and scaffolder for NestJS Hexagonal DDD").version(process.env["npm_package_version"] ?? "0.1.0");
|
|
132
|
+
program.command("analyze").description("Analyze architecture layers, violations and bounded contexts").argument("<path>", "project path").action(async (projectPath) => {
|
|
133
|
+
try {
|
|
134
|
+
const result = await analyzeProject(projectPath);
|
|
135
|
+
console.log("\nArchitecture Graph (Mermaid)\n");
|
|
136
|
+
console.log(generateMermaidGraph(result.model));
|
|
137
|
+
printLayers(result.model.nodes);
|
|
138
|
+
printViolations(result.violations);
|
|
139
|
+
printContexts(result.model.nodes);
|
|
140
|
+
console.log("\nArchitecture Score\n");
|
|
141
|
+
console.log(`Score: ${result.score.score}/${result.score.max}
|
|
142
|
+
`);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
handleError(err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
program.command("docs").description("Generate architecture.md with Mermaid diagram and violations").argument("<path>", "project path").action(async (projectPath) => {
|
|
148
|
+
try {
|
|
149
|
+
const result = await analyzeProject(projectPath);
|
|
150
|
+
generateDocs(result, projectPath);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
handleError(err);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
program.command("graph").description("Generate architecture.svg dependency graph").argument("<path>", "project path").action(async (projectPath) => {
|
|
156
|
+
try {
|
|
157
|
+
const result = await analyzeProject(projectPath);
|
|
158
|
+
const svgFile = generateGraphFile(result);
|
|
159
|
+
console.log(`Architecture graph generated: ${svgFile}`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
handleError(err);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
program.command("init").description("Create a new NestJS Hexagonal DDD project").argument("<name>", "project name (lowercase, hyphens allowed)").action((name) => {
|
|
165
|
+
try {
|
|
166
|
+
generateProject(name);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
handleError(err);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
program.command("generate").description("Generate a context, use case, or aggregate in an existing project").argument("<type>", "context | usecase | aggregate").argument("<name>", "resource name (kebab-case)").argument("[context]", "bounded context name (required for usecase and aggregate)").action((type, name, context) => {
|
|
172
|
+
try {
|
|
173
|
+
if (type === "context") {
|
|
174
|
+
generateContext(name);
|
|
175
|
+
} else if (type === "usecase") {
|
|
176
|
+
if (!context) die("Missing argument: <context> is required for generate usecase.\n Usage: node-hexa generate usecase <name> <context>");
|
|
177
|
+
generateUseCase(name, context);
|
|
178
|
+
console.log(`\u2713 Use case '${name}' generated in context '${context}'`);
|
|
179
|
+
} else if (type === "aggregate") {
|
|
180
|
+
if (!context) die("Missing argument: <context> is required for generate aggregate.\n Usage: node-hexa generate aggregate <name> <context>");
|
|
181
|
+
generateAggregate(name, context);
|
|
182
|
+
} else {
|
|
183
|
+
die(`Unknown type: '${type}'. Use context | usecase | aggregate`);
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
handleError(err);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
program.command("check").description("Check architecture rules \u2014 exits 1 on violations, 0 if clean").argument("<path>", "project path").option("-w, --watch", "re-run every 2s").action(async (projectPath, options) => {
|
|
190
|
+
const runCheck = async () => {
|
|
191
|
+
if (options.watch) process.stdout.write("\x1Bc");
|
|
192
|
+
const result = await analyzeProject(projectPath);
|
|
193
|
+
if (!result.violations.length) {
|
|
194
|
+
console.log("\u2713 Architecture check passed");
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
console.log("\u2717 Architecture violations detected\n");
|
|
198
|
+
result.violations.forEach(
|
|
199
|
+
(v) => console.log(` [${severityBadge(v.severity)}] ${v.message} \u2192 ${v.node}`)
|
|
200
|
+
);
|
|
201
|
+
return false;
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
const passed = await runCheck();
|
|
205
|
+
if (!options.watch) process.exit(passed ? 0 : 1);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
208
|
+
if (!options.watch) process.exit(2);
|
|
209
|
+
}
|
|
210
|
+
if (options.watch) {
|
|
211
|
+
console.log("\nWatching for changes\u2026 (Ctrl+C to stop)");
|
|
212
|
+
setInterval(async () => {
|
|
213
|
+
try {
|
|
214
|
+
await runCheck();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
217
|
+
}
|
|
218
|
+
}, 2e3);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
program.command("list").description("List all bounded contexts and their components").argument("<path>", "project path").action((projectPath) => {
|
|
222
|
+
try {
|
|
223
|
+
const contexts = listContexts(projectPath);
|
|
224
|
+
if (contexts.length === 0) {
|
|
225
|
+
console.log("No bounded contexts found. Expected: <path>/src/contexts/");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
console.log(`
|
|
229
|
+
Bounded Contexts (${contexts.length})
|
|
230
|
+
`);
|
|
231
|
+
for (const ctx of contexts) {
|
|
232
|
+
console.log(` ${ctx.name.toUpperCase()}`);
|
|
233
|
+
if (ctx.entities.length) console.log(` Entities : ${ctx.entities.join(", ")}`);
|
|
234
|
+
if (ctx.valueObjects.length) console.log(` Value Objects : ${ctx.valueObjects.join(", ")}`);
|
|
235
|
+
if (ctx.ports.length) console.log(` Ports : ${ctx.ports.join(", ")}`);
|
|
236
|
+
if (ctx.useCases.length) console.log(` Use Cases : ${ctx.useCases.join(", ")}`);
|
|
237
|
+
console.log("");
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
handleError(err);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
program.command("activate").description("Activate your node-hexa license").argument("<key>", "license key received after purchase").action((key) => {
|
|
244
|
+
activateLicense(key);
|
|
245
|
+
});
|
|
246
|
+
var noLicenseNeeded = ["activate", "--help", "-h", "--version", "-V", void 0];
|
|
247
|
+
if (!noLicenseNeeded.includes(process.argv[2])) checkLicense();
|
|
248
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dawudesign/node-hexa-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to scaffold and analyze NestJS Hexagonal DDD projects",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nestjs",
|
|
7
|
+
"hexagonal",
|
|
8
|
+
"ddd",
|
|
9
|
+
"architecture",
|
|
10
|
+
"cli",
|
|
11
|
+
"scaffold"
|
|
12
|
+
],
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"bin": {
|
|
22
|
+
"node-hexa": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"prepublishOnly": "echo 'Use: pnpm pub' && exit 1",
|
|
30
|
+
"pub": "pnpm build && chmod +x dist/index.js && npm publish --ignore-scripts",
|
|
31
|
+
"dev": "tsx src/index.ts",
|
|
32
|
+
"start": "node dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@node-hexa/core": "workspace:^",
|
|
36
|
+
"@node-hexa/parser": "workspace:^",
|
|
37
|
+
"commander": "^14.0.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.3.5",
|
|
41
|
+
"tsup": "^8.0.0",
|
|
42
|
+
"tsx": "^4.0.0",
|
|
43
|
+
"typescript": "^5.3.0"
|
|
44
|
+
}
|
|
45
|
+
}
|