@tenonhq/dovetail-core 0.0.126 → 0.0.127

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
@@ -424,6 +424,7 @@ const narrowManifestToRecord = (manifest, table, sysId) => {
424
424
  };
425
425
  exports.narrowManifestToRecord = narrowManifestToRecord;
426
426
  const syncManifest = async (scope, options = {}) => {
427
+ var failedScopes = [];
427
428
  // Top-level entry owns the collector lifecycle. Recursive calls (all-scopes
428
429
  // → per-scope) inherit the collector via options._benchmarkCollector.
429
430
  var isBenchmarkOwner = false;
@@ -448,7 +449,7 @@ const syncManifest = async (scope, options = {}) => {
448
449
  Logger_1.logger.warn("Skipping scope '" + scope + "' — not declared in dove.config.js `scopes`. " +
449
450
  "Add it to config.scopes to sync, or remove its manifest file.");
450
451
  FileLogger_1.fileLogger.debug("syncManifest: skipped undeclared scope '" + scope + "'");
451
- return;
452
+ return { failedScopes: failedScopes };
452
453
  }
453
454
  Logger_1.logger.info("Refreshing scope: " + scope + "...");
454
455
  const client = (0, snClient_1.defaultClient)();
@@ -555,20 +556,25 @@ const syncManifest = async (scope, options = {}) => {
555
556
  tables: options.tables,
556
557
  _benchmarkCollector: collector,
557
558
  };
559
+ // Each child call catches its own failure and reports it in its result,
560
+ // so one broken scope never stops the rest — collect, then report.
561
+ var childScopes = [];
558
562
  if (declaredScopes.length > 0) {
559
- for (var d = 0; d < declaredScopes.length; d++) {
560
- await (0, exports.syncManifest)(declaredScopes[d], childOptions);
561
- }
563
+ childScopes = declaredScopes;
562
564
  }
563
565
  else if (ConfigManager.isMultiScopeManifest(curManifest)) {
564
566
  // No declared scopes — fall back to the persisted manifest's scopes.
565
- for (const scopeName of Object.keys(curManifest)) {
566
- await (0, exports.syncManifest)(scopeName, childOptions);
567
- }
567
+ childScopes = Object.keys(curManifest);
568
568
  }
569
569
  else if (curManifest.scope) {
570
570
  // Single scope manifest
571
- await (0, exports.syncManifest)(curManifest.scope, childOptions);
571
+ childScopes = [curManifest.scope];
572
+ }
573
+ for (var d = 0; d < childScopes.length; d++) {
574
+ var childResult = await (0, exports.syncManifest)(childScopes[d], childOptions);
575
+ if (childResult && Array.isArray(childResult.failedScopes)) {
576
+ failedScopes = failedScopes.concat(childResult.failedScopes);
577
+ }
572
578
  }
573
579
  }
574
580
  }
@@ -578,7 +584,8 @@ const syncManifest = async (scope, options = {}) => {
578
584
  message = e.message;
579
585
  else
580
586
  message = String(e);
581
- Logger_1.logger.error("Refresh failed: " + message);
587
+ Logger_1.logger.error("Refresh failed" + (scope ? " for " + scope : "") + ": " + message);
588
+ failedScopes.push({ scope: scope || "(all scopes)", error: message });
582
589
  }
583
590
  finally {
584
591
  if (isBenchmarkOwner && collector) {
@@ -586,6 +593,7 @@ const syncManifest = async (scope, options = {}) => {
586
593
  Logger_1.logger.info(collector.formatSummary());
587
594
  }
588
595
  }
596
+ return { failedScopes: failedScopes };
589
597
  };
590
598
  exports.syncManifest = syncManifest;
