@tenonhq/dovetail-core 0.0.85

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 (78) hide show
  1. package/README.md +6 -0
  2. package/dist/FileLogger.js +207 -0
  3. package/dist/FileUtils.js +387 -0
  4. package/dist/Logger.js +88 -0
  5. package/dist/MultiScopeWatcher.js +615 -0
  6. package/dist/PluginManager.js +125 -0
  7. package/dist/_defaultOptionsOLD.js +44 -0
  8. package/dist/allScopesCommands.js +418 -0
  9. package/dist/appUtils.js +929 -0
  10. package/dist/benchmark.js +104 -0
  11. package/dist/bootstrap.js +62 -0
  12. package/dist/claudeCommand.js +60 -0
  13. package/dist/clickupCommands.js +472 -0
  14. package/dist/clickupPushHelper.js +33 -0
  15. package/dist/commander.js +466 -0
  16. package/dist/commands.js +378 -0
  17. package/dist/config.js +697 -0
  18. package/dist/constants.js +15 -0
  19. package/dist/createRecordCommand.js +314 -0
  20. package/dist/dashboardCommand.js +38 -0
  21. package/dist/defaultOptions.js +14 -0
  22. package/dist/deleteRecordCommand.js +282 -0
  23. package/dist/flowDesigner/values.js +55 -0
  24. package/dist/genericUtils.js +189 -0
  25. package/dist/gitUtils.js +99 -0
  26. package/dist/index.js +35 -0
  27. package/dist/initSystem/corePlugin.js +344 -0
  28. package/dist/initSystem/discovery.js +107 -0
  29. package/dist/initSystem/orchestrator.js +408 -0
  30. package/dist/logMessages.js +82 -0
  31. package/dist/loginCommand.js +20 -0
  32. package/dist/migrateCommand.js +123 -0
  33. package/dist/projectFiles.js +32 -0
  34. package/dist/recentEdits.js +66 -0
  35. package/dist/schemaCommand.js +85 -0
  36. package/dist/snClient.js +519 -0
  37. package/dist/tests/benchmarkRefresh.test.js +265 -0
  38. package/dist/tests/clickupCommands.test.js +384 -0
  39. package/dist/tests/clickupPushHelper.test.js +124 -0
  40. package/dist/tests/configFallback.test.js +68 -0
  41. package/dist/tests/cred.test.js +59 -0
  42. package/dist/tests/discovery.test.js +67 -0
  43. package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
  44. package/dist/tests/errorLogLevels.test.js +273 -0
  45. package/dist/tests/example.test.js +6 -0
  46. package/dist/tests/fileContextSkipReason.test.js +116 -0
  47. package/dist/tests/globalDebounce.test.js +308 -0
  48. package/dist/tests/migrateCommand.test.js +113 -0
  49. package/dist/tests/multi-scope-watcher.test.js +558 -0
  50. package/dist/tests/orchestrator.test.js +23 -0
  51. package/dist/tests/pushFiles.test.js +248 -0
  52. package/dist/tests/rateLimitCoordination.test.js +271 -0
  53. package/dist/tests/resolveConfigReadOnly.test.js +67 -0
  54. package/dist/tests/retryOnHttpErr.test.js +154 -0
  55. package/dist/tests/scopeCaching.test.js +115 -0
  56. package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
  57. package/dist/tests/snClient.fallback.test.js +120 -0
  58. package/dist/tests/syncManifestRefresh.test.js +287 -0
  59. package/dist/tests/syncManifestWhitelist.test.js +192 -0
  60. package/dist/tests/taskClear.test.js +170 -0
  61. package/dist/tests/taskStaleness.test.js +220 -0
  62. package/dist/tests/unwrapSNResponseError.test.js +145 -0
  63. package/dist/tests/v2Values.test.js +79 -0
  64. package/dist/tests/validateTaskId.test.js +304 -0
  65. package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
  66. package/dist/tests/writeEnvVar.test.js +119 -0
  67. package/dist/updateSetCommands.js +702 -0
  68. package/dist/wizard.js +40 -0
  69. package/package.json +69 -0
  70. package/skills/dove-configure-pipeline.md +158 -0
  71. package/skills/dove-create-plugin.md +174 -0
  72. package/skills/dove-create-record.md +135 -0
  73. package/skills/dove-debug-build.md +117 -0
  74. package/skills/dove-delete-record.md +104 -0
  75. package/skills/dove-manage-tables.md +174 -0
  76. package/skills/dove-manage-update-sets.md +88 -0
  77. package/skills/dove-setup-project.md +154 -0
  78. package/skills/dove-troubleshoot-sync.md +113 -0
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ function createMockWatcher() {
40
+ var handlers = {};
41
+ var mock = {
42
+ _handlers: handlers,
43
+ on: jest.fn(function (event, handler) {
44
+ if (!handlers[event])
45
+ handlers[event] = [];
46
+ handlers[event].push(handler);
47
+ return mock;
48
+ }),
49
+ close: jest.fn(),
50
+ _emit: function (event) {
51
+ var args = Array.prototype.slice.call(arguments, 1);
52
+ (handlers[event] || []).forEach(function (h) { h.apply(null, args); });
53
+ },
54
+ };
55
+ return mock;
56
+ }
57
+ var mockWatchers = [];
58
+ jest.mock("chokidar", function () {
59
+ return {
60
+ watch: jest.fn(function () {
61
+ var w = createMockWatcher();
62
+ mockWatchers.push(w);
63
+ return w;
64
+ }),
65
+ };
66
+ });
67
+ // Capture debounce calls to verify single global debounce
68
+ var capturedDebounceFns = [];
69
+ jest.mock("lodash", function () {
70
+ var actual = jest.requireActual("lodash");
71
+ return {
72
+ ...actual,
73
+ debounce: jest.fn(function (fn) {
74
+ capturedDebounceFns.push(fn);
75
+ var wrapper = jest.fn();
76
+ wrapper.cancel = jest.fn();
77
+ wrapper.flush = jest.fn(function () { return fn(); });
78
+ return wrapper;
79
+ }),
80
+ };
81
+ });
82
+ var mockSNClient = {
83
+ getScopeId: jest.fn(),
84
+ getUserSysId: jest.fn(),
85
+ getCurrentAppUserPrefSysId: jest.fn(),
86
+ updateCurrentAppUserPref: jest.fn(),
87
+ createCurrentAppUserPref: jest.fn(),
88
+ getCurrentUpdateSetUserPref: jest.fn(),
89
+ changeScope: jest.fn().mockResolvedValue(undefined),
90
+ };
91
+ jest.mock("../snClient", function () {
92
+ return {
93
+ defaultClient: jest.fn(function () { return mockSNClient; }),
94
+ unwrapSNResponse: jest.fn(function (val) { return val; }),
95
+ };
96
+ });
97
+ jest.mock("../FileUtils", function () {
98
+ return {
99
+ getFileContextFromPath: jest.fn(),
100
+ getFileContextWithSkipReason: jest.fn(),
101
+ };
102
+ });
103
+ jest.mock("../appUtils", function () {
104
+ return {
105
+ groupAppFiles: jest.fn(),
106
+ pushFiles: jest.fn(),
107
+ };
108
+ });
109
+ jest.mock("../logMessages", function () {
110
+ return { logFilePush: jest.fn() };
111
+ });
112
+ jest.mock("../recentEdits", function () {
113
+ return { writeRecentEdit: jest.fn() };
114
+ });
115
+ jest.mock("../Logger", function () {
116
+ return {
117
+ logger: {
118
+ info: jest.fn(),
119
+ error: jest.fn(),
120
+ warn: jest.fn(),
121
+ debug: jest.fn(),
122
+ success: jest.fn(),
123
+ getLogLevel: jest.fn().mockReturnValue("info"),
124
+ },
125
+ };
126
+ });
127
+ jest.mock("../config", function () {
128
+ return {
129
+ loadConfigs: jest.fn().mockResolvedValue(undefined),
130
+ getConfig: jest.fn(),
131
+ getRootDir: jest.fn().mockReturnValue("/project"),
132
+ updateManifest: jest.fn(),
133
+ getManifest: jest.fn(),
134
+ getSourcePath: jest.fn().mockReturnValue("/project/src"),
135
+ getScopeManifestPath: jest.fn(function (scope) { return "/project/dove.manifest." + scope + ".json"; }),
136
+ getManifestPath: jest.fn().mockReturnValue("/project/dove.manifest.json"),
137
+ };
138
+ });
139
+ jest.mock("fs", function () {
140
+ return {
141
+ existsSync: jest.fn(),
142
+ readFileSync: jest.fn(),
143
+ writeFileSync: jest.fn(),
144
+ statSync: jest.fn().mockReturnValue({ mtimeMs: Date.now() }),
145
+ promises: {
146
+ readFile: jest.fn(),
147
+ writeFile: jest.fn(),
148
+ readdir: jest.fn(),
149
+ mkdir: jest.fn(),
150
+ access: jest.fn(),
151
+ stat: jest.fn(),
152
+ },
153
+ };
154
+ });
155
+ jest.mock("axios", function () {
156
+ return {
157
+ default: {
158
+ create: jest.fn(function () {
159
+ return { get: jest.fn().mockResolvedValue({ data: { result: null } }) };
160
+ }),
161
+ },
162
+ };
163
+ });
164
+ // --- Imports ---
165
+ const fs_1 = __importDefault(require("fs"));
166
+ const ConfigManager = __importStar(require("../config"));
167
+ const FileUtils_1 = require("../FileUtils");
168
+ const appUtils_1 = require("../appUtils");
169
+ const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
170
+ // --- Fixtures ---
171
+ var makeFileContext = function (overrides) {
172
+ return Object.assign({
173
+ filePath: "/project/src/x_test_core/sys_script_include/TestScript/script.js",
174
+ ext: ".js",
175
+ sys_id: "abc123",
176
+ name: "TestScript",
177
+ scope: "x_test_core",
178
+ tableName: "sys_script_include",
179
+ targetField: "script",
180
+ }, overrides || {});
181
+ };
182
+ var TWO_SCOPES_CONFIG = {
183
+ sourceDirectory: "src",
184
+ buildDirectory: "build",
185
+ rules: [],
186
+ includes: {},
187
+ excludes: {},
188
+ tableOptions: {},
189
+ refreshInterval: 30,
190
+ scopes: {
191
+ x_scope_a: { sourceDirectory: "src/x_scope_a" },
192
+ x_scope_b: { sourceDirectory: "src/x_scope_b" },
193
+ },
194
+ };
195
+ // --- Tests ---
196
+ describe("US-014: Global debounce for serialized scope processing", function () {
197
+ beforeEach(function () {
198
+ jest.clearAllMocks();
199
+ mockWatchers.length = 0;
200
+ capturedDebounceFns.length = 0;
201
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
202
+ MultiScopeWatcher_1.multiScopeWatcher.cachedScope = null;
203
+ MultiScopeWatcher_1.multiScopeWatcher.cachedUserSysId = null;
204
+ MultiScopeWatcher_1.multiScopeWatcher.pendingScopes = new Map();
205
+ MultiScopeWatcher_1.multiScopeWatcher.globalProcessQueue = null;
206
+ // Default scope switching succeeds
207
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
208
+ mockSNClient.getUserSysId.mockResolvedValue([{ sys_id: "user_sys_id" }]);
209
+ mockSNClient.getCurrentAppUserPrefSysId.mockResolvedValue([{ sys_id: "pref_sys_id" }]);
210
+ mockSNClient.updateCurrentAppUserPref.mockResolvedValue({});
211
+ mockSNClient.createCurrentAppUserPref.mockResolvedValue({});
212
+ fs_1.default.existsSync.mockReturnValue(false);
213
+ });
214
+ afterEach(function () {
215
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
216
+ jest.useRealTimers();
217
+ });
218
+ it("creates a single global debounce, not one per scope", async function () {
219
+ ConfigManager.getConfig.mockReturnValue(TWO_SCOPES_CONFIG);
220
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 0 });
221
+ // Two scopes but only one debounce function captured
222
+ expect(capturedDebounceFns.length).toBe(1);
223
+ });
224
+ it("processes multiple scopes triggered within debounce window in a single batch", async function () {
225
+ ConfigManager.getConfig.mockReturnValue(TWO_SCOPES_CONFIG);
226
+ var ctxA = makeFileContext({ scope: "x_scope_a", filePath: "/project/src/x_scope_a/sys_script_include/A/script.js" });
227
+ var ctxB = makeFileContext({ scope: "x_scope_b", filePath: "/project/src/x_scope_b/sys_script_include/B/script.js" });
228
+ FileUtils_1.getFileContextWithSkipReason.mockImplementation(function (fp) {
229
+ if (fp.indexOf("x_scope_a") !== -1)
230
+ return { context: ctxA };
231
+ if (fp.indexOf("x_scope_b") !== -1)
232
+ return { context: ctxB };
233
+ return { skipReason: "unknown" };
234
+ });
235
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
236
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
237
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 0 });
238
+ // Emit changes in both scopes before debounce fires
239
+ mockWatchers[0]._emit("change", ctxA.filePath);
240
+ mockWatchers[1]._emit("change", ctxB.filePath);
241
+ // Fire the single global debounce
242
+ await capturedDebounceFns[0]();
243
+ // Both scopes should have been processed (pushFiles called twice — once per scope)
244
+ expect(appUtils_1.pushFiles).toHaveBeenCalledTimes(2);
245
+ });
246
+ it("processes scopes in FIFO order by first file change timestamp", async function () {
247
+ ConfigManager.getConfig.mockReturnValue(TWO_SCOPES_CONFIG);
248
+ var ctxA = makeFileContext({ scope: "x_scope_a", filePath: "/project/src/x_scope_a/sys_script_include/A/script.js" });
249
+ var ctxB = makeFileContext({ scope: "x_scope_b", filePath: "/project/src/x_scope_b/sys_script_include/B/script.js" });
250
+ FileUtils_1.getFileContextWithSkipReason.mockImplementation(function (fp) {
251
+ if (fp.indexOf("x_scope_a") !== -1)
252
+ return { context: ctxA };
253
+ if (fp.indexOf("x_scope_b") !== -1)
254
+ return { context: ctxB };
255
+ return { skipReason: "unknown" };
256
+ });
257
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
258
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
259
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 0 });
260
+ // Scope B changes first, then scope A
261
+ // Use pendingScopes directly to control timestamps for deterministic ordering
262
+ MultiScopeWatcher_1.multiScopeWatcher.pendingScopes.set("x_scope_b", 1000);
263
+ MultiScopeWatcher_1.multiScopeWatcher.pendingScopes.set("x_scope_a", 2000);
264
+ // Add files to queues manually
265
+ var watcherA = MultiScopeWatcher_1.multiScopeWatcher.scopeWatchers.get("x_scope_a");
266
+ var watcherB = MultiScopeWatcher_1.multiScopeWatcher.scopeWatchers.get("x_scope_b");
267
+ watcherA.pushQueue.push(ctxA.filePath);
268
+ watcherB.pushQueue.push(ctxB.filePath);
269
+ // Track the order of scope switches
270
+ var switchOrder = [];
271
+ mockSNClient.changeScope.mockImplementation(function (scope) {
272
+ switchOrder.push(scope);
273
+ return Promise.resolve(undefined);
274
+ });
275
+ // Fire the global debounce
276
+ await capturedDebounceFns[0]();
277
+ // Scope B (timestamp 1000) should be processed before scope A (timestamp 2000)
278
+ expect(switchOrder[0]).toBe("x_scope_b");
279
+ expect(switchOrder[1]).toBe("x_scope_a");
280
+ });
281
+ it("clears pendingScopes after processing", async function () {
282
+ ConfigManager.getConfig.mockReturnValue(TWO_SCOPES_CONFIG);
283
+ var ctx = makeFileContext({ scope: "x_scope_a", filePath: "/project/src/x_scope_a/sys_script_include/A/script.js" });
284
+ FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
285
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
286
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
287
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 0 });
288
+ mockWatchers[0]._emit("change", ctx.filePath);
289
+ await capturedDebounceFns[0]();
290
+ expect(MultiScopeWatcher_1.multiScopeWatcher.pendingScopes.size).toBe(0);
291
+ });
292
+ it("only records first change timestamp per scope (not subsequent changes)", async function () {
293
+ ConfigManager.getConfig.mockReturnValue({
294
+ ...TWO_SCOPES_CONFIG,
295
+ scopes: { x_scope_a: { sourceDirectory: "src/x_scope_a" } },
296
+ });
297
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 0 });
298
+ // Record the timestamp after first emit
299
+ mockWatchers[0]._emit("change", "/project/src/x_scope_a/sys_script_include/A/script.js");
300
+ var firstTimestamp = MultiScopeWatcher_1.multiScopeWatcher.pendingScopes.get("x_scope_a");
301
+ // Wait a bit and emit again
302
+ await new Promise(function (r) { setTimeout(r, 5); });
303
+ mockWatchers[0]._emit("change", "/project/src/x_scope_a/sys_script_include/B/script.js");
304
+ var secondTimestamp = MultiScopeWatcher_1.multiScopeWatcher.pendingScopes.get("x_scope_a");
305
+ // Timestamp should not have changed
306
+ expect(secondTimestamp).toBe(firstTimestamp);
307
+ });
308
+ });
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ // Tests for the `dove migrate` command (migrateCommand.ts). Covers: dry-run
3
+ // no-op, --apply renames files and rewrites package.json deps + scripts,
4
+ // collision safety (existing target skips rename), and the widened script
5
+ // rewrite that catches mid-string sinc invocations (e.g. `&& sinc push`).
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ var mockLogger = {
11
+ info: jest.fn(),
12
+ debug: jest.fn(),
13
+ warn: jest.fn(),
14
+ error: jest.fn(),
15
+ success: jest.fn(),
16
+ getLogLevel: function () { return "debug"; },
17
+ };
18
+ jest.mock("../Logger", function () {
19
+ return { logger: mockLogger };
20
+ });
21
+ jest.mock("../commands", function () {
22
+ return { setLogLevel: jest.fn() };
23
+ });
24
+ const fs_1 = __importDefault(require("fs"));
25
+ const os_1 = __importDefault(require("os"));
26
+ const path_1 = __importDefault(require("path"));
27
+ const migrateCommand_1 = require("../migrateCommand");
28
+ function makeTempDir() {
29
+ return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "dove-migrate-test-"));
30
+ }
31
+ function rmrf(dir) {
32
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
33
+ }
34
+ describe("migrateCommand", function () {
35
+ var origCwd = process.cwd();
36
+ var tmp = "";
37
+ beforeEach(function () {
38
+ jest.clearAllMocks();
39
+ tmp = makeTempDir();
40
+ process.chdir(tmp);
41
+ });
42
+ afterEach(function () {
43
+ process.chdir(origCwd);
44
+ rmrf(tmp);
45
+ });
46
+ it("dry-run makes zero filesystem changes", async function () {
47
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "sinc.config.js"), "module.exports = {};\n");
48
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "package.json"), JSON.stringify({ scripts: { build: "sinc push" }, devDependencies: { "@tenonhq/sincronia-core": "^1.0.0" } }, null, 2));
49
+ await (0, migrateCommand_1.migrateCommand)({});
50
+ expect(fs_1.default.existsSync(path_1.default.join(tmp, "sinc.config.js"))).toBe(true);
51
+ expect(fs_1.default.existsSync(path_1.default.join(tmp, "dove.config.js"))).toBe(false);
52
+ var pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmp, "package.json"), "utf8"));
53
+ expect(pkg.scripts.build).toBe("sinc push");
54
+ expect(pkg.devDependencies["@tenonhq/sincronia-core"]).toBe("^1.0.0");
55
+ });
56
+ it("--apply renames sinc.* artifacts and rewrites package.json", async function () {
57
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "sinc.config.js"), "module.exports = {};\n");
58
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "sinc.manifest.json"), "{}\n");
59
+ fs_1.default.writeFileSync(path_1.default.join(tmp, ".sinc-active-task.json"), "{}\n");
60
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "sinc.manifest.x_cadso_core.json"), "{}\n");
61
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "package.json"), JSON.stringify({
62
+ scripts: { build: "sinc push", watch: "npx sinc watch" },
63
+ devDependencies: {
64
+ "@tenonhq/sincronia-core": "^1.0.0",
65
+ "@tenonhq/sincronia-types": "^1.0.0",
66
+ },
67
+ }, null, 2));
68
+ await (0, migrateCommand_1.migrateCommand)({ apply: true });
69
+ expect(fs_1.default.existsSync(path_1.default.join(tmp, "sinc.config.js"))).toBe(false);
70
+ expect(fs_1.default.existsSync(path_1.default.join(tmp, "dove.config.js"))).toBe(true);
71
+ expect(fs_1.default.existsSync(path_1.default.join(tmp, "dove.manifest.json"))).toBe(true);
72
+ expect(fs_1.default.existsSync(path_1.default.join(tmp, ".dove-active-task.json"))).toBe(true);
73
+ expect(fs_1.default.existsSync(path_1.default.join(tmp, "dove.manifest.x_cadso_core.json"))).toBe(true);
74
+ var pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmp, "package.json"), "utf8"));
75
+ expect(pkg.scripts.build).toBe("dove push");
76
+ expect(pkg.scripts.watch).toBe("npx dove watch");
77
+ expect(pkg.devDependencies["@tenonhq/dovetail-core"]).toBe("^1.0.0");
78
+ expect(pkg.devDependencies["@tenonhq/dovetail-types"]).toBe("^1.0.0");
79
+ expect(pkg.devDependencies["@tenonhq/sincronia-core"]).toBeUndefined();
80
+ });
81
+ it("rewrites mid-string sinc invocations (chained commands)", async function () {
82
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "package.json"), JSON.stringify({
83
+ scripts: {
84
+ deploy: "npm run build && sinc push && sinc deploy",
85
+ ci: "lint && test ; sinc status",
86
+ // Should not match `sincronia` (the package name appears in scripts via npx).
87
+ note: "echo @tenonhq/sincronia-core",
88
+ },
89
+ }, null, 2));
90
+ await (0, migrateCommand_1.migrateCommand)({ apply: true });
91
+ var pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmp, "package.json"), "utf8"));
92
+ expect(pkg.scripts.deploy).toBe("npm run build && dove push && dove deploy");
93
+ expect(pkg.scripts.ci).toBe("lint && test ; dove status");
94
+ // \bsinc\b does not match `sincronia` (no word boundary between c and r),
95
+ // and @tenonhq/sincronia-* is rewritten to @tenonhq/dovetail-* by the
96
+ // earlier package-name pass.
97
+ expect(pkg.scripts.note).toBe("echo @tenonhq/dovetail-core");
98
+ });
99
+ it("skips rename when target file already exists and warns", async function () {
100
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "sinc.config.js"), "// legacy\n");
101
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "dove.config.js"), "// new\n");
102
+ await (0, migrateCommand_1.migrateCommand)({ apply: true });
103
+ expect(fs_1.default.readFileSync(path_1.default.join(tmp, "sinc.config.js"), "utf8")).toBe("// legacy\n");
104
+ expect(fs_1.default.readFileSync(path_1.default.join(tmp, "dove.config.js"), "utf8")).toBe("// new\n");
105
+ expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Skipping rename of sinc.config.js"));
106
+ });
107
+ it("reports nothing-to-migrate when project is already on dove.*", async function () {
108
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "dove.config.js"), "module.exports = {};\n");
109
+ fs_1.default.writeFileSync(path_1.default.join(tmp, "package.json"), JSON.stringify({ scripts: { build: "dove push" }, devDependencies: { "@tenonhq/dovetail-core": "^1.0.0" } }, null, 2));
110
+ await (0, migrateCommand_1.migrateCommand)({ apply: true });
111
+ expect(mockLogger.success).toHaveBeenCalledWith(expect.stringContaining("Nothing to migrate"));
112
+ });
113
+ });