@mrpatronz/nexusflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/GETTING_STARTED.md +109 -0
  2. package/README.md +310 -0
  3. package/dist/analyzers/detect-apis.d.ts +19 -0
  4. package/dist/analyzers/detect-apis.d.ts.map +1 -0
  5. package/dist/analyzers/detect-apis.js +121 -0
  6. package/dist/analyzers/detect-apis.js.map +1 -0
  7. package/dist/analyzers/detect-deps.d.ts +30 -0
  8. package/dist/analyzers/detect-deps.d.ts.map +1 -0
  9. package/dist/analyzers/detect-deps.js +121 -0
  10. package/dist/analyzers/detect-deps.js.map +1 -0
  11. package/dist/analyzers/detect-existing.d.ts +15 -0
  12. package/dist/analyzers/detect-existing.d.ts.map +1 -0
  13. package/dist/analyzers/detect-existing.js +65 -0
  14. package/dist/analyzers/detect-existing.js.map +1 -0
  15. package/dist/analyzers/detect-ports.d.ts +20 -0
  16. package/dist/analyzers/detect-ports.d.ts.map +1 -0
  17. package/dist/analyzers/detect-ports.js +103 -0
  18. package/dist/analyzers/detect-ports.js.map +1 -0
  19. package/dist/analyzers/index.d.ts +29 -0
  20. package/dist/analyzers/index.d.ts.map +1 -0
  21. package/dist/analyzers/index.js +74 -0
  22. package/dist/analyzers/index.js.map +1 -0
  23. package/dist/analyzers/readme-summarizer.d.ts +13 -0
  24. package/dist/analyzers/readme-summarizer.d.ts.map +1 -0
  25. package/dist/analyzers/readme-summarizer.js +59 -0
  26. package/dist/analyzers/readme-summarizer.js.map +1 -0
  27. package/dist/analyzers/tech-stack.d.ts +15 -0
  28. package/dist/analyzers/tech-stack.d.ts.map +1 -0
  29. package/dist/analyzers/tech-stack.js +284 -0
  30. package/dist/analyzers/tech-stack.js.map +1 -0
  31. package/dist/commands/create.d.ts +17 -0
  32. package/dist/commands/create.d.ts.map +1 -0
  33. package/dist/commands/create.js +111 -0
  34. package/dist/commands/create.js.map +1 -0
  35. package/dist/commands/init.d.ts +10 -0
  36. package/dist/commands/init.d.ts.map +1 -0
  37. package/dist/commands/init.js +50 -0
  38. package/dist/commands/init.js.map +1 -0
  39. package/dist/commands/list.d.ts +9 -0
  40. package/dist/commands/list.d.ts.map +1 -0
  41. package/dist/commands/list.js +32 -0
  42. package/dist/commands/list.js.map +1 -0
  43. package/dist/commands/logs.d.ts +12 -0
  44. package/dist/commands/logs.d.ts.map +1 -0
  45. package/dist/commands/logs.js +54 -0
  46. package/dist/commands/logs.js.map +1 -0
  47. package/dist/commands/open.d.ts +9 -0
  48. package/dist/commands/open.d.ts.map +1 -0
  49. package/dist/commands/open.js +49 -0
  50. package/dist/commands/open.js.map +1 -0
  51. package/dist/commands/start.d.ts +13 -0
  52. package/dist/commands/start.d.ts.map +1 -0
  53. package/dist/commands/start.js +97 -0
  54. package/dist/commands/start.js.map +1 -0
  55. package/dist/commands/status.d.ts +11 -0
  56. package/dist/commands/status.d.ts.map +1 -0
  57. package/dist/commands/status.js +47 -0
  58. package/dist/commands/status.js.map +1 -0
  59. package/dist/commands/stop.d.ts +11 -0
  60. package/dist/commands/stop.d.ts.map +1 -0
  61. package/dist/commands/stop.js +47 -0
  62. package/dist/commands/stop.js.map +1 -0
  63. package/dist/commands/ui.d.ts +13 -0
  64. package/dist/commands/ui.d.ts.map +1 -0
  65. package/dist/commands/ui.js +44 -0
  66. package/dist/commands/ui.js.map +1 -0
  67. package/dist/core/config.d.ts +32 -0
  68. package/dist/core/config.d.ts.map +1 -0
  69. package/dist/core/config.js +68 -0
  70. package/dist/core/config.js.map +1 -0
  71. package/dist/core/scanner.d.ts +18 -0
  72. package/dist/core/scanner.d.ts.map +1 -0
  73. package/dist/core/scanner.js +73 -0
  74. package/dist/core/scanner.js.map +1 -0
  75. package/dist/core/workspace.d.ts +53 -0
  76. package/dist/core/workspace.d.ts.map +1 -0
  77. package/dist/core/workspace.js +114 -0
  78. package/dist/core/workspace.js.map +1 -0
  79. package/dist/core/worktree.d.ts +27 -0
  80. package/dist/core/worktree.d.ts.map +1 -0
  81. package/dist/core/worktree.js +35 -0
  82. package/dist/core/worktree.js.map +1 -0
  83. package/dist/generators/base.d.ts +15 -0
  84. package/dist/generators/base.d.ts.map +1 -0
  85. package/dist/generators/base.js +175 -0
  86. package/dist/generators/base.js.map +1 -0
  87. package/dist/generators/claude.d.ts +17 -0
  88. package/dist/generators/claude.d.ts.map +1 -0
  89. package/dist/generators/claude.js +29 -0
  90. package/dist/generators/claude.js.map +1 -0
  91. package/dist/generators/codex.d.ts +16 -0
  92. package/dist/generators/codex.d.ts.map +1 -0
  93. package/dist/generators/codex.js +41 -0
  94. package/dist/generators/codex.js.map +1 -0
  95. package/dist/generators/copilot.d.ts +16 -0
  96. package/dist/generators/copilot.d.ts.map +1 -0
  97. package/dist/generators/copilot.js +49 -0
  98. package/dist/generators/copilot.js.map +1 -0
  99. package/dist/generators/cursor.d.ts +17 -0
  100. package/dist/generators/cursor.d.ts.map +1 -0
  101. package/dist/generators/cursor.js +37 -0
  102. package/dist/generators/cursor.js.map +1 -0
  103. package/dist/generators/index.d.ts +18 -0
  104. package/dist/generators/index.d.ts.map +1 -0
  105. package/dist/generators/index.js +95 -0
  106. package/dist/generators/index.js.map +1 -0
  107. package/dist/gui/assets/index-Cq9V485S.js +21 -0
  108. package/dist/gui/assets/index-Dh1b5gSZ.css +2 -0
  109. package/dist/gui/favicon.svg +1 -0
  110. package/dist/gui/icons.svg +24 -0
  111. package/dist/gui/index.html +18 -0
  112. package/dist/index.d.ts +11 -0
  113. package/dist/index.d.ts.map +1 -0
  114. package/dist/index.js +169 -0
  115. package/dist/index.js.map +1 -0
  116. package/dist/orchestration/detect.d.ts +29 -0
  117. package/dist/orchestration/detect.d.ts.map +1 -0
  118. package/dist/orchestration/detect.js +292 -0
  119. package/dist/orchestration/detect.js.map +1 -0
  120. package/dist/orchestration/index.d.ts +7 -0
  121. package/dist/orchestration/index.d.ts.map +1 -0
  122. package/dist/orchestration/index.js +7 -0
  123. package/dist/orchestration/index.js.map +1 -0
  124. package/dist/orchestration/runner.d.ts +40 -0
  125. package/dist/orchestration/runner.d.ts.map +1 -0
  126. package/dist/orchestration/runner.js +242 -0
  127. package/dist/orchestration/runner.js.map +1 -0
  128. package/dist/server.d.ts +16 -0
  129. package/dist/server.d.ts.map +1 -0
  130. package/dist/server.js +419 -0
  131. package/dist/server.js.map +1 -0
  132. package/dist/types.d.ts +210 -0
  133. package/dist/types.d.ts.map +1 -0
  134. package/dist/types.js +6 -0
  135. package/dist/types.js.map +1 -0
  136. package/dist/utils/detect-ai.d.ts +21 -0
  137. package/dist/utils/detect-ai.d.ts.map +1 -0
  138. package/dist/utils/detect-ai.js +76 -0
  139. package/dist/utils/detect-ai.js.map +1 -0
  140. package/dist/utils/detect-editors.d.ts +19 -0
  141. package/dist/utils/detect-editors.d.ts.map +1 -0
  142. package/dist/utils/detect-editors.js +49 -0
  143. package/dist/utils/detect-editors.js.map +1 -0
  144. package/dist/utils/git.d.ts +27 -0
  145. package/dist/utils/git.d.ts.map +1 -0
  146. package/dist/utils/git.js +57 -0
  147. package/dist/utils/git.js.map +1 -0
  148. package/dist/utils/prompts.d.ts +27 -0
  149. package/dist/utils/prompts.d.ts.map +1 -0
  150. package/dist/utils/prompts.js +119 -0
  151. package/dist/utils/prompts.js.map +1 -0
  152. package/dist/utils/session-finder.d.ts +29 -0
  153. package/dist/utils/session-finder.d.ts.map +1 -0
  154. package/dist/utils/session-finder.js +462 -0
  155. package/dist/utils/session-finder.js.map +1 -0
  156. package/gui/README.md +73 -0
  157. package/gui/eslint.config.js +22 -0
  158. package/gui/index.html +17 -0
  159. package/gui/package-lock.json +3054 -0
  160. package/gui/package.json +33 -0
  161. package/gui/public/favicon.svg +1 -0
  162. package/gui/public/icons.svg +24 -0
  163. package/gui/src/App.css +1 -0
  164. package/gui/src/App.tsx +1949 -0
  165. package/gui/src/assets/hero.png +0 -0
  166. package/gui/src/assets/react.svg +1 -0
  167. package/gui/src/assets/vite.svg +1 -0
  168. package/gui/src/index.css +1 -0
  169. package/gui/src/main.tsx +10 -0
  170. package/gui/tsconfig.app.json +25 -0
  171. package/gui/tsconfig.json +7 -0
  172. package/gui/tsconfig.node.json +24 -0
  173. package/gui/vite.config.ts +12 -0
  174. package/package.json +48 -0
  175. package/src/analyzers/detect-apis.ts +138 -0
  176. package/src/analyzers/detect-deps.ts +138 -0
  177. package/src/analyzers/detect-existing.ts +74 -0
  178. package/src/analyzers/detect-ports.ts +110 -0
  179. package/src/analyzers/index.ts +85 -0
  180. package/src/analyzers/readme-summarizer.ts +68 -0
  181. package/src/analyzers/tech-stack.ts +255 -0
  182. package/src/commands/create.ts +142 -0
  183. package/src/commands/init.ts +58 -0
  184. package/src/commands/list.ts +46 -0
  185. package/src/commands/logs.ts +63 -0
  186. package/src/commands/open.ts +54 -0
  187. package/src/commands/start.ts +117 -0
  188. package/src/commands/status.ts +54 -0
  189. package/src/commands/stop.ts +55 -0
  190. package/src/commands/ui.ts +47 -0
  191. package/src/core/config.ts +79 -0
  192. package/src/core/scanner.ts +91 -0
  193. package/src/core/workspace.ts +149 -0
  194. package/src/core/worktree.ts +50 -0
  195. package/src/generators/base.ts +187 -0
  196. package/src/generators/claude.ts +34 -0
  197. package/src/generators/codex.ts +48 -0
  198. package/src/generators/copilot.ts +56 -0
  199. package/src/generators/cursor.ts +44 -0
  200. package/src/generators/index.ts +122 -0
  201. package/src/index.ts +178 -0
  202. package/src/orchestration/detect.ts +313 -0
  203. package/src/orchestration/index.ts +7 -0
  204. package/src/orchestration/runner.ts +279 -0
  205. package/src/server.ts +466 -0
  206. package/src/types.ts +294 -0
  207. package/src/utils/detect-ai.ts +81 -0
  208. package/src/utils/detect-editors.ts +54 -0
  209. package/src/utils/git.ts +61 -0
  210. package/src/utils/prompts.ts +135 -0
  211. package/src/utils/session-finder.ts +483 -0
  212. package/tsconfig.json +19 -0
