@snutils/snu 0.3.4 → 0.3.6

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 (54) hide show
  1. package/dist/cli/format.d.ts +0 -3
  2. package/dist/cli/format.d.ts.map +1 -1
  3. package/dist/cli/format.js +45 -3
  4. package/dist/cli/format.js.map +1 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +46 -1
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/mcp/index.d.ts.map +1 -1
  9. package/dist/mcp/index.js +12 -1
  10. package/dist/mcp/index.js.map +1 -1
  11. package/dist/nowsdk/NowSdkBuild.d.ts +53 -0
  12. package/dist/nowsdk/NowSdkBuild.d.ts.map +1 -0
  13. package/dist/nowsdk/NowSdkBuild.js +152 -0
  14. package/dist/nowsdk/NowSdkBuild.js.map +1 -0
  15. package/dist/nowsdk/NowSdkChoices.d.ts +6 -0
  16. package/dist/nowsdk/NowSdkChoices.d.ts.map +1 -0
  17. package/dist/nowsdk/NowSdkChoices.js +105 -0
  18. package/dist/nowsdk/NowSdkChoices.js.map +1 -0
  19. package/dist/nowsdk/NowSdkFlows.d.ts +99 -0
  20. package/dist/nowsdk/NowSdkFlows.d.ts.map +1 -0
  21. package/dist/nowsdk/NowSdkFlows.js +203 -0
  22. package/dist/nowsdk/NowSdkFlows.js.map +1 -0
  23. package/dist/nowsdk/NowSdkProject.d.ts +71 -0
  24. package/dist/nowsdk/NowSdkProject.d.ts.map +1 -0
  25. package/dist/nowsdk/NowSdkProject.js +341 -0
  26. package/dist/nowsdk/NowSdkProject.js.map +1 -0
  27. package/dist/nowsdk/NowSdkPull.d.ts +147 -0
  28. package/dist/nowsdk/NowSdkPull.d.ts.map +1 -0
  29. package/dist/nowsdk/NowSdkPull.js +824 -0
  30. package/dist/nowsdk/NowSdkPull.js.map +1 -0
  31. package/dist/nowsdk/NowSdkSource.d.ts +6 -0
  32. package/dist/nowsdk/NowSdkSource.d.ts.map +1 -0
  33. package/dist/nowsdk/NowSdkSource.js +102 -0
  34. package/dist/nowsdk/NowSdkSource.js.map +1 -0
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +114 -0
  37. package/dist/registry.js.map +1 -1
  38. package/dist/server/dispatcher.d.ts +1 -0
  39. package/dist/server/dispatcher.d.ts.map +1 -1
  40. package/dist/server/dispatcher.js +65 -0
  41. package/dist/server/dispatcher.js.map +1 -1
  42. package/dist/server/httpBridge.d.ts.map +1 -1
  43. package/dist/server/httpBridge.js +2 -0
  44. package/dist/server/httpBridge.js.map +1 -1
  45. package/dist/server/policy.d.ts.map +1 -1
  46. package/dist/server/policy.js +8 -0
  47. package/dist/server/policy.js.map +1 -1
  48. package/dist/server/wsBridge.d.ts.map +1 -1
  49. package/dist/server/wsBridge.js +7 -1
  50. package/dist/server/wsBridge.js.map +1 -1
  51. package/dist/types.d.ts +6 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/types.js.map +1 -1
  54. package/package.json +2 -1
