@cyberismo/data-handler 0.0.20 → 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 (177) 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 +420 -320
  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/include/index.js +16 -1
  53. package/dist/macros/include/index.js.map +1 -1
  54. package/dist/macros/include/types.d.ts +15 -12
  55. package/dist/macros/index.js +4 -1
  56. package/dist/macros/index.js.map +1 -1
  57. package/dist/macros/report/index.js +1 -1
  58. package/dist/macros/report/index.js.map +1 -1
  59. package/dist/module-manager.js +5 -3
  60. package/dist/module-manager.js.map +1 -1
  61. package/dist/project-settings.js +2 -2
  62. package/dist/project-settings.js.map +1 -1
  63. package/dist/resources/card-type-resource.js +1 -1
  64. package/dist/resources/card-type-resource.js.map +1 -1
  65. package/dist/resources/create-defaults.js +0 -1
  66. package/dist/resources/create-defaults.js.map +1 -1
  67. package/dist/resources/field-type-resource.js +2 -5
  68. package/dist/resources/field-type-resource.js.map +1 -1
  69. package/dist/resources/file-resource.js +4 -1
  70. package/dist/resources/file-resource.js.map +1 -1
  71. package/dist/resources/folder-resource.d.ts +1 -1
  72. package/dist/resources/folder-resource.js +4 -1
  73. package/dist/resources/folder-resource.js.map +1 -1
  74. package/dist/resources/graph-model-resource.d.ts +1 -8
  75. package/dist/resources/graph-model-resource.js +0 -14
  76. package/dist/resources/graph-model-resource.js.map +1 -1
  77. package/dist/resources/graph-view-resource.d.ts +1 -8
  78. package/dist/resources/graph-view-resource.js +0 -14
  79. package/dist/resources/graph-view-resource.js.map +1 -1
  80. package/dist/resources/link-type-resource.js +1 -1
  81. package/dist/resources/link-type-resource.js.map +1 -1
  82. package/dist/resources/report-resource.d.ts +1 -8
  83. package/dist/resources/report-resource.js +0 -14
  84. package/dist/resources/report-resource.js.map +1 -1
  85. package/dist/resources/resource-object.d.ts +11 -1
  86. package/dist/resources/resource-object.js +19 -2
  87. package/dist/resources/resource-object.js.map +1 -1
  88. package/dist/resources/template-resource.d.ts +1 -9
  89. package/dist/resources/template-resource.js +0 -15
  90. package/dist/resources/template-resource.js.map +1 -1
  91. package/dist/resources/workflow-resource.d.ts +6 -0
  92. package/dist/resources/workflow-resource.js +29 -13
  93. package/dist/resources/workflow-resource.js.map +1 -1
  94. package/dist/utils/card-utils.js +1 -1
  95. package/dist/utils/card-utils.js.map +1 -1
  96. package/dist/utils/commit-context.d.ts +23 -0
  97. package/dist/utils/commit-context.js +30 -0
  98. package/dist/utils/commit-context.js.map +1 -0
  99. package/dist/utils/csv.d.ts +8 -0
  100. package/dist/utils/csv.js +11 -0
  101. package/dist/utils/csv.js.map +1 -1
  102. package/dist/utils/file-utils.js +3 -1
  103. package/dist/utils/file-utils.js.map +1 -1
  104. package/dist/utils/git-manager.d.ts +29 -0
  105. package/dist/utils/git-manager.js +76 -0
  106. package/dist/utils/git-manager.js.map +1 -0
  107. package/dist/utils/handlebars-helpers.d.ts +22 -0
  108. package/dist/utils/handlebars-helpers.js +78 -0
  109. package/dist/utils/handlebars-helpers.js.map +1 -0
  110. package/dist/utils/json.d.ts +17 -10
  111. package/dist/utils/json.js +27 -14
  112. package/dist/utils/json.js.map +1 -1
  113. package/dist/utils/log-utils.d.ts +7 -2
  114. package/dist/utils/log-utils.js +28 -3
  115. package/dist/utils/log-utils.js.map +1 -1
  116. package/dist/utils/report.d.ts +0 -19
  117. package/dist/utils/report.js +4 -63
  118. package/dist/utils/report.js.map +1 -1
  119. package/dist/utils/rw-lock.d.ts +71 -0
  120. package/dist/utils/rw-lock.js +220 -0
  121. package/dist/utils/rw-lock.js.map +1 -0
  122. package/dist/utils/user-preferences.js +3 -3
  123. package/dist/utils/user-preferences.js.map +1 -1
  124. package/package.json +10 -10
  125. package/src/command-handler.ts +14 -22
  126. package/src/command-manager.ts +43 -37
  127. package/src/commands/calculate.ts +8 -1
  128. package/src/commands/create.ts +39 -6
  129. package/src/commands/edit.ts +3 -0
  130. package/src/commands/export.ts +8 -2
  131. package/src/commands/fetch.ts +3 -0
  132. package/src/commands/import.ts +5 -0
  133. package/src/commands/migrate.ts +2 -0
  134. package/src/commands/move.ts +34 -0
  135. package/src/commands/remove.ts +24 -2
  136. package/src/commands/rename.ts +2 -0
  137. package/src/commands/show.ts +63 -34
  138. package/src/commands/transition.ts +2 -0
  139. package/src/commands/update.ts +9 -3
  140. package/src/commands/validate.ts +1 -1
  141. package/src/containers/project/calculation-engine.ts +0 -1
  142. package/src/containers/project/card-cache.ts +1 -0
  143. package/src/containers/project.ts +75 -1
  144. package/src/containers/template.ts +1 -1
  145. package/src/interfaces/command-options.ts +1 -0
  146. package/src/interfaces/resource-interfaces.ts +5 -12
  147. package/src/macros/base-macro.ts +1 -1
  148. package/src/macros/graph/index.ts +3 -0
  149. package/src/macros/include/index.ts +19 -1
  150. package/src/macros/include/types.ts +15 -12
  151. package/src/macros/index.ts +4 -1
  152. package/src/macros/report/index.ts +1 -0
  153. package/src/module-manager.ts +5 -2
  154. package/src/project-settings.ts +2 -1
  155. package/src/resources/card-type-resource.ts +1 -1
  156. package/src/resources/create-defaults.ts +0 -1
  157. package/src/resources/field-type-resource.ts +2 -4
  158. package/src/resources/file-resource.ts +3 -1
  159. package/src/resources/folder-resource.ts +7 -2
  160. package/src/resources/graph-model-resource.ts +1 -25
  161. package/src/resources/graph-view-resource.ts +1 -25
  162. package/src/resources/link-type-resource.ts +1 -1
  163. package/src/resources/report-resource.ts +1 -25
  164. package/src/resources/resource-object.ts +22 -1
  165. package/src/resources/template-resource.ts +0 -23
  166. package/src/resources/workflow-resource.ts +45 -16
  167. package/src/utils/card-utils.ts +1 -1
  168. package/src/utils/commit-context.ts +45 -0
  169. package/src/utils/csv.ts +12 -0
  170. package/src/utils/file-utils.ts +3 -1
  171. package/src/utils/git-manager.ts +87 -0
  172. package/src/utils/handlebars-helpers.ts +95 -0
  173. package/src/utils/json.ts +29 -15
  174. package/src/utils/log-utils.ts +33 -4
  175. package/src/utils/report.ts +8 -74
  176. package/src/utils/rw-lock.ts +279 -0
  177. package/src/utils/user-preferences.ts +3 -0
