loopwork 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +528 -0
- package/bin/loopwork +0 -0
- package/examples/README.md +70 -0
- package/examples/basic-json-backend/.specs/tasks/TASK-001.md +22 -0
- package/examples/basic-json-backend/.specs/tasks/TASK-002.md +23 -0
- package/examples/basic-json-backend/.specs/tasks/TASK-003.md +37 -0
- package/examples/basic-json-backend/.specs/tasks/tasks.json +19 -0
- package/examples/basic-json-backend/README.md +32 -0
- package/examples/basic-json-backend/TESTING.md +184 -0
- package/examples/basic-json-backend/hello.test.ts +9 -0
- package/examples/basic-json-backend/hello.ts +3 -0
- package/examples/basic-json-backend/loopwork.config.js +35 -0
- package/examples/basic-json-backend/math.test.ts +29 -0
- package/examples/basic-json-backend/math.ts +3 -0
- package/examples/basic-json-backend/package.json +15 -0
- package/examples/basic-json-backend/quick-start.sh +80 -0
- package/loopwork.config.ts +164 -0
- package/package.json +26 -0
- package/src/backends/github.ts +426 -0
- package/src/backends/index.ts +86 -0
- package/src/backends/json.ts +598 -0
- package/src/backends/plugin.ts +317 -0
- package/src/backends/types.ts +19 -0
- package/src/commands/init.ts +100 -0
- package/src/commands/run.ts +365 -0
- package/src/contracts/backend.ts +127 -0
- package/src/contracts/config.ts +129 -0
- package/src/contracts/index.ts +43 -0
- package/src/contracts/plugin.ts +82 -0
- package/src/contracts/task.ts +78 -0
- package/src/core/cli.ts +275 -0
- package/src/core/config.ts +165 -0
- package/src/core/state.ts +154 -0
- package/src/core/utils.ts +125 -0
- package/src/dashboard/cli.ts +449 -0
- package/src/dashboard/index.ts +6 -0
- package/src/dashboard/kanban.tsx +226 -0
- package/src/dashboard/tui.tsx +372 -0
- package/src/index.ts +19 -0
- package/src/mcp/server.ts +451 -0
- package/src/monitor/index.ts +420 -0
- package/src/plugins/asana.ts +192 -0
- package/src/plugins/cost-tracking.ts +402 -0
- package/src/plugins/discord.ts +269 -0
- package/src/plugins/everhour.ts +335 -0
- package/src/plugins/index.ts +253 -0
- package/src/plugins/telegram/bot.ts +517 -0
- package/src/plugins/telegram/index.ts +6 -0
- package/src/plugins/telegram/notifications.ts +198 -0
- package/src/plugins/todoist.ts +261 -0
- package/test/backends.test.ts +929 -0
- package/test/cli.test.ts +145 -0
- package/test/config.test.ts +90 -0
- package/test/e2e.test.ts +458 -0
- package/test/github-tasks.test.ts +191 -0
- package/test/loopwork-config-types.test.ts +288 -0
- package/test/monitor.test.ts +123 -0
- package/test/plugins.test.ts +1175 -0
- package/test/state.test.ts +295 -0
- package/test/utils.test.ts +60 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend Plugin System for Loopwork
|
|
3
|
+
*
|
|
4
|
+
* Backends (JSON, GitHub) are now plugins that implement both:
|
|
5
|
+
* - LoopworkPlugin: lifecycle hooks
|
|
6
|
+
* - TaskBackend: task CRUD operations
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* export default compose(
|
|
10
|
+
* withJSONBackend({ tasksFile: 'tasks.json' }),
|
|
11
|
+
* // or
|
|
12
|
+
* withGitHubBackend({ repo: 'owner/repo' }),
|
|
13
|
+
* )(defineConfig({ cli: 'opencode' }))
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { LoopworkPlugin, LoopworkConfig } from '../contracts'
|
|
17
|
+
import type { Task, Priority, FindTaskOptions, UpdateResult } from './types'
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Backend Plugin Interface
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Backend plugin combines LoopworkPlugin with TaskBackend operations
|
|
25
|
+
*/
|
|
26
|
+
export interface BackendPlugin extends LoopworkPlugin {
|
|
27
|
+
/** Backend type identifier */
|
|
28
|
+
readonly backendType: 'json' | 'github' | string
|
|
29
|
+
|
|
30
|
+
// Task operations
|
|
31
|
+
findNextTask(options?: FindTaskOptions): Promise<Task | null>
|
|
32
|
+
getTask(taskId: string): Promise<Task | null>
|
|
33
|
+
listPendingTasks(options?: FindTaskOptions): Promise<Task[]>
|
|
34
|
+
countPending(options?: FindTaskOptions): Promise<number>
|
|
35
|
+
markInProgress(taskId: string): Promise<UpdateResult>
|
|
36
|
+
markCompleted(taskId: string, comment?: string): Promise<UpdateResult>
|
|
37
|
+
markFailed(taskId: string, error: string): Promise<UpdateResult>
|
|
38
|
+
resetToPending(taskId: string): Promise<UpdateResult>
|
|
39
|
+
addComment?(taskId: string, comment: string): Promise<UpdateResult>
|
|
40
|
+
ping(): Promise<{ ok: boolean; latencyMs: number; error?: string }>
|
|
41
|
+
|
|
42
|
+
// Sub-task and dependency methods
|
|
43
|
+
getSubTasks(taskId: string): Promise<Task[]>
|
|
44
|
+
getDependencies(taskId: string): Promise<Task[]>
|
|
45
|
+
getDependents(taskId: string): Promise<Task[]>
|
|
46
|
+
areDependenciesMet(taskId: string): Promise<boolean>
|
|
47
|
+
createTask?(task: Omit<Task, 'id' | 'status'>): Promise<Task>
|
|
48
|
+
createSubTask?(parentId: string, task: Omit<Task, 'id' | 'parentId' | 'status'>): Promise<Task>
|
|
49
|
+
addDependency?(taskId: string, dependsOnId: string): Promise<UpdateResult>
|
|
50
|
+
removeDependency?(taskId: string, dependsOnId: string): Promise<UpdateResult>
|
|
51
|
+
|
|
52
|
+
// Priority
|
|
53
|
+
setPriority?(taskId: string, priority: Priority): Promise<UpdateResult>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// JSON Backend Plugin
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
export interface JSONBackendConfig {
|
|
61
|
+
tasksFile?: string
|
|
62
|
+
tasksDir?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create JSON backend plugin
|
|
67
|
+
*/
|
|
68
|
+
export function createJSONBackendPlugin(config: JSONBackendConfig = {}): BackendPlugin {
|
|
69
|
+
const tasksFile = config.tasksFile || '.specs/tasks/tasks.json'
|
|
70
|
+
|
|
71
|
+
// Lazy load the adapter
|
|
72
|
+
let adapter: any = null
|
|
73
|
+
|
|
74
|
+
const getAdapter = async () => {
|
|
75
|
+
if (!adapter) {
|
|
76
|
+
const { JsonTaskAdapter } = await import('./json')
|
|
77
|
+
adapter = new JsonTaskAdapter({ type: 'json', tasksFile, tasksDir: config.tasksDir })
|
|
78
|
+
}
|
|
79
|
+
return adapter
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
name: 'json-backend',
|
|
84
|
+
backendType: 'json',
|
|
85
|
+
|
|
86
|
+
async onConfigLoad(cfg) {
|
|
87
|
+
await getAdapter()
|
|
88
|
+
return cfg
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// Delegate all backend operations to the adapter
|
|
92
|
+
async findNextTask(options) {
|
|
93
|
+
return (await getAdapter()).findNextTask(options)
|
|
94
|
+
},
|
|
95
|
+
async getTask(taskId) {
|
|
96
|
+
return (await getAdapter()).getTask(taskId)
|
|
97
|
+
},
|
|
98
|
+
async listPendingTasks(options) {
|
|
99
|
+
return (await getAdapter()).listPendingTasks(options)
|
|
100
|
+
},
|
|
101
|
+
async countPending(options) {
|
|
102
|
+
return (await getAdapter()).countPending(options)
|
|
103
|
+
},
|
|
104
|
+
async markInProgress(taskId) {
|
|
105
|
+
return (await getAdapter()).markInProgress(taskId)
|
|
106
|
+
},
|
|
107
|
+
async markCompleted(taskId, comment) {
|
|
108
|
+
return (await getAdapter()).markCompleted(taskId, comment)
|
|
109
|
+
},
|
|
110
|
+
async markFailed(taskId, error) {
|
|
111
|
+
return (await getAdapter()).markFailed(taskId, error)
|
|
112
|
+
},
|
|
113
|
+
async resetToPending(taskId) {
|
|
114
|
+
return (await getAdapter()).resetToPending(taskId)
|
|
115
|
+
},
|
|
116
|
+
async addComment(taskId, comment) {
|
|
117
|
+
const a = await getAdapter()
|
|
118
|
+
return a.addComment?.(taskId, comment) || { success: false, error: 'Not supported' }
|
|
119
|
+
},
|
|
120
|
+
async ping() {
|
|
121
|
+
return (await getAdapter()).ping()
|
|
122
|
+
},
|
|
123
|
+
async getSubTasks(taskId) {
|
|
124
|
+
return (await getAdapter()).getSubTasks(taskId)
|
|
125
|
+
},
|
|
126
|
+
async getDependencies(taskId) {
|
|
127
|
+
return (await getAdapter()).getDependencies(taskId)
|
|
128
|
+
},
|
|
129
|
+
async getDependents(taskId) {
|
|
130
|
+
return (await getAdapter()).getDependents(taskId)
|
|
131
|
+
},
|
|
132
|
+
async areDependenciesMet(taskId) {
|
|
133
|
+
return (await getAdapter()).areDependenciesMet(taskId)
|
|
134
|
+
},
|
|
135
|
+
async createTask(task) {
|
|
136
|
+
const a = await getAdapter()
|
|
137
|
+
if (!a.createTask) throw new Error('createTask not supported')
|
|
138
|
+
return a.createTask(task)
|
|
139
|
+
},
|
|
140
|
+
async createSubTask(parentId, task) {
|
|
141
|
+
const a = await getAdapter()
|
|
142
|
+
if (!a.createSubTask) throw new Error('createSubTask not supported')
|
|
143
|
+
return a.createSubTask(parentId, task)
|
|
144
|
+
},
|
|
145
|
+
async addDependency(taskId, dependsOnId) {
|
|
146
|
+
const a = await getAdapter()
|
|
147
|
+
return a.addDependency?.(taskId, dependsOnId) || { success: false, error: 'Not supported' }
|
|
148
|
+
},
|
|
149
|
+
async removeDependency(taskId, dependsOnId) {
|
|
150
|
+
const a = await getAdapter()
|
|
151
|
+
return a.removeDependency?.(taskId, dependsOnId) || { success: false, error: 'Not supported' }
|
|
152
|
+
},
|
|
153
|
+
async setPriority(taskId, priority) {
|
|
154
|
+
const a = await getAdapter()
|
|
155
|
+
if (a.setPriority) {
|
|
156
|
+
return a.setPriority(taskId, priority)
|
|
157
|
+
}
|
|
158
|
+
return { success: false, error: 'setPriority not supported by JSON adapter' }
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Config wrapper for JSON backend
|
|
165
|
+
*/
|
|
166
|
+
export function withJSONBackend(config: JSONBackendConfig = {}) {
|
|
167
|
+
return (baseConfig: LoopworkConfig): LoopworkConfig => ({
|
|
168
|
+
...baseConfig,
|
|
169
|
+
backend: {
|
|
170
|
+
type: 'json',
|
|
171
|
+
tasksFile: config.tasksFile || '.specs/tasks/tasks.json',
|
|
172
|
+
tasksDir: config.tasksDir,
|
|
173
|
+
},
|
|
174
|
+
plugins: [...(baseConfig.plugins || []), createJSONBackendPlugin(config)],
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// GitHub Backend Plugin
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
export interface GitHubBackendConfig {
|
|
183
|
+
repo?: string
|
|
184
|
+
token?: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create GitHub backend plugin
|
|
189
|
+
*/
|
|
190
|
+
export function createGitHubBackendPlugin(config: GitHubBackendConfig = {}): BackendPlugin {
|
|
191
|
+
const repo = config.repo || process.env.GITHUB_REPOSITORY
|
|
192
|
+
|
|
193
|
+
// Lazy load the adapter
|
|
194
|
+
let adapter: any = null
|
|
195
|
+
|
|
196
|
+
const getAdapter = async () => {
|
|
197
|
+
if (!adapter) {
|
|
198
|
+
const { GitHubTaskAdapter } = await import('./github')
|
|
199
|
+
adapter = new GitHubTaskAdapter({ type: 'github', repo })
|
|
200
|
+
}
|
|
201
|
+
return adapter
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
name: 'github-backend',
|
|
206
|
+
backendType: 'github',
|
|
207
|
+
|
|
208
|
+
async onConfigLoad(cfg) {
|
|
209
|
+
if (!repo) {
|
|
210
|
+
console.warn('GitHub backend: Missing repo. Set GITHUB_REPOSITORY or pass repo option.')
|
|
211
|
+
}
|
|
212
|
+
await getAdapter()
|
|
213
|
+
return cfg
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Delegate all backend operations
|
|
217
|
+
async findNextTask(options) {
|
|
218
|
+
return (await getAdapter()).findNextTask(options)
|
|
219
|
+
},
|
|
220
|
+
async getTask(taskId) {
|
|
221
|
+
return (await getAdapter()).getTask(taskId)
|
|
222
|
+
},
|
|
223
|
+
async listPendingTasks(options) {
|
|
224
|
+
return (await getAdapter()).listPendingTasks(options)
|
|
225
|
+
},
|
|
226
|
+
async countPending(options) {
|
|
227
|
+
return (await getAdapter()).countPending(options)
|
|
228
|
+
},
|
|
229
|
+
async markInProgress(taskId) {
|
|
230
|
+
return (await getAdapter()).markInProgress(taskId)
|
|
231
|
+
},
|
|
232
|
+
async markCompleted(taskId, comment) {
|
|
233
|
+
return (await getAdapter()).markCompleted(taskId, comment)
|
|
234
|
+
},
|
|
235
|
+
async markFailed(taskId, error) {
|
|
236
|
+
return (await getAdapter()).markFailed(taskId, error)
|
|
237
|
+
},
|
|
238
|
+
async resetToPending(taskId) {
|
|
239
|
+
return (await getAdapter()).resetToPending(taskId)
|
|
240
|
+
},
|
|
241
|
+
async addComment(taskId, comment) {
|
|
242
|
+
return (await getAdapter()).addComment(taskId, comment)
|
|
243
|
+
},
|
|
244
|
+
async ping() {
|
|
245
|
+
return (await getAdapter()).ping()
|
|
246
|
+
},
|
|
247
|
+
async getSubTasks(taskId) {
|
|
248
|
+
return (await getAdapter()).getSubTasks(taskId)
|
|
249
|
+
},
|
|
250
|
+
async getDependencies(taskId) {
|
|
251
|
+
return (await getAdapter()).getDependencies(taskId)
|
|
252
|
+
},
|
|
253
|
+
async getDependents(taskId) {
|
|
254
|
+
return (await getAdapter()).getDependents(taskId)
|
|
255
|
+
},
|
|
256
|
+
async areDependenciesMet(taskId) {
|
|
257
|
+
return (await getAdapter()).areDependenciesMet(taskId)
|
|
258
|
+
},
|
|
259
|
+
async createTask(task) {
|
|
260
|
+
return (await getAdapter()).createTask(task)
|
|
261
|
+
},
|
|
262
|
+
async createSubTask(parentId, task) {
|
|
263
|
+
return (await getAdapter()).createSubTask(parentId, task)
|
|
264
|
+
},
|
|
265
|
+
async addDependency(taskId, dependsOnId) {
|
|
266
|
+
return (await getAdapter()).addDependency(taskId, dependsOnId)
|
|
267
|
+
},
|
|
268
|
+
async removeDependency(taskId, dependsOnId) {
|
|
269
|
+
return (await getAdapter()).removeDependency(taskId, dependsOnId)
|
|
270
|
+
},
|
|
271
|
+
async setPriority(taskId, priority) {
|
|
272
|
+
return (await getAdapter()).setPriority(taskId, priority)
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Config wrapper for GitHub backend
|
|
279
|
+
*/
|
|
280
|
+
export function withGitHubBackend(config: GitHubBackendConfig = {}) {
|
|
281
|
+
return (baseConfig: LoopworkConfig): LoopworkConfig => ({
|
|
282
|
+
...baseConfig,
|
|
283
|
+
backend: {
|
|
284
|
+
type: 'github',
|
|
285
|
+
repo: config.repo || process.env.GITHUB_REPOSITORY,
|
|
286
|
+
},
|
|
287
|
+
plugins: [...(baseConfig.plugins || []), createGitHubBackendPlugin(config)],
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Helper to get backend from config
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get the backend plugin from config plugins array
|
|
297
|
+
*/
|
|
298
|
+
export function getBackendPlugin(config: LoopworkConfig): BackendPlugin | null {
|
|
299
|
+
const plugins = config.plugins || []
|
|
300
|
+
for (const plugin of plugins) {
|
|
301
|
+
if ('backendType' in plugin) {
|
|
302
|
+
return plugin as BackendPlugin
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get backend or throw error
|
|
310
|
+
*/
|
|
311
|
+
export function requireBackend(config: LoopworkConfig): BackendPlugin {
|
|
312
|
+
const backend = getBackendPlugin(config)
|
|
313
|
+
if (!backend) {
|
|
314
|
+
throw new Error('No backend plugin found. Use withJSONBackend() or withGitHubBackend().')
|
|
315
|
+
}
|
|
316
|
+
return backend
|
|
317
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend Types - Re-exports from contracts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
Task,
|
|
7
|
+
TaskStatus,
|
|
8
|
+
Priority,
|
|
9
|
+
} from '../contracts/task'
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
TaskBackend,
|
|
13
|
+
BackendPlugin,
|
|
14
|
+
BackendConfig,
|
|
15
|
+
BackendFactory,
|
|
16
|
+
FindTaskOptions,
|
|
17
|
+
UpdateResult,
|
|
18
|
+
PingResult,
|
|
19
|
+
} from '../contracts/backend'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { promptUser, logger } from '../core/utils'
|
|
4
|
+
import readline from 'readline'
|
|
5
|
+
|
|
6
|
+
async function ask(question: string, defaultValue: string): Promise<string> {
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout
|
|
10
|
+
})
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
rl.question(`${question} [${defaultValue}]: `, (answer) => {
|
|
13
|
+
rl.close()
|
|
14
|
+
resolve(answer || defaultValue)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function init() {
|
|
20
|
+
const backendChoice = await promptUser('Backend type (github/json) [json]: ', 'json')
|
|
21
|
+
const backendType = backendChoice.toLowerCase().startsWith('g') ? 'github' : 'json'
|
|
22
|
+
|
|
23
|
+
const aiChoice = await promptUser('AI CLI tool (opencode/claude) [opencode]: ', 'opencode')
|
|
24
|
+
const aiTool = aiChoice.toLowerCase().startsWith('c') ? 'claude' : 'opencode'
|
|
25
|
+
|
|
26
|
+
let backendConfig = ''
|
|
27
|
+
let tasksFile = '.specs/tasks/tasks.json'
|
|
28
|
+
let prdDir = '.specs/tasks'
|
|
29
|
+
|
|
30
|
+
if (backendType === 'github') {
|
|
31
|
+
const repoName = await ask('Repo name', 'current repo')
|
|
32
|
+
backendConfig = `withGitHubBackend({ repo: ${repoName === 'current repo' ? 'undefined' : `'${repoName}'`} })`
|
|
33
|
+
} else {
|
|
34
|
+
prdDir = await ask('Task directory', '.specs/tasks')
|
|
35
|
+
if (prdDir.endsWith('/')) prdDir = prdDir.slice(0, -1)
|
|
36
|
+
tasksFile = path.join(prdDir, 'tasks.json')
|
|
37
|
+
backendConfig = `withJSONBackend({ tasksFile: '${tasksFile}' })`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const configContent = `import { defineConfig, compose, withCostTracking } from './src/loopwork-config-types'
|
|
41
|
+
import { withJSONBackend, withGitHubBackend } from './src/backend-plugin'
|
|
42
|
+
|
|
43
|
+
export default compose(
|
|
44
|
+
${backendConfig},
|
|
45
|
+
withCostTracking({ dailyBudget: 10.00 }),
|
|
46
|
+
)(defineConfig({
|
|
47
|
+
cli: '${aiTool}',
|
|
48
|
+
maxIterations: 50,
|
|
49
|
+
}))
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync('loopwork.config.ts')) {
|
|
53
|
+
const overwrite = await promptUser('loopwork.config.ts already exists. Overwrite? (y/N): ', 'n')
|
|
54
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
55
|
+
logger.info('Initialization aborted.')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fs.writeFileSync('loopwork.config.ts', configContent)
|
|
61
|
+
logger.success('Created loopwork.config.ts')
|
|
62
|
+
|
|
63
|
+
if (backendType === 'json') {
|
|
64
|
+
if (!fs.existsSync(prdDir)) {
|
|
65
|
+
fs.mkdirSync(prdDir, { recursive: true })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tasksJson = {
|
|
69
|
+
"tasks": [
|
|
70
|
+
{
|
|
71
|
+
"id": "TASK-001",
|
|
72
|
+
"status": "pending",
|
|
73
|
+
"priority": "high",
|
|
74
|
+
"title": "My First Task",
|
|
75
|
+
"description": "Implement the first feature"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(tasksFile, JSON.stringify(tasksJson, null, 2))
|
|
81
|
+
logger.success(`Created ${tasksFile}`)
|
|
82
|
+
|
|
83
|
+
const samplePrd = `# TASK-001: My First Task
|
|
84
|
+
|
|
85
|
+
## Goal
|
|
86
|
+
Implement the first feature
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
- [ ] Requirement 1
|
|
90
|
+
- [ ] Requirement 2
|
|
91
|
+
`
|
|
92
|
+
const prdFile = path.join(prdDir, 'TASK-001.md')
|
|
93
|
+
fs.writeFileSync(prdFile, samplePrd)
|
|
94
|
+
logger.success(`Created ${prdFile}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.info('\nNext steps:')
|
|
98
|
+
logger.info('1. Install dependencies: bun install')
|
|
99
|
+
logger.info('2. Run loopwork: bun run start')
|
|
100
|
+
}
|