@@ -0,0 +1,824 @@
1
+ "use strict";
2
+ // "Pull instance changes into NOW SDK app": the SN Utils helper tab downloads
3
+ // the application package with the browser session (the same download
4
+ // `now-sdk transform` makes with its own login), and the SDK's own converter
5
+ // turns it into Fluent source in a temporary copy of the project. The user
6
+ // reviews the differences and picks what to copy into the project.
7
+ //
8
+ // Kept free of `vscode` imports so it can be unit tested under plain Node.
9
+ // packages/snu/src/nowsdk/ holds a byte-identical copy for the standalone
10
+ // bridge; src/test/nowSdkShared.test.ts fails when the two drift.
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.TABLE_LABELS = exports.MAX_DOWNLOAD_BYTES = void 0;
46
+ exports.extractZip = extractZip;
47
+ exports.listProjectFiles = listProjectFiles;
48
+ exports.diffProjects = diffProjects;
49
+ exports.stagePull = stagePull;
50
+ exports.applyPullChanges = applyPullChanges;
51
+ exports.captureSyncState = captureSyncState;
52
+ exports.restoreSyncState = restoreSyncState;
53
+ exports.applyPullWithUndo = applyPullWithUndo;
54
+ exports.undoPull = undoPull;
55
+ exports.gitUncommittedPaths = gitUncommittedPaths;
56
+ exports.hasBaseline = hasBaseline;
57
+ exports.readPackageRecords = readPackageRecords;
58
+ exports.saveBaselineFromPackage = saveBaselineFromPackage;
59
+ exports.readBaselineRecord = readBaselineRecord;
60
+ exports.readRecordFields = readRecordFields;
61
+ exports.fieldsBuiltDifferently = fieldsBuiltDifferently;
62
+ exports.changedFieldNames = changedFieldNames;
63
+ exports.readBuiltRecord = readBuiltRecord;
64
+ exports.readKeyNames = readKeyNames;
65
+ exports.recordForReading = recordForReading;
66
+ exports.dismissInstanceChange = dismissInstanceChange;
67
+ exports.instanceChangesFromRecords = instanceChangesFromRecords;
68
+ exports.instanceChangesFromPackage = instanceChangesFromPackage;
69
+ exports.deployConflicts = deployConflicts;
70
+ exports.hasPulledBefore = hasPulledBefore;
71
+ exports.readSyncMarkers = readSyncMarkers;
72
+ exports.markDeployed = markDeployed;
73
+ exports.markPulled = markPulled;
74
+ exports.cleanupPull = cleanupPull;
75
+ const cp = __importStar(require("child_process"));
76
+ const fs = __importStar(require("fs"));
77
+ const os = __importStar(require("os"));
78
+ const path = __importStar(require("path"));
79
+ const zlib = __importStar(require("zlib"));
80
+ const NowSdkProject_1 = require("./NowSdkProject");
81
+ const NowSdkBuild_1 = require("./NowSdkBuild");
82
+ const NowSdkChoices_1 = require("./NowSdkChoices");
83
+ const NowSdkSource_1 = require("./NowSdkSource");
84
+ exports.MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
85
+ const MAX_UNPACKED_BYTES = 200 * 1024 * 1024;
86
+ const MAX_ENTRIES = 20000;
87
+ /** Folders never copied into the staging project nor compared. */
88
+ const SKIP = new Set(['node_modules', 'dist', 'target', '.snu', '.git']);
89
+ /**
90
+ * Unpack a zip into `dest` with Node's zlib. Supports stored and deflated
91
+ * entries, which is what the instance produces. Entry names that would land
92
+ * outside `dest` are refused.
93
+ */
94
+ function extractZip(zip, dest) {
95
+ const root = path.resolve(dest);
96
+ // End of central directory: scan back from the end (it may carry a comment).
97
+ let eocd = -1;
98
+ for (let i = zip.length - 22; i >= Math.max(0, zip.length - 65557); i--) {
99
+ if (zip.readUInt32LE(i) === 0x06054b50) {
100
+ eocd = i;
101
+ break;
102
+ }
103
+ }
104
+ if (eocd < 0)
105
+ throw new NowSdkProject_1.NowSdkProjectError('The downloaded package is not a valid zip file.');
106
+ const entries = zip.readUInt16LE(eocd + 10);
107
+ let offset = zip.readUInt32LE(eocd + 16);
108
+ if (entries > MAX_ENTRIES)
109
+ throw new NowSdkProject_1.NowSdkProjectError('The downloaded package has too many files.');
110
+ const written = [];
111
+ let unpacked = 0;
112
+ for (let n = 0; n < entries; n++) {
113
+ if (offset + 46 > zip.length || zip.readUInt32LE(offset) !== 0x02014b50) {
114
+ throw new NowSdkProject_1.NowSdkProjectError('The downloaded package is damaged.');
115
+ }
116
+ const method = zip.readUInt16LE(offset + 10);
117
+ const compressedSize = zip.readUInt32LE(offset + 20);
118
+ const size = zip.readUInt32LE(offset + 24);
119
+ const nameLength = zip.readUInt16LE(offset + 28);
120
+ const extraLength = zip.readUInt16LE(offset + 30);
121
+ const commentLength = zip.readUInt16LE(offset + 32);
122
+ const localOffset = zip.readUInt32LE(offset + 42);
123
+ const name = zip.subarray(offset + 46, offset + 46 + nameLength).toString('utf8');
124
+ offset += 46 + nameLength + extraLength + commentLength;
125
+ const relative = name.replace(/\\/g, '/').replace(/^\/+/, '');
126
+ if (!relative || relative.endsWith('/'))
127
+ continue; // directory entry
128
+ const target = path.resolve(root, relative);
129
+ if (!target.startsWith(root + path.sep) || relative.split('/').includes('..')) {
130
+ throw new NowSdkProject_1.NowSdkProjectError(`The downloaded package contains an unsafe path: ${name}`);
131
+ }
132
+ unpacked += size;
133
+ if (unpacked > MAX_UNPACKED_BYTES)
134
+ throw new NowSdkProject_1.NowSdkProjectError('The downloaded package is too large to unpack.');
135
+ if (zip.readUInt32LE(localOffset) !== 0x04034b50)
136
+ throw new NowSdkProject_1.NowSdkProjectError('The downloaded package is damaged.');
137
+ const dataStart = localOffset + 30 + zip.readUInt16LE(localOffset + 26) + zip.readUInt16LE(localOffset + 28);
138
+ const data = zip.subarray(dataStart, dataStart + compressedSize);
139
+ let content;
140
+ if (method === 0)
141
+ content = Buffer.from(data);
142
+ else if (method === 8)
143
+ content = zlib.inflateRawSync(data);
144
+ else
145
+ throw new NowSdkProject_1.NowSdkProjectError(`The downloaded package uses an unsupported compression (${method}).`);
146
+ fs.mkdirSync(path.dirname(target), { recursive: true });
147
+ fs.writeFileSync(target, content);
148
+ written.push(relative);
149
+ }
150
+ return written;
151
+ }
152
+ function copyProject(from, to) {
153
+ fs.mkdirSync(to, { recursive: true });
154
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
155
+ if (SKIP.has(entry.name) || entry.isSymbolicLink())
156
+ continue;
157
+ const src = path.join(from, entry.name);
158
+ const dst = path.join(to, entry.name);
159
+ if (entry.isDirectory())
160
+ copyProject(src, dst);
161
+ else if (entry.isFile())
162
+ fs.copyFileSync(src, dst);
163
+ }
164
+ }
165
+ /** Project files (forward-slash relative paths), leaving out build output, dependencies and .snu. */
166
+ function listProjectFiles(root) {
167
+ return listFiles(root);
168
+ }
169
+ function listFiles(root, dir = root, out = []) {
170
+ let entries = [];
171
+ try {
172
+ entries = fs.readdirSync(dir, { withFileTypes: true });
173
+ }
174
+ catch {
175
+ return out;
176
+ }
177
+ for (const entry of entries) {
178
+ if (SKIP.has(entry.name) || entry.isSymbolicLink())
179
+ continue;
180
+ const full = path.join(dir, entry.name);
181
+ if (entry.isDirectory())
182
+ listFiles(root, full, out);
183
+ else if (entry.isFile())
184
+ out.push(path.relative(root, full).split(path.sep).join('/'));
185
+ }
186
+ return out;
187
+ }
188
+ /** Files that differ between the project and the staged copy. */
189
+ function diffProjects(projectRoot, stagingRoot) {
190
+ const before = new Set(listFiles(projectRoot));
191
+ const after = new Set(listFiles(stagingRoot));
192
+ const changes = [];
193
+ for (const file of after) {
194
+ if (!before.has(file)) {
195
+ changes.push({ path: file, status: 'added' });
196
+ }
197
+ else if (!fs.readFileSync(path.join(projectRoot, file)).equals(fs.readFileSync(path.join(stagingRoot, file)))) {
198
+ changes.push({ path: file, status: 'modified' });
199
+ }
200
+ }
201
+ for (const file of before) {
202
+ if (!after.has(file))
203
+ changes.push({ path: file, status: 'removed' });
204
+ }
205
+ return changes.sort((a, b) => a.path.localeCompare(b.path));
206
+ }
207
+ /**
208
+ * Convert a downloaded application package into Fluent in a temporary copy of
209
+ * the project and report what differs. The project itself is not touched.
210
+ */
211
+ async function stagePull(projectRoot, packageZip, onOutput) {
212
+ if (packageZip.length > exports.MAX_DOWNLOAD_BYTES)
213
+ throw new NowSdkProject_1.NowSdkProjectError('The downloaded package is too large.');
214
+ (0, NowSdkProject_1.assertSupportedProject)((0, NowSdkProject_1.readNowSdkProject)(projectRoot));
215
+ (0, NowSdkProject_1.assertSupportedSdk)(projectRoot);
216
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snu-pull-'));
217
+ try {
218
+ const xmlDir = path.join(tempDir, 'package');
219
+ extractZip(packageZip, xmlDir);
220
+ const stagingRoot = path.join(tempDir, path.basename(projectRoot));
221
+ copyProject(projectRoot, stagingRoot);
222
+ fs.symlinkSync(path.join(projectRoot, 'node_modules'), path.join(stagingRoot, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir');
223
+ await (0, NowSdkBuild_1.runNowSdk)(stagingRoot, ['transform', '--from', xmlDir], onOutput);
224
+ let changes = diffProjects(projectRoot, stagingRoot);
225
+ // The SDK's converter can need a second pass to settle (it adds the
226
+ // IDs of records it created on the first pass to keys.ts), so one pull
227
+ // leaves the project stable instead of changing it again next time.
228
+ if (changes.length > 0) {
229
+ await (0, NowSdkBuild_1.runNowSdk)(stagingRoot, ['transform', '--from', xmlDir], onOutput);
230
+ changes = diffProjects(projectRoot, stagingRoot);
231
+ }
232
+ return { tempDir, stagingRoot, changes };
233
+ }
234
+ catch (e) {
235
+ cleanupPull(tempDir);
236
+ throw e;
237
+ }
238
+ }
239
+ /** Copy the chosen changes from the staged copy into the project. */
240
+ function applyPullChanges(projectRoot, stagingRoot, changes) {
241
+ const root = path.resolve(projectRoot);
242
+ for (const change of changes) {
243
+ const target = path.resolve(root, change.path);
244
+ if (!target.startsWith(root + path.sep))
245
+ continue;
246
+ if (change.status === 'removed') {
247
+ fs.rmSync(target, { force: true });
248
+ }
249
+ else {
250
+ fs.mkdirSync(path.dirname(target), { recursive: true });
251
+ fs.copyFileSync(path.join(stagingRoot, change.path), target);
252
+ }
253
+ }
254
+ }
255
+ /** Save the baseline for an instance and the pull marker, to put back later. */
256
+ function captureSyncState(projectRoot, instanceName) {
257
+ const dir = baselineDir(projectRoot, instanceName);
258
+ let baseline = null;
259
+ if (fs.existsSync(dir)) {
260
+ baseline = new Map();
261
+ for (const name of ['format', ...listDir(path.join(dir, 'update')).map((f) => path.join('update', f))]) {
262
+ try {
263
+ baseline.set(name, fs.readFileSync(path.join(dir, name)));
264
+ }
265
+ catch { }
266
+ }
267
+ }
268
+ let marker = null;
269
+ try {
270
+ marker = fs.readFileSync(pullMarker(projectRoot));
271
+ }
272
+ catch { }
273
+ return { instance: instanceName, baseline, pullMarker: marker };
274
+ }
275
+ /** Put back a state saved with captureSyncState. */
276
+ function restoreSyncState(projectRoot, state) {
277
+ const dir = baselineDir(projectRoot, state.instance);
278
+ fs.rmSync(dir, { recursive: true, force: true });
279
+ if (state.baseline) {
280
+ fs.mkdirSync(path.join(dir, 'update'), { recursive: true });
281
+ for (const [name, content] of state.baseline)
282
+ fs.writeFileSync(path.join(dir, name), content);
283
+ }
284
+ if (state.pullMarker)
285
+ fs.writeFileSync(pullMarker(projectRoot), state.pullMarker);
286
+ else
287
+ fs.rmSync(pullMarker(projectRoot), { force: true });
288
+ }
289
+ function listDir(dir) {
290
+ try {
291
+ return fs.readdirSync(dir);
292
+ }
293
+ catch {
294
+ return [];
295
+ }
296
+ }
297
+ /**
298
+ * Apply every added and modified file and remember what was there before.
299
+ * Removals are never applied this way: a file missing from the instance
300
+ * package is left for the user to delete.
301
+ */
302
+ function applyPullWithUndo(projectRoot, stagingRoot, changes, instanceName) {
303
+ const root = path.resolve(projectRoot);
304
+ const undo = { files: [], ...(instanceName ? { sync: captureSyncState(projectRoot, instanceName) } : {}) };
305
+ const applied = changes.filter((c) => c.status !== 'removed');
306
+ for (const change of applied) {
307
+ const target = path.resolve(root, change.path);
308
+ if (!target.startsWith(root + path.sep))
309
+ continue;
310
+ undo.files.push({ path: change.path, previous: fs.existsSync(target) ? fs.readFileSync(target) : null });
311
+ }
312
+ applyPullChanges(projectRoot, stagingRoot, applied);
313
+ return undo;
314
+ }
315
+ /** Put back what a pull changed: restore previous contents, remove added files. */
316
+ function undoPull(projectRoot, undo) {
317
+ const root = path.resolve(projectRoot);
318
+ for (const file of undo.files) {
319
+ const target = path.resolve(root, file.path);
320
+ if (!target.startsWith(root + path.sep))
321
+ continue;
322
+ if (file.previous === null) {
323
+ fs.rmSync(target, { force: true });
324
+ // Remove folders the pull created and left empty.
325
+ let dir = path.dirname(target);
326
+ while (dir.startsWith(root + path.sep)) {
327
+ try {
328
+ if (fs.readdirSync(dir).length > 0)
329
+ break;
330
+ fs.rmdirSync(dir);
331
+ }
332
+ catch {
333
+ break;
334
+ }
335
+ dir = path.dirname(dir);
336
+ }
337
+ }
338
+ else {
339
+ fs.mkdirSync(path.dirname(target), { recursive: true });
340
+ fs.writeFileSync(target, file.previous);
341
+ }
342
+ }
343
+ // The instance changes the pull took in count as changes again.
344
+ if (undo.sync)
345
+ restoreSyncState(projectRoot, undo.sync);
346
+ }
347
+ /**
348
+ * Of `paths` (relative to the project), the ones with uncommitted changes in
349
+ * git. null when the project is not in a git repository or git is missing, in
350
+ * which case a pull is reviewed file by file instead.
351
+ */
352
+ function gitUncommittedPaths(projectRoot, paths) {
353
+ return new Promise((resolve) => {
354
+ cp.execFile('git', ['-C', projectRoot, 'status', '--porcelain', '-z', '--untracked-files=all', '--', ...paths], { timeout: 15000 }, (err, stdout) => {
355
+ if (err)
356
+ return resolve(null);
357
+ const dirty = new Set();
358
+ const entries = String(stdout).split('\0').filter(Boolean);
359
+ for (let i = 0; i < entries.length; i++) {
360
+ const entry = entries[i];
361
+ const status = entry.slice(0, 2);
362
+ dirty.add(entry.slice(3));
363
+ if (status.includes('R') || status.includes('C'))
364
+ i++; // skip the rename source
365
+ }
366
+ cp.execFile('git', ['-C', projectRoot, 'rev-parse', '--show-prefix'], { timeout: 15000 }, (err2, prefixOut) => {
367
+ if (err2)
368
+ return resolve(null);
369
+ const prefix = String(prefixOut).trim();
370
+ resolve(paths.filter((p) => dirty.has(prefix + p)));
371
+ });
372
+ });
373
+ });
374
+ }
375
+ exports.TABLE_LABELS = {
376
+ sys_script_include: 'Script Include',
377
+ sys_script: 'Business Rule',
378
+ sys_script_client: 'Client Script',
379
+ sys_ui_action: 'UI Action',
380
+ sys_ui_policy: 'UI Policy',
381
+ sys_script_fix: 'Fix Script',
382
+ sysauto_script: 'Scheduled Script',
383
+ sys_hub_flow: 'Flow',
384
+ sys_hub_action_type_definition: 'Flow Action',
385
+ sys_db_object: 'Table',
386
+ sys_dictionary: 'Column',
387
+ sys_choice_set: 'Choices',
388
+ sys_choice_v2: 'Choices',
389
+ sys_security_acl: 'ACL',
390
+ sys_user_role: 'Role',
391
+ sys_properties: 'System Property',
392
+ sys_ws_definition: 'Scripted REST API',
393
+ sp_widget: 'Widget',
394
+ sys_ui_page: 'UI Page',
395
+ sys_ux_lib_asset: 'UI Asset',
396
+ sys_module: 'Server Module',
397
+ sys_app_application: 'Menu',
398
+ sys_app_module: 'Module',
399
+ sys_db_view: 'Database View',
400
+ sys_ui_message: 'UI Message',
401
+ };
402
+ const BASELINE_FORMAT = 'records-v1';
403
+ function baselineDir(projectRoot, instanceName) {
404
+ return path.join(projectRoot, '.snu', 'baseline', instanceName.replace(/[^\w.-]/g, '_'));
405
+ }
406
+ /** A baseline in the current format exists; an older one counts as none until the next sync. */
407
+ function hasBaseline(projectRoot, instanceName) {
408
+ try {
409
+ return fs.readFileSync(path.join(baselineDir(projectRoot, instanceName), 'format'), 'utf8').trim() === BASELINE_FORMAT;
410
+ }
411
+ catch {
412
+ return false;
413
+ }
414
+ }
415
+ /** The app's records in a downloaded package, keyed by file name. */
416
+ function readPackageRecords(packageZip) {
417
+ return packageRecords(packageZip);
418
+ }
419
+ function packageRecords(packageZip) {
420
+ if (packageZip.length > exports.MAX_DOWNLOAD_BYTES)
421
+ throw new NowSdkProject_1.NowSdkProjectError('The downloaded package is too large.');
422
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snu-records-'));
423
+ try {
424
+ extractZip(packageZip, tempDir);
425
+ return readBuiltRecords(tempDir);
426
+ }
427
+ finally {
428
+ cleanupPull(tempDir);
429
+ }
430
+ }
431
+ /**
432
+ * Record the instance's version of the app (a downloaded package) as the
433
+ * baseline. Files in `keep` stay at their previous baseline version (or stay
434
+ * absent), so changes a pull could not bring in keep being reported.
435
+ */
436
+ async function saveBaselineFromPackage(projectRoot, instanceName, packageZip, _onOutput, keep = []) {
437
+ const records = packageRecords(packageZip);
438
+ const dir = baselineDir(projectRoot, instanceName);
439
+ const previous = readBaselineRecords(projectRoot, instanceName);
440
+ for (const file of keep) {
441
+ if (previous.has(file))
442
+ records.set(file, previous.get(file));
443
+ else
444
+ records.delete(file);
445
+ }
446
+ fs.rmSync(dir, { recursive: true, force: true });
447
+ fs.mkdirSync(path.join(dir, 'update'), { recursive: true });
448
+ for (const [file, xml] of records)
449
+ fs.writeFileSync(path.join(dir, 'update', file), xml);
450
+ fs.writeFileSync(path.join(dir, 'format'), BASELINE_FORMAT + '\n');
451
+ const ignore = path.join(projectRoot, '.snu', '.gitignore');
452
+ if (!fs.existsSync(ignore))
453
+ fs.writeFileSync(ignore, '*\n');
454
+ }
455
+ function readBaselineRecords(projectRoot, instanceName) {
456
+ const records = new Map();
457
+ const dir = path.join(baselineDir(projectRoot, instanceName), 'update');
458
+ try {
459
+ for (const file of fs.readdirSync(dir))
460
+ records.set(file, fs.readFileSync(path.join(dir, file), 'utf8'));
461
+ }
462
+ catch { }
463
+ return records;
464
+ }
465
+ function readBuiltRecords(appOutputDir) {
466
+ const records = new Map();
467
+ // Both folders use the same update-name identity. Platform downloads may
468
+ // put choices in update while SDK builds put them in author_elective_update.
469
+ for (const folder of ['update', 'author_elective_update']) {
470
+ const dir = path.join(appOutputDir, folder);
471
+ for (const file of listDir(dir)) {
472
+ if (!file.endsWith('.xml'))
473
+ continue;
474
+ const xml = fs.readFileSync(path.join(dir, file), 'utf8');
475
+ if (records.has(file) && records.get(file) !== xml)
476
+ throw new NowSdkProject_1.NowSdkProjectError(`The app package contains conflicting copies of ${file}.`);
477
+ records.set(file, xml);
478
+ }
479
+ }
480
+ return records;
481
+ }
482
+ function unescapeXml(value) {
483
+ return value.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
484
+ }
485
+ function xmlField(xml, name) {
486
+ const m = new RegExp(`<${name}(?:\\s[^>]*)?>(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([^<]*))</${name}>`).exec(xml);
487
+ return (m ? (m[1] ?? unescapeXml(m[2] ?? '')) : '').trim();
488
+ }
489
+ /** Fields that describe the record's history, not its content. */
490
+ // federated_id is filled in by the platform after an install, never by a person.
491
+ const HISTORY_FIELDS = new Set(['sys_updated_on', 'sys_updated_by', 'sys_mod_count', 'sys_created_on', 'sys_created_by', 'sys_update_name', 'sys_package', 'sys_scope', 'sys_policy', 'federated_id']);
492
+ /** The top-level fields of a record's XML, values normalized for comparison (or as stored, with `raw`). */
493
+ function recordFields(xml, raw = false) {
494
+ if ((0, NowSdkChoices_1.isChoiceSet)(xml))
495
+ return (0, NowSdkChoices_1.choiceSetFields)(xml, raw);
496
+ const fields = new Map();
497
+ const body = /<record_update[^>]*>\s*<([a-z0-9_]+)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/.exec(xml)?.[2] || '';
498
+ const re = /<([A-Za-z0-9_]+)(?:\s[^>]*?)?(?:\/>|>(<!\[CDATA\[[\s\S]*?\]\]>|[^<]*)<\/\1>)/g;
499
+ let m;
500
+ while ((m = re.exec(body))) {
501
+ if (HISTORY_FIELDS.has(m[1]))
502
+ continue;
503
+ let value = m[2] ?? '';
504
+ const cdata = /^<!\[CDATA\[([\s\S]*)\]\]>$/.exec(value);
505
+ value = cdata ? cdata[1] : unescapeXml(value);
506
+ fields.set(m[1], raw ? value : comparableValue(value));
507
+ }
508
+ return fields;
509
+ }
510
+ /**
511
+ * A field value as compared: line endings, trailing spaces and blank lines at
512
+ * the ends normalized (a form save introduces those), everything else kept,
513
+ * so a change in indentation or inside a string still counts.
514
+ */
515
+ function comparableValue(value) {
516
+ return value.replace(/\r\n?/g, '\n').replace(/[ \t]+$/gm, '').trim();
517
+ }
518
+ function sameRecord(a, b) {
519
+ const fa = recordFields(a), fb = recordFields(b);
520
+ // A record this parser cannot read is compared as text.
521
+ if (fa.size === 0 && fb.size === 0)
522
+ return comparableValue(a) === comparableValue(b);
523
+ if (fa.size !== fb.size)
524
+ return false;
525
+ for (const [k, v] of fa)
526
+ if (fb.get(k) !== v)
527
+ return false;
528
+ return true;
529
+ }
530
+ function recordLabel(table, xml, sysId) {
531
+ if ((0, NowSdkChoices_1.isChoiceSet)(xml)) {
532
+ const fields = (0, NowSdkChoices_1.choiceSetFields)(xml);
533
+ return `Choices ${fields.get('name')}.${fields.get('element')}`;
534
+ }
535
+ const fields = table === 'sys_ui_page' ? ['description', 'name', 'endpoint'] : table === 'sys_dictionary' ? ['element'] : ['name', 'title', 'label', 'sys_name', 'path', 'key'];
536
+ let name = fields.map((f) => xmlField(xml, f)).find(Boolean) || sysId;
537
+ if (table === 'sys_dictionary' && xmlField(xml, 'name'))
538
+ name = `${xmlField(xml, 'name')}.${name}`;
539
+ if (table === 'sys_module')
540
+ name = path.basename(name);
541
+ return `${exports.TABLE_LABELS[table] || table} ${name}`;
542
+ }
543
+ /** Tables the SDK regenerates from the project on every build and never converts back. */
544
+ const NOT_CONVERTED = new Set([
545
+ 'sys_ux_lib_asset', 'sys_ux_theme_asset', 'db_image', 'sys_ui_message', 'sys_attachment', 'sys_attachment_doc',
546
+ 'sn_glider_source_artifact', 'sn_glider_source_artifact_m2m',
547
+ ]);
548
+ /**
549
+ * Record sys_ids whose Fluent definition carries `@fluent-disable-sync` (on the
550
+ * statement, or `@fluent-disable-sync-for-file`), which the SDK's pull skips.
551
+ */
552
+ function disableSyncIds(projectRoot) {
553
+ const ids = new Set();
554
+ const all = listFiles(projectRoot);
555
+ const files = all.filter((f) => f.endsWith('.now.ts'));
556
+ const marked = files.filter((f) => {
557
+ try {
558
+ return fs.readFileSync(path.join(projectRoot, f), 'utf8').includes('@fluent-disable-sync');
559
+ }
560
+ catch {
561
+ return false;
562
+ }
563
+ });
564
+ if (marked.length === 0)
565
+ return ids;
566
+ const keys = new Map();
567
+ const keysFile = all.find((f) => f.endsWith('generated/keys.ts'));
568
+ if (keysFile) {
569
+ const text = fs.readFileSync(path.join(projectRoot, keysFile), 'utf8');
570
+ const re = /['"]?([\w.$-]+)['"]?\s*:\s*\{\s*table:\s*'[^']+'\s*id:\s*'([0-9a-f]{32})'/g;
571
+ let m;
572
+ while ((m = re.exec(text)))
573
+ keys.set(m[1], m[2]);
574
+ }
575
+ for (const file of marked) {
576
+ const text = fs.readFileSync(path.join(projectRoot, file), 'utf8');
577
+ for (const table of (0, NowSdkSource_1.disabledChoiceTables)(projectRoot, file, text))
578
+ ids.add(`choices:${table}`);
579
+ const wholeFile = text.includes('@fluent-disable-sync-for-file');
580
+ const re = /Now\.ID\[\s*['"]([^'"]+)['"]\s*\]/g;
581
+ let m;
582
+ while ((m = re.exec(text))) {
583
+ const id = keys.get(m[1]) || (/^[0-9a-f]{32}$/.test(m[1]) ? m[1] : undefined);
584
+ if (!id)
585
+ continue;
586
+ if (wholeFile) {
587
+ ids.add(id);
588
+ continue;
589
+ }
590
+ // The statement that holds this $id: its start is the last line before
591
+ // it that begins a top-level call; the marker sits in the lines above.
592
+ const before = text.slice(0, m.index);
593
+ const starts = [...before.matchAll(/^(?:export\s+)?(?:const\s+\w+\s*=\s*)?[A-Za-z_$][\w$.]*\(/gm)];
594
+ const start = starts.length ? starts[starts.length - 1].index : 0;
595
+ const above = before.slice(0, start).split('\n').slice(-4).join('\n');
596
+ if (above.includes('@fluent-disable-sync'))
597
+ ids.add(id);
598
+ }
599
+ }
600
+ return ids;
601
+ }
602
+ function generatedReason(table, xml, sysId, disableSync) {
603
+ if ((0, NowSdkChoices_1.isChoiceSet)(xml)) {
604
+ const fields = (0, NowSdkChoices_1.choiceSetFields)(xml);
605
+ if (disableSync.has('choices:*') || disableSync.has(`choices:${fields.get('name')}`) || disableSync.has(`choices:${fields.get('name')}.${fields.get('element')}`))
606
+ return 'marked @fluent-disable-sync in your source';
607
+ }
608
+ if (xml.includes('@fluent-import-html'))
609
+ return 'built from your UI source';
610
+ if (table === 'sys_module')
611
+ return 'compiled from your server source';
612
+ if (NOT_CONVERTED.has(table) || table.startsWith('sys_aix_'))
613
+ return 'regenerated from your source on every build';
614
+ if (xml.includes('// @fluent-module '))
615
+ return 'its script calls a module function in your source';
616
+ if (disableSync.has(sysId))
617
+ return 'marked @fluent-disable-sync in your source';
618
+ return undefined;
619
+ }
620
+ function describeRecord(file, xml, status, disableSync) {
621
+ const m = /^([a-z0-9_]+?)_([0-9a-f]{32})\.xml$/.exec(file);
622
+ const table = m?.[1] || /<record_update[^>]*>\s*<([a-z0-9_]+)\s/.exec(xml)?.[1] || 'record';
623
+ const sysId = m?.[2] || '';
624
+ const change = { label: recordLabel(table, xml, sysId || file), status, file };
625
+ const reason = generatedReason(table, xml, sysId, disableSync);
626
+ if (reason) {
627
+ change.generated = true;
628
+ change.reason = reason;
629
+ }
630
+ return change;
631
+ }
632
+ /** One record of the baseline (the state after the last sync), if any. */
633
+ function readBaselineRecord(projectRoot, instanceName, file) {
634
+ return readBaselineRecords(projectRoot, instanceName).get(path.basename(file));
635
+ }
636
+ /** A record's fields (history fields left out), values as stored. */
637
+ function readRecordFields(xml) {
638
+ return recordFields(xml, true);
639
+ }
640
+ /** Of `fields`, the ones whose value in `builtXml` differs from `instanceXml` (compared as a deploy check does). */
641
+ function fieldsBuiltDifferently(builtXml, instanceXml, fields) {
642
+ if (!builtXml)
643
+ return fields;
644
+ const fb = recordFields(builtXml), fi = recordFields(instanceXml);
645
+ return fields.filter((f) => (fb.get(f) ?? '') !== (fi.get(f) ?? ''));
646
+ }
647
+ /** The fields that differ between two versions of a record (compared as a deploy check does). */
648
+ function changedFieldNames(beforeXml, afterXml) {
649
+ const fb = recordFields(beforeXml), fa = recordFields(afterXml);
650
+ return [...new Set([...fb.keys(), ...fa.keys()])].filter((f) => (fb.get(f) ?? '') !== (fa.get(f) ?? ''));
651
+ }
652
+ /** One record of the last build (dist/app/update), if any. */
653
+ function readBuiltRecord(appOutputDir, file) {
654
+ return readBuiltRecords(appOutputDir).get(path.basename(file));
655
+ }
656
+ /** keys.ts: record sys_id -> the Now.ID name it is defined with. */
657
+ function readKeyNames(projectRoot) {
658
+ const names = new Map();
659
+ const keysFile = listFiles(projectRoot).find((f) => f.endsWith('generated/keys.ts'));
660
+ if (!keysFile)
661
+ return names;
662
+ const text = fs.readFileSync(path.join(projectRoot, keysFile), 'utf8');
663
+ const re = /['"]?([\w.$-]+)['"]?\s*:\s*\{\s*table:\s*'[^']+'\s*id:\s*'([0-9a-f]{32})'/g;
664
+ let m;
665
+ while ((m = re.exec(text)))
666
+ names.set(m[2], m[1]);
667
+ return names;
668
+ }
669
+ /** A record as readable text, one field per section, for a diff view. */
670
+ function recordForReading(xml) {
671
+ if (!xml)
672
+ return '';
673
+ const lines = [];
674
+ for (const [field, value] of [...recordFields(xml, true)].sort(([a], [b]) => a.localeCompare(b))) {
675
+ lines.push(`── ${field} ──`, value.trim(), '');
676
+ }
677
+ return lines.join('\n');
678
+ }
679
+ /**
680
+ * Accept that the next deploy overwrites a change made on the instance: the
681
+ * baseline takes the instance version of that record, so it is no longer
682
+ * reported. `instanceXml` undefined means the record was deleted there.
683
+ */
684
+ function dismissInstanceChange(projectRoot, instanceName, file, instanceXml) {
685
+ if (!hasBaseline(projectRoot, instanceName))
686
+ return;
687
+ const target = path.join(baselineDir(projectRoot, instanceName), 'update', path.basename(file));
688
+ if (instanceXml === undefined)
689
+ fs.rmSync(target, { force: true });
690
+ else
691
+ fs.writeFileSync(target, instanceXml);
692
+ }
693
+ /** Records that changed on the instance since the baseline, from records already read. */
694
+ function instanceChangesFromRecords(projectRoot, instanceName, now) {
695
+ if (!hasBaseline(projectRoot, instanceName))
696
+ return [];
697
+ const before = readBaselineRecords(projectRoot, instanceName);
698
+ const disableSync = disableSyncIds(projectRoot);
699
+ const changes = [];
700
+ for (const [file, xml] of now) {
701
+ const old = before.get(file);
702
+ if (old === undefined)
703
+ changes.push(describeRecord(file, xml, 'new', disableSync));
704
+ else if (!sameRecord(old, xml))
705
+ changes.push(describeRecord(file, xml, 'changed', disableSync));
706
+ }
707
+ for (const [file, xml] of before) {
708
+ if (!now.has(file))
709
+ changes.push(describeRecord(file, xml, 'removed', disableSync));
710
+ }
711
+ return changes.sort((a, b) => a.label.localeCompare(b.label));
712
+ }
713
+ /** Records that changed on the instance since the baseline, from a downloaded package. */
714
+ async function instanceChangesFromPackage(projectRoot, instanceName, packageZip, _onOutput) {
715
+ if (!hasBaseline(projectRoot, instanceName))
716
+ return [];
717
+ return instanceChangesFromRecords(projectRoot, instanceName, packageRecords(packageZip));
718
+ }
719
+ /**
720
+ * At a deploy, with the app just built: which instance changes since the
721
+ * baseline the install would overwrite. A record counts only when a field
722
+ * changed on the instance differs from the build, so changes already in the
723
+ * source (for example pulled) do not. Records that exist only on the instance
724
+ * are left alone by an install and are reported separately.
725
+ */
726
+ function deployConflicts(projectRoot, instanceName, packageZip, appOutputDir) {
727
+ const overwritten = [];
728
+ const instanceOnly = [];
729
+ if (!hasBaseline(projectRoot, instanceName))
730
+ return { overwritten, instanceOnly };
731
+ const now = packageRecords(packageZip);
732
+ const before = readBaselineRecords(projectRoot, instanceName);
733
+ const built = readBuiltRecords(appOutputDir);
734
+ const disableSync = disableSyncIds(projectRoot);
735
+ const same = (a, b, key) => (a.get(key) ?? '') === (b.get(key) ?? '');
736
+ const lostFields = (was, current, build, fields) => {
737
+ const fi = recordFields(current), fb = recordFields(build);
738
+ return fields.filter((field) => field === 'choices' && (0, NowSdkChoices_1.isChoiceSet)(current) && (0, NowSdkChoices_1.isChoiceSet)(build)
739
+ ? (0, NowSdkChoices_1.choicesWouldBeOverwritten)(was, current, build)
740
+ : !same(fi, fb, field));
741
+ };
742
+ for (const file of new Set([...now.keys(), ...before.keys()])) {
743
+ const was = before.get(file);
744
+ const is = now.get(file);
745
+ const build = built.get(file);
746
+ if (is !== undefined && was === undefined) {
747
+ if (build === undefined) {
748
+ instanceOnly.push(describeRecord(file, is, 'new', disableSync));
749
+ continue;
750
+ }
751
+ const fi = recordFields(is), fb = recordFields(build);
752
+ const differing = lostFields(undefined, is, build, [...new Set([...fi.keys(), ...fb.keys()])]);
753
+ if (differing.length)
754
+ overwritten.push({ ...describeRecord(file, is, 'new', disableSync), fields: differing });
755
+ continue;
756
+ }
757
+ if (is === undefined && was !== undefined) {
758
+ if (build !== undefined)
759
+ overwritten.push({ ...describeRecord(file, was, 'removed', disableSync), fields: [] });
760
+ continue;
761
+ }
762
+ if (is === undefined || was === undefined || build === undefined)
763
+ continue;
764
+ const fw = recordFields(was), fi = recordFields(is);
765
+ const changed = [...new Set([...fw.keys(), ...fi.keys()])].filter((k) => !same(fw, fi, k));
766
+ const lost = lostFields(was, is, build, changed);
767
+ if (lost.length)
768
+ overwritten.push({ ...describeRecord(file, is, 'changed', disableSync), fields: lost });
769
+ }
770
+ const byLabel = (a, b) => a.label.localeCompare(b.label);
771
+ return { overwritten: overwritten.sort(byLabel), instanceOnly: instanceOnly.sort(byLabel) };
772
+ }
773
+ function pullMarker(projectRoot) {
774
+ return path.join(projectRoot, '.snu', 'pull.json');
775
+ }
776
+ /** Whether this project was pulled before (the first pull also writes out SDK defaults). */
777
+ function hasPulledBefore(projectRoot) {
778
+ return fs.existsSync(pullMarker(projectRoot));
779
+ }
780
+ function readMarker(file, key) {
781
+ try {
782
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
783
+ return typeof data?.instance === 'string' && typeof data?.[key] === 'string' ? { instance: data.instance, at: data[key] } : null;
784
+ }
785
+ catch {
786
+ return null;
787
+ }
788
+ }
789
+ /** When the app was last deployed and pulled from this project. */
790
+ function readSyncMarkers(projectRoot) {
791
+ return {
792
+ deploy: readMarker(path.join(projectRoot, '.snu', 'deployed.json'), 'deployedAt'),
793
+ pull: readMarker(pullMarker(projectRoot), 'pulledAt'),
794
+ };
795
+ }
796
+ /** Remember the last successful deploy. */
797
+ function markDeployed(projectRoot, instanceName) {
798
+ try {
799
+ const dir = path.join(projectRoot, '.snu');
800
+ fs.mkdirSync(dir, { recursive: true });
801
+ if (!fs.existsSync(path.join(dir, '.gitignore')))
802
+ fs.writeFileSync(path.join(dir, '.gitignore'), '*\n');
803
+ fs.writeFileSync(path.join(dir, 'deployed.json'), JSON.stringify({ instance: instanceName, deployedAt: new Date().toISOString() }, null, 2) + '\n');
804
+ }
805
+ catch { }
806
+ }
807
+ /** Remember the last pull. */
808
+ function markPulled(projectRoot, instanceName) {
809
+ try {
810
+ const dir = path.dirname(pullMarker(projectRoot));
811
+ fs.mkdirSync(dir, { recursive: true });
812
+ if (!fs.existsSync(path.join(dir, '.gitignore')))
813
+ fs.writeFileSync(path.join(dir, '.gitignore'), '*\n');
814
+ fs.writeFileSync(pullMarker(projectRoot), JSON.stringify({ instance: instanceName, pulledAt: new Date().toISOString() }, null, 2) + '\n');
815
+ }
816
+ catch { }
817
+ }
818
+ function cleanupPull(tempDir) {
819
+ try {
820
+ fs.rmSync(tempDir, { recursive: true, force: true });
821
+ }
822
+ catch { }
823
+ }
824
+ //# sourceMappingURL=NowSdkPull.js.map