@tenonhq/dovetail-core 0.0.112 → 0.0.114

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.
package/dist/appUtils.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.narrowManifestToTables = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = exports.stampMetadataContent = void 0;
39
+ exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.narrowManifestToRecord = exports.narrowManifestToTables = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = exports.stampMetadataContent = void 0;
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const progress_1 = __importDefault(require("progress"));
42
42
  const fUtils = __importStar(require("./FileUtils"));
@@ -298,6 +298,44 @@ const narrowManifestToTables = (manifest, tables) => {
298
298
  return Object.assign({}, manifest, { tables: wantedTables });
299
299
  };
300
300
  exports.narrowManifestToTables = narrowManifestToTables;
301
+ /**
302
+ * Narrow a manifest's table map to a SINGLE record (matched by `table` +
303
+ * `sysId`), for the FILE refresh only.
304
+ *
305
+ * Returns a NEW manifest and NEVER mutates its input — same safety contract as
306
+ * `narrowManifestToTables`: the caller keeps writing the original, full manifest
307
+ * to disk, so the on-disk `dove.manifest.<scope>.json` never loses its other
308
+ * tables/records. Only the file download is scoped to this one record.
309
+ *
310
+ * If the table or a record with that sys_id is absent from the manifest, the
311
+ * returned manifest has an empty table map — refreshAllFiles no-ops on it, so no
312
+ * files are written (the create flow warns and points the user at `dove refresh`).
313
+ */
314
+ const narrowManifestToRecord = (manifest, table, sysId) => {
315
+ var empty = Object.assign({}, manifest, { tables: {} });
316
+ var tables = manifest.tables || {};
317
+ var tableEntry = tables[table];
318
+ if (!tableEntry || !tableEntry.records)
319
+ return empty;
320
+ var records = tableEntry.records;
321
+ var recKeys = Object.keys(records);
322
+ var wantedRecords = {};
323
+ // sys_id is unique per table, so stop at the first match — the record set for
324
+ // a large table can be sizable and this is the exact scenario the feature targets.
325
+ for (var i = 0; i < recKeys.length; i++) {
326
+ var rec = records[recKeys[i]];
327
+ if (rec && rec.sys_id === sysId) {
328
+ wantedRecords[recKeys[i]] = rec;
329
+ break;
330
+ }
331
+ }
332
+ if (Object.keys(wantedRecords).length === 0)
333
+ return empty;
334
+ var narrowedTables = {};
335
+ narrowedTables[table] = Object.assign({}, tableEntry, { records: wantedRecords });
336
+ return Object.assign({}, manifest, { tables: narrowedTables });
337
+ };
338
+ exports.narrowManifestToRecord = narrowManifestToRecord;
301
339
  const syncManifest = async (scope, options = {}) => {
302
340
  // Top-level entry owns the collector lifecycle. Recursive calls (all-scopes
303
341
  // → per-scope) inherit the collector via options._benchmarkCollector.
@@ -368,6 +406,23 @@ const syncManifest = async (scope, options = {}) => {
368
406
  // so intersecting the manifest is enough. `newManifest` is left intact —
369
407
  // see narrowManifestToTables.
370
408
  var refreshManifest = (0, exports.narrowManifestToTables)(newManifest, options.tables);
409
+ // Single-record narrowing (dove create round-trip). Applied after the
410
+ // --table narrowing so the two compose; the full manifest was already
411
+ // written above, so only the file download is scoped to the one record.
412
+ if (options.record) {
413
+ refreshManifest = (0, exports.narrowManifestToRecord)(refreshManifest, options.record.table, options.record.sysId);
414
+ if (Object.keys(refreshManifest.tables).length === 0) {
415
+ Logger_1.logger.warn("Created record " + options.record.sysId + " (" + options.record.table +
416
+ ") not found in the refreshed scope manifest — local files not written. " +
417
+ "If '" + options.record.table + "' is missing from this scope's `_tables` " +
418
+ "whitelist in dove.config.js, add it there first; otherwise run " +
419
+ "'npx dove refresh' to pull it.");
420
+ }
421
+ else {
422
+ FileLogger_1.fileLogger.debug("syncManifest: narrowed file refresh to single record " +
423
+ options.record.sysId + " (" + options.record.table + ")");
424
+ }
425
+ }
371
426
  if (options.tables && options.tables.length > 0) {
372
427
  var wantedNames = Object.keys(refreshManifest.tables);
373
428
  if (wantedNames.length === 0) {
package/dist/commander.js CHANGED
@@ -228,6 +228,11 @@ function configureCli(cli) {
228
228
  default: false,
229
229
  describe: "Skip interactive prompts",
230
230
  },
231
+ refresh: {
232
+ type: "boolean",
233
+ default: true,
234
+ describe: "Pull the created record's files to disk afterward (default: true; --no-refresh skips the local sync)",
235
+ },
231
236
  });
232
237
  return cmdArgs;
233
238
  }, createRecordCommand_1.createRecordCommand)
@@ -281,20 +281,33 @@ async function createRecordCommand(args) {
281
281
  " (" +
282
282
  newSysId +
283
283
  ")");
