@tenonhq/dovetail-core 0.0.124 → 0.0.125

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.
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ /**
3
+ * `dove pull <table> <sysId...>` — the per-record form of pull. See
4
+ * pullRecord.ts for the engine; this file is the CLI surface: argument
5
+ * normalization, the dry-run report, and the summary.
6
+ *
7
+ * A bare `dove pull` (no table, no --from-update-set) is still the historic
8
+ * alias of `dove refresh`; commander.ts routes that case before reaching here.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.collectTargets = collectTargets;
48
+ exports.pullRecordCommand = pullRecordCommand;
49
+ const path_1 = __importDefault(require("path"));
50
+ const chalk_1 = __importDefault(require("chalk"));
51
+ const Logger_1 = require("./Logger");
52
+ const FileLogger_1 = require("./FileLogger");
53
+ const commands_1 = require("./commands");
54
+ const ConfigManager = __importStar(require("./config"));
55
+ const pullRecord_1 = require("./pullRecord");
56
+ function relToRoot(p) {
57
+ try {
58
+ return path_1.default.relative(ConfigManager.getRootDir(), p) || ".";
59
+ }
60
+ catch (e) {
61
+ return p;
62
+ }
63
+ }
64
+ function describeRecord(rec, dryRun) {
65
+ const lines = [];
66
+ const verb = dryRun ? "would write" : "wrote";
67
+ lines.push(chalk_1.default.bold(rec.table) + " " + rec.sysId + " → " + chalk_1.default.cyan(relToRoot(rec.recordDir) + path_1.default.sep) +
68
+ chalk_1.default.dim(" [" + rec.scope + "]"));
69
+ for (const f of rec.files) {
70
+ const mark = f.action === "write" ? chalk_1.default.green("+") : chalk_1.default.dim("=");
71
+ const note = f.action === "write" ? verb : "unchanged";
72
+ lines.push(" " + mark + " " + path_1.default.basename(f.path) + chalk_1.default.dim(" (" + note + ")"));
73
+ }
74
+ const manifestNote = rec.manifestAction === "add" ? "add" : rec.manifestAction === "update" ? "update" : "unchanged";
75
+ lines.push(" " + (rec.manifestAction === "unchanged" ? chalk_1.default.dim("=") : chalk_1.default.green("~")) +
76
+ " manifest key " + JSON.stringify(rec.key) + chalk_1.default.dim(" (" + manifestNote + ")"));
77
+ for (const w of rec.warnings)
78
+ lines.push(" " + chalk_1.default.yellow("!") + " " + w);
79
+ return lines;
80
+ }
81
+ function report(plan, dryRun) {
82
+ for (const rec of plan.records) {
83
+ for (const line of describeRecord(rec, dryRun))
84
+ Logger_1.logger.info(line);
85
+ }
86
+ for (const m of plan.manifests) {
87
+ const touched = m.records.filter((r) => r.manifestAction !== "unchanged").length;
88
+ if (touched === 0)
89
+ continue;
90
+ Logger_1.logger.info((dryRun ? "would splice " : "spliced ") + touched + " entr" + (touched === 1 ? "y" : "ies") +
91
+ " into " + chalk_1.default.cyan(relToRoot(m.manifestPath)) + (m.existed ? "" : chalk_1.default.dim(" (new file)")));
92
+ }
93
+ for (const w of plan.warnings)
94
+ Logger_1.logger.warn(w);
95
+ }
96
+ /**
97
+ * Turn the CLI arguments into pull targets. Positional sys_ids and `--sys-ids`
98
+ * compose; `--from-update-set` expands to the set's per-record captures (and
99
+ * composes with an explicit table + sys_ids too).
100
+ */
101
+ async function collectTargets(args) {
102
+ const targets = [];
103
+ let skipped = [];
104
+ const ids = (0, pullRecord_1.normalizeSysIds)([]
105
+ .concat(Array.isArray(args.sysId) ? args.sysId : [args.sysId])
106
+ .concat(Array.isArray(args.sysIds) ? args.sysIds : [args.sysIds])
107
+ .concat(Array.isArray(args["sys-ids"]) ? args["sys-ids"] : [args["sys-ids"]]));
108
+ if (args.table) {
109
+ const table = (0, pullRecord_1.assertTableName)(args.table);
110
+ if (ids.length === 0 && !args.fromUpdateSet) {
111
+ throw new Error("dove pull " + table + ": give at least one sys_id (positional, or --sys-ids a,b,c), " +
112
+ "or use --from-update-set <sys_id>. A bare 'dove pull' is the scope-wide refresh.");
113
+ }
114
+ for (const sysId of ids)
115
+ targets.push({ table, sysId });
116
+ }
117
+ else if (ids.length > 0) {
118
+ throw new Error("dove pull: a table name is required before the sys_id(s).");
119
+ }
120
+ if (args.fromUpdateSet) {
121
+ const fromSet = await (0, pullRecord_1.targetsFromUpdateSet)(String(args.fromUpdateSet));
122
+ for (const t of fromSet.targets)
123
+ targets.push(t);
124
+ skipped = fromSet.skipped;
125
+ }
126
+ return { targets, skipped };
127
+ }
128
+ async function pullRecordCommand(args) {
129
+ (0, commands_1.setLogLevel)(args);
130
+ try {
131
+ if (args.t !== undefined && !args.table) {
132
+ throw new Error("dove pull has no -t/--table flag. For a table-narrowed scope refresh use " +
133
+ "'dove refresh -t <table>'; for one record use 'dove pull <table> <sys_id>'.");
134
+ }
135
+ const dryRun = !!args.dryRun;
136
+ const { targets, skipped } = await collectTargets(args);
137
+ for (const s of skipped)
138
+ Logger_1.logger.warn("skipped " + s);
139
+ if (targets.length === 0) {
140
+ throw new Error("Nothing to pull: no per-record targets resolved.");
141
+ }
142
+ FileLogger_1.fileLogger.debug("pull: " + targets.length + " target(s), scope=" + (args.scope || "auto") +
143
+ ", force=" + !!args.force + ", dryRun=" + dryRun);
144
+ const plan = await (0, pullRecord_1.planPull)(targets, { scope: args.scope, force: !!args.force });
145
+ if (dryRun) {
146
+ Logger_1.logger.info(chalk_1.default.bold("Dry run — nothing written."));
147
+ report(plan, true);
148
+ return;
149
+ }
150
+ const result = await (0, pullRecord_1.applyPull)(plan);
151
+ report(plan, false);
152
+ Logger_1.logger.success("Pulled " + plan.records.length + " record(s): " + result.filesWritten + " file(s) written, " +
153
+ result.filesUnchanged + " unchanged, " + result.manifestsWritten.length + " manifest(s) updated.");
154
+ }
155
+ catch (e) {
156
+ const message = e instanceof Error ? e.message : String(e);
157
+ Logger_1.logger.error(message);
158
+ FileLogger_1.fileLogger.error("pull failed: " + message);
159
+ process.exitCode = 1;
160
+ }
161
+ }
package/dist/snClient.js CHANGED
@@ -366,6 +366,37 @@ const snClient = (baseURL, username, password, apiKey) => {
366
366
  },
