@tenonhq/dovetail-core 0.0.121 → 0.0.123
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 +9 -1
- package/dist/allScopesCommands.js +8 -0
- package/dist/claudeSession.js +58 -0
- package/dist/commander.js +13 -3
- package/dist/createRecordCommand.js +232 -8
- package/dist/snClient.js +15 -0
- package/dist/tests/claudeSession.test.js +42 -0
- package/dist/tests/createRecordUpdateSet.test.js +276 -0
- package/dist/tests/watchHidden.test.js +146 -0
- package/package.json +1 -1
- package/release-manifest.json +246 -292
- package/skills/dove-manage-update-sets.md +4 -4
- package/skills/dove-setup-project.md +12 -6
- package/skills/dove-troubleshoot-sync.md +3 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The full command surface is documented in the [root CLAUDE.md](../../CLAUDE.md#e
|
|
|
24
24
|
Quick reference:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
npx dove watch # multi-scope watch + dashboard
|
|
27
|
+
npx dove watch # HUMAN-ONLY local dev: multi-scope watch + dashboard (hidden from --help; stop before git checkout — see #155)
|
|
28
28
|
npx dove push # build + push current files
|
|
29
29
|
npx dove refresh # pull manifest + new files (aliases: pull, r)
|
|
30
30
|
npx dove build # local build only
|
|
@@ -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`).
|
|
@@ -47,6 +47,7 @@ const AppUtils = __importStar(require("./appUtils"));
|
|
|
47
47
|
const wizard_1 = require("./wizard");
|
|
48
48
|
const snClient_1 = require("./snClient");
|
|
49
49
|
const commands_1 = require("./commands");
|
|
50
|
+
const claudeSession_1 = require("./claudeSession");
|
|
50
51
|
const path = __importStar(require("path"));
|
|
51
52
|
const fs = __importStar(require("fs"));
|
|
52
53
|
const child_process_1 = require("child_process");
|
|
@@ -371,6 +372,13 @@ async function initScopesCommand(args) {
|
|
|
371
372
|
}
|
|
372
373
|
async function watchAllScopesCommand(args) {
|
|
373
374
|
(0, commands_1.setLogLevel)(args);
|
|
375
|
+
// Soft guard, not a hard fail: `watch` is a human-only local-dev tool and a
|
|
376
|
+
// branch switch mid-watch overwrites instance records. Warn an agent off it
|
|
377
|
+
// but leave the human path untouched. See TenonHQ/Dovetail#155.
|
|
378
|
+
if ((0, claudeSession_1.isClaudeCodeSession)()) {
|
|
379
|
+
Logger_1.logger.warn(claudeSession_1.WATCH_HUMAN_ONLY_WARNING);
|
|
380
|
+
FileLogger_1.fileLogger.warn("dove watch started inside a Claude Code session (TenonHQ/Dovetail#155)");
|
|
381
|
+
}
|
|
374
382
|
var dashboardProcess = null;
|
|
375
383
|
try {
|
|
376
384
|
// First check if we have environment variables set. SN_USER stays
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code session detection for human-only CLI surfaces.
|
|
4
|
+
*
|
|
5
|
+
* `dove watch` auto-syncs the working tree to the instance, and a git branch
|
|
6
|
+
* switch mid-watch pushes the post-switch file state over live records. An
|
|
7
|
+
* agent can't guarantee it stops the watcher before every branch operation, so
|
|
8
|
+
* the command is hidden from `--help` and warns when it detects it is running
|
|
9
|
+
* inside a Claude Code session. See TenonHQ/Dovetail#155.
|
|
10
|
+
*
|
|
11
|
+
* Pure functions — no I/O — so the detection is unit-testable without touching
|
|
12
|
+
* `process.env` directly.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.WATCH_HUMAN_ONLY_WARNING = void 0;
|
|
16
|
+
exports.isClaudeCodeSession = isClaudeCodeSession;
|
|
17
|
+
// Claude Code exports `CLAUDECODE=1` into every tool shell, plus a family of
|
|
18
|
+
// `CLAUDE_CODE_*` variables (e.g. `CLAUDE_CODE_SESSION_ID`, which claude-plans
|
|
19
|
+
// already keys on). Any one of them set to a non-empty value is a session.
|
|
20
|
+
var CLAUDE_SESSION_FLAG = "CLAUDECODE";
|
|
21
|
+
var CLAUDE_SESSION_PREFIX = "CLAUDE_CODE_";
|
|
22
|
+
/**
|
|
23
|
+
* @description Human-only warning printed when `dove watch` starts inside a
|
|
24
|
+
* Claude Code session. Non-blocking by design (TenonHQ/Dovetail#155).
|
|
25
|
+
*/
|
|
26
|
+
exports.WATCH_HUMAN_ONLY_WARNING = "⚠ `dove watch` is a human-only local-dev tool — it auto-syncs to the " +
|
|
27
|
+
"instance and a branch switch mid-watch overwrites records. Don't run this " +
|
|
28
|
+
"inside Claude Code. Use `npx dove push --diff <branch>` / `npx dove refresh` / " +
|
|
29
|
+
"`npx dove status` instead. See TenonHQ/Dovetail#155.";
|
|
30
|
+
/**
|
|
31
|
+
* @description Reports whether the given environment looks like a Claude Code
|
|
32
|
+
* session: `CLAUDECODE` is set, or any `CLAUDE_CODE_*` variable is set, to a
|
|
33
|
+
* non-empty value. Empty strings do not count — an unset-but-declared variable
|
|
34
|
+
* is not a session.
|
|
35
|
+
* @param {NodeJS.ProcessEnv} env - Environment to inspect (defaults to `process.env`).
|
|
36
|
+
* @returns {boolean} True when a Claude Code session is detected.
|
|
37
|
+
*/
|
|
38
|
+
function isClaudeCodeSession(env = process.env) {
|
|
39
|
+
if (!env || typeof env !== "object")
|
|
40
|
+
return false;
|
|
41
|
+
if (hasValue(env[CLAUDE_SESSION_FLAG]))
|
|
42
|
+
return true;
|
|
43
|
+
var keys = Object.keys(env);
|
|
44
|
+
for (var i = 0; i < keys.length; i++) {
|
|
45
|
+
var key = keys[i];
|
|
46
|
+
if (key.indexOf(CLAUDE_SESSION_PREFIX) === 0 && hasValue(env[key]))
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @description True when an env value is a non-empty string.
|
|
53
|
+
* @param {string|undefined} value - Raw env value.
|
|
54
|
+
* @returns {boolean} Whether the value counts as "set".
|
|
55
|
+
*/
|
|
56
|
+
function hasValue(value) {
|
|
57
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
58
|
+
}
|
package/dist/commander.js
CHANGED
|
@@ -49,7 +49,17 @@ function configureCli(cli) {
|
|
|
49
49
|
describe: "Write a dovetail-debug-*.log file for this command (off by default; also enabled by DOVETAIL_DEBUG=1).",
|
|
50
50
|
})
|
|
51
51
|
.global("debug")
|
|
52
|
-
|
|
52
|
+
// INTENTIONAL: 'watch' is hidden from help + Claude-facing docs. It
|
|
53
|
+
// auto-syncs to the instance and a branch switch mid-watch silently
|
|
54
|
+
// overwrites records. Human-only local-dev tool. Do not re-document for
|
|
55
|
+
// Claude. See TenonHQ/Dovetail#155.
|
|
56
|
+
//
|
|
57
|
+
// A `false` description is yargs' hidden-command pattern: the command stays
|
|
58
|
+
// fully wired (a human typing `dove watch` / `w` / `watchAllScopes` still
|
|
59
|
+
// gets it, and `dove watch --help` still prints its flags) but it is omitted
|
|
60
|
+
// from the `dove --help` command index. The handler itself prints a
|
|
61
|
+
// human-only warning when it detects a Claude Code session (claudeSession.ts).
|
|
62
|
+
.command(["watch", "w", "watchAllScopes"], false, (cmdArgs) => {
|
|
53
63
|
cmdArgs.options({
|
|
54
64
|
...sharedOptions,
|
|
55
65
|
noDashboard: {
|
|
@@ -204,7 +214,7 @@ function configureCli(cli) {
|
|
|
204
214
|
await (0, commands_1.buildCommand)(args);
|
|
205
215
|
})
|
|
206
216
|
.command("deploy", "Deploy local build files to the scoped application", sharedOptions, commands_1.deployCommand)
|
|
207
|
-
.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) => {
|
|
208
218
|
cmdArgs.positional("table", {
|
|
209
219
|
describe: "ServiceNow table name (e.g., sys_script_include)",
|
|
210
220
|
type: "string",
|
|
@@ -219,7 +229,7 @@ function configureCli(cli) {
|
|
|
219
229
|
scope: {
|
|
220
230
|
alias: "s",
|
|
221
231
|
type: "string",
|
|
222
|
-
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",
|
|
223
233
|
},
|
|
224
234
|
from: {
|
|
225
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,
|
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,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for the Claude Code session detector behind the `dove watch` soft
|
|
3
|
+
// guard (claudeSession.ts, TenonHQ/Dovetail#155). Pure-function tests — an
|
|
4
|
+
// explicit env object is passed so the real process.env never leaks in.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const claudeSession_1 = require("../claudeSession");
|
|
7
|
+
describe("isClaudeCodeSession", function () {
|
|
8
|
+
it("returns false for an empty environment", function () {
|
|
9
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({})).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
it("returns false when only unrelated variables are set", function () {
|
|
12
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({ HOME: "/tmp", SN_INSTANCE: "x.service-now.com" })).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
it("returns true when CLAUDECODE is set", function () {
|
|
15
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({ CLAUDECODE: "1" })).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it("returns true when any CLAUDE_CODE_* variable is set", function () {
|
|
18
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({ CLAUDE_CODE_SESSION_ID: "abc123" })).toBe(true);
|
|
19
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({ CLAUDE_CODE_ENTRYPOINT: "cli" })).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it("treats empty or whitespace-only values as unset", function () {
|
|
22
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({ CLAUDECODE: "" })).toBe(false);
|
|
23
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({ CLAUDE_CODE_SESSION_ID: " " })).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it("does not match look-alike prefixes", function () {
|
|
26
|
+
expect((0, claudeSession_1.isClaudeCodeSession)({ CLAUDE: "1", CLAUDECODEX: "1", MY_CLAUDE_CODE_X: "1" })).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it("defaults to process.env and agrees with an explicit copy of it", function () {
|
|
29
|
+
expect((0, claudeSession_1.isClaudeCodeSession)()).toBe((0, claudeSession_1.isClaudeCodeSession)({ ...process.env }));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("WATCH_HUMAN_ONLY_WARNING", function () {
|
|
33
|
+
it("names the hazard, the safe headless verbs, and the tracking issue", function () {
|
|
34
|
+
expect(claudeSession_1.WATCH_HUMAN_ONLY_WARNING).toContain("human-only");
|
|
35
|
+
expect(claudeSession_1.WATCH_HUMAN_ONLY_WARNING).toContain("branch switch");
|
|
36
|
+
expect(claudeSession_1.WATCH_HUMAN_ONLY_WARNING).toContain("Claude Code");
|
|
37
|
+
expect(claudeSession_1.WATCH_HUMAN_ONLY_WARNING).toContain("dove push");
|
|
38
|
+
expect(claudeSession_1.WATCH_HUMAN_ONLY_WARNING).toContain("dove refresh");
|
|
39
|
+
expect(claudeSession_1.WATCH_HUMAN_ONLY_WARNING).toContain("dove status");
|
|
40
|
+
expect(claudeSession_1.WATCH_HUMAN_ONLY_WARNING).toContain("TenonHQ/Dovetail#155");
|
|
41
|
+
});
|
|
42
|
+
});
|