forge-remote 0.1.1 → 0.1.3

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.
@@ -1,9 +1,10 @@
1
- import { readdirSync, statSync, existsSync } from "fs";
1
+ import { readdirSync, readFileSync, statSync, existsSync } from "fs";
2
2
  import { join, basename } from "path";
3
3
  import { homedir } from "os";
4
+ import * as log from "./logger.js";
4
5
 
5
- // Directories to scan (one level deep) for projects.
6
- const SCAN_DIRS = [
6
+ // Default directories to scan for projects.
7
+ const DEFAULT_SCAN_DIRS = [
7
8
  join(homedir(), "Documents"),
8
9
  join(homedir(), "Projects"),
9
10
  join(homedir(), "Developer"),
@@ -15,6 +16,29 @@ const SCAN_DIRS = [
15
16
  join(homedir(), "Desktop"),
16
17
  ];
17
18
 
19
+ // Maximum depth to recurse into each scan directory.
20
+ const MAX_DEPTH = 4;
21
+
22
+ // Directories to skip during recursive scanning.
23
+ const SKIP_DIRS = new Set([
24
+ "node_modules",
25
+ ".git",
26
+ "__pycache__",
27
+ "target",
28
+ "build",
29
+ "dist",
30
+ ".next",
31
+ ".nuxt",
32
+ "vendor",
33
+ ".dart_tool",
34
+ ".pub-cache",
35
+ "Pods",
36
+ ".gradle",
37
+ "venv",
38
+ ".venv",
39
+ "env",
40
+ ]);
41
+
18
42
  // Files that indicate a directory is a project root.
19
43
  const PROJECT_MARKERS = [
20
44
  ".git",
@@ -29,8 +53,34 @@ const PROJECT_MARKERS = [
29
53
  "build.gradle",
30
54
  "Makefile",
31
55
  "CMakeLists.txt",
56
+ ".xcodeproj", // Xcode projects (check for any child matching)
57
+ "Podfile",
58
+ "composer.json",
59
+ "mix.exs",
60
+ "stack.yaml",
61
+ "dune-project",
32
62
  ];
33
63
 
64
+ /**
65
+ * Load user-configured extra scan paths from ~/.forge-remote/config.json.
66
+ * The config can contain a "scanPaths" array of absolute directory paths.
67
+ */
68
+ function loadExtraScanPaths() {
69
+ try {
70
+ const configPath = join(homedir(), ".forge-remote", "config.json");
71
+ if (!existsSync(configPath)) return [];
72
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
73
+ if (Array.isArray(config.scanPaths)) {
74
+ return config.scanPaths.filter(
75
+ (p) => typeof p === "string" && p.startsWith("/"),
76
+ );
77
+ }
78
+ } catch {
79
+ // Ignore config parse errors.
80
+ }
81
+ return [];
82
+ }
83
+
34
84
  /**
35
85
  * Scan common directories for projects.
36
86
  * Returns an array of { path, name, lastOpened } objects.
@@ -39,64 +89,80 @@ export async function scanProjects() {
39
89
  const projects = [];
40
90
  const seen = new Set();
41
91
 
42
- for (const scanDir of SCAN_DIRS) {
43
- if (!existsSync(scanDir)) continue;
92
+ const extraPaths = loadExtraScanPaths();
93
+ const scanDirs = [...DEFAULT_SCAN_DIRS, ...extraPaths];
44
94
 
45
- try {
46
- const entries = readdirSync(scanDir, { withFileTypes: true });
47
- for (const entry of entries) {
48
- if (!entry.isDirectory()) continue;
49
- if (entry.name.startsWith(".")) continue;
50
-
51
- const dirPath = join(scanDir, entry.name);
52
- if (seen.has(dirPath)) continue;
53
-
54
- if (isProject(dirPath)) {
55
- seen.add(dirPath);
56
- const stat = statSync(dirPath);
57
- projects.push({
58
- path: dirPath,
59
- name: basename(dirPath),
60
- lastOpened: stat.mtime.toISOString(),
61
- });
62
- }
63
-
64
- // Also scan one level deeper for nested project dirs
65
- // (e.g. ~/Documents/IronForgeApps/Mobile/ForgeRemote).
66
- try {
67
- const subEntries = readdirSync(dirPath, { withFileTypes: true });
68
- for (const sub of subEntries) {
69
- if (!sub.isDirectory() || sub.name.startsWith(".")) continue;
70
- const subPath = join(dirPath, sub.name);
71
- if (seen.has(subPath)) continue;
72
-
73
- if (isProject(subPath)) {
74
- seen.add(subPath);
75
- const stat = statSync(subPath);
76
- projects.push({
77
- path: subPath,
78
- name: basename(subPath),
79
- lastOpened: stat.mtime.toISOString(),
80
- });
81
- }
82
- }
83
- } catch {
84
- // Skip subdirs we can't read.
85
- }
86
- }
87
- } catch {
88
- // Skip dirs we can't read.
89
- }
95
+ log.info(`Scanning ${scanDirs.length} directories for projects...`);
96
+
97
+ for (const scanDir of scanDirs) {
98
+ if (!existsSync(scanDir)) continue;
99
+ scanRecursive(scanDir, 0, projects, seen);
90
100
  }
91
101
 
92
102
  // Sort by lastOpened descending.
93
103
  projects.sort((a, b) => b.lastOpened.localeCompare(a.lastOpened));
94
104
 
105
+ log.info(`Found ${projects.length} projects`);
95
106
  return projects;
96
107
  }
97
108
 
109
+ /**
110
+ * Recursively scan a directory for projects up to MAX_DEPTH.
111
+ * When a project is found, it is added to the list and we do NOT recurse
112
+ * into it (a project root's subdirectories are not separate projects).
113
+ */
114
+ function scanRecursive(dirPath, depth, projects, seen) {
115
+ if (depth > MAX_DEPTH) return;
116
+
117
+ let entries;
118
+ try {
119
+ entries = readdirSync(dirPath, { withFileTypes: true });
120
+ } catch {
121
+ return; // Permission denied or unreadable.
122
+ }
123
+
124
+ for (const entry of entries) {
125
+ if (!entry.isDirectory()) continue;
126
+ const name = entry.name;
127
+ if (name.startsWith(".") && name !== ".git") continue;
128
+ if (SKIP_DIRS.has(name)) continue;
129
+
130
+ const fullPath = join(dirPath, name);
131
+ if (seen.has(fullPath)) continue;
132
+
133
+ if (isProject(fullPath)) {
134
+ seen.add(fullPath);
135
+ try {
136
+ const stat = statSync(fullPath);
137
+ projects.push({
138
+ path: fullPath,
139
+ name: basename(fullPath),
140
+ lastOpened: stat.mtime.toISOString(),
141
+ });
142
+ } catch {
143
+ // stat failed — skip.
144
+ }
145
+ // Don't recurse into project directories.
146
+ continue;
147
+ }
148
+
149
+ // Not a project — recurse deeper.
150
+ scanRecursive(fullPath, depth + 1, projects, seen);
151
+ }
152
+ }
153
+
98
154
  function isProject(dirPath) {
99
155
  for (const marker of PROJECT_MARKERS) {
156
+ // For .xcodeproj, check if any child ends with .xcodeproj
157
+ if (marker === ".xcodeproj") {
158
+ try {
159
+ const children = readdirSync(dirPath);
160
+ if (children.some((c) => c.endsWith(".xcodeproj"))) return true;
161
+ } catch {
162
+ // ignore
163
+ }
164
+ continue;
165
+ }
100
166
  if (existsSync(join(dirPath, marker))) return true;
101
167
  }
102
168
  return false;