@thinkwise/testwise 0.2.0-beta.3 → 0.2.4

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 (136) hide show
  1. package/Testwise.ts +8 -7
  2. package/artifact-builder/ArtifactManager.ts +0 -1
  3. package/artifact-builder/InterfaceGenerator.ts +73 -83
  4. package/artifact-builder/ModelDataBuilder.ts +74 -86
  5. package/artifact-builder/ModelDataRefiner.ts +18 -13
  6. package/artifact-builder/SchemaGenerator.ts +1 -4
  7. package/artifact-builder/ScreenInterfaceRefiner.ts +0 -5
  8. package/artifact-builder/SelectorBuilder.ts +8 -1
  9. package/artifact-builder/SubjectComponentGenerator.ts +77 -7
  10. package/artifact-builder/SubjectGenerator.ts +3 -3
  11. package/artifact-builder/SubjectRegistration.ts +68 -60
  12. package/artifact-builder/helpers/DataRetriever.ts +6 -3
  13. package/artifact-builder/helpers/NamingHandler.ts +28 -20
  14. package/artifact-builder/helpers/Stopwatch.ts +13 -0
  15. package/artifact-builder/helpers/index.ts +1 -0
  16. package/components/BaseComponentObjects.ts +4 -1
  17. package/components/action-bar/ActionBar.ts +3 -3
  18. package/components/grid/Grid.ts +18 -21
  19. package/components/tab/BaseTab.ts +2 -2
  20. package/components/tab/BaseTabObjects.ts +1 -1
  21. package/components/tab/DetailTabPage.ts +2 -2
  22. package/components/tab/Tab.ts +4 -4
  23. package/controls/LookupDropdown.ts +7 -2
  24. package/dist/Testwise.d.ts +1 -0
  25. package/dist/Testwise.js +15 -6
  26. package/dist/Testwise.js.map +1 -1
  27. package/dist/artifact-builder/ArtifactManager.js.map +1 -1
  28. package/dist/artifact-builder/InterfaceGenerator.d.ts +1 -1
  29. package/dist/artifact-builder/InterfaceGenerator.js +60 -67
  30. package/dist/artifact-builder/InterfaceGenerator.js.map +1 -1
  31. package/dist/artifact-builder/ModelDataBuilder.js +49 -60
  32. package/dist/artifact-builder/ModelDataBuilder.js.map +1 -1
  33. package/dist/artifact-builder/ModelDataRefiner.js +11 -7
  34. package/dist/artifact-builder/ModelDataRefiner.js.map +1 -1
  35. package/dist/artifact-builder/SchemaGenerator.js +0 -2
  36. package/dist/artifact-builder/SchemaGenerator.js.map +1 -1
  37. package/dist/artifact-builder/ScreenInterfaceRefiner.js +0 -5
  38. package/dist/artifact-builder/ScreenInterfaceRefiner.js.map +1 -1
  39. package/dist/artifact-builder/SelectorBuilder.js +3 -1
  40. package/dist/artifact-builder/SelectorBuilder.js.map +1 -1
  41. package/dist/artifact-builder/SubjectComponentGenerator.d.ts +4 -0
  42. package/dist/artifact-builder/SubjectComponentGenerator.js +61 -5
  43. package/dist/artifact-builder/SubjectComponentGenerator.js.map +1 -1
  44. package/dist/artifact-builder/SubjectGenerator.js +1 -1
  45. package/dist/artifact-builder/SubjectGenerator.js.map +1 -1
  46. package/dist/artifact-builder/SubjectRegistration.d.ts +9 -10
  47. package/dist/artifact-builder/SubjectRegistration.js +51 -43
  48. package/dist/artifact-builder/SubjectRegistration.js.map +1 -1
  49. package/dist/artifact-builder/helpers/DataRetriever.js +4 -3
  50. package/dist/artifact-builder/helpers/DataRetriever.js.map +1 -1
  51. package/dist/artifact-builder/helpers/NamingHandler.d.ts +2 -1
  52. package/dist/artifact-builder/helpers/NamingHandler.js +23 -16
  53. package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -1
  54. package/dist/artifact-builder/helpers/Stopwatch.d.ts +5 -0
  55. package/dist/artifact-builder/helpers/Stopwatch.js +13 -0
  56. package/dist/artifact-builder/helpers/Stopwatch.js.map +1 -0
  57. package/dist/artifact-builder/helpers/index.d.ts +1 -0
  58. package/dist/artifact-builder/helpers/index.js +1 -0
  59. package/dist/artifact-builder/helpers/index.js.map +1 -1
  60. package/dist/components/BaseComponentObjects.d.ts +1 -0
  61. package/dist/components/BaseComponentObjects.js +3 -1
  62. package/dist/components/BaseComponentObjects.js.map +1 -1
  63. package/dist/components/action-bar/ActionBar.d.ts +1 -1
  64. package/dist/components/action-bar/ActionBar.js +3 -3
  65. package/dist/components/grid/Grid.d.ts +5 -4
  66. package/dist/components/grid/Grid.js +13 -19
  67. package/dist/components/grid/Grid.js.map +1 -1
  68. package/dist/components/tab/BaseTab.d.ts +2 -2
  69. package/dist/components/tab/BaseTab.js +2 -2
  70. package/dist/components/tab/BaseTab.js.map +1 -1
  71. package/dist/components/tab/BaseTabObjects.js +1 -1
  72. package/dist/components/tab/BaseTabObjects.js.map +1 -1
  73. package/dist/components/tab/DetailTabPage.d.ts +2 -2
  74. package/dist/components/tab/DetailTabPage.js +2 -2
  75. package/dist/components/tab/DetailTabPage.js.map +1 -1
  76. package/dist/components/tab/Tab.d.ts +3 -3
  77. package/dist/components/tab/Tab.js +4 -4
  78. package/dist/components/tab/Tab.js.map +1 -1
  79. package/dist/controls/LookupDropdown.d.ts +3 -7
  80. package/dist/controls/LookupDropdown.js.map +1 -1
  81. package/dist/enums/ElementTypes.d.ts +1 -1
  82. package/dist/enums/ElementTypes.js +1 -1
  83. package/dist/enums/ElementTypes.js.map +1 -1
  84. package/dist/helpers/ConfigChecker.d.ts +3 -0
  85. package/dist/helpers/ConfigChecker.js +7 -0
  86. package/dist/helpers/ConfigChecker.js.map +1 -0
  87. package/dist/helpers/LoginHelper.js +1 -1
  88. package/dist/helpers/LoginHelper.js.map +1 -1
  89. package/dist/interfaces/IComponentObjects.d.ts +1 -0
  90. package/dist/page-extensions/SubjectRegistry.d.ts +0 -8
  91. package/dist/page-extensions/SubjectRegistry.js +2 -6
  92. package/dist/page-extensions/SubjectRegistry.js.map +1 -1
  93. package/dist/page-extensions/index.d.ts +0 -1
  94. package/dist/page-extensions/index.js +0 -1
  95. package/dist/page-extensions/index.js.map +1 -1
  96. package/dist/services/IndiciumApi.service.d.ts +27 -0
  97. package/dist/services/IndiciumApi.service.js +135 -0
  98. package/dist/services/IndiciumApi.service.js.map +1 -0
  99. package/dist/templates/test-artifacts/SubjectPageBase.d.ts +5 -0
  100. package/dist/templates/test-artifacts/SubjectPageBase.js +6 -0
  101. package/dist/templates/test-artifacts/SubjectPageBase.js.map +1 -0
  102. package/dist/templates/test-artifacts/screens/index.d.ts +1 -0
  103. package/dist/templates/test-artifacts/screens/index.js +2 -0
  104. package/dist/templates/test-artifacts/screens/index.js.map +1 -0
  105. package/dist/templates/test-artifacts/subjects/index.d.ts +1 -0
  106. package/dist/templates/test-artifacts/subjects/index.js +2 -0
  107. package/dist/templates/test-artifacts/subjects/index.js.map +1 -0
  108. package/enums/ElementTypes.ts +2 -2
  109. package/helpers/ConfigChecker.ts +7 -0
  110. package/helpers/LoginHelper.ts +1 -1
  111. package/interfaces/IComponentObjects.ts +1 -0
  112. package/interfaces/IRegisteredSubjects.ts +1 -1
  113. package/package.json +5 -3
  114. package/page-extensions/SubjectRegistry.ts +2 -19
  115. package/page-extensions/index.ts +0 -1
  116. package/scripts/main.js +63 -82
  117. package/scripts/postinstall.js +40 -42
  118. package/scripts/setup.js +37 -37
  119. package/scripts/sync.js +756 -69
  120. package/services/ConfigBuilder.ts +1 -1
  121. package/services/IndiciumApi.service.ts +159 -0
  122. package/templates/SubjectRegistry.template.ts +73 -0
  123. package/templates/test-artifacts/SubjectPageBase.ts +9 -0
  124. package/templates/test-artifacts/screens/index.ts +0 -0
  125. package/templates/test-artifacts/subjects/index.ts +0 -0
  126. package/tsconfig.json +2 -3
  127. package/types/Components.ts +1 -1
  128. package/dist/config.json +0 -10
  129. package/dist/page-extensions/SubjectProvider.d.ts +0 -11
  130. package/dist/page-extensions/SubjectProvider.js +0 -24
  131. package/dist/page-extensions/SubjectProvider.js.map +0 -1
  132. package/dist/test-artifacts/index.d.ts +0 -3
  133. package/dist/test-artifacts/index.js +0 -4
  134. package/dist/test-artifacts/index.js.map +0 -1
  135. package/page-extensions/SubjectProvider.ts +0 -41
  136. package/test-artifacts/index.ts +0 -3
