forge-dev-framework 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.
- package/.claude/rules/api-patterns.md +98 -0
- package/.claude/rules/security-baseline.md +204 -0
- package/.claude/rules/testing-standards.md +177 -0
- package/.claude/rules/ui-conventions.md +142 -0
- package/README.md +261 -0
- package/bin/forge.js +14 -0
- package/dist/bin/forge.js +14 -0
- package/dist/cli/index.d.ts +22 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +116 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/base.d.ts +31 -0
- package/dist/commands/base.d.ts.map +1 -0
- package/dist/commands/base.js +31 -0
- package/dist/commands/base.js.map +1 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +175 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/generate.d.ts +17 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +159 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/help.d.ts +11 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +65 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +22 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/status.d.ts +13 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +101 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stubs.d.ts +14 -0
- package/dist/commands/stubs.d.ts.map +1 -0
- package/dist/commands/stubs.js +30 -0
- package/dist/commands/stubs.js.map +1 -0
- package/dist/generators/index.d.ts +11 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +10 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/required-fields.d.ts +74 -0
- package/dist/generators/required-fields.d.ts.map +1 -0
- package/dist/generators/required-fields.js +179 -0
- package/dist/generators/required-fields.js.map +1 -0
- package/dist/generators/template-engine.d.ts +65 -0
- package/dist/generators/template-engine.d.ts.map +1 -0
- package/dist/generators/template-engine.js +209 -0
- package/dist/generators/template-engine.js.map +1 -0
- package/dist/generators/token-validator.d.ts +51 -0
- package/dist/generators/token-validator.d.ts.map +1 -0
- package/dist/generators/token-validator.js +141 -0
- package/dist/generators/token-validator.js.map +1 -0
- package/dist/generators/types.d.ts +433 -0
- package/dist/generators/types.d.ts.map +1 -0
- package/dist/generators/types.js +5 -0
- package/dist/generators/types.js.map +1 -0
- package/dist/generators/xml-task-generator.d.ts +67 -0
- package/dist/generators/xml-task-generator.d.ts.map +1 -0
- package/dist/generators/xml-task-generator.js +297 -0
- package/dist/generators/xml-task-generator.js.map +1 -0
- package/dist/git/__tests__/worktree.test.d.ts +5 -0
- package/dist/git/__tests__/worktree.test.d.ts.map +1 -0
- package/dist/git/__tests__/worktree.test.js +121 -0
- package/dist/git/__tests__/worktree.test.js.map +1 -0
- package/dist/git/codeowners.d.ts +101 -0
- package/dist/git/codeowners.d.ts.map +1 -0
- package/dist/git/codeowners.js +216 -0
- package/dist/git/codeowners.js.map +1 -0
- package/dist/git/commit.d.ts +135 -0
- package/dist/git/commit.d.ts.map +1 -0
- package/dist/git/commit.js +223 -0
- package/dist/git/commit.js.map +1 -0
- package/dist/git/hooks/commit-msg.d.ts +8 -0
- package/dist/git/hooks/commit-msg.d.ts.map +1 -0
- package/dist/git/hooks/commit-msg.js +34 -0
- package/dist/git/hooks/commit-msg.js.map +1 -0
- package/dist/git/hooks/pre-commit.d.ts +8 -0
- package/dist/git/hooks/pre-commit.d.ts.map +1 -0
- package/dist/git/hooks/pre-commit.js +34 -0
- package/dist/git/hooks/pre-commit.js.map +1 -0
- package/dist/git/pre-commit-hooks.d.ts +117 -0
- package/dist/git/pre-commit-hooks.d.ts.map +1 -0
- package/dist/git/pre-commit-hooks.js +270 -0
- package/dist/git/pre-commit-hooks.js.map +1 -0
- package/dist/git/wipe-protocol.d.ts +281 -0
- package/dist/git/wipe-protocol.d.ts.map +1 -0
- package/dist/git/wipe-protocol.js +237 -0
- package/dist/git/wipe-protocol.js.map +1 -0
- package/dist/git/worktree.d.ts +69 -0
- package/dist/git/worktree.d.ts.map +1 -0
- package/dist/git/worktree.js +202 -0
- package/dist/git/worktree.js.map +1 -0
- package/dist/scripts/install.d.ts +8 -0
- package/dist/scripts/install.d.ts.map +1 -0
- package/dist/scripts/install.js +161 -0
- package/dist/scripts/install.js.map +1 -0
- package/dist/types/config.d.ts +30 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +23 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/state.d.ts +56 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +6 -0
- package/dist/types/state.js.map +1 -0
- package/dist/utils/config.d.ts +15 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +80 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/errors.d.ts +25 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +48 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +73 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/state-api.d.ts +128 -0
- package/dist/utils/state-api.d.ts.map +1 -0
- package/dist/utils/state-api.js +170 -0
- package/dist/utils/state-api.js.map +1 -0
- package/dist/utils/template-client.d.ts +73 -0
- package/dist/utils/template-client.d.ts.map +1 -0
- package/dist/utils/template-client.js +151 -0
- package/dist/utils/template-client.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# FORGE API Design Patterns
|
|
2
|
+
|
|
3
|
+
> **Scope:** Internal FORGE APIs and interfaces between components.
|
|
4
|
+
|
|
5
|
+
## File Naming
|
|
6
|
+
|
|
7
|
+
- Use `kebab-case.ts` for files
|
|
8
|
+
- Use `PascalCase` for types, interfaces, classes
|
|
9
|
+
- Use `camelCase` for functions, variables
|
|
10
|
+
|
|
11
|
+
## Module Structure
|
|
12
|
+
|
|
13
|
+
Each module exports:
|
|
14
|
+
1. Types/interfaces first
|
|
15
|
+
2. Functions/operations
|
|
16
|
+
3. Constants at the end
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// src/state/event-types.ts
|
|
20
|
+
export interface StateEvent { ... }
|
|
21
|
+
export interface TaskStartedEvent extends StateEvent { ... }
|
|
22
|
+
|
|
23
|
+
export function createEvent(type: EventType, payload: unknown): StateEvent { ... }
|
|
24
|
+
|
|
25
|
+
export const EVENT_TYPES = ['TASK_STARTED', 'TASK_COMPLETED', ...] as const;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Error Handling
|
|
29
|
+
|
|
30
|
+
- Define custom error types extending `Error`
|
|
31
|
+
- Include error codes for programmatic handling
|
|
32
|
+
- Provide context in error messages
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
export class StateValidationError extends Error {
|
|
36
|
+
constructor(
|
|
37
|
+
public schema: string,
|
|
38
|
+
public errors: z.ZodError,
|
|
39
|
+
) {
|
|
40
|
+
super(`State validation failed for ${schema}: ${errors.message}`);
|
|
41
|
+
this.name = 'StateValidationError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Async Operations
|
|
47
|
+
|
|
48
|
+
- Use `async/await`, not Promises directly
|
|
49
|
+
- Return typed promises: `Promise<T>`
|
|
50
|
+
- Handle rejections with try/catch
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export async function loadState(path: string): Promise<State> {
|
|
54
|
+
try {
|
|
55
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new StateLoadError(path, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Input Validation
|
|
64
|
+
|
|
65
|
+
- Use Zod schemas for all public APIs
|
|
66
|
+
- Validate at module boundaries
|
|
67
|
+
- Return typed results, never `any`
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { z } from 'zod';
|
|
71
|
+
|
|
72
|
+
const EventSchema = z.object({
|
|
73
|
+
eventId: z.string(),
|
|
74
|
+
timestamp: z.string().datetime(),
|
|
75
|
+
taskId: z.string(),
|
|
76
|
+
actor: z.string(),
|
|
77
|
+
type: z.enum(EVENT_TYPES),
|
|
78
|
+
payload: z.unknown(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export function parseEvent(data: unknown): StateEvent {
|
|
82
|
+
return EventSchema.parse(data);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## File I/O
|
|
87
|
+
|
|
88
|
+
- Use absolute paths from project root
|
|
89
|
+
- Check file existence before reading
|
|
90
|
+
- Use atomic writes (write to temp, then rename)
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
export async function writeState(path: string, state: State): Promise<void> {
|
|
94
|
+
const tmpPath = `${path}.tmp`;
|
|
95
|
+
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2));
|
|
96
|
+
await fs.rename(tmpPath, path);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# FORGE Security Baseline
|
|
2
|
+
|
|
3
|
+
> **Scope:** Input validation, secrets management, dependency security, file operations.
|
|
4
|
+
|
|
5
|
+
## Input Validation
|
|
6
|
+
|
|
7
|
+
**All user input MUST be validated** with Zod schemas:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const ConfigSchema = z.object({
|
|
11
|
+
mode: z.enum(['yolo', 'interactive']),
|
|
12
|
+
depth: z.enum(['quick', 'standard', 'comprehensive']),
|
|
13
|
+
maxTeammates: z.number().int().min(2).max(6),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function loadConfig(path: string): Config {
|
|
17
|
+
const raw = JSON.parse(fs.readFileSync(path, 'utf-8'));
|
|
18
|
+
return ConfigSchema.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Path Validation
|
|
23
|
+
|
|
24
|
+
Prevent directory traversal:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { resolve, normalize } from 'path';
|
|
28
|
+
|
|
29
|
+
export function safeJoin(base: string, userPath: string): string {
|
|
30
|
+
const resolved = resolve(base, userPath);
|
|
31
|
+
if (!resolved.startsWith(base)) {
|
|
32
|
+
throw new Error('Invalid path: outside base directory');
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Secrets Management
|
|
39
|
+
|
|
40
|
+
**NEVER hardcode secrets.** Use environment variables:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
export function getApiKey(): string {
|
|
44
|
+
const key = process.env.FORGE_API_KEY;
|
|
45
|
+
if (!key) {
|
|
46
|
+
throw new Error('FORGE_API_KEY environment variable not set');
|
|
47
|
+
}
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Git Ignore
|
|
53
|
+
|
|
54
|
+
Add to `.gitignore`:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
# FORGE
|
|
58
|
+
state/events/*.json # Optional: don't commit event logs
|
|
59
|
+
.planning/local/ # User-specific config
|
|
60
|
+
.env # Environment variables
|
|
61
|
+
*.tmp # Temp files
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Command Injection Prevention
|
|
65
|
+
|
|
66
|
+
**NEVER pass user input directly to shell commands.** Use array arguments:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// BAD
|
|
70
|
+
await execa(`git checkout ${userBranch}`);
|
|
71
|
+
|
|
72
|
+
// GOOD
|
|
73
|
+
await execa('git', ['checkout', userBranch]);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## File Permissions
|
|
77
|
+
|
|
78
|
+
Respect user's umask, don't over-permission:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { constants } from 'fs';
|
|
82
|
+
|
|
83
|
+
await fs.writeFile(path, content, {
|
|
84
|
+
mode: 0o644, // rw-r--r--
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For sensitive files:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
await fs.writeFile(path, secrets, {
|
|
92
|
+
mode: 0o600, // rw-------
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Dependency Security
|
|
97
|
+
|
|
98
|
+
Run `npm audit` in CI:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
# .github/workflows/ci.yml
|
|
102
|
+
- name: Audit dependencies
|
|
103
|
+
run: npm audit --audit-level=moderate
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Locked Dependencies
|
|
107
|
+
|
|
108
|
+
Commit `package-lock.json`. Always install with exact versions:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm ci # Use lock file, don't update
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Code Execution Safety
|
|
115
|
+
|
|
116
|
+
**NO `eval()`, no dynamic imports from untrusted sources:**
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// BAD
|
|
120
|
+
const module = await import(userPath);
|
|
121
|
+
|
|
122
|
+
// GOOD — validate against whitelist
|
|
123
|
+
const ALLOWED = ['state', 'cli', 'templates'];
|
|
124
|
+
if (ALLOWED.includes(userPath)) {
|
|
125
|
+
const module = await import(`./${userPath}`);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Event Log Integrity
|
|
130
|
+
|
|
131
|
+
Events should be **append-only**. Never modify existing events:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
export async function writeEvent(path: string, event: StateEvent): Promise<void> {
|
|
135
|
+
if (await fs.pathExists(path)) {
|
|
136
|
+
throw new Error(`Event file already exists: ${path}`);
|
|
137
|
+
}
|
|
138
|
+
await fs.writeFile(path, JSON.stringify(event, null, 2), { mode: 0o444 }); // read-only
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## State Validation
|
|
143
|
+
|
|
144
|
+
Validate `STATE.json` against schema on every read:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
export async function loadState(path: string): Promise<State> {
|
|
148
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
149
|
+
const raw = JSON.parse(content);
|
|
150
|
+
return StateSchema.parse(raw); // Zod validation
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Git Worktree Isolation
|
|
155
|
+
|
|
156
|
+
Ensure worktrees can't access each other's files:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
export function validateWorktreePath(worktreePath: string): void {
|
|
160
|
+
if (!worktreePath.startsWith('.worktrees/')) {
|
|
161
|
+
throw new Error('Invalid worktree: must be under .worktrees/');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Error Messages
|
|
167
|
+
|
|
168
|
+
**Don't leak sensitive info in errors:**
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// BAD — leaks file path
|
|
172
|
+
throw new Error(`Failed to read ${filePath}`);
|
|
173
|
+
|
|
174
|
+
// GOOD — generic error
|
|
175
|
+
throw new Error('Failed to read configuration file');
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## CI/CD Security
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
# .github/workflows/ci.yml
|
|
182
|
+
permissions:
|
|
183
|
+
contents: read # No write access
|
|
184
|
+
issues: read
|
|
185
|
+
pull-requests: read
|
|
186
|
+
|
|
187
|
+
- name: Run tests
|
|
188
|
+
run: npm test
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Third-Party Code
|
|
192
|
+
|
|
193
|
+
Review all dependencies before adding:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm audit
|
|
197
|
+
npm ls <package>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Prefer well-maintained packages with:
|
|
201
|
+
- Active development
|
|
202
|
+
- Security policy
|
|
203
|
+
- TypeScript types
|
|
204
|
+
- Low dependency count
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# FORGE Testing Standards
|
|
2
|
+
|
|
3
|
+
> **Scope:** Test coverage, test structure, what to test and how.
|
|
4
|
+
|
|
5
|
+
## Test Structure
|
|
6
|
+
|
|
7
|
+
Place tests next to source files with `.test.ts` suffix:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/state/
|
|
11
|
+
event-types.ts
|
|
12
|
+
event-types.test.ts
|
|
13
|
+
src/cli/
|
|
14
|
+
init.ts
|
|
15
|
+
init.test.ts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Unit Tests
|
|
19
|
+
|
|
20
|
+
Test **pure functions** and **business logic**:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// event-types.test.ts
|
|
24
|
+
import { describe, it, expect } from 'jest';
|
|
25
|
+
import { parseEvent, createEvent } from './event-types';
|
|
26
|
+
|
|
27
|
+
describe('parseEvent', () => {
|
|
28
|
+
it('parses valid TASK_STARTED event', () => {
|
|
29
|
+
const event = parseEvent({
|
|
30
|
+
eventId: 'evt-001',
|
|
31
|
+
timestamp: '2026-02-11T21:10:00Z',
|
|
32
|
+
taskId: 'api-003',
|
|
33
|
+
actor: 'backend',
|
|
34
|
+
type: 'TASK_STARTED',
|
|
35
|
+
payload: {},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(event.type).toBe('TASK_STARTED');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('throws on missing eventId', () => {
|
|
42
|
+
expect(() => parseEvent({})).toThrow('eventId');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('throws on invalid timestamp format', () => {
|
|
46
|
+
expect(() => parseEvent({
|
|
47
|
+
eventId: 'evt-001',
|
|
48
|
+
timestamp: 'invalid',
|
|
49
|
+
taskId: 'api-003',
|
|
50
|
+
actor: 'backend',
|
|
51
|
+
type: 'TASK_STARTED',
|
|
52
|
+
payload: {},
|
|
53
|
+
})).toThrow('timestamp');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Integration Tests
|
|
59
|
+
|
|
60
|
+
Test **workflows end-to-end**:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// state-merge.integration.test.ts
|
|
64
|
+
describe('State Steward merge', () => {
|
|
65
|
+
const testDir = tmpdir();
|
|
66
|
+
|
|
67
|
+
beforeEach(async () => {
|
|
68
|
+
await fs.mkdir(`${testDir}/state/events`, { recursive: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('merges events in timestamp order', async () => {
|
|
72
|
+
await writeEvent(`${testDir}/state/events/evt-002.json`, {
|
|
73
|
+
timestamp: '2026-02-11T21:11:00Z',
|
|
74
|
+
type: 'TASK_COMPLETED',
|
|
75
|
+
});
|
|
76
|
+
await writeEvent(`${testDir}/state/events/evt-001.json`, {
|
|
77
|
+
timestamp: '2026-02-11T21:10:00Z',
|
|
78
|
+
type: 'TASK_STARTED',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const state = await mergeEvents(testDir);
|
|
82
|
+
|
|
83
|
+
expect(state.tasks[0].status).toBe('completed');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Coverage Targets
|
|
89
|
+
|
|
90
|
+
- **Core logic** (state, events, templates): >90%
|
|
91
|
+
- **CLI commands**: >70%
|
|
92
|
+
- **Git operations**: >80%
|
|
93
|
+
|
|
94
|
+
Run: `npm run test:coverage`
|
|
95
|
+
|
|
96
|
+
## Test Data
|
|
97
|
+
|
|
98
|
+
Create fixtures in `test/fixtures/`:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
test/fixtures/
|
|
102
|
+
events/
|
|
103
|
+
task-started.json
|
|
104
|
+
task-completed.json
|
|
105
|
+
state/
|
|
106
|
+
initial-state.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Mocking
|
|
110
|
+
|
|
111
|
+
Mock external dependencies (git, fs, network):
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { jest } from '@jest/globals';
|
|
115
|
+
|
|
116
|
+
jest.mock('execa', () => ({
|
|
117
|
+
execa: jest.fn(),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
import { execa } from 'execa';
|
|
121
|
+
|
|
122
|
+
it('creates git worktree', async () => {
|
|
123
|
+
(execa as jest.Mock).mockResolvedValue({ stdout: '' });
|
|
124
|
+
|
|
125
|
+
await createWorktree('api-003', 'forge/api-003');
|
|
126
|
+
|
|
127
|
+
expect(execa).toHaveBeenCalledWith('git', [
|
|
128
|
+
'worktree', 'add', '.worktrees/api-003', '-b', 'forge/api-003',
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Test Organization
|
|
134
|
+
|
|
135
|
+
Use `describe` for logical groups, `it` for individual tests:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
describe('State Steward', () => {
|
|
139
|
+
describe('mergeEvents', () => {
|
|
140
|
+
it('handles empty event directory');
|
|
141
|
+
it('sorts events by timestamp');
|
|
142
|
+
it('handles duplicate eventIds');
|
|
143
|
+
it('skips malformed events');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Async Tests
|
|
149
|
+
|
|
150
|
+
Always `await` async operations:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
it('merges events', async () => {
|
|
154
|
+
const state = await mergeEvents('/path/to/events');
|
|
155
|
+
expect(state).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Clean Up
|
|
160
|
+
|
|
161
|
+
Use `beforeEach` / `afterEach` for test isolation:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { tmpdir } from 'os';
|
|
165
|
+
import { rimraf } from 'rimraf';
|
|
166
|
+
|
|
167
|
+
let testDir: string;
|
|
168
|
+
|
|
169
|
+
beforeEach(async () => {
|
|
170
|
+
testDir = path.join(tmpdir(), `forge-test-${Date.now()}`);
|
|
171
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
afterEach(async () => {
|
|
175
|
+
await rimraf(testDir);
|
|
176
|
+
});
|
|
177
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# FORGE CLI UI Conventions
|
|
2
|
+
|
|
3
|
+
> **Scope:** Command-line interface patterns, user interaction, output formatting.
|
|
4
|
+
|
|
5
|
+
## Output Style
|
|
6
|
+
|
|
7
|
+
### Success Messages
|
|
8
|
+
|
|
9
|
+
- Green checkmark emoji ✓
|
|
10
|
+
- Concise, clear
|
|
11
|
+
- Include what changed
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
✓ Created state/events/evt-001.json
|
|
15
|
+
✓ Merged 12 events to STATE.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Error Messages
|
|
19
|
+
|
|
20
|
+
- Red ✗ or [ERROR] prefix
|
|
21
|
+
- What went wrong + why + how to fix
|
|
22
|
+
- Stack traces only in --debug mode
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
✗ Failed to merge events: Invalid event format in evt-003.json
|
|
26
|
+
→ Run 'forge validate' to check event schemas
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Warning Messages
|
|
30
|
+
|
|
31
|
+
- Yellow ⚠ prefix
|
|
32
|
+
- Not blocking, but noteworthy
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
⚠ Task api-003 is blocked by api-001 (in_progress)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Info Messages
|
|
39
|
+
|
|
40
|
+
- Blue → prefix for steps
|
|
41
|
+
- Show progress for long operations
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
→ Loading events from state/events/...
|
|
45
|
+
→ Found 47 events, merging...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Progress Indicators
|
|
49
|
+
|
|
50
|
+
For operations >2 seconds, show progress:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import ora from 'ora';
|
|
54
|
+
|
|
55
|
+
const spinner = ora('Initializing project...').start();
|
|
56
|
+
// ... work ...
|
|
57
|
+
spinner.succeed('Project initialized');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Help Text Format
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
forge init Initialize a new FORGE project
|
|
64
|
+
|
|
65
|
+
USAGE:
|
|
66
|
+
forge init [options]
|
|
67
|
+
|
|
68
|
+
OPTIONS:
|
|
69
|
+
--name <name> Project name (default: current directory)
|
|
70
|
+
--quick Skip interactive prompts (use defaults)
|
|
71
|
+
|
|
72
|
+
EXAMPLES:
|
|
73
|
+
$ forge init
|
|
74
|
+
$ forge init --name my-app --quick
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Interactive Prompts
|
|
78
|
+
|
|
79
|
+
When prompting users:
|
|
80
|
+
- Show current value in brackets: `[default: my-app]`
|
|
81
|
+
- Allow (y/N) confirmations
|
|
82
|
+
- Validate input before accepting
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import inquirer from 'inquirer';
|
|
86
|
+
|
|
87
|
+
const answers = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'input',
|
|
90
|
+
name: 'projectName',
|
|
91
|
+
message: 'Project name:',
|
|
92
|
+
default: path.basename(process.cwd()),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'confirm',
|
|
96
|
+
name: 'confirm',
|
|
97
|
+
message: 'Initialize project?',
|
|
98
|
+
default: true,
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Tables and Lists
|
|
104
|
+
|
|
105
|
+
For structured output (status, tasks, events):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
CURRENT TASKS:
|
|
109
|
+
|
|
110
|
+
ID STATUS OWNER TITLE
|
|
111
|
+
──────────────────────────────────────────────────────
|
|
112
|
+
api-001 completed backend User CRUD API
|
|
113
|
+
api-002 in_progress backend Auth service
|
|
114
|
+
ui-001 pending frontend Login form
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## ASCII Art Banner
|
|
118
|
+
|
|
119
|
+
Show on startup:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
█ ██ ██
|
|
123
|
+
█ █ █ █ █ █
|
|
124
|
+
███ █ █
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Exit Codes
|
|
128
|
+
|
|
129
|
+
- `0`: Success
|
|
130
|
+
- `1`: General error
|
|
131
|
+
- `2`: Invalid arguments
|
|
132
|
+
- `3`: State corruption
|
|
133
|
+
- `4`: Git error
|
|
134
|
+
|
|
135
|
+
## Verbose Flag
|
|
136
|
+
|
|
137
|
+
Support `-v` / `--verbose` for debug output:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
forge merge -v
|
|
141
|
+
# → Shows detailed event replay, validation errors, etc.
|
|
142
|
+
```
|