@tenonhq/dovetail-core 0.0.105 → 0.0.107
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 +22 -16
- package/dist/commander.js +2 -2
- package/dist/reconcile/applyPlan.js +4 -3
- package/dist/reconcile/multiPass.js +55 -0
- package/dist/reconcileCommand.js +178 -16
- package/dist/snClient.js +10 -0
- package/dist/tests/reconcileApplyPlan.test.js +2 -2
- package/dist/tests/reconcileMultiPass.test.js +74 -0
- package/dist/tests/updateSetConfig.test.js +126 -0
- package/dist/updateSetCommands.js +61 -0
- package/dist/updateSetConfig.js +55 -0
- package/package.json +3 -3
- package/release-manifest.json +190 -225
- package/skills/dove-manage-update-sets.md +6 -0
- package/dist/reconcile/deleteOrder.js +0 -52
- package/dist/tests/reconcileDeleteOrder.test.js +0 -70
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TenonHQ/Dovetail#182: readUpdateSetConfig / writeUpdateSetRouting
|
|
4
|
+
*
|
|
5
|
+
* The push-routing file (.dove-update-sets.json) is the source of truth for
|
|
6
|
+
* `dove push`. These tests pin the read/merge/write contract that keeps it in
|
|
7
|
+
* sync with createUpdateSet / switchUpdateSet.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
var mockFsStore = {};
|
|
11
|
+
jest.mock("fs", function () {
|
|
12
|
+
return {
|
|
13
|
+
existsSync: jest.fn(function (p) {
|
|
14
|
+
return p in mockFsStore;
|
|
15
|
+
}),
|
|
16
|
+
readFileSync: jest.fn(function (p) {
|
|
17
|
+
if (p in mockFsStore)
|
|
18
|
+
return mockFsStore[p];
|
|
19
|
+
throw new Error("ENOENT: " + p);
|
|
20
|
+
}),
|
|
21
|
+
writeFileSync: jest.fn(function (p, data) {
|
|
22
|
+
mockFsStore[p] = data;
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
var CONFIG_PATH = "/repo/.dove-update-sets.json";
|
|
27
|
+
jest.mock("../projectFiles", function () {
|
|
28
|
+
return {
|
|
29
|
+
getUpdateSetsConfigPath: jest.fn(function () {
|
|
30
|
+
return CONFIG_PATH;
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
jest.mock("../Logger", function () {
|
|
35
|
+
return {
|
|
36
|
+
logger: {
|
|
37
|
+
info: jest.fn(),
|
|
38
|
+
error: jest.fn(),
|
|
39
|
+
warn: jest.fn(),
|
|
40
|
+
debug: jest.fn(),
|
|
41
|
+
success: jest.fn(),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
const updateSetConfig_1 = require("../updateSetConfig");
|
|
46
|
+
const Logger_1 = require("../Logger");
|
|
47
|
+
describe("updateSetConfig", function () {
|
|
48
|
+
beforeEach(function () {
|
|
49
|
+
for (var k in mockFsStore)
|
|
50
|
+
delete mockFsStore[k];
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
describe("readUpdateSetConfig", function () {
|
|
54
|
+
test("returns {} when the file is absent", function () {
|
|
55
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({});
|
|
56
|
+
});
|
|
57
|
+
test("parses an existing routing file", function () {
|
|
58
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify({
|
|
59
|
+
x_cadso_core: { sys_id: "abc", name: "Set A" },
|
|
60
|
+
});
|
|
61
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({
|
|
62
|
+
x_cadso_core: { sys_id: "abc", name: "Set A" },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
test("returns {} and warns on unparseable JSON", function () {
|
|
66
|
+
mockFsStore[CONFIG_PATH] = "{ not json";
|
|
67
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({});
|
|
68
|
+
expect(Logger_1.logger.warn).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
test("returns {} and warns when the file is a non-object (array)", function () {
|
|
71
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify(["nope"]);
|
|
72
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({});
|
|
73
|
+
expect(Logger_1.logger.warn).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("writeUpdateSetRouting", function () {
|
|
77
|
+
test("writes a fresh routing entry and returns the path", function () {
|
|
78
|
+
var written = (0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
79
|
+
scope: "x_cadso_core",
|
|
80
|
+
sysId: "new123",
|
|
81
|
+
name: "New Set",
|
|
82
|
+
});
|
|
83
|
+
expect(written).toBe(CONFIG_PATH);
|
|
84
|
+
expect(JSON.parse(mockFsStore[CONFIG_PATH])).toEqual({
|
|
85
|
+
x_cadso_core: { sys_id: "new123", name: "New Set" },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
test("merges without clobbering other scopes", function () {
|
|
89
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify({
|
|
90
|
+
x_cadso_automate: { sys_id: "keep", name: "Keep Me" },
|
|
91
|
+
});
|
|
92
|
+
(0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
93
|
+
scope: "x_cadso_core",
|
|
94
|
+
sysId: "new123",
|
|
95
|
+
name: "New Set",
|
|
96
|
+
});
|
|
97
|
+
expect(JSON.parse(mockFsStore[CONFIG_PATH])).toEqual({
|
|
98
|
+
x_cadso_automate: { sys_id: "keep", name: "Keep Me" },
|
|
99
|
+
x_cadso_core: { sys_id: "new123", name: "New Set" },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
test("overwrites the same scope's stale entry", function () {
|
|
103
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify({
|
|
104
|
+
x_cadso_core: { sys_id: "old", name: "Old Set" },
|
|
105
|
+
});
|
|
106
|
+
(0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
107
|
+
scope: "x_cadso_core",
|
|
108
|
+
sysId: "fresh",
|
|
109
|
+
name: "Fresh Set",
|
|
110
|
+
});
|
|
111
|
+
expect(JSON.parse(mockFsStore[CONFIG_PATH])).toEqual({
|
|
112
|
+
x_cadso_core: { sys_id: "fresh", name: "Fresh Set" },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
test("returns null and writes nothing when scope is missing", function () {
|
|
116
|
+
var written = (0, updateSetConfig_1.writeUpdateSetRouting)({ sysId: "x", name: "y" });
|
|
117
|
+
expect(written).toBeNull();
|
|
118
|
+
expect(mockFsStore[CONFIG_PATH]).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
test("returns null when sys_id or name is blank", function () {
|
|
121
|
+
expect((0, updateSetConfig_1.writeUpdateSetRouting)({ scope: "x_cadso_core", sysId: " ", name: "y" })).toBeNull();
|
|
122
|
+
expect((0, updateSetConfig_1.writeUpdateSetRouting)({ scope: "x_cadso_core", sysId: "x", name: "" })).toBeNull();
|
|
123
|
+
expect(mockFsStore[CONFIG_PATH]).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -49,6 +49,7 @@ const commands_1 = require("./commands");
|
|
|
49
49
|
const chalk_1 = __importDefault(require("chalk"));
|
|
50
50
|
const dovetail_clickup_1 = require("@tenonhq/dovetail-clickup");
|
|
51
51
|
const clickupCommands_1 = require("./clickupCommands");
|
|
52
|
+
const updateSetConfig_1 = require("./updateSetConfig");
|
|
52
53
|
/**
|
|
53
54
|
* Shows the current scope
|
|
54
55
|
*/
|
|
@@ -233,6 +234,26 @@ async function createUpdateSetCommand(args) {
|
|
|
233
234
|
if (scope) {
|
|
234
235
|
Logger_1.logger.info(`Scope: ${scope}`);
|
|
235
236
|
}
|
|
237
|
+
// Keep .dove-update-sets.json in sync with the set we just activated.
|
|
238
|
+
// `dove push` routes captures by this file (per scope), not by the
|
|
239
|
+
// instance's active update set — so without this write the new set would
|
|
240
|
+
// stay empty and pushes would land in whatever stale set the file still
|
|
241
|
+
// points at (TenonHQ/Dovetail#182).
|
|
242
|
+
if (scope) {
|
|
243
|
+
const routed = (0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
244
|
+
scope,
|
|
245
|
+
sysId: updateSetSysId,
|
|
246
|
+
name,
|
|
247
|
+
});
|
|
248
|
+
if (routed) {
|
|
249
|
+
Logger_1.logger.info(chalk_1.default.green(`✓ Push routing updated: ${scope} -> ${name} (${updateSetSysId})`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
Logger_1.logger.warn("No scope provided, so .dove-update-sets.json was not updated. " +
|
|
254
|
+
"`dove push` routes captures by scope via that file — pass -s <scope> " +
|
|
255
|
+
"so pushes target this set, or edit .dove-update-sets.json directly.");
|
|
256
|
+
}
|
|
236
257
|
// If created from a ClickUp task, offer to post a comment back
|
|
237
258
|
if (args._clickupTaskId) {
|
|
238
259
|
try {
|
|
@@ -292,6 +313,22 @@ async function switchUpdateSetCommand(args) {
|
|
|
292
313
|
if (applicationName) {
|
|
293
314
|
Logger_1.logger.info(`Scope: ${applicationName}`);
|
|
294
315
|
}
|
|
316
|
+
// Keep .dove-update-sets.json in sync so `dove push` routes captures to the
|
|
317
|
+
// set we just switched to (it routes by this file per scope, not by the
|
|
318
|
+
// instance's active set — TenonHQ/Dovetail#182). `applicationScope` is the
|
|
319
|
+
// scope's sys_id, so resolve it back to the scope name the file is keyed on.
|
|
320
|
+
const scopeName = args.scope || (await resolveScopeName(applicationScope));
|
|
321
|
+
if (scopeName) {
|
|
322
|
+
const routed = (0, updateSetConfig_1.writeUpdateSetRouting)({ scope: scopeName, sysId, name });
|
|
323
|
+
if (routed) {
|
|
324
|
+
Logger_1.logger.info(chalk_1.default.green(`✓ Push routing updated: ${scopeName} -> ${name} (${sysId})`));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
Logger_1.logger.warn("Could not resolve the scope name, so .dove-update-sets.json was not " +
|
|
329
|
+
"updated. `dove push` routes captures by scope via that file — pass " +
|
|
330
|
+
"--scope <scope> so pushes target this set.");
|
|
331
|
+
}
|
|
295
332
|
}
|
|
296
333
|
catch (e) {
|
|
297
334
|
Logger_1.logger.error("Failed to switch update set");
|
|
@@ -489,6 +526,30 @@ async function getActiveUpdateSetName(client, scope) {
|
|
|
489
526
|
return null;
|
|
490
527
|
}
|
|
491
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Resolves a scope's sys_id back to its scope name (e.g. "x_cadso_core"), used
|
|
531
|
+
* to key the .dove-update-sets.json push-routing entry. Returns undefined when
|
|
532
|
+
* the input is missing or the lookup fails so the caller can warn instead.
|
|
533
|
+
*/
|
|
534
|
+
async function resolveScopeName(applicationSysId) {
|
|
535
|
+
if (!applicationSysId || applicationSysId.trim() === "")
|
|
536
|
+
return undefined;
|
|
537
|
+
try {
|
|
538
|
+
const client = (0, snClient_1.defaultClient)();
|
|
539
|
+
const rows = await (0, snClient_1.unwrapSNResponse)(client.getScopeById(applicationSysId));
|
|
540
|
+
if (rows && rows.length > 0) {
|
|
541
|
+
const resolved = rows[0].scope;
|
|
542
|
+
if (resolved && typeof resolved === "string" && resolved.trim() !== "") {
|
|
543
|
+
return resolved;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
Logger_1.logger.debug("Could not resolve scope name from application sys_id: " +
|
|
549
|
+
(e instanceof Error ? e.message : String(e)));
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
492
553
|
/**
|
|
493
554
|
* Helper function to switch to a scope
|
|
494
555
|
*/
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readUpdateSetConfig = readUpdateSetConfig;
|
|
7
|
+
exports.writeUpdateSetRouting = writeUpdateSetRouting;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const projectFiles_1 = require("./projectFiles");
|
|
10
|
+
const Logger_1 = require("./Logger");
|
|
11
|
+
/**
|
|
12
|
+
* Read .dove-update-sets.json (or its legacy .sinc- sibling). Returns {} when
|
|
13
|
+
* the file is absent or unparseable, so callers never have to guard for null.
|
|
14
|
+
*/
|
|
15
|
+
function readUpdateSetConfig() {
|
|
16
|
+
const configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
17
|
+
try {
|
|
18
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
19
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(configPath, "utf8"));
|
|
20
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
Logger_1.logger.warn(`Update set config at ${configPath} is not an object; ignoring it.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
Logger_1.logger.warn(`Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
28
|
+
}
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Merge a single scope's update set into .dove-update-sets.json so that
|
|
33
|
+
* `dove push` routes that scope's records to the set the operator just created
|
|
34
|
+
* or switched to.
|
|
35
|
+
*
|
|
36
|
+
* Without this, `dove createUpdateSet` / `dove switchUpdateSet` only flip the
|
|
37
|
+
* instance's *active* update set, while `dove push` keeps routing via this file
|
|
38
|
+
* — so a freshly "activated" set can stay empty and the work lands in whatever
|
|
39
|
+
* stale set the file still points at (TenonHQ/Dovetail#182).
|
|
40
|
+
*
|
|
41
|
+
* Returns the path written, or null when any input is missing (no scope name,
|
|
42
|
+
* sys_id, or set name to route by) so the caller can warn instead.
|
|
43
|
+
*/
|
|
44
|
+
function writeUpdateSetRouting(opts) {
|
|
45
|
+
const scope = opts.scope ? opts.scope.trim() : "";
|
|
46
|
+
const sysId = opts.sysId ? opts.sysId.trim() : "";
|
|
47
|
+
const name = opts.name ? opts.name.trim() : "";
|
|
48
|
+
if (!scope || !sysId || !name)
|
|
49
|
+
return null;
|
|
50
|
+
const configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
51
|
+
const config = readUpdateSetConfig();
|
|
52
|
+
config[scope] = { sys_id: sysId, name };
|
|
53
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
54
|
+
return configPath;
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenonhq/dovetail-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"description": "Next-gen file syncer",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=22"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@tenonhq/dovetail-types": "~0.0.
|
|
25
|
+
"@tenonhq/dovetail-types": "~0.0.20",
|
|
26
26
|
"@types/dotenv": "^6.1.1",
|
|
27
27
|
"@types/inquirer": "^8.2.10",
|
|
28
28
|
"@types/jest": "^29.5.5",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@tenonhq/dovetail-clickup": "~0.0.12",
|
|
42
|
-
"@tenonhq/dovetail-schema": "~0.0.
|
|
42
|
+
"@tenonhq/dovetail-schema": "~0.0.12",
|
|
43
43
|
"axios": "^1.5.1",
|
|
44
44
|
"axios-cookiejar-support": "^4.0.7",
|
|
45
45
|
"axios-rate-limit": "^1.3.0",
|