retold-data-service 2.0.16 → 2.0.18

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 (28) hide show
  1. package/.claude/launch.json +2 -2
  2. package/.quackage.json +19 -0
  3. package/package.json +13 -6
  4. package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
  5. package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
  6. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
  7. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
  8. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
  9. package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
  10. package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
  11. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
  12. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
  13. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
  14. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
  15. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
  16. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
  17. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
  18. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
  19. package/source/services/data-cloner/web/data-cloner.js +7952 -0
  20. package/source/services/data-cloner/web/data-cloner.js.map +1 -0
  21. package/source/services/data-cloner/web/data-cloner.min.js +2 -0
  22. package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
  23. package/source/services/data-cloner/web/index.html +17 -0
  24. package/test/DataCloner-Integration_tests.js +1205 -0
  25. package/test/DataCloner-Puppeteer_tests.js +502 -0
  26. package/test/integration-report.json +311 -0
  27. package/test/run-integration-tests.js +501 -0
  28. package/source/services/data-cloner/data-cloner-web.html +0 -2706
@@ -0,0 +1,483 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ class DataClonerExportView extends libPictView
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+ }
9
+
10
+ buildConfigObject()
11
+ {
12
+ let tmpProvider = document.getElementById('connProvider').value;
13
+ let tmpConfig = {};
14
+
15
+ // ---- Local Database ----
16
+ tmpConfig.LocalDatabase = { Provider: tmpProvider, Config: {} };
17
+ let tmpDbConfig = tmpConfig.LocalDatabase.Config;
18
+
19
+ if (tmpProvider === 'SQLite')
20
+ {
21
+ tmpDbConfig.SQLiteFilePath = document.getElementById('sqliteFilePath').value.trim() || 'data/cloned.sqlite';
22
+ }
23
+ else if (tmpProvider === 'MySQL')
24
+ {
25
+ tmpDbConfig.host = document.getElementById('mysqlServer').value.trim() || '127.0.0.1';
26
+ tmpDbConfig.port = parseInt(document.getElementById('mysqlPort').value, 10) || 3306;
27
+ tmpDbConfig.user = document.getElementById('mysqlUser').value.trim() || 'root';
28
+ tmpDbConfig.password = document.getElementById('mysqlPassword').value;
29
+ tmpDbConfig.database = document.getElementById('mysqlDatabase').value.trim();
30
+ tmpDbConfig.connectionLimit = parseInt(document.getElementById('mysqlConnectionLimit').value, 10) || 20;
31
+ }
32
+ else if (tmpProvider === 'MSSQL')
33
+ {
34
+ tmpDbConfig.server = document.getElementById('mssqlServer').value.trim() || '127.0.0.1';
35
+ tmpDbConfig.port = parseInt(document.getElementById('mssqlPort').value, 10) || 1433;
36
+ tmpDbConfig.user = document.getElementById('mssqlUser').value.trim() || 'sa';
37
+ tmpDbConfig.password = document.getElementById('mssqlPassword').value;
38
+ tmpDbConfig.database = document.getElementById('mssqlDatabase').value.trim();
39
+ tmpDbConfig.connectionLimit = parseInt(document.getElementById('mssqlConnectionLimit').value, 10) || 20;
40
+ }
41
+ else if (tmpProvider === 'PostgreSQL')
42
+ {
43
+ tmpDbConfig.host = document.getElementById('postgresqlHost').value.trim() || '127.0.0.1';
44
+ tmpDbConfig.port = parseInt(document.getElementById('postgresqlPort').value, 10) || 5432;
45
+ tmpDbConfig.user = document.getElementById('postgresqlUser').value.trim() || 'postgres';
46
+ tmpDbConfig.password = document.getElementById('postgresqlPassword').value;
47
+ tmpDbConfig.database = document.getElementById('postgresqlDatabase').value.trim();
48
+ tmpDbConfig.max = parseInt(document.getElementById('postgresqlConnectionLimit').value, 10) || 10;
49
+ }
50
+
51
+ // ---- Remote Session ----
52
+ tmpConfig.RemoteSession = {};
53
+ let tmpServerURL = document.getElementById('serverURL').value.trim();
54
+ if (tmpServerURL) tmpConfig.RemoteSession.ServerURL = tmpServerURL + '/1.0/';
55
+
56
+ let tmpAuthMethod = document.getElementById('authMethod').value.trim();
57
+ if (tmpAuthMethod) tmpConfig.RemoteSession.AuthenticationMethod = tmpAuthMethod;
58
+
59
+ let tmpAuthURI = document.getElementById('authURI').value.trim();
60
+ if (tmpAuthURI) tmpConfig.RemoteSession.AuthenticationURITemplate = tmpAuthURI;
61
+
62
+ let tmpCheckURI = document.getElementById('checkURI').value.trim();
63
+ if (tmpCheckURI) tmpConfig.RemoteSession.CheckSessionURITemplate = tmpCheckURI;
64
+
65
+ let tmpCookieName = document.getElementById('cookieName').value.trim();
66
+ if (tmpCookieName) tmpConfig.RemoteSession.CookieName = tmpCookieName;
67
+
68
+ let tmpCookieValueAddr = document.getElementById('cookieValueAddr').value.trim();
69
+ if (tmpCookieValueAddr) tmpConfig.RemoteSession.CookieValueAddress = tmpCookieValueAddr;
70
+
71
+ let tmpCookieValueTemplate = document.getElementById('cookieValueTemplate').value.trim();
72
+ if (tmpCookieValueTemplate) tmpConfig.RemoteSession.CookieValueTemplate = tmpCookieValueTemplate;
73
+
74
+ let tmpLoginMarker = document.getElementById('loginMarker').value.trim();
75
+ if (tmpLoginMarker) tmpConfig.RemoteSession.CheckSessionLoginMarker = tmpLoginMarker;
76
+
77
+ // ---- Credentials ----
78
+ let tmpUserName = document.getElementById('userName').value.trim();
79
+ let tmpPassword = document.getElementById('password').value;
80
+ if (tmpUserName || tmpPassword)
81
+ {
82
+ tmpConfig.Credentials = {};
83
+ if (tmpUserName) tmpConfig.Credentials.UserName = tmpUserName;
84
+ if (tmpPassword) tmpConfig.Credentials.Password = tmpPassword;
85
+ }
86
+
87
+ // ---- Schema ----
88
+ let tmpSchemaURL = document.getElementById('schemaURL').value.trim();
89
+ if (tmpSchemaURL) tmpConfig.SchemaURL = tmpSchemaURL;
90
+
91
+ // ---- Tables ----
92
+ let tmpSelectedTables = this.pict.views['DataCloner-Schema'].getSelectedTables();
93
+ if (tmpSelectedTables.length > 0) tmpConfig.Tables = tmpSelectedTables;
94
+
95
+ // ---- Sync Options ----
96
+ tmpConfig.Sync = {};
97
+ tmpConfig.Sync.Mode = document.querySelector('input[name="syncMode"]:checked').value;
98
+ tmpConfig.Sync.PageSize = parseInt(document.getElementById('pageSize').value, 10) || 100;
99
+ tmpConfig.Sync.SyncDeletedRecords = document.getElementById('syncDeletedRecords').checked;
100
+ let tmpPrecision = parseInt(document.getElementById('dateTimePrecisionMS').value, 10);
101
+ if (!isNaN(tmpPrecision) && tmpPrecision !== 1000) tmpConfig.Sync.DateTimePrecisionMS = tmpPrecision;
102
+ let tmpMaxRecords = parseInt(document.getElementById('syncMaxRecords').value, 10);
103
+ if (tmpMaxRecords > 0) tmpConfig.Sync.MaxRecords = tmpMaxRecords;
104
+
105
+ return tmpConfig;
106
+ }
107
+
108
+ buildMeadowIntegrationConfig()
109
+ {
110
+ let tmpProvider = document.getElementById('connProvider').value;
111
+ let tmpConfig = {};
112
+
113
+ // ---- Source ----
114
+ let tmpServerURL = document.getElementById('serverURL').value.trim();
115
+ tmpConfig.Source = { ServerURL: tmpServerURL ? tmpServerURL + '/1.0/' : 'https://localhost:8080/1.0/' };
116
+ // When SessionManager handles auth, Source credentials are not needed
117
+ tmpConfig.Source.UserID = false;
118
+ tmpConfig.Source.Password = false;
119
+
120
+ // ---- Destination ----
121
+ // meadow-integration clone supports MySQL and MSSQL
122
+ tmpConfig.Destination = {};
123
+ if (tmpProvider === 'MySQL')
124
+ {
125
+ tmpConfig.Destination.Provider = 'MySQL';
126
+ tmpConfig.Destination.MySQL = {};
127
+ tmpConfig.Destination.MySQL.server = document.getElementById('mysqlServer').value.trim() || '127.0.0.1';
128
+ tmpConfig.Destination.MySQL.port = parseInt(document.getElementById('mysqlPort').value, 10) || 3306;
129
+ tmpConfig.Destination.MySQL.user = document.getElementById('mysqlUser').value.trim() || 'root';
130
+ tmpConfig.Destination.MySQL.password = document.getElementById('mysqlPassword').value || '';
131
+ tmpConfig.Destination.MySQL.database = document.getElementById('mysqlDatabase').value.trim() || 'meadow';
132
+ tmpConfig.Destination.MySQL.connectionLimit = parseInt(document.getElementById('mysqlConnectionLimit').value, 10) || 20;
133
+ }
134
+ else if (tmpProvider === 'MSSQL')
135
+ {
136
+ tmpConfig.Destination.Provider = 'MSSQL';
137
+ tmpConfig.Destination.MSSQL = {};
138
+ tmpConfig.Destination.MSSQL.server = document.getElementById('mssqlServer').value.trim() || '127.0.0.1';
139
+ tmpConfig.Destination.MSSQL.port = parseInt(document.getElementById('mssqlPort').value, 10) || 1433;
140
+ tmpConfig.Destination.MSSQL.user = document.getElementById('mssqlUser').value.trim() || 'sa';
141
+ tmpConfig.Destination.MSSQL.password = document.getElementById('mssqlPassword').value || '';
142
+ tmpConfig.Destination.MSSQL.database = document.getElementById('mssqlDatabase').value.trim() || 'meadow';
143
+ tmpConfig.Destination.MSSQL.ConnectionPoolLimit = parseInt(document.getElementById('mssqlConnectionLimit').value, 10) || 20;
144
+ }
145
+ else
146
+ {
147
+ // Default to MySQL placeholder for unsupported providers
148
+ tmpConfig.Destination.Provider = 'MySQL';
149
+ tmpConfig.Destination.MySQL = { server: '127.0.0.1', port: 3306, user: 'root', password: '', database: 'meadow', connectionLimit: 20 };
150
+ }
151
+
152
+ // ---- Schema ----
153
+ let tmpSchemaURL = document.getElementById('schemaURL').value.trim();
154
+ if (tmpSchemaURL)
155
+ {
156
+ tmpConfig.SchemaURL = tmpSchemaURL;
157
+ }
158
+ else
159
+ {
160
+ tmpConfig.SchemaPath = './schema/Model-Extended.json';
161
+ }
162
+
163
+ // ---- Sync ----
164
+ tmpConfig.Sync = {};
165
+ tmpConfig.Sync.DefaultSyncMode = document.querySelector('input[name="syncMode"]:checked').value;
166
+ tmpConfig.Sync.PageSize = parseInt(document.getElementById('pageSize').value, 10) || 100;
167
+ let tmpMdwintPrecision = parseInt(document.getElementById('dateTimePrecisionMS').value, 10);
168
+ if (!isNaN(tmpMdwintPrecision)) tmpConfig.Sync.DateTimePrecisionMS = tmpMdwintPrecision;
169
+ let tmpSelectedTables = this.pict.views['DataCloner-Schema'].getSelectedTables();
170
+ tmpConfig.Sync.SyncEntityList = tmpSelectedTables.length > 0 ? tmpSelectedTables : [];
171
+ tmpConfig.Sync.SyncEntityOptions = {};
172
+
173
+ // ---- SessionManager ----
174
+ tmpConfig.SessionManager = { Sessions: {} };
175
+
176
+ let tmpSessionConfig = {};
177
+ tmpSessionConfig.Type = 'Cookie';
178
+
179
+ // Authentication method
180
+ let tmpAuthMethod = document.getElementById('authMethod').value.trim() || 'get';
181
+ tmpSessionConfig.AuthenticationMethod = tmpAuthMethod;
182
+
183
+ // Build the authentication URI template
184
+ let tmpAuthURI = document.getElementById('authURI').value.trim();
185
+ if (tmpAuthURI)
186
+ {
187
+ // If the URI is a relative path, prepend the server URL
188
+ if (tmpAuthURI.charAt(0) === '/')
189
+ {
190
+ tmpSessionConfig.AuthenticationURITemplate = (tmpServerURL || '') + tmpAuthURI;
191
+ }
192
+ else
193
+ {
194
+ tmpSessionConfig.AuthenticationURITemplate = tmpAuthURI;
195
+ }
196
+ }
197
+ else if (tmpServerURL)
198
+ {
199
+ // Default: Meadow-style GET authentication
200
+ if (tmpAuthMethod === 'post')
201
+ {
202
+ tmpSessionConfig.AuthenticationURITemplate = tmpServerURL + '/1.0/Authenticate';
203
+ tmpSessionConfig.AuthenticationRequestBody = {
204
+ UserName: '{~D:Record.UserName~}',
205
+ Password: '{~D:Record.Password~}'
206
+ };
207
+ }
208
+ else
209
+ {
210
+ tmpSessionConfig.AuthenticationURITemplate = tmpServerURL + '/1.0/Authenticate/{~D:Record.UserName~}/{~D:Record.Password~}';
211
+ }
212
+ }
213
+
214
+ // Check session URI
215
+ let tmpCheckURI = document.getElementById('checkURI').value.trim();
216
+ if (tmpCheckURI)
217
+ {
218
+ tmpSessionConfig.CheckSessionURITemplate = tmpCheckURI.charAt(0) === '/' ? (tmpServerURL || '') + tmpCheckURI : tmpCheckURI;
219
+ }
220
+ else if (tmpServerURL)
221
+ {
222
+ tmpSessionConfig.CheckSessionURITemplate = tmpServerURL + '/1.0/CheckSession';
223
+ }
224
+
225
+ // Login marker
226
+ let tmpLoginMarker = document.getElementById('loginMarker').value.trim();
227
+ tmpSessionConfig.CheckSessionLoginMarkerType = 'boolean';
228
+ tmpSessionConfig.CheckSessionLoginMarker = tmpLoginMarker || 'LoggedIn';
229
+
230
+ // Domain match — extract from server URL for auto-injection
231
+ if (tmpServerURL)
232
+ {
233
+ try
234
+ {
235
+ let tmpUrlObj = new URL(tmpServerURL);
236
+ tmpSessionConfig.DomainMatch = tmpUrlObj.host;
237
+ }
238
+ catch (pError)
239
+ {
240
+ tmpSessionConfig.DomainMatch = tmpServerURL;
241
+ }
242
+ }
243
+
244
+ // Cookie injection
245
+ let tmpCookieName = document.getElementById('cookieName').value.trim();
246
+ tmpSessionConfig.CookieName = tmpCookieName || 'SessionID';
247
+
248
+ let tmpCookieValueAddr = document.getElementById('cookieValueAddr').value.trim();
249
+ if (tmpCookieValueAddr) tmpSessionConfig.CookieValueAddress = tmpCookieValueAddr;
250
+
251
+ let tmpCookieValueTemplate = document.getElementById('cookieValueTemplate').value.trim();
252
+ if (tmpCookieValueTemplate) tmpSessionConfig.CookieValueTemplate = tmpCookieValueTemplate;
253
+
254
+ // Credentials
255
+ let tmpUserName = document.getElementById('userName').value.trim();
256
+ let tmpPassword = document.getElementById('password').value;
257
+ tmpSessionConfig.Credentials = {};
258
+ if (tmpUserName) tmpSessionConfig.Credentials.UserName = tmpUserName;
259
+ if (tmpPassword) tmpSessionConfig.Credentials.Password = tmpPassword;
260
+
261
+ tmpConfig.SessionManager.Sessions.SourceAPI = tmpSessionConfig;
262
+
263
+ return tmpConfig;
264
+ }
265
+
266
+ generateConfig()
267
+ {
268
+ let tmpConfig = this.buildConfigObject();
269
+ let tmpJson = JSON.stringify(tmpConfig, null, '\t');
270
+
271
+ let tmpTextarea = document.getElementById('configOutput');
272
+ tmpTextarea.value = tmpJson;
273
+ tmpTextarea.style.display = '';
274
+
275
+ // Build CLI flags from export options
276
+ let tmpLogFlag = document.getElementById('syncLogFile').checked ? ' --log' : '';
277
+ let tmpMaxFlag = '';
278
+ let tmpExportMax = parseInt(document.getElementById('syncMaxRecords').value, 10);
279
+ if (tmpExportMax > 0) tmpMaxFlag = ' --max ' + tmpExportMax;
280
+
281
+ // Build CLI command (with config file)
282
+ let tmpCliDiv = document.getElementById('cliCommand');
283
+ tmpCliDiv.style.display = '';
284
+ tmpCliDiv.querySelector('div').textContent = 'npx retold-data-service-clone --config clone-config.json --run' + tmpLogFlag + tmpMaxFlag;
285
+
286
+ // Build one-liner (no config file needed) using --config-json
287
+ let tmpOneShotDiv = document.getElementById('cliOneShot');
288
+ tmpOneShotDiv.style.display = '';
289
+ let tmpCompactJSON = JSON.stringify(tmpConfig);
290
+ // Escape single quotes for shell wrapping
291
+ let tmpEscapedJSON = tmpCompactJSON.replace(/'/g, "'\\''");
292
+ let tmpOneShot = "npx retold-data-service-clone --config-json '" + tmpEscapedJSON + "' --run" + tmpLogFlag + tmpMaxFlag;
293
+ tmpOneShotDiv.querySelector('div').textContent = tmpOneShot;
294
+
295
+ // ---- meadow-integration (mdwint clone) config ----
296
+ let tmpMdwintConfig = this.buildMeadowIntegrationConfig();
297
+ let tmpMdwintJSON = JSON.stringify(tmpMdwintConfig, null, '\t');
298
+
299
+ let tmpMdwintDiv = document.getElementById('mdwintExport');
300
+ tmpMdwintDiv.style.display = '';
301
+
302
+ let tmpMdwintTextarea = document.getElementById('mdwintConfigOutput');
303
+ tmpMdwintTextarea.value = tmpMdwintJSON;
304
+
305
+ // Build the mdwint CLI command
306
+ let tmpMdwintCLI = 'mdwint clone --schema_path ./schema/Model-Extended.json';
307
+ let tmpMdwintCLIDiv = document.getElementById('mdwintCLICommand');
308
+ tmpMdwintCLIDiv.querySelector('div').textContent = tmpMdwintCLI;
309
+
310
+ // Provider compatibility note
311
+ let tmpProvider = document.getElementById('connProvider').value;
312
+ if (tmpProvider !== 'MySQL' && tmpProvider !== 'MSSQL')
313
+ {
314
+ this.pict.providers.DataCloner.setStatus('mdwintConfigStatus', 'Note: mdwint clone only supports MySQL and MSSQL destinations. The config defaults to MySQL; update the Destination section for your target database.', 'warn');
315
+ }
316
+ else
317
+ {
318
+ this.pict.providers.DataCloner.setStatus('mdwintConfigStatus', '', '');
319
+ }
320
+
321
+ this.pict.providers.DataCloner.setStatus('configExportStatus', 'Config generated. Save as clone-config.json or copy the one-liner below.', 'ok');
322
+ }
323
+
324
+ copyConfig()
325
+ {
326
+ let tmpTextarea = document.getElementById('configOutput');
327
+ if (!tmpTextarea.value)
328
+ {
329
+ this.pict.providers.DataCloner.setStatus('configExportStatus', 'Generate a config first.', 'warn');
330
+ return;
331
+ }
332
+ let tmpSelf = this;
333
+ navigator.clipboard.writeText(tmpTextarea.value).then(function()
334
+ {
335
+ tmpSelf.pict.providers.DataCloner.setStatus('configExportStatus', 'Config copied to clipboard.', 'ok');
336
+ });
337
+ }
338
+
339
+ copyCLI()
340
+ {
341
+ let tmpCmd = document.getElementById('cliCommand').querySelector('div').textContent;
342
+ let tmpSelf = this;
343
+ navigator.clipboard.writeText(tmpCmd).then(function()
344
+ {
345
+ tmpSelf.pict.providers.DataCloner.setStatus('configExportStatus', 'CLI command copied to clipboard.', 'ok');
346
+ });
347
+ }
348
+
349
+ copyOneShot()
350
+ {
351
+ let tmpCmd = document.getElementById('cliOneShot').querySelector('div').textContent;
352
+ let tmpSelf = this;
353
+ navigator.clipboard.writeText(tmpCmd).then(function()
354
+ {
355
+ tmpSelf.pict.providers.DataCloner.setStatus('configExportStatus', 'One-liner copied to clipboard.', 'ok');
356
+ });
357
+ }
358
+
359
+ downloadConfig()
360
+ {
361
+ let tmpTextarea = document.getElementById('configOutput');
362
+ if (!tmpTextarea.value)
363
+ {
364
+ this.generateConfig();
365
+ }
366
+ let tmpBlob = new Blob([tmpTextarea.value], { type: 'application/json' });
367
+ let tmpAnchor = document.createElement('a');
368
+ tmpAnchor.href = URL.createObjectURL(tmpBlob);
369
+ tmpAnchor.download = 'clone-config.json';
370
+ tmpAnchor.click();
371
+ URL.revokeObjectURL(tmpAnchor.href);
372
+ this.pict.providers.DataCloner.setStatus('configExportStatus', 'Config downloaded as clone-config.json.', 'ok');
373
+ }
374
+
375
+ copyMdwintConfig()
376
+ {
377
+ let tmpTextarea = document.getElementById('mdwintConfigOutput');
378
+ if (!tmpTextarea.value)
379
+ {
380
+ this.pict.providers.DataCloner.setStatus('mdwintConfigStatus', 'Generate a config first.', 'warn');
381
+ return;
382
+ }
383
+ let tmpSelf = this;
384
+ navigator.clipboard.writeText(tmpTextarea.value).then(function()
385
+ {
386
+ tmpSelf.pict.providers.DataCloner.setStatus('mdwintConfigStatus', '.meadow.config.json copied to clipboard.', 'ok');
387
+ });
388
+ }
389
+
390
+ copyMdwintCLI()
391
+ {
392
+ let tmpCmd = document.getElementById('mdwintCLICommand').querySelector('div').textContent;
393
+ let tmpSelf = this;
394
+ navigator.clipboard.writeText(tmpCmd).then(function()
395
+ {
396
+ tmpSelf.pict.providers.DataCloner.setStatus('mdwintConfigStatus', 'mdwint CLI command copied to clipboard.', 'ok');
397
+ });
398
+ }
399
+
400
+ downloadMdwintConfig()
401
+ {
402
+ let tmpTextarea = document.getElementById('mdwintConfigOutput');
403
+ if (!tmpTextarea.value)
404
+ {
405
+ this.generateConfig();
406
+ }
407
+ let tmpBlob = new Blob([tmpTextarea.value], { type: 'application/json' });
408
+ let tmpAnchor = document.createElement('a');
409
+ tmpAnchor.href = URL.createObjectURL(tmpBlob);
410
+ tmpAnchor.download = '.meadow.config.json';
411
+ tmpAnchor.click();
412
+ URL.revokeObjectURL(tmpAnchor.href);
413
+ this.pict.providers.DataCloner.setStatus('mdwintConfigStatus', 'Config downloaded as .meadow.config.json.', 'ok');
414
+ }
415
+ }
416
+
417
+ module.exports = DataClonerExportView;
418
+
419
+ module.exports.default_configuration =
420
+ {
421
+ ViewIdentifier: 'DataCloner-Export',
422
+ DefaultRenderable: 'DataCloner-Export',
423
+ DefaultDestinationAddress: '#DataCloner-Section-Export',
424
+ Templates:
425
+ [
426
+ {
427
+ Hash: 'DataCloner-Export',
428
+ Template: /*html*/`
429
+ <div class="accordion-row">
430
+ <div class="accordion-number">6</div>
431
+ <div class="accordion-card" id="section6" data-section="6">
432
+ <div class="accordion-header" onclick="pict.views['DataCloner-Layout'].toggleSection('section6')">
433
+ <div class="accordion-title">Export Configuration</div>
434
+ <div class="accordion-preview" id="preview6">Generate JSON config for headless cloning</div>
435
+ <div class="accordion-toggle">&#9660;</div>
436
+ </div>
437
+ <div class="accordion-body">
438
+ <p style="font-size:0.9em; color:#666; margin-bottom:10px">Generate a JSON config file from your current settings. Use it to run headless clones from the command line.</p>
439
+ <div style="display:flex; gap:8px; margin-bottom:10px">
440
+ <button class="primary" onclick="pict.views['DataCloner-Export'].generateConfig()">Generate Config</button>
441
+ <button class="secondary" onclick="pict.views['DataCloner-Export'].copyConfig()">Copy to Clipboard</button>
442
+ <button class="secondary" onclick="pict.views['DataCloner-Export'].downloadConfig()">Download JSON</button>
443
+ </div>
444
+ <div id="configExportStatus"></div>
445
+ <div id="cliCommand" style="display:none; margin-bottom:10px">
446
+ <label style="margin-bottom:4px">CLI Command <span style="color:#888; font-weight:normal">(with config file)</span></label>
447
+ <div style="background:#1a1a1a; color:#4fc3f7; padding:10px 14px; border-radius:4px; font-family:monospace; font-size:0.9em; word-break:break-all; cursor:pointer" onclick="pict.views['DataCloner-Export'].copyCLI()" title="Click to copy"></div>
448
+ </div>
449
+ <div id="cliOneShot" style="display:none; margin-bottom:10px">
450
+ <label style="margin-bottom:4px">One-liner <span style="color:#888; font-weight:normal">(no config file needed)</span></label>
451
+ <div style="background:#1a1a1a; color:#4fc3f7; padding:10px 14px; border-radius:4px; font-family:monospace; font-size:0.9em; word-break:break-all; cursor:pointer; white-space:pre-wrap" onclick="pict.views['DataCloner-Export'].copyOneShot()" title="Click to copy"></div>
452
+ </div>
453
+ <textarea id="configOutput" style="display:none; width:100%; min-height:300px; font-family:monospace; font-size:0.85em; padding:10px; border:1px solid #ccc; border-radius:4px; background:#fafafa; tab-size:4; resize:vertical" readonly></textarea>
454
+
455
+ <div id="mdwintExport" style="display:none; margin-top:16px; padding-top:16px; border-top:1px solid #eee">
456
+ <h3 style="margin:0 0 8px; font-size:1em">meadow-integration CLI <span style="color:#888; font-weight:normal; font-size:0.85em">(mdwint clone)</span></h3>
457
+ <p style="font-size:0.85em; color:#666; margin-bottom:8px">Save as <code>.meadow.config.json</code> in your project root, then run the command below. Requires a local Meadow extended schema JSON file.</p>
458
+ <div style="display:flex; gap:8px; margin-bottom:10px">
459
+ <button class="secondary" onclick="pict.views['DataCloner-Export'].copyMdwintConfig()">Copy Config</button>
460
+ <button class="secondary" onclick="pict.views['DataCloner-Export'].downloadMdwintConfig()">Download .meadow.config.json</button>
461
+ </div>
462
+ <div id="mdwintCLICommand" style="margin-bottom:10px">
463
+ <label style="margin-bottom:4px">CLI Command</label>
464
+ <div style="background:#1a1a1a; color:#4fc3f7; padding:10px 14px; border-radius:4px; font-family:monospace; font-size:0.9em; word-break:break-all; cursor:pointer" onclick="pict.views['DataCloner-Export'].copyMdwintCLI()" title="Click to copy"></div>
465
+ </div>
466
+ <div id="mdwintConfigStatus"></div>
467
+ <textarea id="mdwintConfigOutput" style="width:100%; min-height:250px; font-family:monospace; font-size:0.85em; padding:10px; border:1px solid #ccc; border-radius:4px; background:#fafafa; tab-size:4; resize:vertical" readonly></textarea>
468
+ </div>
469
+ </div>
470
+ </div>
471
+ </div>
472
+ `
473
+ }
474
+ ],
475
+ Renderables:
476
+ [
477
+ {
478
+ RenderableHash: 'DataCloner-Export',
479
+ TemplateHash: 'DataCloner-Export',
480
+ DestinationAddress: '#DataCloner-Section-Export'
481
+ }
482
+ ]
483
+ };