package/scripts/sync.js CHANGED
@@ -1,69 +1,756 @@
1
-
2
- /* eslint-disable @typescript-eslint/naming-convention */
3
-
4
- import { execSync } from 'node:child_process';
5
- import path from 'node:path';
6
- import process from 'node:process';
7
- import { fileURLToPath } from 'node:url';
8
- import { ArtifactManager } from '../dist/artifact-builder/index.js';
9
-
10
- const _filename = fileURLToPath(import.meta.url);
11
- const _dirname = path.dirname(_filename);
12
-
13
- export async function sync() {
14
-
15
- try {
16
- const rootDir = path.resolve(_dirname, '..');
17
-
18
- const { rimrafSync } = await import('rimraf');
19
-
20
- // Remove dist folder synchronously
21
- rimrafSync(path.join(rootDir, 'dist'));
22
-
23
- // Compile TypeScript
24
- execSync('npx tsc', { cwd: rootDir, stdio: 'inherit' });
25
-
26
- // Lazy import post recompilation
27
- const { InterfaceGenerator } = await import('../dist/artifact-builder/index.js');
28
- const { ScreenInterfaceRefiner } = await import('../dist/artifact-builder/index.js');
29
- const { SchemaGenerator } = await import('../dist/artifact-builder/index.js');
30
- const { SubjectComponentGenerator } = await import('../dist/artifact-builder/index.js');
31
- const { SubjectGenerator } = await import('../dist/artifact-builder/index.js');
32
- const { ModelDataRefiner } = await import('../dist/artifact-builder/index.js');
33
- const { buildSubjects, buildScreens } = await import('../dist/artifact-builder/ModelDataBuilder.js');
34
- // const { SubjectRegistration } = await import('../dist/artifact-builder/index.js'); - do not remove: future feature
35
-
36
- // Build Seed Data Axios Instance
37
- await buildSubjects();
38
- await buildScreens();
39
-
40
- // Seed data refinement pre-processing
41
- await new ModelDataRefiner().run();
42
-
43
-
44
- // Generate artifacts
45
- const interfaceGenerator = new InterfaceGenerator();
46
-
47
- await interfaceGenerator.generateScreenInterfacesFromSchemas();
48
-
49
- new ScreenInterfaceRefiner().refine();
50
- new SchemaGenerator().generateSchema();
51
-
52
- await interfaceGenerator.generateSubjectInterfacesFromSchemas();
53
-
54
- new SubjectComponentGenerator().run();
55
- new SubjectGenerator().run();
56
- // new SubjectRegistration().run(); - do not remove: future feature
57
-
58
- // Compile TypeScript again
59
- execSync('npx tsc', { cwd: rootDir, stdio: 'inherit' });
60
-
61
- console.info('Sync process completed successfully.');
62
- } catch (error) {
63
- // reinstate the backup -- simulate this to see what happens first
64
- console.error('Sync process failed:', error.message);
65
- process.exit(1); // Will npm do an automatic rollback here?
66
- } finally {
67
- new ArtifactManager().performBackup();
68
- }
69
- }
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+
3
+ import { execSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { PathResolver } from '../dist/helpers/index.js';
9
+ import { indiciumApi } from '../dist/services/IndiciumApi.service.js';
10
+
11
+ // ============================================================================
12
+ // Constants & Configuration
13
+ // ============================================================================
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ /** @type {Readonly<{ROOT: string, DIST: string, SEED_DATA: string, TEST_ARTIFACTS: string, TEMP_TEST_ARTIFACTS: string, TEMPLATE_TEST_ARTIFACTS: string}>} */
19
+ const PATHS = Object.freeze({
20
+ ROOT: path.resolve(__dirname, '..'),
21
+ DIST: path.resolve(__dirname, '..', 'dist'),
22
+ SEED_DATA: path.resolve(__dirname, '..', 'seed-data'),
23
+ TEST_ARTIFACTS: path.resolve(__dirname, '..', 'test-artifacts'),
24
+ TEMP_TEST_ARTIFACTS: path.resolve(__dirname, '..', 'test-artifacts-temp'),
25
+ TEMPLATE_TEST_ARTIFACTS: path.resolve(__dirname, '..', 'templates', 'test-artifacts')
26
+ });
27
+
28
+ /** @type {readonly string[]} */
29
+ const REQUIRED_SEED_FILES = Object.freeze(['screensToBuild.json', 'subjectsToBuild.json']);
30
+
31
+ /** @type {Readonly<{CLEANUP: string, BUILD: string, REFINE: string, GENERATE: string, COMPILE: string, BACKUP: string}>} */
32
+ const PHASE_NAMES = Object.freeze({
33
+ CLEANUP: 'Cleanup and Initial Compilation',
34
+ BUILD: 'Build Model Data',
35
+ REFINE: 'Refine Model Data',
36
+ GENERATE: 'Generate Artifacts',
37
+ COMPILE: 'Final Compilation',
38
+ BACKUP: 'Backup'
39
+ });
40
+
41
+ /** @type {Readonly<{INFO: string, SUCCESS: string, ERROR: string, WARNING: string, PHASE: string, STEP: string, DIVIDER: string}>} */
42
+ const LOG_SYMBOLS = Object.freeze({
43
+ INFO: 'ℹ️',
44
+ SUCCESS: '✅',
45
+ ERROR: '❌',
46
+ WARNING: '⚠️',
47
+ PHASE: '📦',
48
+ STEP: '▸',
49
+ DIVIDER: '═'
50
+ });
51
+
52
+ // ============================================================================
53
+ // Types & Interfaces
54
+ // ============================================================================
55
+
56
+ /**
57
+ * @typedef {Object} SyncContext
58
+ * @property {import('../dist/artifact-builder/helpers/index.js').Stopwatch} stopwatch - Stopwatch instance for timing
59
+ * @property {ReturnType<import('../dist/artifact-builder/helpers/index.js').Stopwatch['start']>} totalTimer - Total elapsed timer
60
+ * @property {AbortSignal} [signal] - Optional abort signal for graceful shutdown
61
+ * @property {SyncOptions} options - Sync configuration options
62
+ */
63
+
64
+ /**
65
+ * @typedef {Object} SyncOptions
66
+ * @property {boolean} [dryRun=false] - If true, only logs what would happen without executing
67
+ * @property {boolean} [verbose=false] - If true, enables verbose logging
68
+ * @property {boolean} [skipBackup=false] - If true, skips the backup phase
69
+ * @property {number} [retryAttempts=0] - Number of retry attempts for failed operations
70
+ * @property {number} [retryDelay=1000] - Delay in ms between retry attempts
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} StepResult
75
+ * @property {boolean} success - Whether the step succeeded
76
+ * @property {number} duration - Duration in milliseconds
77
+ * @property {Error} [error] - Error if step failed
78
+ */
79
+
80
+ // ============================================================================
81
+ // Logger Utility
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Centralized logging utility with consistent formatting.
86
+ */
87
+ const Logger = {
88
+ /**
89
+ * Logs an informational message.
90
+ * @param {string} message - The message to log
91
+ */
92
+ info(message) {
93
+ console.info(`${LOG_SYMBOLS.INFO} ${message}`);
94
+ },
95
+
96
+ /**
97
+ * Logs a success message.
98
+ * @param {string} message - The message to log
99
+ */
100
+ success(message) {
101
+ console.info(`${LOG_SYMBOLS.SUCCESS} ${message}`);
102
+ },
103
+
104
+ /**
105
+ * Logs an error message.
106
+ * @param {string} message - The message to log
107
+ * @param {Error} [error] - Optional error object
108
+ */
109
+ error(message, error) {
110
+ console.error(`${LOG_SYMBOLS.ERROR} ${message}`);
111
+ if (error?.stack) {
112
+ console.error('\nStack trace:');
113
+ console.error(error.stack);
114
+ }
115
+ },
116
+
117
+ /**
118
+ * Logs a warning message.
119
+ * @param {string} message - The message to log
120
+ */
121
+ warn(message) {
122
+ console.warn(`${LOG_SYMBOLS.WARNING} ${message}`);
123
+ },
124
+
125
+ /**
126
+ * Logs a phase header.
127
+ * @param {number} phaseNumber - The phase number
128
+ * @param {string} phaseName - The phase name
129
+ */
130
+ phase(phaseNumber, phaseName) {
131
+ console.info(`\n${LOG_SYMBOLS.PHASE} Phase ${phaseNumber}: ${phaseName}\n`);
132
+ },
133
+
134
+ /**
135
+ * Logs a step execution.
136
+ * @param {string} stepName - The step name
137
+ */
138
+ step(stepName) {
139
+ console.info(` ${LOG_SYMBOLS.STEP} ${stepName}`);
140
+ },
141
+
142
+ /**
143
+ * Logs a divider line.
144
+ * @param {number} [width=60] - The width of the divider
145
+ */
146
+ divider(width = 60) {
147
+ console.info(LOG_SYMBOLS.DIVIDER.repeat(width));
148
+ },
149
+
150
+ /**
151
+ * Logs a header with dividers.
152
+ * @param {string} message - The header message
153
+ */
154
+ header(message) {
155
+ this.divider();
156
+ console.info(message);
157
+ this.divider();
158
+ }
159
+ };
160
+
161
+ // ============================================================================
162
+ // Utility Functions
163
+ // ============================================================================
164
+
165
+ /**
166
+ * Sleeps for the specified duration.
167
+ * @param {number} ms - Duration in milliseconds
168
+ * @returns {Promise<void>}
169
+ */
170
+ function sleep(ms) {
171
+ return new Promise((resolve) => setTimeout(resolve, ms));
172
+ }
173
+
174
+ /**
175
+ * Executes a function with retry logic.
176
+ * @template T
177
+ * @param {() => Promise<T> | T} fn - The function to execute
178
+ * @param {number} [attempts=3] - Number of retry attempts
179
+ * @param {number} [delay=1000] - Delay between retries in ms
180
+ * @returns {Promise<T>}
181
+ */
182
+ async function withRetry(fn, attempts = 3, delay = 1000) {
183
+ let lastError;
184
+
185
+ for (let attempt = 1; attempt <= attempts; attempt++) {
186
+ try {
187
+ return await fn();
188
+ } catch (error) {
189
+ lastError = error;
190
+ if (attempt < attempts) {
191
+ Logger.warn(`Attempt ${attempt}/${attempts} failed, retrying in ${delay}ms...`);
192
+ await sleep(delay);
193
+ }
194
+ }
195
+ }
196
+
197
+ throw lastError;
198
+ }
199
+
200
+ /**
201
+ * Safely cleans a directory without deleting the root folder
202
+ * to avoid EPERM errors during npm postinstall.
203
+ * @param {string} dirPath - The directory path to clean
204
+ * @returns {void}
205
+ */
206
+ function safeCleanDir(dirPath) {
207
+ if (!fs.existsSync(dirPath)) {
208
+ return;
209
+ }
210
+
211
+ // Rename test-artifacts to temp directory if needed (only once)
212
+ if (fs.existsSync(PATHS.TEST_ARTIFACTS) && !fs.existsSync(PATHS.TEMP_TEST_ARTIFACTS)) {
213
+ fs.renameSync(PATHS.TEST_ARTIFACTS, PATHS.TEMP_TEST_ARTIFACTS);
214
+ fs.cpSync(PATHS.TEMPLATE_TEST_ARTIFACTS, PATHS.TEST_ARTIFACTS, { recursive: true });
215
+ }
216
+
217
+ const files = fs.readdirSync(dirPath);
218
+ for (const file of files) {
219
+ const filePath = path.join(dirPath, file);
220
+ try {
221
+ fs.rmSync(filePath, { recursive: true, force: true });
222
+ } catch (error) {
223
+ Logger.warn(`Could not remove ${filePath}: ${error.message}`);
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Ensures a JSON file exists to prevent ENOENT crashes
230
+ * if the Refiner doesn't handle missing files gracefully.
231
+ * @param {string} filePath - The file path to ensure exists
232
+ * @param {unknown} [defaultContent=[]] - Default content to write if file doesn't exist
233
+ * @returns {void}
234
+ */
235
+ function ensureJsonExists(filePath, defaultContent = []) {
236
+ if (fs.existsSync(filePath)) {
237
+ return;
238
+ }
239
+
240
+ const dir = path.dirname(filePath);
241
+ if (!fs.existsSync(dir)) {
242
+ fs.mkdirSync(dir, { recursive: true });
243
+ }
244
+ fs.writeFileSync(filePath, JSON.stringify(defaultContent, null, 2));
245
+ }
246
+
247
+ /**
248
+ * Validates that all required dependencies and directories exist.
249
+ * @throws {Error} If validation fails
250
+ * @returns {void}
251
+ */
252
+ function validateEnvironment() {
253
+ // Ensure seed-data directory exists
254
+ if (!fs.existsSync(PATHS.SEED_DATA)) {
255
+ fs.mkdirSync(PATHS.SEED_DATA, { recursive: true });
256
+ }
257
+
258
+ // Verify TypeScript config exists
259
+ const tsconfigPath = path.join(PATHS.ROOT, 'tsconfig.json');
260
+ if (!fs.existsSync(tsconfigPath)) {
261
+ throw new Error('tsconfig.json not found. Please ensure the project is properly set up.');
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Executes a TypeScript compilation.
267
+ * @param {string} cwd - The working directory
268
+ * @throws {Error} If compilation fails
269
+ * @returns {void}
270
+ */
271
+ function compileTypeScript(cwd) {
272
+ try {
273
+ execSync('npx tsc', { cwd, stdio: 'inherit' });
274
+ } catch (error) {
275
+ throw new Error(`TypeScript compilation failed: ${error.message}`);
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Resets the SubjectRegistry.ts file from template.
281
+ * @returns {void}
282
+ */
283
+ function resetSubjectRegistry() {
284
+ const registryPath = path.join(PATHS.ROOT, 'page-extensions', 'SubjectRegistry.ts');
285
+ const templatePath = path.join(PATHS.ROOT, 'templates', 'SubjectRegistry.template.ts');
286
+
287
+ if (fs.existsSync(registryPath)) {
288
+ fs.rmSync(registryPath, { force: true });
289
+ }
290
+
291
+ if (fs.existsSync(templatePath)) {
292
+ fs.copyFileSync(templatePath, registryPath);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Cleans up temporary test artifacts directory.
298
+ * @returns {void}
299
+ */
300
+ function cleanupTempArtifacts() {
301
+ if (fs.existsSync(PATHS.TEMP_TEST_ARTIFACTS)) {
302
+ fs.rmSync(PATHS.TEMP_TEST_ARTIFACTS, { recursive: true, force: true });
303
+ }
304
+ }
305
+
306
+ // ============================================================================
307
+ // Sync Pipeline Steps
308
+ // ============================================================================
309
+
310
+ /**
311
+ * Runs a single sync step with timing and optional dry-run support.
312
+ * @param {SyncContext} ctx - The sync context
313
+ * @param {string} stepName - Name of the step
314
+ * @param {() => Promise<void> | void} fn - The function to execute
315
+ * @returns {Promise<StepResult>}
316
+ */
317
+ async function runStep(ctx, stepName, fn) {
318
+ Logger.step(stepName);
319
+
320
+ if (ctx.options.dryRun) {
321
+ return { success: true, duration: 0 };
322
+ }
323
+
324
+ const timer = ctx.stopwatch.start(stepName);
325
+ const startTime = Date.now();
326
+
327
+ try {
328
+ // Check for abort signal
329
+ if (ctx.signal?.aborted) {
330
+ throw new Error('Sync process was aborted');
331
+ }
332
+
333
+ const execute =
334
+ ctx.options.retryAttempts > 0 ? () => withRetry(fn, ctx.options.retryAttempts, ctx.options.retryDelay) : fn;
335
+
336
+ await execute();
337
+
338
+ return { success: true, duration: Date.now() - startTime };
339
+ } catch (error) {
340
+ return { success: false, duration: Date.now() - startTime, error };
341
+ } finally {
342
+ timer.stop();
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Phase 1: Cleanup and initial compilation
348
+ * @param {SyncContext} ctx - The sync context
349
+ * @returns {Promise<void>}
350
+ */
351
+ async function phaseCleanupAndCompile(ctx) {
352
+ Logger.phase(1, PHASE_NAMES.CLEANUP);
353
+
354
+ resetSubjectRegistry();
355
+
356
+ const cleanResult = await runStep(ctx, 'Clear distribution directory', () => {
357
+ safeCleanDir(PATHS.DIST);
358
+ });
359
+
360
+ if (!cleanResult.success) {
361
+ throw cleanResult.error;
362
+ }
363
+
364
+ const compileResult = await runStep(ctx, 'Recompile TypeScript sources', () => {
365
+ compileTypeScript(PATHS.ROOT);
366
+ });
367
+
368
+ if (!compileResult.success) {
369
+ throw compileResult.error;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Phase 2: Build model data
375
+ * @param {SyncContext} ctx - The sync context
376
+ * @returns {Promise<void>}
377
+ */
378
+ async function phaseBuildModelData(ctx) {
379
+ Logger.phase(2, PHASE_NAMES.BUILD);
380
+
381
+ const { buildSubjects, buildScreens } = await import('../dist/artifact-builder/ModelDataBuilder.js');
382
+
383
+ const subjectsResult = await runStep(ctx, 'Build subjects from model data', async () => {
384
+ await buildSubjects();
385
+ });
386
+
387
+ if (!subjectsResult.success) {
388
+ throw subjectsResult.error;
389
+ }
390
+
391
+ const screensResult = await runStep(ctx, 'Build screens from model data', async () => {
392
+ await buildScreens();
393
+ });
394
+
395
+ if (!screensResult.success) {
396
+ throw screensResult.error;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Phase 3: Refine model data
402
+ * @param {SyncContext} ctx - The sync context
403
+ * @returns {Promise<void>}
404
+ */
405
+ async function phaseRefineModelData(ctx) {
406
+ Logger.phase(3, PHASE_NAMES.REFINE);
407
+
408
+ // Ensure required seed files exist before refinement
409
+ for (const seedFile of REQUIRED_SEED_FILES) {
410
+ ensureJsonExists(path.join(PATHS.SEED_DATA, seedFile));
411
+ }
412
+
413
+ const { ModelDataRefiner } = await import('../dist/artifact-builder/index.js');
414
+
415
+ const refineResult = await runStep(ctx, 'Refine model data', async () => {
416
+ await new ModelDataRefiner().run();
417
+ });
418
+
419
+ if (!refineResult.success) {
420
+ throw refineResult.error;
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Phase 4: Generate artifacts
426
+ * @param {SyncContext} ctx - The sync context
427
+ * @returns {Promise<void>}
428
+ */
429
+ async function phaseGenerateArtifacts(ctx) {
430
+ Logger.phase(4, PHASE_NAMES.GENERATE);
431
+
432
+ const {
433
+ InterfaceGenerator,
434
+ ScreenInterfaceRefiner,
435
+ SchemaGenerator,
436
+ SubjectComponentGenerator,
437
+ SubjectGenerator,
438
+ SubjectRegistration
439
+ } = await import('../dist/artifact-builder/index.js');
440
+
441
+ const interfaceGenerator = new InterfaceGenerator();
442
+
443
+ /** @type {Array<{name: string, fn: () => Promise<void> | void}>} */
444
+ const steps = [
445
+ {
446
+ name: 'Generate screen interfaces from screen schemas',
447
+ fn: () => interfaceGenerator.generateScreenInterfacesFromSchemas()
448
+ },
449
+ {
450
+ name: 'Refine interfaces',
451
+ fn: () => new ScreenInterfaceRefiner().refine()
452
+ },
453
+ {
454
+ name: 'Generate subject component schemas',
455
+ fn: () => new SchemaGenerator().generateSchema()
456
+ },
457
+ {
458
+ name: 'Generate subject component interfaces from schemas',
459
+ fn: () => interfaceGenerator.generateSubjectInterfacesFromSchemas()
460
+ },
461
+ {
462
+ name: 'Build components that implement interfaces',
463
+ fn: () => new SubjectComponentGenerator().run()
464
+ },
465
+ {
466
+ name: 'Generate subject classes',
467
+ fn: () => new SubjectGenerator().run()
468
+ },
469
+ {
470
+ name: 'Add subjects to registry',
471
+ fn: () => new SubjectRegistration().run()
472
+ }
473
+ ];
474
+
475
+ for (const step of steps) {
476
+ const result = await runStep(ctx, step.name, step.fn);
477
+ if (!result.success) {
478
+ throw result.error;
479
+ }
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Phase 5: Final compilation
485
+ * @param {SyncContext} ctx - The sync context
486
+ * @returns {Promise<void>}
487
+ */
488
+ async function phaseFinalCompilation(ctx) {
489
+ Logger.phase(5, PHASE_NAMES.COMPILE);
490
+
491
+ const cleanResult = await runStep(ctx, 'Clear distribution directory', () => {
492
+ safeCleanDir(PATHS.DIST);
493
+ });
494
+
495
+ if (!cleanResult.success) {
496
+ throw cleanResult.error;
497
+ }
498
+
499
+ const compileResult = await runStep(ctx, 'Compile generated artifacts', () => {
500
+ compileTypeScript(PATHS.ROOT);
501
+ });
502
+
503
+ if (!compileResult.success) {
504
+ throw compileResult.error;
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Phase 6: Backup
510
+ * @param {SyncContext} ctx - The sync context
511
+ * @returns {Promise<void>}
512
+ */
513
+ async function phaseBackup(ctx) {
514
+ if (ctx.options.skipBackup) {
515
+ Logger.info('Skipping backup phase (--skip-backup flag)');
516
+ return;
517
+ }
518
+
519
+ Logger.phase(6, PHASE_NAMES.BACKUP);
520
+
521
+ const { ArtifactManager } = await import('../dist/artifact-builder/index.js');
522
+
523
+ const backupResult = await runStep(ctx, 'Perform backup', () => {
524
+ new ArtifactManager().performBackup();
525
+ cleanupTempArtifacts();
526
+ });
527
+
528
+ if (!backupResult.success) {
529
+ throw backupResult.error;
530
+ }
531
+ }
532
+
533
+ // ============================================================================
534
+ // Main Sync Function
535
+ // ============================================================================
536
+
537
+ /**
538
+ * Parses command line arguments into sync options.
539
+ * @returns {SyncOptions}
540
+ */
541
+ function parseCommandLineArgs() {
542
+ const args = process.argv.slice(2);
543
+
544
+ return {
545
+ dryRun: args.includes('--dry-run'),
546
+ verbose: args.includes('--verbose') || args.includes('-v'),
547
+ skipBackup: args.includes('--skip-backup'),
548
+ retryAttempts: parseInt(args.find((arg) => arg.startsWith('--retry='))?.split('=')[1] ?? '0', 10),
549
+ retryDelay: parseInt(args.find((arg) => arg.startsWith('--retry-delay='))?.split('=')[1] ?? '1000', 10)
550
+ };
551
+ }
552
+
553
+ /**
554
+ * Sets up graceful shutdown handlers.
555
+ * @param {AbortController} controller - The abort controller
556
+ * @returns {void}
557
+ */
558
+ function setupShutdownHandlers(controller) {
559
+ const shutdown = () => {
560
+ Logger.warn('Received shutdown signal, attempting graceful shutdown...');
561
+ controller.abort();
562
+ };
563
+
564
+ process.on('SIGINT', shutdown);
565
+ process.on('SIGTERM', shutdown);
566
+ }
567
+
568
+ /**
569
+ * Executes the complete synchronization process.
570
+ *
571
+ * This process:
572
+ * 1. Cleans the dist directory and compiles TypeScript
573
+ * 2. Builds model data from subjects and screens
574
+ * 3. Refines the model data
575
+ * 4. Generates all artifacts (interfaces, schemas, components, subjects)
576
+ * 5. Performs final compilation
577
+ * 6. Creates a backup
578
+ *
579
+ * @param {SyncOptions} [options={}] - Optional sync configuration
580
+ * @returns {Promise<void>}
581
+ * @throws {Error} If sync process fails
582
+ */
583
+ export async function sync(options = {}) {
584
+ // Merge provided options with command line args (CLI takes precedence)
585
+ const cliOptions = parseCommandLineArgs();
586
+ const mergedOptions = { ...options, ...cliOptions };
587
+
588
+ // Setup abort controller for graceful shutdown
589
+ const abortController = new AbortController();
590
+ setupShutdownHandlers(abortController);
591
+
592
+ // Late import to avoid circular dependency issues during initial compile
593
+ const { Stopwatch } = await import('../dist/artifact-builder/helpers/index.js');
594
+
595
+ const stopwatch = new Stopwatch();
596
+ const totalTimer = stopwatch.start('Sync process start to end');
597
+
598
+ /** @type {SyncContext} */
599
+ const ctx = {
600
+ stopwatch,
601
+ totalTimer,
602
+ signal: abortController.signal,
603
+ options: mergedOptions
604
+ };
605
+
606
+ Logger.header('🚀 Starting Testwise Sync Process');
607
+
608
+ if (mergedOptions.dryRun) {
609
+ Logger.warn('Running in DRY RUN mode - no changes will be made');
610
+ }
611
+
612
+ if (mergedOptions.verbose) {
613
+ Logger.info(`Options: ${JSON.stringify(mergedOptions, null, 2)}`);
614
+ }
615
+
616
+ // Model data verification before any sync steps
617
+ if (!(await verifiedModelData())) {
618
+ Logger.info('Model data verification failed. Sync aborted.');
619
+ process.exit(0);
620
+ }
621
+
622
+ try {
623
+ // Pre-flight validation
624
+ validateEnvironment();
625
+
626
+ // Execute phases in sequence
627
+ await phaseCleanupAndCompile(ctx);
628
+ await phaseBuildModelData(ctx);
629
+ await phaseRefineModelData(ctx);
630
+ await phaseGenerateArtifacts(ctx);
631
+ await phaseFinalCompilation(ctx);
632
+
633
+ Logger.header('✅ Sync process completed successfully!');
634
+ } catch (error) {
635
+ Logger.divider();
636
+ Logger.error('Sync process failed!', error);
637
+ Logger.divider();
638
+ process.exit(1);
639
+ } finally {
640
+ try {
641
+ await phaseBackup(ctx);
642
+ } catch (backupError) {
643
+ Logger.warn(`Backup failed: ${backupError.message}`);
644
+ }
645
+
646
+ totalTimer.stop();
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Verify that model data is properly configured
652
+ * @returns {Promise<boolean>} True if model data is verified, false otherwise
653
+ */
654
+ async function verifiedModelData() {
655
+ try {
656
+ const consumerRoot = new PathResolver().getConsumerRootDirectory();
657
+ const seedDataDir = path.join(consumerRoot, 'seed-data');
658
+ const subjectsJson = path.join(seedDataDir, 'subjects.json');
659
+ const screensDir = path.join(seedDataDir, 'screen-schemas');
660
+ const projectIsConfigured = await isConfigured();
661
+
662
+ // Check if seed-data directory exists
663
+ if (!fs.existsSync(seedDataDir)) {
664
+ return handleMissingSeedDataDirectory(seedDataDir, screensDir, projectIsConfigured);
665
+ }
666
+
667
+ // Check if subjects.json exists
668
+ if (!fs.existsSync(subjectsJson) && !projectIsConfigured) {
669
+ Logger.info('Required file not found: subjects.json');
670
+ return false;
671
+ }
672
+
673
+ // Check if screens directory exists
674
+ if (!fs.existsSync(screensDir)) {
675
+ return handleMissingScreensDirectory(screensDir, projectIsConfigured);
676
+ }
677
+
678
+ // Check if there are any screen files
679
+ const screenFiles = fs.readdirSync(screensDir).filter((f) => f.endsWith('.json'));
680
+ if (screenFiles.length === 0 && !projectIsConfigured) {
681
+ Logger.info('No screen JSON files found in seed-data/screen-schemas');
682
+ return false;
683
+ }
684
+
685
+ return true;
686
+ } catch (error) {
687
+ Logger.error('Model data verification error:', error);
688
+ return false;
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Handle missing seed-data directory
694
+ * @param {string} seedDataDir - Path to seed-data directory
695
+ * @param {string} screensDir - Path to screens directory
696
+ * @param {boolean} projectIsConfigured - Whether project is configured
697
+ * @returns {boolean} True if handled successfully, false otherwise
698
+ */
699
+ function handleMissingSeedDataDirectory(seedDataDir, screensDir, projectIsConfigured) {
700
+ if (projectIsConfigured) {
701
+ try {
702
+ fs.mkdirSync(seedDataDir, { recursive: true });
703
+ fs.mkdirSync(screensDir, { recursive: true });
704
+ Logger.info('Created seed-data directory structure.');
705
+ return true;
706
+ } catch (error) {
707
+ Logger.error('Failed to create seed-data directory:', error);
708
+ return false;
709
+ }
710
+ }
711
+
712
+ Logger.info('No seed-data directory found.');
713
+ return false;
714
+ }
715
+
716
+ /**
717
+ * Handle missing screens directory
718
+ * @param {string} screensDir - Path to screens directory
719
+ * @param {boolean} projectIsConfigured - Whether project is configured
720
+ * @returns {boolean} True if handled successfully, false otherwise
721
+ */
722
+ function handleMissingScreensDirectory(screensDir, projectIsConfigured) {
723
+ if (projectIsConfigured) {
724
+ try {
725
+ fs.mkdirSync(screensDir, { recursive: true });
726
+ Logger.info('Created screen-schemas directory.');
727
+ return true;
728
+ } catch (error) {
729
+ Logger.error('Failed to create screen-schemas directory:', error);
730
+ return false;
731
+ }
732
+ }
733
+
734
+ Logger.info('No screen-schemas directory found.');
735
+ return false;
736
+ }
737
+
738
+ /**
739
+ * Check if project is configured with valid credentials
740
+ * @returns {Promise<boolean>} True if project is configured, false otherwise
741
+ */
742
+ async function isConfigured() {
743
+ try {
744
+ if (typeof indiciumApi.canConnect === 'function') {
745
+ return await indiciumApi.canConnect();
746
+ }
747
+ if (typeof indiciumApi.canConnectToProject === 'function') {
748
+ return await indiciumApi.canConnectToProject();
749
+ }
750
+ Logger.warn('No valid connection check found on indiciumApi.');
751
+ return false;
752
+ } catch (error) {
753
+ Logger.error('Configuration check failed:', error);
754
+ return false;
755
+ }
756
+ }