@@ -157,6 +157,7 @@ export class ModuleManager {
157
157
  if (error instanceof Error)
158
158
  throw new Error(
159
159
  `Failed to clone module '${module.name}': ${error.message}`,
160
+ { cause: error },
160
161
  );
161
162
  }
162
163
 
@@ -245,7 +246,7 @@ export class ModuleManager {
245
246
  module: ModuleSetting,
246
247
  credentials?: Credentials,
247
248
  ) {
248
- let moduleRoot = '';
249
+ let moduleRoot: string;
249
250
  if (this.isFileModule(module)) {
250
251
  const urlStart = FILE_PROTOCOL.length;
251
252
  // Remove 'file:' from location
@@ -350,7 +351,9 @@ export class ModuleManager {
350
351
  });
351
352
  }
352
353
  } catch (error) {
353
- throw new Error(`Failed to prepare temporary directory: ${error}`);
354
+ throw new Error(`Failed to prepare temporary directory: ${error}`, {
355
+ cause: error,
356
+ });
354
357
  }
355
358
  }
356
359
 
@@ -150,6 +150,7 @@ export class ProjectConfiguration implements ProjectSettings {
150
150
  if (error instanceof TypeError) {
151
151
  throw new Error(
152
152
  `Invalid hub URL '${trimmedHub}'. Please provide a valid HTTP or HTTPS URL.`,
153
+ { cause: error },
153
154
  );
154
155
  }
155
156
  throw error;
@@ -195,7 +196,7 @@ export class ProjectConfiguration implements ProjectSettings {
195
196
  if (this.schemaVersion < SCHEMA_VERSION) {
196
197
  return {
197
198
  isCompatible: false,
198
- message: `Schema version mismatch: Project schema version (${this.schemaVersion}) is older than the application schema version (${SCHEMA_VERSION}). A migration is needed.`,
199
+ message: `Schema version mismatch: Project schema version (${this.schemaVersion}) is older than the application schema version (${SCHEMA_VERSION}). A migration is needed. Run 'cyberismo migrate' to update the project schema.`,
199
200
  };
200
201
  }
201
202
 
@@ -448,7 +448,7 @@ export class CardTypeResource extends FileResource<CardType> {
448
448
  ) {
449
449
  const { key } = updateKey;
450
450
 
451
- if (key === 'name' || key === 'description' || key === 'displayName') {
451
+ if (this.isBaseProperty(key)) {
452
452
  await super.update(updateKey, op);
453
453
  } else {
454
454
  const content = structuredClone(this.content);
@@ -184,7 +184,6 @@ export abstract class DefaultContent {
184
184
  return {
185
185
  name: reportName,
186
186
  displayName: '',
187
- category: 'Uncategorised report',
188
187
  };
189
188
  }
190
189
 
@@ -408,16 +408,14 @@ export class FieldTypeResource extends FileResource<FieldType> {
408
408
  ) {
409
409
  const { key } = updateKey;
410
410
 
411
- if (key === 'name' || key === 'description' || key === 'displayName') {
411
+ if (this.isBaseProperty(key)) {
412
412
  await super.update(updateKey, op);
413
413
  } else {
414
414
  const content = structuredClone(this.content);
415
415
  const typeChange = key === 'dataType';
416
416
  const enumChange = key === 'enumValues';
417
417
  const existingType = this.content.dataType;
418
- if (key === 'name') {
419
- content.name = super.handleScalar(op) as string;
420
- } else if (key === 'dataType') {
418
+ if (key === 'dataType') {
421
419
  const toType = op as ChangeOperation<DataType>;
422
420
  if (!FieldTypeResource.fieldDataTypes().includes(toType.to)) {
423
421
  throw new Error(
@@ -118,8 +118,10 @@ export abstract class FileResource<
118
118
  content.displayName = super.handleScalar(op) as string;
119
119
  } else if (key === 'description') {
120
120
  content.description = super.handleScalar(op) as string;
121
+ } else if (key === 'category') {
122
+ content.category = super.handleScalar(op) as string;
121
123
  } else {
122
- throw new Error(`Unknown property '${key}' for folder resource`);
124
+ throw new Error(`Unknown property '${key}' for file resource`);
123
125
  }
124
126
 
125
127
  await super.postUpdate(content, updateKey, op);
@@ -62,7 +62,7 @@ export abstract class FolderResource<
62
62
  * Creates a new folder type object. Base class writes the object to disk automatically.
63
63
  * @param newContent Content for the type.
64
64
  */
65
- protected async create(newContent?: T) {
65
+ public async create(newContent?: T) {
66
66
  // Validate resource identifier before creating on disk
67
67
  this.validateResourceIdentifier();
68
68
  await super.create(newContent);
@@ -154,7 +154,10 @@ export abstract class FolderResource<
154
154
  } catch (error) {
155
155
  const message =
156
156
  error instanceof Error ? error.message : 'Unknown error';
157
- throw new Error(`Invalid JSON content for '${key}' update: ${message}`);
157
+ throw new Error(
158
+ `Invalid JSON content for '${key}' update: ${message}`,
159
+ { cause: error },
160
+ );
158
161
  }
159
162
  }
160
163
  const contentToWrite = isJson
@@ -252,6 +255,8 @@ export abstract class FolderResource<
252
255
  content.displayName = super.handleScalar(op) as string;
253
256
  } else if (key === 'description') {
254
257
  content.description = super.handleScalar(op) as string;
258
+ } else if (key === 'category') {
259
+ content.category = super.handleScalar(op) as string;
255
260
  } else {
256
261
  throw new Error(`Unknown property '${key}' for folder resource`);
257
262
  }
@@ -22,12 +22,8 @@ import { writeFileSafe } from '../utils/file-utils.js';
22
22
  import { CONTENT_FILES } from '../interfaces/folder-content-interfaces.js';
23
23
 
24
24
  import type { Card } from '../interfaces/project-interfaces.js';
25
- import type {
26
- GraphModelMetadata,
27
- UpdateKey,
28
- } from '../interfaces/resource-interfaces.js';
25
+ import type { GraphModelMetadata } from '../interfaces/resource-interfaces.js';
29
26
  import type { GraphModelContent } from '../interfaces/folder-content-interfaces.js';
30
- import type { Operation } from './resource-object.js';
31
27
  import type { Project } from '../containers/project.js';
32
28
  import type { ResourceName } from '../utils/resource-utils.js';
33
29
 
@@ -103,26 +99,6 @@ export class GraphModelResource extends FolderResource<
103
99
  return this.onNameChange(existingName);
104
100
  }
105
101
 
106
- /**
107
- * Updates graph model resource.
108
- * @param updateKey Key to modify
109
- * @param op Operation to perform on 'key'
110
- */
111
- public async update<Type, K extends string>(
112
- updateKey: UpdateKey<K>,
113
- op: Operation<Type>,
114
- ) {
115
- if (updateKey.key === 'category') {
116
- const content = structuredClone(this.content);
117
- content.category = super.handleScalar(op) as string;
118
-
119
- await super.postUpdate(content, updateKey, op);
120
- return;
121
- }
122
-
123
- await super.update(updateKey, op);
124
- }
125
-
126
102
  /**
127
103
  * List where this resource is used.
128
104
  * Always returns card key references first, then calculation references.
@@ -24,11 +24,7 @@ import { sortCards } from '../utils/card-utils.js';
24
24
 
25
25
  import type { Card } from '../interfaces/project-interfaces.js';
26
26
  import type { GraphViewContent } from '../interfaces/folder-content-interfaces.js';
27
- import type {
28
- GraphViewMetadata,
29
- UpdateKey,
30
- } from '../interfaces/resource-interfaces.js';
31
- import type { Operation } from './resource-object.js';
27
+ import type { GraphViewMetadata } from '../interfaces/resource-interfaces.js';
32
28
  import type { Project } from '../containers/project.js';
33
29
  import type { ResourceName } from '../utils/resource-utils.js';
34
30
 
@@ -115,26 +111,6 @@ export class GraphViewResource extends FolderResource<
115
111
  return this.onNameChange(existingName);
116
112
  }
117
113
 
118
- /**
119
- * Updates graph view resource.
120
- * @param updateKey Key to modify
121
- * @param op Operation to perform on 'key'
122
- */
123
- public async update<Type, K extends string>(
124
- updateKey: UpdateKey<K>,
125
- op: Operation<Type>,
126
- ) {
127
- if (updateKey.key === 'category') {
128
- const content = structuredClone(this.content) as GraphViewMetadata;
129
- content.category = super.handleScalar(op) as string;
130
-
131
- await super.postUpdate(content, updateKey, op);
132
- return;
133
- }
134
-
135
- await super.update(updateKey, op);
136
- }
137
-
138
114
  /**
139
115
  * List where this resource is used.
140
116
  * Always returns card key references first, then calculation references.
@@ -127,7 +127,7 @@ export class LinkTypeResource extends FileResource<LinkType> {
127
127
  ) {
128
128
  const { key } = updateKey;
129
129
 
130
- if (key === 'name' || key === 'displayName' || key === 'description') {
130
+ if (this.isBaseProperty(key)) {
131
131
  await super.update(updateKey, op);
132
132
  } else {
133
133
  const content = structuredClone(this.content);
@@ -24,13 +24,9 @@ import { sortCards } from '../utils/card-utils.js';
24
24
  import { Validate } from '../commands/validate.js';
25
25
 
26
26
  import type { Card } from '../interfaces/project-interfaces.js';
27
- import type { Operation } from './resource-object.js';
28
27
  import type { Project } from '../containers/project.js';
29
28
  import type { ReportContent } from '../interfaces/folder-content-interfaces.js';
30
- import type {
31
- ReportMetadata,
32
- UpdateKey,
33
- } from '../interfaces/resource-interfaces.js';
29
+ import type { ReportMetadata } from '../interfaces/resource-interfaces.js';
34
30
  import type { ResourceName } from '../utils/resource-utils.js';
35
31
 
36
32
  const PARAMETER_SCHEMA_ID = 'jsonSchema';
@@ -120,26 +116,6 @@ export class ReportResource extends FolderResource<
120
116
  return this.onNameChange(existingName);
121
117
  }
122
118
 
123
- /**
124
- * Updates report resource.
125
- * @param updateKey Key to modify
126
- * @param op Operation to perform on 'key'
127
- */
128
- public async update<Type, K extends string>(
129
- updateKey: UpdateKey<K>,
130
- op: Operation<Type>,
131
- ) {
132
- if (updateKey.key === 'category') {
133
- const content = structuredClone(this.content);
134
- content.category = super.handleScalar(op) as string;
135
-
136
- await super.postUpdate(content, updateKey, op);
137
- return;
138
- }
139
-
140
- await super.update(updateKey, op);
141
- }
142
-
143
119
  /**
144
120
  * List where this resource is used.
145
121
  * Always returns card key references first, then calculation references.
@@ -270,7 +270,7 @@ export abstract class ResourceObject<
270
270
  * @param newContent Content for resource.
271
271
  * @throws when resource already exists in the project.
272
272
  */
273
- protected async create(newContent?: T) {
273
+ public async create(newContent?: T) {
274
274
  this.validateResourceIdentifier();
275
275
 
276
276
  if (this.exists()) {
@@ -357,6 +357,7 @@ export abstract class ResourceObject<
357
357
  if (error instanceof Error) {
358
358
  throw new Error(
359
359
  `Cannot perform operation on '${arrayName}'. ${error.message}`,
360
+ { cause: error },
360
361
  );
361
362
  }
362
363
  }
@@ -507,6 +508,7 @@ export abstract class ResourceObject<
507
508
  const errorValue = typeof op === 'object' ? toValue(op) : op;
508
509
  throw new Error(
509
510
  `Cannot ${op.name} '${updateKey.key}' --> '${errorValue}: ${error.message}'`,
511
+ { cause: error },
510
512
  );
511
513
  }
512
514
  }
@@ -576,6 +578,25 @@ export abstract class ResourceObject<
576
578
  } as ChangeOperation<string>);
577
579
  }
578
580
 
581
+ /**
582
+ * Base properties shared by all resources.
583
+ */
584
+ private static readonly BASE_PROPERTIES = [
585
+ 'name',
586
+ 'displayName',
587
+ 'description',
588
+ 'category',
589
+ ] as const;
590
+
591
+ /**
592
+ * Checks if the given key is a base property shared by all resources.
593
+ * @param key The property key to check
594
+ * @returns true if the key is a base property
595
+ */
596
+ protected isBaseProperty(key: string): boolean {
597
+ return (ResourceObject.BASE_PROPERTIES as readonly string[]).includes(key);
598
+ }
599
+
579
600
  /**
580
601
  * Update resource; the base class makes some checks only.
581
602
  * @template type Resource type
@@ -22,13 +22,11 @@ import { Template } from '../containers/template.js';
22
22
  import { writeJsonFile } from '../utils/json.js';
23
23
 
24
24
  import type { Card } from '../interfaces/project-interfaces.js';
25
- import type { Operation } from './resource-object.js';
26
25
  import type { Project } from '../containers/project.js';
27
26
  import type { ResourceName } from '../utils/resource-utils.js';
28
27
  import type {
29
28
  TemplateConfiguration,
30
29
  TemplateMetadata,
31
- UpdateKey,
32
30
  } from '../interfaces/resource-interfaces.js';
33
31
 
34
32
  /**
@@ -135,27 +133,6 @@ export class TemplateResource extends FolderResource<TemplateMetadata, never> {
135
133
  return this.cardContainer;
136
134
  }
137
135
 
138
- /**
139
- * Updates template resource.
140
- * @param updateKey Key to modify
141
- * @param op Operation to perform on 'key'
142
- * @throws if key is unknown.
143
- */
144
- public async update<Type, K extends string>(
145
- updateKey: UpdateKey<K>,
146
- op: Operation<Type>,
147
- ) {
148
- if (updateKey.key === 'category') {
149
- const content = structuredClone(this.content);
150
- content.category = super.handleScalar(op) as string;
151
-
152
- await super.postUpdate(content, updateKey, op);
153
- return;
154
- }
155
-
156
- await super.update(updateKey, op);
157
- }
158
-
159
136
  /**
160
137
  * List where template is used.
161
138
  * Always returns card key references first, then calculation references.
@@ -78,16 +78,11 @@ export class WorkflowResource extends FileResource<Workflow> {
78
78
  }
79
79
 
80
80
  // Handle change of workflow state.
81
- private async handleStateChange(op: ChangeOperation<WorkflowState>) {
82
- const content = structuredClone(this.content);
81
+ private async handleStateChange(
82
+ content: Workflow,
83
+ op: ChangeOperation<WorkflowState>,
84
+ ) {
83
85
  const stateName = this.targetName(op) as string;
84
- // Check that state can be changed to
85
- content.transitions = content.transitions.filter(
86
- (t) => t.toState !== stateName,
87
- );
88
- content.transitions.forEach((t) => {
89
- t.fromState = t.fromState.filter((state) => state !== stateName);
90
- });
91
86
  // validate that new state contains 'name' and 'category'
92
87
  if (op.to.name === undefined || op.to.category === undefined) {
93
88
  throw new Error(
@@ -95,16 +90,26 @@ export class WorkflowResource extends FileResource<Workflow> {
95
90
  Updated state must have 'name' and 'category' properties.`,
96
91
  );
97
92
  }
98
- // Update all cards that use this state.
93
+ // Rename transitions to use the new state name
99
94
  const toStateName = op.to.name;
100
-
95
+ content.transitions.forEach((t) => {
96
+ if (t.toState === stateName) {
97
+ t.toState = toStateName;
98
+ }
99
+ t.fromState = t.fromState.map((state) =>
100
+ state === stateName ? toStateName : state,
101
+ );
102
+ });
103
+ // Update all cards that use this state.
101
104
  await this.updateCardStates(stateName, toStateName);
102
105
  }
103
106
 
104
107
  // Handle removal of workflow state.
105
108
  // State can be removed with or without replacement.
106
- private async handleStateRemoval(op: RemoveOperation<WorkflowState>) {
107
- const content = structuredClone(this.content);
109
+ private async handleStateRemoval(
110
+ content: Workflow,
111
+ op: RemoveOperation<WorkflowState>,
112
+ ) {
108
113
  const stateName = this.targetName(op) as string;
109
114
 
110
115
  // If there is no replacement value, remove all transitions "to" and "from" this state.
@@ -232,6 +237,28 @@ export class WorkflowResource extends FileResource<Workflow> {
232
237
  return super.create(newContent);
233
238
  }
234
239
 
240
+ /**
241
+ * Validates the content of the workflow resource.
242
+ * @param content Content to be validated.
243
+ * @throws if content is invalid.
244
+ */
245
+ public async validate(content?: Workflow) {
246
+ // Base class run basic schema checks
247
+ await super.validate(content);
248
+
249
+ const workflowContent = content ?? this.content;
250
+
251
+ const newCardTransitions = workflowContent.transitions.filter(
252
+ (t) => t.fromState.includes('') || t.fromState.length === 0,
253
+ );
254
+
255
+ if (newCardTransitions.length !== 1) {
256
+ throw new Error(
257
+ `Workflow '${workflowContent.name}' must have exactly one transition from "New Card" (empty fromState), found ${newCardTransitions.length}.`,
258
+ );
259
+ }
260
+ }
261
+
235
262
  /**
236
263
  * Renames the object and the file.
237
264
  * @param newName New name for the resource.
@@ -254,7 +281,7 @@ export class WorkflowResource extends FileResource<Workflow> {
254
281
  ) {
255
282
  const { key } = updateKey;
256
283
 
257
- if (key === 'name' || key === 'displayName' || key === 'description') {
284
+ if (this.isBaseProperty(key)) {
258
285
  await super.update(updateKey, op);
259
286
  } else {
260
287
  const content = structuredClone(this.content) as Workflow;
@@ -329,11 +356,13 @@ export class WorkflowResource extends FileResource<Workflow> {
329
356
  removeOp = {
330
357
  name: 'remove',
331
358
  target: toBeRemovedState as WorkflowState,
359
+ replacementValue: (op as RemoveOperation<unknown>)
360
+ .replacementValue as WorkflowState,
332
361
  };
333
362
  } else {
334
363
  removeOp = op as RemoveOperation<WorkflowState>;
335
364
  }
336
- await this.handleStateRemoval(removeOp);
365
+ await this.handleStateRemoval(content, removeOp);
337
366
  } else if (key === 'states' && op.name === 'change') {
338
367
  // If workflow state is renamed, replace all transitions "to" and "from" the old state with new state.
339
368
  let changeOp: ChangeOperation<WorkflowState>;
@@ -349,7 +378,7 @@ export class WorkflowResource extends FileResource<Workflow> {
349
378
  } else {
350
379
  changeOp = op as ChangeOperation<WorkflowState>;
351
380
  }
352
- await this.handleStateChange(changeOp);
381
+ await this.handleStateChange(content, changeOp);
353
382
  }
354
383
 
355
384
  await super.postUpdate(content, updateKey, op);
@@ -95,7 +95,7 @@ export const cardPathParts = (
95
95
  const cardKey = pathParts.at(pathParts.length - 1);
96
96
  const parents = [];
97
97
  let template = '';
98
- let startIndex = -1;
98
+ let startIndex: number;
99
99
  let templatesNameIndex = -1;
100
100
 
101
101
  const cardRootIndex = pathParts.indexOf('cardRoot');
@@ -0,0 +1,45 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2026
4
+
5
+ This program is free software: you can redistribute it and/or modify it under
6
+ the terms of the GNU Affero General Public License version 3 as published by
7
+ the Free Software Foundation. This program is distributed in the hope that it
8
+ will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
9
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+ See the GNU Affero General Public License for more details.
11
+ You should have received a copy of the GNU Affero General Public
12
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
13
+ */
14
+
15
+ import { AsyncLocalStorage } from 'node:async_hooks';
16
+
17
+ export interface CommitContext {
18
+ message?: string;
19
+ author?: { name: string; email: string };
20
+ }
21
+
22
+ const context = new AsyncLocalStorage<CommitContext>();
23
+
24
+ export function runWithCommitContext<T>(
25
+ ctx: CommitContext,
26
+ fn: () => Promise<T>,
27
+ ): Promise<T> {
28
+ const current = context.getStore();
29
+ // Merge with any existing context (e.g. author set at middleware level, message set at decorator level)
30
+ const merged = { ...current, ...ctx };
31
+ return context.run(merged, fn);
32
+ }
33
+
34
+ export function getCommitContext(): CommitContext {
35
+ return context.getStore() ?? {};
36
+ }
37
+
38
+ export function runWithDefaultCommitMessage<T>(
39
+ message: string,
40
+ fn: () => Promise<T>,
41
+ ): Promise<T> {
42
+ return getCommitContext().message !== undefined
43
+ ? fn()
44
+ : runWithCommitContext({ message }, fn);
45
+ }
package/src/utils/csv.ts CHANGED
@@ -14,6 +14,18 @@ import { readFile } from 'node:fs/promises';
14
14
  import { parse } from 'csv-parse/sync';
15
15
  import type { CSVRowRaw } from '../interfaces/project-interfaces.js';
16
16
 
17
+ /**
18
+ * Escapes a string for use as a CSV field.
19
+ * Escapes double quotes by doubling them. The caller is responsible for
20
+ * wrapping the result in quotes.
21
+ * @param str The string to escape
22
+ * @returns The escaped string (without surrounding quotes)
23
+ */
24
+ export function escapeCsvField(str: string): string {
25
+ // Escape double quotes by doubling them
26
+ return str.replace(/"/g, '""');
27
+ }
28
+
17
29
  /**
18
30
  * Reads a CSV file and returns its content as an array of objects.
19
31
  * @param file Path to the CSV file.
@@ -36,7 +36,9 @@ export async function availableSpace(path: string): Promise<number> {
36
36
  const stats = await statfs(path);
37
37
  return stats.bavail * stats.bsize;
38
38
  } catch (error) {
39
- throw new Error(`Failed to check available disk space: ${error}`);
39
+ throw new Error(`Failed to check available disk space: ${error}`, {
40
+ cause: error,
41
+ });
40
42
  }
41
43
  }
42
44
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2026
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation. This program is distributed in the hope that it
7
+ will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
8
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ See the GNU Affero General Public License for more details.
10
+ You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+
14
+ import { simpleGit, type SimpleGit } from 'simple-git';
15
+ import { getChildLogger } from './log-utils.js';
16
+
17
+ export class GitManager {
18
+ private git: SimpleGit;
19
+ private logger = getChildLogger({ module: 'GitManager' });
20
+
21
+ constructor(projectPath: string) {
22
+ this.git = simpleGit(projectPath, {
23
+ config: ['user.name=Cyberismo Bot', 'user.email=bot@cyberismo.com'],
24
+ });
25
+ }
26
+
27
+ /** Ensure the project is a git repo. Idempotent. */
28
+ async initialize(author?: { name: string; email: string }): Promise<void> {
29
+ const isRepo = await this.git.checkIsRepo();
30
+ if (!isRepo) {
31
+ await this.git.init();
32
+ // Initial commit so rollback has a baseline
33
+ await this.git.add('.');
34
+ const commitOpts: Record<string, string | null> = {
35
+ '--allow-empty': null,
36
+ };
37
+ if (author) {
38
+ commitOpts['--author'] = `${author.name} <${author.email}>`;
39
+ }
40
+ await this.git.commit('Initial commit', undefined, commitOpts);
41
+ this.logger.info('New repo created with baseline commit');
42
+ } else {
43
+ this.logger.debug('Repo already exists');
44
+ }
45
+ }
46
+
47
+ /** Commit current changes (cardRoot + .cards). */
48
+ async commit(
49
+ message: string = 'Autocommit',
50
+ author?: { name: string; email: string },
51
+ ): Promise<void> {
52
+ // Stage only the directories we care about
53
+ this.logger.debug('Staging changes');
54
+ await this.git.add(['cardRoot', '.cards']);
55
+
56
+ // Check if there's anything to commit
57
+ const status = await this.git.status();
58
+ if (status.staged.length === 0) {
59
+ this.logger.debug('Nothing to commit, skipping');
60
+ return;
61
+ }
62
+
63
+ this.logger.info(
64
+ { message, stagedFiles: status.staged.length },
65
+ 'Committing changes',
66
+ );
67
+ const commitOpts: Record<string, string> = {};
68
+ if (author) {
69
+ commitOpts['--author'] = `${author.name} <${author.email}>`;
70
+ }
71
+ await this.git.commit(message, undefined, commitOpts);
72
+ }
73
+
74
+ /** Rollback: restore cardRoot and .cards to last committed state. */
75
+ async rollback(): Promise<void> {
76
+ this.logger.info('Rollback starting');
77
+ // Restore modified tracked files (ignore errors if paths have no tracked content)
78
+ try {
79
+ await this.git.checkout(['--', 'cardRoot', '.cards']);
80
+ } catch {
81
+ this.logger.debug('No tracked files to restore');
82
+ }
83
+ // Remove new untracked files created during the failed write
84
+ await this.git.clean('f', ['-d', 'cardRoot', '.cards']);
85
+ this.logger.info('Rollback completed');
86
+ }
87
+ }