367
367
  });
368
368
  };
369
+ /**
370
+ * Reads ONE record by sys_id, selecting only its class and owning scope, so a
371
+ * per-record pull can resolve (and refuse) the target scope before it fetches
372
+ * any content. Plain Table API read — no Dovetail server op involved.
373
+ */
374
+ const getRecordScope = (table, sysId) => {
375
+ const endpoint = "api/now/table/" + encodeURIComponent(table) + "/" + encodeURIComponent(sysId);
376
+ return client.get(endpoint, {
377
+ params: {
378
+ sysparm_fields: "sys_id,sys_class_name,sys_scope,sys_scope.scope",
379
+ sysparm_display_value: "false",
380
+ },
381
+ });
382
+ };
383
+ /**
384
+ * One page of a update set's customer updates (`sys_update_xml`), ordered by
385
+ * name so paging is stable. The caller pages with offset/limit; the instance
386
+ * caps a single page at 1000 rows.
387
+ */
388
+ const getUpdateSetMembers = (updateSetSysId, offset, limit) => {
389
+ const endpoint = "api/now/table/sys_update_xml";
390
+ return client.get(endpoint, {
391
+ params: {
392
+ sysparm_query: "update_set=" + updateSetSysId + "^ORDERBYname",
393
+ sysparm_fields: "sys_id,name,action,target_name,type",
394
+ sysparm_display_value: "false",
395
+ sysparm_limit: limit,
396
+ sysparm_offset: offset,
397
+ },
398
+ });
399
+ };
369
400
  const getUserSysId = (userName = process.env.SN_USER) => {
370
401
  const endpoint = "api/now/table/sys_user";
371
402
  return client.get(endpoint, {
@@ -538,6 +569,8 @@ const snClient = (baseURL, username, password, apiKey) => {
538
569
  getScopeId,
539
570
  getScopeById,
540
571
  getUpdateSetById,
572
+ getRecordScope,
573
+ getUpdateSetMembers,
541
574
  getUserSysId,
542
575
  getCurrentAppUserPrefSysId,
543
576
  updateCurrentAppUserPref,
@@ -39,6 +39,7 @@ jest.mock("../schemaCommand", () => ({
39
39
  jest.mock("../claudeCommand", () => ({ initClaudeCommand: jest.fn() }));
40
40
  jest.mock("../createRecordCommand", () => ({ createRecordCommand: jest.fn() }));
41
41
  jest.mock("../deleteRecordCommand", () => ({ deleteRecordCommand: jest.fn() }));
42
+ jest.mock("../pullRecordCommand", () => ({ pullRecordCommand: jest.fn() }));
42
43
  jest.mock("../reconcileCommand", () => ({ reconcileCommand: jest.fn() }));
43
44
  jest.mock("../migrateCommand", () => ({ migrateCommand: jest.fn() }));
44
45
  jest.mock("../loginCommand", () => ({ loginCommand: jest.fn() }));
@@ -58,6 +59,7 @@ jest.mock("../clickupCommands", () => ({
58
59
  const yargs_1 = __importDefault(require("yargs/yargs"));
59
60
  const commander_1 = require("../commander");
60
61
  const commands_1 = require("../commands");
62
+ const pullRecordCommand_1 = require("../pullRecordCommand");
61
63
  /**
62
64
  * @description Runs a fake `dove` invocation through a fresh, non-exiting parser.
63
65
  * @param {string[]} args - Argv as the user would type it, minus the binary name.
@@ -101,6 +103,45 @@ describe("dove pull", function () {
101
103
  expect(argv.scope).toBe("x_cadso_core");
102
104
  expect(argv.force).toBe(true);
103
105
  expect(commands_1.refreshCommand).toHaveBeenCalledTimes(1);
106
+ expect(pullRecordCommand_1.pullRecordCommand).not.toHaveBeenCalled();
107
+ });
108
+ it("re-maps a bare `pull -t <table>` to refresh --table (the old spelling keeps working)", async function () {
109
+ const { error } = await run(["pull", "-t", "sys_script_include", "--metadata-only"]);
110
+ expect(error).toBeUndefined();
111
+ expect(commands_1.refreshCommand).toHaveBeenCalledTimes(1);
112
+ const passed = commands_1.refreshCommand.mock.calls[0][0];
113
+ expect(passed.table).toBe("sys_script_include");
114
+ expect(passed.metadataOnly).toBe(true);
115
+ expect(pullRecordCommand_1.pullRecordCommand).not.toHaveBeenCalled();
116
+ });
117
+ it("routes `pull <table> <sysId...>` to the per-record pull, not refresh", async function () {
118
+ const { error, argv } = await run([
119
+ "pull", "sys_security_acl", "94243ee5c3f78f10d4ddf1db050131a6", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1",
120
+ "--scope", "x_cadso_journey", "--dry-run",
121
+ ]);
122
+ expect(error).toBeUndefined();
123
+ expect(argv.table).toBe("sys_security_acl");
124
+ expect(argv.sysId).toEqual(["94243ee5c3f78f10d4ddf1db050131a6", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"]);
125
+ expect(argv.dryRun).toBe(true);
126
+ expect(pullRecordCommand_1.pullRecordCommand).toHaveBeenCalledTimes(1);
127
+ expect(commands_1.refreshCommand).not.toHaveBeenCalled();
128
+ });
129
+ it("keeps --sys-ids distinct from the positional list", async function () {
130
+ const { error, argv } = await run([
131
+ "pull", "sys_security_acl", "94243ee5c3f78f10d4ddf1db050131a6",
132
+ "--sys-ids", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1,bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2",
133
+ ]);
134
+ expect(error).toBeUndefined();
135
+ expect(argv.sysId).toEqual(["94243ee5c3f78f10d4ddf1db050131a6"]);
136
+ expect(argv.sysIds).toBe("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1,bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2");
137
+ expect(pullRecordCommand_1.pullRecordCommand).toHaveBeenCalledTimes(1);
138
+ });
139
+ it("routes `pull --from-update-set` (no table) to the per-record pull", async function () {
140
+ const { error, argv } = await run(["pull", "--from-update-set", "dddddddddddddddddddddddddddddddd"]);
141
+ expect(error).toBeUndefined();
142
+ expect(argv.fromUpdateSet).toBe("dddddddddddddddddddddddddddddddd");
143
+ expect(pullRecordCommand_1.pullRecordCommand).toHaveBeenCalledTimes(1);
144
+ expect(commands_1.refreshCommand).not.toHaveBeenCalled();
104
145
  });
105
146
  });
106
147
  describe("unknown commands", function () {