@specmarket/cli 0.0.4 → 0.0.5
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/README.md +1 -1
- package/dist/{chunk-MS2DYACY.js → chunk-DLEMNRTH.js} +19 -2
- package/dist/chunk-DLEMNRTH.js.map +1 -0
- package/dist/{config-R5KWZSJP.js → config-OAU6SJLC.js} +2 -2
- package/dist/index.js +980 -181
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/comment.test.ts +211 -0
- package/src/commands/comment.ts +176 -0
- package/src/commands/fork.test.ts +163 -0
- package/src/commands/info.test.ts +192 -0
- package/src/commands/info.ts +66 -2
- package/src/commands/init.test.ts +106 -0
- package/src/commands/init.ts +12 -10
- package/src/commands/issues.test.ts +377 -0
- package/src/commands/issues.ts +443 -0
- package/src/commands/login.test.ts +99 -0
- package/src/commands/logout.test.ts +54 -0
- package/src/commands/publish.test.ts +146 -0
- package/src/commands/report.test.ts +181 -0
- package/src/commands/run.test.ts +213 -0
- package/src/commands/run.ts +10 -2
- package/src/commands/search.test.ts +147 -0
- package/src/commands/validate.test.ts +129 -2
- package/src/commands/validate.ts +333 -192
- package/src/commands/whoami.test.ts +106 -0
- package/src/index.ts +6 -0
- package/src/lib/convex-client.ts +6 -2
- package/src/lib/format-detection.test.ts +223 -0
- package/src/lib/format-detection.ts +172 -0
- package/src/lib/ralph-loop.ts +49 -20
- package/src/lib/telemetry.ts +2 -1
- package/dist/chunk-MS2DYACY.js.map +0 -1
- /package/dist/{config-R5KWZSJP.js.map → config-OAU6SJLC.js.map} +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// --- Hoisted mocks ---
|
|
4
|
+
|
|
5
|
+
const { mockQuery, mockAction, mockClient, mockSpinner } = vi.hoisted(() => {
|
|
6
|
+
const mockQuery = vi.fn();
|
|
7
|
+
const mockAction = vi.fn();
|
|
8
|
+
const mockClient = { query: mockQuery, action: mockAction };
|
|
9
|
+
const mockSpinner = {
|
|
10
|
+
start: vi.fn().mockReturnThis(),
|
|
11
|
+
stop: vi.fn().mockReturnThis(),
|
|
12
|
+
succeed: vi.fn().mockReturnThis(),
|
|
13
|
+
fail: vi.fn().mockReturnThis(),
|
|
14
|
+
};
|
|
15
|
+
return { mockQuery, mockAction, mockClient, mockSpinner };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock('../lib/convex-client.js', () => ({
|
|
19
|
+
getConvexClient: vi.fn().mockResolvedValue(mockClient),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('ora', () => ({
|
|
23
|
+
default: vi.fn().mockReturnValue(mockSpinner),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('@specmarket/convex/api', () => ({
|
|
27
|
+
api: {
|
|
28
|
+
specs: {
|
|
29
|
+
get: 'specs.get',
|
|
30
|
+
getVersions: 'specs.getVersions',
|
|
31
|
+
},
|
|
32
|
+
runs: { getStats: 'runs.getStats' },
|
|
33
|
+
users: { getProfile: 'users.getProfile' },
|
|
34
|
+
issues: { list: 'issues.list' },
|
|
35
|
+
specMaintainers: { list: 'specMaintainers.list' },
|
|
36
|
+
comments: { list: 'comments.list' },
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
41
|
+
throw new Error('process.exit called');
|
|
42
|
+
}) as any);
|
|
43
|
+
|
|
44
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
45
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
46
|
+
|
|
47
|
+
import { handleInfo } from './info.js';
|
|
48
|
+
|
|
49
|
+
// --- Test data ---
|
|
50
|
+
|
|
51
|
+
const MOCK_SPEC = {
|
|
52
|
+
_id: 'spec123',
|
|
53
|
+
scopedName: '@alice/todo-app',
|
|
54
|
+
displayName: 'Todo App',
|
|
55
|
+
description: 'A full-featured todo application',
|
|
56
|
+
slug: 'todo-app',
|
|
57
|
+
authorId: 'user1',
|
|
58
|
+
outputType: 'web-app',
|
|
59
|
+
primaryStack: 'nextjs-typescript',
|
|
60
|
+
currentVersion: '1.2.0',
|
|
61
|
+
runner: 'claude',
|
|
62
|
+
minModel: 'claude-opus-4-5',
|
|
63
|
+
status: 'published',
|
|
64
|
+
tags: ['productivity', 'todo'],
|
|
65
|
+
estimatedTokens: 50000,
|
|
66
|
+
estimatedCostUsd: 2.5,
|
|
67
|
+
estimatedTimeMinutes: 30,
|
|
68
|
+
totalRuns: 42,
|
|
69
|
+
successfulRuns: 36,
|
|
70
|
+
successRate: 0.857,
|
|
71
|
+
avgCostUsd: 2.1,
|
|
72
|
+
communityRating: 4.3,
|
|
73
|
+
ratingCount: 15,
|
|
74
|
+
forkCount: 3,
|
|
75
|
+
replacesSaas: 'Todoist',
|
|
76
|
+
replacesPricing: '$4/mo',
|
|
77
|
+
forkedFromId: null,
|
|
78
|
+
infrastructure: null,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Return Promises from mockQuery since info.ts chains .catch() on query results
|
|
82
|
+
function setupQueryMock(overrides: Record<string, unknown> = {}) {
|
|
83
|
+
const defaults: Record<string, unknown> = {
|
|
84
|
+
'specs.get': MOCK_SPEC,
|
|
85
|
+
'runs.getStats': { totalRuns: 0, modelBreakdown: [], costDistribution: { min: 0, max: 0, p50: 0 } },
|
|
86
|
+
'specs.getVersions': { page: [] },
|
|
87
|
+
'issues.list': { page: [], isDone: true },
|
|
88
|
+
'specMaintainers.list': [],
|
|
89
|
+
'comments.list': { page: [], isDone: true },
|
|
90
|
+
'users.getProfile': null,
|
|
91
|
+
};
|
|
92
|
+
const merged = { ...defaults, ...overrides };
|
|
93
|
+
mockQuery.mockImplementation((fn: string) => Promise.resolve(merged[fn] ?? null));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('handleInfo', () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
vi.clearAllMocks();
|
|
99
|
+
mockExit.mockImplementation((() => {
|
|
100
|
+
throw new Error('process.exit called');
|
|
101
|
+
}) as any);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('displays full spec information for a scoped name', async () => {
|
|
105
|
+
setupQueryMock({
|
|
106
|
+
'users.getProfile': { username: 'alice', displayName: 'Alice', bio: 'Developer' },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await handleInfo('@alice/todo-app');
|
|
110
|
+
|
|
111
|
+
expect(mockQuery).toHaveBeenCalledWith('specs.get', {
|
|
112
|
+
scopedName: '@alice/todo-app',
|
|
113
|
+
});
|
|
114
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
115
|
+
expect.stringContaining('Todo App')
|
|
116
|
+
);
|
|
117
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
118
|
+
expect.stringContaining('web-app')
|
|
119
|
+
);
|
|
120
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
121
|
+
expect.stringContaining('85.7%')
|
|
122
|
+
);
|
|
123
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
124
|
+
expect.stringContaining('Todoist')
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('exits with error when spec not found', async () => {
|
|
129
|
+
setupQueryMock({ 'specs.get': null });
|
|
130
|
+
|
|
131
|
+
await expect(handleInfo('@alice/nonexistent')).rejects.toThrow(
|
|
132
|
+
'process.exit called'
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
136
|
+
expect.stringContaining('Spec not found')
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('shows version history when versions exist', async () => {
|
|
141
|
+
setupQueryMock({
|
|
142
|
+
'specs.getVersions': {
|
|
143
|
+
page: [
|
|
144
|
+
{ version: '1.2.0', publishedAt: Date.now(), changelog: 'Added dark mode' },
|
|
145
|
+
{ version: '1.1.0', publishedAt: Date.now() - 86400_000, changelog: 'Bug fixes' },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await handleInfo('@alice/todo-app');
|
|
151
|
+
|
|
152
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
153
|
+
expect.stringContaining('Versions (2)')
|
|
154
|
+
);
|
|
155
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
156
|
+
expect.stringContaining('v1.2.0')
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('displays infrastructure details when present', async () => {
|
|
161
|
+
setupQueryMock({
|
|
162
|
+
'specs.get': {
|
|
163
|
+
...MOCK_SPEC,
|
|
164
|
+
infrastructure: {
|
|
165
|
+
monthlyCost: { freeTierUsd: 0, productionUsd: 25 },
|
|
166
|
+
setupTimeMinutes: 15,
|
|
167
|
+
services: [
|
|
168
|
+
{
|
|
169
|
+
category: 'database',
|
|
170
|
+
name: 'PostgreSQL',
|
|
171
|
+
purpose: 'Data store',
|
|
172
|
+
required: true,
|
|
173
|
+
defaultProvider: 'Neon',
|
|
174
|
+
providers: [{ name: 'Neon', freeTier: true }],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
userProvided: [],
|
|
178
|
+
deploymentTargets: [{ name: 'Vercel', notes: 'Recommended' }],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await handleInfo('@alice/todo-app');
|
|
184
|
+
|
|
185
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
186
|
+
expect.stringContaining('Infrastructure')
|
|
187
|
+
);
|
|
188
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
189
|
+
expect.stringContaining('PostgreSQL')
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
package/src/commands/info.ts
CHANGED
|
@@ -26,11 +26,56 @@ export async function handleInfo(specId: string): Promise<void> {
|
|
|
26
26
|
// Determine if it's a scoped name or an ID
|
|
27
27
|
const isScopedName = specId.startsWith('@') || specId.includes('/');
|
|
28
28
|
|
|
29
|
-
const [spec, stats,
|
|
29
|
+
const [spec, stats, versionsResult] = await Promise.all([
|
|
30
30
|
client.query(api.specs.get, isScopedName ? { scopedName: specId } : { specId }),
|
|
31
31
|
client.query(api.runs.getStats, { specId: specId as any }).catch(() => null),
|
|
32
|
-
client.query(api.specs.getVersions, { specId: specId as any }).catch(() => []),
|
|
32
|
+
client.query(api.specs.getVersions, { specId: specId as any, paginationOpts: { numItems: 25, cursor: null } }).catch(() => ({ page: [] })),
|
|
33
33
|
]);
|
|
34
|
+
const versions = versionsResult.page;
|
|
35
|
+
|
|
36
|
+
// Fetch collaboration data (best-effort — don't fail if these aren't available)
|
|
37
|
+
let openIssueCount = 0;
|
|
38
|
+
let maintainers: Array<{ user: { username: string } | null }> = [];
|
|
39
|
+
let commentCount = 0;
|
|
40
|
+
|
|
41
|
+
if (spec) {
|
|
42
|
+
const [issuesResult, maintainersResult, commentsResult] = await Promise.all([
|
|
43
|
+
client
|
|
44
|
+
.query(api.issues.list, {
|
|
45
|
+
specId: spec._id,
|
|
46
|
+
status: 'open' as const,
|
|
47
|
+
paginationOpts: { numItems: 1, cursor: null },
|
|
48
|
+
})
|
|
49
|
+
.catch(() => null),
|
|
50
|
+
client
|
|
51
|
+
.query(api.specMaintainers.list, { specId: spec._id })
|
|
52
|
+
.catch(() => []),
|
|
53
|
+
client
|
|
54
|
+
.query(api.comments.list, {
|
|
55
|
+
targetType: 'spec' as const,
|
|
56
|
+
targetId: spec._id,
|
|
57
|
+
paginationOpts: { numItems: 1, cursor: null },
|
|
58
|
+
})
|
|
59
|
+
.catch(() => null),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// issues.list returns paginated results — use page length as approximation
|
|
63
|
+
// For accurate count we'd need a dedicated count query, but this shows "at least N"
|
|
64
|
+
if (issuesResult) {
|
|
65
|
+
openIssueCount = issuesResult.page.length;
|
|
66
|
+
if (!issuesResult.isDone && openIssueCount > 0) {
|
|
67
|
+
// There are more pages — indicate "N+"
|
|
68
|
+
openIssueCount = -1; // sentinel for "many"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
maintainers = maintainersResult;
|
|
72
|
+
if (commentsResult) {
|
|
73
|
+
commentCount = commentsResult.page.length;
|
|
74
|
+
if (!commentsResult.isDone && commentCount > 0) {
|
|
75
|
+
commentCount = -1; // sentinel for "many"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
34
79
|
|
|
35
80
|
spinner.stop();
|
|
36
81
|
|
|
@@ -91,6 +136,25 @@ export async function handleInfo(specId: string): Promise<void> {
|
|
|
91
136
|
console.log(chalk.gray(` (Forked from v${spec.forkedFromVersion})`));
|
|
92
137
|
}
|
|
93
138
|
|
|
139
|
+
// Collaboration info
|
|
140
|
+
const issueDisplay =
|
|
141
|
+
openIssueCount === -1 ? 'many' : String(openIssueCount);
|
|
142
|
+
const commentDisplay =
|
|
143
|
+
commentCount === -1 ? 'many' : String(commentCount);
|
|
144
|
+
console.log(
|
|
145
|
+
` Open Issues: ${issueDisplay}`
|
|
146
|
+
);
|
|
147
|
+
console.log(
|
|
148
|
+
` Comments: ${commentDisplay}`
|
|
149
|
+
);
|
|
150
|
+
if (maintainers.length > 0) {
|
|
151
|
+
const names = maintainers
|
|
152
|
+
.filter((m) => m.user)
|
|
153
|
+
.map((m) => `@${m.user!.username}`)
|
|
154
|
+
.join(', ');
|
|
155
|
+
console.log(` Maintainers: ${names}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
94
158
|
if (author) {
|
|
95
159
|
console.log('');
|
|
96
160
|
console.log(chalk.bold('Creator:'));
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { readFile, rm, access } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
|
|
7
|
+
// --- Hoisted mocks ---
|
|
8
|
+
|
|
9
|
+
const { mockSpinner, mockPrompt } = vi.hoisted(() => {
|
|
10
|
+
const mockSpinner = {
|
|
11
|
+
start: vi.fn().mockReturnThis(),
|
|
12
|
+
stop: vi.fn().mockReturnThis(),
|
|
13
|
+
succeed: vi.fn().mockReturnThis(),
|
|
14
|
+
fail: vi.fn().mockReturnThis(),
|
|
15
|
+
warn: vi.fn().mockReturnThis(),
|
|
16
|
+
};
|
|
17
|
+
const mockPrompt = vi.fn();
|
|
18
|
+
return { mockSpinner, mockPrompt };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
vi.mock('ora', () => ({
|
|
22
|
+
default: vi.fn().mockReturnValue(mockSpinner),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('inquirer', () => ({
|
|
26
|
+
default: { prompt: mockPrompt },
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
30
|
+
|
|
31
|
+
import { handleInit } from './init.js';
|
|
32
|
+
|
|
33
|
+
describe('handleInit', () => {
|
|
34
|
+
let tmpDir: string;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
tmpDir = join(tmpdir(), `init-test-${randomUUID()}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('creates spec directory with all required files', async () => {
|
|
46
|
+
mockPrompt.mockResolvedValue({
|
|
47
|
+
name: 'test-spec',
|
|
48
|
+
displayName: 'Test Spec',
|
|
49
|
+
replacesSaas: '',
|
|
50
|
+
outputType: 'web-app',
|
|
51
|
+
primaryStack: 'nextjs-typescript',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await handleInit({ path: tmpDir });
|
|
55
|
+
|
|
56
|
+
// Verify all required files were created
|
|
57
|
+
await expect(access(join(tmpDir, 'spec.yaml'))).resolves.toBeUndefined();
|
|
58
|
+
await expect(access(join(tmpDir, 'PROMPT.md'))).resolves.toBeUndefined();
|
|
59
|
+
await expect(access(join(tmpDir, 'SPEC.md'))).resolves.toBeUndefined();
|
|
60
|
+
await expect(access(join(tmpDir, 'SUCCESS_CRITERIA.md'))).resolves.toBeUndefined();
|
|
61
|
+
await expect(access(join(tmpDir, 'stdlib', 'STACK.md'))).resolves.toBeUndefined();
|
|
62
|
+
await expect(access(join(tmpDir, 'TASKS.md'))).resolves.toBeUndefined();
|
|
63
|
+
|
|
64
|
+
expect(mockSpinner.succeed).toHaveBeenCalledWith(
|
|
65
|
+
expect.stringContaining('Spec created')
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('writes correct spec.yaml content based on prompts', async () => {
|
|
70
|
+
mockPrompt.mockResolvedValue({
|
|
71
|
+
name: 'my-project',
|
|
72
|
+
displayName: 'My Project',
|
|
73
|
+
replacesSaas: 'Notion',
|
|
74
|
+
outputType: 'web-app',
|
|
75
|
+
primaryStack: 'nextjs-typescript',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await handleInit({ path: tmpDir });
|
|
79
|
+
|
|
80
|
+
const specYaml = await readFile(join(tmpDir, 'spec.yaml'), 'utf-8');
|
|
81
|
+
expect(specYaml).toContain('name: my-project');
|
|
82
|
+
expect(specYaml).toContain('display_name: "My Project"');
|
|
83
|
+
expect(specYaml).toContain('replaces_saas: "Notion"');
|
|
84
|
+
expect(specYaml).toContain('output_type: web-app');
|
|
85
|
+
expect(specYaml).toContain('primary_stack: nextjs-typescript');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('shows next-steps instructions after creation', async () => {
|
|
89
|
+
mockPrompt.mockResolvedValue({
|
|
90
|
+
name: 'test-spec',
|
|
91
|
+
displayName: 'Test Spec',
|
|
92
|
+
replacesSaas: '',
|
|
93
|
+
outputType: 'cli-tool',
|
|
94
|
+
primaryStack: 'go',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await handleInit({ path: tmpDir });
|
|
98
|
+
|
|
99
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
100
|
+
expect.stringContaining('Next steps')
|
|
101
|
+
);
|
|
102
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
103
|
+
expect.stringContaining('specmarket validate')
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
package/src/commands/init.ts
CHANGED
|
@@ -26,7 +26,7 @@ ${data.replacesSaas ? `replaces_saas: "${data.replacesSaas}"` : '# replaces_saas
|
|
|
26
26
|
output_type: ${data.outputType}
|
|
27
27
|
primary_stack: ${data.primaryStack}
|
|
28
28
|
version: "1.0.0"
|
|
29
|
-
runner: claude
|
|
29
|
+
runner: claude
|
|
30
30
|
min_model: "claude-opus-4-5"
|
|
31
31
|
|
|
32
32
|
estimated_tokens: 50000
|
|
@@ -71,9 +71,9 @@ Read the requirements in SPEC.md and implement the application step by step.
|
|
|
71
71
|
## Process
|
|
72
72
|
|
|
73
73
|
1. Read SPEC.md completely before writing any code
|
|
74
|
-
2. Check
|
|
74
|
+
2. Check TASKS.md for outstanding items
|
|
75
75
|
3. Implement features, run tests, iterate
|
|
76
|
-
4. Update
|
|
76
|
+
4. Update TASKS.md as you complete items
|
|
77
77
|
5. Verify SUCCESS_CRITERIA.md criteria are met
|
|
78
78
|
|
|
79
79
|
## Rules
|
|
@@ -81,7 +81,7 @@ Read the requirements in SPEC.md and implement the application step by step.
|
|
|
81
81
|
- Follow stdlib/STACK.md for technology choices
|
|
82
82
|
- Write tests for all business logic
|
|
83
83
|
- Do not skip steps or take shortcuts
|
|
84
|
-
- Update
|
|
84
|
+
- Update TASKS.md after each significant change
|
|
85
85
|
`;
|
|
86
86
|
|
|
87
87
|
const SPEC_MD_TEMPLATE = (data: { displayName: string }) => `# ${data.displayName} — Specification
|
|
@@ -144,12 +144,12 @@ ${primaryStack}
|
|
|
144
144
|
- Playwright for E2E (optional)
|
|
145
145
|
`;
|
|
146
146
|
|
|
147
|
-
const
|
|
147
|
+
const TASKS_MD_TEMPLATE = (displayName: string) => `# Tasks
|
|
148
148
|
|
|
149
149
|
> This file tracks outstanding work. Update it after each change.
|
|
150
|
-
>
|
|
150
|
+
> All items checked = implementation complete.
|
|
151
151
|
|
|
152
|
-
## ${displayName} — Initial Implementation
|
|
152
|
+
## Phase 1: ${displayName} — Initial Implementation
|
|
153
153
|
|
|
154
154
|
- [ ] Set up project structure and dependencies
|
|
155
155
|
- [ ] Implement core data model
|
|
@@ -158,6 +158,8 @@ const FIX_PLAN_TEMPLATE = (displayName: string) => `# Fix Plan
|
|
|
158
158
|
- [ ] Implement UI/interface
|
|
159
159
|
- [ ] Write integration tests
|
|
160
160
|
- [ ] Update README.md
|
|
161
|
+
|
|
162
|
+
## Discovered Issues
|
|
161
163
|
`;
|
|
162
164
|
|
|
163
165
|
export async function handleInit(opts: {
|
|
@@ -247,7 +249,7 @@ export async function handleInit(opts: {
|
|
|
247
249
|
writeFile(join(targetDir, 'SPEC.md'), SPEC_MD_TEMPLATE(data)),
|
|
248
250
|
writeFile(join(targetDir, 'SUCCESS_CRITERIA.md'), SUCCESS_CRITERIA_TEMPLATE),
|
|
249
251
|
writeFile(join(targetDir, 'stdlib', 'STACK.md'), STACK_MD_TEMPLATE(answers.primaryStack)),
|
|
250
|
-
writeFile(join(targetDir, '
|
|
252
|
+
writeFile(join(targetDir, 'TASKS.md'), TASKS_MD_TEMPLATE(answers.displayName)),
|
|
251
253
|
]);
|
|
252
254
|
|
|
253
255
|
spinner.succeed(chalk.green(`Spec created at ${targetDir}`));
|
|
@@ -257,8 +259,8 @@ export async function handleInit(opts: {
|
|
|
257
259
|
console.log(` 1. ${chalk.cyan(`cd ${answers.name}`)}`);
|
|
258
260
|
console.log(` 2. Edit ${chalk.cyan('SPEC.md')} with your application requirements`);
|
|
259
261
|
console.log(` 3. Edit ${chalk.cyan('SUCCESS_CRITERIA.md')} with specific pass/fail criteria`);
|
|
260
|
-
console.log(` 4. Run ${chalk.cyan(`specmarket validate
|
|
261
|
-
console.log(` 5. Run ${chalk.cyan(`specmarket run
|
|
262
|
+
console.log(` 4. Run ${chalk.cyan(`specmarket validate`)} to check your spec`);
|
|
263
|
+
console.log(` 5. Run ${chalk.cyan(`specmarket run`)} to execute the spec`);
|
|
262
264
|
} catch (err) {
|
|
263
265
|
spinner.fail(chalk.red(`Failed to create spec: ${(err as Error).message}`));
|
|
264
266
|
throw err;
|