@cyberismo/data-handler 0.0.21 → 0.0.22

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 (166) hide show
  1. package/dist/command-handler.js +13 -24
  2. package/dist/command-handler.js.map +1 -1
  3. package/dist/command-manager.d.ts +21 -6
  4. package/dist/command-manager.js +34 -32
  5. package/dist/command-manager.js.map +1 -1
  6. package/dist/commands/calculate.js +101 -46
  7. package/dist/commands/calculate.js.map +1 -1
  8. package/dist/commands/create.js +417 -328
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/edit.js +117 -68
  11. package/dist/commands/edit.js.map +1 -1
  12. package/dist/commands/export.js +301 -252
  13. package/dist/commands/export.js.map +1 -1
  14. package/dist/commands/fetch.js +205 -156
  15. package/dist/commands/fetch.js.map +1 -1
  16. package/dist/commands/import.js +189 -134
  17. package/dist/commands/import.js.map +1 -1
  18. package/dist/commands/migrate.js +91 -45
  19. package/dist/commands/migrate.js.map +1 -1
  20. package/dist/commands/move.js +347 -267
  21. package/dist/commands/move.js.map +1 -1
  22. package/dist/commands/remove.d.ts +1 -0
  23. package/dist/commands/remove.js +202 -135
  24. package/dist/commands/remove.js.map +1 -1
  25. package/dist/commands/rename.js +233 -187
  26. package/dist/commands/rename.js.map +1 -1
  27. package/dist/commands/show.d.ts +8 -8
  28. package/dist/commands/show.js +477 -372
  29. package/dist/commands/show.js.map +1 -1
  30. package/dist/commands/transition.js +119 -73
  31. package/dist/commands/transition.js.map +1 -1
  32. package/dist/commands/update.js +8 -3
  33. package/dist/commands/update.js.map +1 -1
  34. package/dist/commands/validate.js +1 -1
  35. package/dist/commands/validate.js.map +1 -1
  36. package/dist/containers/project/calculation-engine.js +0 -1
  37. package/dist/containers/project/calculation-engine.js.map +1 -1
  38. package/dist/containers/project/card-cache.js +1 -1
  39. package/dist/containers/project/card-cache.js.map +1 -1
  40. package/dist/containers/project.d.ts +16 -0
  41. package/dist/containers/project.js +59 -1
  42. package/dist/containers/project.js.map +1 -1
  43. package/dist/containers/template.js +1 -1
  44. package/dist/containers/template.js.map +1 -1
  45. package/dist/interfaces/command-options.d.ts +1 -0
  46. package/dist/interfaces/resource-interfaces.d.ts +5 -12
  47. package/dist/interfaces/resource-interfaces.js.map +1 -1
  48. package/dist/macros/base-macro.js +1 -1
  49. package/dist/macros/base-macro.js.map +1 -1
  50. package/dist/macros/graph/index.js +3 -1
  51. package/dist/macros/graph/index.js.map +1 -1
  52. package/dist/macros/index.js +3 -1
  53. package/dist/macros/index.js.map +1 -1
  54. package/dist/macros/report/index.js +1 -1
  55. package/dist/macros/report/index.js.map +1 -1
  56. package/dist/module-manager.js +5 -3
  57. package/dist/module-manager.js.map +1 -1
  58. package/dist/project-settings.js +2 -2
  59. package/dist/project-settings.js.map +1 -1
  60. package/dist/resources/card-type-resource.js +1 -1
  61. package/dist/resources/card-type-resource.js.map +1 -1
  62. package/dist/resources/create-defaults.js +0 -1
  63. package/dist/resources/create-defaults.js.map +1 -1
  64. package/dist/resources/field-type-resource.js +2 -5
  65. package/dist/resources/field-type-resource.js.map +1 -1
  66. package/dist/resources/file-resource.js +4 -1
  67. package/dist/resources/file-resource.js.map +1 -1
  68. package/dist/resources/folder-resource.d.ts +1 -1
  69. package/dist/resources/folder-resource.js +4 -1
  70. package/dist/resources/folder-resource.js.map +1 -1
  71. package/dist/resources/graph-model-resource.d.ts +1 -8
  72. package/dist/resources/graph-model-resource.js +0 -14
  73. package/dist/resources/graph-model-resource.js.map +1 -1
  74. package/dist/resources/graph-view-resource.d.ts +1 -8
  75. package/dist/resources/graph-view-resource.js +0 -14
  76. package/dist/resources/graph-view-resource.js.map +1 -1
  77. package/dist/resources/link-type-resource.js +1 -1
  78. package/dist/resources/link-type-resource.js.map +1 -1
  79. package/dist/resources/report-resource.d.ts +1 -8
  80. package/dist/resources/report-resource.js +0 -14
  81. package/dist/resources/report-resource.js.map +1 -1
  82. package/dist/resources/resource-object.d.ts +11 -1
  83. package/dist/resources/resource-object.js +19 -2
  84. package/dist/resources/resource-object.js.map +1 -1
  85. package/dist/resources/template-resource.d.ts +1 -9
  86. package/dist/resources/template-resource.js +0 -15
  87. package/dist/resources/template-resource.js.map +1 -1
  88. package/dist/resources/workflow-resource.js +1 -1
  89. package/dist/resources/workflow-resource.js.map +1 -1
  90. package/dist/utils/card-utils.js +1 -1
  91. package/dist/utils/card-utils.js.map +1 -1
  92. package/dist/utils/commit-context.d.ts +23 -0
  93. package/dist/utils/commit-context.js +30 -0
  94. package/dist/utils/commit-context.js.map +1 -0
  95. package/dist/utils/file-utils.js +3 -1
  96. package/dist/utils/file-utils.js.map +1 -1
  97. package/dist/utils/git-manager.d.ts +29 -0
  98. package/dist/utils/git-manager.js +76 -0
  99. package/dist/utils/git-manager.js.map +1 -0
  100. package/dist/utils/handlebars-helpers.d.ts +22 -0
  101. package/dist/utils/handlebars-helpers.js +78 -0
  102. package/dist/utils/handlebars-helpers.js.map +1 -0
  103. package/dist/utils/json.js +6 -2
  104. package/dist/utils/json.js.map +1 -1
  105. package/dist/utils/log-utils.d.ts +7 -2
  106. package/dist/utils/log-utils.js +28 -3
  107. package/dist/utils/log-utils.js.map +1 -1
  108. package/dist/utils/report.d.ts +0 -19
  109. package/dist/utils/report.js +4 -67
  110. package/dist/utils/report.js.map +1 -1
  111. package/dist/utils/rw-lock.d.ts +71 -0
  112. package/dist/utils/rw-lock.js +220 -0
  113. package/dist/utils/rw-lock.js.map +1 -0
  114. package/dist/utils/user-preferences.js +3 -3
  115. package/dist/utils/user-preferences.js.map +1 -1
  116. package/package.json +5 -5
  117. package/src/command-handler.ts +14 -22
  118. package/src/command-manager.ts +43 -37
  119. package/src/commands/calculate.ts +8 -1
  120. package/src/commands/create.ts +24 -1
  121. package/src/commands/edit.ts +3 -0
  122. package/src/commands/export.ts +8 -2
  123. package/src/commands/fetch.ts +3 -0
  124. package/src/commands/import.ts +5 -0
  125. package/src/commands/migrate.ts +2 -0
  126. package/src/commands/move.ts +34 -0
  127. package/src/commands/remove.ts +24 -2
  128. package/src/commands/rename.ts +2 -0
  129. package/src/commands/show.ts +63 -34
  130. package/src/commands/transition.ts +2 -0
  131. package/src/commands/update.ts +9 -3
  132. package/src/commands/validate.ts +1 -1
  133. package/src/containers/project/calculation-engine.ts +0 -1
  134. package/src/containers/project/card-cache.ts +1 -0
  135. package/src/containers/project.ts +75 -1
  136. package/src/containers/template.ts +1 -1
  137. package/src/interfaces/command-options.ts +1 -0
  138. package/src/interfaces/resource-interfaces.ts +5 -12
  139. package/src/macros/base-macro.ts +1 -1
  140. package/src/macros/graph/index.ts +3 -0
  141. package/src/macros/index.ts +3 -1
  142. package/src/macros/report/index.ts +1 -0
  143. package/src/module-manager.ts +5 -2
  144. package/src/project-settings.ts +2 -1
  145. package/src/resources/card-type-resource.ts +1 -1
  146. package/src/resources/create-defaults.ts +0 -1
  147. package/src/resources/field-type-resource.ts +2 -4
  148. package/src/resources/file-resource.ts +3 -1
  149. package/src/resources/folder-resource.ts +7 -2
  150. package/src/resources/graph-model-resource.ts +1 -25
  151. package/src/resources/graph-view-resource.ts +1 -25
  152. package/src/resources/link-type-resource.ts +1 -1
  153. package/src/resources/report-resource.ts +1 -25
  154. package/src/resources/resource-object.ts +22 -1
  155. package/src/resources/template-resource.ts +0 -23
  156. package/src/resources/workflow-resource.ts +1 -1
  157. package/src/utils/card-utils.ts +1 -1
  158. package/src/utils/commit-context.ts +45 -0
  159. package/src/utils/file-utils.ts +3 -1
  160. package/src/utils/git-manager.ts +87 -0
  161. package/src/utils/handlebars-helpers.ts +95 -0
  162. package/src/utils/json.ts +6 -2
  163. package/src/utils/log-utils.ts +33 -4
  164. package/src/utils/report.ts +8 -78
  165. package/src/utils/rw-lock.ts +279 -0
  166. package/src/utils/user-preferences.ts +3 -0
