@navieo/cli 1.0.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/README.md +66 -0
- package/dist/index.js +880 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @navieo/cli
|
|
2
|
+
|
|
3
|
+
CLI tool for Navieo — scan your codebase, generate sitemaps, and sync with the Navieo backend.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @navieo/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @navieo/cli <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `navieo init`
|
|
20
|
+
|
|
21
|
+
Scan your codebase and generate an initial sitemap with a docs folder.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
navieo init [--dir <path>]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Detects your framework (Next.js App Router, Pages Router, Vite)
|
|
28
|
+
- Scans routes and extracts interactive elements
|
|
29
|
+
- Creates `navieo/sitemap.navieo.json` and `navieo/docs/`
|
|
30
|
+
|
|
31
|
+
### `navieo generate`
|
|
32
|
+
|
|
33
|
+
Re-scan the codebase and update the existing sitemap, merging new routes with existing data.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
navieo generate [--dir <path>]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `navieo sync`
|
|
40
|
+
|
|
41
|
+
Upload your sitemap and documentation to the Navieo backend.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
navieo sync [--dir <path>] [--key <apiKey>] [--endpoint <url>]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Create a `.navieorc` file in your project root:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"apiKey": "your-api-key",
|
|
54
|
+
"endpoint": "https://api.navieo.io"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or set the `NAVIEO_API_KEY` environment variable.
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
- Node.js >= 18
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/init.ts
|
|
30
|
+
var import_fs5 = __toESM(require("fs"));
|
|
31
|
+
var import_path4 = __toESM(require("path"));
|
|
32
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
33
|
+
|
|
34
|
+
// src/scanners/framework.ts
|
|
35
|
+
var import_fs = __toESM(require("fs"));
|
|
36
|
+
var import_path = __toESM(require("path"));
|
|
37
|
+
function detectFramework(projectRoot) {
|
|
38
|
+
const nextConfigExists = ["next.config.js", "next.config.mjs", "next.config.ts"].some(
|
|
39
|
+
(f) => import_fs.default.existsSync(import_path.default.join(projectRoot, f))
|
|
40
|
+
);
|
|
41
|
+
if (nextConfigExists) {
|
|
42
|
+
const hasAppDir = import_fs.default.existsSync(import_path.default.join(projectRoot, "app"));
|
|
43
|
+
const hasPagesDir = import_fs.default.existsSync(import_path.default.join(projectRoot, "pages"));
|
|
44
|
+
const hasSrcAppDir = import_fs.default.existsSync(import_path.default.join(projectRoot, "src", "app"));
|
|
45
|
+
const hasSrcPagesDir = import_fs.default.existsSync(import_path.default.join(projectRoot, "src", "pages"));
|
|
46
|
+
if (hasAppDir || hasSrcAppDir) {
|
|
47
|
+
return {
|
|
48
|
+
name: "nextjs-app",
|
|
49
|
+
appDir: hasSrcAppDir ? "src/app" : "app",
|
|
50
|
+
pagesDir: null,
|
|
51
|
+
componentsDir: resolveComponentsDir(projectRoot)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (hasPagesDir || hasSrcPagesDir) {
|
|
55
|
+
return {
|
|
56
|
+
name: "nextjs-pages",
|
|
57
|
+
appDir: null,
|
|
58
|
+
pagesDir: hasSrcPagesDir ? "src/pages" : "pages",
|
|
59
|
+
componentsDir: resolveComponentsDir(projectRoot)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
name: "nextjs-app",
|
|
64
|
+
appDir: "app",
|
|
65
|
+
pagesDir: null,
|
|
66
|
+
componentsDir: resolveComponentsDir(projectRoot)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const viteConfigExists = ["vite.config.ts", "vite.config.js"].some(
|
|
70
|
+
(f) => import_fs.default.existsSync(import_path.default.join(projectRoot, f))
|
|
71
|
+
);
|
|
72
|
+
if (viteConfigExists) {
|
|
73
|
+
return {
|
|
74
|
+
name: "vite",
|
|
75
|
+
appDir: null,
|
|
76
|
+
pagesDir: null,
|
|
77
|
+
componentsDir: resolveComponentsDir(projectRoot)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name: "unknown",
|
|
82
|
+
appDir: null,
|
|
83
|
+
pagesDir: null,
|
|
84
|
+
componentsDir: resolveComponentsDir(projectRoot)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function resolveComponentsDir(projectRoot) {
|
|
88
|
+
const candidates = [
|
|
89
|
+
"components",
|
|
90
|
+
"src/components",
|
|
91
|
+
"app/components",
|
|
92
|
+
"src/app/components"
|
|
93
|
+
];
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
if (import_fs.default.existsSync(import_path.default.join(projectRoot, candidate))) {
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/scanners/routes.ts
|
|
103
|
+
var import_fs2 = __toESM(require("fs"));
|
|
104
|
+
var import_path2 = __toESM(require("path"));
|
|
105
|
+
var import_glob = require("glob");
|
|
106
|
+
var import_parser = require("@babel/parser");
|
|
107
|
+
var import_traverse = __toESM(require("@babel/traverse"));
|
|
108
|
+
function scanRoutes(projectRoot, framework) {
|
|
109
|
+
if (framework.name === "nextjs-app" && framework.appDir) {
|
|
110
|
+
return scanNextjsAppRouter(projectRoot, framework.appDir);
|
|
111
|
+
}
|
|
112
|
+
if (framework.name === "nextjs-pages" && framework.pagesDir) {
|
|
113
|
+
return scanNextjsPagesRouter(projectRoot, framework.pagesDir);
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
function scanNextjsAppRouter(projectRoot, appDir) {
|
|
118
|
+
const absAppDir = import_path2.default.join(projectRoot, appDir);
|
|
119
|
+
const pageFiles = import_glob.glob.sync("**/page.{tsx,jsx,ts,js}", {
|
|
120
|
+
cwd: absAppDir,
|
|
121
|
+
ignore: ["**/node_modules/**"]
|
|
122
|
+
});
|
|
123
|
+
const routes = [];
|
|
124
|
+
const tsconfigPaths = loadTsconfigPaths(projectRoot);
|
|
125
|
+
for (const pageFile of pageFiles) {
|
|
126
|
+
const fullPagePath = import_path2.default.join(absAppDir, pageFile);
|
|
127
|
+
const routePath = filePathToRoute(pageFile);
|
|
128
|
+
if (routePath.startsWith("/api")) continue;
|
|
129
|
+
let code;
|
|
130
|
+
try {
|
|
131
|
+
code = import_fs2.default.readFileSync(fullPagePath, "utf-8");
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (isRedirectOnly(code)) continue;
|
|
136
|
+
const title = extractTitle(code, routePath);
|
|
137
|
+
const importedFiles = resolveImports(code, fullPagePath, projectRoot, tsconfigPaths);
|
|
138
|
+
routes.push({
|
|
139
|
+
path: routePath,
|
|
140
|
+
filePath: fullPagePath,
|
|
141
|
+
title,
|
|
142
|
+
description: `${title} page`,
|
|
143
|
+
importedFiles
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
147
|
+
}
|
|
148
|
+
function scanNextjsPagesRouter(projectRoot, pagesDir) {
|
|
149
|
+
const absPagesDir = import_path2.default.join(projectRoot, pagesDir);
|
|
150
|
+
const pageFiles = import_glob.glob.sync("**/*.{tsx,jsx,ts,js}", {
|
|
151
|
+
cwd: absPagesDir,
|
|
152
|
+
ignore: [
|
|
153
|
+
"**/node_modules/**",
|
|
154
|
+
"_app.*",
|
|
155
|
+
"_document.*",
|
|
156
|
+
"_error.*",
|
|
157
|
+
"api/**"
|
|
158
|
+
]
|
|
159
|
+
});
|
|
160
|
+
const routes = [];
|
|
161
|
+
const tsconfigPaths = loadTsconfigPaths(projectRoot);
|
|
162
|
+
for (const pageFile of pageFiles) {
|
|
163
|
+
const fullPagePath = import_path2.default.join(absPagesDir, pageFile);
|
|
164
|
+
const routePath = pagesFilePathToRoute(pageFile);
|
|
165
|
+
let code;
|
|
166
|
+
try {
|
|
167
|
+
code = import_fs2.default.readFileSync(fullPagePath, "utf-8");
|
|
168
|
+
} catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const title = extractTitle(code, routePath);
|
|
172
|
+
const importedFiles = resolveImports(code, fullPagePath, projectRoot, tsconfigPaths);
|
|
173
|
+
routes.push({
|
|
174
|
+
path: routePath,
|
|
175
|
+
filePath: fullPagePath,
|
|
176
|
+
title,
|
|
177
|
+
description: `${title} page`,
|
|
178
|
+
importedFiles
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
182
|
+
}
|
|
183
|
+
function filePathToRoute(filePath) {
|
|
184
|
+
let route = filePath.replace(/\/page\.(tsx|jsx|ts|js)$/, "").replace(/page\.(tsx|jsx|ts|js)$/, "");
|
|
185
|
+
route = route.replace(/\([^)]+\)\/?/g, "");
|
|
186
|
+
route = "/" + route.replace(/\/$/, "");
|
|
187
|
+
if (route === "/") return "/";
|
|
188
|
+
return route;
|
|
189
|
+
}
|
|
190
|
+
function pagesFilePathToRoute(filePath) {
|
|
191
|
+
let route = filePath.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
192
|
+
route = route.replace(/\/index$/, "").replace(/^index$/, "");
|
|
193
|
+
return "/" + route || "/";
|
|
194
|
+
}
|
|
195
|
+
function isRedirectOnly(code) {
|
|
196
|
+
const hasRedirectImport = /import\s+.*redirect.*from\s+['"]next\/navigation['"]/.test(code);
|
|
197
|
+
const hasRedirectCall = /\bredirect\s*\(/.test(code);
|
|
198
|
+
const hasMinimalJSX = (code.match(/<[A-Za-z]/g) || []).length <= 1;
|
|
199
|
+
return hasRedirectImport && hasRedirectCall && hasMinimalJSX;
|
|
200
|
+
}
|
|
201
|
+
function extractTitle(code, routePath) {
|
|
202
|
+
let ast;
|
|
203
|
+
try {
|
|
204
|
+
ast = (0, import_parser.parse)(code, {
|
|
205
|
+
sourceType: "module",
|
|
206
|
+
plugins: ["jsx", "typescript"],
|
|
207
|
+
errorRecovery: true
|
|
208
|
+
});
|
|
209
|
+
} catch {
|
|
210
|
+
return titleFromPath(routePath);
|
|
211
|
+
}
|
|
212
|
+
let foundTitle = null;
|
|
213
|
+
(0, import_traverse.default)(ast, {
|
|
214
|
+
// Look for: export const metadata = { title: "..." }
|
|
215
|
+
ExportNamedDeclaration(path7) {
|
|
216
|
+
const decl = path7.node.declaration;
|
|
217
|
+
if (decl?.type === "VariableDeclaration") {
|
|
218
|
+
for (const declarator of decl.declarations) {
|
|
219
|
+
if (declarator.id?.type === "Identifier" && declarator.id.name === "metadata" && declarator.init?.type === "ObjectExpression") {
|
|
220
|
+
for (const prop of declarator.init.properties) {
|
|
221
|
+
if (prop.type === "ObjectProperty" && prop.key?.type === "Identifier" && prop.key.name === "title" && prop.value?.type === "StringLiteral") {
|
|
222
|
+
foundTitle = prop.value.value;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
// Look for: <h1>Title Text</h1> or <h2>Title Text</h2>
|
|
230
|
+
JSXOpeningElement(path7) {
|
|
231
|
+
if (foundTitle) return;
|
|
232
|
+
const node = path7.node;
|
|
233
|
+
if (node.name?.type !== "JSXIdentifier") return;
|
|
234
|
+
if (node.name.name !== "h1" && node.name.name !== "h2") return;
|
|
235
|
+
const parent = path7.parent;
|
|
236
|
+
if (parent?.type !== "JSXElement") return;
|
|
237
|
+
const textParts = [];
|
|
238
|
+
for (const child of parent.children || []) {
|
|
239
|
+
if (child.type === "JSXText") {
|
|
240
|
+
const trimmed = child.value.trim();
|
|
241
|
+
if (trimmed) textParts.push(trimmed);
|
|
242
|
+
}
|
|
243
|
+
if (child.type === "JSXExpressionContainer" && child.expression?.type === "StringLiteral") {
|
|
244
|
+
textParts.push(child.expression.value);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (textParts.length > 0 && node.name.name === "h1") {
|
|
248
|
+
foundTitle = textParts.join(" ");
|
|
249
|
+
} else if (textParts.length > 0 && !foundTitle) {
|
|
250
|
+
foundTitle = textParts.join(" ");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return foundTitle || titleFromPath(routePath);
|
|
255
|
+
}
|
|
256
|
+
function titleFromPath(routePath) {
|
|
257
|
+
if (routePath === "/") return "Home";
|
|
258
|
+
const segments = routePath.split("/").filter(Boolean);
|
|
259
|
+
const last = segments[segments.length - 1];
|
|
260
|
+
if (last.startsWith("[") && last.endsWith("]")) {
|
|
261
|
+
const paramName = last.slice(1, -1);
|
|
262
|
+
return capitalize(paramName) + " Detail";
|
|
263
|
+
}
|
|
264
|
+
return capitalize(last);
|
|
265
|
+
}
|
|
266
|
+
function capitalize(s) {
|
|
267
|
+
return s.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
268
|
+
}
|
|
269
|
+
function resolveImports(code, sourceFilePath, projectRoot, tsconfigPaths) {
|
|
270
|
+
let ast;
|
|
271
|
+
try {
|
|
272
|
+
ast = (0, import_parser.parse)(code, {
|
|
273
|
+
sourceType: "module",
|
|
274
|
+
plugins: ["jsx", "typescript"],
|
|
275
|
+
errorRecovery: true
|
|
276
|
+
});
|
|
277
|
+
} catch {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
const imports = [];
|
|
281
|
+
(0, import_traverse.default)(ast, {
|
|
282
|
+
ImportDeclaration(path7) {
|
|
283
|
+
const source = path7.node.source.value;
|
|
284
|
+
if (!source.startsWith(".") && !source.startsWith("@/") && !source.startsWith("~/")) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const resolved = resolveImportPath(source, sourceFilePath, projectRoot, tsconfigPaths);
|
|
288
|
+
if (resolved && import_fs2.default.existsSync(resolved)) {
|
|
289
|
+
imports.push(resolved);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
return imports;
|
|
294
|
+
}
|
|
295
|
+
function resolveImportPath(importSource, sourceFilePath, projectRoot, tsconfigPaths) {
|
|
296
|
+
let resolvedBase;
|
|
297
|
+
if (importSource.startsWith("@/") || importSource.startsWith("~/")) {
|
|
298
|
+
const prefix = importSource.startsWith("@/") ? "@/*" : "~/*";
|
|
299
|
+
const mappings = tsconfigPaths[prefix];
|
|
300
|
+
if (!mappings || mappings.length === 0) {
|
|
301
|
+
resolvedBase = import_path2.default.join(projectRoot, importSource.slice(2));
|
|
302
|
+
} else {
|
|
303
|
+
const mappingBase = mappings[0].replace("*", "");
|
|
304
|
+
resolvedBase = import_path2.default.join(projectRoot, mappingBase, importSource.slice(2));
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
resolvedBase = import_path2.default.resolve(import_path2.default.dirname(sourceFilePath), importSource);
|
|
308
|
+
}
|
|
309
|
+
const extensions = [".tsx", ".ts", ".jsx", ".js"];
|
|
310
|
+
for (const ext of extensions) {
|
|
311
|
+
const candidate = resolvedBase + ext;
|
|
312
|
+
if (import_fs2.default.existsSync(candidate)) return candidate;
|
|
313
|
+
}
|
|
314
|
+
for (const ext of extensions) {
|
|
315
|
+
const candidate = import_path2.default.join(resolvedBase, "index" + ext);
|
|
316
|
+
if (import_fs2.default.existsSync(candidate)) return candidate;
|
|
317
|
+
}
|
|
318
|
+
if (import_fs2.default.existsSync(resolvedBase)) return resolvedBase;
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
function loadTsconfigPaths(projectRoot) {
|
|
322
|
+
const tsconfigPath = import_path2.default.join(projectRoot, "tsconfig.json");
|
|
323
|
+
if (!import_fs2.default.existsSync(tsconfigPath)) return {};
|
|
324
|
+
try {
|
|
325
|
+
const raw = import_fs2.default.readFileSync(tsconfigPath, "utf-8");
|
|
326
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
327
|
+
const config = JSON.parse(stripped);
|
|
328
|
+
return config.compilerOptions?.paths || {};
|
|
329
|
+
} catch {
|
|
330
|
+
return {};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/scanners/elements.ts
|
|
335
|
+
var import_fs3 = __toESM(require("fs"));
|
|
336
|
+
var import_parser2 = require("@babel/parser");
|
|
337
|
+
var import_traverse2 = __toESM(require("@babel/traverse"));
|
|
338
|
+
function scanElements(filePaths) {
|
|
339
|
+
const elementsMap = /* @__PURE__ */ new Map();
|
|
340
|
+
for (const filePath of filePaths) {
|
|
341
|
+
if (!import_fs3.default.existsSync(filePath)) continue;
|
|
342
|
+
let code;
|
|
343
|
+
try {
|
|
344
|
+
code = import_fs3.default.readFileSync(filePath, "utf-8");
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const fileElements = extractElementsFromCode(code);
|
|
349
|
+
for (const el of fileElements) {
|
|
350
|
+
const existing = elementsMap.get(el.id);
|
|
351
|
+
if (!existing || selectorScore(el.selector) > selectorScore(existing.selector)) {
|
|
352
|
+
elementsMap.set(el.id, el);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return Array.from(elementsMap.values());
|
|
357
|
+
}
|
|
358
|
+
function extractElementsFromCode(code) {
|
|
359
|
+
const elements = [];
|
|
360
|
+
let ast;
|
|
361
|
+
try {
|
|
362
|
+
ast = (0, import_parser2.parse)(code, {
|
|
363
|
+
sourceType: "module",
|
|
364
|
+
plugins: ["jsx", "typescript"],
|
|
365
|
+
errorRecovery: true
|
|
366
|
+
});
|
|
367
|
+
} catch {
|
|
368
|
+
return elements;
|
|
369
|
+
}
|
|
370
|
+
(0, import_traverse2.default)(ast, {
|
|
371
|
+
JSXOpeningElement(path7) {
|
|
372
|
+
const node = path7.node;
|
|
373
|
+
const attrs = node.attributes;
|
|
374
|
+
const tag = getTagName(node);
|
|
375
|
+
if (!tag) return;
|
|
376
|
+
const testId = getStaticPropValue(attrs, "data-testid");
|
|
377
|
+
if (!testId) return;
|
|
378
|
+
const ariaLabel = getStaticPropValue(attrs, "aria-label");
|
|
379
|
+
const elType = mapTagToType(tag);
|
|
380
|
+
const text = ariaLabel || humanize(testId);
|
|
381
|
+
const cssSelector = buildCssSelector(tag, testId);
|
|
382
|
+
elements.push({
|
|
383
|
+
id: testId,
|
|
384
|
+
type: elType,
|
|
385
|
+
text,
|
|
386
|
+
selector: {
|
|
387
|
+
testId,
|
|
388
|
+
cssSelector,
|
|
389
|
+
...ariaLabel ? { ariaLabel } : {}
|
|
390
|
+
},
|
|
391
|
+
description: `${humanize(testId)} ${elType}`
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
return elements;
|
|
396
|
+
}
|
|
397
|
+
function getTagName(node) {
|
|
398
|
+
if (node.name?.type === "JSXIdentifier") {
|
|
399
|
+
return node.name.name;
|
|
400
|
+
}
|
|
401
|
+
if (node.name?.type === "JSXMemberExpression") {
|
|
402
|
+
return `${node.name.object?.name || ""}.${node.name.property?.name || ""}`;
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
function getStaticPropValue(attrs, propName) {
|
|
407
|
+
for (const attr of attrs) {
|
|
408
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
409
|
+
const attrName = attr.name?.type === "JSXIdentifier" ? attr.name.name : attr.name?.type === "JSXNamespacedName" ? `${attr.name.namespace?.name}:${attr.name.name?.name}` : null;
|
|
410
|
+
if (attrName !== propName) continue;
|
|
411
|
+
if (attr.value?.type === "StringLiteral") {
|
|
412
|
+
return attr.value.value;
|
|
413
|
+
}
|
|
414
|
+
if (attr.value?.type === "JSXExpressionContainer" && attr.value.expression?.type === "StringLiteral") {
|
|
415
|
+
return attr.value.expression.value;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function mapTagToType(tag) {
|
|
422
|
+
const lower = tag.toLowerCase();
|
|
423
|
+
if (lower === "button") return "button";
|
|
424
|
+
if (tag === "Link" || lower === "a") return "link";
|
|
425
|
+
if (lower === "input" || lower === "textarea") return "input";
|
|
426
|
+
if (lower === "select") return "select";
|
|
427
|
+
return "text";
|
|
428
|
+
}
|
|
429
|
+
function buildCssSelector(tag, testId) {
|
|
430
|
+
const htmlTag = tag === "Link" ? "a" : tag.toLowerCase();
|
|
431
|
+
const nonSpecificTags = ["div", "section", "span", "main", "nav", "header", "footer", "article", "aside"];
|
|
432
|
+
if (nonSpecificTags.includes(htmlTag) || htmlTag.includes(".")) {
|
|
433
|
+
return `[data-testid='${testId}']`;
|
|
434
|
+
}
|
|
435
|
+
return `${htmlTag}[data-testid='${testId}']`;
|
|
436
|
+
}
|
|
437
|
+
function humanize(testId) {
|
|
438
|
+
return testId.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
439
|
+
}
|
|
440
|
+
function selectorScore(selector) {
|
|
441
|
+
let score = 0;
|
|
442
|
+
if (selector.testId) score += 1;
|
|
443
|
+
if (selector.ariaLabel) score += 1;
|
|
444
|
+
if (selector.cssSelector) score += 1;
|
|
445
|
+
if (selector.text) score += 1;
|
|
446
|
+
return score;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/utils/config.ts
|
|
450
|
+
var import_fs4 = __toESM(require("fs"));
|
|
451
|
+
var import_path3 = __toESM(require("path"));
|
|
452
|
+
var CONFIG_FILENAME = ".navieorc";
|
|
453
|
+
var DEFAULT_ENDPOINT = "https://api.navieo.io";
|
|
454
|
+
function loadConfig(projectRoot) {
|
|
455
|
+
let fileConfig = {};
|
|
456
|
+
const configPath = import_path3.default.join(projectRoot, CONFIG_FILENAME);
|
|
457
|
+
if (import_fs4.default.existsSync(configPath)) {
|
|
458
|
+
try {
|
|
459
|
+
const raw = import_fs4.default.readFileSync(configPath, "utf-8");
|
|
460
|
+
fileConfig = JSON.parse(raw);
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
apiKey: process.env.NAVIEO_API_KEY || fileConfig.apiKey,
|
|
466
|
+
endpoint: process.env.NAVIEO_ENDPOINT || fileConfig.endpoint || DEFAULT_ENDPOINT,
|
|
467
|
+
appId: fileConfig.appId
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function saveConfig(projectRoot, config) {
|
|
471
|
+
const configPath = import_path3.default.join(projectRoot, CONFIG_FILENAME);
|
|
472
|
+
import_fs4.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
473
|
+
}
|
|
474
|
+
function resolveProjectRoot(startDir) {
|
|
475
|
+
let dir = startDir || process.cwd();
|
|
476
|
+
while (true) {
|
|
477
|
+
if (import_fs4.default.existsSync(import_path3.default.join(dir, "package.json"))) {
|
|
478
|
+
return dir;
|
|
479
|
+
}
|
|
480
|
+
const parent = import_path3.default.dirname(dir);
|
|
481
|
+
if (parent === dir) {
|
|
482
|
+
return startDir || process.cwd();
|
|
483
|
+
}
|
|
484
|
+
dir = parent;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/commands/init.ts
|
|
489
|
+
var GETTING_STARTED_TEMPLATE = `# Getting Started
|
|
490
|
+
|
|
491
|
+
## Overview
|
|
492
|
+
Describe what your application does and its main features.
|
|
493
|
+
|
|
494
|
+
## Navigation
|
|
495
|
+
Explain how users navigate between the main sections of your app.
|
|
496
|
+
`;
|
|
497
|
+
async function initCommand(options) {
|
|
498
|
+
const projectRoot = resolveProjectRoot(import_path4.default.resolve(options.dir));
|
|
499
|
+
const navieoDir = import_path4.default.join(projectRoot, "navieo");
|
|
500
|
+
const docsDir = import_path4.default.join(navieoDir, "docs");
|
|
501
|
+
const sitemapPath = import_path4.default.join(navieoDir, "sitemap.navieo.json");
|
|
502
|
+
if (import_fs5.default.existsSync(navieoDir)) {
|
|
503
|
+
console.log(
|
|
504
|
+
import_picocolors.default.yellow("Warning:") + " navieo/ folder already exists. Use " + import_picocolors.default.cyan("navieo generate") + " to re-scan and update the existing sitemap."
|
|
505
|
+
);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
console.log(import_picocolors.default.bold("Initializing Navieo...\n"));
|
|
509
|
+
const framework = detectFramework(projectRoot);
|
|
510
|
+
console.log(import_picocolors.default.green("\u2713") + ` Detected framework: ${import_picocolors.default.cyan(framework.name)}`);
|
|
511
|
+
const routes = scanRoutes(projectRoot, framework);
|
|
512
|
+
console.log(import_picocolors.default.green("\u2713") + ` Found ${import_picocolors.default.cyan(String(routes.length))} routes`);
|
|
513
|
+
let totalElements = 0;
|
|
514
|
+
const sitemapRoutes = routes.map((route) => {
|
|
515
|
+
const filesToScan = [route.filePath, ...route.importedFiles];
|
|
516
|
+
const elements = scanElements(filesToScan);
|
|
517
|
+
totalElements += elements.length;
|
|
518
|
+
return {
|
|
519
|
+
path: route.path,
|
|
520
|
+
title: route.title,
|
|
521
|
+
description: route.description,
|
|
522
|
+
elements
|
|
523
|
+
};
|
|
524
|
+
});
|
|
525
|
+
console.log(import_picocolors.default.green("\u2713") + ` Found ${import_picocolors.default.cyan(String(totalElements))} interactive elements`);
|
|
526
|
+
const sitemap = {
|
|
527
|
+
appId: import_path4.default.basename(projectRoot),
|
|
528
|
+
framework: framework.name.replace("-app", "").replace("-pages", ""),
|
|
529
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
530
|
+
routes: sitemapRoutes,
|
|
531
|
+
flows: [],
|
|
532
|
+
docs: []
|
|
533
|
+
};
|
|
534
|
+
import_fs5.default.mkdirSync(navieoDir, { recursive: true });
|
|
535
|
+
import_fs5.default.writeFileSync(sitemapPath, JSON.stringify(sitemap, null, 2) + "\n", "utf-8");
|
|
536
|
+
console.log(import_picocolors.default.green("\u2713") + ` Generated ${import_picocolors.default.cyan("navieo/sitemap.navieo.json")}`);
|
|
537
|
+
import_fs5.default.mkdirSync(docsDir, { recursive: true });
|
|
538
|
+
const gettingStartedPath = import_path4.default.join(docsDir, "getting-started.md");
|
|
539
|
+
if (!import_fs5.default.existsSync(gettingStartedPath)) {
|
|
540
|
+
import_fs5.default.writeFileSync(gettingStartedPath, GETTING_STARTED_TEMPLATE, "utf-8");
|
|
541
|
+
console.log(import_picocolors.default.green("\u2713") + ` Created ${import_picocolors.default.cyan("navieo/docs/getting-started.md")}`);
|
|
542
|
+
}
|
|
543
|
+
const configPath = import_path4.default.join(projectRoot, ".navieorc");
|
|
544
|
+
if (!import_fs5.default.existsSync(configPath)) {
|
|
545
|
+
saveConfig(projectRoot, {
|
|
546
|
+
endpoint: "https://api.navieo.io",
|
|
547
|
+
appId: import_path4.default.basename(projectRoot)
|
|
548
|
+
});
|
|
549
|
+
console.log(import_picocolors.default.green("\u2713") + ` Created ${import_picocolors.default.cyan(".navieorc")}`);
|
|
550
|
+
}
|
|
551
|
+
console.log(
|
|
552
|
+
"\n" + import_picocolors.default.bold("Next steps:") + `
|
|
553
|
+
1. Review and adjust ${import_picocolors.default.cyan("navieo/sitemap.navieo.json")}
|
|
554
|
+
2. Write documentation in ${import_picocolors.default.cyan("navieo/docs/")}
|
|
555
|
+
3. Add your API key to ${import_picocolors.default.cyan(".navieorc")} or set ${import_picocolors.default.cyan("NAVIEO_API_KEY")} env var
|
|
556
|
+
4. Run: ${import_picocolors.default.cyan("navieo sync")}
|
|
557
|
+
`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/commands/sync.ts
|
|
562
|
+
var import_fs6 = __toESM(require("fs"));
|
|
563
|
+
var import_path5 = __toESM(require("path"));
|
|
564
|
+
var import_glob2 = require("glob");
|
|
565
|
+
var import_picocolors2 = __toESM(require("picocolors"));
|
|
566
|
+
|
|
567
|
+
// src/utils/markdown.ts
|
|
568
|
+
function parseMarkdownDoc(relativePath, content) {
|
|
569
|
+
const lines = content.split("\n");
|
|
570
|
+
let title = "";
|
|
571
|
+
const sections = [];
|
|
572
|
+
let currentHeading = "";
|
|
573
|
+
let currentContent = [];
|
|
574
|
+
for (const line of lines) {
|
|
575
|
+
const h1Match = line.match(/^#\s+(.+)$/);
|
|
576
|
+
if (h1Match && !title) {
|
|
577
|
+
title = h1Match[1].trim();
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const h2Match = line.match(/^##\s+(.+)$/);
|
|
581
|
+
if (h2Match) {
|
|
582
|
+
if (currentHeading) {
|
|
583
|
+
sections.push({
|
|
584
|
+
heading: currentHeading,
|
|
585
|
+
content: currentContent.join("\n").trim()
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
currentHeading = h2Match[1].trim();
|
|
589
|
+
currentContent = [];
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (currentHeading) {
|
|
593
|
+
currentContent.push(line);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (currentHeading) {
|
|
597
|
+
sections.push({
|
|
598
|
+
heading: currentHeading,
|
|
599
|
+
content: currentContent.join("\n").trim()
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
if (sections.length === 0 && title) {
|
|
603
|
+
const afterTitle = content.replace(/^#\s+.+\n?/, "").trim();
|
|
604
|
+
if (afterTitle) {
|
|
605
|
+
sections.push({
|
|
606
|
+
heading: "Overview",
|
|
607
|
+
content: afterTitle
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
path: relativePath,
|
|
613
|
+
title: title || relativePath.replace(/\.md$/, ""),
|
|
614
|
+
sections
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/utils/api.ts
|
|
619
|
+
async function syncToBackend(endpoint, apiKey, sitemap) {
|
|
620
|
+
const url = `${endpoint.replace(/\/$/, "")}/api/sitemap`;
|
|
621
|
+
if (!url.startsWith("https://") && !url.startsWith("http://localhost")) {
|
|
622
|
+
console.warn(
|
|
623
|
+
"WARNING: Syncing to a non-HTTPS endpoint. Your API key may be exposed in transit."
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const response = await fetch(url, {
|
|
627
|
+
method: "POST",
|
|
628
|
+
headers: {
|
|
629
|
+
"Content-Type": "application/json",
|
|
630
|
+
"x-api-key": apiKey
|
|
631
|
+
},
|
|
632
|
+
body: JSON.stringify({ sitemap })
|
|
633
|
+
});
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
if (response.status === 401) {
|
|
636
|
+
return {
|
|
637
|
+
success: false,
|
|
638
|
+
error: "Invalid API key. Check your .navieorc or NAVIEO_API_KEY env var."
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
let errorMessage;
|
|
642
|
+
try {
|
|
643
|
+
const body2 = await response.json();
|
|
644
|
+
errorMessage = body2.error || `Server returned ${response.status}`;
|
|
645
|
+
} catch {
|
|
646
|
+
errorMessage = `Server returned ${response.status}: ${response.statusText}`;
|
|
647
|
+
}
|
|
648
|
+
return { success: false, error: errorMessage };
|
|
649
|
+
}
|
|
650
|
+
const body = await response.json();
|
|
651
|
+
return {
|
|
652
|
+
success: true,
|
|
653
|
+
appId: body.appId,
|
|
654
|
+
routeCount: body.routeCount,
|
|
655
|
+
docCount: body.docCount,
|
|
656
|
+
chunkCount: body.chunkCount,
|
|
657
|
+
embedded: body.embedded
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/commands/sync.ts
|
|
662
|
+
async function syncCommand(options) {
|
|
663
|
+
const projectRoot = resolveProjectRoot(import_path5.default.resolve(options.dir));
|
|
664
|
+
const navieoDir = import_path5.default.join(projectRoot, "navieo");
|
|
665
|
+
const sitemapPath = import_path5.default.join(navieoDir, "sitemap.navieo.json");
|
|
666
|
+
const config = loadConfig(projectRoot);
|
|
667
|
+
const apiKey = options.key || config.apiKey;
|
|
668
|
+
const endpoint = options.endpoint || config.endpoint || "https://api.navieo.io";
|
|
669
|
+
if (!apiKey) {
|
|
670
|
+
console.error(
|
|
671
|
+
import_picocolors2.default.red("Error:") + ` No API key found.
|
|
672
|
+
Set ${import_picocolors2.default.cyan("NAVIEO_API_KEY")} environment variable, or
|
|
673
|
+
Add ${import_picocolors2.default.cyan('"apiKey"')} to ${import_picocolors2.default.cyan(".navieorc")}, or
|
|
674
|
+
Use ${import_picocolors2.default.cyan("--key <apiKey>")} flag
|
|
675
|
+
`
|
|
676
|
+
);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
if (!import_fs6.default.existsSync(sitemapPath)) {
|
|
680
|
+
console.error(
|
|
681
|
+
import_picocolors2.default.red("Error:") + ` ${import_picocolors2.default.cyan("navieo/sitemap.navieo.json")} not found.
|
|
682
|
+
Run ${import_picocolors2.default.cyan("navieo init")} first to generate the sitemap.
|
|
683
|
+
`
|
|
684
|
+
);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
let sitemap;
|
|
688
|
+
try {
|
|
689
|
+
const raw = import_fs6.default.readFileSync(sitemapPath, "utf-8");
|
|
690
|
+
sitemap = JSON.parse(raw);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
console.error(import_picocolors2.default.red("Error:") + " Failed to parse sitemap.navieo.json");
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
console.log(
|
|
696
|
+
import_picocolors2.default.green("\u2713") + ` Loaded sitemap: ${import_picocolors2.default.cyan(String(sitemap.routes?.length ?? 0))} routes, ${import_picocolors2.default.cyan(String(sitemap.flows?.length ?? 0))} flows`
|
|
697
|
+
);
|
|
698
|
+
const docsDir = import_path5.default.join(navieoDir, "docs");
|
|
699
|
+
if (import_fs6.default.existsSync(docsDir)) {
|
|
700
|
+
const mdFiles = import_glob2.glob.sync("*.md", { cwd: docsDir });
|
|
701
|
+
const parsedDocs = mdFiles.map((mdFile) => {
|
|
702
|
+
const content = import_fs6.default.readFileSync(import_path5.default.join(docsDir, mdFile), "utf-8");
|
|
703
|
+
return parseMarkdownDoc(`docs/${mdFile}`, content);
|
|
704
|
+
});
|
|
705
|
+
sitemap.docs = parsedDocs;
|
|
706
|
+
const totalSections = parsedDocs.reduce((sum, doc) => sum + doc.sections.length, 0);
|
|
707
|
+
console.log(
|
|
708
|
+
import_picocolors2.default.green("\u2713") + ` Loaded ${import_picocolors2.default.cyan(String(parsedDocs.length))} doc files (${import_picocolors2.default.cyan(String(totalSections))} sections)`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
console.log(`
|
|
712
|
+
Syncing to ${import_picocolors2.default.cyan(endpoint)}...`);
|
|
713
|
+
try {
|
|
714
|
+
const result = await syncToBackend(endpoint, apiKey, sitemap);
|
|
715
|
+
if (!result.success) {
|
|
716
|
+
console.error(import_picocolors2.default.red("Error:") + ` ${result.error}`);
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
console.log(
|
|
720
|
+
import_picocolors2.default.green("\u2713") + ` Synced successfully
|
|
721
|
+
Routes: ${import_picocolors2.default.cyan(String(result.routeCount))} | Docs: ${import_picocolors2.default.cyan(String(result.docCount))} | Chunks: ${import_picocolors2.default.cyan(String(result.chunkCount))} | Embedded: ${result.embedded ? import_picocolors2.default.green("true") : import_picocolors2.default.yellow("false")}
|
|
722
|
+
`
|
|
723
|
+
);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
if (err.cause?.code === "ECONNREFUSED") {
|
|
726
|
+
console.error(
|
|
727
|
+
import_picocolors2.default.red("Error:") + ` Could not connect to ${import_picocolors2.default.cyan(endpoint)}.
|
|
728
|
+
Is the backend running? Try: ${import_picocolors2.default.cyan("pnpm dev:backend")}
|
|
729
|
+
`
|
|
730
|
+
);
|
|
731
|
+
} else {
|
|
732
|
+
console.error(import_picocolors2.default.red("Error:") + ` ${err.message || err}`);
|
|
733
|
+
}
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/commands/generate.ts
|
|
739
|
+
var import_fs7 = __toESM(require("fs"));
|
|
740
|
+
var import_path6 = __toESM(require("path"));
|
|
741
|
+
var import_picocolors3 = __toESM(require("picocolors"));
|
|
742
|
+
async function generateCommand(options) {
|
|
743
|
+
const projectRoot = resolveProjectRoot(import_path6.default.resolve(options.dir));
|
|
744
|
+
const navieoDir = import_path6.default.join(projectRoot, "navieo");
|
|
745
|
+
const sitemapPath = import_path6.default.join(navieoDir, "sitemap.navieo.json");
|
|
746
|
+
console.log(import_picocolors3.default.bold("Re-scanning codebase...\n"));
|
|
747
|
+
let existingSitemap = null;
|
|
748
|
+
if (import_fs7.default.existsSync(sitemapPath)) {
|
|
749
|
+
try {
|
|
750
|
+
const raw = import_fs7.default.readFileSync(sitemapPath, "utf-8");
|
|
751
|
+
existingSitemap = JSON.parse(raw);
|
|
752
|
+
} catch {
|
|
753
|
+
console.log(import_picocolors3.default.yellow("Warning:") + " Could not parse existing sitemap. Starting fresh.");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const framework = detectFramework(projectRoot);
|
|
757
|
+
console.log(import_picocolors3.default.green("\u2713") + ` Detected framework: ${import_picocolors3.default.cyan(framework.name)}`);
|
|
758
|
+
const scannedRoutes = scanRoutes(projectRoot, framework);
|
|
759
|
+
console.log(import_picocolors3.default.green("\u2713") + ` Scanned ${import_picocolors3.default.cyan(String(scannedRoutes.length))} routes`);
|
|
760
|
+
const newRoutes = scannedRoutes.map((route) => {
|
|
761
|
+
const filesToScan = [route.filePath, ...route.importedFiles];
|
|
762
|
+
const elements = scanElements(filesToScan);
|
|
763
|
+
return {
|
|
764
|
+
path: route.path,
|
|
765
|
+
title: route.title,
|
|
766
|
+
description: route.description,
|
|
767
|
+
elements
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
let mergedRoutes;
|
|
771
|
+
let stats = { newRoutes: 0, updatedRoutes: 0, preservedRoutes: 0 };
|
|
772
|
+
if (existingSitemap) {
|
|
773
|
+
const result = mergeRoutes(existingSitemap.routes || [], newRoutes);
|
|
774
|
+
mergedRoutes = result.routes;
|
|
775
|
+
stats = result.stats;
|
|
776
|
+
} else {
|
|
777
|
+
mergedRoutes = newRoutes;
|
|
778
|
+
stats.newRoutes = newRoutes.length;
|
|
779
|
+
}
|
|
780
|
+
const sitemap = {
|
|
781
|
+
appId: existingSitemap?.appId || import_path6.default.basename(projectRoot),
|
|
782
|
+
framework: framework.name.replace("-app", "").replace("-pages", ""),
|
|
783
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
784
|
+
routes: mergedRoutes,
|
|
785
|
+
flows: existingSitemap?.flows || [],
|
|
786
|
+
docs: existingSitemap?.docs || []
|
|
787
|
+
};
|
|
788
|
+
import_fs7.default.mkdirSync(navieoDir, { recursive: true });
|
|
789
|
+
import_fs7.default.writeFileSync(sitemapPath, JSON.stringify(sitemap, null, 2) + "\n", "utf-8");
|
|
790
|
+
const totalElements = mergedRoutes.reduce((sum, r) => sum + (r.elements?.length || 0), 0);
|
|
791
|
+
console.log(
|
|
792
|
+
import_picocolors3.default.green("\u2713") + ` Found ${import_picocolors3.default.cyan(String(totalElements))} interactive elements`
|
|
793
|
+
);
|
|
794
|
+
console.log(
|
|
795
|
+
"\n" + import_picocolors3.default.green("\u2713") + " Updated " + import_picocolors3.default.cyan("navieo/sitemap.navieo.json") + `
|
|
796
|
+
New routes: ${import_picocolors3.default.cyan(String(stats.newRoutes))} | Updated: ${import_picocolors3.default.cyan(String(stats.updatedRoutes))} | Preserved: ${import_picocolors3.default.cyan(String(stats.preservedRoutes))}
|
|
797
|
+
Flows: ${import_picocolors3.default.cyan(String(sitemap.flows?.length ?? 0))} (preserved) | Docs: ${import_picocolors3.default.cyan(String(sitemap.docs?.length ?? 0))} (preserved)
|
|
798
|
+
`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
function mergeRoutes(oldRoutes, newRoutes) {
|
|
802
|
+
const oldRouteMap = new Map(oldRoutes.map((r) => [r.path, r]));
|
|
803
|
+
const newRouteMap = new Map(newRoutes.map((r) => [r.path, r]));
|
|
804
|
+
const mergedRoutes = [];
|
|
805
|
+
let newCount = 0;
|
|
806
|
+
let updatedCount = 0;
|
|
807
|
+
let preservedCount = 0;
|
|
808
|
+
for (const newRoute of newRoutes) {
|
|
809
|
+
const oldRoute = oldRouteMap.get(newRoute.path);
|
|
810
|
+
if (oldRoute) {
|
|
811
|
+
mergedRoutes.push({
|
|
812
|
+
path: newRoute.path,
|
|
813
|
+
title: oldRoute.title !== `${capitalize2(newRoute.path.split("/").pop() || "")} page` ? oldRoute.title : newRoute.title,
|
|
814
|
+
description: isPlaceholderDescription(oldRoute.description) ? newRoute.description : oldRoute.description,
|
|
815
|
+
elements: mergeElements(oldRoute.elements || [], newRoute.elements || [])
|
|
816
|
+
});
|
|
817
|
+
updatedCount++;
|
|
818
|
+
} else {
|
|
819
|
+
mergedRoutes.push(newRoute);
|
|
820
|
+
newCount++;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
for (const oldRoute of oldRoutes) {
|
|
824
|
+
if (!newRouteMap.has(oldRoute.path)) {
|
|
825
|
+
mergedRoutes.push(oldRoute);
|
|
826
|
+
preservedCount++;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
routes: mergedRoutes.sort((a, b) => a.path.localeCompare(b.path)),
|
|
831
|
+
stats: { newRoutes: newCount, updatedRoutes: updatedCount, preservedRoutes: preservedCount }
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function mergeElements(oldElements, newElements) {
|
|
835
|
+
const oldMap = new Map(oldElements.map((e) => [e.id, e]));
|
|
836
|
+
const newMap = new Map(newElements.map((e) => [e.id, e]));
|
|
837
|
+
const merged = [];
|
|
838
|
+
for (const newEl of newElements) {
|
|
839
|
+
const oldEl = oldMap.get(newEl.id);
|
|
840
|
+
if (oldEl) {
|
|
841
|
+
merged.push({
|
|
842
|
+
id: newEl.id,
|
|
843
|
+
type: newEl.type,
|
|
844
|
+
text: oldEl.text !== humanize2(oldEl.id) ? oldEl.text : newEl.text,
|
|
845
|
+
selector: newEl.selector,
|
|
846
|
+
// Source code is truth for selectors
|
|
847
|
+
description: isPlaceholderElementDescription(oldEl.description, oldEl.id) ? newEl.description : oldEl.description
|
|
848
|
+
});
|
|
849
|
+
} else {
|
|
850
|
+
merged.push(newEl);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
for (const oldEl of oldElements) {
|
|
854
|
+
if (!newMap.has(oldEl.id)) {
|
|
855
|
+
merged.push(oldEl);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return merged;
|
|
859
|
+
}
|
|
860
|
+
function isPlaceholderDescription(desc) {
|
|
861
|
+
return /^\w[\w\s]+ page$/i.test(desc);
|
|
862
|
+
}
|
|
863
|
+
function isPlaceholderElementDescription(desc, id) {
|
|
864
|
+
const humanized = humanize2(id);
|
|
865
|
+
return desc === `${humanized} button` || desc === `${humanized} link` || desc === `${humanized} input` || desc === `${humanized} select` || desc === `${humanized} text`;
|
|
866
|
+
}
|
|
867
|
+
function humanize2(testId) {
|
|
868
|
+
return testId.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
869
|
+
}
|
|
870
|
+
function capitalize2(s) {
|
|
871
|
+
return s.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/index.ts
|
|
875
|
+
var program = new import_commander.Command();
|
|
876
|
+
program.name("navieo").description("CLI for Navieo \u2014 scan, generate, and sync sitemaps").version("1.0.0");
|
|
877
|
+
program.command("init").description("Scan codebase and generate initial sitemap + docs folder").option("-d, --dir <path>", "Project root directory", ".").action(initCommand);
|
|
878
|
+
program.command("sync").description("Upload sitemap + docs to Navieo backend").option("-d, --dir <path>", "Project root directory", ".").option("-k, --key <apiKey>", "API key (overrides .navieorc and env)").option("-e, --endpoint <url>", "Backend URL (overrides .navieorc)").action(syncCommand);
|
|
879
|
+
program.command("generate").description("Re-scan codebase and update existing sitemap").option("-d, --dir <path>", "Project root directory", ".").action(generateCommand);
|
|
880
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@navieo/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for Navieo — scan, generate, and sync sitemaps",
|
|
5
|
+
"bin": {
|
|
6
|
+
"navieo": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup",
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"prepublishOnly": "tsup"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"commander": "^12",
|
|
15
|
+
"@babel/parser": "^7",
|
|
16
|
+
"@babel/traverse": "^7",
|
|
17
|
+
"picocolors": "^1",
|
|
18
|
+
"glob": "^11"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@navieo/types": "workspace:*",
|
|
22
|
+
"@babel/types": "^7",
|
|
23
|
+
"tsup": "^8",
|
|
24
|
+
"typescript": "^5",
|
|
25
|
+
"@types/node": "^20",
|
|
26
|
+
"@types/babel__traverse": "^7"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/Navieo-org/navieo-sdk.git",
|
|
41
|
+
"directory": "packages/cli"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Navieo-org/navieo-sdk/tree/main/packages/cli#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/Navieo-org/navieo-sdk/issues"
|
|
46
|
+
},
|
|
47
|
+
"keywords": ["navieo", "cli", "sitemap", "documentation", "onboarding", "product-tour"]
|
|
48
|
+
}
|