larats 1.0.2 → 1.1.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/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // src/index.ts
4
4
  import fs3 from "fs";
5
5
  import path4 from "path";
6
+ import os2 from "os";
7
+ import { exec } from "child_process";
6
8
  import { parseArgs } from "util";
7
9
  import * as p from "@clack/prompts";
8
10
  import pc from "picocolors";
@@ -69,7 +71,7 @@ async function detectProjectType(targetDir = process.cwd()) {
69
71
  summary
70
72
  };
71
73
  }
72
- if (hasTsConfig && hasPackageJson) {
74
+ if (hasPackageJson) {
73
75
  const pkg = readJsonSafe(packageJsonPath);
74
76
  const allDeps = {
75
77
  ...pkg?.dependencies || {},
@@ -91,13 +93,15 @@ async function detectProjectType(targetDir = process.cwd()) {
91
93
  const hasSrc = fs.existsSync(path.join(resolvedDir, "src"));
92
94
  const srcDir = hasSrc ? "src" : ".";
93
95
  const isModular = fs.existsSync(path.join(resolvedDir, srcDir, "modules")) || fs.existsSync(path.join(resolvedDir, "modules"));
94
- const extras = [framework !== "node-vanilla" ? framework : "TypeScript"];
96
+ const isTypeScript = hasTsConfig || "typescript" in allDeps;
97
+ const stackLabel = isTypeScript ? "TypeScript" : "JavaScript / Node.js";
98
+ const extras = [framework !== "node-vanilla" ? framework : stackLabel];
95
99
  if (hasPrisma) extras.push("Prisma");
96
100
  if (hasTypeORM) extras.push("TypeORM");
97
101
  if (hasDrizzle) extras.push("Drizzle");
98
102
  if (isMonorepo) extras.push("Monorepo");
99
103
  if (isModular) extras.push("Modular");
100
- const summary = `TypeScript (${extras.join(", ")})`;
104
+ const summary = isTypeScript ? `TypeScript (${extras.join(", ")})` : `JavaScript / Node.js (${extras.join(", ")})`;
101
105
  return {
102
106
  type: "typescript",
103
107
  name: pkg?.name || dirName,
@@ -109,7 +113,8 @@ async function detectProjectType(targetDir = process.cwd()) {
109
113
  hasDrizzle,
110
114
  isMonorepo,
111
115
  isModular,
112
- srcDir
116
+ srcDir,
117
+ isTypeScript
113
118
  },
114
119
  summary
115
120
  };
@@ -505,7 +510,7 @@ ${f}`;
505
510
  }
506
511
  function mermaidLabel(str, maxLen = 40) {
507
512
  if (!str) return "";
508
- let cleaned = str.replace(/"/g, "'").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/`/g, "'").replace(/[\r\n]+/g, " ").trim();
513
+ let cleaned = str.replace(/"/g, "'").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/`/g, "'").replace(/[\[\]\(\)\{\}]/g, "").replace(/[\r\n]+/g, " ").trim();
509
514
  if (cleaned.length > maxLen) {
510
515
  cleaned = cleaned.slice(0, maxLen - 3) + "...";
511
516
  }
@@ -908,6 +913,312 @@ ${fence(directoryTree || "(Empty directory structure)", "text")}
908
913
  ${securityNotice}
909
914
  `;
910
915
  }
916
+ function renderTerminalFlow(scanResult) {
917
+ const { projectInfo, components } = scanResult;
918
+ const steps = [];
919
+ if (projectInfo.type === "laravel") {
920
+ steps.push("Client");
921
+ const routes = components["Routes"]?.count ?? 0;
922
+ if (routes > 0) steps.push(`Routes [${routes}]`);
923
+ const requests = components["Form Requests"]?.count ?? 0;
924
+ if (requests > 0) steps.push(`Requests [${requests}]`);
925
+ const controllers = components["Controllers"]?.count ?? 0;
926
+ if (controllers > 0) steps.push(`Controllers [${controllers}]`);
927
+ const services = (components["Services"]?.count ?? 0) + (components["Actions"]?.count ?? 0);
928
+ if (services > 0) steps.push(`Services [${services}]`);
929
+ const models = components["Models"]?.count ?? 0;
930
+ if (models > 0) steps.push(`Models [${models}]`);
931
+ steps.push("Database");
932
+ } else if (projectInfo.type === "typescript") {
933
+ const hasAnyLayer = ["Controllers", "Services", "Repositories", "Routes", "DTOs & Schemas", "Models & Entities", "UI Components"].some((k) => (components[k]?.count ?? 0) > 0);
934
+ if (hasAnyLayer) {
935
+ steps.push("Client");
936
+ const ui = components["UI Components"]?.count ?? 0;
937
+ if (ui > 0) steps.push(`UI [${ui}]`);
938
+ const routes = components["Routes"]?.count ?? 0;
939
+ if (routes > 0) steps.push(`Router [${routes}]`);
940
+ const dtos = components["DTOs & Schemas"]?.count ?? 0;
941
+ if (dtos > 0) steps.push(`DTOs [${dtos}]`);
942
+ const controllers = components["Controllers"]?.count ?? 0;
943
+ if (controllers > 0) steps.push(`Controllers [${controllers}]`);
944
+ const services = components["Services"]?.count ?? 0;
945
+ if (services > 0) steps.push(`Services [${services}]`);
946
+ const data = (components["Repositories"]?.count ?? 0) + (components["Models & Entities"]?.count ?? 0);
947
+ if (data > 0) steps.push(`Data Layer [${data}]`);
948
+ steps.push("Database");
949
+ } else {
950
+ const src = components["Source Files"]?.count ?? 0;
951
+ const tests = components["Test Files"]?.count ?? 0;
952
+ steps.push("CLI / Entrypoint");
953
+ if (src > 0) steps.push(`Source Modules [${src}]`);
954
+ if (tests > 0) steps.push(`Test Suites [${tests}]`);
955
+ }
956
+ } else {
957
+ const src = components["Source Files"]?.count ?? 0;
958
+ const tests = components["Test Files"]?.count ?? 0;
959
+ steps.push("Project Root");
960
+ if (src > 0) steps.push(`Source [${src}]`);
961
+ if (tests > 0) steps.push(`Tests [${tests}]`);
962
+ }
963
+ return steps.join(" \u2500\u2500> ");
964
+ }
965
+ function escapeHtml(str) {
966
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
967
+ }
968
+ function generateArchitectureHtml(scanResult) {
969
+ const { projectInfo, totalFilesScanned, components, modules, directoryTree } = scanResult;
970
+ const mermaid = generateMermaidDiagram(scanResult);
971
+ const componentRowsHtml = Object.entries(components).map(([, comp]) => {
972
+ const samples = comp.files.slice(0, 3).map((f) => `<code>${escapeHtml(f)}</code>`).join(", ");
973
+ const more = comp.files.length > 3 ? ` <span class="muted">(+${comp.files.length - 3} more)</span>` : "";
974
+ return `<tr>
975
+ <td><strong>${escapeHtml(comp.name)}</strong></td>
976
+ <td><span class="badge">${comp.count}</span></td>
977
+ <td>${escapeHtml(comp.description)}</td>
978
+ <td>${samples ? samples + more : "-"}</td>
979
+ </tr>`;
980
+ }).join("\n");
981
+ const modulesHtml = modules.length > 0 ? `<div class="card">
982
+ <h2>\u{1F4E6} Detected Modules & Domains (${modules.length})</h2>
983
+ <div class="modules-grid">
984
+ ${modules.map((m) => `<div class="module-item"><code>${escapeHtml(m)}</code></div>`).join("\n")}
985
+ </div>
986
+ </div>` : "";
987
+ return `<!DOCTYPE html>
988
+ <html lang="en">
989
+ <head>
990
+ <meta charset="UTF-8">
991
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
992
+ <title>LaraTS Architecture: ${escapeHtml(projectInfo.name)}</title>
993
+ <style>
994
+ :root {
995
+ --bg: #0f172a;
996
+ --card-bg: #1e293b;
997
+ --text: #f8fafc;
998
+ --muted: #94a3b8;
999
+ --border: #334155;
1000
+ --primary: #38bdf8;
1001
+ --accent: #818cf8;
1002
+ }
1003
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1004
+ body {
1005
+ background: var(--bg);
1006
+ color: var(--text);
1007
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
1008
+ line-height: 1.5;
1009
+ padding: 2rem 1rem;
1010
+ }
1011
+ .container { max-width: 1080px; margin: 0 auto; }
1012
+ header { margin-bottom: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; }
1013
+ .header-top { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem; }
1014
+ h1 { font-size: 1.8rem; font-weight: 700; color: #fff; }
1015
+ .pill { background: #0284c7; color: #fff; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.85rem; font-weight: 600; }
1016
+ .subtitle { color: var(--muted); margin-top: 0.5rem; font-size: 0.95rem; }
1017
+ .card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; margin-bottom: 2rem; }
1018
+ h2 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: #e2e8f0; }
1019
+ table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; font-size: 0.9rem; }
1020
+ th, td { text-align: left; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); }
1021
+ th { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
1022
+ .badge { background: #334155; padding: 0.2rem 0.6rem; border-radius: 4px; font-weight: 600; font-size: 0.85rem; }
1023
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; background: #090d16; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; color: #38bdf8; }
1024
+ pre { background: #090d16; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; border: 1px solid var(--border); line-height: 1.4; }
1025
+ .mermaid-box { display: flex; justify-content: center; background: #090d16; border-radius: 8px; padding: 2rem 1rem; overflow-x: auto; border: 1px solid var(--border); }
1026
+ .modules-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; }
1027
+ .module-item { background: #090d16; border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 1rem; }
1028
+ .muted { color: var(--muted); font-size: 0.85rem; }
1029
+ footer { text-align: center; color: var(--muted); font-size: 0.85rem; margin-top: 3rem; }
1030
+ </style>
1031
+ </head>
1032
+ <body>
1033
+ <div class="container">
1034
+ <header>
1035
+ <div class="header-top">
1036
+ <div>
1037
+ <h1>\u{1F3DB}\uFE0F Architecture Overview: ${escapeHtml(projectInfo.name)}</h1>
1038
+ <p class="subtitle">Stack: <strong>${escapeHtml(projectInfo.summary)}</strong> &bull; Total Files: <strong>${totalFilesScanned}</strong></p>
1039
+ </div>
1040
+ <span class="pill">LaraTS Architecture Map</span>
1041
+ </div>
1042
+ </header>
1043
+
1044
+ <div class="card">
1045
+ <h2>\u{1F5FA}\uFE0F Interactive Architecture Flowchart</h2>
1046
+ <div class="mermaid-box">
1047
+ <pre class="mermaid">${mermaid}</pre>
1048
+ </div>
1049
+ </div>
1050
+
1051
+ <div class="card">
1052
+ <h2>\u{1F4CA} Architectural Component Statistics</h2>
1053
+ <table>
1054
+ <thead>
1055
+ <tr>
1056
+ <th>Component Layer</th>
1057
+ <th>Files</th>
1058
+ <th>Description</th>
1059
+ <th>Sample Files</th>
1060
+ </tr>
1061
+ </thead>
1062
+ <tbody>
1063
+ ${componentRowsHtml || '<tr><td colspan="4">No architectural components detected</td></tr>'}
1064
+ </tbody>
1065
+ </table>
1066
+ </div>
1067
+
1068
+ ${modulesHtml}
1069
+
1070
+ <div class="card">
1071
+ <h2>\u{1F332} Directory Structure Tree</h2>
1072
+ <pre><code>${escapeHtml(directoryTree || "(Empty directory)")}</code></pre>
1073
+ </div>
1074
+
1075
+ <footer>
1076
+ Generated locally &amp; offline with \u2764\uFE0F by <strong>LaraTS CLI</strong>
1077
+ </footer>
1078
+ </div>
1079
+
1080
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1081
+ <script>
1082
+ if (typeof mermaid !== 'undefined') {
1083
+ mermaid.initialize({
1084
+ startOnLoad: true,
1085
+ theme: 'dark',
1086
+ flowchart: { curve: 'basis' }
1087
+ });
1088
+ }
1089
+ </script>
1090
+ </body>
1091
+ </html>`;
1092
+ }
1093
+
1094
+ // src/generator/ai-context.ts
1095
+ var AI_RULES_START_TAG = "<!-- larats-rules:start -->";
1096
+ var AI_RULES_END_TAG = "<!-- larats-rules:end -->";
1097
+ function generateAiContext(scanResult) {
1098
+ const { projectInfo, components, modules } = scanResult;
1099
+ const lines = [];
1100
+ lines.push("# Codebase Architecture Rules");
1101
+ lines.push(`> Project: ${projectInfo.name} | Stack: ${projectInfo.summary}`);
1102
+ lines.push("");
1103
+ lines.push("## Architectural Boundaries & Principles");
1104
+ lines.push("- Maintain strict separation of concerns across active architectural layers.");
1105
+ lines.push("- Keep Controllers thin: handle HTTP requests, invoke Services, and return responses.");
1106
+ lines.push("- Encapsulate all business rules, calculations, and mutations inside Services/Actions.");
1107
+ lines.push("- Isolate data storage and queries to Repositories or Model entities.");
1108
+ lines.push("- Validate all incoming user inputs with dedicated DTOs or Form Requests before reaching domain logic.");
1109
+ lines.push("");
1110
+ lines.push("## Project Structure & Layer Conventions");
1111
+ if (projectInfo.type === "laravel") {
1112
+ const isModular = Boolean(projectInfo.laravel?.isModular);
1113
+ const modDir = projectInfo.laravel?.modulesDir || "app/Modules";
1114
+ if (isModular) {
1115
+ lines.push(`- **Architecture Style**: Modular Domain Architecture`);
1116
+ lines.push(`- **Module Root**: \`${modDir}/<FeatureName>/\``);
1117
+ lines.push(`- **Controllers**: \`${modDir}/<FeatureName>/Controllers/\` (HTTP handlers)`);
1118
+ lines.push(`- **Services / Actions**: \`${modDir}/<FeatureName>/Services/\` or \`Actions/\` (Business logic)`);
1119
+ lines.push(`- **Models**: \`${modDir}/<FeatureName>/Models/\` (Eloquent entities)`);
1120
+ lines.push(`- **Validation**: \`${modDir}/<FeatureName>/Requests/\` (FormRequest classes)`);
1121
+ lines.push(`- **Routes**: \`${modDir}/<FeatureName>/routes.php\` or root \`routes/\``);
1122
+ } else {
1123
+ lines.push(`- **Architecture Style**: Standard Laravel Layered Architecture`);
1124
+ lines.push(`- **Controllers**: \`app/Http/Controllers/\``);
1125
+ lines.push(`- **Services / Actions**: \`app/Services/\` or \`app/Actions/\``);
1126
+ lines.push(`- **Models**: \`app/Models/\``);
1127
+ lines.push(`- **Validation**: \`app/Http/Requests/\``);
1128
+ lines.push(`- **Routes**: \`routes/web.php\`, \`routes/api.php\``);
1129
+ lines.push(`- **Migrations**: \`database/migrations/\``);
1130
+ }
1131
+ if (projectInfo.laravel?.hasFilament) {
1132
+ lines.push(`- **Admin Panel**: Filament Resources live in \`app/Filament/Resources/\``);
1133
+ }
1134
+ } else if (projectInfo.type === "typescript") {
1135
+ const isModular = Boolean(projectInfo.typescript?.isModular);
1136
+ const srcDir = projectInfo.typescript?.srcDir || "src";
1137
+ const orm = projectInfo.typescript?.hasPrisma ? "Prisma" : projectInfo.typescript?.hasTypeORM ? "TypeORM" : projectInfo.typescript?.hasDrizzle ? "Drizzle" : null;
1138
+ if (isModular) {
1139
+ lines.push(`- **Architecture Style**: Feature Module Architecture`);
1140
+ lines.push(`- **Module Root**: \`${srcDir}/modules/<feature>/\``);
1141
+ lines.push(`- **Controllers**: \`${srcDir}/modules/<feature>/<feature>.controller.ts\``);
1142
+ lines.push(`- **Services**: \`${srcDir}/modules/<feature>/<feature>.service.ts\``);
1143
+ lines.push(`- **Repositories**: \`${srcDir}/modules/<feature>/<feature>.repository.ts\``);
1144
+ lines.push(`- **DTOs / Schemas**: \`${srcDir}/modules/<feature>/<feature>.dto.ts\``);
1145
+ lines.push(`- **Routes**: \`${srcDir}/modules/<feature>/<feature>.routes.ts\``);
1146
+ lines.push(`- **Module Index**: \`${srcDir}/modules/<feature>/index.ts\` (barrel export)`);
1147
+ } else {
1148
+ lines.push(`- **Architecture Style**: Layered Architecture`);
1149
+ lines.push(`- **Controllers**: \`${srcDir}/controllers/\``);
1150
+ lines.push(`- **Services**: \`${srcDir}/services/\``);
1151
+ lines.push(`- **Repositories / Data**: \`${srcDir}/repositories/\``);
1152
+ lines.push(`- **DTOs / Schemas**: \`${srcDir}/dtos/\` or \`${srcDir}/schemas/\``);
1153
+ lines.push(`- **Routes**: \`${srcDir}/routes/\``);
1154
+ }
1155
+ if (orm) {
1156
+ lines.push(`- **Database ORM**: ${orm} is used for data persistence.`);
1157
+ }
1158
+ } else {
1159
+ lines.push(`- **Source Code**: \`src/\``);
1160
+ lines.push(`- **Automated Tests**: \`tests/\``);
1161
+ }
1162
+ if (modules.length > 0) {
1163
+ lines.push("");
1164
+ lines.push(`## Active Feature Modules (${modules.length})`);
1165
+ lines.push(`Existing domains: ${modules.map((m) => `\`${m}\``).join(", ")}`);
1166
+ lines.push("When creating new features, follow this established domain partition.");
1167
+ }
1168
+ lines.push("");
1169
+ lines.push("## AI Coding Guardrails");
1170
+ lines.push("1. NEVER place raw queries or database transactions inside Controller files.");
1171
+ lines.push("2. When implementing a new feature, always create cohesive layers: Route -> Controller -> Service -> Repository/Model.");
1172
+ lines.push("3. Keep naming conventions consistent with existing files in the repository.");
1173
+ lines.push("4. Write automated unit/feature tests for new domain services.");
1174
+ return lines.join("\n");
1175
+ }
1176
+ function mergeAiRules(existingContent, newRules) {
1177
+ const block = `${AI_RULES_START_TAG}
1178
+ ${newRules}
1179
+ ${AI_RULES_END_TAG}`;
1180
+ if (!existingContent || existingContent.trim() === "") {
1181
+ return block + "\n";
1182
+ }
1183
+ const regex = new RegExp(`${AI_RULES_START_TAG}[\\s\\S]*?${AI_RULES_END_TAG}`, "m");
1184
+ if (regex.test(existingContent)) {
1185
+ return existingContent.replace(regex, block);
1186
+ }
1187
+ return existingContent.trimEnd() + "\n\n" + block + "\n";
1188
+ }
1189
+
1190
+ // src/utils/clipboard.ts
1191
+ import { spawn } from "child_process";
1192
+ async function copyToClipboard(text2) {
1193
+ const platform = process.platform;
1194
+ let command;
1195
+ let args = [];
1196
+ if (platform === "win32") {
1197
+ command = "clip";
1198
+ } else if (platform === "darwin") {
1199
+ command = "pbcopy";
1200
+ } else {
1201
+ command = process.env.WAYLAND_DISPLAY ? "wl-copy" : "xclip";
1202
+ if (command === "xclip") {
1203
+ args = ["-selection", "clipboard"];
1204
+ }
1205
+ }
1206
+ return new Promise((resolve) => {
1207
+ try {
1208
+ const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
1209
+ child.on("error", () => {
1210
+ resolve(false);
1211
+ });
1212
+ child.on("close", (code) => {
1213
+ resolve(code === 0);
1214
+ });
1215
+ child.stdin.write(text2);
1216
+ child.stdin.end();
1217
+ } catch {
1218
+ resolve(false);
1219
+ }
1220
+ });
1221
+ }
911
1222
 
912
1223
  // src/generator/scaffolder.ts
913
1224
  import fs2 from "fs";
@@ -1850,7 +2161,7 @@ async function scaffoldModule(projectInfo, options) {
1850
2161
  }
1851
2162
 
1852
2163
  // src/index.ts
1853
- var VERSION = true ? "1.0.2" : "1.0.1";
2164
+ var VERSION = true ? "1.1.1" : "1.1.1";
1854
2165
  function showHelp() {
1855
2166
  console.log(`
1856
2167
  ${pc.bold(pc.cyan("LaraTS CLI"))} ${pc.gray(`v${VERSION}`)}
@@ -1862,19 +2173,26 @@ ${pc.bold("USAGE:")}
1862
2173
 
1863
2174
  ${pc.bold("COMMANDS:")}
1864
2175
  ${pc.green("scan")} Scan repository & generate ARCHITECTURE.md
2176
+ ${pc.green("view")}, ${pc.green("preview")} Open interactive architecture diagram in local browser
2177
+ ${pc.green("ai")} Export architecture context prompt for AI (Cursor, Claude, Copilot)
1865
2178
  ${pc.green("scaffold")}, ${pc.green("make")} [name] Scaffold a new module or feature with clean boilerplate
1866
2179
  ${pc.green("info")} Show detected project type & architectural flavor
1867
2180
  ${pc.green("help")} Display this help message
1868
2181
 
1869
2182
  ${pc.bold("OPTIONS:")}
1870
- ${pc.yellow("-o, --output")} <file> Output markdown path for scan (default: ARCHITECTURE.md)
2183
+ ${pc.yellow("-o, --output")} <file> Output path for scan (default: ARCHITECTURE.md)
2184
+ ${pc.yellow("--view")} Open local browser viewer immediately after scanning
2185
+ ${pc.yellow("--copy")} Copy AI architecture context prompt to clipboard
2186
+ ${pc.yellow("--cursor")} Inject architecture rules into .cursorrules
2187
+ ${pc.yellow("--claude")} Inject architecture rules into CLAUDE.md
2188
+ ${pc.yellow("-t, --tree")} Print directory tree in terminal
2189
+ ${pc.yellow("--html")} Also export standalone ARCHITECTURE.html
1871
2190
  ${pc.yellow("-m, --modular")} Enforce modular directory structure
1872
2191
  ${pc.yellow("-s, --standard")} Enforce standard/layered directory structure
1873
2192
  ${pc.yellow("--dry-run")} Simulate generation without writing files
1874
2193
  ${pc.yellow("-f, --force")} Overwrite files if they already exist
1875
2194
  ${pc.yellow("-v, --version")} Display CLI version
1876
2195
  ${pc.yellow("-h, --help")} Display this help message
1877
-
1878
2196
  ${pc.bold("EXAMPLES:")}
1879
2197
  ${pc.cyan("$")} larats
1880
2198
  ${pc.cyan("$")} larats scan
@@ -1887,6 +2205,113 @@ function handleCancel() {
1887
2205
  p.cancel(pc.yellow("Operation cancelled. See you next time! \u{1F44B}"));
1888
2206
  process.exit(0);
1889
2207
  }
2208
+ async function checkAndPromptStar(customStatsPath, forceAnswer) {
2209
+ try {
2210
+ const configDir = path4.join(os2.homedir(), ".larats");
2211
+ const statsFile = customStatsPath || path4.join(configDir, "stats.json");
2212
+ if (!fs3.existsSync(path4.dirname(statsFile))) {
2213
+ fs3.mkdirSync(path4.dirname(statsFile), { recursive: true });
2214
+ }
2215
+ let stats = {};
2216
+ if (fs3.existsSync(statsFile)) {
2217
+ stats = JSON.parse(fs3.readFileSync(statsFile, "utf-8"));
2218
+ }
2219
+ const runs = (stats.runs || 0) + 1;
2220
+ stats.runs = runs;
2221
+ if (runs >= 3 && !stats.hasPromptedStar) {
2222
+ stats.hasPromptedStar = true;
2223
+ const repoUrl = "https://github.com/SidiqFatuh187/larats-cli";
2224
+ if (process.stdin.isTTY || typeof forceAnswer === "boolean") {
2225
+ const shouldStar = typeof forceAnswer === "boolean" ? forceAnswer : await p.confirm({
2226
+ message: "\u2B50 Enjoying LaraTS? Would you like to support the project with a star on GitHub?",
2227
+ initialValue: true
2228
+ });
2229
+ if (shouldStar && !p.isCancel(shouldStar)) {
2230
+ openViewer(repoUrl);
2231
+ p.log.success(pc.green("Thank you so much for your support! \u2B50 Opening GitHub repository in your browser..."));
2232
+ } else {
2233
+ p.log.message(pc.gray("No problem, enjoy building with LaraTS! \u{1F44D}"));
2234
+ }
2235
+ } else {
2236
+ console.log(
2237
+ `
2238
+ ${pc.yellow("\u2B50 Enjoying LaraTS?")} If you find this CLI useful, please consider giving it a star on GitHub:
2239
+ ${pc.bold(pc.cyan("\u{1F449} " + repoUrl))}
2240
+ `
2241
+ );
2242
+ }
2243
+ }
2244
+ fs3.writeFileSync(statsFile, JSON.stringify(stats, null, 2), "utf-8");
2245
+ } catch {
2246
+ }
2247
+ }
2248
+ function openViewer(destination) {
2249
+ const startCmd = process.platform === "win32" ? `start "" "${destination}"` : process.platform === "darwin" ? `open "${destination}"` : `xdg-open "${destination}"`;
2250
+ try {
2251
+ exec(startCmd, (err) => {
2252
+ if (err) {
2253
+ p.log.warn(`Could not open browser automatically. Please open ${pc.cyan(destination)} manually.`);
2254
+ }
2255
+ });
2256
+ } catch {
2257
+ p.log.warn(`Could not open browser automatically. Please open ${pc.cyan(destination)} manually.`);
2258
+ }
2259
+ }
2260
+ async function runAi(projectInfo, options = {}) {
2261
+ const spinner2 = p.spinner();
2262
+ spinner2.start("Scanning codebase & formulating AI architecture context...");
2263
+ try {
2264
+ const scanResult = await scanProject(projectInfo);
2265
+ spinner2.stop(pc.green(`Architecture context ready (${scanResult.scanDurationMs}ms)`));
2266
+ const aiContext = generateAiContext(scanResult);
2267
+ let handled = false;
2268
+ if (options.cursor) {
2269
+ const cursorPath = path4.resolve(projectInfo.rootPath, ".cursorrules");
2270
+ const existing = fs3.existsSync(cursorPath) ? fs3.readFileSync(cursorPath, "utf-8") : "";
2271
+ const updated = mergeAiRules(existing, aiContext);
2272
+ fs3.writeFileSync(cursorPath, updated, "utf-8");
2273
+ p.log.success(pc.green(`\u2714 Architecture rules injected into ${pc.bold(".cursorrules")}`));
2274
+ handled = true;
2275
+ }
2276
+ if (options.claude) {
2277
+ const claudePath = path4.resolve(projectInfo.rootPath, "CLAUDE.md");
2278
+ const existing = fs3.existsSync(claudePath) ? fs3.readFileSync(claudePath, "utf-8") : "";
2279
+ const updated = mergeAiRules(existing, aiContext);
2280
+ fs3.writeFileSync(claudePath, updated, "utf-8");
2281
+ p.log.success(pc.green(`\u2714 Architecture rules injected into ${pc.bold("CLAUDE.md")}`));
2282
+ handled = true;
2283
+ }
2284
+ if (options.copy) {
2285
+ const copied = await copyToClipboard(aiContext);
2286
+ if (copied) {
2287
+ p.log.success(pc.green("\u2714 Architecture context copied to clipboard! Ready to paste into ChatGPT or Claude."));
2288
+ } else {
2289
+ p.log.warn("Could not access system clipboard. Printing prompt to console instead.");
2290
+ console.log(`
2291
+ ${aiContext}
2292
+ `);
2293
+ }
2294
+ handled = true;
2295
+ }
2296
+ if (!handled) {
2297
+ console.log(`
2298
+ ${pc.bold(pc.cyan("--- AI Codebase Architecture Prompt ---"))}
2299
+ ${aiContext}
2300
+ ${pc.bold(pc.cyan("---------------------------------------"))}
2301
+ `);
2302
+ p.note(
2303
+ `Tip: Run with ${pc.yellow("--copy")} to copy directly to clipboard,
2304
+ or ${pc.yellow("--cursor")} / ${pc.yellow("--claude")} to inject into rules files automatically!`,
2305
+ pc.green("Prompt Ready for AI \u{1F916}")
2306
+ );
2307
+ }
2308
+ p.outro(pc.cyan("Done!"));
2309
+ } catch (err) {
2310
+ spinner2.stop(pc.red("Failed to generate AI context"));
2311
+ p.log.error(err instanceof Error ? err.message : String(err));
2312
+ process.exitCode = 1;
2313
+ }
2314
+ }
1890
2315
  async function runScan(projectInfo, outputPath = "ARCHITECTURE.md", options = {}) {
1891
2316
  const spinner2 = p.spinner();
1892
2317
  spinner2.start("Scanning codebase structure & components (strict ignore list active)...");
@@ -1915,15 +2340,25 @@ To protect user files from accidental overwrites, use --force to overwrite.`
1915
2340
  const scanResult = await scanProject(projectInfo);
1916
2341
  const nonZeroLayers = Object.values(scanResult.components).filter((c) => c.count > 0).length;
1917
2342
  const totalComponents = Object.values(scanResult.components).reduce((sum, c) => sum + c.count, 0);
2343
+ const terminalFlow = renderTerminalFlow(scanResult);
1918
2344
  if (options.dryRun) {
1919
2345
  spinner2.stop(pc.green(`Scan completed in ${scanResult.scanDurationMs}ms (dry run)`));
1920
2346
  p.note(
1921
- `${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
2347
+ `${pc.bold("Architecture Flow:")}
2348
+ ${pc.cyan(terminalFlow)}
2349
+
2350
+ ${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
1922
2351
  ${pc.bold("Total Components:")} ${totalComponents} across ${nonZeroLayers} active architectural layers
1923
2352
  ${pc.bold("Modules Detected:")} ${scanResult.modules.length > 0 ? scanResult.modules.join(", ") : "None (monolithic structure)"}
1924
2353
  ${pc.bold("Simulated Output:")} Would write ARCHITECTURE.md to ${pc.cyan(outputPath)} (dry run: no files written)`,
1925
2354
  pc.green("Architecture Map (Dry Run) \u{1F5FA}\uFE0F")
1926
2355
  );
2356
+ if (options.tree) {
2357
+ console.log(`
2358
+ ${pc.bold("Directory Tree:")}
2359
+ ${scanResult.directoryTree}
2360
+ `);
2361
+ }
1927
2362
  p.outro(pc.cyan("Dry run completed: no files written."));
1928
2363
  return;
1929
2364
  }
@@ -1934,14 +2369,38 @@ ${pc.bold("Simulated Output:")} Would write ARCHITECTURE.md to ${pc.cyan(outputP
1934
2369
  fs3.mkdirSync(parentDir, { recursive: true });
1935
2370
  }
1936
2371
  fs3.writeFileSync(destination, markdown, "utf-8");
2372
+ const shouldWriteHtml = options.html || options.view;
2373
+ if (shouldWriteHtml) {
2374
+ const htmlDestination = destination.replace(/\.md$/i, "") + ".html";
2375
+ fs3.writeFileSync(htmlDestination, generateArchitectureHtml(scanResult), "utf-8");
2376
+ p.log.success(`HTML architecture saved to ${pc.cyan(path4.relative(projectInfo.rootPath, htmlDestination) || htmlDestination)}`);
2377
+ if (options.view) {
2378
+ openViewer(htmlDestination);
2379
+ p.log.info(pc.green("\u{1F310} Opened interactive architecture diagram in your browser!"));
2380
+ }
2381
+ }
2382
+ if (options.tree) {
2383
+ console.log(`
2384
+ ${pc.bold("Directory Tree:")}
2385
+ ${scanResult.directoryTree}
2386
+ `);
2387
+ }
1937
2388
  p.note(
1938
- `${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
2389
+ `${pc.bold("Architecture Flow:")}
2390
+ ${pc.cyan(terminalFlow)}
2391
+
2392
+ ${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
1939
2393
  ${pc.bold("Total Components:")} ${totalComponents} across ${nonZeroLayers} active architectural layers
1940
2394
  ${pc.bold("Modules Detected:")} ${scanResult.modules.length > 0 ? scanResult.modules.join(", ") : "None (monolithic structure)"}
1941
- ${pc.bold("Documentation:")} Saved to ${pc.cyan(path4.relative(projectInfo.rootPath, destination) || outputPath)}`,
2395
+ ${pc.bold("Documentation:")} Saved to ${pc.cyan(path4.relative(projectInfo.rootPath, destination) || outputPath)}
2396
+
2397
+ ${pc.bold("\u{1F4A1} How to view diagram:")}
2398
+ \u2022 On GitHub: Push ${outputPath} (Mermaid renders automatically)
2399
+ \u2022 In VS Code: Press ${pc.yellow("Ctrl+Shift+V")} for Markdown Preview
2400
+ \u2022 In Browser: Run ${pc.cyan("npx larats view")} for instant local web preview`,
1942
2401
  pc.green("Architecture Map Generated! \u{1F5FA}\uFE0F")
1943
2402
  );
1944
- p.outro(pc.cyan(`Open ${outputPath} to view your project structure & Mermaid diagram!`));
2403
+ p.outro(pc.cyan(`Done! Open ${outputPath} or run "npx larats view" to inspect architecture!`));
1945
2404
  } catch (err) {
1946
2405
  spinner2.stop(pc.red("Scan failed"));
1947
2406
  p.log.error(err.message || String(err));
@@ -2172,14 +2631,20 @@ async function main() {
2172
2631
  standard: { type: "boolean", short: "s" },
2173
2632
  force: { type: "boolean", short: "f" },
2174
2633
  "dry-run": { type: "boolean" },
2634
+ view: { type: "boolean" },
2635
+ tree: { type: "boolean", short: "t" },
2636
+ html: { type: "boolean" },
2175
2637
  version: { type: "boolean", short: "v" },
2638
+ copy: { type: "boolean" },
2639
+ cursor: { type: "boolean" },
2640
+ claude: { type: "boolean" },
2176
2641
  help: { type: "boolean", short: "h" }
2177
2642
  },
2178
2643
  allowPositionals: true,
2179
2644
  strict: true
2180
2645
  });
2181
2646
  } catch (err) {
2182
- console.error(pc.red(`Error: ${err.message}`));
2647
+ console.error(pc.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
2183
2648
  showHelp();
2184
2649
  process.exitCode = 1;
2185
2650
  return;
@@ -2193,8 +2658,17 @@ async function main() {
2193
2658
  return;
2194
2659
  }
2195
2660
  const command = parsed.positionals[0];
2196
- const VALID_COMMANDS = /* @__PURE__ */ new Set(["scan", "scaffold", "make", "info", "help"]);
2197
- if (command && !VALID_COMMANDS.has(command)) {
2661
+ const VALID_COMMANDS = {
2662
+ scan: true,
2663
+ view: true,
2664
+ preview: true,
2665
+ scaffold: true,
2666
+ make: true,
2667
+ info: true,
2668
+ ai: true,
2669
+ help: true
2670
+ };
2671
+ if (command && !VALID_COMMANDS[command]) {
2198
2672
  console.error(pc.red(`Error: Unknown command "${command}".`));
2199
2673
  showHelp();
2200
2674
  process.exitCode = 1;
@@ -2205,14 +2679,52 @@ async function main() {
2205
2679
  const isStandardFlag = parsed.values.standard;
2206
2680
  const isForceFlag = parsed.values.force;
2207
2681
  const isDryRunFlag = parsed.values["dry-run"];
2682
+ const isViewFlag = parsed.values.view;
2683
+ const isTreeFlag = parsed.values.tree;
2684
+ const isHtmlFlag = parsed.values.html;
2208
2685
  const projectInfo = await detectProjectType(process.cwd());
2209
2686
  if (command === "scan") {
2210
2687
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - Scanning Architecture`);
2211
2688
  p.log.info(`Detected: ${pc.green(projectInfo.summary)}`);
2212
2689
  await runScan(projectInfo, outputPath, {
2213
2690
  force: isForceFlag,
2214
- dryRun: isDryRunFlag
2691
+ dryRun: isDryRunFlag,
2692
+ view: isViewFlag,
2693
+ tree: isTreeFlag,
2694
+ html: isHtmlFlag
2695
+ });
2696
+ await checkAndPromptStar();
2697
+ return;
2698
+ }
2699
+ if (command === "view" || command === "preview") {
2700
+ p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - Architecture Viewer`);
2701
+ p.log.info(`Target: ${pc.green(projectInfo.summary)}`);
2702
+ const spinner2 = p.spinner();
2703
+ spinner2.start("Scanning repository & generating architecture map...");
2704
+ const scanResult = await scanProject(projectInfo);
2705
+ spinner2.stop(pc.green(`Scanned ${scanResult.totalFilesScanned} files in ${scanResult.scanDurationMs}ms`));
2706
+ const htmlDestination = path4.resolve(projectInfo.rootPath, "ARCHITECTURE.html");
2707
+ fs3.writeFileSync(htmlDestination, generateArchitectureHtml(scanResult), "utf-8");
2708
+ openViewer(htmlDestination);
2709
+ p.note(
2710
+ `${pc.bold("File:")} ${pc.cyan("ARCHITECTURE.html")}
2711
+ Interactive Mermaid.js flowchart & component breakdown loaded in your browser.
2712
+ Double-click ${pc.cyan("ARCHITECTURE.html")} anytime to view offline!`,
2713
+ pc.green("Opened in Browser! \u{1F310}")
2714
+ );
2715
+ p.outro(pc.cyan("Done!"));
2716
+ await checkAndPromptStar();
2717
+ return;
2718
+ }
2719
+ if (command === "ai") {
2720
+ p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - AI Architecture Context`);
2721
+ p.log.info(`Target: ${pc.green(projectInfo.summary)}`);
2722
+ await runAi(projectInfo, {
2723
+ copy: parsed.values.copy,
2724
+ cursor: parsed.values.cursor,
2725
+ claude: parsed.values.claude
2215
2726
  });
2727
+ await checkAndPromptStar();
2216
2728
  return;
2217
2729
  }
2218
2730
  if (command === "scaffold" || command === "make") {
@@ -2225,12 +2737,14 @@ async function main() {
2225
2737
  force: isForceFlag,
2226
2738
  dryRun: isDryRunFlag
2227
2739
  });
2740
+ await checkAndPromptStar();
2228
2741
  return;
2229
2742
  }
2230
2743
  if (command === "info") {
2231
2744
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)}`);
2232
2745
  showProjectInfo(projectInfo);
2233
2746
  p.outro(pc.cyan("Done."));
2747
+ await checkAndPromptStar();
2234
2748
  return;
2235
2749
  }
2236
2750
  if (!process.stdin.isTTY) {
@@ -2248,6 +2762,16 @@ async function main() {
2248
2762
  label: "\u{1F50D} Scan Architecture & Generate ARCHITECTURE.md",
2249
2763
  hint: "Creates component stats, directory tree & Mermaid flowchart"
2250
2764
  },
2765
+ {
2766
+ value: "ai",
2767
+ label: "\u{1F916} Export Architecture Context for AI (Cursor, Claude, Copilot)",
2768
+ hint: "Generate architecture rules for AI coding agents"
2769
+ },
2770
+ {
2771
+ value: "view",
2772
+ label: "\u{1F310} Open Interactive Architecture Viewer in Browser",
2773
+ hint: "Spawns local browser tab with Mermaid flowchart & stats"
2774
+ },
2251
2775
  {
2252
2776
  value: "scaffold",
2253
2777
  label: "\u{1F3D7}\uFE0F Scaffold New Feature / Module",
@@ -2270,8 +2794,38 @@ async function main() {
2270
2794
  if (action === "scan") {
2271
2795
  await runScan(projectInfo, outputPath, {
2272
2796
  force: isForceFlag,
2273
- dryRun: isDryRunFlag
2797
+ dryRun: isDryRunFlag,
2798
+ view: isViewFlag,
2799
+ tree: isTreeFlag,
2800
+ html: isHtmlFlag
2274
2801
  });
2802
+ } else if (action === "ai") {
2803
+ const aiTarget = await p.select({
2804
+ message: "How would you like to export the AI architecture context?",
2805
+ options: [
2806
+ { value: "copy", label: "\u{1F4CB} Copy directly to Clipboard", hint: "Ready to paste into ChatGPT or Claude web" },
2807
+ { value: "cursor", label: "\u{1F5B1}\uFE0F Save into .cursorrules", hint: "Inject rules for Cursor AI" },
2808
+ { value: "claude", label: "\u{1F9E0} Save into CLAUDE.md", hint: "Inject rules for Claude Code" },
2809
+ { value: "print", label: "\u{1F5A5}\uFE0F Display prompt in terminal", hint: "View formatted markdown text" }
2810
+ ]
2811
+ });
2812
+ if (p.isCancel(aiTarget)) {
2813
+ handleCancel();
2814
+ }
2815
+ await runAi(projectInfo, {
2816
+ copy: aiTarget === "copy",
2817
+ cursor: aiTarget === "cursor",
2818
+ claude: aiTarget === "claude"
2819
+ });
2820
+ } else if (action === "view") {
2821
+ const spinner2 = p.spinner();
2822
+ spinner2.start("Scanning repository & generating architecture map...");
2823
+ const scanResult = await scanProject(projectInfo);
2824
+ spinner2.stop(pc.green(`Scanned in ${scanResult.scanDurationMs}ms`));
2825
+ const htmlDestination = path4.resolve(projectInfo.rootPath, "ARCHITECTURE.html");
2826
+ fs3.writeFileSync(htmlDestination, generateArchitectureHtml(scanResult), "utf-8");
2827
+ openViewer(htmlDestination);
2828
+ p.outro(pc.green("\u{1F310} Opened interactive architecture diagram in your browser!"));
2275
2829
  } else if (action === "scaffold") {
2276
2830
  await runScaffold(projectInfo, void 0, {
2277
2831
  modular: isModularFlag,
@@ -2283,9 +2837,13 @@ async function main() {
2283
2837
  showProjectInfo(projectInfo);
2284
2838
  p.outro(pc.cyan("Done!"));
2285
2839
  }
2840
+ await checkAndPromptStar();
2286
2841
  }
2287
2842
  main().catch((err) => {
2288
2843
  p.log.error(pc.red(`Fatal error: ${err.message || String(err)}`));
2289
2844
  process.exit(1);
2290
2845
  });
2846
+ export {
2847
+ checkAndPromptStar
2848
+ };
2291
2849
  //# sourceMappingURL=index.js.map