opencode-planpilot 0.1.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/LICENSE +21 -0
- package/README.md +31 -0
- package/docs/planpilot.md +219 -0
- package/package.json +62 -0
- package/src/cli.ts +1483 -0
- package/src/index.ts +140 -0
- package/src/lib/app.ts +1072 -0
- package/src/lib/argv.ts +58 -0
- package/src/lib/db.ts +108 -0
- package/src/lib/errors.ts +36 -0
- package/src/lib/format.ts +239 -0
- package/src/lib/instructions.ts +26 -0
- package/src/lib/models.ts +144 -0
- package/src/lib/util.ts +76 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import { openDatabase, resolvePlanMarkdownPath, ensureParentDir } from "./lib/db"
|
|
4
|
+
import { PlanpilotApp } from "./lib/app"
|
|
5
|
+
import {
|
|
6
|
+
createEmptyStatusChanges,
|
|
7
|
+
statusChangesEmpty,
|
|
8
|
+
type GoalStatus,
|
|
9
|
+
type PlanOrder,
|
|
10
|
+
type PlanStatus,
|
|
11
|
+
type StepExecutor,
|
|
12
|
+
type StepStatus,
|
|
13
|
+
} from "./lib/models"
|
|
14
|
+
import { AppError, invalidInput } from "./lib/errors"
|
|
15
|
+
import { ensureNonEmpty, projectMatchesPath, resolveMaybeRealpath } from "./lib/util"
|
|
16
|
+
import { formatGoalDetail, formatPlanDetail, formatPlanMarkdown, formatStepDetail } from "./lib/format"
|
|
17
|
+
|
|
18
|
+
const CWD_FLAG = "--cwd"
|
|
19
|
+
const SESSION_ID_FLAG = "--session-id"
|
|
20
|
+
const DEFAULT_PAGE = 1
|
|
21
|
+
const DEFAULT_LIMIT = 20
|
|
22
|
+
|
|
23
|
+
export type CliIO = {
|
|
24
|
+
log: (...args: any[]) => void
|
|
25
|
+
error: (...args: any[]) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultIO: CliIO = {
|
|
29
|
+
log: (...args) => {
|
|
30
|
+
console.log(...args)
|
|
31
|
+
},
|
|
32
|
+
error: (...args) => {
|
|
33
|
+
console.error(...args)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let currentIO: CliIO = defaultIO
|
|
38
|
+
|
|
39
|
+
function log(...args: any[]) {
|
|
40
|
+
currentIO.log(...args)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function error(...args: any[]) {
|
|
44
|
+
currentIO.error(...args)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function withIO<T>(io: CliIO, fn: () => Promise<T> | T): Promise<T> {
|
|
48
|
+
const prev = currentIO
|
|
49
|
+
currentIO = io
|
|
50
|
+
try {
|
|
51
|
+
return await fn()
|
|
52
|
+
} finally {
|
|
53
|
+
currentIO = prev
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatCliError(err: unknown): string {
|
|
58
|
+
if (err instanceof AppError) {
|
|
59
|
+
return `Error: ${err.toDisplayString()}`
|
|
60
|
+
}
|
|
61
|
+
if (err instanceof Error) {
|
|
62
|
+
return `Error: ${err.message}`
|
|
63
|
+
}
|
|
64
|
+
return `Error: ${String(err)}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runCLI(argv: string[] = process.argv.slice(2), io: CliIO = defaultIO) {
|
|
68
|
+
return withIO(io, async () => {
|
|
69
|
+
const { cwd, cwdFlagPresent, sessionId, remaining } = parseGlobalArgs(argv)
|
|
70
|
+
|
|
71
|
+
if (!remaining.length) {
|
|
72
|
+
throw invalidInput("missing command")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const [section, subcommand, ...args] = remaining
|
|
76
|
+
|
|
77
|
+
const resolvedSessionId = resolveSessionId(sessionId)
|
|
78
|
+
|
|
79
|
+
const db = openDatabase()
|
|
80
|
+
const resolvedCwd = cwd ? resolveMaybeRealpath(cwd) : undefined
|
|
81
|
+
const app = new PlanpilotApp(db, resolvedSessionId, resolvedCwd)
|
|
82
|
+
|
|
83
|
+
let planIds: number[] = []
|
|
84
|
+
let shouldSync = false
|
|
85
|
+
|
|
86
|
+
switch (section) {
|
|
87
|
+
case "plan": {
|
|
88
|
+
const result = await handlePlan(app, subcommand, args, { cwd, cwdFlagPresent })
|
|
89
|
+
planIds = result.planIds
|
|
90
|
+
shouldSync = result.shouldSync
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
case "step": {
|
|
94
|
+
const result = await handleStep(app, subcommand, args)
|
|
95
|
+
planIds = result.planIds
|
|
96
|
+
shouldSync = result.shouldSync
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case "goal": {
|
|
100
|
+
const result = await handleGoal(app, subcommand, args)
|
|
101
|
+
planIds = result.planIds
|
|
102
|
+
shouldSync = result.shouldSync
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
default:
|
|
106
|
+
throw invalidInput(`unknown command: ${section}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (shouldSync) {
|
|
110
|
+
syncPlanMarkdown(app, planIds)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseGlobalArgs(argv: string[]) {
|
|
116
|
+
let cwd: string | undefined
|
|
117
|
+
let sessionId: string | undefined
|
|
118
|
+
let cwdFlagPresent = false
|
|
119
|
+
const remaining: string[] = []
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
122
|
+
const token = argv[i]
|
|
123
|
+
if (token === CWD_FLAG) {
|
|
124
|
+
cwdFlagPresent = true
|
|
125
|
+
cwd = argv[i + 1]
|
|
126
|
+
i += 1
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (token === SESSION_ID_FLAG) {
|
|
130
|
+
sessionId = argv[i + 1]
|
|
131
|
+
i += 1
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
remaining.push(token)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { cwd, cwdFlagPresent, sessionId, remaining }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveSessionId(sessionId: string | undefined): string {
|
|
141
|
+
if (!sessionId) {
|
|
142
|
+
throw invalidInput(`${SESSION_ID_FLAG} is required`)
|
|
143
|
+
}
|
|
144
|
+
const trimmed = sessionId.trim()
|
|
145
|
+
if (!trimmed) {
|
|
146
|
+
throw invalidInput(`${SESSION_ID_FLAG} is empty`)
|
|
147
|
+
}
|
|
148
|
+
return trimmed
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function requireCwd(cwdFlagPresent: boolean, cwd: string | undefined): string {
|
|
152
|
+
if (!cwdFlagPresent) {
|
|
153
|
+
throw invalidInput(`${CWD_FLAG} is required`)
|
|
154
|
+
}
|
|
155
|
+
if (!cwd || !cwd.trim()) {
|
|
156
|
+
throw invalidInput(`${CWD_FLAG} is empty`)
|
|
157
|
+
}
|
|
158
|
+
return cwd
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function handlePlan(
|
|
162
|
+
app: PlanpilotApp,
|
|
163
|
+
subcommand: string | undefined,
|
|
164
|
+
args: string[],
|
|
165
|
+
context: { cwd: string | undefined; cwdFlagPresent: boolean },
|
|
166
|
+
) {
|
|
167
|
+
switch (subcommand) {
|
|
168
|
+
case "add":
|
|
169
|
+
return { planIds: handlePlanAdd(app, args), shouldSync: true }
|
|
170
|
+
case "add-tree":
|
|
171
|
+
return { planIds: handlePlanAddTree(app, args), shouldSync: true }
|
|
172
|
+
case "list":
|
|
173
|
+
return { planIds: handlePlanList(app, args, context), shouldSync: false }
|
|
174
|
+
case "count":
|
|
175
|
+
return { planIds: handlePlanCount(app, args, context), shouldSync: false }
|
|
176
|
+
case "search":
|
|
177
|
+
return { planIds: handlePlanSearch(app, args, context), shouldSync: false }
|
|
178
|
+
case "show":
|
|
179
|
+
return { planIds: handlePlanShow(app, args), shouldSync: false }
|
|
180
|
+
case "export":
|
|
181
|
+
return { planIds: handlePlanExport(app, args), shouldSync: false }
|
|
182
|
+
case "comment":
|
|
183
|
+
return { planIds: handlePlanComment(app, args), shouldSync: true }
|
|
184
|
+
case "update":
|
|
185
|
+
return { planIds: handlePlanUpdate(app, args), shouldSync: true }
|
|
186
|
+
case "done":
|
|
187
|
+
return { planIds: handlePlanDone(app, args), shouldSync: true }
|
|
188
|
+
case "remove":
|
|
189
|
+
return { planIds: handlePlanRemove(app, args), shouldSync: true }
|
|
190
|
+
case "activate":
|
|
191
|
+
return { planIds: handlePlanActivate(app, args), shouldSync: true }
|
|
192
|
+
case "show-active":
|
|
193
|
+
return { planIds: handlePlanActive(app), shouldSync: false }
|
|
194
|
+
case "deactivate":
|
|
195
|
+
return { planIds: handlePlanDeactivate(app), shouldSync: true }
|
|
196
|
+
default:
|
|
197
|
+
throw invalidInput(`unknown plan command: ${subcommand ?? ""}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function handleStep(app: PlanpilotApp, subcommand: string | undefined, args: string[]) {
|
|
202
|
+
switch (subcommand) {
|
|
203
|
+
case "add":
|
|
204
|
+
return { planIds: handleStepAdd(app, args), shouldSync: true }
|
|
205
|
+
case "add-tree":
|
|
206
|
+
return { planIds: handleStepAddTree(app, args), shouldSync: true }
|
|
207
|
+
case "list":
|
|
208
|
+
return { planIds: handleStepList(app, args), shouldSync: false }
|
|
209
|
+
case "count":
|
|
210
|
+
return { planIds: handleStepCount(app, args), shouldSync: false }
|
|
211
|
+
case "show":
|
|
212
|
+
return { planIds: handleStepShow(app, args), shouldSync: false }
|
|
213
|
+
case "show-next":
|
|
214
|
+
return { planIds: handleStepShowNext(app), shouldSync: false }
|
|
215
|
+
case "comment":
|
|
216
|
+
return { planIds: handleStepComment(app, args), shouldSync: true }
|
|
217
|
+
case "update":
|
|
218
|
+
return { planIds: handleStepUpdate(app, args), shouldSync: true }
|
|
219
|
+
case "done":
|
|
220
|
+
return { planIds: handleStepDone(app, args), shouldSync: true }
|
|
221
|
+
case "move":
|
|
222
|
+
return { planIds: handleStepMove(app, args), shouldSync: true }
|
|
223
|
+
case "remove":
|
|
224
|
+
return { planIds: handleStepRemove(app, args), shouldSync: true }
|
|
225
|
+
default:
|
|
226
|
+
throw invalidInput(`unknown step command: ${subcommand ?? ""}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function handleGoal(app: PlanpilotApp, subcommand: string | undefined, args: string[]) {
|
|
231
|
+
switch (subcommand) {
|
|
232
|
+
case "add":
|
|
233
|
+
return { planIds: handleGoalAdd(app, args), shouldSync: true }
|
|
234
|
+
case "list":
|
|
235
|
+
return { planIds: handleGoalList(app, args), shouldSync: false }
|
|
236
|
+
case "count":
|
|
237
|
+
return { planIds: handleGoalCount(app, args), shouldSync: false }
|
|
238
|
+
case "show":
|
|
239
|
+
return { planIds: handleGoalShow(app, args), shouldSync: false }
|
|
240
|
+
case "comment":
|
|
241
|
+
return { planIds: handleGoalComment(app, args), shouldSync: true }
|
|
242
|
+
case "update":
|
|
243
|
+
return { planIds: handleGoalUpdate(app, args), shouldSync: true }
|
|
244
|
+
case "done":
|
|
245
|
+
return { planIds: handleGoalDone(app, args), shouldSync: true }
|
|
246
|
+
case "remove":
|
|
247
|
+
return { planIds: handleGoalRemove(app, args), shouldSync: true }
|
|
248
|
+
default:
|
|
249
|
+
throw invalidInput(`unknown goal command: ${subcommand ?? ""}`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function handlePlanAdd(app: PlanpilotApp, args: string[]): number[] {
|
|
254
|
+
const [title, content] = args
|
|
255
|
+
if (!title || content === undefined) {
|
|
256
|
+
throw invalidInput("plan add requires <title> <content>")
|
|
257
|
+
}
|
|
258
|
+
ensureNonEmpty("plan content", content)
|
|
259
|
+
const plan = app.addPlan({ title, content })
|
|
260
|
+
log(`Created plan ID: ${plan.id}: ${plan.title}`)
|
|
261
|
+
return [plan.id]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function handlePlanAddTree(app: PlanpilotApp, args: string[]): number[] {
|
|
265
|
+
const [title, content, ...rest] = args
|
|
266
|
+
if (!title || content === undefined) {
|
|
267
|
+
throw invalidInput("plan add-tree requires <title> <content> and at least one --step")
|
|
268
|
+
}
|
|
269
|
+
ensureNonEmpty("plan title", title)
|
|
270
|
+
ensureNonEmpty("plan content", content)
|
|
271
|
+
const specs = parsePlanAddTreeSteps(rest)
|
|
272
|
+
if (!specs.length) {
|
|
273
|
+
throw invalidInput("plan add-tree requires at least one --step")
|
|
274
|
+
}
|
|
275
|
+
const steps = specs.map((spec) => ({
|
|
276
|
+
content: spec.content,
|
|
277
|
+
executor: spec.executor ?? "ai",
|
|
278
|
+
goals: spec.goals ?? [],
|
|
279
|
+
}))
|
|
280
|
+
const result = app.addPlanTree({ title, content }, steps)
|
|
281
|
+
log(`Created plan ID: ${result.plan.id}: ${result.plan.title} (steps: ${result.stepCount}, goals: ${result.goalCount})`)
|
|
282
|
+
app.setActivePlan(result.plan.id, false)
|
|
283
|
+
log(`Active plan set to ${result.plan.id}: ${result.plan.title}`)
|
|
284
|
+
return [result.plan.id]
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function handlePlanList(
|
|
288
|
+
app: PlanpilotApp,
|
|
289
|
+
args: string[],
|
|
290
|
+
context: { cwd: string | undefined; cwdFlagPresent: boolean },
|
|
291
|
+
): number[] {
|
|
292
|
+
const { options, positionals } = parseOptions(args)
|
|
293
|
+
if (positionals.length) {
|
|
294
|
+
throw invalidInput(`plan list unexpected argument: ${positionals.join(" ")}`)
|
|
295
|
+
}
|
|
296
|
+
if (options.search && options.search.length) {
|
|
297
|
+
throw invalidInput("plan list does not accept --search")
|
|
298
|
+
}
|
|
299
|
+
const allowed = new Set(["scope", "status", "limit", "page", "order", "desc", "search"])
|
|
300
|
+
for (const key of Object.keys(options)) {
|
|
301
|
+
if (!allowed.has(key)) {
|
|
302
|
+
throw invalidInput(`plan list does not support --${key}`)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
|
|
307
|
+
const cwd = requireCwd(context.cwdFlagPresent, context.cwd)
|
|
308
|
+
|
|
309
|
+
const order = options.order ? parsePlanOrder(options.order) : "updated"
|
|
310
|
+
const desc = options.desc ?? true
|
|
311
|
+
let plans = app.listPlans(order, desc)
|
|
312
|
+
if (!plans.length) {
|
|
313
|
+
log("No plans found.")
|
|
314
|
+
return []
|
|
315
|
+
}
|
|
316
|
+
plans = plans.filter((plan) => (desiredStatus ? plan.status === desiredStatus : true))
|
|
317
|
+
const scope = parseScope(options.scope)
|
|
318
|
+
if (scope === "project") {
|
|
319
|
+
const cwdValue = resolveMaybeRealpath(cwd)
|
|
320
|
+
plans = plans.filter((plan) => plan.last_cwd && projectMatchesPath(plan.last_cwd, cwdValue))
|
|
321
|
+
}
|
|
322
|
+
if (!plans.length) {
|
|
323
|
+
log("No plans found.")
|
|
324
|
+
return []
|
|
325
|
+
}
|
|
326
|
+
const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE })
|
|
327
|
+
const total = plans.length
|
|
328
|
+
if (total === 0) {
|
|
329
|
+
log("No plans found.")
|
|
330
|
+
return []
|
|
331
|
+
}
|
|
332
|
+
const totalPages = Math.ceil(total / pagination.limit)
|
|
333
|
+
if (pagination.page > totalPages) {
|
|
334
|
+
log(`Page ${pagination.page} exceeds total pages ${totalPages}.`)
|
|
335
|
+
return []
|
|
336
|
+
}
|
|
337
|
+
const start = pagination.offset
|
|
338
|
+
const end = start + pagination.limit
|
|
339
|
+
plans = plans.slice(start, end)
|
|
340
|
+
const details = app.getPlanDetails(plans)
|
|
341
|
+
printPlanList(details)
|
|
342
|
+
logPageFooter(pagination.page, pagination.limit)
|
|
343
|
+
return []
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function handlePlanCount(
|
|
347
|
+
app: PlanpilotApp,
|
|
348
|
+
args: string[],
|
|
349
|
+
context: { cwd: string | undefined; cwdFlagPresent: boolean },
|
|
350
|
+
): number[] {
|
|
351
|
+
const { options, positionals } = parseOptions(args)
|
|
352
|
+
if (positionals.length) {
|
|
353
|
+
throw invalidInput(`plan count unexpected argument: ${positionals.join(" ")}`)
|
|
354
|
+
}
|
|
355
|
+
if (options.search && options.search.length) {
|
|
356
|
+
throw invalidInput("plan count does not accept --search")
|
|
357
|
+
}
|
|
358
|
+
const allowed = new Set(["scope", "status", "search"])
|
|
359
|
+
for (const key of Object.keys(options)) {
|
|
360
|
+
if (!allowed.has(key)) {
|
|
361
|
+
throw invalidInput(`plan count does not support --${key}`)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
|
|
366
|
+
const cwd = requireCwd(context.cwdFlagPresent, context.cwd)
|
|
367
|
+
|
|
368
|
+
let plans = app.listPlans()
|
|
369
|
+
if (!plans.length) {
|
|
370
|
+
log("Total: 0")
|
|
371
|
+
return []
|
|
372
|
+
}
|
|
373
|
+
plans = plans.filter((plan) => (desiredStatus ? plan.status === desiredStatus : true))
|
|
374
|
+
const scope = parseScope(options.scope)
|
|
375
|
+
if (scope === "project") {
|
|
376
|
+
const cwdValue = resolveMaybeRealpath(cwd)
|
|
377
|
+
plans = plans.filter((plan) => plan.last_cwd && projectMatchesPath(plan.last_cwd, cwdValue))
|
|
378
|
+
}
|
|
379
|
+
log(`Total: ${plans.length}`)
|
|
380
|
+
return []
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function handlePlanSearch(
|
|
384
|
+
app: PlanpilotApp,
|
|
385
|
+
args: string[],
|
|
386
|
+
context: { cwd: string | undefined; cwdFlagPresent: boolean },
|
|
387
|
+
): number[] {
|
|
388
|
+
const { options, positionals } = parseOptions(args)
|
|
389
|
+
if (positionals.length) {
|
|
390
|
+
throw invalidInput(`plan search unexpected argument: ${positionals.join(" ")}`)
|
|
391
|
+
}
|
|
392
|
+
if (options.search && !options.search.length) {
|
|
393
|
+
throw invalidInput("plan search requires at least one --search")
|
|
394
|
+
}
|
|
395
|
+
const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
|
|
396
|
+
const cwd = requireCwd(context.cwdFlagPresent, context.cwd)
|
|
397
|
+
|
|
398
|
+
const order = options.order ? parsePlanOrder(options.order) : "updated"
|
|
399
|
+
const desc = options.desc ?? true
|
|
400
|
+
let plans = app.listPlans(order, desc)
|
|
401
|
+
if (!plans.length) {
|
|
402
|
+
log("No plans found.")
|
|
403
|
+
return []
|
|
404
|
+
}
|
|
405
|
+
plans = plans.filter((plan) => (desiredStatus ? plan.status === desiredStatus : true))
|
|
406
|
+
const scope = parseScope(options.scope)
|
|
407
|
+
if (scope === "project") {
|
|
408
|
+
const cwdValue = resolveMaybeRealpath(cwd)
|
|
409
|
+
plans = plans.filter((plan) => plan.last_cwd && projectMatchesPath(plan.last_cwd, cwdValue))
|
|
410
|
+
}
|
|
411
|
+
if (!plans.length) {
|
|
412
|
+
log("No plans found.")
|
|
413
|
+
return []
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const details = app.getPlanDetails(plans)
|
|
417
|
+
const query = new PlanSearchQuery(options.search, options.searchMode, options.searchField, options.matchCase)
|
|
418
|
+
const filtered = details.filter((detail) => planMatchesSearch(detail, query))
|
|
419
|
+
if (!filtered.length) {
|
|
420
|
+
log("No plans found.")
|
|
421
|
+
return []
|
|
422
|
+
}
|
|
423
|
+
const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE })
|
|
424
|
+
const totalPages = Math.ceil(filtered.length / pagination.limit)
|
|
425
|
+
if (pagination.page > totalPages) {
|
|
426
|
+
log(`Page ${pagination.page} exceeds total pages ${totalPages}.`)
|
|
427
|
+
return []
|
|
428
|
+
}
|
|
429
|
+
const start = pagination.offset
|
|
430
|
+
const end = start + pagination.limit
|
|
431
|
+
const paged = filtered.slice(start, end)
|
|
432
|
+
printPlanList(paged)
|
|
433
|
+
logPageFooter(pagination.page, pagination.limit)
|
|
434
|
+
return []
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function handlePlanShow(app: PlanpilotApp, args: string[]): number[] {
|
|
438
|
+
const id = parseIdArg(args, "plan show")
|
|
439
|
+
const detail = app.getPlanDetail(id)
|
|
440
|
+
log(formatPlanDetail(detail.plan, detail.steps, detail.goals))
|
|
441
|
+
return []
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function handlePlanExport(app: PlanpilotApp, args: string[]): number[] {
|
|
445
|
+
if (args.length < 2) {
|
|
446
|
+
throw invalidInput("plan export requires <id> <path>")
|
|
447
|
+
}
|
|
448
|
+
const id = parseNumber(args[0], "plan id")
|
|
449
|
+
const filePath = args[1]
|
|
450
|
+
const detail = app.getPlanDetail(id)
|
|
451
|
+
const active = app.getActivePlan()
|
|
452
|
+
const isActive = active?.plan_id === detail.plan.id
|
|
453
|
+
const activatedAt = isActive ? active?.updated_at ?? null : null
|
|
454
|
+
ensureParentDir(filePath)
|
|
455
|
+
const markdown = formatPlanMarkdown(isActive, activatedAt ?? null, detail.plan, detail.steps, detail.goals)
|
|
456
|
+
fs.writeFileSync(filePath, markdown, "utf8")
|
|
457
|
+
log(`Exported plan ID: ${detail.plan.id} to ${filePath}`)
|
|
458
|
+
return []
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function handlePlanComment(app: PlanpilotApp, args: string[]): number[] {
|
|
462
|
+
const entries = parseCommentPairs("plan", args)
|
|
463
|
+
const planIds = app.commentPlans(entries)
|
|
464
|
+
if (planIds.length === 1) {
|
|
465
|
+
log(`Updated plan comment for plan ID: ${planIds[0]}.`)
|
|
466
|
+
} else {
|
|
467
|
+
log(`Updated plan comments for ${planIds.length} plans.`)
|
|
468
|
+
}
|
|
469
|
+
return planIds
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function handlePlanUpdate(app: PlanpilotApp, args: string[]): number[] {
|
|
473
|
+
if (!args.length) {
|
|
474
|
+
throw invalidInput("plan update requires <id>")
|
|
475
|
+
}
|
|
476
|
+
const id = parseNumber(args[0], "plan id")
|
|
477
|
+
const { options } = parseOptions(args.slice(1))
|
|
478
|
+
if (options.content !== undefined) {
|
|
479
|
+
ensureNonEmpty("plan content", options.content)
|
|
480
|
+
}
|
|
481
|
+
const changes = {
|
|
482
|
+
title: options.title,
|
|
483
|
+
content: options.content,
|
|
484
|
+
status: options.status ? parsePlanStatus(options.status) : undefined,
|
|
485
|
+
comment: options.comment,
|
|
486
|
+
}
|
|
487
|
+
const result = app.updatePlanWithActiveClear(id, changes)
|
|
488
|
+
log(`Updated plan ID: ${result.plan.id}: ${result.plan.title}`)
|
|
489
|
+
if (result.cleared) {
|
|
490
|
+
log("Active plan deactivated because plan is done.")
|
|
491
|
+
}
|
|
492
|
+
if (result.plan.status === "done") {
|
|
493
|
+
notifyPlanCompleted(result.plan.id)
|
|
494
|
+
}
|
|
495
|
+
return [result.plan.id]
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function handlePlanDone(app: PlanpilotApp, args: string[]): number[] {
|
|
499
|
+
const id = parseIdArg(args, "plan done")
|
|
500
|
+
const result = app.updatePlanWithActiveClear(id, { status: "done" })
|
|
501
|
+
log(`Plan ID: ${result.plan.id} marked done.`)
|
|
502
|
+
if (result.cleared) {
|
|
503
|
+
log("Active plan deactivated because plan is done.")
|
|
504
|
+
}
|
|
505
|
+
if (result.plan.status === "done") {
|
|
506
|
+
notifyPlanCompleted(result.plan.id)
|
|
507
|
+
}
|
|
508
|
+
return [result.plan.id]
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function handlePlanRemove(app: PlanpilotApp, args: string[]): number[] {
|
|
512
|
+
const id = parseIdArg(args, "plan remove")
|
|
513
|
+
app.deletePlan(id)
|
|
514
|
+
log(`Plan ID: ${id} removed.`)
|
|
515
|
+
return []
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function handlePlanActivate(app: PlanpilotApp, args: string[]): number[] {
|
|
519
|
+
if (!args.length) {
|
|
520
|
+
throw invalidInput("plan activate requires <id>")
|
|
521
|
+
}
|
|
522
|
+
const id = parseNumber(args[0], "plan id")
|
|
523
|
+
const force = args.slice(1).includes("--force")
|
|
524
|
+
const plan = app.getPlan(id)
|
|
525
|
+
if (plan.status === "done") {
|
|
526
|
+
throw invalidInput("cannot activate plan; plan is done")
|
|
527
|
+
}
|
|
528
|
+
app.setActivePlan(id, force)
|
|
529
|
+
log(`Active plan set to ${plan.id}: ${plan.title}`)
|
|
530
|
+
return [plan.id]
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function handlePlanActive(app: PlanpilotApp): number[] {
|
|
534
|
+
const active = app.getActivePlan()
|
|
535
|
+
if (!active) {
|
|
536
|
+
log("No active plan.")
|
|
537
|
+
return []
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const detail = app.getPlanDetail(active.plan_id)
|
|
541
|
+
log(formatPlanDetail(detail.plan, detail.steps, detail.goals))
|
|
542
|
+
return []
|
|
543
|
+
} catch (err) {
|
|
544
|
+
if (err instanceof AppError && err.kind === "NotFound") {
|
|
545
|
+
app.clearActivePlan()
|
|
546
|
+
log(`Active plan ID: ${active.plan_id} not found.`)
|
|
547
|
+
return []
|
|
548
|
+
}
|
|
549
|
+
throw err
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function handlePlanDeactivate(app: PlanpilotApp): number[] {
|
|
554
|
+
const active = app.getActivePlan()
|
|
555
|
+
app.clearActivePlan()
|
|
556
|
+
log("Active plan deactivated.")
|
|
557
|
+
return active ? [active.plan_id] : []
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function handleStepAdd(app: PlanpilotApp, args: string[]): number[] {
|
|
561
|
+
if (args.length < 2) {
|
|
562
|
+
throw invalidInput("step add requires <plan_id> <content...>")
|
|
563
|
+
}
|
|
564
|
+
const planId = parseNumber(args[0], "plan id")
|
|
565
|
+
const parsed = parseStepAddArgs(args.slice(1))
|
|
566
|
+
if (!parsed.contents.length) {
|
|
567
|
+
throw invalidInput("no contents provided")
|
|
568
|
+
}
|
|
569
|
+
if (parsed.at !== undefined && parsed.at === 0) {
|
|
570
|
+
throw invalidInput("position starts at 1")
|
|
571
|
+
}
|
|
572
|
+
parsed.contents.forEach((content) => ensureNonEmpty("step content", content))
|
|
573
|
+
|
|
574
|
+
const result = app.addStepsBatch(planId, parsed.contents, "todo", parsed.executor ?? "ai", parsed.at)
|
|
575
|
+
if (result.steps.length === 1) {
|
|
576
|
+
log(`Created step ID: ${result.steps[0].id} for plan ID: ${result.steps[0].plan_id}`)
|
|
577
|
+
} else {
|
|
578
|
+
log(`Created ${result.steps.length} steps for plan ID: ${planId}`)
|
|
579
|
+
}
|
|
580
|
+
printStatusChanges(result.changes)
|
|
581
|
+
return [planId]
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function handleStepAddTree(app: PlanpilotApp, args: string[]): number[] {
|
|
585
|
+
if (args.length < 2) {
|
|
586
|
+
throw invalidInput("step add-tree requires <plan_id> <content>")
|
|
587
|
+
}
|
|
588
|
+
const planId = parseNumber(args[0], "plan id")
|
|
589
|
+
const content = args[1]
|
|
590
|
+
const parsed = parseStepAddTreeArgs(args.slice(2))
|
|
591
|
+
ensureNonEmpty("step content", content)
|
|
592
|
+
parsed.goals.forEach((goal) => ensureNonEmpty("goal content", goal))
|
|
593
|
+
const executor = parsed.executor ?? "ai"
|
|
594
|
+
|
|
595
|
+
const result = app.addStepTree(planId, content, executor, parsed.goals)
|
|
596
|
+
log(`Created step ID: ${result.step.id} for plan ID: ${result.step.plan_id} (goals: ${result.goals.length})`)
|
|
597
|
+
printStatusChanges(result.changes)
|
|
598
|
+
notifyAfterStepChanges(app, result.changes)
|
|
599
|
+
notifyPlansCompleted(app, result.changes)
|
|
600
|
+
return [planId]
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function handleStepList(app: PlanpilotApp, args: string[]): number[] {
|
|
604
|
+
if (!args.length) {
|
|
605
|
+
throw invalidInput("step list requires <plan_id>")
|
|
606
|
+
}
|
|
607
|
+
const planId = parseNumber(args[0], "plan id")
|
|
608
|
+
const { options } = parseOptions(args.slice(1))
|
|
609
|
+
const allowed = new Set(["status", "executor", "limit", "page"])
|
|
610
|
+
for (const key of Object.keys(options)) {
|
|
611
|
+
if (key === "search" && Array.isArray(options.search) && options.search.length === 0) continue
|
|
612
|
+
if (!allowed.has(key)) {
|
|
613
|
+
throw invalidInput(`step list does not support --${key}`)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const status = parseStepStatusFilter(options.status)
|
|
617
|
+
const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE })
|
|
618
|
+
const countQuery = {
|
|
619
|
+
status,
|
|
620
|
+
executor: options.executor ? parseStepExecutor(options.executor) : undefined,
|
|
621
|
+
limit: undefined,
|
|
622
|
+
offset: undefined,
|
|
623
|
+
order: undefined,
|
|
624
|
+
desc: undefined,
|
|
625
|
+
}
|
|
626
|
+
const total = app.countSteps(planId, countQuery)
|
|
627
|
+
if (total === 0) {
|
|
628
|
+
log(`No steps found for plan ID: ${planId}.`)
|
|
629
|
+
return []
|
|
630
|
+
}
|
|
631
|
+
const totalPages = Math.ceil(total / pagination.limit)
|
|
632
|
+
if (pagination.page > totalPages) {
|
|
633
|
+
log(`Page ${pagination.page} exceeds total pages ${totalPages} for plan ID: ${planId}.`)
|
|
634
|
+
return []
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const query = {
|
|
638
|
+
status,
|
|
639
|
+
executor: options.executor ? parseStepExecutor(options.executor) : undefined,
|
|
640
|
+
limit: pagination.limit,
|
|
641
|
+
offset: pagination.offset,
|
|
642
|
+
order: "order" as const,
|
|
643
|
+
desc: false,
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const steps = app.listStepsFiltered(planId, query)
|
|
647
|
+
const details = app.getStepsDetail(steps)
|
|
648
|
+
printStepList(details)
|
|
649
|
+
logPageFooter(pagination.page, pagination.limit)
|
|
650
|
+
return []
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function handleStepCount(app: PlanpilotApp, args: string[]): number[] {
|
|
654
|
+
if (!args.length) {
|
|
655
|
+
throw invalidInput("step count requires <plan_id>")
|
|
656
|
+
}
|
|
657
|
+
const planId = parseNumber(args[0], "plan id")
|
|
658
|
+
const { options } = parseOptions(args.slice(1))
|
|
659
|
+
const allowed = new Set(["status", "executor"])
|
|
660
|
+
for (const key of Object.keys(options)) {
|
|
661
|
+
if (key === "search" && Array.isArray(options.search) && options.search.length === 0) continue
|
|
662
|
+
if (!allowed.has(key)) {
|
|
663
|
+
throw invalidInput(`step count does not support --${key}`)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const status = parseStepStatusFilter(options.status)
|
|
667
|
+
const query = {
|
|
668
|
+
status,
|
|
669
|
+
executor: options.executor ? parseStepExecutor(options.executor) : undefined,
|
|
670
|
+
limit: undefined,
|
|
671
|
+
offset: undefined,
|
|
672
|
+
order: undefined,
|
|
673
|
+
desc: undefined,
|
|
674
|
+
}
|
|
675
|
+
const total = app.countSteps(planId, query)
|
|
676
|
+
log(`Total: ${total}`)
|
|
677
|
+
return []
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function handleStepShow(app: PlanpilotApp, args: string[]): number[] {
|
|
681
|
+
const id = parseIdArg(args, "step show")
|
|
682
|
+
const detail = app.getStepDetail(id)
|
|
683
|
+
log(formatStepDetail(detail.step, detail.goals))
|
|
684
|
+
return []
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function handleStepShowNext(app: PlanpilotApp): number[] {
|
|
688
|
+
const active = app.getActivePlan()
|
|
689
|
+
if (!active) {
|
|
690
|
+
log("No active plan.")
|
|
691
|
+
return []
|
|
692
|
+
}
|
|
693
|
+
const next = app.nextStep(active.plan_id)
|
|
694
|
+
if (!next) {
|
|
695
|
+
log("No pending step.")
|
|
696
|
+
return []
|
|
697
|
+
}
|
|
698
|
+
const goals = app.goalsForStep(next.id)
|
|
699
|
+
log(formatStepDetail(next, goals))
|
|
700
|
+
return []
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function handleStepUpdate(app: PlanpilotApp, args: string[]): number[] {
|
|
704
|
+
if (!args.length) {
|
|
705
|
+
throw invalidInput("step update requires <id>")
|
|
706
|
+
}
|
|
707
|
+
const id = parseNumber(args[0], "step id")
|
|
708
|
+
const { options } = parseOptions(args.slice(1))
|
|
709
|
+
if (options.content !== undefined) {
|
|
710
|
+
ensureNonEmpty("step content", options.content)
|
|
711
|
+
}
|
|
712
|
+
const status = options.status ? parseStepStatus(options.status) : undefined
|
|
713
|
+
const result = app.updateStep(id, {
|
|
714
|
+
content: options.content,
|
|
715
|
+
status,
|
|
716
|
+
executor: options.executor ? parseStepExecutor(options.executor) : undefined,
|
|
717
|
+
comment: options.comment,
|
|
718
|
+
})
|
|
719
|
+
log(`Updated step ID: ${result.step.id}.`)
|
|
720
|
+
printStatusChanges(result.changes)
|
|
721
|
+
if (status === "done" && result.step.status === "done") {
|
|
722
|
+
notifyNextStepForPlan(app, result.step.plan_id)
|
|
723
|
+
}
|
|
724
|
+
notifyPlansCompleted(app, result.changes)
|
|
725
|
+
return [result.step.plan_id]
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function handleStepComment(app: PlanpilotApp, args: string[]): number[] {
|
|
729
|
+
const entries = parseCommentPairs("step", args)
|
|
730
|
+
const planIds = app.commentSteps(entries)
|
|
731
|
+
if (planIds.length === 1) {
|
|
732
|
+
log(`Updated step comments for plan ID: ${planIds[0]}.`)
|
|
733
|
+
} else {
|
|
734
|
+
log(`Updated step comments for ${planIds.length} plans.`)
|
|
735
|
+
}
|
|
736
|
+
return planIds
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function handleStepDone(app: PlanpilotApp, args: string[]): number[] {
|
|
740
|
+
if (!args.length) {
|
|
741
|
+
throw invalidInput("step done requires <id>")
|
|
742
|
+
}
|
|
743
|
+
const id = parseNumber(args[0], "step id")
|
|
744
|
+
const allGoals = args.slice(1).includes("--all-goals")
|
|
745
|
+
const result = app.setStepDoneWithGoals(id, allGoals)
|
|
746
|
+
log(`Step ID: ${result.step.id} marked done.`)
|
|
747
|
+
printStatusChanges(result.changes)
|
|
748
|
+
notifyNextStepForPlan(app, result.step.plan_id)
|
|
749
|
+
notifyPlansCompleted(app, result.changes)
|
|
750
|
+
return [result.step.plan_id]
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function handleStepMove(app: PlanpilotApp, args: string[]): number[] {
|
|
754
|
+
if (!args.length) {
|
|
755
|
+
throw invalidInput("step move requires <id> --to <pos>")
|
|
756
|
+
}
|
|
757
|
+
const id = parseNumber(args[0], "step id")
|
|
758
|
+
const toIndex = args.indexOf("--to")
|
|
759
|
+
if (toIndex === -1 || toIndex === args.length - 1) {
|
|
760
|
+
throw invalidInput("step move requires --to <pos>")
|
|
761
|
+
}
|
|
762
|
+
const to = parseNumber(args[toIndex + 1], "position")
|
|
763
|
+
if (to === 0) {
|
|
764
|
+
throw invalidInput("position starts at 1")
|
|
765
|
+
}
|
|
766
|
+
const steps = app.moveStep(id, to)
|
|
767
|
+
log(`Reordered steps for plan ID: ${steps[0].plan_id}:`)
|
|
768
|
+
const details = app.getStepsDetail(steps)
|
|
769
|
+
printStepList(details)
|
|
770
|
+
return [steps[0].plan_id]
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function handleStepRemove(app: PlanpilotApp, args: string[]): number[] {
|
|
774
|
+
if (!args.length) {
|
|
775
|
+
throw invalidInput("no step ids provided")
|
|
776
|
+
}
|
|
777
|
+
const ids = args.map((arg) => parseNumber(arg, "step id"))
|
|
778
|
+
const planIds = app.planIdsForSteps(ids)
|
|
779
|
+
const result = app.deleteSteps(ids)
|
|
780
|
+
if (ids.length === 1) {
|
|
781
|
+
log(`Step ID: ${ids[0]} removed.`)
|
|
782
|
+
} else {
|
|
783
|
+
log(`Removed ${result.deleted} steps.`)
|
|
784
|
+
}
|
|
785
|
+
printStatusChanges(result.changes)
|
|
786
|
+
return planIds
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function handleGoalAdd(app: PlanpilotApp, args: string[]): number[] {
|
|
790
|
+
if (args.length < 2) {
|
|
791
|
+
throw invalidInput("goal add requires <step_id> <content...>")
|
|
792
|
+
}
|
|
793
|
+
const stepId = parseNumber(args[0], "step id")
|
|
794
|
+
const contents = args.slice(1)
|
|
795
|
+
if (!contents.length) {
|
|
796
|
+
throw invalidInput("no contents provided")
|
|
797
|
+
}
|
|
798
|
+
contents.forEach((content) => ensureNonEmpty("goal content", content))
|
|
799
|
+
const result = app.addGoalsBatch(stepId, contents, "todo")
|
|
800
|
+
if (result.goals.length === 1) {
|
|
801
|
+
log(`Created goal ID: ${result.goals[0].id} for step ID: ${result.goals[0].step_id}`)
|
|
802
|
+
} else {
|
|
803
|
+
log(`Created ${result.goals.length} goals for step ID: ${stepId}`)
|
|
804
|
+
}
|
|
805
|
+
printStatusChanges(result.changes)
|
|
806
|
+
notifyAfterStepChanges(app, result.changes)
|
|
807
|
+
notifyPlansCompleted(app, result.changes)
|
|
808
|
+
const step = app.getStep(stepId)
|
|
809
|
+
return [step.plan_id]
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function handleGoalList(app: PlanpilotApp, args: string[]): number[] {
|
|
813
|
+
if (!args.length) {
|
|
814
|
+
throw invalidInput("goal list requires <step_id>")
|
|
815
|
+
}
|
|
816
|
+
const stepId = parseNumber(args[0], "step id")
|
|
817
|
+
const { options } = parseOptions(args.slice(1))
|
|
818
|
+
const status = parseGoalStatusFilter(options.status)
|
|
819
|
+
const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE })
|
|
820
|
+
const countQuery = {
|
|
821
|
+
status,
|
|
822
|
+
limit: undefined,
|
|
823
|
+
offset: undefined,
|
|
824
|
+
}
|
|
825
|
+
const total = app.countGoals(stepId, countQuery)
|
|
826
|
+
if (total === 0) {
|
|
827
|
+
log(`No goals found for step ID: ${stepId}.`)
|
|
828
|
+
return []
|
|
829
|
+
}
|
|
830
|
+
const totalPages = Math.ceil(total / pagination.limit)
|
|
831
|
+
if (pagination.page > totalPages) {
|
|
832
|
+
log(`Page ${pagination.page} exceeds total pages ${totalPages} for step ID: ${stepId}.`)
|
|
833
|
+
return []
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const query = {
|
|
837
|
+
status,
|
|
838
|
+
limit: pagination.limit,
|
|
839
|
+
offset: pagination.offset,
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const goals = app.listGoalsFiltered(stepId, query)
|
|
843
|
+
printGoalList(goals)
|
|
844
|
+
logPageFooter(pagination.page, pagination.limit)
|
|
845
|
+
return []
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function handleGoalCount(app: PlanpilotApp, args: string[]): number[] {
|
|
849
|
+
if (!args.length) {
|
|
850
|
+
throw invalidInput("goal count requires <step_id>")
|
|
851
|
+
}
|
|
852
|
+
const stepId = parseNumber(args[0], "step id")
|
|
853
|
+
const { options } = parseOptions(args.slice(1))
|
|
854
|
+
const status = parseGoalStatusFilter(options.status)
|
|
855
|
+
const query = {
|
|
856
|
+
status,
|
|
857
|
+
limit: undefined,
|
|
858
|
+
offset: undefined,
|
|
859
|
+
}
|
|
860
|
+
const total = app.countGoals(stepId, query)
|
|
861
|
+
log(`Total: ${total}`)
|
|
862
|
+
return []
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function handleGoalShow(app: PlanpilotApp, args: string[]): number[] {
|
|
866
|
+
const id = parseIdArg(args, "goal show")
|
|
867
|
+
const detail = app.getGoalDetail(id)
|
|
868
|
+
log(formatGoalDetail(detail.goal, detail.step))
|
|
869
|
+
return []
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function handleGoalComment(app: PlanpilotApp, args: string[]): number[] {
|
|
873
|
+
const entries = parseCommentPairs("goal", args)
|
|
874
|
+
const planIds = app.commentGoals(entries)
|
|
875
|
+
if (planIds.length === 1) {
|
|
876
|
+
log(`Updated goal comments for plan ID: ${planIds[0]}.`)
|
|
877
|
+
} else {
|
|
878
|
+
log(`Updated goal comments for ${planIds.length} plans.`)
|
|
879
|
+
}
|
|
880
|
+
return planIds
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function handleGoalUpdate(app: PlanpilotApp, args: string[]): number[] {
|
|
884
|
+
if (!args.length) {
|
|
885
|
+
throw invalidInput("goal update requires <id>")
|
|
886
|
+
}
|
|
887
|
+
const id = parseNumber(args[0], "goal id")
|
|
888
|
+
const { options } = parseOptions(args.slice(1))
|
|
889
|
+
if (options.content !== undefined) {
|
|
890
|
+
ensureNonEmpty("goal content", options.content)
|
|
891
|
+
}
|
|
892
|
+
const result = app.updateGoal(id, {
|
|
893
|
+
content: options.content,
|
|
894
|
+
status: options.status ? parseGoalStatus(options.status) : undefined,
|
|
895
|
+
comment: options.comment,
|
|
896
|
+
})
|
|
897
|
+
log(`Updated goal ${result.goal.id}.`)
|
|
898
|
+
printStatusChanges(result.changes)
|
|
899
|
+
notifyAfterStepChanges(app, result.changes)
|
|
900
|
+
notifyPlansCompleted(app, result.changes)
|
|
901
|
+
const step = app.getStep(result.goal.step_id)
|
|
902
|
+
return [step.plan_id]
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function handleGoalDone(app: PlanpilotApp, args: string[]): number[] {
|
|
906
|
+
if (!args.length) {
|
|
907
|
+
throw invalidInput("goal done requires <id>")
|
|
908
|
+
}
|
|
909
|
+
const ids = args.map((arg) => parseNumber(arg, "goal id"))
|
|
910
|
+
if (ids.length === 1) {
|
|
911
|
+
const result = app.setGoalStatus(ids[0], "done")
|
|
912
|
+
log(`Goal ID: ${result.goal.id} marked done.`)
|
|
913
|
+
printStatusChanges(result.changes)
|
|
914
|
+
notifyAfterStepChanges(app, result.changes)
|
|
915
|
+
notifyPlansCompleted(app, result.changes)
|
|
916
|
+
const step = app.getStep(result.goal.step_id)
|
|
917
|
+
return [step.plan_id]
|
|
918
|
+
}
|
|
919
|
+
const planIds = app.planIdsForGoals(ids)
|
|
920
|
+
const result = app.setGoalsStatus(ids, "done")
|
|
921
|
+
log(`Goals marked done: ${result.updated}.`)
|
|
922
|
+
printStatusChanges(result.changes)
|
|
923
|
+
notifyAfterStepChanges(app, result.changes)
|
|
924
|
+
notifyPlansCompleted(app, result.changes)
|
|
925
|
+
return planIds
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function handleGoalRemove(app: PlanpilotApp, args: string[]): number[] {
|
|
929
|
+
if (!args.length) {
|
|
930
|
+
throw invalidInput("no goal ids provided")
|
|
931
|
+
}
|
|
932
|
+
const ids = args.map((arg) => parseNumber(arg, "goal id"))
|
|
933
|
+
const planIds = app.planIdsForGoals(ids)
|
|
934
|
+
const result = app.deleteGoals(ids)
|
|
935
|
+
if (ids.length === 1) {
|
|
936
|
+
log(`Goal ID: ${ids[0]} removed.`)
|
|
937
|
+
} else {
|
|
938
|
+
log(`Removed ${result.deleted} goals.`)
|
|
939
|
+
}
|
|
940
|
+
printStatusChanges(result.changes)
|
|
941
|
+
notifyAfterStepChanges(app, result.changes)
|
|
942
|
+
notifyPlansCompleted(app, result.changes)
|
|
943
|
+
return planIds
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function parseIdArg(args: string[], label: string): number {
|
|
947
|
+
if (!args.length) {
|
|
948
|
+
throw invalidInput(`${label} requires <id>`)
|
|
949
|
+
}
|
|
950
|
+
return parseNumber(args[0], "id")
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function parseNumber(value: string, label: string): number {
|
|
954
|
+
const num = Number(value)
|
|
955
|
+
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
956
|
+
throw invalidInput(`${label} '${value}' is invalid`)
|
|
957
|
+
}
|
|
958
|
+
return num
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function parseOptions(args: string[]) {
|
|
962
|
+
const options: Record<string, any> = { search: [] }
|
|
963
|
+
const positionals: string[] = []
|
|
964
|
+
let i = 0
|
|
965
|
+
while (i < args.length) {
|
|
966
|
+
const token = args[i]
|
|
967
|
+
if (!token.startsWith("--")) {
|
|
968
|
+
positionals.push(token)
|
|
969
|
+
i += 1
|
|
970
|
+
continue
|
|
971
|
+
}
|
|
972
|
+
switch (token) {
|
|
973
|
+
case "--scope":
|
|
974
|
+
options.scope = expectValue(args, i, token)
|
|
975
|
+
i += 2
|
|
976
|
+
break
|
|
977
|
+
case "--match-case":
|
|
978
|
+
options.matchCase = true
|
|
979
|
+
i += 1
|
|
980
|
+
break
|
|
981
|
+
case "--desc":
|
|
982
|
+
options.desc = true
|
|
983
|
+
i += 1
|
|
984
|
+
break
|
|
985
|
+
case "--all-goals":
|
|
986
|
+
options.allGoals = true
|
|
987
|
+
i += 1
|
|
988
|
+
break
|
|
989
|
+
case "--force":
|
|
990
|
+
options.force = true
|
|
991
|
+
i += 1
|
|
992
|
+
break
|
|
993
|
+
case "--search":
|
|
994
|
+
options.search.push(expectValue(args, i, token))
|
|
995
|
+
i += 2
|
|
996
|
+
break
|
|
997
|
+
case "--search-mode":
|
|
998
|
+
options.searchMode = expectValue(args, i, token)
|
|
999
|
+
i += 2
|
|
1000
|
+
break
|
|
1001
|
+
case "--search-field":
|
|
1002
|
+
options.searchField = expectValue(args, i, token)
|
|
1003
|
+
i += 2
|
|
1004
|
+
break
|
|
1005
|
+
case "--title":
|
|
1006
|
+
options.title = expectValue(args, i, token)
|
|
1007
|
+
i += 2
|
|
1008
|
+
break
|
|
1009
|
+
case "--content":
|
|
1010
|
+
options.content = expectValue(args, i, token)
|
|
1011
|
+
i += 2
|
|
1012
|
+
break
|
|
1013
|
+
case "--status":
|
|
1014
|
+
options.status = expectValue(args, i, token)
|
|
1015
|
+
i += 2
|
|
1016
|
+
break
|
|
1017
|
+
case "--comment":
|
|
1018
|
+
options.comment = expectValue(args, i, token)
|
|
1019
|
+
i += 2
|
|
1020
|
+
break
|
|
1021
|
+
case "--executor":
|
|
1022
|
+
options.executor = expectValue(args, i, token)
|
|
1023
|
+
i += 2
|
|
1024
|
+
break
|
|
1025
|
+
case "--limit":
|
|
1026
|
+
options.limit = expectValue(args, i, token)
|
|
1027
|
+
i += 2
|
|
1028
|
+
break
|
|
1029
|
+
case "--page":
|
|
1030
|
+
options.page = expectValue(args, i, token)
|
|
1031
|
+
i += 2
|
|
1032
|
+
break
|
|
1033
|
+
case "--order":
|
|
1034
|
+
options.order = expectValue(args, i, token)
|
|
1035
|
+
i += 2
|
|
1036
|
+
break
|
|
1037
|
+
case "--to":
|
|
1038
|
+
options.to = expectValue(args, i, token)
|
|
1039
|
+
i += 2
|
|
1040
|
+
break
|
|
1041
|
+
case "--goal":
|
|
1042
|
+
if (!options.goals) options.goals = []
|
|
1043
|
+
options.goals.push(expectValue(args, i, token))
|
|
1044
|
+
i += 2
|
|
1045
|
+
break
|
|
1046
|
+
default:
|
|
1047
|
+
throw invalidInput(`unexpected argument: ${token}`)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return { options, positionals }
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function expectValue(args: string[], index: number, token: string): string {
|
|
1054
|
+
const value = args[index + 1]
|
|
1055
|
+
if (value === undefined) {
|
|
1056
|
+
throw invalidInput(`${token} requires a value`)
|
|
1057
|
+
}
|
|
1058
|
+
return value
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function parsePlanStatus(value: string): PlanStatus {
|
|
1062
|
+
const normalized = value.trim().toLowerCase()
|
|
1063
|
+
if (normalized === "todo" || normalized === "done") return normalized
|
|
1064
|
+
throw invalidInput(`invalid status '${value}', expected todo|done`)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function parsePlanStatusFilter(value?: string): PlanStatus | null {
|
|
1068
|
+
if (!value) return null
|
|
1069
|
+
const normalized = value.trim().toLowerCase()
|
|
1070
|
+
if (normalized === "all") return null
|
|
1071
|
+
return parsePlanStatus(normalized)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function parseStepStatus(value: string): StepStatus {
|
|
1075
|
+
return parsePlanStatus(value) as StepStatus
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function parseGoalStatus(value: string): GoalStatus {
|
|
1079
|
+
return parsePlanStatus(value) as GoalStatus
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function parseStepStatusFilter(value?: string): StepStatus | null {
|
|
1083
|
+
if (!value) return null
|
|
1084
|
+
const normalized = value.trim().toLowerCase()
|
|
1085
|
+
if (normalized === "all") return null
|
|
1086
|
+
return parseStepStatus(normalized)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function parseGoalStatusFilter(value?: string): GoalStatus | null {
|
|
1090
|
+
if (!value) return null
|
|
1091
|
+
const normalized = value.trim().toLowerCase()
|
|
1092
|
+
if (normalized === "all") return null
|
|
1093
|
+
return parseGoalStatus(normalized)
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function parseScope(value?: string): "project" | "all" {
|
|
1097
|
+
if (!value) return "project"
|
|
1098
|
+
const normalized = value.trim().toLowerCase()
|
|
1099
|
+
if (normalized === "project" || normalized === "all") return normalized as "project" | "all"
|
|
1100
|
+
throw invalidInput(`invalid scope '${value}', expected project|all`)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function parsePlanOrder(value: string): PlanOrder {
|
|
1104
|
+
const normalized = value.trim().toLowerCase()
|
|
1105
|
+
if (normalized === "id" || normalized === "title" || normalized === "created" || normalized === "updated") {
|
|
1106
|
+
return normalized as PlanOrder
|
|
1107
|
+
}
|
|
1108
|
+
throw invalidInput(`invalid order '${value}', expected id|title|created|updated`)
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function parseStepExecutor(value: string): StepExecutor {
|
|
1112
|
+
const normalized = value.trim().toLowerCase()
|
|
1113
|
+
if (normalized === "ai" || normalized === "human") return normalized
|
|
1114
|
+
throw invalidInput(`invalid executor '${value}', expected ai|human`)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function resolvePagination(
|
|
1118
|
+
options: Record<string, any>,
|
|
1119
|
+
defaults: { limit: number; page: number },
|
|
1120
|
+
): { limit: number; page: number; offset: number } {
|
|
1121
|
+
if (defaults.limit <= 0 || defaults.page < 1) {
|
|
1122
|
+
throw invalidInput("invalid default pagination configuration")
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const limit = options.limit !== undefined ? parseNumber(options.limit, "limit") : defaults.limit
|
|
1126
|
+
if (limit <= 0) {
|
|
1127
|
+
throw invalidInput("limit must be >= 1")
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const page = options.page !== undefined ? parseNumber(options.page, "page") : defaults.page
|
|
1131
|
+
if (page < 1) {
|
|
1132
|
+
throw invalidInput("page must be >= 1")
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return { limit, page, offset: (page - 1) * limit }
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
type PlanSearchMode = "any" | "all"
|
|
1139
|
+
type PlanSearchField = "plan" | "title" | "content" | "comment" | "steps" | "goals" | "all"
|
|
1140
|
+
|
|
1141
|
+
class PlanSearchQuery {
|
|
1142
|
+
terms: string[]
|
|
1143
|
+
mode: PlanSearchMode
|
|
1144
|
+
field: PlanSearchField
|
|
1145
|
+
matchCase: boolean
|
|
1146
|
+
|
|
1147
|
+
constructor(rawTerms: string[] = [], searchMode?: string, searchField?: string, matchCase?: boolean) {
|
|
1148
|
+
let terms = rawTerms.map((term) => term.trim()).filter((term) => term.length > 0)
|
|
1149
|
+
const caseSensitive = !!matchCase
|
|
1150
|
+
if (!caseSensitive) {
|
|
1151
|
+
terms = terms.map((term) => term.toLowerCase())
|
|
1152
|
+
}
|
|
1153
|
+
this.terms = terms
|
|
1154
|
+
this.mode = parseSearchMode(searchMode)
|
|
1155
|
+
this.field = parseSearchField(searchField)
|
|
1156
|
+
this.matchCase = caseSensitive
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
hasTerms() {
|
|
1160
|
+
return this.terms.length > 0
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function parseSearchMode(value?: string): PlanSearchMode {
|
|
1165
|
+
if (!value) return "all"
|
|
1166
|
+
const normalized = value.trim().toLowerCase()
|
|
1167
|
+
if (normalized === "any" || normalized === "all") return normalized
|
|
1168
|
+
throw invalidInput(`invalid search mode '${value}', expected any|all`)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function parseSearchField(value?: string): PlanSearchField {
|
|
1172
|
+
if (!value) return "plan"
|
|
1173
|
+
const normalized = value.trim().toLowerCase()
|
|
1174
|
+
if (["plan", "title", "content", "comment", "steps", "goals", "all"].includes(normalized)) {
|
|
1175
|
+
return normalized as PlanSearchField
|
|
1176
|
+
}
|
|
1177
|
+
throw invalidInput(
|
|
1178
|
+
`invalid search field '${value}', expected plan|title|content|comment|steps|goals|all`
|
|
1179
|
+
)
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function planMatchesSearch(detail: ReturnType<PlanpilotApp["getPlanDetail"]>, search: PlanSearchQuery): boolean {
|
|
1183
|
+
const haystacks: string[] = []
|
|
1184
|
+
const addValue = (value: string) => {
|
|
1185
|
+
haystacks.push(search.matchCase ? value : value.toLowerCase())
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const includePlan = search.field === "plan" || search.field === "all"
|
|
1189
|
+
const includeTitle = search.field === "title" || includePlan || search.field === "all"
|
|
1190
|
+
const includeContent = search.field === "content" || includePlan || search.field === "all"
|
|
1191
|
+
const includeComment = search.field === "comment" || includePlan || search.field === "all"
|
|
1192
|
+
const includeSteps = search.field === "steps" || search.field === "all"
|
|
1193
|
+
const includeGoals = search.field === "goals" || search.field === "all"
|
|
1194
|
+
|
|
1195
|
+
if (includePlan || includeTitle) addValue(detail.plan.title)
|
|
1196
|
+
if (includePlan || includeContent) addValue(detail.plan.content)
|
|
1197
|
+
if (includePlan || includeComment) {
|
|
1198
|
+
if (detail.plan.comment) addValue(detail.plan.comment)
|
|
1199
|
+
}
|
|
1200
|
+
if (includeSteps) {
|
|
1201
|
+
detail.steps.forEach((step) => addValue(step.content))
|
|
1202
|
+
}
|
|
1203
|
+
if (includeGoals) {
|
|
1204
|
+
detail.goals.forEach((goalList) => goalList.forEach((goal) => addValue(goal.content)))
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (!haystacks.length) return false
|
|
1208
|
+
if (search.mode === "any") {
|
|
1209
|
+
return search.terms.some((term) => haystacks.some((value) => value.includes(term)))
|
|
1210
|
+
}
|
|
1211
|
+
return search.terms.every((term) => haystacks.some((value) => value.includes(term)))
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function parsePlanAddTreeSteps(args: string[]) {
|
|
1215
|
+
if (!args.length) {
|
|
1216
|
+
throw invalidInput("plan add-tree requires at least one --step")
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const steps: Array<{ content: string; executor?: StepExecutor; goals?: string[] }> = []
|
|
1220
|
+
let current: { content: string; executor?: StepExecutor; goals: string[] } | null = null
|
|
1221
|
+
let i = 0
|
|
1222
|
+
while (i < args.length) {
|
|
1223
|
+
const token = args[i]
|
|
1224
|
+
if (token === "--") {
|
|
1225
|
+
i += 1
|
|
1226
|
+
continue
|
|
1227
|
+
}
|
|
1228
|
+
if (token === "--step") {
|
|
1229
|
+
const value = args[i + 1]
|
|
1230
|
+
if (value === undefined) {
|
|
1231
|
+
throw invalidInput("plan add-tree --step requires a value")
|
|
1232
|
+
}
|
|
1233
|
+
if (!value.trim()) {
|
|
1234
|
+
throw invalidInput("plan add-tree --step cannot be empty")
|
|
1235
|
+
}
|
|
1236
|
+
if (current) {
|
|
1237
|
+
steps.push({ content: current.content, executor: current.executor, goals: current.goals.length ? current.goals : undefined })
|
|
1238
|
+
}
|
|
1239
|
+
current = { content: value, goals: [] }
|
|
1240
|
+
i += 2
|
|
1241
|
+
continue
|
|
1242
|
+
}
|
|
1243
|
+
if (token === "--executor") {
|
|
1244
|
+
const value = args[i + 1]
|
|
1245
|
+
if (value === undefined) {
|
|
1246
|
+
throw invalidInput("plan add-tree --executor requires a value")
|
|
1247
|
+
}
|
|
1248
|
+
if (!current) {
|
|
1249
|
+
throw invalidInput("plan add-tree --executor must follow a --step")
|
|
1250
|
+
}
|
|
1251
|
+
current.executor = parseStepExecutor(value)
|
|
1252
|
+
i += 2
|
|
1253
|
+
continue
|
|
1254
|
+
}
|
|
1255
|
+
if (token === "--goal") {
|
|
1256
|
+
const value = args[i + 1]
|
|
1257
|
+
if (value === undefined) {
|
|
1258
|
+
throw invalidInput("plan add-tree --goal requires a value")
|
|
1259
|
+
}
|
|
1260
|
+
if (!current) {
|
|
1261
|
+
throw invalidInput("plan add-tree --goal must follow a --step")
|
|
1262
|
+
}
|
|
1263
|
+
current.goals.push(value)
|
|
1264
|
+
i += 2
|
|
1265
|
+
continue
|
|
1266
|
+
}
|
|
1267
|
+
throw invalidInput(`plan add-tree unexpected argument: ${token}`)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (current) {
|
|
1271
|
+
steps.push({ content: current.content, executor: current.executor, goals: current.goals.length ? current.goals : undefined })
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (!steps.length) {
|
|
1275
|
+
throw invalidInput("plan add-tree requires at least one --step")
|
|
1276
|
+
}
|
|
1277
|
+
return steps
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function parseCommentPairs(kind: string, pairs: string[]): Array<[number, string]> {
|
|
1281
|
+
if (!pairs.length) {
|
|
1282
|
+
throw invalidInput(`${kind} comment requires <id> <comment> pairs`)
|
|
1283
|
+
}
|
|
1284
|
+
if (pairs.length % 2 !== 0) {
|
|
1285
|
+
throw invalidInput(`${kind} comment expects <id> <comment> pairs`)
|
|
1286
|
+
}
|
|
1287
|
+
const entries: Array<[number, string]> = []
|
|
1288
|
+
for (let i = 0; i < pairs.length; i += 2) {
|
|
1289
|
+
const idValue = pairs[i]
|
|
1290
|
+
const comment = pairs[i + 1]
|
|
1291
|
+
const id = parseNumber(idValue, `${kind} comment id`)
|
|
1292
|
+
ensureNonEmpty("comment", comment)
|
|
1293
|
+
entries.push([id, comment])
|
|
1294
|
+
}
|
|
1295
|
+
return entries
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function parseStepAddArgs(args: string[]) {
|
|
1299
|
+
const contents: string[] = []
|
|
1300
|
+
let executor: StepExecutor | undefined
|
|
1301
|
+
let at: number | undefined
|
|
1302
|
+
let i = 0
|
|
1303
|
+
while (i < args.length) {
|
|
1304
|
+
const token = args[i]
|
|
1305
|
+
if (token === "--executor") {
|
|
1306
|
+
const value = expectValue(args, i, token)
|
|
1307
|
+
executor = parseStepExecutor(value)
|
|
1308
|
+
i += 2
|
|
1309
|
+
continue
|
|
1310
|
+
}
|
|
1311
|
+
if (token === "--at") {
|
|
1312
|
+
const value = expectValue(args, i, token)
|
|
1313
|
+
at = parseNumber(value, "position")
|
|
1314
|
+
i += 2
|
|
1315
|
+
continue
|
|
1316
|
+
}
|
|
1317
|
+
if (token.startsWith("--")) {
|
|
1318
|
+
throw invalidInput(`unexpected argument: ${token}`)
|
|
1319
|
+
}
|
|
1320
|
+
contents.push(token)
|
|
1321
|
+
i += 1
|
|
1322
|
+
}
|
|
1323
|
+
return { contents, executor, at }
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function parseStepAddTreeArgs(args: string[]) {
|
|
1327
|
+
let executor: StepExecutor | undefined
|
|
1328
|
+
const goals: string[] = []
|
|
1329
|
+
let i = 0
|
|
1330
|
+
while (i < args.length) {
|
|
1331
|
+
const token = args[i]
|
|
1332
|
+
if (token === "--executor") {
|
|
1333
|
+
const value = expectValue(args, i, token)
|
|
1334
|
+
executor = parseStepExecutor(value)
|
|
1335
|
+
i += 2
|
|
1336
|
+
continue
|
|
1337
|
+
}
|
|
1338
|
+
if (token === "--goal") {
|
|
1339
|
+
const value = expectValue(args, i, token)
|
|
1340
|
+
goals.push(value)
|
|
1341
|
+
i += 2
|
|
1342
|
+
continue
|
|
1343
|
+
}
|
|
1344
|
+
throw invalidInput(`unexpected argument: ${token}`)
|
|
1345
|
+
}
|
|
1346
|
+
return { executor, goals }
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function printStatusChanges(changes: ReturnType<typeof createEmptyStatusChanges>) {
|
|
1350
|
+
if (statusChangesEmpty(changes)) return
|
|
1351
|
+
log("Auto status updates:")
|
|
1352
|
+
changes.steps.forEach((change) => {
|
|
1353
|
+
log(`- Step ID: ${change.step_id} status auto-updated from ${change.from} to ${change.to} (${change.reason}).`)
|
|
1354
|
+
})
|
|
1355
|
+
changes.plans.forEach((change) => {
|
|
1356
|
+
log(`- Plan ID: ${change.plan_id} status auto-updated from ${change.from} to ${change.to} (${change.reason}).`)
|
|
1357
|
+
})
|
|
1358
|
+
changes.active_plans_cleared.forEach((change) => {
|
|
1359
|
+
log(`- Active plan deactivated for plan ID: ${change.plan_id} (${change.reason}).`)
|
|
1360
|
+
})
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function notifyAfterStepChanges(app: PlanpilotApp, changes: ReturnType<typeof createEmptyStatusChanges>) {
|
|
1364
|
+
const planIds = new Set<number>()
|
|
1365
|
+
changes.steps.forEach((change) => {
|
|
1366
|
+
if (change.to === "done") {
|
|
1367
|
+
const step = app.getStep(change.step_id)
|
|
1368
|
+
planIds.add(step.plan_id)
|
|
1369
|
+
}
|
|
1370
|
+
})
|
|
1371
|
+
planIds.forEach((planId) => notifyNextStepForPlan(app, planId))
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function notifyPlansCompleted(app: PlanpilotApp, changes: ReturnType<typeof createEmptyStatusChanges>) {
|
|
1375
|
+
const planIds = new Set<number>()
|
|
1376
|
+
changes.plans.forEach((change) => {
|
|
1377
|
+
if (change.to === "done") planIds.add(change.plan_id)
|
|
1378
|
+
})
|
|
1379
|
+
planIds.forEach((planId) => {
|
|
1380
|
+
const plan = app.getPlan(planId)
|
|
1381
|
+
if (plan.status === "done") {
|
|
1382
|
+
notifyPlanCompleted(plan.id)
|
|
1383
|
+
}
|
|
1384
|
+
})
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function notifyPlanCompleted(planId: number) {
|
|
1388
|
+
log(`Plan ID: ${planId} is complete. Summarize the completed results to the user, then end this turn.`)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function notifyNextStepForPlan(app: PlanpilotApp, planId: number) {
|
|
1392
|
+
const next = app.nextStep(planId)
|
|
1393
|
+
if (!next) return
|
|
1394
|
+
if (next.executor === "ai") {
|
|
1395
|
+
log(`Next step is assigned to ai (step ID: ${next.id}). Please end this turn so Planpilot can surface it.`)
|
|
1396
|
+
return
|
|
1397
|
+
}
|
|
1398
|
+
const goals = app.goalsForStep(next.id)
|
|
1399
|
+
log("Next step requires human action:")
|
|
1400
|
+
log(formatStepDetail(next, goals))
|
|
1401
|
+
log(
|
|
1402
|
+
"Tell the user to complete the above step and goals. Confirm each goal when done, then end this turn."
|
|
1403
|
+
)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const COL_ID = 6
|
|
1407
|
+
const COL_STAT = 6
|
|
1408
|
+
const COL_STEPS = 9
|
|
1409
|
+
const COL_ORDER = 5
|
|
1410
|
+
const COL_EXEC = 6
|
|
1411
|
+
const COL_GOALS = 9
|
|
1412
|
+
const COL_TEXT = 30
|
|
1413
|
+
|
|
1414
|
+
function printPlanList(details: ReturnType<PlanpilotApp["getPlanDetails"]>) {
|
|
1415
|
+
log(`${pad("ID", COL_ID)} ${pad("STAT", COL_STAT)} ${pad("STEPS", COL_STEPS)} ${pad("TITLE", COL_TEXT)} COMMENT`)
|
|
1416
|
+
details.forEach((detail) => {
|
|
1417
|
+
const total = detail.steps.length
|
|
1418
|
+
const done = detail.steps.filter((step) => step.status === "done").length
|
|
1419
|
+
log(
|
|
1420
|
+
`${pad(String(detail.plan.id), COL_ID)} ${pad(detail.plan.status, COL_STAT)} ${pad(`${done}/${total}`, COL_STEPS)} ${pad(detail.plan.title, COL_TEXT)} ${detail.plan.comment ?? ""}`
|
|
1421
|
+
)
|
|
1422
|
+
})
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function printStepList(details: ReturnType<PlanpilotApp["getStepsDetail"]>) {
|
|
1426
|
+
log(
|
|
1427
|
+
`${pad("ID", COL_ID)} ${pad("STAT", COL_STAT)} ${pad("ORD", COL_ORDER)} ${pad("EXEC", COL_EXEC)} ${pad("GOALS", COL_GOALS)} ${pad("CONTENT", COL_TEXT)} COMMENT`
|
|
1428
|
+
)
|
|
1429
|
+
details.forEach((detail) => {
|
|
1430
|
+
const total = detail.goals.length
|
|
1431
|
+
const done = detail.goals.filter((goal) => goal.status === "done").length
|
|
1432
|
+
log(
|
|
1433
|
+
`${pad(String(detail.step.id), COL_ID)} ${pad(detail.step.status, COL_STAT)} ${pad(String(detail.step.sort_order), COL_ORDER)} ${pad(detail.step.executor, COL_EXEC)} ${pad(`${done}/${total}`, COL_GOALS)} ${pad(detail.step.content, COL_TEXT)} ${detail.step.comment ?? ""}`
|
|
1434
|
+
)
|
|
1435
|
+
})
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function printGoalList(goals: ReturnType<PlanpilotApp["listGoalsFiltered"]>) {
|
|
1439
|
+
log(`${pad("ID", COL_ID)} ${pad("STAT", COL_STAT)} ${pad("CONTENT", COL_TEXT)} COMMENT`)
|
|
1440
|
+
goals.forEach((goal) => {
|
|
1441
|
+
log(`${pad(String(goal.id), COL_ID)} ${pad(goal.status, COL_STAT)} ${pad(goal.content, COL_TEXT)} ${goal.comment ?? ""}`)
|
|
1442
|
+
})
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function logPageFooter(page: number, limit: number) {
|
|
1446
|
+
log(`Page ${page} / Limit ${limit}`)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function pad(value: string, width: number) {
|
|
1450
|
+
if (value.length >= width) return value
|
|
1451
|
+
return value.padEnd(width, " ")
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function syncPlanMarkdown(app: PlanpilotApp, planIds: number[]) {
|
|
1455
|
+
if (!planIds.length) return
|
|
1456
|
+
const unique = Array.from(new Set(planIds))
|
|
1457
|
+
const active = app.getActivePlan()
|
|
1458
|
+
const activeId = active?.plan_id
|
|
1459
|
+
const activeUpdated = active?.updated_at ?? null
|
|
1460
|
+
|
|
1461
|
+
unique.forEach((planId) => {
|
|
1462
|
+
let detail
|
|
1463
|
+
try {
|
|
1464
|
+
detail = app.getPlanDetail(planId)
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
if (err instanceof AppError && err.kind === "NotFound") return
|
|
1467
|
+
throw err
|
|
1468
|
+
}
|
|
1469
|
+
const isActive = activeId === planId
|
|
1470
|
+
const activatedAt = isActive ? activeUpdated : null
|
|
1471
|
+
const mdPath = resolvePlanMarkdownPath(planId)
|
|
1472
|
+
ensureParentDir(mdPath)
|
|
1473
|
+
const markdown = formatPlanMarkdown(isActive, activatedAt, detail.plan, detail.steps, detail.goals)
|
|
1474
|
+
fs.writeFileSync(mdPath, markdown, "utf8")
|
|
1475
|
+
})
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (import.meta.main) {
|
|
1479
|
+
runCLI(process.argv.slice(2), defaultIO).catch((err) => {
|
|
1480
|
+
error(formatCliError(err))
|
|
1481
|
+
process.exit(1)
|
|
1482
|
+
})
|
|
1483
|
+
}
|