feat-forge 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +350 -0
  3. package/dist/cli.js +306 -0
  4. package/dist/commands/AbstractCommands.js +16 -0
  5. package/dist/commands/AgentCommands.js +14 -0
  6. package/dist/commands/BranchCommands.js +400 -0
  7. package/dist/commands/CompletionCommands.js +702 -0
  8. package/dist/commands/EnvCommands.js +56 -0
  9. package/dist/commands/FeatureCommands.js +4 -0
  10. package/dist/commands/FixCommands.js +4 -0
  11. package/dist/commands/InitCommands.js +380 -0
  12. package/dist/commands/MaintenanceCommands.js +39 -0
  13. package/dist/commands/ModeCommands.js +15 -0
  14. package/dist/commands/ProxyCommands.js +14 -0
  15. package/dist/commands/ReleaseCommands.js +4 -0
  16. package/dist/commands/ServicesCommands.js +95 -0
  17. package/dist/commands/SubBranchCommands.js +49 -0
  18. package/dist/commands/types/InitOptions.js +1 -0
  19. package/dist/foundation/BranchContext.js +427 -0
  20. package/dist/foundation/ForgeConfig.js +264 -0
  21. package/dist/foundation/ForgeConfigFile.js +391 -0
  22. package/dist/foundation/ForgeContext.js +169 -0
  23. package/dist/foundation/NpmHelper.js +131 -0
  24. package/dist/foundation/PathHelper.js +56 -0
  25. package/dist/foundation/PortAllocator.js +192 -0
  26. package/dist/foundation/Proxy.js +176 -0
  27. package/dist/foundation/Repository.js +431 -0
  28. package/dist/foundation/errors/ForgeError.js +9 -0
  29. package/dist/foundation/errors/_error.config.js +12 -0
  30. package/dist/foundation/errors/generated/ForgeBadStateError.js +11 -0
  31. package/dist/foundation/errors/generated/ForgeConfigError.js +11 -0
  32. package/dist/foundation/errors/generated/ForgeExpectMainRepositoryError.js +11 -0
  33. package/dist/foundation/errors/generated/ForgeModeNotDefinedError.js +11 -0
  34. package/dist/foundation/errors/generated/ForgeNotInActiveBranchError.js +11 -0
  35. package/dist/foundation/errors/generated/ForgePortAllocationsLoadError.js +11 -0
  36. package/dist/foundation/errors/generated/ForgePortNotAssignedError.js +11 -0
  37. package/dist/foundation/errors/generated/ForgePortRangeExhaustedError.js +11 -0
  38. package/dist/foundation/errors/generated/ForgeServicesScanError.js +11 -0
  39. package/dist/foundation/errors/generated/ForgeServicesValidationError.js +11 -0
  40. package/dist/foundation/errors/index.js +13 -0
  41. package/dist/foundation/types/AIAgent.js +1 -0
  42. package/dist/foundation/types/AIAgentName.js +11 -0
  43. package/dist/foundation/types/DeepPartial.js +1 -0
  44. package/dist/foundation/types/IDE.js +1 -0
  45. package/dist/foundation/types/IDEName.js +7 -0
  46. package/dist/foundation/types/ModeConfig.js +1 -0
  47. package/dist/foundation/types/RepositoryInfos.js +1 -0
  48. package/dist/foundation/types/Services.js +156 -0
  49. package/dist/foundation/types/ShellName.js +11 -0
  50. package/dist/lib/agents.js +47 -0
  51. package/dist/lib/bootstrap.js +54 -0
  52. package/dist/lib/branch.js +4 -0
  53. package/dist/lib/config.js +65 -0
  54. package/dist/lib/constants.js +13 -0
  55. package/dist/lib/env.js +20 -0
  56. package/dist/lib/fs.js +156 -0
  57. package/dist/lib/git.js +170 -0
  58. package/dist/lib/hooks.js +98 -0
  59. package/dist/lib/ide.js +75 -0
  60. package/dist/lib/merger.js +103 -0
  61. package/dist/lib/platform.js +13 -0
  62. package/dist/lib/prompt.js +134 -0
  63. package/dist/lib/proxy-dashboard.js +75 -0
  64. package/dist/lib/scanner.js +118 -0
  65. package/dist/lib/services.js +132 -0
  66. package/dist/lib/slug.js +35 -0
  67. package/dist/lib/templates.js +115 -0
  68. package/dist/lib/validator.js +15 -0
  69. package/dist/templates/SPEC.md +21 -0
  70. package/dist/templates/TODO.md +5 -0
  71. package/dist/templates/agent/001.general.Omnibus.agent.md +4 -0
  72. package/dist/templates/agent/002.discovery.Inventorius.agent.md +4 -0
  73. package/dist/templates/agent/003.design.Architecturius.agent.md +8 -0
  74. package/dist/templates/agent/004.plan.Strategos.agent.md +8 -0
  75. package/dist/templates/agent/005.tdd.TestDrivenCodificius.agent.md +8 -0
  76. package/dist/templates/agent/006.code.Codificius.agent.md +8 -0
  77. package/dist/templates/agent/007.simplify.Consolidarius.agent.md +8 -0
  78. package/dist/templates/agent/008.review.Auditorix.agent.md +8 -0
  79. package/dist/templates/agent/009.testwriter.TestScriptor.agent.md +8 -0
  80. package/dist/templates/agent/010.testexecutor.TestExecutor.agent.md +8 -0
  81. package/dist/templates/agent/011.commit.Scribus.agent.md +10 -0
  82. package/dist/templates/agent/CONTEXT.code.md +145 -0
  83. package/dist/templates/agent/CONTEXT.spec.md +98 -0
  84. package/dist/templates/agent/Copilot/Code.agent.md +28 -0
  85. package/dist/templates/agent/Copilot/CodeCommit.agent.md +16 -0
  86. package/dist/templates/agent/Copilot/Feature-Builder.agent.md +49 -0
  87. package/dist/templates/agent/Copilot/Reviewer.agent.md +17 -0
  88. package/dist/templates/agent/Copilot/Simplifier.agent.md +21 -0
  89. package/dist/templates/agent/Copilot/Specs.agent.md +66 -0
  90. package/dist/templates/agent/Copilot/SpecsCommit.agent.md +19 -0
  91. package/dist/templates/agent/Copilot/TODO-Reader.agent.md +18 -0
  92. package/dist/templates/agent/Copilot/Tester.agent.md +12 -0
  93. package/package.json +76 -0
