graftapp 0.2.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.
@@ -0,0 +1,10 @@
1
+ import {
2
+ clearAuth,
3
+ loadAuth,
4
+ saveAuth
5
+ } from "./chunk-IFQ6DFWT.js";
6
+ export {
7
+ clearAuth,
8
+ loadAuth,
9
+ saveAuth
10
+ };
@@ -0,0 +1,137 @@
1
+ // src/scanners/react-router.ts
2
+ import { readFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { existsSync } from "fs";
5
+ var ReactRouterScanner = class {
6
+ constructor() {
7
+ this.cwd = process.cwd();
8
+ }
9
+ async scan() {
10
+ const routes = [];
11
+ const routerFiles = [
12
+ "src/router.ts",
13
+ "src/router.tsx",
14
+ "src/routes.ts",
15
+ "src/routes.tsx",
16
+ "src/router/index.ts",
17
+ "src/router/index.tsx",
18
+ "src/App.tsx",
19
+ "src/App.ts",
20
+ "src/app.tsx",
21
+ "src/app.ts"
22
+ ];
23
+ for (const file of routerFiles) {
24
+ const filePath = join(this.cwd, file);
25
+ if (existsSync(filePath)) {
26
+ try {
27
+ const content = await readFile(filePath, "utf-8");
28
+ const extractedRoutes = this.extractRoutes(content);
29
+ routes.push(...extractedRoutes);
30
+ } catch (error) {
31
+ continue;
32
+ }
33
+ }
34
+ }
35
+ const uniqueRoutes = this.deduplicateRoutes(routes);
36
+ return {
37
+ routes: uniqueRoutes,
38
+ framework: "react-router"
39
+ };
40
+ }
41
+ /**
42
+ * Extract routes from file content using regex patterns
43
+ * Looks for:
44
+ * - path: "/some/path"
45
+ * - path="/some/path"
46
+ * - path='/some/path'
47
+ * - <Route path="/some/path" />
48
+ */
49
+ extractRoutes(content) {
50
+ const routes = [];
51
+ if (!this.usesReactRouter(content)) {
52
+ return routes;
53
+ }
54
+ const colonPattern = /path:\s*["']([^"']+)["']/g;
55
+ let match;
56
+ while ((match = colonPattern.exec(content)) !== null) {
57
+ const path = match[1];
58
+ if (this.isValidRoutePath(path)) {
59
+ routes.push({
60
+ path: this.normalizePath(path),
61
+ isDynamic: this.isDynamicRoute(path),
62
+ framework: "react-router"
63
+ });
64
+ }
65
+ }
66
+ const equalsPattern = /path\s*=\s*["']([^"']+)["']/g;
67
+ while ((match = equalsPattern.exec(content)) !== null) {
68
+ const path = match[1];
69
+ if (this.isValidRoutePath(path)) {
70
+ routes.push({
71
+ path: this.normalizePath(path),
72
+ isDynamic: this.isDynamicRoute(path),
73
+ framework: "react-router"
74
+ });
75
+ }
76
+ }
77
+ return routes;
78
+ }
79
+ /**
80
+ * Check if the file content uses react-router
81
+ */
82
+ usesReactRouter(content) {
83
+ return content.includes("react-router-dom") || content.includes("react-router") || content.includes("createBrowserRouter") || content.includes("createHashRouter") || content.includes("BrowserRouter") || content.includes("HashRouter") || content.includes("<Route");
84
+ }
85
+ /**
86
+ * Check if a path is a valid route path (not a variable or complex expression)
87
+ */
88
+ isValidRoutePath(path) {
89
+ if (!path.startsWith("/")) {
90
+ return false;
91
+ }
92
+ if (path.includes("${") || path.includes("`")) {
93
+ return false;
94
+ }
95
+ if (path.includes(" ")) {
96
+ return false;
97
+ }
98
+ return true;
99
+ }
100
+ /**
101
+ * Normalize the path to ensure consistency
102
+ */
103
+ normalizePath(path) {
104
+ if (!path.startsWith("/")) {
105
+ path = "/" + path;
106
+ }
107
+ if (path.length > 1 && path.endsWith("/")) {
108
+ path = path.slice(0, -1);
109
+ }
110
+ return path;
111
+ }
112
+ /**
113
+ * Check if a route path contains dynamic segments
114
+ * React Router uses :param for dynamic segments
115
+ */
116
+ isDynamicRoute(path) {
117
+ return path.includes(":") || path.includes("*");
118
+ }
119
+ /**
120
+ * Remove duplicate routes
121
+ */
122
+ deduplicateRoutes(routes) {
123
+ const seen = /* @__PURE__ */ new Set();
124
+ const unique = [];
125
+ for (const route of routes) {
126
+ if (!seen.has(route.path)) {
127
+ seen.add(route.path);
128
+ unique.push(route);
129
+ }
130
+ }
131
+ return unique;
132
+ }
133
+ };
134
+
135
+ export {
136
+ ReactRouterScanner
137
+ };
@@ -0,0 +1,30 @@
1
+ // src/utils/auth.ts
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import { readFile, writeFile, unlink, mkdir } from "fs/promises";
5
+ var CONFIG_DIR = join(homedir(), ".config", "graft");
6
+ var AUTH_FILE = join(CONFIG_DIR, "auth.json");
7
+ async function saveAuth(auth) {
8
+ await mkdir(CONFIG_DIR, { recursive: true });
9
+ await writeFile(AUTH_FILE, JSON.stringify(auth, null, 2));
10
+ }
11
+ async function loadAuth() {
12
+ try {
13
+ const content = await readFile(AUTH_FILE, "utf-8");
14
+ return JSON.parse(content);
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ async function clearAuth() {
20
+ try {
21
+ await unlink(AUTH_FILE);
22
+ } catch {
23
+ }
24
+ }
25
+
26
+ export {
27
+ saveAuth,
28
+ loadAuth,
29
+ clearAuth
30
+ };
@@ -0,0 +1,30 @@
1
+ // src/utils/config.ts
2
+ import { existsSync } from "fs";
3
+ import { readFile, writeFile } from "fs/promises";
4
+ import { join } from "path";
5
+ var CONFIG_FILE = "graft.config.json";
6
+ async function loadConfig() {
7
+ const configPath = join(process.cwd(), CONFIG_FILE);
8
+ if (!existsSync(configPath)) {
9
+ return null;
10
+ }
11
+ try {
12
+ const content = await readFile(configPath, "utf-8");
13
+ return JSON.parse(content);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ async function saveConfig(config) {
19
+ const configPath = join(process.cwd(), CONFIG_FILE);
20
+ await writeFile(configPath, JSON.stringify(config, null, 2));
21
+ }
22
+ function configExists() {
23
+ return existsSync(join(process.cwd(), CONFIG_FILE));
24
+ }
25
+
26
+ export {
27
+ loadConfig,
28
+ saveConfig,
29
+ configExists
30
+ };
@@ -0,0 +1,182 @@
1
+ // src/scanners/next.ts
2
+ import { readdir } from "fs/promises";
3
+ import { join, sep } from "path";
4
+ import { existsSync } from "fs";
5
+ var NextScanner = class {
6
+ constructor() {
7
+ this.cwd = process.cwd();
8
+ }
9
+ async scan() {
10
+ const routes = [];
11
+ const appDir = join(this.cwd, "app");
12
+ if (existsSync(appDir)) {
13
+ const appRoutes = await this.scanAppRouter(appDir);
14
+ routes.push(...appRoutes);
15
+ }
16
+ const pagesDir = join(this.cwd, "pages");
17
+ if (existsSync(pagesDir)) {
18
+ const pageRoutes = await this.scanPagesRouter(pagesDir);
19
+ routes.push(...pageRoutes);
20
+ }
21
+ return {
22
+ routes,
23
+ framework: "next"
24
+ };
25
+ }
26
+ /**
27
+ * Scan Next.js App Router (app directory)
28
+ * Looks for page.tsx, page.ts, page.jsx, page.js files
29
+ */
30
+ async scanAppRouter(appDir) {
31
+ const routes = [];
32
+ await this.walkDirectory(appDir, async (filePath, relativePath) => {
33
+ const fileName = filePath.split(sep).pop() || "";
34
+ if (!fileName.match(/^page\.(tsx?|jsx?)$/)) {
35
+ return;
36
+ }
37
+ const routePath = this.convertAppRouterPath(relativePath);
38
+ if (routePath) {
39
+ routes.push({
40
+ path: routePath,
41
+ isDynamic: this.isDynamicRoute(routePath),
42
+ framework: "next"
43
+ });
44
+ }
45
+ });
46
+ return routes;
47
+ }
48
+ /**
49
+ * Scan Next.js Pages Router (pages directory)
50
+ * Looks for .tsx, .ts, .jsx, .js files
51
+ */
52
+ async scanPagesRouter(pagesDir) {
53
+ const routes = [];
54
+ await this.walkDirectory(pagesDir, async (filePath, relativePath) => {
55
+ const fileName = filePath.split(sep).pop() || "";
56
+ if (fileName.match(/^(_app|_document|_error)\.(tsx?|jsx?)$/)) {
57
+ return;
58
+ }
59
+ if (relativePath.startsWith("api" + sep) || relativePath.startsWith("api/")) {
60
+ return;
61
+ }
62
+ if (!fileName.match(/\.(tsx?|jsx?)$/)) {
63
+ return;
64
+ }
65
+ const routePath = this.convertPagesRouterPath(relativePath);
66
+ if (routePath) {
67
+ routes.push({
68
+ path: routePath,
69
+ isDynamic: this.isDynamicRoute(routePath),
70
+ framework: "next"
71
+ });
72
+ }
73
+ });
74
+ return routes;
75
+ }
76
+ /**
77
+ * Convert App Router file path to route path
78
+ * Examples:
79
+ * - "page.tsx" → "/"
80
+ * - "dashboard/page.tsx" → "/dashboard"
81
+ * - "users/[id]/page.tsx" → "/users/:id"
82
+ * - "blog/[...slug]/page.tsx" → "/blog/*"
83
+ * - "shop/[[...slug]]/page.tsx" → "/shop/*?"
84
+ * - "(auth)/login/page.tsx" → "/login" (route groups removed)
85
+ * - "@modal/photo/page.tsx" → skipped (parallel routes)
86
+ * - "_components/card.tsx" → skipped (private folders)
87
+ */
88
+ convertAppRouterPath(relativePath) {
89
+ let path = relativePath.replace(/\/?page\.(tsx?|jsx?)$/, "");
90
+ const segments = path ? path.split(sep) : [];
91
+ const processedSegments = [];
92
+ for (const segment of segments) {
93
+ if (segment.startsWith("_")) {
94
+ continue;
95
+ }
96
+ if (segment.startsWith("@")) {
97
+ return null;
98
+ }
99
+ if (segment.startsWith("(") && segment.endsWith(")")) {
100
+ continue;
101
+ }
102
+ if (segment.startsWith("[") && segment.endsWith("]")) {
103
+ const param = segment.slice(1, -1);
104
+ if (param.startsWith("...")) {
105
+ processedSegments.push("*");
106
+ } else if (param.startsWith("[...") && param.endsWith("]")) {
107
+ processedSegments.push("*?");
108
+ } else {
109
+ processedSegments.push(":" + param);
110
+ }
111
+ } else {
112
+ processedSegments.push(segment);
113
+ }
114
+ }
115
+ return "/" + processedSegments.join("/");
116
+ }
117
+ /**
118
+ * Convert Pages Router file path to route path
119
+ * Examples:
120
+ * - "index.tsx" → "/"
121
+ * - "about.tsx" → "/about"
122
+ * - "blog/index.tsx" → "/blog"
123
+ * - "blog/[slug].tsx" → "/blog/:slug"
124
+ * - "blog/[...slug].tsx" → "/blog/*"
125
+ */
126
+ convertPagesRouterPath(relativePath) {
127
+ let path = relativePath.replace(/\.(tsx?|jsx?)$/, "");
128
+ if (path === "index") {
129
+ return "/";
130
+ }
131
+ path = path.replace(/\/index$/, "");
132
+ const segments = path.split(sep);
133
+ const processedSegments = [];
134
+ for (const segment of segments) {
135
+ if (segment.startsWith("[") && segment.endsWith("]")) {
136
+ const param = segment.slice(1, -1);
137
+ if (param.startsWith("...")) {
138
+ processedSegments.push("*");
139
+ } else {
140
+ processedSegments.push(":" + param);
141
+ }
142
+ } else {
143
+ processedSegments.push(segment);
144
+ }
145
+ }
146
+ return "/" + processedSegments.join("/");
147
+ }
148
+ /**
149
+ * Check if a route path contains dynamic segments
150
+ */
151
+ isDynamicRoute(path) {
152
+ return path.includes(":") || path.includes("*");
153
+ }
154
+ /**
155
+ * Recursively walk a directory and call the callback for each file
156
+ */
157
+ async walkDirectory(dir, callback) {
158
+ const walk = async (currentDir, basePath = "") => {
159
+ try {
160
+ const entries = await readdir(currentDir, { withFileTypes: true });
161
+ for (const entry of entries) {
162
+ const fullPath = join(currentDir, entry.name);
163
+ const relativePath = basePath ? join(basePath, entry.name) : entry.name;
164
+ if (entry.isDirectory()) {
165
+ await walk(fullPath, relativePath);
166
+ } else if (entry.isFile()) {
167
+ await callback(fullPath, relativePath);
168
+ }
169
+ }
170
+ } catch (error) {
171
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT" && error.code !== "EACCES") {
172
+ throw error;
173
+ }
174
+ }
175
+ };
176
+ await walk(dir);
177
+ }
178
+ };
179
+
180
+ export {
181
+ NextScanner
182
+ };
@@ -0,0 +1,204 @@
1
+ // src/scanners/vue-router.ts
2
+ import { readFile, readdir } from "fs/promises";
3
+ import { join, sep } from "path";
4
+ import { existsSync } from "fs";
5
+ var VueRouterScanner = class {
6
+ constructor() {
7
+ this.cwd = process.cwd();
8
+ }
9
+ async scan() {
10
+ const routes = [];
11
+ const routerFiles = [
12
+ "src/router/index.ts",
13
+ "src/router/index.js",
14
+ "src/router.ts",
15
+ "src/router.js",
16
+ "router/index.ts",
17
+ "router/index.js"
18
+ ];
19
+ let foundRouterConfig = false;
20
+ for (const file of routerFiles) {
21
+ const filePath = join(this.cwd, file);
22
+ if (existsSync(filePath)) {
23
+ try {
24
+ const content = await readFile(filePath, "utf-8");
25
+ const extractedRoutes = this.extractRoutesFromConfig(content);
26
+ routes.push(...extractedRoutes);
27
+ foundRouterConfig = true;
28
+ } catch (error) {
29
+ continue;
30
+ }
31
+ }
32
+ }
33
+ if (!foundRouterConfig) {
34
+ const pagesRoutes = await this.scanPagesDirectory();
35
+ routes.push(...pagesRoutes);
36
+ }
37
+ const uniqueRoutes = this.deduplicateRoutes(routes);
38
+ return {
39
+ routes: uniqueRoutes,
40
+ framework: "vue"
41
+ };
42
+ }
43
+ /**
44
+ * Extract routes from Vue Router configuration file
45
+ * Looks for path: '/some/path' patterns
46
+ */
47
+ extractRoutesFromConfig(content) {
48
+ const routes = [];
49
+ if (!this.usesVueRouter(content)) {
50
+ return routes;
51
+ }
52
+ const pathPattern = /path:\s*["'`]([^"'`]+)["'`]/g;
53
+ let match;
54
+ while ((match = pathPattern.exec(content)) !== null) {
55
+ const path = match[1];
56
+ if (this.isValidRoutePath(path)) {
57
+ routes.push({
58
+ path: this.normalizePath(path),
59
+ isDynamic: this.isDynamicRoute(path),
60
+ framework: "vue"
61
+ });
62
+ }
63
+ }
64
+ return routes;
65
+ }
66
+ /**
67
+ * Scan Nuxt-style pages directory for routes
68
+ */
69
+ async scanPagesDirectory() {
70
+ const routes = [];
71
+ const pagesDir = join(this.cwd, "pages");
72
+ if (!existsSync(pagesDir)) {
73
+ return routes;
74
+ }
75
+ await this.walkDirectory(pagesDir, async (filePath, relativePath) => {
76
+ const fileName = filePath.split(sep).pop() || "";
77
+ if (!fileName.match(/\.vue$/)) {
78
+ return;
79
+ }
80
+ const routePath = this.convertNuxtPagePath(relativePath);
81
+ if (routePath) {
82
+ routes.push({
83
+ path: routePath,
84
+ isDynamic: this.isDynamicRoute(routePath),
85
+ framework: "vue"
86
+ });
87
+ }
88
+ });
89
+ return routes;
90
+ }
91
+ /**
92
+ * Convert Nuxt pages file path to route path
93
+ * Examples:
94
+ * - "index.vue" → "/"
95
+ * - "about.vue" → "/about"
96
+ * - "users/index.vue" → "/users"
97
+ * - "users/[id].vue" → "/users/:id"
98
+ * - "blog/[...slug].vue" → "/blog/*"
99
+ */
100
+ convertNuxtPagePath(relativePath) {
101
+ let path = relativePath.replace(/\.vue$/, "");
102
+ if (path === "index") {
103
+ return "/";
104
+ }
105
+ path = path.replace(/\/index$/, "");
106
+ const segments = path.split(sep);
107
+ const processedSegments = [];
108
+ for (const segment of segments) {
109
+ if (segment.startsWith("[") && segment.endsWith("]")) {
110
+ const param = segment.slice(1, -1);
111
+ if (param.startsWith("...")) {
112
+ processedSegments.push("*");
113
+ } else {
114
+ processedSegments.push(":" + param);
115
+ }
116
+ } else {
117
+ processedSegments.push(segment);
118
+ }
119
+ }
120
+ return "/" + processedSegments.join("/");
121
+ }
122
+ /**
123
+ * Check if the file content uses vue-router
124
+ */
125
+ usesVueRouter(content) {
126
+ return content.includes("vue-router") || content.includes("createRouter") || content.includes("VueRouter") || content.includes("RouteRecordRaw");
127
+ }
128
+ /**
129
+ * Check if a path is a valid route path
130
+ */
131
+ isValidRoutePath(path) {
132
+ if (!path.startsWith("/")) {
133
+ return false;
134
+ }
135
+ if (path.includes("${") || path.includes("`")) {
136
+ return false;
137
+ }
138
+ if (path.includes(" ")) {
139
+ return false;
140
+ }
141
+ return true;
142
+ }
143
+ /**
144
+ * Normalize the path to ensure consistency
145
+ */
146
+ normalizePath(path) {
147
+ if (!path.startsWith("/")) {
148
+ path = "/" + path;
149
+ }
150
+ if (path.length > 1 && path.endsWith("/")) {
151
+ path = path.slice(0, -1);
152
+ }
153
+ return path;
154
+ }
155
+ /**
156
+ * Check if a route path contains dynamic segments
157
+ * Vue Router uses :param for dynamic segments
158
+ */
159
+ isDynamicRoute(path) {
160
+ return path.includes(":") || path.includes("*");
161
+ }
162
+ /**
163
+ * Remove duplicate routes
164
+ */
165
+ deduplicateRoutes(routes) {
166
+ const seen = /* @__PURE__ */ new Set();
167
+ const unique = [];
168
+ for (const route of routes) {
169
+ if (!seen.has(route.path)) {
170
+ seen.add(route.path);
171
+ unique.push(route);
172
+ }
173
+ }
174
+ return unique;
175
+ }
176
+ /**
177
+ * Recursively walk a directory and call the callback for each file
178
+ */
179
+ async walkDirectory(dir, callback) {
180
+ const walk = async (currentDir, basePath = "") => {
181
+ try {
182
+ const entries = await readdir(currentDir, { withFileTypes: true });
183
+ for (const entry of entries) {
184
+ const fullPath = join(currentDir, entry.name);
185
+ const relativePath = basePath ? join(basePath, entry.name) : entry.name;
186
+ if (entry.isDirectory()) {
187
+ await walk(fullPath, relativePath);
188
+ } else if (entry.isFile()) {
189
+ await callback(fullPath, relativePath);
190
+ }
191
+ }
192
+ } catch (error) {
193
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT" && error.code !== "EACCES") {
194
+ throw error;
195
+ }
196
+ }
197
+ };
198
+ await walk(dir);
199
+ }
200
+ };
201
+
202
+ export {
203
+ VueRouterScanner
204
+ };
@@ -0,0 +1,99 @@
1
+ // src/scanners/remix.ts
2
+ import { readdir } from "fs/promises";
3
+ import { join } from "path";
4
+ import { existsSync } from "fs";
5
+ var RemixScanner = class {
6
+ constructor() {
7
+ this.cwd = process.cwd();
8
+ }
9
+ async scan() {
10
+ const routes = [];
11
+ const routesDir = join(this.cwd, "app", "routes");
12
+ if (!existsSync(routesDir)) {
13
+ return {
14
+ routes,
15
+ framework: "remix"
16
+ };
17
+ }
18
+ try {
19
+ const entries = await readdir(routesDir, { withFileTypes: true });
20
+ for (const entry of entries) {
21
+ if (!entry.isFile()) {
22
+ continue;
23
+ }
24
+ const fileName = entry.name;
25
+ if (!fileName.match(/\.(tsx?|jsx?)$/)) {
26
+ continue;
27
+ }
28
+ const routePath = this.convertRemixRoute(fileName);
29
+ if (routePath) {
30
+ routes.push({
31
+ path: routePath,
32
+ isDynamic: this.isDynamicRoute(routePath),
33
+ framework: "remix"
34
+ });
35
+ }
36
+ }
37
+ } catch (error) {
38
+ return {
39
+ routes,
40
+ framework: "remix"
41
+ };
42
+ }
43
+ return {
44
+ routes,
45
+ framework: "remix"
46
+ };
47
+ }
48
+ /**
49
+ * Convert Remix flat file route convention to route path
50
+ * Examples:
51
+ * - "_index.tsx" → "/"
52
+ * - "about.tsx" → "/about"
53
+ * - "dashboard.users.tsx" → "/dashboard/users"
54
+ * - "$userId.tsx" → "/:userId"
55
+ * - "blog.$slug.tsx" → "/blog/:slug"
56
+ * - "$.tsx" → "/*" (splat route)
57
+ * - "_layout.tsx" → null (layout file, not a route)
58
+ * - "posts._index.tsx" → "/posts"
59
+ */
60
+ convertRemixRoute(fileName) {
61
+ let name = fileName.replace(/\.(tsx?|jsx?)$/, "");
62
+ if (name.startsWith("_") && name !== "_index") {
63
+ return null;
64
+ }
65
+ if (name === "_index") {
66
+ return "/";
67
+ }
68
+ const segments = name.split(".");
69
+ const processedSegments = [];
70
+ for (let i = 0; i < segments.length; i++) {
71
+ const segment = segments[i];
72
+ if (segment === "_index") {
73
+ continue;
74
+ }
75
+ if (segment === "$") {
76
+ processedSegments.push("*");
77
+ continue;
78
+ }
79
+ if (segment.startsWith("$")) {
80
+ const param = segment.slice(1);
81
+ processedSegments.push(":" + param);
82
+ continue;
83
+ }
84
+ processedSegments.push(segment);
85
+ }
86
+ const path = "/" + processedSegments.join("/");
87
+ return path;
88
+ }
89
+ /**
90
+ * Check if a route path contains dynamic segments
91
+ */
92
+ isDynamicRoute(path) {
93
+ return path.includes(":") || path.includes("*");
94
+ }
95
+ };
96
+
97
+ export {
98
+ RemixScanner
99
+ };