@@ -18,6 +18,7 @@ import { ActionGuard } from '../permissions/action-guard.js';
18
18
  import { copyDir, deleteDir } from '../utils/file-utils.js';
19
19
  import type { Card } from '../interfaces/project-interfaces.js';
20
20
  import type { Project } from '../containers/project.js';
21
+ import { write } from '../utils/rw-lock.js';
21
22
  import {
22
23
  EMPTY_RANK,
23
24
  FIRST_RANK,
@@ -100,6 +101,7 @@ export class Move {
100
101
  * @param source source card to move
101
102
  * @param destination destination card where source card will be moved to; or to root
102
103
  */
104
+ @write((source, destination) => `Move card ${source} to ${destination}`)
103
105
  public async moveCard(source: string, destination: string) {
104
106
  if (source === ROOT) {
105
107
  throw new Error('Cannot move "root"');
@@ -188,6 +190,9 @@ export class Move {
188
190
  ? getRankAfter(lastChild.metadata.rank)
189
191
  : FIRST_RANK;
190
192
 
193
+ // Save old path before moving (needed to update descendant paths)
194
+ const oldPath = sourceCard.path;
195
+
191
196
  // First do the file operations, then update metadata
192
197
  await copyDir(sourceCard.path, destinationPath);
193
198
  await deleteDir(sourceCard.path);
@@ -199,8 +204,32 @@ export class Move {
199
204
  sourceCard.metadata.rank = rank;
200
205
  }
201
206
 
207
+ // Update attachment paths for the moved card
208
+ if (sourceCard.attachments && sourceCard.attachments.length > 0) {
209
+ for (const attachment of sourceCard.attachments) {
210
+ if (attachment.path.startsWith(oldPath)) {
211
+ attachment.path = attachment.path.replace(oldPath, destinationPath);
212
+ }
213
+ }
214
+ }
215
+
202
216
  // Handle cache update and persistence
203
217
  await this.project.updateCard(sourceCard);
218
+
219
+ // Update all descendant card paths in the cache to reflect the new filesystem location.
220
+ // This is critical: files have been moved on disk, but children's cached paths
221
+ // still point to the old location. Without this, operations on children
222
+ // (like edit or delete) would target non-existent paths, leaving orphaned files.
223
+ if (sourceCard.children && sourceCard.children.length > 0) {
224
+ for (const childKey of sourceCard.children) {
225
+ this.project.updateDescendantPathsAfterMove(
226
+ childKey,
227
+ oldPath,
228
+ destinationPath,
229
+ );
230
+ }
231
+ }
232
+
204
233
  const updatedCard: Card = {
205
234
  ...sourceCard,
206
235
  path: destinationPath,
@@ -238,6 +267,7 @@ export class Move {
238
267
  * @param cardKey card key
239
268
  * @param index to which position should card be ranked to
240
269
  */
270
+ @write((cardKey) => `Reorder card ${cardKey}`)
241
271
  public async rankByIndex(cardKey: string, index: number) {
242
272
  if (index < 0) {
243
273
  throw new Error(`Index must be greater than 0`);
@@ -272,6 +302,7 @@ export class Move {
272
302
  * @param cardKey Card to rank
273
303
  * @param beforeCardKey Card key after which the card will be ranked
274
304
  */
305
+ @write((cardKey) => `Reorder card ${cardKey}`)
275
306
  public async rankCard(cardKey: string, beforeCardKey: string) {
276
307
  const card = this.project.findCard(cardKey);
277
308
  const beforeCard = this.project.findCard(beforeCardKey);
@@ -325,6 +356,7 @@ export class Move {
325
356
  * Ranks card first.
326
357
  * @param cardKey card key
327
358
  */
359
+ @write((cardKey) => `Rank card ${cardKey} first`)
328
360
  public async rankFirst(cardKey: string) {
329
361
  const card = this.project.findCard(cardKey);
330
362
  const children = sortItems(
@@ -370,6 +402,7 @@ export class Move {
370
402
  * Rebalances the ranks of the children of a card.
371
403
  * @param parentCardKey parent card key
372
404
  */
405
+ @write((parentCardKey) => `Rebalance children of ${parentCardKey}`)
373
406
  public async rebalanceChildren(parentCardKey: string) {
374
407
  const parentCard = this.project.findCard(parentCardKey);
375
408
  if (!parentCard || !parentCard.children) {
@@ -384,6 +417,7 @@ export class Move {
384
417
  * Rebalances the ranks of the cards in the whole project, including templates
385
418
  * Can be used even if the ranks do not exist
386
419
  */
420
+ @write(() => 'Rebalance project')
387
421
  public async rebalanceProject() {
388
422
  const cards = this.project.showProjectCards();
389
423
 
@@ -13,16 +13,21 @@
13
13
 
14
14
  import { ActionGuard } from '../permissions/action-guard.js';
15
15
  import { isModuleCard } from '../utils/card-utils.js';
16
+ import { getChildLogger } from '../utils/log-utils.js';
16
17
  import { ModuleManager } from '../module-manager.js';
17
18
  import type { Fetch } from './fetch.js';
18
19
  import type { Project } from '../containers/project.js';
19
20
  import type { RemovableResourceTypes } from '../interfaces/project-interfaces.js';
21
+ import { write } from '../utils/rw-lock.js';
20
22
 
21
23
  /**
22
24
  * Remove command.
23
25
  */
24
26
  export class Remove {
25
27
  private moduleManager: ModuleManager;
28
+ private get logger() {
29
+ return getChildLogger({ module: 'remove' });
30
+ }
26
31
  /**
27
32
  * Creates a new instance of Remove command.
28
33
  * @param project Project instance to use
@@ -73,14 +78,30 @@ export class Remove {
73
78
  await actionGuard.checkPermission('delete', cardKey);
74
79
  }
75
80
 
76
- // If card is destination of a link, remove the link.
81
+ // Collect all card keys that will be deleted (the card itself and all descendants).
82
+ const cardsToDelete = new Set<string>();
83
+ const collectDescendants = (c: typeof card) => {
84
+ cardsToDelete.add(c.key);
85
+ for (const childKey of c.children) {
86
+ try {
87
+ const childCard = this.project.findCard(childKey);
88
+ collectDescendants(childCard);
89
+ } catch {
90
+ this.logger.debug({ childKey }, 'Child card not found, skipping');
91
+ }
92
+ }
93
+ };
94
+ collectDescendants(card);
95
+
96
+ // If any of the cards to be deleted is a destination of a link, remove the link.
77
97
  const allCards = this.project.cards(this.project.paths.cardRootFolder);
78
98
  const promiseContainer: Promise<void>[] = [];
79
99
 
80
100
  for (const item of allCards) {
101
+ if (cardsToDelete.has(item.key)) continue;
81
102
  const links = item.metadata?.links ?? [];
82
103
  for (const link of links) {
83
- if (link.cardKey === cardKey) {
104
+ if (cardsToDelete.has(link.cardKey)) {
84
105
  promiseContainer.push(this.removeLink(item.key, link.cardKey));
85
106
  }
86
107
  }
@@ -163,6 +184,7 @@ export class Remove {
163
184
  * when removing link, some of the mandatory parameters are missing, or
164
185
  * when trying to remove unknown type
165
186
  */
187
+ @write((type, targetName) => `Remove ${type} ${targetName}`)
166
188
  public async remove(
167
189
  type: RemovableResourceTypes,
168
190
  targetName: string,
@@ -24,6 +24,7 @@ import {
24
24
  import { isTemplateCard } from '../utils/card-utils.js';
25
25
  import { type Project, ResourcesFrom } from '../containers/project.js';
26
26
  import { resourceName } from '../utils/resource-utils.js';
27
+ import { write } from '../utils/rw-lock.js';
27
28
 
28
29
  const FILE_TYPES_WITH_PREFIX_REFERENCES = ['adoc', 'hbs', 'json', 'lp'];
29
30
 
@@ -186,6 +187,7 @@ export class Rename {
186
187
  * @throws if trying to rename with current name
187
188
  * @param to Card id, or template name
188
189
  */
190
+ @write((to) => `Rename project prefix to ${to}`)
189
191
  public async rename(to: string) {
190
192
  if (!to) {
191
193
  throw new Error(`Input validation error: empty 'to' is not allowed`);
@@ -48,6 +48,7 @@ import type { ResourceName } from '../utils/resource-utils.js';
48
48
  import type { ResourceMap } from '../containers/project/resource-cache.js';
49
49
 
50
50
  import { UserPreferences } from '../utils/user-preferences.js';
51
+ import { read, write } from '../utils/rw-lock.js';
51
52
  import ReportMacro from '../macros/report/index.js';
52
53
  import TaskQueue from '../macros/task-queue.js';
53
54
  import { evaluateMacros } from '../macros/index.js';
@@ -146,10 +147,13 @@ export class Show {
146
147
  * Shows all template cards in a project.
147
148
  * @returns all template cards in a project.
148
149
  */
149
- public showAllTemplateCards(): {
150
- name: string;
151
- cards: CardWithChildrenCards[];
152
- }[] {
150
+ @read
151
+ public async showAllTemplateCards(): Promise<
152
+ {
153
+ name: string;
154
+ cards: CardWithChildrenCards[];
155
+ }[]
156
+ > {
153
157
  return this.project.resources.templates().map((template) => {
154
158
  const cards = template.templateObject().listCards();
155
159
  const buildCards = buildCardHierarchy(cards);
@@ -165,6 +169,7 @@ export class Show {
165
169
  * Shows all attachments (either template or project attachments) from a project.
166
170
  * @returns array of card attachments
167
171
  */
172
+ @read
168
173
  public async showAttachments(): Promise<CardAttachment[]> {
169
174
  const attachments = this.project.attachments();
170
175
  const templateAttachments = await this.attachmentsFromTemplates();
@@ -178,7 +183,11 @@ export class Show {
178
183
  * @param filename attachment filename
179
184
  * @returns attachment details
180
185
  */
181
- public showAttachment(cardKey: string, filename: string): attachmentPayload {
186
+ @read
187
+ public async showAttachment(
188
+ cardKey: string,
189
+ filename: string,
190
+ ): Promise<attachmentPayload> {
182
191
  if (!cardKey) {
183
192
  throw new Error(`Mandatory parameter 'cardKey' missing`);
184
193
  }
@@ -203,6 +212,7 @@ export class Show {
203
212
  * @param waitDelay amount of time to wait for the application to open the attachment
204
213
  * @todo: Move away from Show.
205
214
  */
215
+ @read
206
216
  public async openAttachment(
207
217
  cardKey: string,
208
218
  filename: string,
@@ -259,10 +269,11 @@ export class Show {
259
269
  * @param contentType Content format in which content is to be shown
260
270
  * @returns card details
261
271
  */
262
- public showCardDetails(
272
+ @read
273
+ public async showCardDetails(
263
274
  cardKey?: string,
264
275
  contentType?: FileContentType,
265
- ): Card {
276
+ ): Promise<Card> {
266
277
  if (!cardKey) {
267
278
  throw new Error(`Mandatory parameter 'cardKey' missing`);
268
279
  }
@@ -285,6 +296,7 @@ export class Show {
285
296
  * @param cardsFrom - The location from which to look for cards. Either from the project, templates or both.
286
297
  * @returns cards list array
287
298
  */
299
+ @read
288
300
  public async showCards(
289
301
  cardsFrom?: CardLocation,
290
302
  ): Promise<CardListContainer[]> {
@@ -296,6 +308,7 @@ export class Show {
296
308
  * @param cardKey The key of the card.
297
309
  * @returns the content of the logic program.
298
310
  */
311
+ @read
299
312
  public async showCardLogicProgram(cardKey: string) {
300
313
  return this.project.calculationEngine.cardLogicProgram(cardKey);
301
314
  }
@@ -304,6 +317,7 @@ export class Show {
304
317
  * Shows all card types in a project.
305
318
  * @returns array of card type details
306
319
  */
320
+ @read
307
321
  public async showCardTypesWithDetails(): Promise<(CardType | undefined)[]> {
308
322
  const container = [];
309
323
  for (const cardType of this.project.resources.cardTypes()) {
@@ -324,6 +338,7 @@ export class Show {
324
338
  * with 'showAll' true, the list consists of all modules in the hubs, even if they have already been imported
325
339
  * Note that the two boolean options can be combined.
326
340
  */
341
+ @write()
327
342
  public async showImportableModules(
328
343
  showAll?: boolean,
329
344
  showDetails?: boolean,
@@ -368,7 +383,8 @@ export class Show {
368
383
  * Returns all unique labels in a project
369
384
  * @returns labels in a list
370
385
  */
371
- public showLabels(): string[] {
386
+ @read
387
+ public async showLabels(): Promise<string[]> {
372
388
  const cards = flattenCardArray(
373
389
  this.project.showProjectCards(),
374
390
  this.project,
@@ -384,6 +400,7 @@ export class Show {
384
400
  * @param resource Name of the resource.
385
401
  * @returns the content of the logic program.
386
402
  */
403
+ @read
387
404
  public async showLogicProgram(resource: ResourceName) {
388
405
  return this.project.calculationEngine.resourceLogicProgram(resource);
389
406
  }
@@ -393,6 +410,7 @@ export class Show {
393
410
  * @param moduleName name of a module
394
411
  * @returns details of a module.
395
412
  */
413
+ @read
396
414
  public async showModule(moduleName: string): Promise<ModuleContent> {
397
415
  const moduleDetails = await this.project.module(moduleName);
398
416
  if (!moduleDetails) {
@@ -405,6 +423,7 @@ export class Show {
405
423
  * Shows hubs of the project.
406
424
  * @returns list of hubs.
407
425
  */
426
+ @write()
408
427
  public async showHubs(): Promise<HubSetting[]> {
409
428
  // Ensure module list is up to date before showing
410
429
  await this.fetchCmd.ensureModuleListUpToDate();
@@ -415,7 +434,8 @@ export class Show {
415
434
  * Returns all project cards in the project. Cards don't have content and nor metadata.
416
435
  * @returns array of cards
417
436
  */
418
- public showProjectCards(): Card[] {
437
+ @read
438
+ public async showProjectCards(): Promise<Card[]> {
419
439
  return this.project.showProjectCards();
420
440
  }
421
441
 
@@ -423,7 +443,8 @@ export class Show {
423
443
  * Shows all modules (if any) in a project.
424
444
  * @returns all modules in a project.
425
445
  */
426
- public showModules(): string[] {
446
+ @read
447
+ public async showModules(): Promise<string[]> {
427
448
  return this.project.resources.moduleNames().sort();
428
449
  }
429
450
 
@@ -431,6 +452,7 @@ export class Show {
431
452
  * Shows details of a particular project.
432
453
  * @returns project information
433
454
  */
455
+ @read
434
456
  public async showProject(): Promise<ProjectMetadata> {
435
457
  return this.project.show();
436
458
  }
@@ -445,6 +467,7 @@ export class Show {
445
467
  * @returns Report results as a string
446
468
  * @throws Error if the report does not exist
447
469
  */
470
+ @read
448
471
  public async showReportResults(
449
472
  reportName: string,
450
473
  cardKey: string,
@@ -487,6 +510,7 @@ export class Show {
487
510
  if (error instanceof Error) {
488
511
  throw new Error(
489
512
  `Failed to write report to ${outputPath}: ${error.message}`,
513
+ { cause: error },
490
514
  );
491
515
  }
492
516
  }
@@ -515,29 +539,31 @@ export class Show {
515
539
  arg2?: boolean | ResourceType,
516
540
  arg3?: boolean,
517
541
  ): Promise<AnyResourceContent> {
518
- const hasResourceType = typeof arg2 === 'string';
519
- const resourceType = hasResourceType ? arg2 : null;
520
- const showUse = hasResourceType ? arg3 : arg2;
521
-
522
- const type = this.project.resources.extractType(name);
523
- if (resourceType !== null && resourceType !== type) {
524
- throw new Error(
525
- `While fetching '${name}': Expected type '${resourceType}', but got '${type}' instead`,
526
- );
527
- }
528
- const resource = this.project.resources.byType(name, type);
529
- const [details, usage] = await Promise.all([
530
- resource?.show(),
531
- showUse ? resource?.usage() : [],
532
- ]);
533
- if (showUse) {
534
- return {
535
- ...details,
536
- usedIn: [...usage],
537
- };
538
- } else {
539
- return details;
540
- }
542
+ return this.project.lock.read(async () => {
543
+ const hasResourceType = typeof arg2 === 'string';
544
+ const resourceType = hasResourceType ? arg2 : null;
545
+ const showUse = hasResourceType ? arg3 : arg2;
546
+
547
+ const type = this.project.resources.extractType(name);
548
+ if (resourceType !== null && resourceType !== type) {
549
+ throw new Error(
550
+ `While fetching '${name}': Expected type '${resourceType}', but got '${type}' instead`,
551
+ );
552
+ }
553
+ const resource = this.project.resources.byType(name, type);
554
+ const [details, usage] = await Promise.all([
555
+ resource?.show(),
556
+ showUse ? resource?.usage() : [],
557
+ ]);
558
+ if (showUse) {
559
+ return {
560
+ ...details,
561
+ usedIn: [...usage],
562
+ };
563
+ } else {
564
+ return details;
565
+ }
566
+ });
541
567
  }
542
568
 
543
569
  /**
@@ -545,6 +571,7 @@ export class Show {
545
571
  * @param type Name of resources to return (in plural form, e.g. 'templates')
546
572
  * @returns sorted array of resources
547
573
  */
574
+ @read
548
575
  public async showResources(type: string): Promise<string[]> {
549
576
  const func = this.resourceFunctions[type];
550
577
  if (!func) return [];
@@ -555,6 +582,7 @@ export class Show {
555
582
  * Shows all templates with full details in a project.
556
583
  * @returns all templates in a project.
557
584
  */
585
+ @read
558
586
  public async showTemplatesWithDetails(): Promise<TemplateConfiguration[]> {
559
587
  const templates = [];
560
588
  for (const template of this.project.resources.templates()) {
@@ -567,7 +595,8 @@ export class Show {
567
595
  * Shows all workflows with full details in a project.
568
596
  * @returns workflows with full details
569
597
  */
570
- public showWorkflowsWithDetails(): (Workflow | undefined)[] {
598
+ @read
599
+ public async showWorkflowsWithDetails(): Promise<(Workflow | undefined)[]> {
571
600
  const workflows = [];
572
601
  for (const workflow of this.project.resources.workflows()) {
573
602
  workflows.push(workflow.data);
@@ -15,6 +15,7 @@ import { ActionGuard } from '../permissions/action-guard.js';
15
15
  import { CardMetadataUpdater } from '../card-metadata-updater.js';
16
16
  import type { Project } from '../containers/project.js';
17
17
  import type { WorkflowState } from '../interfaces/resource-interfaces.js';
18
+ import { write } from '../utils/rw-lock.js';
18
19
 
19
20
  /**
20
21
  * Handles transitions.
@@ -40,6 +41,7 @@ export class Transition {
40
41
  * @param cardKey card key
41
42
  * @param transition which transition to do
42
43
  */
44
+ @write((cardKey) => `Transition card ${cardKey}`)
43
45
  public async cardTransition(cardKey: string, transition: WorkflowState) {
44
46
  const card = this.project.findCard(cardKey);
45
47
 
@@ -21,6 +21,7 @@ import type {
21
21
  } from '../resources/resource-object.js';
22
22
  import type { Project } from '../containers/project.js';
23
23
  import type { UpdateKey } from '../interfaces/resource-interfaces.js';
24
+ import { runWithDefaultCommitMessage } from '../utils/commit-context.js';
24
25
 
25
26
  /**
26
27
  * Class that handles 'update' commands.
@@ -47,9 +48,13 @@ export class Update {
47
48
  T extends UpdateOperations,
48
49
  K extends string,
49
50
  >(name: string, updateKey: UpdateKey<K>, operation: OperationFor<Type, T>) {
50
- const type = this.project.resources.extractType(name);
51
- const resource = this.project.resources.byType(name, type);
52
- await resource?.update(updateKey, operation);
51
+ const run = () =>
52
+ this.project.lock.write(async () => {
53
+ const type = this.project.resources.extractType(name);
54
+ const resource = this.project.resources.byType(name, type);
55
+ await resource?.update(updateKey, operation);
56
+ });
57
+ return runWithDefaultCommitMessage('Apply resource operation', run);
53
58
  }
54
59
 
55
60
  /**
@@ -69,6 +74,7 @@ export class Update {
69
74
  optionalDetail?: Type, // todo: for 'rank' it might be reasonable to accept also 'number'
70
75
  mappingTable?: { stateMapping: Record<string, string> },
71
76
  ) {
77
+ // Safe to not have lock here, this is just a wrapper to applyResourceOperation
72
78
  const op: Operation<Type> = {
73
79
  name: operation,
74
80
  target: '' as Type,
@@ -240,7 +240,7 @@ export class Validate {
240
240
  const result = await Promise.all(promises);
241
241
  message.push(...result.flat(1));
242
242
  } catch (error) {
243
- throw new Error(errorFunction(error));
243
+ throw new Error(errorFunction(error), { cause: error });
244
244
  }
245
245
  return message;
246
246
  }
@@ -476,7 +476,6 @@ export class CalculationEngine {
476
476
  model: model,
477
477
  view: view,
478
478
  },
479
- graph: true,
480
479
  context,
481
480
  });
482
481
  let graph = (await instance()).renderString(result, {
@@ -174,6 +174,7 @@ export class CardCache {
174
174
  if (error instanceof Error) {
175
175
  throw new Error(
176
176
  `Invalid JSON in file '${metadataPath}': ${error.message}`,
177
+ { cause: error },
177
178
  );
178
179
  }
179
180
  throw error;
@@ -56,6 +56,9 @@ import { Validate } from '../commands/validate.js';
56
56
  import { ContentWatcher } from './project/project-content-watcher.js';
57
57
  import { getChildLogger } from '../utils/log-utils.js';
58
58
  import { MigrationExecutor } from '../migrations/migration-executor.js';
59
+ import { RWLock } from '../utils/rw-lock.js';
60
+ import { GitManager } from '../utils/git-manager.js';
61
+ import { getCommitContext } from '../utils/commit-context.js';
59
62
 
60
63
  import type { MigrationResult } from '@cyberismo/migrations';
61
64
  import type { Template } from './template.js';
@@ -78,13 +81,16 @@ export { ResourcesFrom };
78
81
  export interface ProjectOptions {
79
82
  autoSave?: boolean;
80
83
  watchResourceChanges?: boolean;
84
+ autocommit?: boolean;
81
85
  }
82
86
 
83
87
  /**
84
88
  * Represents project folder.
85
89
  */
86
90
  export class Project extends CardContainer {
91
+ public readonly lock = new RWLock();
87
92
  public calculationEngine: CalculationEngine;
93
+ private gitManager?: GitManager;
88
94
  private logger = getChildLogger({ module: 'Project' });
89
95
  private projectPaths: ProjectPaths;
90
96
  private resourceHandler: ResourceHandler;
@@ -140,6 +146,28 @@ export class Project extends CardContainer {
140
146
  },
141
147
  );
142
148
  }
149
+
150
+ if (this.options.autocommit) {
151
+ this.gitManager = new GitManager(path);
152
+
153
+ // Commit after successful writes
154
+ this.lock.onAfterWrite(async () => {
155
+ const context = getCommitContext();
156
+ await this.gitManager!.commit(
157
+ context.message ?? 'Autocommit',
158
+ context.author,
159
+ );
160
+ });
161
+
162
+ // Rollback on failed writes
163
+ this.lock.onWriteError(async () => {
164
+ await this.gitManager!.rollback();
165
+ // Invalidate caches after rollback since filesystem state changed
166
+ this.cardCache.clear();
167
+ await this.populateCardsCache();
168
+ this.resources.changed();
169
+ });
170
+ }
143
171
  }
144
172
 
145
173
  // Changes a card's parent in the cache and updates all relationships.
@@ -868,6 +896,15 @@ export class Project extends CardContainer {
868
896
  return this.projectPaths;
869
897
  }
870
898
 
899
+ /**
900
+ * Initialize git repo for autocommit mode. No-op if autocommit is disabled.
901
+ */
902
+ public async initializeGit(): Promise<void> {
903
+ if (this.gitManager) {
904
+ await this.gitManager.initialize(getCommitContext().author);
905
+ }
906
+ }
907
+
871
908
  /**
872
909
  * Populates the card cache, if it has not been populated.
873
910
  */
@@ -939,7 +976,7 @@ export class Project extends CardContainer {
939
976
  await unlink(attachmentPath);
940
977
  } catch (error) {
941
978
  this.logger.error({ error }, 'Removing card attachment');
942
- throw new Error(`Attachment not found: ${fileName}`);
979
+ throw new Error(`Attachment not found: ${fileName}`, { cause: error });
943
980
  }
944
981
  await this.handleAttachmentChange(cardKey, 'removed', fileName);
945
982
  }
@@ -1134,6 +1171,43 @@ export class Project extends CardContainer {
1134
1171
  }
1135
1172
  }
1136
1173
 
1174
+ /**
1175
+ * Updates descendant card paths in the cache after a parent card has been moved.
1176
+ * This ensures cached paths reflect the actual filesystem locations.
1177
+ * @param cardKey The card whose descendants need path updates
1178
+ * @param oldBasePath The old base path before the move
1179
+ * @param newBasePath The new base path after the move
1180
+ */
1181
+ public updateDescendantPathsAfterMove(
1182
+ cardKey: string,
1183
+ oldBasePath: string,
1184
+ newBasePath: string,
1185
+ ): void {
1186
+ const card = this.cardCache.getCard(cardKey);
1187
+ if (!card) return;
1188
+
1189
+ if (card.path.startsWith(oldBasePath)) {
1190
+ card.path = card.path.replace(oldBasePath, newBasePath);
1191
+
1192
+ if (card.attachments && card.attachments.length > 0) {
1193
+ for (const attachment of card.attachments) {
1194
+ if (attachment.path.startsWith(oldBasePath)) {
1195
+ attachment.path = attachment.path.replace(oldBasePath, newBasePath);
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ this.cardCache.updateCard(card.key, card);
1201
+ }
1202
+
1203
+ // Recursively update children
1204
+ if (card.children && card.children.length > 0) {
1205
+ for (const childKey of card.children) {
1206
+ this.updateDescendantPathsAfterMove(childKey, oldBasePath, newBasePath);
1207
+ }
1208
+ }
1209
+ }
1210
+
1137
1211
  /**
1138
1212
  * Updates the entire card in the card cache and handles any path/parent changes.
1139
1213
  * Also persists changes to content and metadata files.
@@ -399,7 +399,7 @@ export class Template extends CardContainer {
399
399
  const destinationCardPath = parentCard
400
400
  ? join(this.cardFolder(parentCard.key), 'c')
401
401
  : this.templateCardsPath;
402
- let newCardKey = '';
402
+ let newCardKey: string;
403
403
 
404
404
  try {
405
405
  // todo: to use cache instead of file access
@@ -92,6 +92,7 @@ export interface ShowCommandOptions extends BaseCommandOptions {
92
92
  export interface StartCommandOptions extends BaseCommandOptions {
93
93
  forceStart?: boolean;
94
94
  watchResourceChanges?: boolean;
95
+ autocommit?: boolean;
95
96
  }
96
97
 
97
98
  // Options for 'transition' command