591
599
  const markFileMissing = (missingObj) => (table) => (recordId) => (file) => {
package/dist/commands.js CHANGED
@@ -134,13 +134,27 @@ async function refreshCommand(args, log = true) {
134
134
  ", metadataOnly=" + !!args.metadataOnly +
135
135
  ", benchmark=" + !!args.benchmark +
136
136
  ", tables=" + (tables.length > 0 ? tables.join(",") : "all") + ")");
137
- await AppUtils.syncManifest(args.scope, {
137
+ const result = await AppUtils.syncManifest(args.scope, {
138
138
  force: !!args.force,
139
139
  metadataOnly: !!args.metadataOnly,
140
140
  benchmark: !!args.benchmark,
141
141
  tables: tables.length > 0 ? tables : undefined,
142
142
  });
143
- Logger_1.logger.success("Refresh complete!");
143
+ const failed = (result && Array.isArray(result.failedScopes)) ? result.failedScopes : [];
144
+ if (failed.length > 0) {
145
+ // Every remaining scope was still attempted (syncManifest keeps going
146
+ // past a broken scope); now say so loudly and exit non-zero so scripts
147
+ // and CI can't mistake a partial refresh for a clean one.
148
+ Logger_1.logger.error("Refresh finished with " + failed.length + " failed scope" +
149
+ (failed.length === 1 ? "" : "s") + ":");
150
+ for (const f of failed) {
151
+ Logger_1.logger.error(" - " + f.scope + ": " + f.error);
152
+ }
153
+ process.exitCode = 1;
154
+ }
155
+ else {
156
+ Logger_1.logger.success("Refresh complete!");
157
+ }
144
158
  setLogLevel(args);
145
159
  }
146
160
  catch (e) {
@@ -536,9 +536,15 @@ async function createRecordCommand(args) {
536
536
  Logger_1.logger.info("Syncing record to local files...");
537
537
  FileLogger_1.fileLogger.debug("Starting single-record sync for scope:", scope);
538
538
  try {
539
- await AppUtils.syncManifest(scope, {
539
+ var syncResult = await AppUtils.syncManifest(scope, {
540
540
  record: { table: table, sysId: newSysId },
541
541
  });
542
+ // syncManifest reports a failed scope in its result instead of
543
+ // throwing; route it to the catch below so we never claim the local
544
+ // files were created when the refresh failed.
545
+ if (syncResult && syncResult.failedScopes && syncResult.failedScopes.length > 0) {
546
+ throw new Error(syncResult.failedScopes[0].error);
547
+ }
542
548
  // Resolve the scope's own source directory — syncManifest writes into
543
549
  // getSourcePathForScope(scope), which a scope config can override. Using
544
550
  // the top-level getSourcePath() here would print a wrong path in
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ /**
3
+ * `dove refresh` must fail loudly when a scope fails.
4
+ *
5
+ * Regression target: against an instance without the sync API, every scope's
6
+ * getManifest returned HTTP 400, each failure was caught and logged inside
7
+ * syncManifest, and refreshCommand still printed "Refresh complete!" and
8
+ * exited 0 — so scripts and CI read a total failure as success.
9
+ *
10
+ * Contract pinned here:
11
+ * 1. An all-scopes refresh keeps going past a failed scope (the rest still
12
+ * refresh) and returns every failure in `failedScopes`.
13
+ * 2. refreshCommand prints a per-scope failure summary, sets a non-zero exit
14
+ * code, and does NOT print "Refresh complete!".
15
+ * 3. A clean refresh still prints "Refresh complete!" and leaves the exit
16
+ * code alone.
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
+ var mockLogger = {
53
+ info: jest.fn(),
54
+ debug: jest.fn(),
55
+ warn: jest.fn(),
56
+ error: jest.fn(),
57
+ success: jest.fn(),
58
+ setLogLevel: jest.fn(),
59
+ getLogLevel: function () { return "info"; },
60
+ };
61
+ var mockClient = {
62
+ getManifest: jest.fn(),
63
+ getMissingFiles: jest.fn().mockResolvedValue({}),
64
+ };
65
+ var mockFUtils = {
66
+ writeScopeManifest: jest.fn().mockResolvedValue(undefined),
67
+ writeFileForce: jest.fn().mockResolvedValue(undefined),
68
+ writeSNFileCurry: jest.fn(() => jest.fn().mockResolvedValue(undefined)),
69
+ createDirRecursively: jest.fn().mockResolvedValue(undefined),
70
+ };
71
+ jest.mock("../Logger", function () { return { logger: mockLogger }; });
72
+ jest.mock("../FileLogger", function () {
73
+ return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
74
+ });
75
+ jest.mock("../FileUtils", function () { return mockFUtils; });
76
+ jest.mock("../snClient", function () {
77
+ return {
78
+ defaultClient: function () { return mockClient; },
79
+ unwrapSNResponse: function (p) { return Promise.resolve(p); },
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(),
88
+ getManifest: jest.fn(),
89
+ getSourcePathForScope: jest.fn().mockReturnValue("/tmp/src"),
90
+ getSourcePath: jest.fn().mockReturnValue("/tmp/src"),
91
+ getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
92
+ resolveConfigForScope: jest.fn().mockReturnValue({
93
+ tables: ["sys_script_include"],
94
+ fieldOverrides: {},
95
+ apiIncludes: {},
96
+ apiExcludes: {},
97
+ }),
98
+ isMultiScopeManifest: jest.fn().mockReturnValue(true),
99
+ updateManifest: jest.fn(),
100
+ };
101
+ jest.mock("../config", function () { return mockConfig; });
102
+ jest.mock("progress", function () {
103
+ return jest.fn().mockImplementation(function () { return { tick: jest.fn() }; });
104
+ });
105
+ const AppUtils = __importStar(require("../appUtils"));
106
+ const commands_1 = require("../commands");
107
+ var SCOPES = ["x_cadso_core", "x_cadso_work", "x_cadso_journey"];
108
+ function declareScopes() {
109
+ var scopes = {};
110
+ var manifest = {};
111
+ SCOPES.forEach(function (s) {
112
+ scopes[s] = {};
113
+ manifest[s] = { scope: s, tables: {} };
114
+ });
115
+ mockConfig.getConfig.mockReturnValue({ scopes: scopes });
116
+ mockConfig.getManifest.mockResolvedValue(manifest);
117
+ }
118
+ function errorLines() {
119
+ return mockLogger.error.mock.calls.map(function (c) { return String(c[0]); });
120
+ }
121
+ describe("syncManifest — failed scopes are collected, not swallowed", function () {
122
+ beforeEach(function () {
123
+ jest.clearAllMocks();
124
+ mockConfig.isMultiScopeManifest.mockReturnValue(true);
125
+ declareScopes();
126
+ });
127
+ test("keeps refreshing past a failed scope and reports it", async function () {
128
+ mockClient.getManifest.mockImplementation(function (scope) {
129
+ if (scope === "x_cadso_work") {
130
+ return Promise.reject(new Error("HTTP 400 from api/cadso/dovetail_sync/getManifest/x_cadso_work"));
131
+ }
132
+ return Promise.resolve({ scope: scope, tables: {} });
133
+ });
134
+ var result = await AppUtils.syncManifest();
135
+ var attempted = mockClient.getManifest.mock.calls.map(function (c) { return c[0]; });
136
+ expect(attempted).toEqual(SCOPES);
137
+ expect(result.failedScopes).toHaveLength(1);
138
+ expect(result.failedScopes[0].scope).toBe("x_cadso_work");
139
+ expect(result.failedScopes[0].error).toContain("HTTP 400");
140
+ // The scopes either side of the failure still wrote their manifests.
141
+ expect(mockFUtils.writeScopeManifest).toHaveBeenCalledTimes(2);
142
+ });
143
+ test("every scope failing reports every scope", async function () {
144
+ mockClient.getManifest.mockRejectedValue(new Error("HTTP 400"));
145
+ var result = await AppUtils.syncManifest();
146
+ expect(result.failedScopes.map(function (f) { return f.scope; })).toEqual(SCOPES);
147
+ });
148
+ test("a clean refresh reports no failures", async function () {
149
+ mockClient.getManifest.mockImplementation(function (scope) {
150
+ return Promise.resolve({ scope: scope, tables: {} });
151
+ });
152
+ var result = await AppUtils.syncManifest();
153
+ expect(result.failedScopes).toEqual([]);
154
+ });
155
+ test("a top-level failure (no manifest) is reported too", async function () {
156
+ mockConfig.getManifest.mockResolvedValue(undefined);
157
+ var result = await AppUtils.syncManifest();
158
+ expect(result.failedScopes).toHaveLength(1);
159
+ expect(result.failedScopes[0].error).toContain("No manifest file loaded");
160
+ });
161
+ });
162
+ describe("refreshCommand — exit code reflects failed scopes", function () {
163
+ var originalExitCode;
164
+ beforeEach(function () {
165
+ jest.clearAllMocks();
166
+ originalExitCode = process.exitCode;
167
+ process.exitCode = undefined;
168
+ });
169
+ afterEach(function () {
170
+ process.exitCode = originalExitCode;
171
+ jest.restoreAllMocks();
172
+ });
173
+ test("failed scopes → summary, non-zero exit, no 'Refresh complete!'", async function () {
174
+ jest.spyOn(AppUtils, "syncManifest").mockResolvedValue({
175
+ failedScopes: [
176
+ { scope: "x_cadso_core", error: "HTTP 400" },
177
+ { scope: "x_cadso_work", error: "HTTP 400" },
178
+ ],
179
+ });
180
+ await (0, commands_1.refreshCommand)({});
181
+ expect(process.exitCode).toBe(1);
182
+ expect(mockLogger.success).not.toHaveBeenCalled();
183
+ var lines = errorLines();
184
+ expect(lines.some(function (l) { return l.indexOf("2 failed scopes") !== -1; })).toBe(true);
185
+ expect(lines.some(function (l) { return l.indexOf("x_cadso_core: HTTP 400") !== -1; })).toBe(true);
186
+ expect(lines.some(function (l) { return l.indexOf("x_cadso_work: HTTP 400") !== -1; })).toBe(true);
187
+ });
188
+ test("clean refresh → 'Refresh complete!' and exit code untouched", async function () {
189
+ jest.spyOn(AppUtils, "syncManifest").mockResolvedValue({ failedScopes: [] });
190
+ await (0, commands_1.refreshCommand)({});
191
+ expect(process.exitCode).toBeUndefined();
192
+ expect(mockLogger.success).toHaveBeenCalledWith("Refresh complete!");
193
+ });
194
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.126",
3
+ "version": "0.0.127",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -1,7 +1,42 @@
1
1
  {
2
2
  "$schema": "dovetail-manifest/v1",
3
- "generated_at": "2026-10-08T18:59:46.354Z",
3
+ "generated_at": "2026-10-09T03:55:11.874Z",
4
4
  "events": [
5
+ {
6
+ "$schema": "dovetail-release-event/v1",
7
+ "event_id": "dovetail-core@0.0.126",
8
+ "package": "@tenonhq/dovetail-core",
9
+ "version": "0.0.126",
10
+ "prev_version": "0.0.125",
11
+ "published_at": "2026-10-09T03:55:11.868Z",
12
+ "git": {
13
+ "sha": "ca4fdead0bbd5c4f7ed3dd544ac3ae19a4037e42",
14
+ "tag": "dovetail-core@0.0.126"
15
+ },
16
+ "semver_bump": "patch",
17
+ "commits": [
18
+ {
19
+ "sha": "ca4fdead0",
20
+ "subject": "fix(core): dove --env accepts a bare instance name like dove-sn (#325)",
21
+ "pr": 325
22
+ },
23
+ {
24
+ "sha": "435341abf",
25
+ "subject": "chore(release): publish 2 packages [skip ci]"
26
+ },
27
+ {
28
+ "sha": "301dfbb75",
29
+ "subject": "fix: review fixes for the Oct 1-8 merges, re-land reverted #288 and #311 (#322)",
30
+ "pr": 322
31
+ },
32
+ {
33
+ "sha": "16fd9248d",
34
+ "subject": "chore(release): @tenonhq/dovetail-core@0.0.125 [skip ci]"
35
+ }
36
+ ],
37
+ "exports_delta": null,
38
+ "narrative": null
39
+ },
5
40
  {
6
41
  "$schema": "dovetail-release-event/v1",
7
42
  "event_id": "dovetail-servicenow@0.0.52",
@@ -1312,32 +1347,6 @@
1312
1347
  ],
1313
1348
  "exports_delta": null,
1314
1349
  "narrative": null
1315
- },
1316
- {
1317
- "$schema": "dovetail-release-event/v1",
1318
- "event_id": "dovetail-servicenow@0.0.38",
1319
- "package": "@tenonhq/dovetail-servicenow",
1320
- "version": "0.0.38",
1321
- "prev_version": "0.0.37",
1322
- "published_at": "2026-09-15T23:33:16.570Z",
1323
- "git": {
1324
- "sha": "925c0ca40cc6407fa78f28b25b82f8dc0fbd460f",
1325
- "tag": "dovetail-servicenow@0.0.38"
1326
- },
1327
- "semver_bump": "patch",
1328
- "commits": [
1329
- {
1330
- "sha": "925c0ca40",
1331
- "subject": "feat(servicenow): headless update-set / app export with mandatory secret stripping (#276)",
1332
- "pr": 276
1333
- },
1334
- {
1335
- "sha": "83df94645",
1336
- "subject": "chore(release): publish 2 packages [skip ci]"
1337
- }
1338
- ],
1339
- "exports_delta": null,
1340
- "narrative": null
1341
1350
  }
1342
1351
  ]
1343
1352
  }