@tenonhq/dovetail-core 0.0.122 → 0.0.124
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/README.md +8 -0
- package/dist/allScopesCommands.js +2 -1
- package/dist/commander.js +2 -2
- package/dist/createRecordCommand.js +232 -8
- package/dist/initSystem/orchestrator.js +10 -2
- package/dist/snClient.js +15 -0
- package/dist/tests/createRecordUpdateSet.test.js +276 -0
- package/dist/tests/watchHidden.test.js +50 -0
- package/package.json +1 -1
- package/release-manifest.json +23 -22
package/README.md
CHANGED
|
@@ -35,6 +35,14 @@ npx dove migrate # migrate a Sincronia project to Dovetail (dry-run by def
|
|
|
35
35
|
|
|
36
36
|
See [`UPDATE_SET_COMMANDS.md`](UPDATE_SET_COMMANDS.md) for the full update-set CLI surface.
|
|
37
37
|
|
|
38
|
+
### `dove create sys_update_set`
|
|
39
|
+
|
|
40
|
+
`npx dove create <table>` inserts through the generic `createRecord` endpoint — except for `sys_update_set`. A plain insert lets ServiceNow default the set's `application` to the API session's current app, so `--scope` was silently ignored and the set landed in whatever scope the session was in (TenonHQ/Dovetail#231). Update sets therefore route through the atomic, scope-correct `createUpdateSet` server op (`POST /api/cadso/dovetail_core/createUpdateSet`) and the created set is read back: if its `application` does not match the requested scope the command exits non-zero, naming the requested vs. actual scope and the set's sys_id, so a mis-scoped set is never reported as success.
|
|
41
|
+
|
|
42
|
+
- `--scope` decides the application. It is **required** with `--ci`; interactively it is confirmed in the summary.
|
|
43
|
+
- Only `name` and `description` map onto the op; other `--field` values are ignored with a warning.
|
|
44
|
+
- The set is created, not activated — use `npx dove switchUpdateSet --name "<name>" -s <scope>` (or `npx dove createUpdateSet`, which creates and activates in one step).
|
|
45
|
+
|
|
38
46
|
## Plugins
|
|
39
47
|
|
|
40
48
|
Dovetail's build pipeline is plugin-driven. Each plugin is an npm package implementing `run(context, content, options) => Promise<PluginResults>`. Shipped plugins live in sibling packages (`@tenonhq/dovetail-typescript-plugin`, `-babel-plugin`, `-webpack-plugin`, `-sass-plugin`, `-eslint-plugin`, `-prettier-plugin`, `-babel-preset-servicenow`, `-babel-plugin-remove-modules`).
|
|
@@ -363,7 +363,8 @@ async function initScopesCommand(args) {
|
|
|
363
363
|
Logger_1.logger.warn(failCount + " scope(s) failed — check errors above");
|
|
364
364
|
}
|
|
365
365
|
Logger_1.logger.info("Manifests: dove.manifest.<scope>.json");
|
|
366
|
-
|
|
366
|
+
// Headless next step only — the file watcher is human-only (TenonHQ/Dovetail#155).
|
|
367
|
+
Logger_1.logger.success("Next: edit locally, then 'npx dove push --diff <branch>' to sync the changed files");
|
|
367
368
|
}
|
|
368
369
|
catch (e) {
|
|
369
370
|
Logger_1.logger.error("Error initializing scopes: " + e);
|
package/dist/commander.js
CHANGED
|
@@ -214,7 +214,7 @@ function configureCli(cli) {
|
|
|
214
214
|
await (0, commands_1.buildCommand)(args);
|
|
215
215
|
})
|
|
216
216
|
.command("deploy", "Deploy local build files to the scoped application", sharedOptions, commands_1.deployCommand)
|
|
217
|
-
.command("create <table>", "Create a new record on the ServiceNow instance", (cmdArgs) => {
|
|
217
|
+
.command("create <table>", "Create a new record on the ServiceNow instance (sys_update_set goes through the scope-correct createUpdateSet op; --scope decides its application)", (cmdArgs) => {
|
|
218
218
|
cmdArgs.positional("table", {
|
|
219
219
|
describe: "ServiceNow table name (e.g., sys_script_include)",
|
|
220
220
|
type: "string",
|
|
@@ -229,7 +229,7 @@ function configureCli(cli) {
|
|
|
229
229
|
scope: {
|
|
230
230
|
alias: "s",
|
|
231
231
|
type: "string",
|
|
232
|
-
describe: "Target scope (e.g., x_cadso_core)",
|
|
232
|
+
describe: "Target scope (e.g., x_cadso_core). For sys_update_set this decides the set's application and is required with --ci",
|
|
233
233
|
},
|
|
234
234
|
from: {
|
|
235
235
|
alias: "f",
|
|
@@ -36,6 +36,8 @@ 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.UpdateSetScopeMismatchError = exports.UPDATE_SET_TABLE = void 0;
|
|
40
|
+
exports.createUpdateSetRecord = createUpdateSetRecord;
|
|
39
41
|
exports.createRecordCommand = createRecordCommand;
|
|
40
42
|
const fs_1 = __importDefault(require("fs"));
|
|
41
43
|
const path_1 = __importDefault(require("path"));
|
|
@@ -48,6 +50,55 @@ const commands_1 = require("./commands");
|
|
|
48
50
|
const ConfigManager = __importStar(require("./config"));
|
|
49
51
|
const AppUtils = __importStar(require("./appUtils"));
|
|
50
52
|
const projectFiles_1 = require("./projectFiles");
|
|
53
|
+
/**
|
|
54
|
+
* Table whose rows must NOT go through the generic record insert. A plain
|
|
55
|
+
* insert lets ServiceNow default `application` to the API session's current
|
|
56
|
+
* app (gs.getCurrentApplicationId()), so `--scope` was silently ignored and
|
|
57
|
+
* the set landed in whatever scope the session happened to be in
|
|
58
|
+
* (TenonHQ/Dovetail#231). Rows for this table route through the atomic
|
|
59
|
+
* `createUpdateSet` server op instead.
|
|
60
|
+
*/
|
|
61
|
+
exports.UPDATE_SET_TABLE = "sys_update_set";
|
|
62
|
+
/** Fields `dove create` can map onto the createUpdateSet server op. */
|
|
63
|
+
const UPDATE_SET_OP_FIELDS = ["name", "description"];
|
|
64
|
+
/** Scope names are identifiers (`x_cadso_core`, `global`) — nothing else. */
|
|
65
|
+
const SCOPE_NAME_PATTERN = /^[A-Za-z0-9_]+$/;
|
|
66
|
+
/**
|
|
67
|
+
* Raised when a created update set is readable but its `application` does
|
|
68
|
+
* not match the requested scope. Carries both sides so callers can print an
|
|
69
|
+
* actionable message and exit non-zero instead of claiming success.
|
|
70
|
+
*/
|
|
71
|
+
class UpdateSetScopeMismatchError extends Error {
|
|
72
|
+
updateSetSysId;
|
|
73
|
+
requestedScope;
|
|
74
|
+
requestedScopeSysId;
|
|
75
|
+
actualScope;
|
|
76
|
+
actualScopeSysId;
|
|
77
|
+
constructor(details) {
|
|
78
|
+
super('Update set "' +
|
|
79
|
+
details.updateSetName +
|
|
80
|
+
'" (' +
|
|
81
|
+
details.updateSetSysId +
|
|
82
|
+
") landed in scope " +
|
|
83
|
+
details.actualScope +
|
|
84
|
+
" (" +
|
|
85
|
+
details.actualScopeSysId +
|
|
86
|
+
") but --scope " +
|
|
87
|
+
details.requestedScope +
|
|
88
|
+
" (" +
|
|
89
|
+
details.requestedScopeSysId +
|
|
90
|
+
") was requested. The set is MIS-SCOPED: delete it on the instance " +
|
|
91
|
+
"before it collects captures, then confirm the Dovetail " +
|
|
92
|
+
"createUpdateSet REST op is installed on this instance and retry.");
|
|
93
|
+
this.name = "UpdateSetScopeMismatchError";
|
|
94
|
+
this.updateSetSysId = details.updateSetSysId;
|
|
95
|
+
this.requestedScope = details.requestedScope;
|
|
96
|
+
this.requestedScopeSysId = details.requestedScopeSysId;
|
|
97
|
+
this.actualScope = details.actualScope;
|
|
98
|
+
this.actualScopeSysId = details.actualScopeSysId;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.UpdateSetScopeMismatchError = UpdateSetScopeMismatchError;
|
|
51
102
|
const getUpdateSetConfig = () => {
|
|
52
103
|
const configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
53
104
|
try {
|
|
@@ -155,6 +206,142 @@ async function resolveScope(args) {
|
|
|
155
206
|
]);
|
|
156
207
|
return scopeAnswer.scope;
|
|
157
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Normalizes a Table API reference field — a bare sys_id string, or the
|
|
211
|
+
* `{ value, link }` object the Table API returns without display values —
|
|
212
|
+
* to its sys_id. Returns undefined for anything else (missing, empty, wrong
|
|
213
|
+
* shape) so callers never compare against garbage.
|
|
214
|
+
*/
|
|
215
|
+
function referenceSysId(value) {
|
|
216
|
+
if (typeof value === "string") {
|
|
217
|
+
return value.trim() === "" ? undefined : value.trim();
|
|
218
|
+
}
|
|
219
|
+
if (value !== null && typeof value === "object") {
|
|
220
|
+
var inner = value.value;
|
|
221
|
+
if (typeof inner === "string" && inner.trim() !== "") {
|
|
222
|
+
return inner.trim();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Reads the created set back and returns its `application` sys_id. Falls
|
|
229
|
+
* back to the create response when the read-back fails or carries no
|
|
230
|
+
* application. Returns undefined when neither source can prove the scope.
|
|
231
|
+
*/
|
|
232
|
+
async function readBackApplication(client, updateSetSysId, createResponseApplication) {
|
|
233
|
+
try {
|
|
234
|
+
var rows = await (0, snClient_1.unwrapSNResponse)(client.getUpdateSetById(updateSetSysId));
|
|
235
|
+
if (Array.isArray(rows) && rows.length > 0) {
|
|
236
|
+
var fromReadBack = referenceSysId(rows[0].application);
|
|
237
|
+
if (fromReadBack) {
|
|
238
|
+
return fromReadBack;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
FileLogger_1.fileLogger.warn("Update set read-back returned no application for " + updateSetSysId);
|
|
242
|
+
}
|
|
243
|
+
catch (readErr) {
|
|
244
|
+
FileLogger_1.fileLogger.warn("Update set read-back failed for " +
|
|
245
|
+
updateSetSysId +
|
|
246
|
+
": " +
|
|
247
|
+
(readErr instanceof Error ? readErr.message : String(readErr)));
|
|
248
|
+
}
|
|
249
|
+
return referenceSysId(createResponseApplication);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Resolves a scope sys_id back to its name for error messages. Never throws —
|
|
253
|
+
* falls back to the sys_id itself so a lookup failure can't mask the
|
|
254
|
+
* mismatch we are about to report.
|
|
255
|
+
*/
|
|
256
|
+
async function scopeNameForSysId(client, scopeSysId) {
|
|
257
|
+
try {
|
|
258
|
+
var rows = await (0, snClient_1.unwrapSNResponse)(client.getScopeById(scopeSysId));
|
|
259
|
+
if (Array.isArray(rows) && rows.length > 0 && rows[0].scope) {
|
|
260
|
+
return rows[0].scope;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
// fall through to the sys_id
|
|
265
|
+
}
|
|
266
|
+
return scopeSysId;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Creates a `sys_update_set` through the scope-correct `createUpdateSet`
|
|
270
|
+
* server op and PROVES it landed in the requested scope. Throws on a
|
|
271
|
+
* mis-scoped or unverifiable set — the caller must never report success.
|
|
272
|
+
*/
|
|
273
|
+
async function createUpdateSetRecord(client, scope, fields) {
|
|
274
|
+
if (typeof scope !== "string" || !SCOPE_NAME_PATTERN.test(scope)) {
|
|
275
|
+
throw new Error("Invalid scope name " +
|
|
276
|
+
JSON.stringify(scope) +
|
|
277
|
+
": expected an application scope such as x_cadso_core.");
|
|
278
|
+
}
|
|
279
|
+
var name = typeof fields.name === "string" ? fields.name.trim() : "";
|
|
280
|
+
if (name === "") {
|
|
281
|
+
throw new Error("Update set name is required.");
|
|
282
|
+
}
|
|
283
|
+
var description = typeof fields.description === "string" && fields.description.trim() !== ""
|
|
284
|
+
? fields.description
|
|
285
|
+
: undefined;
|
|
286
|
+
var unsupported = Object.keys(fields).filter(function (key) {
|
|
287
|
+
return UPDATE_SET_OP_FIELDS.indexOf(key) === -1;
|
|
288
|
+
});
|
|
289
|
+
if (unsupported.length > 0) {
|
|
290
|
+
Logger_1.logger.warn("sys_update_set is created through the scope-correct createUpdateSet " +
|
|
291
|
+
"op, which accepts only " +
|
|
292
|
+
UPDATE_SET_OP_FIELDS.join(", ") +
|
|
293
|
+
". Ignoring field(s): " +
|
|
294
|
+
unsupported.join(", "));
|
|
295
|
+
}
|
|
296
|
+
// Resolve the scope name to the application sys_id the op needs.
|
|
297
|
+
var scopeRows = await (0, snClient_1.unwrapSNResponse)(client.getScopeId(scope));
|
|
298
|
+
if (!Array.isArray(scopeRows) || scopeRows.length === 0) {
|
|
299
|
+
throw new Error('Scope "' + scope + '" not found on the instance.');
|
|
300
|
+
}
|
|
301
|
+
var scopeSysId = scopeRows[0].sys_id;
|
|
302
|
+
if (typeof scopeSysId !== "string" || scopeSysId.trim() === "") {
|
|
303
|
+
throw new Error('Scope "' + scope + '" resolved without a sys_id.');
|
|
304
|
+
}
|
|
305
|
+
FileLogger_1.fileLogger.debug("createUpdateSet op:", JSON.stringify({ name: name, scope: scope, application: scopeSysId }));
|
|
306
|
+
var created = await (0, snClient_1.unwrapSNResponse)(client.createUpdateSet(name, scopeSysId, description));
|
|
307
|
+
var createdSysId = created && typeof created.sys_id === "string" ? created.sys_id : "";
|
|
308
|
+
if (createdSysId === "") {
|
|
309
|
+
throw new Error("createUpdateSet returned no sys_id for " + name);
|
|
310
|
+
}
|
|
311
|
+
// Verify on the way out: the whole point of this path is that the set is
|
|
312
|
+
// in the requested scope, so prove it rather than trust the op.
|
|
313
|
+
var actualScopeSysId = await readBackApplication(client, createdSysId, created.application);
|
|
314
|
+
if (!actualScopeSysId) {
|
|
315
|
+
throw new Error('Update set "' +
|
|
316
|
+
name +
|
|
317
|
+
'" (' +
|
|
318
|
+
createdSysId +
|
|
319
|
+
") was created but its application scope could not be verified " +
|
|
320
|
+
"(requested " +
|
|
321
|
+
scope +
|
|
322
|
+
" / " +
|
|
323
|
+
scopeSysId +
|
|
324
|
+
"). Inspect the set on the instance before using it.");
|
|
325
|
+
}
|
|
326
|
+
if (actualScopeSysId !== scopeSysId) {
|
|
327
|
+
var actualScope = await scopeNameForSysId(client, actualScopeSysId);
|
|
328
|
+
throw new UpdateSetScopeMismatchError({
|
|
329
|
+
updateSetName: name,
|
|
330
|
+
updateSetSysId: createdSysId,
|
|
331
|
+
requestedScope: scope,
|
|
332
|
+
requestedScopeSysId: scopeSysId,
|
|
333
|
+
actualScope: actualScope,
|
|
334
|
+
actualScopeSysId: actualScopeSysId,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
var createdName = created.name;
|
|
338
|
+
return {
|
|
339
|
+
sysId: createdSysId,
|
|
340
|
+
name: typeof createdName === "string" && createdName !== "" ? createdName : name,
|
|
341
|
+
scope: scope,
|
|
342
|
+
scopeSysId: scopeSysId,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
158
345
|
/**
|
|
159
346
|
* Main create record command handler.
|
|
160
347
|
*/
|
|
@@ -167,7 +354,16 @@ async function createRecordCommand(args) {
|
|
|
167
354
|
Logger_1.logger.error("Table name is required");
|
|
168
355
|
process.exit(1);
|
|
169
356
|
}
|
|
357
|
+
var isUpdateSet = table === exports.UPDATE_SET_TABLE;
|
|
170
358
|
FileLogger_1.fileLogger.debug("Create record: table=" + table);
|
|
359
|
+
// An update set's application is decided by --scope alone. In CI there is
|
|
360
|
+
// no human to confirm a manifest-inferred scope, so require it explicitly
|
|
361
|
+
// rather than let an inherited scope mis-route every later capture.
|
|
362
|
+
if (isUpdateSet && typedArgs.ci && !typedArgs.scope) {
|
|
363
|
+
Logger_1.logger.error("--scope is required when creating sys_update_set in --ci mode: " +
|
|
364
|
+
"the scope decides the update set's application.");
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
171
367
|
// 1. Build field values from all sources
|
|
172
368
|
var fields = {};
|
|
173
369
|
// Load from JSON file if provided
|
|
@@ -209,13 +405,17 @@ async function createRecordCommand(args) {
|
|
|
209
405
|
// 3. Resolve scope
|
|
210
406
|
var scope = await resolveScope(typedArgs);
|
|
211
407
|
Logger_1.logger.info("Scope: " + chalk_1.default.cyan(scope));
|
|
212
|
-
// 4. Check update set configuration
|
|
213
|
-
|
|
214
|
-
var updateSet
|
|
408
|
+
// 4. Check update set configuration. An update set is not itself captured
|
|
409
|
+
// in an update set, so skip the lookup for sys_update_set.
|
|
410
|
+
var updateSet;
|
|
215
411
|
var updateSetSysId;
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
412
|
+
if (!isUpdateSet) {
|
|
413
|
+
var updateSetConfig = getUpdateSetConfig();
|
|
414
|
+
updateSet = updateSetConfig[scope];
|
|
415
|
+
if (updateSet) {
|
|
416
|
+
Logger_1.logger.info("Update set: " + chalk_1.default.cyan(updateSet.name));
|
|
417
|
+
updateSetSysId = updateSet.sys_id;
|
|
418
|
+
}
|
|
219
419
|
}
|
|
220
420
|
// 5. Confirmation prompt
|
|
221
421
|
if (!typedArgs.ci) {
|
|
@@ -224,6 +424,9 @@ async function createRecordCommand(args) {
|
|
|
224
424
|
Logger_1.logger.info(" Table: " + chalk_1.default.cyan(table));
|
|
225
425
|
Logger_1.logger.info(" Name: " + chalk_1.default.cyan(fields.name));
|
|
226
426
|
Logger_1.logger.info(" Scope: " + chalk_1.default.cyan(scope));
|
|
427
|
+
if (isUpdateSet) {
|
|
428
|
+
Logger_1.logger.info(" Via: Dovetail createUpdateSet op (application = scope above)");
|
|
429
|
+
}
|
|
227
430
|
if (updateSet) {
|
|
228
431
|
Logger_1.logger.info(" Update Set: " + chalk_1.default.cyan(updateSet.name));
|
|
229
432
|
}
|
|
@@ -249,7 +452,29 @@ async function createRecordCommand(args) {
|
|
|
249
452
|
return;
|
|
250
453
|
}
|
|
251
454
|
}
|
|
252
|
-
|
|
455
|
+
var client = (0, snClient_1.defaultClient)();
|
|
456
|
+
// 6a. sys_update_set: the scope-correct server op, verified on the way out.
|
|
457
|
+
if (isUpdateSet) {
|
|
458
|
+
Logger_1.logger.info("Creating update set via the Dovetail createUpdateSet op...");
|
|
459
|
+
var createdSet = await createUpdateSetRecord(client, scope, fields);
|
|
460
|
+
Logger_1.logger.success(chalk_1.default.green("Update set created: ") +
|
|
461
|
+
chalk_1.default.bold(createdSet.name) +
|
|
462
|
+
" (" +
|
|
463
|
+
createdSet.sysId +
|
|
464
|
+
") in scope " +
|
|
465
|
+
chalk_1.default.cyan(createdSet.scope));
|
|
466
|
+
Logger_1.logger.info("Verified application = " +
|
|
467
|
+
createdSet.scope +
|
|
468
|
+
" (" +
|
|
469
|
+
createdSet.scopeSysId +
|
|
470
|
+
").");
|
|
471
|
+
Logger_1.logger.info("Not activated. To route pushes to it: npx dove switchUpdateSet --name " +
|
|
472
|
+
JSON.stringify(createdSet.name) +
|
|
473
|
+
" -s " +
|
|
474
|
+
createdSet.scope);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// 6b. Every other table: the generic create endpoint
|
|
253
478
|
Logger_1.logger.info("Creating record...");
|
|
254
479
|
FileLogger_1.fileLogger.debug("Create request:", JSON.stringify({
|
|
255
480
|
table: table,
|
|
@@ -257,7 +482,6 @@ async function createRecordCommand(args) {
|
|
|
257
482
|
scope: scope,
|
|
258
483
|
update_set_sys_id: updateSetSysId,
|
|
259
484
|
}));
|
|
260
|
-
var client = (0, snClient_1.defaultClient)();
|
|
261
485
|
var createResponse = await client.createRecord({
|
|
262
486
|
table: table,
|
|
263
487
|
fields: fields,
|
|
@@ -375,7 +375,14 @@ async function runInit(options) {
|
|
|
375
375
|
Logger_1.logger.warn(" Setup completed with errors. Review the output above.");
|
|
376
376
|
}
|
|
377
377
|
else {
|
|
378
|
-
|
|
378
|
+
// Headless next steps only — the file watcher is human-only and hidden from
|
|
379
|
+
// Claude-facing output (TenonHQ/Dovetail#155).
|
|
380
|
+
Logger_1.logger.success(chalk_1.default.green(" Setup complete!") +
|
|
381
|
+
" Run " +
|
|
382
|
+
chalk_1.default.cyan("dove status") +
|
|
383
|
+
", then " +
|
|
384
|
+
chalk_1.default.cyan("dove refresh") +
|
|
385
|
+
" to pull the manifest and files.");
|
|
379
386
|
}
|
|
380
387
|
Logger_1.logger.info("");
|
|
381
388
|
}
|
|
@@ -447,7 +454,8 @@ async function runLogin(options) {
|
|
|
447
454
|
Logger_1.logger.info("");
|
|
448
455
|
Logger_1.logger.info("You can now use:");
|
|
449
456
|
Logger_1.logger.info(" dove init — Initialize a new project");
|
|
450
|
-
Logger_1.logger.info(" dove
|
|
457
|
+
Logger_1.logger.info(" dove refresh — Pull the manifest + new files");
|
|
458
|
+
Logger_1.logger.info(" dove push --diff main — Push the files changed vs a branch");
|
|
451
459
|
Logger_1.logger.info(" dove status — Check instance connection");
|
|
452
460
|
Logger_1.logger.info("");
|
|
453
461
|
}
|
package/dist/snClient.js
CHANGED
|
@@ -352,6 +352,20 @@ const snClient = (baseURL, username, password, apiKey) => {
|
|
|
352
352
|
},
|
|
353
353
|
});
|
|
354
354
|
};
|
|
355
|
+
/**
|
|
356
|
+
* Reads one update set back by sys_id, selecting only `application`, so a
|
|
357
|
+
* caller can PROVE which application a freshly created set landed in. The
|
|
358
|
+
* Table API returns the reference as `{ value, link }` (no display value).
|
|
359
|
+
*/
|
|
360
|
+
const getUpdateSetById = (updateSetSysId) => {
|
|
361
|
+
const endpoint = "api/now/table/sys_update_set";
|
|
362
|
+
return client.get(endpoint, {
|
|
363
|
+
params: {
|
|
364
|
+
sysparm_query: `sys_id=${updateSetSysId}`,
|
|
365
|
+
sysparm_fields: "sys_id,name,application",
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
};
|
|
355
369
|
const getUserSysId = (userName = process.env.SN_USER) => {
|
|
356
370
|
const endpoint = "api/now/table/sys_user";
|
|
357
371
|
return client.get(endpoint, {
|
|
@@ -523,6 +537,7 @@ const snClient = (baseURL, username, password, apiKey) => {
|
|
|
523
537
|
updateRecord,
|
|
524
538
|
getScopeId,
|
|
525
539
|
getScopeById,
|
|
540
|
+
getUpdateSetById,
|
|
526
541
|
getUserSysId,
|
|
527
542
|
getCurrentAppUserPrefSysId,
|
|
528
543
|
updateCurrentAppUserPref,
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for `dove create sys_update_set` (TenonHQ/Dovetail#231).
|
|
3
|
+
//
|
|
4
|
+
// A generic record insert into sys_update_set lets ServiceNow default
|
|
5
|
+
// `application` to the API session's current app, so `--scope` was silently
|
|
6
|
+
// ignored. `dove create` must instead:
|
|
7
|
+
// 1. route sys_update_set through snClient.createUpdateSet with the resolved
|
|
8
|
+
// scope's sys_id — and NOT call the generic createRecord;
|
|
9
|
+
// 2. read the set back and exit non-zero, naming requested vs. actual scope
|
|
10
|
+
// and the set sys_id, when the application does not match;
|
|
11
|
+
// 3. require --scope for sys_update_set in --ci mode;
|
|
12
|
+
// 4. leave every other table on the generic createRecord path.
|
|
13
|
+
//
|
|
14
|
+
// Everything is mocked — no instance is contacted.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
var logMessages = [];
|
|
17
|
+
function pushLog(level) {
|
|
18
|
+
return jest.fn(function (msg) {
|
|
19
|
+
logMessages.push({ level: level, msg: String(msg) });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
jest.mock("../Logger", function () {
|
|
23
|
+
return {
|
|
24
|
+
logger: {
|
|
25
|
+
setLogLevel: jest.fn(),
|
|
26
|
+
success: pushLog("success"),
|
|
27
|
+
info: pushLog("info"),
|
|
28
|
+
error: pushLog("error"),
|
|
29
|
+
warn: pushLog("warn"),
|
|
30
|
+
debug: jest.fn(),
|
|
31
|
+
getLogLevel: function () { return "info"; },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
jest.mock("../FileLogger", function () {
|
|
36
|
+
return {
|
|
37
|
+
fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
jest.mock("../commands", function () {
|
|
41
|
+
return { setLogLevel: jest.fn() };
|
|
42
|
+
});
|
|
43
|
+
jest.mock("../config", function () {
|
|
44
|
+
return {
|
|
45
|
+
getManifest: jest.fn(function () { return undefined; }),
|
|
46
|
+
isMultiScopeManifest: jest.fn(function () { return false; }),
|
|
47
|
+
getConfig: jest.fn(function () { return {}; }),
|
|
48
|
+
getSourcePathForScope: jest.fn(function () { return "/virtual/src"; }),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
var mockSyncManifest = jest.fn().mockResolvedValue(undefined);
|
|
52
|
+
jest.mock("../appUtils", function () {
|
|
53
|
+
return { syncManifest: mockSyncManifest };
|
|
54
|
+
});
|
|
55
|
+
jest.mock("../projectFiles", function () {
|
|
56
|
+
return {
|
|
57
|
+
getUpdateSetsConfigPath: jest.fn(function () {
|
|
58
|
+
return "/nonexistent/dove-create-update-set-test/.dove-update-sets.json";
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
jest.mock("inquirer", function () {
|
|
63
|
+
return { prompt: jest.fn() };
|
|
64
|
+
});
|
|
65
|
+
var mockClient = {
|
|
66
|
+
getScopeId: jest.fn(),
|
|
67
|
+
getScopeById: jest.fn(),
|
|
68
|
+
getUpdateSetById: jest.fn(),
|
|
69
|
+
createUpdateSet: jest.fn(),
|
|
70
|
+
createRecord: jest.fn(),
|
|
71
|
+
};
|
|
72
|
+
jest.mock("../snClient", function () {
|
|
73
|
+
return {
|
|
74
|
+
defaultClient: jest.fn(function () { return mockClient; }),
|
|
75
|
+
// Mirror the real unwrapSNResponse: await the axios promise, return .data.result
|
|
76
|
+
unwrapSNResponse: jest.fn(async function (p) {
|
|
77
|
+
var resp = await p;
|
|
78
|
+
return resp.data.result;
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
const createRecordCommand_1 = require("../createRecordCommand");
|
|
83
|
+
function axiosResult(result) {
|
|
84
|
+
return Promise.resolve({ status: 200, data: { result: result } });
|
|
85
|
+
}
|
|
86
|
+
var REQUESTED_SCOPE = "x_cadso_journey";
|
|
87
|
+
var REQUESTED_SCOPE_SYS_ID = "journeyScopeSysId0000000000000001";
|
|
88
|
+
var SESSION_SCOPE = "x_cadso_core";
|
|
89
|
+
var SESSION_SCOPE_SYS_ID = "coreScopeSysId00000000000000000002";
|
|
90
|
+
var SET_SYS_ID = "updateSetSysId000000000000000000ab";
|
|
91
|
+
class ExitSignal extends Error {
|
|
92
|
+
code;
|
|
93
|
+
constructor(code) {
|
|
94
|
+
super("process.exit(" + code + ")");
|
|
95
|
+
this.code = code;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
describe("dove create sys_update_set (#231)", function () {
|
|
99
|
+
var exitSpy;
|
|
100
|
+
beforeEach(function () {
|
|
101
|
+
logMessages = [];
|
|
102
|
+
jest.clearAllMocks();
|
|
103
|
+
exitSpy = jest
|
|
104
|
+
.spyOn(process, "exit")
|
|
105
|
+
.mockImplementation(function (code) {
|
|
106
|
+
throw new ExitSignal(typeof code === "number" ? code : undefined);
|
|
107
|
+
});
|
|
108
|
+
mockClient.getScopeId.mockImplementation(function (scopeName) {
|
|
109
|
+
if (scopeName === REQUESTED_SCOPE) {
|
|
110
|
+
return axiosResult([{ sys_id: REQUESTED_SCOPE_SYS_ID }]);
|
|
111
|
+
}
|
|
112
|
+
return axiosResult([]);
|
|
113
|
+
});
|
|
114
|
+
mockClient.getScopeById.mockImplementation(function (sysId) {
|
|
115
|
+
if (sysId === SESSION_SCOPE_SYS_ID) {
|
|
116
|
+
return axiosResult([{ sys_id: SESSION_SCOPE_SYS_ID, scope: SESSION_SCOPE }]);
|
|
117
|
+
}
|
|
118
|
+
if (sysId === REQUESTED_SCOPE_SYS_ID) {
|
|
119
|
+
return axiosResult([{ sys_id: REQUESTED_SCOPE_SYS_ID, scope: REQUESTED_SCOPE }]);
|
|
120
|
+
}
|
|
121
|
+
return axiosResult([]);
|
|
122
|
+
});
|
|
123
|
+
mockClient.createUpdateSet.mockImplementation(function (name, scopeSysId) {
|
|
124
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name, application: scopeSysId });
|
|
125
|
+
});
|
|
126
|
+
mockClient.createRecord.mockResolvedValue({
|
|
127
|
+
status: 200,
|
|
128
|
+
data: { result: { sys_id: "genericSysId", table: "sys_script_include", name: "Generic" } },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
afterEach(function () {
|
|
132
|
+
exitSpy.mockRestore();
|
|
133
|
+
});
|
|
134
|
+
function errorText() {
|
|
135
|
+
return logMessages
|
|
136
|
+
.filter(function (m) { return m.level === "error"; })
|
|
137
|
+
.map(function (m) { return m.msg; })
|
|
138
|
+
.join("\n");
|
|
139
|
+
}
|
|
140
|
+
it("routes through createUpdateSet with the requested scope and never calls the generic insert", async function () {
|
|
141
|
+
// Table API shape for a reference field without display values: { value, link }
|
|
142
|
+
mockClient.getUpdateSetById.mockReturnValue(axiosResult([{ sys_id: SET_SYS_ID, application: { value: REQUESTED_SCOPE_SYS_ID, link: "ref-link" } }]));
|
|
143
|
+
await (0, createRecordCommand_1.createRecordCommand)({
|
|
144
|
+
table: "sys_update_set",
|
|
145
|
+
name: "Journey - Fix 231",
|
|
146
|
+
scope: REQUESTED_SCOPE,
|
|
147
|
+
field: ["description=Issue 231 regression set"],
|
|
148
|
+
ci: true,
|
|
149
|
+
logLevel: "info",
|
|
150
|
+
});
|
|
151
|
+
expect(mockClient.createUpdateSet).toHaveBeenCalledTimes(1);
|
|
152
|
+
expect(mockClient.createUpdateSet).toHaveBeenCalledWith("Journey - Fix 231", REQUESTED_SCOPE_SYS_ID, "Issue 231 regression set");
|
|
153
|
+
expect(mockClient.createRecord).not.toHaveBeenCalled();
|
|
154
|
+
expect(mockClient.getUpdateSetById).toHaveBeenCalledWith(SET_SYS_ID);
|
|
155
|
+
expect(mockSyncManifest).not.toHaveBeenCalled();
|
|
156
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
157
|
+
var success = logMessages.filter(function (m) { return m.level === "success"; });
|
|
158
|
+
expect(success.length).toBe(1);
|
|
159
|
+
expect(success[0].msg).toContain(SET_SYS_ID);
|
|
160
|
+
expect(success[0].msg).toContain(REQUESTED_SCOPE);
|
|
161
|
+
});
|
|
162
|
+
it("fails loudly (exit 1, no success) when the created set's application is not the requested scope", async function () {
|
|
163
|
+
// The op "succeeded" but the set landed in the session's current app.
|
|
164
|
+
mockClient.createUpdateSet.mockImplementation(function (name) {
|
|
165
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name, application: SESSION_SCOPE_SYS_ID });
|
|
166
|
+
});
|
|
167
|
+
mockClient.getUpdateSetById.mockReturnValue(axiosResult([{ sys_id: SET_SYS_ID, application: { value: SESSION_SCOPE_SYS_ID } }]));
|
|
168
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
169
|
+
table: "sys_update_set",
|
|
170
|
+
name: "Journey - Fix 231",
|
|
171
|
+
scope: REQUESTED_SCOPE,
|
|
172
|
+
ci: true,
|
|
173
|
+
logLevel: "info",
|
|
174
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
175
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
176
|
+
expect(mockClient.createRecord).not.toHaveBeenCalled();
|
|
177
|
+
expect(logMessages.filter(function (m) { return m.level === "success"; })).toHaveLength(0);
|
|
178
|
+
var errors = errorText();
|
|
179
|
+
expect(errors).toContain(SET_SYS_ID);
|
|
180
|
+
expect(errors).toContain(REQUESTED_SCOPE);
|
|
181
|
+
expect(errors).toContain(SESSION_SCOPE);
|
|
182
|
+
expect(errors).toContain(REQUESTED_SCOPE_SYS_ID);
|
|
183
|
+
expect(errors).toContain(SESSION_SCOPE_SYS_ID);
|
|
184
|
+
});
|
|
185
|
+
it("falls back to the op response when read-back is unavailable, and still catches a mismatch", async function () {
|
|
186
|
+
mockClient.createUpdateSet.mockImplementation(function (name) {
|
|
187
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name, application: SESSION_SCOPE_SYS_ID });
|
|
188
|
+
});
|
|
189
|
+
mockClient.getUpdateSetById.mockReturnValue(Promise.reject(new Error("read denied")));
|
|
190
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
191
|
+
table: "sys_update_set",
|
|
192
|
+
name: "Journey - Fix 231",
|
|
193
|
+
scope: REQUESTED_SCOPE,
|
|
194
|
+
ci: true,
|
|
195
|
+
logLevel: "info",
|
|
196
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
197
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
198
|
+
expect(errorText()).toContain(SESSION_SCOPE);
|
|
199
|
+
expect(errorText()).toContain(SET_SYS_ID);
|
|
200
|
+
});
|
|
201
|
+
it("refuses to verify-by-assumption: no application from any source is a failure, not a success", async function () {
|
|
202
|
+
mockClient.createUpdateSet.mockImplementation(function (name) {
|
|
203
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name });
|
|
204
|
+
});
|
|
205
|
+
mockClient.getUpdateSetById.mockReturnValue(axiosResult([{ sys_id: SET_SYS_ID }]));
|
|
206
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
207
|
+
table: "sys_update_set",
|
|
208
|
+
name: "Journey - Fix 231",
|
|
209
|
+
scope: REQUESTED_SCOPE,
|
|
210
|
+
ci: true,
|
|
211
|
+
logLevel: "info",
|
|
212
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
213
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
214
|
+
expect(errorText()).toContain("could not be verified");
|
|
215
|
+
expect(logMessages.filter(function (m) { return m.level === "success"; })).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
it("requires --scope for sys_update_set in --ci mode before touching the instance", async function () {
|
|
218
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
219
|
+
table: "sys_update_set",
|
|
220
|
+
name: "No Scope Set",
|
|
221
|
+
ci: true,
|
|
222
|
+
logLevel: "info",
|
|
223
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
224
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
225
|
+
expect(mockClient.createUpdateSet).not.toHaveBeenCalled();
|
|
226
|
+
expect(mockClient.createRecord).not.toHaveBeenCalled();
|
|
227
|
+
expect(mockClient.getScopeId).not.toHaveBeenCalled();
|
|
228
|
+
expect(errorText()).toContain("--scope");
|
|
229
|
+
});
|
|
230
|
+
it("errors when the requested scope does not exist on the instance", async function () {
|
|
231
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
232
|
+
table: "sys_update_set",
|
|
233
|
+
name: "Ghost Scope Set",
|
|
234
|
+
scope: "x_cadso_nope",
|
|
235
|
+
ci: true,
|
|
236
|
+
logLevel: "info",
|
|
237
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
238
|
+
expect(mockClient.createUpdateSet).not.toHaveBeenCalled();
|
|
239
|
+
expect(errorText()).toContain("x_cadso_nope");
|
|
240
|
+
});
|
|
241
|
+
it("keeps every other table on the generic createRecord path", async function () {
|
|
242
|
+
await (0, createRecordCommand_1.createRecordCommand)({
|
|
243
|
+
table: "sys_script_include",
|
|
244
|
+
name: "Generic",
|
|
245
|
+
scope: REQUESTED_SCOPE,
|
|
246
|
+
ci: true,
|
|
247
|
+
refresh: false,
|
|
248
|
+
logLevel: "info",
|
|
249
|
+
});
|
|
250
|
+
expect(mockClient.createRecord).toHaveBeenCalledTimes(1);
|
|
251
|
+
expect(mockClient.createRecord).toHaveBeenCalledWith({
|
|
252
|
+
table: "sys_script_include",
|
|
253
|
+
fields: { name: "Generic" },
|
|
254
|
+
scope: REQUESTED_SCOPE,
|
|
255
|
+
update_set_sys_id: undefined,
|
|
256
|
+
});
|
|
257
|
+
expect(mockClient.createUpdateSet).not.toHaveBeenCalled();
|
|
258
|
+
expect(mockClient.getScopeId).not.toHaveBeenCalled();
|
|
259
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
it("exposes a typed mismatch error carrying both scopes", function () {
|
|
262
|
+
var err = new createRecordCommand_1.UpdateSetScopeMismatchError({
|
|
263
|
+
updateSetName: "n",
|
|
264
|
+
updateSetSysId: "s",
|
|
265
|
+
requestedScope: REQUESTED_SCOPE,
|
|
266
|
+
requestedScopeSysId: REQUESTED_SCOPE_SYS_ID,
|
|
267
|
+
actualScope: SESSION_SCOPE,
|
|
268
|
+
actualScopeSysId: SESSION_SCOPE_SYS_ID,
|
|
269
|
+
});
|
|
270
|
+
expect(err).toBeInstanceOf(Error);
|
|
271
|
+
expect(err.name).toBe("UpdateSetScopeMismatchError");
|
|
272
|
+
expect(err.requestedScope).toBe(REQUESTED_SCOPE);
|
|
273
|
+
expect(err.actualScope).toBe(SESSION_SCOPE);
|
|
274
|
+
expect(err.message).toContain("MIS-SCOPED");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -4,6 +4,39 @@
|
|
|
4
4
|
// never lists `watch` / `w` / `watchAllScopes`, and all three spellings still
|
|
5
5
|
// route to the watch handler. Mocks mirror commanderStrict.test.ts — the parser
|
|
6
6
|
// is under test, not the commands, which would hit ServiceNow.
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
7
40
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
42
|
};
|
|
@@ -55,6 +88,8 @@ jest.mock("../clickupCommands", () => ({
|
|
|
55
88
|
clickupListsCommand: jest.fn(),
|
|
56
89
|
}));
|
|
57
90
|
// --- Imports (after mocks) ---
|
|
91
|
+
const fs = __importStar(require("fs"));
|
|
92
|
+
const path = __importStar(require("path"));
|
|
58
93
|
const yargs_1 = __importDefault(require("yargs/yargs"));
|
|
59
94
|
const commander_1 = require("../commander");
|
|
60
95
|
const allScopesCommands_1 = require("../allScopesCommands");
|
|
@@ -144,3 +179,18 @@ describe("dove watch stays wired for humans", function () {
|
|
|
144
179
|
expect(args.port).toBe(4000);
|
|
145
180
|
});
|
|
146
181
|
});
|
|
182
|
+
// The command index is not the only surface Claude reads: `dove init`, `dove login`
|
|
183
|
+
// and `dove initScopes` print next-step hints too. None of them may point at the
|
|
184
|
+
// watcher (#155). Source-level so the check needs no instance and no prompt mocks.
|
|
185
|
+
describe("next-step hints never recommend the watcher", function () {
|
|
186
|
+
const HINT_SOURCES = ["../initSystem/orchestrator.ts", "../allScopesCommands.ts"];
|
|
187
|
+
const USER_FACING_WATCH_HINT = /\blogger\.(info|success|warn)\([^;]*\bdove\b[^;]*\bwatch\b/;
|
|
188
|
+
it.each(HINT_SOURCES)("%s prints only headless next steps", function (rel) {
|
|
189
|
+
const source = fs.readFileSync(path.join(__dirname, rel), "utf8");
|
|
190
|
+
expect(source.length).toBeGreaterThan(0);
|
|
191
|
+
const offenders = source.split("\n").filter(function (line) {
|
|
192
|
+
return USER_FACING_WATCH_HINT.test(line);
|
|
193
|
+
});
|
|
194
|
+
expect(offenders).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
});
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-10-
|
|
3
|
+
"generated_at": "2026-10-06T16:25:11.503Z",
|
|
4
4
|
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-core@0.0.123",
|
|
8
|
+
"package": "@tenonhq/dovetail-core",
|
|
9
|
+
"version": "0.0.123",
|
|
10
|
+
"prev_version": "0.0.122",
|
|
11
|
+
"published_at": "2026-10-06T16:25:11.496Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "0927c64611ab1901d9ce6b6cbba15cb17eee684e",
|
|
14
|
+
"tag": "dovetail-core@0.0.123"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "0927c6461",
|
|
20
|
+
"subject": "fix(core): dove create sys_update_set routes through the scope-correct createUpdateSet op (#306)",
|
|
21
|
+
"pr": 306
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"exports_delta": null,
|
|
25
|
+
"narrative": null
|
|
26
|
+
},
|
|
5
27
|
{
|
|
6
28
|
"$schema": "dovetail-release-event/v1",
|
|
7
29
|
"event_id": "dovetail-types@0.0.22",
|
|
@@ -1268,27 +1290,6 @@
|
|
|
1268
1290
|
],
|
|
1269
1291
|
"exports_delta": null,
|
|
1270
1292
|
"narrative": null
|
|
1271
|
-
},
|
|
1272
|
-
{
|
|
1273
|
-
"$schema": "dovetail-release-event/v1",
|
|
1274
|
-
"event_id": "dovetail-mcp@0.0.33",
|
|
1275
|
-
"package": "@tenonhq/dovetail-mcp",
|
|
1276
|
-
"version": "0.0.33",
|
|
1277
|
-
"prev_version": "0.0.32",
|
|
1278
|
-
"published_at": "2026-08-31T21:34:20.896Z",
|
|
1279
|
-
"git": {
|
|
1280
|
-
"sha": "81ab50a4e0fc105445ffc6d6983bc93a1483859d",
|
|
1281
|
-
"tag": "dovetail-mcp@0.0.33"
|
|
1282
|
-
},
|
|
1283
|
-
"semver_bump": "patch",
|
|
1284
|
-
"commits": [
|
|
1285
|
-
{
|
|
1286
|
-
"sha": "025e4eb4d",
|
|
1287
|
-
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
1288
|
-
}
|
|
1289
|
-
],
|
|
1290
|
-
"exports_delta": null,
|
|
1291
|
-
"narrative": null
|
|
1292
1293
|
}
|
|
1293
1294
|
]
|
|
1294
1295
|
}
|