284
- // 7. Full round-trip: pull the record back locally
285
- Logger_1.logger.info("Syncing record to local files...");
286
- FileLogger_1.fileLogger.debug("Starting manifest sync for scope:", scope);
287
- try {
288
- await AppUtils.syncManifest(scope);
289
- var sourcePath = ConfigManager.getSourcePath();
290
- var localPath = path_1.default.join(sourcePath, table, recordName);
291
- Logger_1.logger.success(chalk_1.default.green("Local files created at: ") + localPath);
284
+ // 7. Round-trip: pull ONLY the new record's files back locally. Scoped to
285
+ // { table, sysId } so creating one record no longer refreshes the entire
286
+ // scope (the whole-scope-churn footgun). --no-refresh skips it entirely.
287
+ if (typedArgs.refresh === false) {
288
+ Logger_1.logger.info("Skipping local sync (--no-refresh). Run 'npx dove refresh' to pull the record locally.");
292
289
  }
293
- catch (syncErr) {
294
- Logger_1.logger.warn("Record created on instance but local sync failed.");
295
- Logger_1.logger.warn("Run 'npx dove refresh' to pull the record locally.");
296
- if (syncErr instanceof Error) {
297
- FileLogger_1.fileLogger.error("Sync error:", syncErr.message);
290
+ else {
291
+ Logger_1.logger.info("Syncing record to local files...");
292
+ FileLogger_1.fileLogger.debug("Starting single-record sync for scope:", scope);
293
+ try {
294
+ await AppUtils.syncManifest(scope, {
295
+ record: { table: table, sysId: newSysId },
296
+ });
297
+ // Resolve the scope's own source directory — syncManifest writes into
298
+ // getSourcePathForScope(scope), which a scope config can override. Using
299
+ // the top-level getSourcePath() here would print a wrong path in
300
+ // multi-scope setups even though the files landed correctly.
301
+ var sourcePath = ConfigManager.getSourcePathForScope(scope);
302
+ var localPath = path_1.default.join(sourcePath, table, recordName);
303
+ Logger_1.logger.success(chalk_1.default.green("Local files created at: ") + localPath);
304
+ }
305
+ catch (syncErr) {
306
+ Logger_1.logger.warn("Record created on instance but local sync failed.");
307
+ Logger_1.logger.warn("Run 'npx dove refresh' to pull the record locally.");
308
+ if (syncErr instanceof Error) {
309
+ FileLogger_1.fileLogger.error("Sync error:", syncErr.message);
310
+ }
298
311
  }
299
312
  }
300
313
  }
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ /**
3
+ * Tests the SERVER half of the sync engine — servicenow/dovetail/sys_script_include/
4
+ * DovetailUtilsMS.js — against the record-name collision that silently dropped records.
5
+ *
6
+ * Before the fix, `records` was keyed by display name, so two records sharing a name
7
+ * collapsed into one entry: one vanished from the manifest with no warning, and because
8
+ * the survivor kept the shared folder, a later push to that folder wrote to the WRONG
9
+ * record.
10
+ *
11
+ * This loads the real script include into a sandbox with minimal ServiceNow stubs
12
+ * (Class / GlideRecord / gs) rather than re-implementing the logic, so the test fails if
13
+ * the deployed source regresses. There is no ATF/SN harness in this repo, and this code
14
+ * is deployed to a shared instance — so this is the only gate it gets before deploy.
15
+ *
16
+ * getFileMap() short-circuits when `includes[table]` carries explicit field overrides,
17
+ * which is why no sys_dictionary / TableUtils stub is needed here.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const vm_1 = __importDefault(require("vm"));
26
+ const SOURCE = path_1.default.join(__dirname, "../../../../servicenow/dovetail/sys_script_include/DovetailUtilsMS.js");
27
+ /** Minimal GlideRecord over a fixture list. Only what buildTableMap and processMissingFiles touch. */
28
+ function makeGlideRecord(rows) {
29
+ return function GlideRecordStub(_table) {
30
+ var i = -1;
31
+ this.get = function (sysId) {
32
+ i = rows.findIndex(function (r) {
33
+ return r.sys_id === sysId;
34
+ });
35
+ return i !== -1;
36
+ };
37
+ this.addQuery = function () {
38
+ return { addOrCondition: function () { } };
39
+ };
40
+ this.addEncodedQuery = function () { };
41
+ this.query = function () {
42
+ i = -1;
43
+ };
44
+ this.next = function () {
45
+ i += 1;
46
+ return i < rows.length;
47
+ };
48
+ this.getValue = function (field) {
49
+ return rows[i][field];
50
+ };
51
+ this.getDisplayValue = function (field) {
52
+ // No arg → the record's display value (the name). This is what
53
+ // generateRecordName() uses, and it is where the collision comes from.
54
+ return field ? rows[i][field] : rows[i].name;
55
+ };
56
+ this.getElements = function () {
57
+ return null; // metaData capture is skipped; getContents is false below
58
+ };
59
+ };
60
+ }
61
+ function loadUtils(rows) {
62
+ const warnings = [];
63
+ const sandbox = {
64
+ Class: {
65
+ create: function () {
66
+ return function () {
67
+ if (this.initialize)
68
+ this.initialize.apply(this, arguments);
69
+ };
70
+ },
71
+ },
72
+ GlideRecord: makeGlideRecord(rows),
73
+ gs: {
74
+ warn: function (msg) {
75
+ warnings.push(msg);
76
+ },
77
+ getProperty: function () {
78
+ return "https://example.service-now.com/";
79
+ },
80
+ },
81
+ };
82
+ vm_1.default.createContext(sandbox);
83
+ vm_1.default.runInContext(fs_1.default.readFileSync(SOURCE, "utf8"), sandbox);
84
+ return { utils: new sandbox.DovetailUtilsMS(), warnings: warnings };
85
+ }
86
+ function buildMap(rows) {
87
+ const loaded = loadUtils(rows);
88
+ const result = loaded.utils.buildTableMap({
89
+ tableName: "x_cadso_journey_action",
90
+ scopeId: "scope1",
91
+ getContents: false,
92
+ // Explicit field override → getFileMap short-circuits (no sys_dictionary stub).
93
+ includes: { x_cadso_journey_action: { script: { type: "js" } } },
94
+ excludes: {},
95
+ tableOptions: {},
96
+ });
97
+ return { records: result.records, warnings: loaded.warnings };
98
+ }
99
+ describe("DovetailUtilsMS.buildTableMap — duplicate display names", () => {
100
+ it("keeps BOTH records when two share a display name (regression: one was silently dropped)", () => {
101
+ const out = buildMap([
102
+ { sys_id: "1607d7f0c373f2d0d4ddf1db05013101", name: "Blueprint" },
103
+ { sys_id: "46ae2decc33f32d0d4ddf1db050131d9", name: "Blueprint" },
104
+ ]);
105
+ const keys = Object.keys(out.records);
106
+ expect(keys).toHaveLength(2); // was 1 — the second overwrote the first
107
+ const sysIds = keys.map((k) => out.records[k].sys_id).sort();
108
+ expect(sysIds).toEqual([
109
+ "1607d7f0c373f2d0d4ddf1db05013101",
110
+ "46ae2decc33f32d0d4ddf1db050131d9",
111
+ ]);
112
+ });
113
+ it("suffixes EVERY member of a colliding set, so no record arbitrarily keeps the bare name", () => {
114
+ // If only the later duplicate were suffixed, which record kept "Blueprint" would
115
+ // depend on query order — and folder names would churn between refreshes.
116
+ const out = buildMap([
117
+ { sys_id: "1607d7f0c373f2d0d4ddf1db05013101", name: "Blueprint" },
118
+ { sys_id: "46ae2decc33f32d0d4ddf1db050131d9", name: "Blueprint" },
119
+ ]);
120
+ expect(Object.keys(out.records).sort()).toEqual([
121
+ "Blueprint (1607d7f0)",
122
+ "Blueprint (46ae2dec)",
123
+ ]);
124
+ expect(out.records["Blueprint"]).toBeUndefined();
125
+ });
126
+ it("keeps record.name identical to the map key (writers build the folder path from it)", () => {
127
+ const out = buildMap([
128
+ { sys_id: "1607d7f0c373f2d0d4ddf1db05013101", name: "Blueprint" },
129
+ { sys_id: "46ae2decc33f32d0d4ddf1db050131d9", name: "Blueprint" },
130
+ ]);
131
+ Object.keys(out.records).forEach((key) => {
132
+ expect(out.records[key].name).toBe(key);
133
+ });
134
+ });
135
+ it("warns on a collision instead of dropping silently", () => {
136
+ const out = buildMap([
137
+ { sys_id: "1607d7f0c373f2d0d4ddf1db05013101", name: "Blueprint" },
138
+ { sys_id: "46ae2decc33f32d0d4ddf1db050131d9", name: "Blueprint" },
139
+ ]);
140
+ expect(out.warnings).toHaveLength(2);
141
+ expect(out.warnings[0]).toContain("duplicate display name");
142
+ expect(out.warnings[0]).toContain("Blueprint");
143
+ });
144
+ it("leaves unique names completely untouched (no suffix, no warning)", () => {
145
+ const out = buildMap([
146
+ { sys_id: "dc2ef56133a0e2507b18bc534d5c7bf5", name: "Send Email" },
147
+ { sys_id: "0d4626f933e826507b18bc534d5c7b37", name: "Send Text" },
148
+ ]);
149
+ expect(Object.keys(out.records).sort()).toEqual(["Send Email", "Send Text"]);
150
+ expect(out.warnings).toHaveLength(0);
151
+ });
152
+ it("handles three-way collisions", () => {
153
+ const out = buildMap([
154
+ { sys_id: "aaaaaaaa11111111", name: "Dup" },
155
+ { sys_id: "bbbbbbbb22222222", name: "Dup" },
156
+ { sys_id: "cccccccc33333333", name: "Dup" },
157
+ ]);
158
+ expect(Object.keys(out.records).sort()).toEqual([
159
+ "Dup (aaaaaaaa)",
160
+ "Dup (bbbbbbbb)",
161
+ "Dup (cccccccc)",
162
+ ]);
163
+ });
164
+ it("disambiguates only the colliding set, not the whole table", () => {
165
+ const out = buildMap([
166
+ { sys_id: "aaaaaaaa11111111", name: "Blueprint" },
167
+ { sys_id: "bbbbbbbb22222222", name: "Blueprint" },
168
+ { sys_id: "cccccccc33333333", name: "Send Email" },
169
+ ]);
170
+ expect(Object.keys(out.records).sort()).toEqual([
171
+ "Blueprint (aaaaaaaa)",
172
+ "Blueprint (bbbbbbbb)",
173
+ "Send Email", // untouched — this is why differentiatorField was the wrong tool
174
+ ]);
175
+ });
176
+ });
177
+ /**
178
+ * bulkDownload keys its response by display name too — the same collapse dropped the
179
+ * file CONTENT for one of a colliding pair even once the manifest listed both.
180
+ */
181
+ function processMissing(rows) {
182
+ const loaded = loadUtils(rows);
183
+ const recordMap = {};
184
+ rows.forEach((r) => {
185
+ recordMap[r.sys_id] = [{ name: "script", type: "js" }];
186
+ });
187
+ const result = loaded.utils.processMissingFiles({ x_cadso_journey_action: recordMap }, {});
188
+ return {
189
+ records: result.x_cadso_journey_action.records,
190
+ warnings: loaded.warnings,
191
+ };
192
+ }
193
+ describe("DovetailUtilsMS.processMissingFiles — duplicate display names", () => {
194
+ it("keeps BOTH records' content when two share a display name (regression: one payload was silently dropped)", () => {
195
+ const out = processMissing([
196
+ { sys_id: "1607d7f0c373f2d0d4ddf1db05013101", name: "Blueprint", script: "var a = 1;" },
197
+ { sys_id: "46ae2decc33f32d0d4ddf1db050131d9", name: "Blueprint", script: "var b = 2;" },
198
+ ]);
199
+ const keys = Object.keys(out.records).sort();
200
+ expect(keys).toEqual(["Blueprint (1607d7f0)", "Blueprint (46ae2dec)"]);
201
+ expect(out.records["Blueprint"]).toBeUndefined();
202
+ // The actual bulkDownload payload — each record keeps its OWN file content.
203
+ expect(out.records["Blueprint (1607d7f0)"].files[0].content).toBe("var a = 1;");
204
+ expect(out.records["Blueprint (46ae2dec)"].files[0].content).toBe("var b = 2;");
205
+ });
206
+ it("keeps record.name identical to the map key (writers build the folder path from it)", () => {
207
+ const out = processMissing([
208
+ { sys_id: "1607d7f0c373f2d0d4ddf1db05013101", name: "Blueprint", script: "" },
209
+ { sys_id: "46ae2decc33f32d0d4ddf1db050131d9", name: "Blueprint", script: "" },
210
+ ]);
211
+ Object.keys(out.records).forEach((key) => {
212
+ expect(out.records[key].name).toBe(key);
213
+ });
214
+ });
215
+ it("warns on a collision instead of staying silent (parity with buildTableMap)", () => {
216
+ const out = processMissing([
217
+ { sys_id: "1607d7f0c373f2d0d4ddf1db05013101", name: "Blueprint", script: "" },
218
+ { sys_id: "46ae2decc33f32d0d4ddf1db050131d9", name: "Blueprint", script: "" },
219
+ ]);
220
+ expect(out.warnings).toHaveLength(2);
221
+ expect(out.warnings[0]).toContain("duplicate display name");
222
+ expect(out.warnings[0]).toContain("bulkDownload");
223
+ });
224
+ it("leaves unique names completely untouched (no suffix, no warning)", () => {
225
+ const out = processMissing([
226
+ { sys_id: "dc2ef56133a0e2507b18bc534d5c7bf5", name: "Send Email", script: "" },
227
+ { sys_id: "0d4626f933e826507b18bc534d5c7b37", name: "Send Text", script: "" },
228
+ ]);
229
+ expect(Object.keys(out.records).sort()).toEqual(["Send Email", "Send Text"]);
230
+ expect(out.warnings).toHaveLength(0);
231
+ });
232
+ });
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for the single-record narrowing of syncManifest — the `record` option
4
+ * used by `dove create`'s post-create round-trip.
5
+ *
6
+ * Regression target: `dove create <table>` previously called syncManifest(scope)
7
+ * with no narrowing, which re-downloaded and rewrote EVERY record in the scope.
8
+ * Creating one record churned ~1,892 files in a scope like x_cadso_text_spoke.
9
+ * The fix scopes the file refresh to just the created record.
10
+ *
11
+ * These tests assert:
12
+ * 1. narrowManifestToRecord returns only the matching record, never mutating input
13
+ * 2. narrowManifestToRecord returns an empty table map when the record is absent
14
+ * 3. syncManifest({ record }) fetches + writes ONLY the target record's files
15
+ * 4. sibling records already on disk are left untouched (never re-fetched)
16
+ * 5. a record absent from the fetched manifest → warn, no fetch, no throw
17
+ */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ const fs = __importStar(require("fs"));
53
+ const os = __importStar(require("os"));
54
+ const path = __importStar(require("path"));
55
+ // ---------- mocks (must come before importing the module under test) ----------
56
+ var mockLogger = {
57
+ info: jest.fn(),
58
+ debug: jest.fn(),
59
+ warn: jest.fn(),
60
+ error: jest.fn(),
61
+ success: jest.fn(),
62
+ getLogLevel: function () { return "warn"; },
63
+ };
64
+ var mockFileLogger = {
65
+ debug: jest.fn(),
66
+ info: jest.fn(),
67
+ warn: jest.fn(),
68
+ error: jest.fn(),
69
+ };
70
+ var mockClient = {
71
+ getManifest: jest.fn(),
72
+ getMissingFiles: jest.fn(),
73
+ };
74
+ jest.mock("../Logger", function () { return { logger: mockLogger }; });
75
+ jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
76
+ jest.mock("../snClient", function () {
77
+ return {
78
+ defaultClient: function () { return mockClient; },
79
+ unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
80
+ processPushResponse: jest.fn(),
81
+ retryOnErr: jest.fn(),
82
+ retryOnHttpErr: jest.fn(),
83
+ unwrapTableAPIFirstItem: jest.fn(),
84
+ };
85
+ });
86
+ var mockConfig = {
87
+ getConfig: jest.fn().mockReturnValue({
88
+ scopes: { x_cadso_core: {} },
89
+ tableOptions: {},
90
+ }),
91
+ getManifest: jest.fn().mockResolvedValue({
92
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
93
+ }),
94
+ getSourcePathForScope: jest.fn(),
95
+ getSourcePath: jest.fn(),
96
+ getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
97
+ resolveConfigForScope: jest.fn().mockImplementation(function () {
98
+ return {
99
+ tables: ["sys_script_include"],
100
+ fieldOverrides: {},
101
+ apiIncludes: {},
102
+ apiExcludes: {},
103
+ };
104
+ }),
105
+ isMultiScopeManifest: jest.fn().mockReturnValue(true),
106
+ updateManifest: jest.fn(),
107
+ };
108
+ jest.mock("../config", function () { return mockConfig; });
109
+ jest.mock("progress", function () {
110
+ return jest.fn().mockImplementation(function () {
111
+ return { tick: jest.fn() };
112
+ });
113
+ });
114
+ // Stub writeScopeManifest so the test never writes a manifest file. Leave the
115
+ // other FileUtils exports real so we can assert against the file system.
116
+ jest.mock("../FileUtils", function () {
117
+ var actual = jest.requireActual("../FileUtils");
118
+ return Object.assign({}, actual, {
119
+ writeScopeManifest: jest.fn().mockResolvedValue(undefined),
120
+ });
121
+ });
122
+ const AppUtils = __importStar(require("../appUtils"));
123
+ // ---------- fixtures ----------
124
+ // A three-record scope manifest, all in sys_script_include, matching the shape
125
+ // client.getManifest returns (files carry name+type only; no content).
126
+ function threeRecordManifest() {
127
+ return {
128
+ scope: "x_cadso_core",
129
+ tables: {
130
+ sys_script_include: {
131
+ records: {
132
+ New: { name: "New", sys_id: "sysid_New", files: [{ name: "script", type: "js" }] },
133
+ Sib1: { name: "Sib1", sys_id: "sysid_Sib1", files: [{ name: "script", type: "js" }] },
134
+ Sib2: { name: "Sib2", sys_id: "sysid_Sib2", files: [{ name: "script", type: "js" }] },
135
+ },
136
+ },
137
+ },
138
+ };
139
+ }
140
+ var tmpRoot;
141
+ function writeLocal(record, name, type, content) {
142
+ var recDir = path.join(tmpRoot, "sys_script_include", record);
143
+ fs.mkdirSync(recDir, { recursive: true });
144
+ fs.writeFileSync(path.join(recDir, name + "." + type), content);
145
+ }
146
+ function readLocal(record, name, type) {
147
+ var p = path.join(tmpRoot, "sys_script_include", record, name + "." + type);
148
+ try {
149
+ return fs.readFileSync(p, "utf8");
150
+ }
151
+ catch (e) {
152
+ return null;
153
+ }
154
+ }
155
+ // ---------- pure helper ----------
156
+ describe("narrowManifestToRecord", function () {
157
+ test("returns only the matching record and does NOT mutate the input", function () {
158
+ var manifest = threeRecordManifest();
159
+ var out = AppUtils.narrowManifestToRecord(manifest, "sys_script_include", "sysid_New");
160
+ // input untouched — all three records still present
161
+ expect(Object.keys(manifest.tables.sys_script_include.records).sort())
162
+ .toEqual(["New", "Sib1", "Sib2"]);
163
+ // output holds exactly the one record
164
+ expect(Object.keys(out.tables)).toEqual(["sys_script_include"]);
165
+ expect(Object.keys(out.tables.sys_script_include.records)).toEqual(["New"]);
166
+ expect(out.tables.sys_script_include.records.New.sys_id).toBe("sysid_New");
167
+ });
168
+ test("returns an empty table map when the sys_id is absent", function () {
169
+ var manifest = threeRecordManifest();
170
+ var out = AppUtils.narrowManifestToRecord(manifest, "sys_script_include", "sysid_Missing");
171
+ expect(Object.keys(out.tables)).toEqual([]);
172
+ });
173
+ test("returns an empty table map when the table is absent", function () {
174
+ var manifest = threeRecordManifest();
175
+ var out = AppUtils.narrowManifestToRecord(manifest, "sys_ui_action", "sysid_New");
176
+ expect(Object.keys(out.tables)).toEqual([]);
177
+ });
178
+ });
179
+ // ---------- syncManifest({ record }) ----------
180
+ describe("syncManifest — single-record round-trip (dove create)", function () {
181
+ beforeEach(function () {
182
+ jest.clearAllMocks();
183
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sinc-record-test-"));
184
+ mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
185
+ mockConfig.getSourcePath.mockReturnValue(tmpRoot);
186
+ });
187
+ afterEach(function () {
188
+ try {
189
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
190
+ }
191
+ catch (e) { }
192
+ });
193
+ test("fetches + writes ONLY the target record; siblings are never re-fetched", async function () {
194
+ // A stale sibling file already on disk — it must be left exactly as-is.
195
+ writeLocal("Sib1", "script", "js", "STALE SIBLING — must not change");
196
+ mockClient.getManifest.mockResolvedValue(threeRecordManifest());
197
+ // getMissingFiles should be asked for the new record ONLY; return its content.
198
+ mockClient.getMissingFiles.mockResolvedValue({
199
+ sys_script_include: {
200
+ records: {
201
+ New: {
202
+ name: "New", sys_id: "sysid_New",
203
+ files: [{ name: "script", type: "js", content: "// fresh created record" }],
204
+ },
205
+ },
206
+ },
207
+ });
208
+ await AppUtils.syncManifest("x_cadso_core", {
209
+ record: { table: "sys_script_include", sysId: "sysid_New" },
210
+ });
211
+ // exactly one content fetch, scoped to the one record
212
+ expect(mockClient.getMissingFiles).toHaveBeenCalledTimes(1);
213
+ var missingArg = mockClient.getMissingFiles.mock.calls[0][0];
214
+ expect(Object.keys(missingArg.sys_script_include)).toEqual(["sysid_New"]);
215
+ // the new record landed
216
+ expect(readLocal("New", "script", "js")).toBe("// fresh created record");
217
+ // siblings untouched
218
+ expect(readLocal("Sib1", "script", "js")).toBe("STALE SIBLING — must not change");
219
+ expect(readLocal("Sib2", "script", "js")).toBeNull();
220
+ });
221
+ test("record absent from the fetched manifest → warn, no fetch, no throw", async function () {
222
+ mockClient.getManifest.mockResolvedValue(threeRecordManifest());
223
+ await AppUtils.syncManifest("x_cadso_core", {
224
+ record: { table: "sys_script_include", sysId: "sysid_DoesNotExist" },
225
+ });
226
+ // empty narrowed manifest → refreshAllFiles no-ops, so no content fetch
227
+ expect(mockClient.getMissingFiles).not.toHaveBeenCalled();
228
+ // and the user is told to run a full refresh
229
+ var warned = mockLogger.warn.mock.calls.some(function (c) {
230
+ return typeof c[0] === "string" && c[0].indexOf("sysid_DoesNotExist") !== -1;
231
+ });
232
+ expect(warned).toBe(true);
233
+ });
234
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.112",
3
+ "version": "0.0.114",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -1,7 +1,247 @@
1
1
  {
2
2
  "$schema": "dovetail-manifest/v1",
3
- "generated_at": "2026-07-14T18:00:31.248Z",
3
+ "generated_at": "2026-07-15T02:38:50.750Z",
4
4
  "events": [
5
+ {
6
+ "$schema": "dovetail-release-event/v1",
7
+ "event_id": "dovetail-servicenow@0.0.30",
8
+ "package": "@tenonhq/dovetail-servicenow",
9
+ "version": "0.0.30",
10
+ "prev_version": "0.0.29",
11
+ "published_at": "2026-07-15T02:38:50.737Z",
12
+ "git": {
13
+ "sha": "2977ae7b00bf62482f12d7a6ccf7b87babdbae21",
14
+ "tag": "dovetail-servicenow@0.0.30"
15
+ },
16
+ "semver_bump": "patch",
17
+ "commits": [
18
+ {
19
+ "sha": "2977ae7b0",
20
+ "subject": "feat(servicenow): set-field/create-record accept --from-json for large fields (#224)",
21
+ "pr": 224
22
+ },
23
+ {
24
+ "sha": "e0c350259",
25
+ "subject": "chore(release): publish 2 packages [skip ci]"
26
+ }
27
+ ],
28
+ "exports_delta": null,
29
+ "narrative": null
30
+ },
31
+ {
32
+ "$schema": "dovetail-release-event/v1",
33
+ "event_id": "dovetail-mcp@0.0.27",
34
+ "package": "@tenonhq/dovetail-mcp",
35
+ "version": "0.0.27",
36
+ "prev_version": "0.0.26",
37
+ "published_at": "2026-07-15T02:38:50.737Z",
38
+ "git": {
39
+ "sha": "2977ae7b00bf62482f12d7a6ccf7b87babdbae21",
40
+ "tag": "dovetail-mcp@0.0.27"
41
+ },
42
+ "semver_bump": "patch",
43
+ "commits": [
44
+ {
45
+ "sha": "e0c350259",
46
+ "subject": "chore(release): publish 2 packages [skip ci]"
47
+ }
48
+ ],
49
+ "exports_delta": null,
50
+ "narrative": null
51
+ },
52
+ {
53
+ "$schema": "dovetail-release-event/v1",
54
+ "event_id": "dovetail-servicenow@0.0.29",
55
+ "package": "@tenonhq/dovetail-servicenow",
56
+ "version": "0.0.29",
57
+ "prev_version": "0.0.28",
58
+ "published_at": "2026-07-15T01:16:10.133Z",
59
+ "git": {
60
+ "sha": "6d7309949cc82ece927f9a4f61614ef4a23cc5ee",
61
+ "tag": "dovetail-servicenow@0.0.29"
62
+ },
63
+ "semver_bump": "patch",
64
+ "commits": [
65
+ {
66
+ "sha": "6d7309949",
67
+ "subject": "feat(servicenow): set-column verb + set_column MCP tool (DEV-509) (#217)",
68
+ "pr": 217
69
+ },
70
+ {
71
+ "sha": "6a4081d25",
72
+ "subject": "chore(release): publish 2 packages [skip ci]"
73
+ }
74
+ ],
75
+ "exports_delta": null,
76
+ "narrative": null
77
+ },
78
+ {
79
+ "$schema": "dovetail-release-event/v1",
80
+ "event_id": "dovetail-mcp@0.0.26",
81
+ "package": "@tenonhq/dovetail-mcp",
82
+ "version": "0.0.26",
83
+ "prev_version": "0.0.25",
84
+ "published_at": "2026-07-15T01:16:10.133Z",
85
+ "git": {
86
+ "sha": "6d7309949cc82ece927f9a4f61614ef4a23cc5ee",
87
+ "tag": "dovetail-mcp@0.0.26"
88
+ },
89
+ "semver_bump": "patch",
90
+ "commits": [
91
+ {
92
+ "sha": "6a4081d25",
93
+ "subject": "chore(release): publish 2 packages [skip ci]"
94
+ }
95
+ ],
96
+ "exports_delta": null,
97
+ "narrative": null
98
+ },
99
+ {
100
+ "$schema": "dovetail-release-event/v1",
101
+ "event_id": "dovetail-servicenow@0.0.28",
102
+ "package": "@tenonhq/dovetail-servicenow",
103
+ "version": "0.0.28",
104
+ "prev_version": "0.0.27",
105
+ "published_at": "2026-07-14T20:25:03.138Z",
106
+ "git": {
107
+ "sha": "a5aeaa3c3f56c0bb24de80f12808689c5987e457",
108
+ "tag": "dovetail-servicenow@0.0.28"
109
+ },
110
+ "semver_bump": "patch",
111
+ "commits": [
112
+ {
113
+ "sha": "a5aeaa3c3",
114
+ "subject": "feat(servicenow): edit a published action type's steps in one publish (#216) (#223)",
115
+ "pr": 216
116
+ },
117
+ {
118
+ "sha": "b8967c79d",
119
+ "subject": "chore(release): publish 2 packages [skip ci]"
120
+ }
121
+ ],
122
+ "exports_delta": null,
123
+ "narrative": null
124
+ },
125
+ {
126
+ "$schema": "dovetail-release-event/v1",
127
+ "event_id": "dovetail-mcp@0.0.25",
128
+ "package": "@tenonhq/dovetail-mcp",
129
+ "version": "0.0.25",
130
+ "prev_version": "0.0.24",
131
+ "published_at": "2026-07-14T20:25:03.138Z",
132
+ "git": {
133
+ "sha": "a5aeaa3c3f56c0bb24de80f12808689c5987e457",
134
+ "tag": "dovetail-mcp@0.0.25"
135
+ },
136
+ "semver_bump": "patch",
137
+ "commits": [
138
+ {
139
+ "sha": "b8967c79d",
140
+ "subject": "chore(release): publish 2 packages [skip ci]"
141
+ }
142
+ ],
143
+ "exports_delta": null,
144
+ "narrative": null
145
+ },
146
+ {
147
+ "$schema": "dovetail-release-event/v1",
148
+ "event_id": "dovetail-servicenow@0.0.27",
149
+ "package": "@tenonhq/dovetail-servicenow",
150
+ "version": "0.0.27",
151
+ "prev_version": "0.0.26",
152
+ "published_at": "2026-07-14T20:15:42.598Z",
153
+ "git": {
154
+ "sha": "43d6c5124bec7e79537d8ad6364749f36c601d9c",
155
+ "tag": "dovetail-servicenow@0.0.27"
156
+ },
157
+ "semver_bump": "patch",
158
+ "commits": [
159
+ {
160
+ "sha": "43d6c5124",
161
+ "subject": "feat(servicenow): add-column mandatory/default via scope-aware insert (#213)",
162
+ "pr": 213
163
+ },
164
+ {
165
+ "sha": "ef9e73505",
166
+ "subject": "chore(release): publish 2 packages [skip ci]"
167
+ }
168
+ ],
169
+ "exports_delta": null,
170
+ "narrative": null
171
+ },
172
+ {
173
+ "$schema": "dovetail-release-event/v1",
174
+ "event_id": "dovetail-mcp@0.0.24",
175
+ "package": "@tenonhq/dovetail-mcp",
176
+ "version": "0.0.24",
177
+ "prev_version": "0.0.23",
178
+ "published_at": "2026-07-14T20:15:42.598Z",
179
+ "git": {
180
+ "sha": "43d6c5124bec7e79537d8ad6364749f36c601d9c",
181
+ "tag": "dovetail-mcp@0.0.24"
182
+ },
183
+ "semver_bump": "patch",
184
+ "commits": [
185
+ {
186
+ "sha": "ef9e73505",
187
+ "subject": "chore(release): publish 2 packages [skip ci]"
188
+ }
189
+ ],
190
+ "exports_delta": null,
191
+ "narrative": null
192
+ },
193
+ {
194
+ "$schema": "dovetail-release-event/v1",
195
+ "event_id": "dovetail-core@0.0.113",
196
+ "package": "@tenonhq/dovetail-core",
197
+ "version": "0.0.113",
198
+ "prev_version": "0.0.112",
199
+ "published_at": "2026-07-14T20:02:38.069Z",
200
+ "git": {
201
+ "sha": "99f064bf76f0fa4a2e049debff52be8303ebed24",
202
+ "tag": "dovetail-core@0.0.113"
203
+ },
204
+ "semver_bump": "patch",
205
+ "commits": [
206
+ {
207
+ "sha": "99f064bf7",
208
+ "subject": "fix(servicenow): stop getManifest silently dropping same-named records (#222)",
209
+ "pr": 222
210
+ },
211
+ {
212
+ "sha": "38ffe1e27",
213
+ "subject": "chore(release): @tenonhq/dovetail-core@0.0.112 [skip ci]"
214
+ }
215
+ ],
216
+ "exports_delta": null,
217
+ "narrative": null
218
+ },
219
+ {
220
+ "$schema": "dovetail-release-event/v1",
221
+ "event_id": "dovetail-core@0.0.112",
222
+ "package": "@tenonhq/dovetail-core",
223
+ "version": "0.0.112",
224
+ "prev_version": "0.0.111",
225
+ "published_at": "2026-07-14T19:40:43.702Z",
226
+ "git": {
227
+ "sha": "a2777edcbd46ee69de13e73173132f9cd18aa618",
228
+ "tag": "dovetail-core@0.0.112"
229
+ },
230
+ "semver_bump": "patch",
231
+ "commits": [
232
+ {
233
+ "sha": "a2777edcb",
234
+ "subject": "fix: reject unknown dove commands and alias pull to refresh (#219) (#221)",
235
+ "pr": 219
236
+ },
237
+ {
238
+ "sha": "6a42ef546",
239
+ "subject": "chore(release): @tenonhq/dovetail-core@0.0.111 [skip ci]"
240
+ }
241
+ ],
242
+ "exports_delta": null,
243
+ "narrative": null
244
+ },
5
245
  {
6
246
  "$schema": "dovetail-release-event/v1",
7
247
  "event_id": "dovetail-core@0.0.111",
@@ -985,298 +1225,6 @@
985
1225
  ],
986
1226
  "exports_delta": null,
987
1227
  "narrative": null
988
- },
989
- {
990
- "$schema": "dovetail-release-event/v1",
991
- "event_id": "dovetail-babel-plugin@0.0.10",
992
- "package": "@tenonhq/dovetail-babel-plugin",
993
- "version": "0.0.10",
994
- "prev_version": "0.0.9",
995
- "published_at": "2026-06-05T06:43:18.252Z",
996
- "git": {
997
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
998
- "tag": "dovetail-babel-plugin@0.0.10"
999
- },
1000
- "semver_bump": "patch",
1001
- "commits": [
1002
- {
1003
- "sha": "71bdf9bba",
1004
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1005
- "pr": 148
1006
- },
1007
- {
1008
- "sha": "9332fdf7a",
1009
- "subject": "chore(release): publish 20 packages [skip ci]"
1010
- },
1011
- {
1012
- "sha": "9046da774",
1013
- "subject": "chore(release): publish 19 packages [skip ci]"
1014
- }
1015
- ],
1016
- "exports_delta": null,
1017
- "narrative": null
1018
- },
1019
- {
1020
- "$schema": "dovetail-release-event/v1",
1021
- "event_id": "dovetail-babel-plugin-remove-modules@0.0.10",
1022
- "package": "@tenonhq/dovetail-babel-plugin-remove-modules",
1023
- "version": "0.0.10",
1024
- "prev_version": "0.0.9",
1025
- "published_at": "2026-06-05T06:43:18.252Z",
1026
- "git": {
1027
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1028
- "tag": "dovetail-babel-plugin-remove-modules@0.0.10"
1029
- },
1030
- "semver_bump": "patch",
1031
- "commits": [
1032
- {
1033
- "sha": "71bdf9bba",
1034
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1035
- "pr": 148
1036
- },
1037
- {
1038
- "sha": "9332fdf7a",
1039
- "subject": "chore(release): publish 20 packages [skip ci]"
1040
- },
1041
- {
1042
- "sha": "9046da774",
1043
- "subject": "chore(release): publish 19 packages [skip ci]"
1044
- }
1045
- ],
1046
- "exports_delta": null,
1047
- "narrative": null
1048
- },
1049
- {
1050
- "$schema": "dovetail-release-event/v1",
1051
- "event_id": "dovetail-babel-preset-servicenow@0.0.10",
1052
- "package": "@tenonhq/dovetail-babel-preset-servicenow",
1053
- "version": "0.0.10",
1054
- "prev_version": "0.0.9",
1055
- "published_at": "2026-06-05T06:43:18.252Z",
1056
- "git": {
1057
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1058
- "tag": "dovetail-babel-preset-servicenow@0.0.10"
1059
- },
1060
- "semver_bump": "patch",
1061
- "commits": [
1062
- {
1063
- "sha": "71bdf9bba",
1064
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1065
- "pr": 148
1066
- },
1067
- {
1068
- "sha": "9332fdf7a",
1069
- "subject": "chore(release): publish 20 packages [skip ci]"
1070
- },
1071
- {
1072
- "sha": "9046da774",
1073
- "subject": "chore(release): publish 19 packages [skip ci]"
1074
- }
1075
- ],
1076
- "exports_delta": null,
1077
- "narrative": null
1078
- },
1079
- {
1080
- "$schema": "dovetail-release-event/v1",
1081
- "event_id": "dovetail-schema@0.0.11",
1082
- "package": "@tenonhq/dovetail-schema",
1083
- "version": "0.0.11",
1084
- "prev_version": "0.0.10",
1085
- "published_at": "2026-06-05T06:43:18.252Z",
1086
- "git": {
1087
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1088
- "tag": "dovetail-schema@0.0.11"
1089
- },
1090
- "semver_bump": "patch",
1091
- "commits": [
1092
- {
1093
- "sha": "71bdf9bba",
1094
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1095
- "pr": 148
1096
- },
1097
- {
1098
- "sha": "a8922d9d5",
1099
- "subject": "chore(release): @tenonhq/dovetail-schema@0.0.10 [skip ci]"
1100
- }
1101
- ],
1102
- "exports_delta": null,
1103
- "narrative": null
1104
- },
1105
- {
1106
- "$schema": "dovetail-release-event/v1",
1107
- "event_id": "dovetail-core@0.0.103",
1108
- "package": "@tenonhq/dovetail-core",
1109
- "version": "0.0.103",
1110
- "prev_version": "0.0.102",
1111
- "published_at": "2026-06-05T06:43:18.252Z",
1112
- "git": {
1113
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1114
- "tag": "dovetail-core@0.0.103"
1115
- },
1116
- "semver_bump": "patch",
1117
- "commits": [
1118
- {
1119
- "sha": "71bdf9bba",
1120
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1121
- "pr": 148
1122
- },
1123
- {
1124
- "sha": "d5e064760",
1125
- "subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.15 [skip ci]"
1126
- },
1127
- {
1128
- "sha": "af1417796",
1129
- "subject": "chore(release): publish 3 packages [skip ci]"
1130
- },
1131
- {
1132
- "sha": "e25ae74c2",
1133
- "subject": "chore(release): @tenonhq/dovetail-core@0.0.102 [skip ci]"
1134
- }
1135
- ],
1136
- "exports_delta": null,
1137
- "narrative": null
1138
- },
1139
- {
1140
- "$schema": "dovetail-release-event/v1",
1141
- "event_id": "dovetail-dashboard@0.0.37",
1142
- "package": "@tenonhq/dovetail-dashboard",
1143
- "version": "0.0.37",
1144
- "prev_version": "0.0.36",
1145
- "published_at": "2026-06-05T06:43:18.252Z",
1146
- "git": {
1147
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1148
- "tag": "dovetail-dashboard@0.0.37"
1149
- },
1150
- "semver_bump": "patch",
1151
- "commits": [
1152
- {
1153
- "sha": "71bdf9bba",
1154
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1155
- "pr": 148
1156
- },
1157
- {
1158
- "sha": "ba1c67775",
1159
- "subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.36 [skip ci]"
1160
- }
1161
- ],
1162
- "exports_delta": null,
1163
- "narrative": null
1164
- },
1165
- {
1166
- "$schema": "dovetail-release-event/v1",
1167
- "event_id": "dovetail-eslint-plugin@0.0.10",
1168
- "package": "@tenonhq/dovetail-eslint-plugin",
1169
- "version": "0.0.10",
1170
- "prev_version": "0.0.9",
1171
- "published_at": "2026-06-05T06:43:18.252Z",
1172
- "git": {
1173
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1174
- "tag": "dovetail-eslint-plugin@0.0.10"
1175
- },
1176
- "semver_bump": "patch",
1177
- "commits": [
1178
- {
1179
- "sha": "71bdf9bba",
1180
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1181
- "pr": 148
1182
- },
1183
- {
1184
- "sha": "9332fdf7a",
1185
- "subject": "chore(release): publish 20 packages [skip ci]"
1186
- },
1187
- {
1188
- "sha": "9046da774",
1189
- "subject": "chore(release): publish 19 packages [skip ci]"
1190
- }
1191
- ],
1192
- "exports_delta": null,
1193
- "narrative": null
1194
- },
1195
- {
1196
- "$schema": "dovetail-release-event/v1",
1197
- "event_id": "dovetail-gmail@0.0.11",
1198
- "package": "@tenonhq/dovetail-gmail",
1199
- "version": "0.0.11",
1200
- "prev_version": "0.0.10",
1201
- "published_at": "2026-06-05T06:43:18.252Z",
1202
- "git": {
1203
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1204
- "tag": "dovetail-gmail@0.0.11"
1205
- },
1206
- "semver_bump": "patch",
1207
- "commits": [
1208
- {
1209
- "sha": "71bdf9bba",
1210
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1211
- "pr": 148
1212
- },
1213
- {
1214
- "sha": "9332fdf7a",
1215
- "subject": "chore(release): publish 20 packages [skip ci]"
1216
- },
1217
- {
1218
- "sha": "9046da774",
1219
- "subject": "chore(release): publish 19 packages [skip ci]"
1220
- }
1221
- ],
1222
- "exports_delta": null,
1223
- "narrative": null
1224
- },
1225
- {
1226
- "$schema": "dovetail-release-event/v1",
1227
- "event_id": "dovetail-google-calendar@0.0.11",
1228
- "package": "@tenonhq/dovetail-google-calendar",
1229
- "version": "0.0.11",
1230
- "prev_version": "0.0.10",
1231
- "published_at": "2026-06-05T06:43:18.252Z",
1232
- "git": {
1233
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1234
- "tag": "dovetail-google-calendar@0.0.11"
1235
- },
1236
- "semver_bump": "patch",
1237
- "commits": [
1238
- {
1239
- "sha": "71bdf9bba",
1240
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1241
- "pr": 148
1242
- },
1243
- {
1244
- "sha": "9332fdf7a",
1245
- "subject": "chore(release): publish 20 packages [skip ci]"
1246
- },
1247
- {
1248
- "sha": "9046da774",
1249
- "subject": "chore(release): publish 19 packages [skip ci]"
1250
- }
1251
- ],
1252
- "exports_delta": null,
1253
- "narrative": null
1254
- },
1255
- {
1256
- "$schema": "dovetail-release-event/v1",
1257
- "event_id": "dovetail-mcp@0.0.9",
1258
- "package": "@tenonhq/dovetail-mcp",
1259
- "version": "0.0.9",
1260
- "prev_version": "0.0.8",
1261
- "published_at": "2026-06-05T06:43:18.252Z",
1262
- "git": {
1263
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1264
- "tag": "dovetail-mcp@0.0.9"
1265
- },
1266
- "semver_bump": "patch",
1267
- "commits": [
1268
- {
1269
- "sha": "71bdf9bba",
1270
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1271
- "pr": 148
1272
- },
1273
- {
1274
- "sha": "af1417796",
1275
- "subject": "chore(release): publish 3 packages [skip ci]"
1276
- }
1277
- ],
1278
- "exports_delta": null,
1279
- "narrative": null
1280
1228
  }
1281
1229
  ]
1282
1230
  }