@@ -0,0 +1,56 @@
1
+ import { AbstractCommands } from './AbstractCommands.js';
2
+ import { BranchContext } from '../foundation/BranchContext.js';
3
+ import { generateEnvrcFile, loadGeneratedServicesFile } from '../lib/services.js';
4
+ import { readTextFile, pathExists } from '../lib/fs.js';
5
+ import path from 'path';
6
+ export class EnvCommands extends AbstractCommands {
7
+ constructor(context) {
8
+ super(context);
9
+ }
10
+ /**
11
+ * Generate .envrc from existing generated.services.json
12
+ */
13
+ async update(branchName) {
14
+ // Load branch context
15
+ const branchContext = branchName
16
+ ? await this.context.loadBranchContext(branchName)
17
+ : await BranchContext.findNearestBranchContext(this.context);
18
+ const generatedServicesPath = path.join(branchContext.path, 'generated.services.json');
19
+ if (!(await pathExists(generatedServicesPath))) {
20
+ console.log('❌ No generated.services.json found. Run "forge services scan" first.');
21
+ return;
22
+ }
23
+ const generatedServicesFile = await loadGeneratedServicesFile(branchContext);
24
+ // Read generated services
25
+ await this.generateEnvrcFile(branchContext, generatedServicesFile.services);
26
+ }
27
+ /**
28
+ * Display current .envrc and port allocation info
29
+ */
30
+ async show(branchName) {
31
+ // Load branch context
32
+ const branchContext = branchName
33
+ ? await this.context.loadBranchContext(branchName)
34
+ : await BranchContext.findNearestBranchContext(this.context);
35
+ const envrcPath = path.join(branchContext.path, '.envrc');
36
+ console.log(`📋 Environment configuration for branch "${branchContext.branchName}"`);
37
+ console.log('');
38
+ // Check if .envrc exists
39
+ if (!(await pathExists(envrcPath))) {
40
+ console.log('❌ No .envrc found. Run "forge env update" first.');
41
+ return;
42
+ }
43
+ // Read and display .envrc
44
+ const envrcContent = await readTextFile(envrcPath);
45
+ console.log('=== .envrc content ===');
46
+ console.log(envrcContent);
47
+ console.log('');
48
+ }
49
+ async generateEnvrcFile(branchContext, services) {
50
+ console.log(`🔄 Generating .envrc for branch "${branchContext.branchName}"...`);
51
+ const env = await generateEnvrcFile(this.context, branchContext, services);
52
+ console.log(` ✅ .envrc: ${env.path}`);
53
+ console.log('');
54
+ return env;
55
+ }
56
+ }
@@ -0,0 +1,4 @@
1
+ import { SubBranchCommands } from './SubBranchCommands.js';
2
+ export class FeatureCommands extends SubBranchCommands {
3
+ prefix = this.context.options.git.featureBranchPrefix;
4
+ }
@@ -0,0 +1,4 @@
1
+ import { SubBranchCommands } from './SubBranchCommands.js';
2
+ export class FixCommands extends SubBranchCommands {
3
+ prefix = this.context.options.git.fixBranchPrefix;
4
+ }
@@ -0,0 +1,380 @@
1
+ import { ForgeConfigError } from '../foundation/errors/index.js';
2
+ import { AIAgentName } from '../foundation/types/AIAgentName.js';
3
+ import { IDEName } from '../foundation/types/IDEName.js';
4
+ import { FEAT_FORGE_CONFIG_FILE } from '../lib/constants.js';
5
+ import { pathExists, writeConfigSafely } from '../lib/fs.js';
6
+ import { promptCheckbox, promptChoice, promptConfirm, promptText } from '../lib/prompt.js';
7
+ import { findAncestorForgeConfig, scanGitRepos } from '../lib/scanner.js';
8
+ import { stat } from 'fs/promises';
9
+ import path from 'path';
10
+ // Constants for UI strings
11
+ const DEFAULT_LABEL = ' (default)';
12
+ const FEATURES_FOLDER = '.features';
13
+ const VSCODE_FOLDER = '.vscode';
14
+ const IDEA_FOLDER = '.idea';
15
+ export class InitCommands {
16
+ // ============================================================================
17
+ // PUBLIC COMMAND METHODS
18
+ // ============================================================================
19
+ /**
20
+ * Create a .feat-forge.json in the current working directory.
21
+ * Supports both interactive and non-interactive modes via options.
22
+ *
23
+ * @param options - Configuration options for init behavior
24
+ */
25
+ async init(options = {}) {
26
+ const workingDir = options.path ? path.resolve(options.path) : process.cwd();
27
+ const targetPath = path.join(workingDir, FEAT_FORGE_CONFIG_FILE);
28
+ const isInteractive = !options.nonInteractive && !options.yes;
29
+ const isQuiet = options.quiet || false;
30
+ // Validate environment and check for conflicts
31
+ await this.validateInitEnvironment(targetPath, workingDir, options.force || false, isInteractive);
32
+ // Gather repository paths
33
+ const repoPaths = await this.gatherRepositoryPaths(workingDir, options.repositories, isInteractive, options.nonInteractive || false, options.yes || false);
34
+ // Validate repository paths exist
35
+ await this.validateRepositoryPaths(repoPaths, workingDir);
36
+ // Build configuration
37
+ const configFile = await this.buildConfiguration(repoPaths, workingDir, options.rootDir, options.agents, options.ides, isInteractive, options.yes || false);
38
+ // Confirm and write configuration
39
+ await this.confirmAndWriteConfig(configFile, targetPath, isInteractive, isQuiet);
40
+ if (!isQuiet) {
41
+ console.log(`✓ Initialized ${FEAT_FORGE_CONFIG_FILE} at ${targetPath}`);
42
+ }
43
+ }
44
+ // ============================================================================
45
+ // PRIVATE HELPER METHODS
46
+ // ============================================================================
47
+ /**
48
+ * Validate that the environment is suitable for initialization.
49
+ * Checks for ancestor configs and handles existing config files.
50
+ */
51
+ async validateInitEnvironment(targetPath, workingDir, force, isInteractive) {
52
+ // Check for ancestor configuration
53
+ const ancestor = await findAncestorForgeConfig(workingDir);
54
+ if (ancestor) {
55
+ throw new ForgeConfigError(`An ancestor configuration already exists at ${ancestor}. Aborting init.`);
56
+ }
57
+ // Handle existing config file
58
+ if (await pathExists(targetPath)) {
59
+ if (force) {
60
+ // Force mode: will create backup via writeConfigSafely
61
+ return;
62
+ }
63
+ if (!isInteractive) {
64
+ throw new ForgeConfigError(`Configuration already exists at ${targetPath}. Use --force to overwrite.`);
65
+ }
66
+ const overwrite = await promptConfirm(`Configuration already exists at ${targetPath}. Overwrite?`);
67
+ if (!overwrite) {
68
+ console.log('Initialization cancelled.');
69
+ process.exit(0);
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Gather repository paths through scanning and user interaction.
75
+ */
76
+ async gatherRepositoryPaths(workingDir, repositoriesFlag, isInteractive = true, nonInteractive = false, useDefaults = false) {
77
+ // If repositories provided via flag, use those
78
+ if (repositoriesFlag) {
79
+ return this.parseStringOrJSON(repositoriesFlag);
80
+ }
81
+ // In non-interactive mode without repositories flag, must fail
82
+ if (nonInteractive) {
83
+ throw new ForgeConfigError('Missing required --repositories flag in non-interactive mode. ' + 'Provide as comma-separated list or JSON array.');
84
+ }
85
+ const repos = await scanGitRepos(workingDir);
86
+ // Handle no repositories found
87
+ if (repos.length === 0) {
88
+ return await this.handleNoRepositoriesFound(isInteractive);
89
+ }
90
+ const repoPaths = repos.map((r) => this.makeRelativePath(r.path, workingDir));
91
+ // In --yes mode, use discovered repositories automatically
92
+ if (useDefaults) {
93
+ console.log('Discovered repositories:');
94
+ repoPaths.forEach((r) => console.log(` - ${r}`));
95
+ return repoPaths;
96
+ }
97
+ // Present discovered repositories to user with checkbox selection
98
+ console.log('Discovered repositories:');
99
+ repoPaths.forEach((r) => console.log(` - ${r}`));
100
+ const choices = repoPaths.map((r) => ({
101
+ name: r,
102
+ value: r,
103
+ checked: true,
104
+ }));
105
+ choices.push({
106
+ name: 'Add custom path...',
107
+ value: '__custom__',
108
+ checked: false,
109
+ });
110
+ const selected = await promptCheckbox('Select repositories to include in the config:', choices);
111
+ // Handle custom path addition
112
+ if (selected.includes('__custom__')) {
113
+ const customPath = await promptText('Enter custom repository path (relative to this folder):');
114
+ if (customPath && customPath.trim()) {
115
+ selected.splice(selected.indexOf('__custom__'), 1);
116
+ selected.push(customPath.trim());
117
+ }
118
+ else {
119
+ selected.splice(selected.indexOf('__custom__'), 1);
120
+ }
121
+ }
122
+ return selected.length > 0 ? selected : ['.'];
123
+ }
124
+ /**
125
+ * Handle the case when no git repositories are found.
126
+ */
127
+ async handleNoRepositoriesFound(isInteractive) {
128
+ if (!isInteractive) {
129
+ // Non-interactive: default to current directory
130
+ return ['.'];
131
+ }
132
+ const useCwd = await promptConfirm('No git repositories found. Use current directory as repository?');
133
+ if (useCwd) {
134
+ return ['.'];
135
+ }
136
+ const manual = await promptText('Enter repository path to add (relative to this folder):');
137
+ return [manual || '.'];
138
+ }
139
+ /**
140
+ * Build the complete configuration object.
141
+ */
142
+ async buildConfiguration(repoPaths, workingDir, rootDirFlag, agentsFlag, idesFlag, isInteractive = true, useDefaults = false) {
143
+ const repositories = await this.selectMainRepository(repoPaths, workingDir, isInteractive, useDefaults);
144
+ const agents = await this.configureAgents(agentsFlag, isInteractive, useDefaults);
145
+ const ides = await this.configureIDEs(workingDir, idesFlag, isInteractive, useDefaults);
146
+ return {
147
+ rootDir: rootDirFlag || '.',
148
+ repositories,
149
+ agents,
150
+ ides,
151
+ };
152
+ }
153
+ /**
154
+ * Display configuration summary and confirm before writing.
155
+ */
156
+ async confirmAndWriteConfig(configFile, targetPath, isInteractive, isQuiet) {
157
+ if (!isQuiet) {
158
+ console.log('\n=== Configuration Summary ===');
159
+ console.log(JSON.stringify(configFile, null, 4));
160
+ console.log('=============================\n');
161
+ }
162
+ if (isInteractive) {
163
+ const confirmed = await promptConfirm('Write this configuration to .feat-forge.json?');
164
+ if (!confirmed) {
165
+ console.log('Initialization cancelled.');
166
+ process.exit(0);
167
+ }
168
+ }
169
+ const contents = JSON.stringify(configFile, null, 4);
170
+ await writeConfigSafely(targetPath, `${contents}\n`);
171
+ }
172
+ /**
173
+ * Convert an absolute path to a relative path from working directory.
174
+ * Returns '.' if the path is the current directory.
175
+ */
176
+ makeRelativePath(absolutePath, workingDir) {
177
+ return path.relative(workingDir, absolutePath) || '.';
178
+ }
179
+ /**
180
+ * Parse comma-separated input string into trimmed array.
181
+ */
182
+ parseCommaSeparated(input) {
183
+ return input
184
+ .split(',')
185
+ .map((s) => s.trim())
186
+ .filter(Boolean);
187
+ }
188
+ /**
189
+ * Parse string input that could be comma-separated or JSON array.
190
+ */
191
+ parseStringOrJSON(input) {
192
+ const trimmed = input.trim();
193
+ // Try to parse as JSON array first
194
+ if (trimmed.startsWith('[')) {
195
+ try {
196
+ const parsed = JSON.parse(trimmed);
197
+ if (Array.isArray(parsed)) {
198
+ return parsed.map(String).filter(Boolean);
199
+ }
200
+ }
201
+ catch {
202
+ throw new ForgeConfigError(`Invalid JSON array: ${trimmed}`);
203
+ }
204
+ }
205
+ // Otherwise treat as comma-separated
206
+ return this.parseCommaSeparated(trimmed);
207
+ }
208
+ /**
209
+ * Validate that repository paths exist and are directories.
210
+ */
211
+ async validateRepositoryPaths(repoPaths, workingDir) {
212
+ for (const repoPath of repoPaths) {
213
+ const fullPath = path.join(workingDir, repoPath);
214
+ if (!(await pathExists(fullPath))) {
215
+ throw new ForgeConfigError(`Repository path does not exist: ${repoPath}`);
216
+ }
217
+ try {
218
+ const stats = await stat(fullPath);
219
+ if (!stats.isDirectory()) {
220
+ throw new ForgeConfigError(`Repository path is not a directory: ${repoPath}`);
221
+ }
222
+ }
223
+ catch (error) {
224
+ throw new ForgeConfigError(`Cannot access repository path: ${repoPath}`);
225
+ }
226
+ }
227
+ }
228
+ /**
229
+ * Select the main repository from the list of repository paths.
230
+ * Auto-selects if one has a .features folder, otherwise prompts the user.
231
+ */
232
+ async selectMainRepository(repoPaths, workingDir, isInteractive, useDefaults) {
233
+ // Handle empty or single repository cases
234
+ if (repoPaths.length === 0) {
235
+ return [];
236
+ }
237
+ if (repoPaths.length === 1) {
238
+ return [{ path: repoPaths[0], main: true }];
239
+ }
240
+ // Determine main repository from multiple options
241
+ const mainRepoPath = await this.determineMainRepository(repoPaths, workingDir, isInteractive, useDefaults);
242
+ // Build repository config entries with main flag
243
+ return repoPaths.map((rp) => ({
244
+ path: rp,
245
+ main: rp === mainRepoPath,
246
+ }));
247
+ }
248
+ /**
249
+ * Determine which repository should be marked as main.
250
+ * Auto-selects if exactly one has .features folder, otherwise prompts.
251
+ */
252
+ async determineMainRepository(repoPaths, workingDir, isInteractive, useDefaults) {
253
+ const reposWithFeatures = await this.findRepositoriesWithFeatures(repoPaths, workingDir);
254
+ // Auto-select if exactly one repo has .features folder
255
+ if (reposWithFeatures.length === 1) {
256
+ const mainRepo = reposWithFeatures[0];
257
+ console.log(`Auto-selected '${mainRepo}' as main repository (has ${FEATURES_FOLDER} folder).`);
258
+ return mainRepo;
259
+ }
260
+ // If using defaults or non-interactive, pick first repo
261
+ if (!isInteractive || useDefaults) {
262
+ return repoPaths[0];
263
+ }
264
+ // Multiple repos with .features - let user choose
265
+ if (reposWithFeatures.length > 1) {
266
+ console.log(`\nMultiple repositories have ${FEATURES_FOLDER} folders. Select the main repository:`);
267
+ return await this.promptForMainRepository(reposWithFeatures, false);
268
+ }
269
+ // No repos with .features - prompt with first as default
270
+ console.log('\nMultiple repositories found. Select the main repository:');
271
+ return await this.promptForMainRepository(repoPaths, true);
272
+ }
273
+ /**
274
+ * Find which repositories contain a .features folder.
275
+ */
276
+ async findRepositoriesWithFeatures(repoPaths, workingDir) {
277
+ const reposWithFeatures = [];
278
+ for (const repoPath of repoPaths) {
279
+ const fullPath = path.join(workingDir, repoPath);
280
+ const featuresPath = path.join(fullPath, FEATURES_FOLDER);
281
+ if (await pathExists(featuresPath)) {
282
+ reposWithFeatures.push(repoPath);
283
+ }
284
+ }
285
+ return reposWithFeatures;
286
+ }
287
+ /**
288
+ * Prompt user to select main repository from list.
289
+ * @param repos - List of repository paths to choose from
290
+ * @param markFirstAsDefault - Whether to mark the first option as default
291
+ */
292
+ async promptForMainRepository(repos, markFirstAsDefault) {
293
+ const choices = repos.map((rp, idx) => ({
294
+ value: String(idx + 1),
295
+ name: `${rp}${markFirstAsDefault && idx === 0 ? DEFAULT_LABEL : ''}`,
296
+ }));
297
+ const selection = await promptChoice('Choose main repository:', choices);
298
+ const selectedChoice = choices.find((c) => c.value === selection);
299
+ if (selectedChoice) {
300
+ return selectedChoice.name.replace(DEFAULT_LABEL, '');
301
+ }
302
+ // Fallback to first repository if selection failed
303
+ return repos[0];
304
+ }
305
+ /**
306
+ * Configure agents for the project.
307
+ * Suggests default agents and allows user to customize via checkboxes.
308
+ */
309
+ async configureAgents(agentsFlag, isInteractive = true, useDefaults = false) {
310
+ // If agents provided via flag, use those
311
+ if (agentsFlag) {
312
+ const agentNames = this.parseStringOrJSON(agentsFlag);
313
+ return agentNames.length > 0 ? agentNames : undefined;
314
+ }
315
+ // If using defaults mode, return default agents
316
+ if (useDefaults) {
317
+ return [AIAgentName.COPILOT];
318
+ }
319
+ // Non-interactive: use ForgeConfig defaults (undefined)
320
+ if (!isInteractive) {
321
+ return undefined;
322
+ }
323
+ console.log('\n--- Agent Configuration ---');
324
+ // Build checkbox choices for all available agents
325
+ const availableAgents = Object.values(AIAgentName);
326
+ const choices = availableAgents.map((agent) => ({
327
+ name: agent,
328
+ value: agent,
329
+ checked: agent === AIAgentName.COPILOT, // Pre-check COPILOT as default
330
+ }));
331
+ const selected = await promptCheckbox('Select agents to enable:', choices);
332
+ return selected.length > 0 ? selected : undefined;
333
+ }
334
+ /**
335
+ * Configure IDEs for the project.
336
+ * Detects .vscode or .idea folders and allows selection via checkboxes.
337
+ */
338
+ async configureIDEs(workingDir, idesFlag, isInteractive = true, useDefaults = false) {
339
+ // If IDEs provided via flag, use those
340
+ if (idesFlag) {
341
+ const ideNames = this.parseStringOrJSON(idesFlag);
342
+ return ideNames.length > 0 ? ideNames : undefined;
343
+ }
344
+ const detectedIDEs = await this.detectIDEs(workingDir);
345
+ // If using defaults mode, use detected IDEs or undefined
346
+ if (useDefaults) {
347
+ return detectedIDEs.length > 0 ? detectedIDEs : undefined;
348
+ }
349
+ // Non-interactive: use detected IDEs or undefined
350
+ if (!isInteractive) {
351
+ return detectedIDEs.length > 0 ? detectedIDEs : undefined;
352
+ }
353
+ console.log('\n--- IDE Configuration ---');
354
+ // Build checkbox choices for available IDEs
355
+ const availableIDEs = [IDEName.VSCODE]; // Only VSCode is currently supported
356
+ const choices = availableIDEs.map((ide) => ({
357
+ name: ide,
358
+ value: ide,
359
+ checked: detectedIDEs.includes(ide), // Pre-check detected IDEs
360
+ }));
361
+ const selected = await promptCheckbox('Select IDEs to configure:', choices);
362
+ return selected.length > 0 ? selected : undefined;
363
+ }
364
+ /**
365
+ * Detect IDE folders in the working directory.
366
+ */
367
+ async detectIDEs(workingDir) {
368
+ const vscodeExists = await pathExists(path.join(workingDir, VSCODE_FOLDER));
369
+ const ideaExists = await pathExists(path.join(workingDir, IDEA_FOLDER));
370
+ const detectedIDEs = [];
371
+ if (vscodeExists) {
372
+ detectedIDEs.push(IDEName.VSCODE);
373
+ console.log(`Detected: VSCode (${VSCODE_FOLDER} folder found)`);
374
+ }
375
+ if (ideaExists) {
376
+ console.log(`Note: ${IDEA_FOLDER} folder found, but IntelliJ IDEA is not yet supported in this version.`);
377
+ }
378
+ return detectedIDEs;
379
+ }
380
+ }
@@ -0,0 +1,39 @@
1
+ import { AbstractCommands } from './AbstractCommands.js';
2
+ import { promptConfirm } from '../lib/prompt.js';
3
+ export class MaintenanceCommands extends AbstractCommands {
4
+ /**
5
+ * (Re)Write agent template files in the .specs/.template/.forge-agents/ directory of the main repository.
6
+ * This is useful if you want to fix the agent's files in the project.
7
+ */
8
+ async installAgentTemplateLocally(options = {}) {
9
+ const deleteExisting = Boolean(options.deleteExisting);
10
+ const dryRun = Boolean(options.dryRun);
11
+ const commit = options.commit === undefined ? undefined : Boolean(options.commit);
12
+ const overwrite = Boolean(options.overwrite);
13
+ console.log(`Install agent templates locally (deleteExisting=${deleteExisting}, dryRun=${dryRun}, commit=${commit}, overwrite=${overwrite})...`);
14
+ const changed = await this.context.installAgentTemplateLocally(this.context.mainRepo, { deleteExisting, dryRun, overwrite });
15
+ if (changed.length === 0) {
16
+ console.log('No agent template files were modified.');
17
+ return;
18
+ }
19
+ console.log(`Modified ${changed.length} file(s):`);
20
+ for (const f of changed) {
21
+ console.log(` - ${f}`);
22
+ }
23
+ if (dryRun) {
24
+ console.log('\nDry run: no files were written.');
25
+ }
26
+ else {
27
+ const doCommit = commit === undefined ? await promptConfirm('Do you want to commit the changes to the repository?') : commit;
28
+ if (doCommit) {
29
+ if (changed.length > 0 && !dryRun && doCommit) {
30
+ await this.context.mainRepo.commit(`chore: add feat-forge agent templates ${overwrite ? ' (overwriting existing templates)' : ''}`, changed);
31
+ }
32
+ console.log('\nFiles were written and committed.');
33
+ }
34
+ else {
35
+ console.log('\nFiles were written but not committed (use --commit to commit).');
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,15 @@
1
+ import { BranchContext } from '../foundation/BranchContext.js';
2
+ import { AbstractCommands } from './AbstractCommands.js';
3
+ export class ModeCommands extends AbstractCommands {
4
+ // ============================================================================
5
+ // PUBLIC COMMAND METHODS
6
+ // ============================================================================
7
+ /**
8
+ * Set the current mode and refresh agent adapters for the active feature.
9
+ */
10
+ async setMode(mode) {
11
+ const branchContext = await BranchContext.findNearestBranchContext(this.context);
12
+ await branchContext.setMode(mode);
13
+ await branchContext.refreshAgentContextFiles(mode);
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ import { Proxy } from '../foundation/Proxy.js';
2
+ import { AbstractCommands } from './AbstractCommands.js';
3
+ export class ProxyCommands extends AbstractCommands {
4
+ async start(options) {
5
+ if (options.port) {
6
+ console.warn(`⚠️ Specifying a custom port with --port may result in problems if services communicates with each other through the proxy port/url declared in the .envrc files.
7
+ It is recommended to set the desired port in the configuration file instead.`);
8
+ }
9
+ const proxy = new Proxy(this.context);
10
+ await proxy.start({
11
+ port: options.port ? parseInt(options.port, 10) : undefined,
12
+ });
13
+ }
14
+ }
@@ -0,0 +1,4 @@
1
+ import { SubBranchCommands } from './SubBranchCommands.js';
2
+ export class ReleaseCommands extends SubBranchCommands {
3
+ prefix = this.context.options.git.releaseBranchPrefix;
4
+ }
@@ -0,0 +1,95 @@
1
+ import { BranchContext } from '../foundation/BranchContext.js';
2
+ import { PortAllocator } from '../foundation/PortAllocator.js';
3
+ import { generateBranchServicesFile, getServiceOutputs, loadGeneratedServicesFile, scanReposServices, } from '../lib/services.js';
4
+ import { AbstractCommands } from './AbstractCommands.js';
5
+ import { EnvCommands } from './EnvCommands.js';
6
+ import { ForgeNotInActiveBranchError } from '../foundation/errors/index.js';
7
+ export class ServicesCommands extends AbstractCommands {
8
+ // ============================================================================
9
+ // PUBLIC COMMAND METHODS
10
+ // ============================================================================
11
+ /**
12
+ * Scan repositories for services and generate configuration files
13
+ */
14
+ async scan(branchName) {
15
+ // Load branch context (use current branch if not specified)
16
+ let branches = [];
17
+ try {
18
+ const branchContext = branchName
19
+ ? await this.context.loadBranchContext(branchName)
20
+ : await BranchContext.findNearestBranchContext(this.context);
21
+ branches = [branchContext];
22
+ }
23
+ catch (err) {
24
+ if (err instanceof ForgeNotInActiveBranchError) {
25
+ branches.push(...(await this.context.loadActiveBranchesContexts()));
26
+ }
27
+ else {
28
+ throw err;
29
+ }
30
+ }
31
+ // Load port allocator
32
+ const portAllocator = await PortAllocator.load(this.context);
33
+ for (const branchContext of branches) {
34
+ // Scan for services in all repositories
35
+ console.log('-----------------------------------------------------------------------');
36
+ console.log(`🔍 Scanning for services in branch "${branchContext.branchName}"...`);
37
+ const reposServices = await scanReposServices(branchContext);
38
+ if (reposServices.some((repo) => repo.services.length > 0)) {
39
+ console.log(`✅ Found services in ${reposServices.length} repository(ies)`);
40
+ }
41
+ else {
42
+ console.log('⚠️ No services found in any repositories');
43
+ continue;
44
+ }
45
+ console.log('🔄 Allocating ports for discovered services...');
46
+ // Allocate ports for all repositories
47
+ portAllocator.allocatePorts(branchContext.branchName, reposServices);
48
+ // Save port allocations
49
+ await portAllocator.save();
50
+ console.log(' ✅ Port allocation completed');
51
+ console.log('');
52
+ // Generate and write generated.services.json
53
+ console.log('🔄 Generating generated.services.json with port assignments...');
54
+ const generated = await generateBranchServicesFile(branchContext, portAllocator);
55
+ console.log(` ✅ generated.services.json: ${generated.path}`);
56
+ console.log('');
57
+ // Generate and write .envrc
58
+ await new EnvCommands(this.context).generateEnvrcFile(branchContext, generated.services);
59
+ // Display summary
60
+ this.showServiceSummary(branchContext, generated.services);
61
+ console.log('');
62
+ }
63
+ }
64
+ /**
65
+ * List discovered services with their allocated ports
66
+ */
67
+ async list(format) {
68
+ // Load current branch context
69
+ const branchContext = await BranchContext.findNearestBranchContext(this.context);
70
+ // Load generated services file
71
+ const generatedServicesFile = await loadGeneratedServicesFile(branchContext);
72
+ if (format === 'json') {
73
+ // Output as JSON
74
+ console.log(JSON.stringify(generatedServicesFile, null, 2));
75
+ }
76
+ else {
77
+ // Default log format
78
+ this.showServiceSummary(branchContext, generatedServicesFile.services);
79
+ }
80
+ }
81
+ // ============================================================================
82
+ // PRIVATE UTILITY METHODS
83
+ // ============================================================================
84
+ showServiceSummary(branchContext, services) {
85
+ console.log('Services Summary:');
86
+ for (const service of services) {
87
+ const { name, port, url, proxyUrl } = getServiceOutputs(this.context, branchContext, service);
88
+ console.log(` 🚀 ${name}: (PORT:${port})`);
89
+ console.log(` 🌐 url: ${url}`);
90
+ if (this.context.options.proxy.enabled) {
91
+ console.log(` 🔀 proxy-url: ${proxyUrl}`);
92
+ }
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,49 @@
1
+ import { AbstractCommands } from './AbstractCommands.js';
2
+ import { BranchCommands } from './BranchCommands.js';
3
+ export class SubBranchCommands extends AbstractCommands {
4
+ branchCommands = new BranchCommands(this.context);
5
+ // ============================================================================
6
+ // PUBLIC COMMAND METHODS
7
+ // ============================================================================
8
+ async create(rawSlug) {
9
+ rawSlug = this.toSubBranchSlug(rawSlug);
10
+ return this.branchCommands.create(rawSlug);
11
+ }
12
+ async start(rawSlug) {
13
+ rawSlug = this.toSubBranchSlug(rawSlug);
14
+ return this.branchCommands.start(rawSlug);
15
+ }
16
+ async list() {
17
+ return this.branchCommands.list(this.prefix);
18
+ }
19
+ async resync(rawSlug) {
20
+ rawSlug = this.toSubBranchSlug(rawSlug);
21
+ return this.branchCommands.resync(rawSlug);
22
+ }
23
+ async stop(rawSlug) {
24
+ rawSlug = this.toSubBranchSlug(rawSlug);
25
+ return this.branchCommands.stop(rawSlug);
26
+ }
27
+ async archive(rawSlug) {
28
+ rawSlug = this.toSubBranchSlug(rawSlug);
29
+ return this.branchCommands.archive(rawSlug);
30
+ }
31
+ async open(rawSlug) {
32
+ rawSlug = rawSlug ? this.toSubBranchSlug(rawSlug) : undefined;
33
+ return this.branchCommands.open(rawSlug);
34
+ }
35
+ async merge(rawSlug) {
36
+ rawSlug = this.toSubBranchSlug(rawSlug);
37
+ return this.branchCommands.merge(rawSlug);
38
+ }
39
+ async rebase(rawSlug) {
40
+ rawSlug = this.toSubBranchSlug(rawSlug);
41
+ return this.branchCommands.rebase(rawSlug);
42
+ }
43
+ // ============================================================================
44
+ // PRIVATE METHODS
45
+ // ============================================================================
46
+ toSubBranchSlug(rawSlug) {
47
+ return `${this.prefix}${rawSlug}`;
48
+ }
49
+ }
@@ -0,0 +1 @@
1
+ export {};