package/src/types.ts ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ * @module types
3
+ * Shared interfaces and types for NexusFlow.
4
+ */
5
+
6
+ /** Supported AI assistant identifiers. */
7
+ export type AIAssistant = 'claude' | 'antigravity' | 'codex' | 'copilot' | 'cursor';
8
+
9
+ /** Top-level NexusFlow configuration stored in ~/.nexusflow/config.json. */
10
+ export interface NexusFlowConfig {
11
+ /** Semantic version of the NexusFlow config schema. */
12
+ version: string;
13
+
14
+ /** Root development directory. Default: ~/dev */
15
+ devDir: string;
16
+
17
+ /** Directory where workspaces are created. Default: ~/dev/workspaces */
18
+ workspacesDir: string;
19
+
20
+ /** The user's preferred AI assistant, or null if none chosen yet. */
21
+ defaultAssistant: AIAssistant | null;
22
+
23
+ /** How many directory levels deep to scan for git repos. Default: 2 */
24
+ scanDepth: number;
25
+ }
26
+
27
+ /** Result of probing for an AI assistant on the system. */
28
+ export interface DetectedAI {
29
+ /** Canonical assistant identifier. */
30
+ name: AIAssistant;
31
+
32
+ /** Human-readable label shown in prompts. */
33
+ displayName: string;
34
+
35
+ /** Whether the assistant's CLI was found. */
36
+ detected: boolean;
37
+
38
+ /** The CLI command that was detected (e.g. 'claude', 'antigravity'). */
39
+ command?: string;
40
+ }
41
+
42
+ /** Result of probing for a code editor on the system. */
43
+ export interface DetectedEditor {
44
+ /** Human-readable editor name. */
45
+ name: string;
46
+
47
+ /** CLI command used to launch the editor. */
48
+ command: string;
49
+
50
+ /** Whether the editor's CLI was found in PATH. */
51
+ detected: boolean;
52
+ }
53
+
54
+ /** Metadata about a discovered git repository. */
55
+ export interface RepoInfo {
56
+ /** Directory name of the repo. */
57
+ name: string;
58
+
59
+ /** Absolute path to the repo root. */
60
+ path: string;
61
+
62
+ /** Default branch — typically 'main' or 'master'. */
63
+ defaultBranch: string;
64
+ }
65
+
66
+ /** Metadata about a past AI session. */
67
+ export interface AISession {
68
+ id: string;
69
+ assistant: AIAssistant;
70
+ title: string;
71
+ createdAt: string;
72
+ updatedAt: string;
73
+ messageCount: number;
74
+ workspacePath: string;
75
+ }
76
+
77
+ /** A single chat message in a session transcript. */
78
+ export interface ChatMessage {
79
+ role: 'user' | 'assistant';
80
+ content: string;
81
+ timestamp?: string;
82
+ }
83
+
84
+
85
+ /** Configuration for resuming the active session. */
86
+ export interface ResumptionConfig {
87
+ /** The command to run verification tests. */
88
+ testCommand?: string;
89
+ /** The command to spin up dev databases/mocks/caches. */
90
+ mockCommand?: string;
91
+ /** The command to start workspace services manually if needed. */
92
+ startCommand?: string;
93
+ }
94
+
95
+ /** A feature workspace that spans one or more repos. */
96
+ export interface Feature {
97
+ /** Unique identifier — the git branch name. */
98
+ id: string;
99
+
100
+ /** Git branch name created for this feature. */
101
+ branchName: string;
102
+
103
+ /** Short human-readable description of the feature. */
104
+ description: string;
105
+
106
+ /** Absolute paths to the repos included in this feature. */
107
+ repos: string[];
108
+
109
+ /** AI assistants enabled for this feature workspace. */
110
+ assistants: AIAssistant[];
111
+
112
+ /** Absolute path to the workspace directory on disk. */
113
+ workspacePath: string;
114
+
115
+ /** ISO-8601 timestamp of when the feature was created. */
116
+ createdAt: string;
117
+
118
+ /** Resumption configuration. */
119
+ resumption?: ResumptionConfig;
120
+ }
121
+
122
+ /** Runtime context for an active workspace — now includes analysis data. */
123
+ export interface WorkspaceContext {
124
+ /** The feature definition. */
125
+ feature: Feature;
126
+
127
+ /** Full repo metadata for every repo in the feature. */
128
+ repos: RepoInfo[];
129
+
130
+ /** Analysis results for each repo (keyed by repo path). */
131
+ analysis?: Map<string, ProjectAnalysis>;
132
+ }
133
+
134
+ // ─── Phase 2: Project Analysis Types ──────────────────────────────────────
135
+
136
+ /** Recognized language/runtime. */
137
+ export type Language =
138
+ | 'typescript'
139
+ | 'javascript'
140
+ | 'csharp'
141
+ | 'python'
142
+ | 'go'
143
+ | 'java'
144
+ | 'rust'
145
+ | 'ruby'
146
+ | 'php'
147
+ | 'other';
148
+
149
+ /** Recognized framework. */
150
+ export type Framework =
151
+ | 'react'
152
+ | 'nextjs'
153
+ | 'angular'
154
+ | 'vue'
155
+ | 'svelte'
156
+ | 'express'
157
+ | 'nestjs'
158
+ | 'fastify'
159
+ | 'hono'
160
+ | 'aspnet'
161
+ | 'blazor'
162
+ | 'django'
163
+ | 'flask'
164
+ | 'fastapi'
165
+ | 'gin'
166
+ | 'spring'
167
+ | 'rails'
168
+ | 'laravel'
169
+ | 'other';
170
+
171
+ /** Recognized project type. */
172
+ export type ProjectType =
173
+ | 'frontend'
174
+ | 'backend'
175
+ | 'fullstack'
176
+ | 'library'
177
+ | 'api'
178
+ | 'cli'
179
+ | 'worker'
180
+ | 'other';
181
+
182
+ /** Detected tech stack for a repo. */
183
+ export interface TechStack {
184
+ languages: Language[];
185
+ frameworks: Framework[];
186
+ buildTools: string[];
187
+ projectType: ProjectType;
188
+ }
189
+
190
+ /** A detected API endpoint or route. */
191
+ export interface ApiEndpoint {
192
+ method: string;
193
+ path: string;
194
+ source?: string;
195
+ }
196
+
197
+ /** An inter-repo dependency reference. */
198
+ export interface RepoDependency {
199
+ /** Name of the dependency (npm package, NuGet package, etc.). */
200
+ name: string;
201
+ /** Type of dependency. */
202
+ type: 'npm' | 'nuget' | 'pip' | 'go' | 'other';
203
+ /** Version constraint if known. */
204
+ version?: string;
205
+ }
206
+
207
+ /** Port/service configuration detected in a repo. */
208
+ export interface ServicePort {
209
+ port: number;
210
+ protocol: 'http' | 'https' | 'grpc' | 'other';
211
+ source: string;
212
+ }
213
+
214
+ /** Full analysis result for a single project/repo. */
215
+ export interface ProjectAnalysis {
216
+ /** Repo name. */
217
+ name: string;
218
+ /** Repo path. */
219
+ path: string;
220
+ /** Detected tech stack. */
221
+ techStack: TechStack;
222
+ /** Detected API endpoints. */
223
+ endpoints: ApiEndpoint[];
224
+ /** Detected dependencies. */
225
+ dependencies: RepoDependency[];
226
+ /** Detected ports/services. */
227
+ ports: ServicePort[];
228
+ /** Summary from README.md if found. */
229
+ readmeSummary: string | null;
230
+ /** Existing AI config files found in the repo. */
231
+ existingAIConfigs: ExistingAIConfig[];
232
+ }
233
+
234
+ /** An existing AI configuration file found in a repo. */
235
+ export interface ExistingAIConfig {
236
+ /** Which assistant this config is for. */
237
+ assistant: AIAssistant | 'agents';
238
+ /** File path relative to repo root. */
239
+ relativePath: string;
240
+ /** First 500 chars of the content for context. */
241
+ contentPreview: string;
242
+ }
243
+
244
+ // ─── Phase 3: Service Orchestration Types ─────────────────────────────────
245
+
246
+ /** How to start a single service/project. */
247
+ export interface ServiceConfig {
248
+ /** Display name for the service. */
249
+ name: string;
250
+ /** Absolute path to the project directory. */
251
+ cwd: string;
252
+ /** The command to run (e.g., 'npm', 'dotnet', 'python'). */
253
+ command: string;
254
+ /** Arguments for the command. */
255
+ args: string[];
256
+ /** Optional port the service listens on. */
257
+ port?: number;
258
+ /** How the start command was detected. */
259
+ source: 'package.json' | 'dotnet' | 'docker-compose' | 'aspire' | 'python' | 'go' | 'makefile' | 'manual';
260
+ }
261
+
262
+ /** Detected orchestration tool in a workspace. */
263
+ export interface OrchestrationDetection {
264
+ /** Which tool was found. */
265
+ tool: 'docker-compose' | 'aspire' | 'tilt' | 'procfile' | 'makefile';
266
+ /** Path to the config file. */
267
+ configPath: string;
268
+ /** Start command for this tool. */
269
+ startCommand: string;
270
+ /** Stop command for this tool. */
271
+ stopCommand: string;
272
+ }
273
+
274
+ /** A currently running service process. */
275
+ export interface RunningService {
276
+ /** Service name. */
277
+ name: string;
278
+ /** Process ID. */
279
+ pid: number;
280
+ /** Service config used to start it. */
281
+ config: ServiceConfig;
282
+ /** Timestamp when started. */
283
+ startedAt: string;
284
+ }
285
+
286
+ /** State file saved to track running services. */
287
+ export interface RunningState {
288
+ /** Workspace path this state belongs to. */
289
+ workspacePath: string;
290
+ /** List of running services. */
291
+ services: RunningService[];
292
+ /** Timestamp when the state was last updated. */
293
+ updatedAt: string;
294
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @module utils/detect-ai
3
+ * Detects which AI coding assistants are available on the system.
4
+ */
5
+
6
+ import { execa } from 'execa';
7
+
8
+ import type { AIAssistant, DetectedAI } from '../types.js';
9
+
10
+ /**
11
+ * Attempts to run `<command> --version` and returns `true` if the process
12
+ * exits successfully (exit code 0).
13
+ */
14
+ async function commandExists(command: string): Promise<boolean> {
15
+ try {
16
+ const result = await execa(command, ['--version'], { reject: false });
17
+ return result.exitCode === 0;
18
+ } catch {
19
+ // The command could not be spawned at all (not in PATH).
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Probes the system for known AI coding assistants and returns their status.
26
+ *
27
+ * Detection rules:
28
+ * - **Claude**: detected if `claude` *or* `antigravity` is on PATH.
29
+ * When Antigravity is found it is labelled as `'claude'` because it
30
+ * generates `CLAUDE.md` context files.
31
+ * - **Codex**: detected if `codex` is on PATH.
32
+ * - **Copilot**: always shown as an option (convention-based via
33
+ * `.github/copilot-instructions.md`).
34
+ * - **Cursor**: detected if `cursor` is on PATH.
35
+ *
36
+ * @returns An array of {@link DetectedAI} results, one per assistant.
37
+ */
38
+ export async function detectAIAssistants(): Promise<DetectedAI[]> {
39
+ // Run all probes concurrently.
40
+ const [hasClaude, hasAntigravity, hasCodex, hasCursor] = await Promise.all([
41
+ commandExists('claude'),
42
+ commandExists('agy'),
43
+ commandExists('codex'),
44
+ commandExists('cursor'),
45
+ ]);
46
+
47
+ const results: DetectedAI[] = [
48
+ {
49
+ name: 'claude' as AIAssistant,
50
+ displayName: 'Claude Code',
51
+ detected: hasClaude,
52
+ ...(hasClaude ? { command: 'claude' } : {}),
53
+ },
54
+ {
55
+ name: 'antigravity' as AIAssistant,
56
+ displayName: 'Antigravity',
57
+ detected: hasAntigravity,
58
+ ...(hasAntigravity ? { command: 'agy' } : {}),
59
+ },
60
+ {
61
+ name: 'codex' as AIAssistant,
62
+ displayName: 'OpenAI Codex',
63
+ detected: hasCodex,
64
+ ...(hasCodex ? { command: 'codex' } : {}),
65
+ },
66
+ {
67
+ name: 'copilot' as AIAssistant,
68
+ displayName: 'GitHub Copilot',
69
+ // Copilot is convention-based; always available as an option.
70
+ detected: true,
71
+ },
72
+ {
73
+ name: 'cursor' as AIAssistant,
74
+ displayName: 'Cursor',
75
+ detected: hasCursor,
76
+ ...(hasCursor ? { command: 'cursor' } : {}),
77
+ },
78
+ ];
79
+
80
+ return results;
81
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @module utils/detect-editors
3
+ * Detects which code editors are available on the system.
4
+ */
5
+
6
+ import { execa } from 'execa';
7
+
8
+ import type { DetectedEditor } from '../types.js';
9
+
10
+ /** Editor definitions we probe for. */
11
+ const EDITOR_CANDIDATES: ReadonlyArray<{ name: string; command: string }> = [
12
+ { name: 'VS Code', command: 'code' },
13
+ { name: 'VS Code Insiders', command: 'code-insiders' },
14
+ { name: 'Cursor', command: 'cursor' },
15
+ { name: 'Antigravity', command: 'agy' },
16
+ ];
17
+
18
+ /**
19
+ * Attempts to run `<command> --version` and returns `true` if the process
20
+ * exits successfully (exit code 0).
21
+ */
22
+ async function commandExists(command: string): Promise<boolean> {
23
+ try {
24
+ const result = await execa(command, ['--version'], { reject: false });
25
+ return result.exitCode === 0;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Probes the system for known code editors by checking whether their CLI
33
+ * commands are available on PATH.
34
+ *
35
+ * Currently checks for:
36
+ * - **VS Code** (`code`)
37
+ * - **VS Code Insiders** (`code-insiders`)
38
+ * - **Cursor** (`cursor`)
39
+ * - **Antigravity** (`antigravity`)
40
+ *
41
+ * @returns An array of {@link DetectedEditor} results, one per editor.
42
+ */
43
+ export async function detectEditors(): Promise<DetectedEditor[]> {
44
+ const probes = EDITOR_CANDIDATES.map(async (editor) => {
45
+ const detected = await commandExists(editor.command);
46
+ return {
47
+ name: editor.name,
48
+ command: editor.command,
49
+ detected,
50
+ } satisfies DetectedEditor;
51
+ });
52
+
53
+ return Promise.all(probes);
54
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module utils/git
3
+ * Low-level git helper functions used throughout NexusFlow.
4
+ */
5
+
6
+ import * as fs from 'node:fs/promises';
7
+ import * as path from 'node:path';
8
+ import { execa } from 'execa';
9
+
10
+ /**
11
+ * Checks whether the given directory is a git repository
12
+ * by testing for the presence of a `.git` entry.
13
+ *
14
+ * @param dirPath - Absolute path to the directory.
15
+ * @returns `true` if a `.git` file or directory exists.
16
+ */
17
+ export async function isGitRepo(dirPath: string): Promise<boolean> {
18
+ try {
19
+ const gitPath = path.join(dirPath, '.git');
20
+ await fs.access(gitPath);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Runs `git fetch origin` inside the given repository.
29
+ *
30
+ * @param repoPath - Absolute path to the repo root.
31
+ */
32
+ export async function gitFetch(repoPath: string): Promise<void> {
33
+ await execa('git', ['fetch', 'origin'], { cwd: repoPath });
34
+ }
35
+
36
+ /**
37
+ * Detects the default branch of a repository by inspecting remote branches.
38
+ * Prefers `main` over `master`. Falls back to `'main'` when neither is found.
39
+ *
40
+ * @param repoPath - Absolute path to the repo root.
41
+ * @returns The name of the default branch.
42
+ */
43
+ export async function detectDefaultBranch(repoPath: string): Promise<string> {
44
+ try {
45
+ const { stdout } = await execa('git', ['branch', '-r'], { cwd: repoPath });
46
+ const branches = stdout.split('\n').map((b) => b.trim());
47
+
48
+ if (branches.some((b) => b === 'origin/main' || b.endsWith('/origin/main'))) {
49
+ return 'main';
50
+ }
51
+
52
+ if (branches.some((b) => b === 'origin/master' || b.endsWith('/origin/master'))) {
53
+ return 'master';
54
+ }
55
+
56
+ // Fallback — assume main.
57
+ return 'main';
58
+ } catch {
59
+ return 'main';
60
+ }
61
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @module utils/prompts
3
+ * Interactive CLI prompts for NexusFlow using @inquirer/prompts.
4
+ */
5
+
6
+ import { input, checkbox, confirm } from '@inquirer/prompts';
7
+ import chalk from 'chalk';
8
+
9
+ import type { AIAssistant, DetectedAI, DetectedEditor, RepoInfo } from '../types.js';
10
+
11
+ /**
12
+ * Prompts the user for a feature branch name.
13
+ */
14
+ export async function promptBranchName(): Promise<string> {
15
+ const branchName = await input({
16
+ message: 'Feature branch name:',
17
+ validate: (value: string) => {
18
+ if (!value.trim()) return 'Branch name cannot be empty';
19
+ if (/\s/.test(value)) return 'Branch name cannot contain spaces';
20
+ return true;
21
+ },
22
+ });
23
+ return branchName.trim();
24
+ }
25
+
26
+ /**
27
+ * Prompts the user for a feature description.
28
+ * Accepts plain text or a path to a file.
29
+ */
30
+ export async function promptDescription(): Promise<string> {
31
+ const { readFile } = await import('node:fs/promises');
32
+ const { existsSync } = await import('node:fs');
33
+
34
+ const descInput = await input({
35
+ message: 'Describe the feature (or paste a path to a .md/.txt file):',
36
+ validate: (value: string) => {
37
+ if (!value.trim()) return 'Description cannot be empty';
38
+ return true;
39
+ },
40
+ });
41
+
42
+ const trimmed = descInput.trim();
43
+
44
+ // Check if it's a file path
45
+ if (existsSync(trimmed)) {
46
+ try {
47
+ const content = await readFile(trimmed, 'utf-8');
48
+ console.log(chalk.dim(` Read description from ${trimmed}`));
49
+ return content;
50
+ } catch {
51
+ // Fall through to use as plain text
52
+ }
53
+ }
54
+
55
+ return trimmed;
56
+ }
57
+
58
+ /**
59
+ * Prompts the user to select repos from a list of discovered repos.
60
+ */
61
+ export async function promptSelectRepos(repos: RepoInfo[]): Promise<RepoInfo[]> {
62
+ if (repos.length === 0) {
63
+ return [];
64
+ }
65
+
66
+ const selected = await checkbox({
67
+ message: 'Select projects to include:',
68
+ choices: repos.map((repo) => ({
69
+ name: `${repo.name} ${chalk.dim(`(${repo.path})`)}`,
70
+ value: repo.path,
71
+ checked: false,
72
+ })),
73
+ validate: (items) => {
74
+ if (items.length === 0) return 'Please select at least one project';
75
+ return true;
76
+ },
77
+ });
78
+
79
+ return repos.filter((r) => selected.includes(r.path));
80
+ }
81
+
82
+ /**
83
+ * Prompts the user to select AI assistants to generate context for.
84
+ */
85
+ export async function promptSelectAI(
86
+ detected: DetectedAI[],
87
+ ): Promise<AIAssistant[]> {
88
+ const selected = await checkbox({
89
+ message: 'Select AI assistant(s) to generate context for:',
90
+ choices: detected.map((ai) => ({
91
+ name: `${ai.displayName} ${ai.detected ? chalk.green('✓ detected') : chalk.dim('not found')}`,
92
+ value: ai.name,
93
+ checked: ai.detected,
94
+ })),
95
+ validate: (items) => {
96
+ if (items.length === 0) return 'Please select at least one AI assistant';
97
+ return true;
98
+ },
99
+ });
100
+
101
+ return selected as AIAssistant[];
102
+ }
103
+
104
+ /**
105
+ * Prompts the user to select an editor to open the workspace in.
106
+ */
107
+ export async function promptSelectEditor(
108
+ detected: DetectedEditor[],
109
+ ): Promise<DetectedEditor | null> {
110
+ const shouldOpen = await confirm({
111
+ message: 'Open workspace in an editor?',
112
+ default: true,
113
+ });
114
+
115
+ if (!shouldOpen) return null;
116
+
117
+ const available = detected.filter((e) => e.detected);
118
+
119
+ if (available.length === 0) {
120
+ console.log(chalk.yellow(' No editors detected in PATH.'));
121
+ return null;
122
+ }
123
+
124
+ const selected = await checkbox({
125
+ message: 'Select editor:',
126
+ choices: available.map((editor) => ({
127
+ name: editor.name,
128
+ value: editor.command,
129
+ })),
130
+ });
131
+
132
+ if (selected.length === 0) return null;
133
+
134
+ return available.find((e) => e.command === selected[0]) ?? null;
135
+ }