@schnick371/devsteps-shared 0.0.1 → 0.5.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.
Files changed (104) hide show
  1. package/README.md +66 -7
  2. package/dist/core/add.d.ts +23 -0
  3. package/dist/core/add.d.ts.map +1 -0
  4. package/dist/core/add.js +102 -0
  5. package/dist/core/add.js.map +1 -0
  6. package/dist/core/archive.d.ts +24 -0
  7. package/dist/core/archive.d.ts.map +1 -0
  8. package/dist/core/archive.js +109 -0
  9. package/dist/core/archive.js.map +1 -0
  10. package/dist/core/bulk-update.d.ts +34 -0
  11. package/dist/core/bulk-update.d.ts.map +1 -0
  12. package/dist/core/bulk-update.js +141 -0
  13. package/dist/core/bulk-update.js.map +1 -0
  14. package/dist/core/context.d.ts +54 -0
  15. package/dist/core/context.d.ts.map +1 -0
  16. package/dist/core/context.js +198 -0
  17. package/dist/core/context.js.map +1 -0
  18. package/dist/core/get.d.ts +10 -0
  19. package/dist/core/get.d.ts.map +1 -0
  20. package/dist/core/get.js +28 -0
  21. package/dist/core/get.js.map +1 -0
  22. package/dist/core/index.d.ts +9 -0
  23. package/dist/core/index.d.ts.map +1 -0
  24. package/dist/core/index.js +9 -0
  25. package/dist/core/index.js.map +1 -0
  26. package/dist/core/list.d.ts +19 -0
  27. package/dist/core/list.d.ts.map +1 -0
  28. package/dist/core/list.js +64 -0
  29. package/dist/core/list.js.map +1 -0
  30. package/dist/core/update.d.ts +23 -0
  31. package/dist/core/update.d.ts.map +1 -0
  32. package/dist/core/update.js +118 -0
  33. package/dist/core/update.js.map +1 -0
  34. package/dist/core/validation.d.ts +28 -0
  35. package/dist/core/validation.d.ts.map +1 -0
  36. package/dist/core/validation.js +205 -0
  37. package/dist/core/validation.js.map +1 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +5 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/schemas/index.d.ts +286 -0
  43. package/dist/schemas/index.d.ts.map +1 -0
  44. package/dist/schemas/index.js +200 -0
  45. package/dist/schemas/index.js.map +1 -0
  46. package/dist/schemas/project.d.ts +134 -0
  47. package/dist/schemas/project.d.ts.map +1 -0
  48. package/dist/schemas/project.js +54 -0
  49. package/dist/schemas/project.js.map +1 -0
  50. package/dist/schemas/relationships.d.ts +30 -0
  51. package/dist/schemas/relationships.d.ts.map +1 -0
  52. package/dist/schemas/relationships.js +41 -0
  53. package/dist/schemas/relationships.js.map +1 -0
  54. package/dist/types/commands.d.ts +92 -0
  55. package/dist/types/commands.d.ts.map +1 -0
  56. package/dist/types/commands.js +14 -0
  57. package/dist/types/commands.js.map +1 -0
  58. package/dist/types/index.d.ts +3 -0
  59. package/dist/types/index.d.ts.map +1 -0
  60. package/dist/types/index.js +2 -0
  61. package/dist/types/index.js.map +1 -0
  62. package/dist/utils/cache.d.ts +80 -0
  63. package/dist/utils/cache.d.ts.map +1 -0
  64. package/dist/utils/cache.js +169 -0
  65. package/dist/utils/cache.js.map +1 -0
  66. package/dist/utils/index.d.ts +72 -0
  67. package/dist/utils/index.d.ts.map +1 -0
  68. package/dist/utils/index.js +148 -0
  69. package/dist/utils/index.js.map +1 -0
  70. package/package.json +48 -15
  71. package/src/core/add.js.map +1 -0
  72. package/src/core/add.ts +145 -0
  73. package/src/core/archive.js.map +1 -0
  74. package/src/core/archive.ts +153 -0
  75. package/src/core/bulk-update.js.map +1 -0
  76. package/src/core/bulk-update.ts +180 -0
  77. package/src/core/context.js.map +1 -0
  78. package/src/core/context.ts +269 -0
  79. package/src/core/get.js.map +1 -0
  80. package/src/core/get.ts +39 -0
  81. package/src/core/index.js.map +1 -0
  82. package/src/core/index.ts +26 -0
  83. package/src/core/list.js.map +1 -0
  84. package/src/core/list.ts +96 -0
  85. package/src/core/update.js.map +1 -0
  86. package/src/core/update.ts +154 -0
  87. package/src/core/validation.ts +252 -0
  88. package/src/index.js +21 -0
  89. package/src/index.js.map +1 -0
  90. package/src/index.ts +4 -0
  91. package/src/schemas/index.js.map +1 -0
  92. package/src/schemas/index.ts +226 -0
  93. package/src/schemas/project.js.map +1 -0
  94. package/src/schemas/project.ts +73 -0
  95. package/src/schemas/relationships.ts +48 -0
  96. package/src/types/commands.js.map +1 -0
  97. package/src/types/commands.ts +116 -0
  98. package/src/types/index.js.map +1 -0
  99. package/src/types/index.ts +13 -0
  100. package/src/utils/cache.js.map +1 -0
  101. package/src/utils/cache.ts +200 -0
  102. package/src/utils/index.js.map +1 -0
  103. package/src/utils/index.ts +170 -0
  104. package/index.js +0 -6
