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 path from 'path';
2
+ import { FEAT_FORGE_CONFIG_FOLDER, TEMP_FEATURE_ARCHIVE_FOLDER, TEMP_FEATURE_INIT_FOLDER, TEMP_FOLDER, TemporaryFolderType, } from '../lib/constants.js';
3
+ import { branchNameAsPath } from '../lib/branch.js';
4
+ export class PathHelper {
5
+ context;
6
+ constructor(context) {
7
+ this.context = context;
8
+ }
9
+ get folders() {
10
+ return this.context.options.folders;
11
+ }
12
+ get rootDir() {
13
+ return this.context.rootDir;
14
+ }
15
+ get worktreesRoot() {
16
+ return path.join(this.rootDir, this.folders.worktrees);
17
+ }
18
+ get featForgeConfigRoot() {
19
+ return path.join(this.rootDir, FEAT_FORGE_CONFIG_FOLDER);
20
+ }
21
+ get tempFolderRoot() {
22
+ return path.join(this.featForgeConfigRoot, TEMP_FOLDER);
23
+ }
24
+ getPathInRoot(...segments) {
25
+ return path.join(this.rootDir, ...segments);
26
+ }
27
+ getPathInWorktrees(...segments) {
28
+ return path.join(this.worktreesRoot, ...segments);
29
+ }
30
+ getBranchRootPath(branchName) {
31
+ return this.getPathInBranchRoot(branchNameAsPath(branchName));
32
+ }
33
+ getPathInBranchRoot(branchName, ...segments) {
34
+ return this.getPathInWorktrees(branchNameAsPath(branchName), ...segments);
35
+ }
36
+ /**
37
+ * Get the temporary folder path for a specific operation (e.g. feature-init, feature-archive).
38
+ * Default Pattern: <rootDir>/.feat-forge/tmp/<operation>/<slug>/<repoName>/
39
+ */
40
+ getPathInTemp(...segments) {
41
+ return path.join(this.tempFolderRoot, ...segments);
42
+ }
43
+ /**
44
+ * Get the temporary worktree path for a specific repository and operation type.
45
+ */
46
+ getTempWorktreePathForRepo(type, branchName, repoName) {
47
+ switch (type) {
48
+ case TemporaryFolderType.BRANCH_INIT:
49
+ return this.getPathInTemp(TEMP_FEATURE_INIT_FOLDER, branchNameAsPath(branchName), repoName);
50
+ case TemporaryFolderType.BRANCH_ARCHIVE:
51
+ return this.getPathInTemp(TEMP_FEATURE_ARCHIVE_FOLDER, branchNameAsPath(branchName), repoName);
52
+ default:
53
+ throw new Error(`Unknown temporary type: ${type}`);
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,192 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { ForgePortAllocationsLoadError, ForgePortRangeExhaustedError } from '../foundation/errors/index.js';
11
+ import { PortAllocatorDTO, } from '../foundation/types/Services.js';
12
+ import { pathExists, readJSONFile, writeTextFile } from '../lib/fs.js';
13
+ import { Type } from 'class-transformer';
14
+ import { IsArray, ValidateNested } from 'class-validator';
15
+ const PORT_ALLOCATIONS_FILE = 'port-allocations.json';
16
+ /**
17
+ * Business entity representing port allocation for a branch
18
+ */
19
+ export class BranchPortAllocation {
20
+ /**
21
+ * Branch name
22
+ */
23
+ name;
24
+ /**
25
+ * Starting port number for this branch's allocation range
26
+ */
27
+ start;
28
+ /**
29
+ * Ending port number for this branch's allocation range
30
+ */
31
+ end;
32
+ /**
33
+ * Services with their allocated ports for this branch
34
+ */
35
+ services; // Maps service name to its allocated port
36
+ constructor(branchName, start, end, services = []) {
37
+ this.name = branchName;
38
+ this.start = start;
39
+ this.end = end;
40
+ this.services = services;
41
+ }
42
+ static load(file) {
43
+ return new BranchPortAllocation(file.name, file.start, file.end, file.services);
44
+ }
45
+ toJSON() {
46
+ return {
47
+ name: this.name,
48
+ start: this.start,
49
+ end: this.end,
50
+ services: this.services,
51
+ };
52
+ }
53
+ get nextAvailablePort() {
54
+ const usedPorts = this.services.map((s) => s.port);
55
+ if (usedPorts.length === 0) {
56
+ return this.start;
57
+ }
58
+ const nextPort = Math.max(...usedPorts) + 1;
59
+ if (nextPort > this.end) {
60
+ throw new ForgePortRangeExhaustedError();
61
+ }
62
+ return nextPort;
63
+ }
64
+ hasService(serviceName) {
65
+ return this.services.some((s) => s.name === serviceName);
66
+ }
67
+ getService(serviceName) {
68
+ return this.services.find((s) => s.name === serviceName);
69
+ }
70
+ /**
71
+ * Allocate a port for the given service. Returns the existing port if already allocated,
72
+ * or assigns the next available port.
73
+ */
74
+ allocatePort(service) {
75
+ const serviceName = service.name;
76
+ const existingService = this.getService(serviceName);
77
+ if (existingService) {
78
+ return existingService.port;
79
+ }
80
+ const nextPort = this.nextAvailablePort;
81
+ if (nextPort > this.end) {
82
+ throw new ForgePortRangeExhaustedError(`Cannot allocate port for service "${serviceName}": Port range exhausted (${this.start}-${this.end}).`);
83
+ }
84
+ this.services.push({ ...service, port: nextPort });
85
+ return nextPort;
86
+ }
87
+ getServices() {
88
+ return this.services.slice();
89
+ }
90
+ }
91
+ /**
92
+ * Port allocator manages port assignments for services across branches
93
+ */
94
+ export class PortAllocator {
95
+ forgeContext;
96
+ allocations;
97
+ constructor(forgeContext, allocations) {
98
+ this.forgeContext = forgeContext;
99
+ this.allocations = allocations;
100
+ }
101
+ /**
102
+ * Load or create port allocations from rootDir
103
+ */
104
+ static async load(forgeContext) {
105
+ const portAllocationFilePath = this.getPortAllocationsFilePath(forgeContext);
106
+ let allocations = [];
107
+ if (await pathExists(portAllocationFilePath)) {
108
+ try {
109
+ const portAllocationFile = await readJSONFile(PortAllocatorDTO, portAllocationFilePath);
110
+ allocations = portAllocationFile.allocations.map(BranchPortAllocation.load);
111
+ }
112
+ catch (error) {
113
+ if (error instanceof Error) {
114
+ throw new ForgePortAllocationsLoadError(error.message);
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+ return new PortAllocator(forgeContext, allocations);
120
+ }
121
+ static getPortAllocationsFilePath(forgeContext) {
122
+ return forgeContext.paths.getPathInRoot(PORT_ALLOCATIONS_FILE);
123
+ }
124
+ /**
125
+ * Persist allocations to disk
126
+ */
127
+ toJSON() {
128
+ return {
129
+ _doNotEdit: "This file is auto-generated by 'forge services scan'. Manual edits will be lost on next generation.",
130
+ allocations: this.allocations.map((alloc) => alloc.toJSON()),
131
+ };
132
+ }
133
+ async save() {
134
+ const filePath = PortAllocator.getPortAllocationsFilePath(this.forgeContext);
135
+ const content = JSON.stringify(this, null, 2);
136
+ await writeTextFile(filePath, content);
137
+ }
138
+ get config() {
139
+ return this.forgeContext.config.options.proxy;
140
+ }
141
+ getAllocation(branchName) {
142
+ return this.allocations.find((alloc) => alloc.name === branchName);
143
+ }
144
+ hasAllocation(branchName) {
145
+ return this.allocations.some((alloc) => alloc.name === branchName);
146
+ }
147
+ /**
148
+ * Get or create allocation for a branch
149
+ */
150
+ getOrCreateBranchAllocation(branchName) {
151
+ if (this.hasAllocation(branchName)) {
152
+ return this.getAllocation(branchName);
153
+ }
154
+ const branchIndex = this.allocations.length;
155
+ const start = this.config.servicesBasePort + branchIndex * this.config.branchRangeSize;
156
+ const end = start + this.config.branchRangeSize - 1;
157
+ const allocation = new BranchPortAllocation(branchName, start, end);
158
+ this.allocations.push(allocation);
159
+ return allocation;
160
+ }
161
+ /**
162
+ * Allocate ports for services in a branch
163
+ * Returns mapping of service names to assigned ports
164
+ */
165
+ allocateBranchServicesPorts(branchName, services) {
166
+ const allocation = this.getOrCreateBranchAllocation(branchName);
167
+ for (const service of services) {
168
+ allocation.allocatePort(service);
169
+ }
170
+ return allocation;
171
+ }
172
+ /**
173
+ * Allocate ports for all repositories in a branch at once
174
+ */
175
+ allocatePorts(branchName, repoServices) {
176
+ for (const repoService of repoServices) {
177
+ this.allocateBranchServicesPorts(branchName, repoService.services);
178
+ }
179
+ }
180
+ /**
181
+ * Get all allocations
182
+ */
183
+ getAllAllocations() {
184
+ return this.allocations.slice();
185
+ }
186
+ }
187
+ __decorate([
188
+ IsArray(),
189
+ ValidateNested({ each: true }),
190
+ Type(() => BranchPortAllocation),
191
+ __metadata("design:type", Array)
192
+ ], PortAllocator.prototype, "allocations", void 0);
@@ -0,0 +1,176 @@
1
+ import { FEAT_FORGE_GENERATED_SERVICES_FILE } from '../lib/constants.js';
2
+ import { handleDashboardRequest } from '../lib/proxy-dashboard.js';
3
+ import { getServiceOutputs, loadGeneratedServicesFile } from '../lib/services.js';
4
+ import fs from 'fs';
5
+ import { createServer, ServerResponse } from 'http';
6
+ import { createProxyServer } from 'http-proxy-3';
7
+ export class Proxy {
8
+ context;
9
+ server = null;
10
+ routingTable = new Map();
11
+ stopWatching = null;
12
+ constructor(context) {
13
+ this.context = context;
14
+ }
15
+ async start(options = {}) {
16
+ const proxyConfig = this.context.options.proxy;
17
+ if (!proxyConfig.enabled) {
18
+ console.error('āŒ Proxy is disabled in configuration. Set proxy.enabled to true in .feat-forge.json');
19
+ return;
20
+ }
21
+ const port = options.port ?? proxyConfig.port;
22
+ const branchContexts = await this.context.loadActiveBranchesContexts();
23
+ if (branchContexts.length === 0) {
24
+ console.log('āš ļø No active branches found. Start a branch first with `forge start <branch>`.');
25
+ return;
26
+ }
27
+ this.registerShutdownHandlers();
28
+ this.routingTable = await this.buildRoutingTable(branchContexts);
29
+ this.showSummary(port);
30
+ this.startServer(port);
31
+ this.startWatching();
32
+ console.log(`\nšŸš€ Proxy server running on http://localhost:${port}`);
33
+ console.log(`šŸ“Š Dashboard: http://localhost:${port}`);
34
+ console.log('\nPress Ctrl+C to stop.\n');
35
+ }
36
+ stop() {
37
+ this.stopWatching?.();
38
+ this.stopWatching = null;
39
+ if (this.server) {
40
+ this.server.close();
41
+ this.server = null;
42
+ }
43
+ }
44
+ async buildRoutingTable(branchContexts) {
45
+ const table = new Map();
46
+ for (const branchContext of branchContexts) {
47
+ try {
48
+ const generated = await loadGeneratedServicesFile(branchContext);
49
+ for (const service of generated.services) {
50
+ const { proxyUrl, url, key, name } = getServiceOutputs(this.context, branchContext, service);
51
+ table.set(key, {
52
+ branchName: branchContext.branchName,
53
+ serviceName: name,
54
+ url,
55
+ proxyUrl,
56
+ });
57
+ }
58
+ }
59
+ catch {
60
+ // Branch has no generated services file, skip
61
+ }
62
+ }
63
+ return table;
64
+ }
65
+ startServer(port) {
66
+ const proxy = createProxyServer({});
67
+ proxy.on('error', (err, req, res) => {
68
+ if (res instanceof ServerResponse && !res.headersSent) {
69
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
70
+ res.end(`502 Bad Gateway - Service is down or unreachable`);
71
+ }
72
+ });
73
+ this.server = createServer((req, res) => {
74
+ const host = req.headers.host || '';
75
+ const hostWithoutPort = host.split(':')[0];
76
+ const parts = hostWithoutPort.replace(/\.localhost$/, '').split('.');
77
+ if (parts.length < 2 || hostWithoutPort === 'localhost') {
78
+ return handleDashboardRequest(req, res, this.routingTable, port);
79
+ }
80
+ const key = parts.join('.');
81
+ const route = this.routingTable.get(key);
82
+ if (!route) {
83
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
84
+ res.end(`404 Not Found - No route for "${key}"`);
85
+ return;
86
+ }
87
+ proxy.web(req, res, { target: route.url });
88
+ });
89
+ this.server.on('upgrade', (req, socket, head) => {
90
+ const host = req.headers.host || '';
91
+ const hostWithoutPort = host.split(':')[0];
92
+ const parts = hostWithoutPort.replace(/\.localhost$/, '').split('.');
93
+ const key = parts.join('.');
94
+ const route = this.routingTable.get(key);
95
+ if (route) {
96
+ proxy.ws(req, socket, head, { target: route.url });
97
+ }
98
+ else {
99
+ socket.destroy();
100
+ }
101
+ });
102
+ this.server.on('error', (err) => {
103
+ if (err.code === 'EADDRINUSE') {
104
+ console.error(`\nāŒ Port ${port} is already in use. Please choose a different proxy port.\n`);
105
+ process.exit(1);
106
+ }
107
+ throw err;
108
+ });
109
+ this.server.listen(port);
110
+ }
111
+ startWatching() {
112
+ const watchers = [];
113
+ let debounceTimer = null;
114
+ const rebuild = async () => {
115
+ if (debounceTimer)
116
+ clearTimeout(debounceTimer);
117
+ debounceTimer = setTimeout(async () => {
118
+ try {
119
+ const branchContexts = await this.context.loadActiveBranchesContexts();
120
+ this.routingTable = await this.buildRoutingTable(branchContexts);
121
+ console.log(`\nšŸ”„ Routing table reloaded (${this.routingTable.size} routes)`);
122
+ }
123
+ catch (err) {
124
+ console.error('āš ļø Failed to reload routing table:', err);
125
+ }
126
+ }, 500);
127
+ };
128
+ const worktreesRoot = this.context.paths.worktreesRoot;
129
+ if (fs.existsSync(worktreesRoot)) {
130
+ try {
131
+ const watcher = fs.watch(worktreesRoot, { recursive: true }, (event, filename) => {
132
+ if (filename && filename.endsWith(FEAT_FORGE_GENERATED_SERVICES_FILE)) {
133
+ rebuild();
134
+ }
135
+ });
136
+ watchers.push(watcher);
137
+ }
138
+ catch {
139
+ const watcher = fs.watch(worktreesRoot, () => rebuild());
140
+ watchers.push(watcher);
141
+ }
142
+ }
143
+ this.stopWatching = () => {
144
+ if (debounceTimer)
145
+ clearTimeout(debounceTimer);
146
+ watchers.forEach((w) => w.close());
147
+ };
148
+ }
149
+ showSummary(port) {
150
+ console.log('\nProxy Routing Table:');
151
+ const byBranch = new Map();
152
+ for (const [key, route] of this.routingTable) {
153
+ const entries = byBranch.get(route.branchName) || [];
154
+ entries.push({ serviceName: route.serviceName, key, target: route.url });
155
+ byBranch.set(route.branchName, entries);
156
+ }
157
+ for (const [branch, services] of byBranch) {
158
+ console.log(`\n šŸ“¦ ${branch}:`);
159
+ for (const svc of services) {
160
+ console.log(` šŸš€ ${svc.serviceName}`);
161
+ console.log(` šŸ”€ http://${svc.key}.localhost:${port}`);
162
+ console.log(` šŸŽÆ ${svc.target}`);
163
+ }
164
+ }
165
+ }
166
+ registerShutdownHandlers() {
167
+ const cleanup = () => {
168
+ console.log('\n\nShutting down proxy...');
169
+ this.stop();
170
+ console.log('Proxy stopped.');
171
+ process.exit(0);
172
+ };
173
+ process.on('SIGINT', cleanup);
174
+ process.on('SIGTERM', cleanup);
175
+ }
176
+ }