prjct-cli 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -6
- package/CLAUDE.md +56 -15
- package/README.md +5 -6
- package/bin/prjct +59 -42
- package/bin/prjct.ts +60 -0
- package/core/__tests__/agentic/memory-system.test.ts +18 -3
- package/core/__tests__/agentic/plan-mode.test.ts +55 -26
- package/core/__tests__/agentic/prompt-builder.test.ts +6 -6
- package/core/__tests__/utils/project-commands.test.ts +72 -0
- package/core/agentic/agent-router.ts +3 -12
- package/core/agentic/command-executor.ts +372 -3
- package/core/agentic/context-builder.ts +7 -27
- package/core/agentic/ground-truth.ts +604 -5
- package/core/agentic/index.ts +180 -0
- package/core/agentic/loop-detector.ts +418 -4
- package/core/agentic/memory-system.ts +857 -3
- package/core/agentic/plan-mode.ts +491 -4
- package/core/agentic/prompt-builder.ts +44 -65
- package/core/agentic/services.ts +13 -5
- package/core/agentic/skill-loader.ts +112 -0
- package/core/agentic/smart-context.ts +37 -122
- package/core/agentic/template-loader.ts +79 -122
- package/core/agentic/tool-registry.ts +5 -11
- package/core/agents/index.ts +1 -1
- package/core/agents/performance.ts +4 -2
- package/core/bus/bus.ts +262 -0
- package/core/bus/index.ts +3 -313
- package/core/commands/analysis.ts +5 -5
- package/core/commands/analytics.ts +11 -11
- package/core/commands/base.ts +33 -209
- package/core/commands/cleanup.ts +148 -0
- package/core/commands/command-data.ts +346 -0
- package/core/commands/commands.ts +216 -0
- package/core/commands/design.ts +83 -0
- package/core/commands/index.ts +13 -207
- package/core/commands/maintenance.ts +52 -473
- package/core/commands/planning.ts +3 -3
- package/core/commands/register.ts +104 -0
- package/core/commands/registry.ts +441 -0
- package/core/commands/setup.ts +25 -9
- package/core/commands/shipping.ts +48 -11
- package/core/commands/snapshots.ts +299 -0
- package/core/commands/workflow.ts +2 -2
- package/core/constants/index.ts +254 -4
- package/core/domain/agent-loader.ts +5 -6
- package/core/domain/task-stack.ts +555 -4
- package/core/errors.ts +127 -1
- package/core/events/events.ts +87 -0
- package/core/events/index.ts +4 -138
- package/core/index.ts +15 -23
- package/core/infrastructure/agent-detector.ts +126 -201
- package/core/infrastructure/author-detector.ts +99 -171
- package/core/infrastructure/command-installer.ts +476 -4
- package/core/infrastructure/config-manager.ts +41 -37
- package/core/infrastructure/path-manager.ts +59 -9
- package/core/infrastructure/permission-manager.ts +286 -0
- package/core/outcomes/analyzer.ts +7 -41
- package/core/outcomes/index.ts +1 -1
- package/core/outcomes/recorder.ts +1 -1
- package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
- package/core/plugin/loader.ts +5 -5
- package/core/plugin/registry.ts +2 -2
- package/core/schemas/ideas.ts +85 -54
- package/core/schemas/index.ts +14 -33
- package/core/schemas/permissions.ts +177 -0
- package/core/schemas/project.ts +39 -12
- package/core/schemas/roadmap.ts +94 -59
- package/core/schemas/schemas.ts +39 -0
- package/core/schemas/shipped.ts +87 -60
- package/core/schemas/state.ts +110 -70
- package/core/server/index.ts +21 -0
- package/core/server/routes.ts +165 -0
- package/core/server/server.ts +136 -0
- package/core/server/sse.ts +135 -0
- package/core/services/agent-service.ts +170 -0
- package/core/services/breakdown-service.ts +126 -0
- package/core/services/index.ts +21 -0
- package/core/services/memory-service.ts +108 -0
- package/core/services/project-service.ts +146 -0
- package/core/services/skill-service.ts +253 -0
- package/core/session/compaction.ts +257 -0
- package/core/session/index.ts +20 -8
- package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
- package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
- package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
- package/core/session/utils.ts +1 -1
- package/core/storage/ideas-storage.ts +10 -26
- package/core/storage/index.ts +14 -162
- package/core/storage/queue-storage.ts +13 -11
- package/core/storage/shipped-storage.ts +4 -17
- package/core/storage/state-storage.ts +35 -43
- package/core/storage/storage-manager.ts +42 -52
- package/core/storage/storage.ts +160 -0
- package/core/sync/auth-config.ts +1 -8
- package/core/sync/index.ts +17 -10
- package/core/sync/oauth-handler.ts +1 -6
- package/core/sync/sync-client.ts +6 -34
- package/core/sync/sync-manager.ts +11 -40
- package/core/types/agentic.ts +577 -0
- package/core/types/agents.ts +145 -0
- package/core/types/bus.ts +82 -0
- package/core/types/commands.ts +366 -0
- package/core/types/config.ts +66 -0
- package/core/types/core.ts +96 -0
- package/core/types/domain.ts +71 -0
- package/core/types/events.ts +42 -0
- package/core/types/fs.ts +56 -0
- package/core/types/index.ts +387 -500
- package/core/types/infrastructure.ts +196 -0
- package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
- package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
- package/core/types/plugin.ts +25 -0
- package/core/types/server.ts +54 -0
- package/core/types/services.ts +65 -0
- package/core/types/session.ts +135 -0
- package/core/types/storage.ts +148 -0
- package/core/types/sync.ts +121 -0
- package/core/types/task.ts +72 -0
- package/core/types/template.ts +24 -0
- package/core/types/utils.ts +90 -0
- package/core/utils/cache.ts +195 -0
- package/core/utils/collection-filters.ts +245 -0
- package/core/utils/date-helper.ts +1 -5
- package/core/utils/file-helper.ts +20 -10
- package/core/utils/jsonl-helper.ts +5 -8
- package/core/utils/markdown-builder.ts +277 -0
- package/core/utils/project-commands.ts +132 -0
- package/core/utils/runtime.ts +119 -0
- package/dist/bin/prjct.mjs +12568 -0
- package/package.json +13 -8
- package/scripts/build.js +106 -0
- package/scripts/postinstall.js +50 -8
- package/templates/agentic/agents/uxui.md +210 -0
- package/templates/agentic/subagent-generation.md +1 -1
- package/templates/commands/bug.md +219 -41
- package/templates/commands/feature.md +368 -80
- package/templates/commands/serve.md +118 -0
- package/templates/commands/ship.md +152 -14
- package/templates/commands/skill.md +110 -0
- package/templates/commands/sync.md +63 -4
- package/templates/commands/test.md +40 -188
- package/templates/mcp-config.json +0 -36
- package/templates/permissions/default.jsonc +60 -0
- package/templates/permissions/permissive.jsonc +49 -0
- package/templates/permissions/strict.jsonc +62 -0
- package/templates/skills/code-review.md +47 -0
- package/templates/skills/debug.md +61 -0
- package/templates/skills/refactor.md +47 -0
- package/templates/subagents/domain/devops.md +1 -1
- package/templates/subagents/domain/testing.md +6 -10
- package/templates/subagents/workflow/prjct-shipper.md +16 -7
- package/templates/tools/bash.txt +22 -0
- package/templates/tools/edit.txt +18 -0
- package/templates/tools/glob.txt +19 -0
- package/templates/tools/grep.txt +21 -0
- package/templates/tools/read.txt +14 -0
- package/templates/tools/task.txt +20 -0
- package/templates/tools/webfetch.txt +16 -0
- package/templates/tools/websearch.txt +18 -0
- package/templates/tools/write.txt +17 -0
- package/core/agentic/command-executor/command-executor.ts +0 -312
- package/core/agentic/command-executor/index.ts +0 -16
- package/core/agentic/command-executor/status-signal.ts +0 -38
- package/core/agentic/command-executor/types.ts +0 -79
- package/core/agentic/ground-truth/index.ts +0 -76
- package/core/agentic/ground-truth/types.ts +0 -33
- package/core/agentic/ground-truth/utils.ts +0 -48
- package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
- package/core/agentic/ground-truth/verifiers/done.ts +0 -75
- package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
- package/core/agentic/ground-truth/verifiers/index.ts +0 -37
- package/core/agentic/ground-truth/verifiers/init.ts +0 -52
- package/core/agentic/ground-truth/verifiers/now.ts +0 -57
- package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
- package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
- package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
- package/core/agentic/ground-truth/verifiers.ts +0 -6
- package/core/agentic/loop-detector/error-analysis.ts +0 -97
- package/core/agentic/loop-detector/hallucination.ts +0 -71
- package/core/agentic/loop-detector/index.ts +0 -41
- package/core/agentic/loop-detector/loop-detector.ts +0 -222
- package/core/agentic/loop-detector/types.ts +0 -66
- package/core/agentic/memory-system/history.ts +0 -53
- package/core/agentic/memory-system/index.ts +0 -192
- package/core/agentic/memory-system/patterns.ts +0 -156
- package/core/agentic/memory-system/semantic-memories.ts +0 -278
- package/core/agentic/memory-system/session.ts +0 -21
- package/core/agentic/plan-mode/approval.ts +0 -57
- package/core/agentic/plan-mode/constants.ts +0 -44
- package/core/agentic/plan-mode/index.ts +0 -28
- package/core/agentic/plan-mode/plan-mode.ts +0 -407
- package/core/agentic/plan-mode/types.ts +0 -193
- package/core/agents/types.ts +0 -126
- package/core/command-registry/categories.ts +0 -23
- package/core/command-registry/commands.ts +0 -15
- package/core/command-registry/core-commands.ts +0 -344
- package/core/command-registry/index.ts +0 -158
- package/core/command-registry/optional-commands.ts +0 -163
- package/core/command-registry/setup-commands.ts +0 -83
- package/core/command-registry/types.ts +0 -59
- package/core/command-registry.ts +0 -9
- package/core/commands/types.ts +0 -185
- package/core/commands.ts +0 -11
- package/core/constants/formats.ts +0 -187
- package/core/context-sync.ts +0 -18
- package/core/data/index.ts +0 -27
- package/core/data/md-base-manager.ts +0 -203
- package/core/data/md-ideas-manager.ts +0 -155
- package/core/data/md-queue-manager.ts +0 -180
- package/core/data/md-shipped-manager.ts +0 -90
- package/core/data/md-state-manager.ts +0 -137
- package/core/domain/task-stack/index.ts +0 -19
- package/core/domain/task-stack/parser.ts +0 -86
- package/core/domain/task-stack/storage.ts +0 -123
- package/core/domain/task-stack/task-stack.ts +0 -340
- package/core/domain/task-stack/types.ts +0 -51
- package/core/infrastructure/command-installer/command-installer.ts +0 -327
- package/core/infrastructure/command-installer/global-config.ts +0 -136
- package/core/infrastructure/command-installer/index.ts +0 -25
- package/core/infrastructure/command-installer/types.ts +0 -41
- package/core/infrastructure/session-manager/index.ts +0 -23
- package/core/infrastructure/session-manager/types.ts +0 -45
- package/core/infrastructure/session-manager.ts +0 -8
- package/core/serializers/ideas-serializer.ts +0 -187
- package/core/serializers/index.ts +0 -36
- package/core/serializers/queue-serializer.ts +0 -210
- package/core/serializers/shipped-serializer.ts +0 -108
- package/core/serializers/state-serializer.ts +0 -136
- package/core/session/types.ts +0 -29
- /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Filter Utilities
|
|
3
|
+
*
|
|
4
|
+
* Reusable filtering and sorting functions for storage collections.
|
|
5
|
+
* Eliminates duplicated filter logic across storage classes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Priority, TaskSection } from '../schemas/state'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Priority order mapping for sorting (highest priority first)
|
|
12
|
+
*/
|
|
13
|
+
export const PRIORITY_ORDER: Record<Priority, number> = {
|
|
14
|
+
critical: 0,
|
|
15
|
+
high: 1,
|
|
16
|
+
medium: 2,
|
|
17
|
+
low: 3,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Section order mapping for sorting
|
|
22
|
+
*/
|
|
23
|
+
export const SECTION_ORDER: Record<TaskSection, number> = {
|
|
24
|
+
active: 0,
|
|
25
|
+
previously_active: 1,
|
|
26
|
+
backlog: 2,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Filter items by a specific field value
|
|
31
|
+
*/
|
|
32
|
+
export function filterByField<T, K extends keyof T>(
|
|
33
|
+
items: T[],
|
|
34
|
+
field: K,
|
|
35
|
+
value: T[K]
|
|
36
|
+
): T[] {
|
|
37
|
+
return items.filter((item) => item[field] === value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Filter items by multiple field values (OR logic)
|
|
42
|
+
*/
|
|
43
|
+
export function filterByFieldIn<T, K extends keyof T>(
|
|
44
|
+
items: T[],
|
|
45
|
+
field: K,
|
|
46
|
+
values: T[K][]
|
|
47
|
+
): T[] {
|
|
48
|
+
return items.filter((item) => values.includes(item[field]))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Filter items excluding a specific field value
|
|
53
|
+
*/
|
|
54
|
+
export function filterByFieldNot<T, K extends keyof T>(
|
|
55
|
+
items: T[],
|
|
56
|
+
field: K,
|
|
57
|
+
value: T[K]
|
|
58
|
+
): T[] {
|
|
59
|
+
return items.filter((item) => item[field] !== value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Combined filter: field equals value AND another field is falsy
|
|
64
|
+
* Useful for filtering active items that aren't completed
|
|
65
|
+
*/
|
|
66
|
+
export function filterActiveByField<T, K extends keyof T>(
|
|
67
|
+
items: T[],
|
|
68
|
+
field: K,
|
|
69
|
+
value: T[K],
|
|
70
|
+
activeField: keyof T
|
|
71
|
+
): T[] {
|
|
72
|
+
return items.filter((item) => item[field] === value && !item[activeField])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Filter items where a field is truthy
|
|
77
|
+
*/
|
|
78
|
+
export function filterByTruthy<T, K extends keyof T>(items: T[], field: K): T[] {
|
|
79
|
+
return items.filter((item) => Boolean(item[field]))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Filter items where a field is falsy
|
|
84
|
+
*/
|
|
85
|
+
export function filterByFalsy<T, K extends keyof T>(items: T[], field: K): T[] {
|
|
86
|
+
return items.filter((item) => !item[field])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sort items by priority (highest first)
|
|
91
|
+
*/
|
|
92
|
+
export function sortByPriority<T extends { priority: Priority }>(items: T[]): T[] {
|
|
93
|
+
return [...items].sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sort items by section, then priority
|
|
98
|
+
*/
|
|
99
|
+
export function sortBySectionAndPriority<T extends { section: TaskSection; priority: Priority }>(
|
|
100
|
+
items: T[]
|
|
101
|
+
): T[] {
|
|
102
|
+
return [...items].sort((a, b) => {
|
|
103
|
+
const sectionDiff = SECTION_ORDER[a.section] - SECTION_ORDER[b.section]
|
|
104
|
+
if (sectionDiff !== 0) return sectionDiff
|
|
105
|
+
return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sort items by date field
|
|
111
|
+
*/
|
|
112
|
+
export function sortByDate<T>(
|
|
113
|
+
items: T[],
|
|
114
|
+
dateField: keyof T,
|
|
115
|
+
direction: 'asc' | 'desc' = 'desc'
|
|
116
|
+
): T[] {
|
|
117
|
+
return [...items].sort((a, b) => {
|
|
118
|
+
const dateA = new Date(a[dateField] as string).getTime()
|
|
119
|
+
const dateB = new Date(b[dateField] as string).getTime()
|
|
120
|
+
return direction === 'desc' ? dateB - dateA : dateA - dateB
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Filter items by date range
|
|
126
|
+
*/
|
|
127
|
+
export function filterByDateRange<T>(
|
|
128
|
+
items: T[],
|
|
129
|
+
dateField: keyof T,
|
|
130
|
+
startDate: Date,
|
|
131
|
+
endDate: Date
|
|
132
|
+
): T[] {
|
|
133
|
+
return items.filter((item) => {
|
|
134
|
+
const date = new Date(item[dateField] as string)
|
|
135
|
+
return date >= startDate && date <= endDate
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Filter items from the last N days
|
|
141
|
+
*/
|
|
142
|
+
export function filterByLastDays<T>(items: T[], dateField: keyof T, days: number): T[] {
|
|
143
|
+
const startDate = new Date()
|
|
144
|
+
startDate.setDate(startDate.getDate() - days)
|
|
145
|
+
return filterByDateRange(items, dateField, startDate, new Date())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Group items by a field value
|
|
150
|
+
*/
|
|
151
|
+
export function groupByField<T, K extends keyof T>(items: T[], field: K): Map<T[K], T[]> {
|
|
152
|
+
const groups = new Map<T[K], T[]>()
|
|
153
|
+
|
|
154
|
+
for (const item of items) {
|
|
155
|
+
const key = item[field]
|
|
156
|
+
if (!groups.has(key)) {
|
|
157
|
+
groups.set(key, [])
|
|
158
|
+
}
|
|
159
|
+
groups.get(key)!.push(item)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return groups
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Count items by field value
|
|
167
|
+
*/
|
|
168
|
+
export function countByField<T, K extends keyof T>(items: T[], field: K): Map<T[K], number> {
|
|
169
|
+
const counts = new Map<T[K], number>()
|
|
170
|
+
|
|
171
|
+
for (const item of items) {
|
|
172
|
+
const key = item[field]
|
|
173
|
+
counts.set(key, (counts.get(key) || 0) + 1)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return counts
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the first N items
|
|
181
|
+
*/
|
|
182
|
+
export function take<T>(items: T[], count: number): T[] {
|
|
183
|
+
return items.slice(0, count)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the last N items
|
|
188
|
+
*/
|
|
189
|
+
export function takeLast<T>(items: T[], count: number): T[] {
|
|
190
|
+
return items.slice(-count)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Find item by field value
|
|
195
|
+
*/
|
|
196
|
+
export function findByField<T, K extends keyof T>(
|
|
197
|
+
items: T[],
|
|
198
|
+
field: K,
|
|
199
|
+
value: T[K]
|
|
200
|
+
): T | undefined {
|
|
201
|
+
return items.find((item) => item[field] === value)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if any item matches field value
|
|
206
|
+
*/
|
|
207
|
+
export function anyByField<T, K extends keyof T>(items: T[], field: K, value: T[K]): boolean {
|
|
208
|
+
return items.some((item) => item[field] === value)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Remove duplicates by field
|
|
213
|
+
*/
|
|
214
|
+
export function uniqueByField<T, K extends keyof T>(items: T[], field: K): T[] {
|
|
215
|
+
const seen = new Set<T[K]>()
|
|
216
|
+
return items.filter((item) => {
|
|
217
|
+
if (seen.has(item[field])) return false
|
|
218
|
+
seen.add(item[field])
|
|
219
|
+
return true
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Default export for CommonJS compatibility
|
|
224
|
+
export default {
|
|
225
|
+
PRIORITY_ORDER,
|
|
226
|
+
SECTION_ORDER,
|
|
227
|
+
filterByField,
|
|
228
|
+
filterByFieldIn,
|
|
229
|
+
filterByFieldNot,
|
|
230
|
+
filterActiveByField,
|
|
231
|
+
filterByTruthy,
|
|
232
|
+
filterByFalsy,
|
|
233
|
+
sortByPriority,
|
|
234
|
+
sortBySectionAndPriority,
|
|
235
|
+
sortByDate,
|
|
236
|
+
filterByDateRange,
|
|
237
|
+
filterByLastDays,
|
|
238
|
+
groupByField,
|
|
239
|
+
countByField,
|
|
240
|
+
take,
|
|
241
|
+
takeLast,
|
|
242
|
+
findByField,
|
|
243
|
+
anyByField,
|
|
244
|
+
uniqueByField,
|
|
245
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import { type NodeError, isNotFoundError } from '../types/fs'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* File Helper - Centralized file operations with error handling
|
|
@@ -16,10 +17,6 @@ interface ListFilesOptions {
|
|
|
16
17
|
extension?: string
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
interface NodeError extends Error {
|
|
20
|
-
code?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
/**
|
|
24
21
|
* Read JSON file and parse
|
|
25
22
|
*/
|
|
@@ -28,7 +25,7 @@ export async function readJson<T = unknown>(filePath: string, defaultValue: T |
|
|
|
28
25
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
29
26
|
return JSON.parse(content) as T
|
|
30
27
|
} catch (error) {
|
|
31
|
-
if ((error
|
|
28
|
+
if (isNotFoundError(error)) {
|
|
32
29
|
return defaultValue
|
|
33
30
|
}
|
|
34
31
|
throw error
|
|
@@ -50,7 +47,7 @@ export async function readFile(filePath: string, defaultValue = ''): Promise<str
|
|
|
50
47
|
try {
|
|
51
48
|
return await fs.readFile(filePath, 'utf-8')
|
|
52
49
|
} catch (error) {
|
|
53
|
-
if ((error
|
|
50
|
+
if (isNotFoundError(error)) {
|
|
54
51
|
return defaultValue
|
|
55
52
|
}
|
|
56
53
|
throw error
|
|
@@ -84,6 +81,15 @@ export async function appendToFile(filePath: string, content: string): Promise<v
|
|
|
84
81
|
await fs.appendFile(filePath, content, 'utf-8')
|
|
85
82
|
}
|
|
86
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Append a single line to file (with newline and directory creation)
|
|
86
|
+
*/
|
|
87
|
+
export async function appendLine(filePath: string, line: string): Promise<void> {
|
|
88
|
+
const dir = path.dirname(filePath)
|
|
89
|
+
await fs.mkdir(dir, { recursive: true })
|
|
90
|
+
await fs.appendFile(filePath, line + '\n', 'utf-8')
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
/**
|
|
88
94
|
* Prepend to text file (adds content at beginning)
|
|
89
95
|
*/
|
|
@@ -92,7 +98,7 @@ export async function prependToFile(filePath: string, content: string): Promise<
|
|
|
92
98
|
const existing = await fs.readFile(filePath, 'utf-8')
|
|
93
99
|
await fs.writeFile(filePath, content + existing, 'utf-8')
|
|
94
100
|
} catch (error) {
|
|
95
|
-
if ((error
|
|
101
|
+
if (isNotFoundError(error)) {
|
|
96
102
|
await fs.writeFile(filePath, content, 'utf-8')
|
|
97
103
|
} else {
|
|
98
104
|
throw error
|
|
@@ -139,7 +145,7 @@ export async function deleteFile(filePath: string): Promise<boolean> {
|
|
|
139
145
|
await fs.unlink(filePath)
|
|
140
146
|
return true
|
|
141
147
|
} catch (error) {
|
|
142
|
-
if ((error
|
|
148
|
+
if (isNotFoundError(error)) {
|
|
143
149
|
return false // File didn't exist
|
|
144
150
|
}
|
|
145
151
|
throw error
|
|
@@ -154,7 +160,7 @@ export async function deleteDir(dirPath: string): Promise<boolean> {
|
|
|
154
160
|
await fs.rm(dirPath, { recursive: true, force: true })
|
|
155
161
|
return true
|
|
156
162
|
} catch (error) {
|
|
157
|
-
if ((error
|
|
163
|
+
if (isNotFoundError(error)) {
|
|
158
164
|
return false
|
|
159
165
|
}
|
|
160
166
|
throw error
|
|
@@ -183,7 +189,7 @@ export async function listFiles(dirPath: string, options: ListFilesOptions = {})
|
|
|
183
189
|
|
|
184
190
|
return files.map((entry) => entry.name)
|
|
185
191
|
} catch (error) {
|
|
186
|
-
if ((error
|
|
192
|
+
if (isNotFoundError(error)) {
|
|
187
193
|
return []
|
|
188
194
|
}
|
|
189
195
|
throw error
|
|
@@ -257,7 +263,11 @@ export default {
|
|
|
257
263
|
readFile,
|
|
258
264
|
writeFile,
|
|
259
265
|
atomicWrite,
|
|
266
|
+
appendToFile,
|
|
267
|
+
appendLine,
|
|
268
|
+
prependToFile,
|
|
260
269
|
fileExists,
|
|
270
|
+
dirExists,
|
|
261
271
|
ensureDir,
|
|
262
272
|
deleteFile,
|
|
263
273
|
deleteDir,
|
|
@@ -2,6 +2,7 @@ import fs from 'fs/promises'
|
|
|
2
2
|
import fsSync from 'fs'
|
|
3
3
|
import readline from 'readline'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import { isNotFoundError } from '../types/fs'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* JSONL Helper - Centralized JSONL parsing and writing
|
|
@@ -17,10 +18,6 @@ import path from 'path'
|
|
|
17
18
|
* {"ts":"2025-10-04T15:00:00Z","type":"task_start","task":"JWT"}
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
|
-
interface NodeError extends Error {
|
|
21
|
-
code?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
interface FileSizeWarning {
|
|
25
22
|
sizeMB: number
|
|
26
23
|
isLarge: boolean
|
|
@@ -60,7 +57,7 @@ export async function readJsonLines<T = Record<string, unknown>>(filePath: strin
|
|
|
60
57
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
61
58
|
return parseJsonLines<T>(content)
|
|
62
59
|
} catch (error) {
|
|
63
|
-
if ((error
|
|
60
|
+
if (isNotFoundError(error)) {
|
|
64
61
|
return [] // File doesn't exist, return empty array
|
|
65
62
|
}
|
|
66
63
|
throw error
|
|
@@ -113,7 +110,7 @@ export async function countJsonLines(filePath: string): Promise<number> {
|
|
|
113
110
|
const lines = content.split('\n').filter((line) => line.trim())
|
|
114
111
|
return lines.length
|
|
115
112
|
} catch (error) {
|
|
116
|
-
if ((error
|
|
113
|
+
if (isNotFoundError(error)) {
|
|
117
114
|
return 0
|
|
118
115
|
}
|
|
119
116
|
throw error
|
|
@@ -200,7 +197,7 @@ export async function readJsonLinesStreaming<T = Record<string, unknown>>(
|
|
|
200
197
|
|
|
201
198
|
return lines
|
|
202
199
|
} catch (error) {
|
|
203
|
-
if ((error
|
|
200
|
+
if (isNotFoundError(error)) {
|
|
204
201
|
return []
|
|
205
202
|
}
|
|
206
203
|
throw error
|
|
@@ -215,7 +212,7 @@ export async function getFileSizeMB(filePath: string): Promise<number> {
|
|
|
215
212
|
const stats = await fs.stat(filePath)
|
|
216
213
|
return stats.size / (1024 * 1024)
|
|
217
214
|
} catch (error) {
|
|
218
|
-
if ((error
|
|
215
|
+
if (isNotFoundError(error)) {
|
|
219
216
|
return 0
|
|
220
217
|
}
|
|
221
218
|
throw error
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownBuilder - Fluent API for constructing markdown documents
|
|
3
|
+
*
|
|
4
|
+
* Eliminates duplicated markdown generation across:
|
|
5
|
+
* - state-storage.ts (~35 lines)
|
|
6
|
+
* - queue-storage.ts (~45 lines)
|
|
7
|
+
* - ideas-storage.ts (~40 lines)
|
|
8
|
+
* - shipped-storage.ts (~25 lines)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const content = md()
|
|
13
|
+
* .h1('Title')
|
|
14
|
+
* .p('Description')
|
|
15
|
+
* .when(hasItems, m => m.list(items))
|
|
16
|
+
* .build()
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class MarkdownBuilder {
|
|
21
|
+
private lines: string[] = []
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Add H1 heading
|
|
25
|
+
*/
|
|
26
|
+
h1(text: string): this {
|
|
27
|
+
this.lines.push(`# ${text}`, '')
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add H2 heading
|
|
33
|
+
*/
|
|
34
|
+
h2(text: string): this {
|
|
35
|
+
this.lines.push(`## ${text}`, '')
|
|
36
|
+
return this
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add H3 heading
|
|
41
|
+
*/
|
|
42
|
+
h3(text: string): this {
|
|
43
|
+
this.lines.push(`### ${text}`)
|
|
44
|
+
return this
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add H4 heading
|
|
49
|
+
*/
|
|
50
|
+
h4(text: string): this {
|
|
51
|
+
this.lines.push(`#### ${text}`)
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add paragraph
|
|
57
|
+
*/
|
|
58
|
+
p(text: string): this {
|
|
59
|
+
this.lines.push(text, '')
|
|
60
|
+
return this
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add bold text on its own line
|
|
65
|
+
*/
|
|
66
|
+
bold(text: string): this {
|
|
67
|
+
this.lines.push(`**${text}**`)
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Add italic text on its own line
|
|
73
|
+
*/
|
|
74
|
+
italic(text: string): this {
|
|
75
|
+
this.lines.push(`*${text}*`)
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add inline code
|
|
81
|
+
*/
|
|
82
|
+
code(text: string): this {
|
|
83
|
+
this.lines.push(`\`${text}\``)
|
|
84
|
+
return this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add code block
|
|
89
|
+
*/
|
|
90
|
+
codeBlock(code: string, lang = ''): this {
|
|
91
|
+
this.lines.push(`\`\`\`${lang}`, code, '```', '')
|
|
92
|
+
return this
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add list item with optional checkbox
|
|
97
|
+
*/
|
|
98
|
+
li(text: string, options?: { checked?: boolean; indent?: number }): this {
|
|
99
|
+
const indent = ' '.repeat(options?.indent ?? 0)
|
|
100
|
+
if (options?.checked !== undefined) {
|
|
101
|
+
this.lines.push(`${indent}- [${options.checked ? 'x' : ' '}] ${text}`)
|
|
102
|
+
} else {
|
|
103
|
+
this.lines.push(`${indent}- ${text}`)
|
|
104
|
+
}
|
|
105
|
+
return this
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Add numbered list item
|
|
110
|
+
*/
|
|
111
|
+
oli(text: string, number: number): this {
|
|
112
|
+
this.lines.push(`${number}. ${text}`)
|
|
113
|
+
return this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Add multiple list items
|
|
118
|
+
*/
|
|
119
|
+
list(items: string[], options?: { checked?: boolean }): this {
|
|
120
|
+
items.forEach((item) => this.li(item, options))
|
|
121
|
+
return this
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Add multiple numbered list items
|
|
126
|
+
*/
|
|
127
|
+
orderedList(items: string[]): this {
|
|
128
|
+
items.forEach((item, i) => this.oli(item, i + 1))
|
|
129
|
+
return this
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Add horizontal rule
|
|
134
|
+
*/
|
|
135
|
+
hr(): this {
|
|
136
|
+
this.lines.push('', '---', '')
|
|
137
|
+
return this
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Add blank line
|
|
142
|
+
*/
|
|
143
|
+
blank(): this {
|
|
144
|
+
this.lines.push('')
|
|
145
|
+
return this
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Add raw line(s)
|
|
150
|
+
*/
|
|
151
|
+
raw(text: string): this {
|
|
152
|
+
this.lines.push(text)
|
|
153
|
+
return this
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add multiple raw lines
|
|
158
|
+
*/
|
|
159
|
+
rawLines(lines: string[]): this {
|
|
160
|
+
this.lines.push(...lines)
|
|
161
|
+
return this
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Add blockquote
|
|
166
|
+
*/
|
|
167
|
+
quote(text: string): this {
|
|
168
|
+
this.lines.push(`> ${text}`)
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Add link
|
|
174
|
+
*/
|
|
175
|
+
link(text: string, url: string): this {
|
|
176
|
+
this.lines.push(`[${text}](${url})`)
|
|
177
|
+
return this
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Add key-value pair (bold key)
|
|
182
|
+
*/
|
|
183
|
+
kv(key: string, value: string): this {
|
|
184
|
+
this.lines.push(`**${key}:** ${value}`)
|
|
185
|
+
return this
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Add table from data
|
|
190
|
+
*/
|
|
191
|
+
table(headers: string[], rows: string[][]): this {
|
|
192
|
+
// Header row
|
|
193
|
+
this.lines.push(`| ${headers.join(' | ')} |`)
|
|
194
|
+
// Separator
|
|
195
|
+
this.lines.push(`| ${headers.map(() => '---').join(' | ')} |`)
|
|
196
|
+
// Data rows
|
|
197
|
+
rows.forEach((row) => {
|
|
198
|
+
this.lines.push(`| ${row.join(' | ')} |`)
|
|
199
|
+
})
|
|
200
|
+
this.blank()
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Conditional block - only adds content if condition is true
|
|
206
|
+
*/
|
|
207
|
+
when(condition: boolean, builder: (md: MarkdownBuilder) => void): this {
|
|
208
|
+
if (condition) {
|
|
209
|
+
builder(this)
|
|
210
|
+
}
|
|
211
|
+
return this
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Optional block - only adds content if value exists
|
|
216
|
+
*/
|
|
217
|
+
maybe<T>(value: T | null | undefined, builder: (md: MarkdownBuilder, val: T) => void): this {
|
|
218
|
+
if (value != null) {
|
|
219
|
+
builder(this, value)
|
|
220
|
+
}
|
|
221
|
+
return this
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Iterate and build for each item
|
|
226
|
+
*/
|
|
227
|
+
each<T>(items: T[], builder: (md: MarkdownBuilder, item: T, index: number) => void): this {
|
|
228
|
+
items.forEach((item, i) => builder(this, item, i))
|
|
229
|
+
return this
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add section with heading and content
|
|
234
|
+
*/
|
|
235
|
+
section(heading: string, level: 1 | 2 | 3 | 4, builder: (md: MarkdownBuilder) => void): this {
|
|
236
|
+
switch (level) {
|
|
237
|
+
case 1:
|
|
238
|
+
this.h1(heading)
|
|
239
|
+
break
|
|
240
|
+
case 2:
|
|
241
|
+
this.h2(heading)
|
|
242
|
+
break
|
|
243
|
+
case 3:
|
|
244
|
+
this.h3(heading)
|
|
245
|
+
break
|
|
246
|
+
case 4:
|
|
247
|
+
this.h4(heading)
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
builder(this)
|
|
251
|
+
return this
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build final markdown string
|
|
256
|
+
*/
|
|
257
|
+
build(): string {
|
|
258
|
+
return this.lines.join('\n')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get current line count
|
|
263
|
+
*/
|
|
264
|
+
get length(): number {
|
|
265
|
+
return this.lines.length
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Factory function for fluent API
|
|
271
|
+
*/
|
|
272
|
+
export function md(): MarkdownBuilder {
|
|
273
|
+
return new MarkdownBuilder()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Default export
|
|
277
|
+
export default { MarkdownBuilder, md }
|