@@ -0,0 +1,269 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { ContextLevel, QuickContext } from '../schemas/project.js';
4
+ import { CACHE_CONFIG, getCache } from '../utils/cache.js';
5
+
6
+ interface PackageInfo {
7
+ name: string;
8
+ purpose: string;
9
+ dependencies: string[];
10
+ }
11
+
12
+ interface ItemCounts {
13
+ total: number;
14
+ by_type: Record<string, number>;
15
+ by_status: Record<string, number>;
16
+ }
17
+
18
+ interface RecentUpdate {
19
+ id: string;
20
+ type: string;
21
+ title: string;
22
+ updated: string;
23
+ }
24
+
25
+ interface ContextResponse {
26
+ context_level: ContextLevel;
27
+ tokens_used: number;
28
+ static_context?: unknown;
29
+ dynamic_context: {
30
+ packages: PackageInfo[];
31
+ active_items: ItemCounts;
32
+ recent_updates: RecentUpdate[];
33
+ };
34
+ suggestions?: string[];
35
+ }
36
+
37
+ /**
38
+ * Analyze packages from workspace (with caching)
39
+ */
40
+ export async function analyzePackages(projectDir: string): Promise<PackageInfo[]> {
41
+ const cache = getCache();
42
+ const cacheKey = `package_analysis:${projectDir}`;
43
+ const packagesDir = path.join(projectDir, 'packages');
44
+
45
+ // Try cache first
46
+ const cached = cache.get<PackageInfo[]>(cacheKey, CACHE_CONFIG.PACKAGE_ANALYSIS_TTL, packagesDir);
47
+ if (cached) {
48
+ return cached;
49
+ }
50
+
51
+ const packages: PackageInfo[] = [];
52
+
53
+ try {
54
+ const entries = await fs.readdir(packagesDir, { withFileTypes: true });
55
+
56
+ for (const entry of entries) {
57
+ if (entry.isDirectory()) {
58
+ const pkgPath = path.join(packagesDir, entry.name, 'package.json');
59
+ try {
60
+ const content = await fs.readFile(pkgPath, 'utf-8');
61
+ const pkg = JSON.parse(content);
62
+
63
+ packages.push({
64
+ name: pkg.name || entry.name,
65
+ purpose: pkg.description || 'No description',
66
+ dependencies: Object.keys(pkg.dependencies || {}),
67
+ });
68
+ } catch {
69
+ // Skip if no package.json
70
+ }
71
+ }
72
+ }
73
+ } catch {
74
+ // No packages directory
75
+ }
76
+
77
+ // Cache the result
78
+ cache.set(cacheKey, packages, packagesDir);
79
+
80
+ return packages;
81
+ }
82
+
83
+ /**
84
+ * Get item counts from index (with caching)
85
+ */
86
+ export async function getItemCounts(devcrumbsDir: string): Promise<ItemCounts> {
87
+ const cache = getCache();
88
+ const cacheKey = `item_counts:${devcrumbsDir}`;
89
+ const indexPath = path.join(devcrumbsDir, 'index.json');
90
+
91
+ // Try cache first
92
+ const cached = cache.get<ItemCounts>(cacheKey, CACHE_CONFIG.ITEM_COUNTS_TTL, indexPath);
93
+ if (cached) {
94
+ return cached;
95
+ }
96
+
97
+ try {
98
+ const content = await fs.readFile(indexPath, 'utf-8');
99
+ const index = JSON.parse(content);
100
+
101
+ const counts: ItemCounts = {
102
+ total: index.items.length,
103
+ by_type: {},
104
+ by_status: {},
105
+ };
106
+
107
+ for (const item of index.items) {
108
+ counts.by_type[item.type] = (counts.by_type[item.type] || 0) + 1;
109
+ counts.by_status[item.status] = (counts.by_status[item.status] || 0) + 1;
110
+ }
111
+
112
+ // Cache the result
113
+ cache.set(cacheKey, counts, indexPath);
114
+
115
+ return counts;
116
+ } catch {
117
+ return { total: 0, by_type: {}, by_status: {} };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Get recent updates (last N days) (with caching)
123
+ */
124
+ export async function getRecentUpdates(devcrumbsDir: string, days: number): Promise<RecentUpdate[]> {
125
+ const cache = getCache();
126
+ const cacheKey = `recent_updates:${devcrumbsDir}:${days}`;
127
+ const indexPath = path.join(devcrumbsDir, 'index.json');
128
+
129
+ // Try cache first
130
+ const cached = cache.get<RecentUpdate[]>(cacheKey, CACHE_CONFIG.RECENT_UPDATES_TTL, indexPath);
131
+ if (cached) {
132
+ return cached;
133
+ }
134
+
135
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
136
+
137
+ try {
138
+ const content = await fs.readFile(indexPath, 'utf-8');
139
+ const index = JSON.parse(content);
140
+
141
+ const updates = index.items
142
+ .filter((item: { updated: string }) => new Date(item.updated).getTime() > cutoff)
143
+ .sort(
144
+ (a: { updated: string }, b: { updated: string }) =>
145
+ new Date(b.updated).getTime() - new Date(a.updated).getTime()
146
+ )
147
+ .slice(0, 10)
148
+ .map((item: { id: string; type: string; title: string; updated: string }) => ({
149
+ id: item.id,
150
+ type: item.type,
151
+ title: item.title,
152
+ updated: item.updated,
153
+ }));
154
+
155
+ // Cache the result
156
+ cache.set(cacheKey, updates, indexPath);
157
+
158
+ return updates;
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Read PROJECT.md if exists (with caching)
166
+ */
167
+ export async function readProjectMd(devcrumbsDir: string): Promise<QuickContext | null> {
168
+ const cache = getCache();
169
+ const cacheKey = `project_md:${devcrumbsDir}`;
170
+ const projectPath = path.join(devcrumbsDir, 'PROJECT.md');
171
+
172
+ // Try cache first
173
+ const cached = cache.get<QuickContext | null>(cacheKey, CACHE_CONFIG.PROJECT_MD_TTL, projectPath);
174
+ if (cached !== null) {
175
+ return cached;
176
+ }
177
+
178
+ try {
179
+ const content = await fs.readFile(projectPath, 'utf-8');
180
+
181
+ // Parse markdown to extract context (simplified)
182
+ // In production, use a proper markdown parser
183
+ const techStack: Record<string, string> = {};
184
+ const packages: PackageInfo[] = [];
185
+
186
+ // Extract tech stack (look for bullet points after "### Tech Stack")
187
+ const techStackMatch = content.match(/### Tech Stack\n([\s\S]*?)(?=\n###|$)/);
188
+ if (techStackMatch) {
189
+ const lines = techStackMatch[1].split('\n');
190
+ for (const line of lines) {
191
+ const match = line.match(/- \*\*(.+?)\*\*: (.+)/);
192
+ if (match) {
193
+ techStack[match[1]] = match[2];
194
+ }
195
+ }
196
+ }
197
+
198
+ // Extract project type
199
+ const projectTypeMatch = content.match(/### Project Type\n(.+)/);
200
+ const projectType = projectTypeMatch?.[1] || 'Unknown';
201
+
202
+ const result = {
203
+ tech_stack: techStack,
204
+ packages,
205
+ project_type: projectType,
206
+ };
207
+
208
+ // Cache the result
209
+ cache.set(cacheKey, result, projectPath);
210
+
211
+ return result;
212
+ } catch {
213
+ // Cache the null result (file doesn't exist)
214
+ cache.set(cacheKey, null);
215
+ return null;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Estimate token count (rough approximation: 1 token ≈ 4 chars)
221
+ */
222
+ export function estimateTokens(obj: unknown): number {
223
+ const str = JSON.stringify(obj);
224
+ return Math.ceil(str.length / 4);
225
+ }
226
+
227
+ /**
228
+ * Get quick context
229
+ */
230
+ export async function getQuickContext(
231
+ projectDir: string,
232
+ devcrumbsDir: string
233
+ ): Promise<ContextResponse> {
234
+ const packages = await analyzePackages(projectDir);
235
+ const activeItems = await getItemCounts(devcrumbsDir);
236
+ const recentUpdates = await getRecentUpdates(devcrumbsDir, 7);
237
+ const staticContext = await readProjectMd(devcrumbsDir);
238
+
239
+ const response: ContextResponse = {
240
+ context_level: 'quick',
241
+ tokens_used: 0,
242
+ dynamic_context: {
243
+ packages,
244
+ active_items: activeItems,
245
+ recent_updates: recentUpdates,
246
+ },
247
+ };
248
+
249
+ if (staticContext) {
250
+ response.static_context = staticContext;
251
+ }
252
+
253
+ response.tokens_used = estimateTokens(response);
254
+
255
+ // Add suggestions
256
+ const suggestions: string[] = [];
257
+ if (!staticContext) {
258
+ suggestions.push('Consider creating .devcrumbs/PROJECT.md for static context');
259
+ }
260
+ if (activeItems.by_status.blocked > 0) {
261
+ suggestions.push(`${activeItems.by_status.blocked} blocked items need attention`);
262
+ }
263
+
264
+ if (suggestions.length > 0) {
265
+ response.suggestions = suggestions;
266
+ }
267
+
268
+ return response;
269
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get.js","sourceRoot":"","sources":["get.ts"],"names":[],"mappings":";;AAaA,0BAyBC;AAtCD,qCAAmD;AACnD,yCAAiC;AAEjC,gDAAmE;AAOnE;;GAEG;AACI,KAAK,UAAU,OAAO,CAAC,SAAiB,EAAE,MAAc;IAC7D,IAAI,CAAC,IAAA,oBAAU,EAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,sBAAW,EAAC,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,UAAU,GAAG,4BAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,IAAA,gBAAI,EAAC,SAAS,EAAE,UAAU,EAAE,GAAG,MAAM,OAAO,CAAC,CAAC;IACnE,MAAM,eAAe,GAAG,IAAA,gBAAI,EAAC,SAAS,EAAE,UAAU,EAAE,GAAG,MAAM,KAAK,CAAC,CAAC;IAEpE,IAAI,CAAC,IAAA,oBAAU,EAAC,YAAY,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,QAAQ,GAAiB,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/E,MAAM,WAAW,GAAG,IAAA,oBAAU,EAAC,eAAe,CAAC,CAAC,CAAC,CAAC,IAAA,sBAAY,EAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE9F,OAAO;QACL,QAAQ;QACR,WAAW;KACZ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { ItemMetadata } from '../schemas/index.js';
4
+ import { TYPE_TO_DIRECTORY, parseItemId } from '../utils/index.js';
5
+
6
+ export interface GetItemResult {
7
+ metadata: ItemMetadata;
8
+ description: string;
9
+ }
10
+
11
+ /**
12
+ * Core business logic for getting an item
13
+ */
14
+ export async function getItem(devcrumbsDir: string, itemId: string): Promise<GetItemResult> {
15
+ if (!existsSync(devcrumbsDir)) {
16
+ throw new Error('Project not initialized. Run devcrumbs-init first.');
17
+ }
18
+
19
+ const parsed = parseItemId(itemId);
20
+ if (!parsed) {
21
+ throw new Error(`Invalid item ID: ${itemId}`);
22
+ }
23
+
24
+ const typeFolder = TYPE_TO_DIRECTORY[parsed.type];
25
+ const metadataPath = join(devcrumbsDir, typeFolder, `${itemId}.json`);
26
+ const descriptionPath = join(devcrumbsDir, typeFolder, `${itemId}.md`);
27
+
28
+ if (!existsSync(metadataPath)) {
29
+ throw new Error(`Item not found: ${itemId}`);
30
+ }
31
+
32
+ const metadata: ItemMetadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
33
+ const description = existsSync(descriptionPath) ? readFileSync(descriptionPath, 'utf-8') : '';
34
+
35
+ return {
36
+ metadata,
37
+ description,
38
+ };
39
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,mCAAyE;AAAhE,iGAAA,OAAO,OAAA;AAChB,mCAAuD;AAA9C,iGAAA,OAAO,OAAA;AAChB,qCAAgF;AAAvE,oGAAA,SAAS,OAAA;AAClB,yCAAqF;AAA5E,uGAAA,UAAU,OAAA;AACnB,2CAMsB;AALpB,yGAAA,WAAW,OAAA;AAEX,wGAAA,UAAU,OAAA;AAIZ,2CAOsB;AANpB,6GAAA,eAAe,OAAA;AACf,6GAAA,eAAe,OAAA;AACf,2GAAA,aAAa,OAAA;AACb,8GAAA,gBAAgB,OAAA;AAChB,2GAAA,aAAa,OAAA;AACb,4GAAA,cAAc,OAAA;AAEhB,mDAK0B;AAJxB,iHAAA,eAAe,OAAA;AACf,6GAAA,WAAW,OAAA;AACX,gHAAA,cAAc,OAAA"}
@@ -0,0 +1,26 @@
1
+ export { addItem, type AddItemArgs, type AddItemResult } from './add.js';
2
+ export { getItem, type GetItemResult } from './get.js';
3
+ export { listItems, type ListItemsArgs, type ListItemsResult } from './list.js';
4
+ export { updateItem, type UpdateItemArgs, type UpdateItemResult } from './update.js';
5
+ export {
6
+ archiveItem,
7
+ type ArchiveItemResult,
8
+ purgeItems,
9
+ type PurgeItemsArgs,
10
+ type PurgeItemsResult,
11
+ } from './archive.js';
12
+ export {
13
+ getQuickContext,
14
+ analyzePackages,
15
+ getItemCounts,
16
+ getRecentUpdates,
17
+ readProjectMd,
18
+ estimateTokens,
19
+ } from './context.js';
20
+ export {
21
+ bulkUpdateItems,
22
+ bulkAddTags,
23
+ bulkRemoveTags,
24
+ type BulkUpdateResult,
25
+ } from './bulk-update.js';
26
+ export { validateRelationship, type ValidationResult, type WorkItem } from './validation.js';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["list.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type {
4
+ DevCrumbsIndex,
5
+ EisenhowerQuadrant,
6
+ ItemStatus,
7
+ ItemType,
8
+ Priority,
9
+ } from '../schemas/index.js';
10
+ import { TYPE_TO_DIRECTORY } from '../utils/index.js';
11
+
12
+ export interface ListItemsArgs {
13
+ type?: ItemType;
14
+ status?: ItemStatus;
15
+ priority?: Priority;
16
+ assignee?: string;
17
+ tags?: string[];
18
+ eisenhower?: EisenhowerQuadrant;
19
+ limit?: number;
20
+ }
21
+
22
+ export interface ListItemsResult {
23
+ count: number;
24
+ items: DevCrumbsIndex['items'];
25
+ }
26
+
27
+ /**
28
+ * Core business logic for listing items with filters
29
+ */
30
+ export async function listItems(
31
+ devcrumbsDir: string,
32
+ args: ListItemsArgs = {}
33
+ ): Promise<ListItemsResult> {
34
+ if (!existsSync(devcrumbsDir)) {
35
+ throw new Error('Project not initialized. Run devcrumbs-init first.');
36
+ }
37
+
38
+ const indexPath = join(devcrumbsDir, 'index.json');
39
+ const index: DevCrumbsIndex = JSON.parse(readFileSync(indexPath, 'utf-8'));
40
+
41
+ let items = [...index.items];
42
+
43
+ // Apply filters
44
+ if (args.type) {
45
+ items = items.filter((i) => i.type === args.type);
46
+ }
47
+
48
+ if (args.status) {
49
+ items = items.filter((i) => i.status === args.status);
50
+ }
51
+
52
+ if (args.priority) {
53
+ items = items.filter((i) => i.priority === args.priority);
54
+ }
55
+
56
+ if (args.assignee) {
57
+ // Load full metadata for assignee filter
58
+ items = items.filter((i) => {
59
+ const metadataPath = join(devcrumbsDir, TYPE_TO_DIRECTORY[i.type], `${i.id}.json`);
60
+ if (!existsSync(metadataPath)) return false;
61
+ const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
62
+ return metadata.assignee === args.assignee;
63
+ });
64
+ }
65
+
66
+ if (args.tags && args.tags.length > 0) {
67
+ // Load full metadata for tags filter
68
+ const filterTags = args.tags;
69
+ items = items.filter((i) => {
70
+ const metadataPath = join(devcrumbsDir, TYPE_TO_DIRECTORY[i.type], `${i.id}.json`);
71
+ if (!existsSync(metadataPath)) return false;
72
+ const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
73
+ return filterTags.every((tag) => metadata.tags.includes(tag));
74
+ });
75
+ }
76
+
77
+ if (args.eisenhower) {
78
+ // Load full metadata for eisenhower filter
79
+ items = items.filter((i) => {
80
+ const metadataPath = join(devcrumbsDir, TYPE_TO_DIRECTORY[i.type], `${i.id}.json`);
81
+ if (!existsSync(metadataPath)) return false;
82
+ const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
83
+ return metadata.eisenhower === args.eisenhower;
84
+ });
85
+ }
86
+
87
+ // Apply limit
88
+ if (args.limit && args.limit > 0) {
89
+ items = items.slice(0, args.limit);
90
+ }
91
+
92
+ return {
93
+ count: items.length,
94
+ items,
95
+ };
96
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.js","sourceRoot":"","sources":["update.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,154 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type {
4
+ DevCrumbsIndex,
5
+ EisenhowerQuadrant,
6
+ ItemMetadata,
7
+ ItemStatus,
8
+ Priority,
9
+ } from '../schemas/index.js';
10
+ import { TYPE_TO_DIRECTORY, getCurrentTimestamp, parseItemId } from '../utils/index.js';
11
+
12
+ export interface UpdateItemArgs {
13
+ id: string;
14
+ status?: ItemStatus;
15
+ priority?: Priority;
16
+ eisenhower?: EisenhowerQuadrant;
17
+ superseded_by?: string;
18
+ title?: string;
19
+ description?: string;
20
+ append_description?: string;
21
+ assignee?: string;
22
+ tags?: string[];
23
+ affected_paths?: string[];
24
+ }
25
+
26
+ export interface UpdateItemResult {
27
+ metadata: ItemMetadata;
28
+ oldStatus: ItemStatus;
29
+ }
30
+
31
+ /**
32
+ * Core business logic for updating an item
33
+ */
34
+ export async function updateItem(
35
+ devcrumbsDir: string,
36
+ args: UpdateItemArgs
37
+ ): Promise<UpdateItemResult> {
38
+ if (!existsSync(devcrumbsDir)) {
39
+ throw new Error('Project not initialized. Run devcrumbs-init first.');
40
+ }
41
+
42
+ const parsed = parseItemId(args.id);
43
+ if (!parsed) {
44
+ throw new Error(`Invalid item ID: ${args.id}`);
45
+ }
46
+
47
+ const typeFolder = TYPE_TO_DIRECTORY[parsed.type];
48
+ const metadataPath = join(devcrumbsDir, typeFolder, `${args.id}.json`);
49
+ const descriptionPath = join(devcrumbsDir, typeFolder, `${args.id}.md`);
50
+
51
+ if (!existsSync(metadataPath)) {
52
+ throw new Error(`Item not found: ${args.id}`);
53
+ }
54
+
55
+ // Read and update metadata
56
+ const metadata: ItemMetadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
57
+ const oldStatus = metadata.status;
58
+
59
+ // Validate status transitions (parent-child rules)
60
+ if (args.status === 'done') {
61
+ // Helper function to validate children are complete
62
+ const validateChildren = (relationshipType: 'implemented-by' | 'tested-by'): void => {
63
+ const children = metadata.linked_items[relationshipType];
64
+ if (children.length > 0) {
65
+ const openChildren: string[] = [];
66
+ for (const childId of children) {
67
+ const childParsed = parseItemId(childId);
68
+ if (childParsed) {
69
+ const childFolder = TYPE_TO_DIRECTORY[childParsed.type];
70
+ const childPath = join(devcrumbsDir, childFolder, `${childId}.json`);
71
+ if (existsSync(childPath)) {
72
+ const childMeta: ItemMetadata = JSON.parse(readFileSync(childPath, 'utf-8'));
73
+ if (childMeta.status !== 'done' && childMeta.status !== 'cancelled' && childMeta.status !== 'obsolete') {
74
+ openChildren.push(childId);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ if (openChildren.length > 0) {
80
+ const relationLabel = relationshipType === 'implemented-by' ? 'implementation' : 'test';
81
+ throw new Error(
82
+ `Cannot close ${args.id}: ${openChildren.length} ${relationLabel} item(s) still open: ${openChildren.join(', ')}`
83
+ );
84
+ }
85
+ }
86
+ };
87
+
88
+ // Validate implemented-by children (Scrum: Epic→Story, Story→Task; Waterfall: Requirement→Feature, Feature→Task)
89
+ if (['epic', 'story', 'requirement', 'feature'].includes(metadata.type)) {
90
+ validateChildren('implemented-by');
91
+ }
92
+
93
+ // Validate tested-by children (all parent types must have tests complete)
94
+ if (['epic', 'story', 'requirement', 'feature'].includes(metadata.type)) {
95
+ validateChildren('tested-by');
96
+ }
97
+ }
98
+
99
+ if (args.status) metadata.status = args.status;
100
+ if (args.priority) metadata.priority = args.priority;
101
+ if (args.eisenhower) metadata.eisenhower = args.eisenhower;
102
+ if (args.superseded_by !== undefined) metadata.superseded_by = args.superseded_by;
103
+ if (args.title) metadata.title = args.title;
104
+ if (args.assignee !== undefined) metadata.assignee = args.assignee;
105
+ if (args.tags) metadata.tags = args.tags;
106
+ if (args.affected_paths) metadata.affected_paths = args.affected_paths;
107
+
108
+ metadata.updated = getCurrentTimestamp();
109
+
110
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
111
+
112
+ // Update description
113
+ if (args.description && args.append_description) {
114
+ throw new Error('Cannot use both description and append_description simultaneously');
115
+ }
116
+
117
+ if (args.description) {
118
+ // Replace entire description
119
+ writeFileSync(descriptionPath, args.description);
120
+ } else if (args.append_description) {
121
+ // Append to existing (or create new if doesn't exist)
122
+ const existing = existsSync(descriptionPath)
123
+ ? readFileSync(descriptionPath, 'utf-8')
124
+ : '';
125
+ writeFileSync(descriptionPath, existing + args.append_description);
126
+ }
127
+
128
+ // Update index
129
+ const indexPath = join(devcrumbsDir, 'index.json');
130
+ const index: DevCrumbsIndex = JSON.parse(readFileSync(indexPath, 'utf-8'));
131
+
132
+ const itemIndex = index.items.findIndex((i) => i.id === args.id);
133
+ if (itemIndex !== -1) {
134
+ if (args.status) index.items[itemIndex].status = args.status;
135
+ if (args.priority) index.items[itemIndex].priority = args.priority;
136
+ if (args.title) index.items[itemIndex].title = args.title;
137
+ index.items[itemIndex].updated = metadata.updated;
138
+
139
+ // Update stats
140
+ if (args.status && oldStatus !== args.status) {
141
+ index.stats = index.stats || { total: 0, by_type: {}, by_status: {} };
142
+ index.stats.by_status[oldStatus] = (index.stats.by_status[oldStatus] || 1) - 1;
143
+ index.stats.by_status[args.status] = (index.stats.by_status[args.status] || 0) + 1;
144
+ }
145
+ }
146
+
147
+ index.last_updated = metadata.updated;
148
+ writeFileSync(indexPath, JSON.stringify(index, null, 2));
149
+
150
+ return {
151
+ metadata,
152
+ oldStatus,
153
+ };
154
+ }