@tenonhq/dovetail-core 0.0.104 → 0.0.106
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/commander.js +34 -0
- package/dist/reconcile/applyPlan.js +60 -0
- package/dist/reconcile/baseline.js +126 -0
- package/dist/reconcile/fields.js +28 -0
- package/dist/reconcile/gitignore.js +68 -0
- package/dist/reconcile/multiPass.js +55 -0
- package/dist/reconcile/recordDiff.js +103 -0
- package/dist/reconcile/recordSource.js +231 -0
- package/dist/reconcile/report.js +193 -0
- package/dist/reconcile/types.js +6 -0
- package/dist/reconcileCommand.js +571 -0
- package/dist/tests/reconcileApplyPlan.test.js +98 -0
- package/dist/tests/reconcileBaseline.test.js +103 -0
- package/dist/tests/reconcileFields.test.js +24 -0
- package/dist/tests/reconcileGitignore.test.js +51 -0
- package/dist/tests/reconcileMultiPass.test.js +74 -0
- package/dist/tests/reconcileRecordDiff.test.js +125 -0
- package/dist/tests/reconcileReport.test.js +136 -0
- package/package.json +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// I/O adapters that materialize a scope's records into the normalized
|
|
3
|
+
// ReconcileRecord shape, one builder per side:
|
|
4
|
+
// - loadBranchRecords reads the on-disk per-scope manifest + each record's
|
|
5
|
+
// field files (the checked-out branch IS the working tree).
|
|
6
|
+
// - loadLiveRecords reads the instance's current record set via the same
|
|
7
|
+
// getManifest + bulk-download path `dove refresh` uses, but keeps content
|
|
8
|
+
// in memory instead of writing it to disk.
|
|
9
|
+
// Both strip the metaData bookkeeping file from the comparable field set (see
|
|
10
|
+
// fields.ts), so the pure diff engine sees genuine field content only.
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.loadBranchRecords = loadBranchRecords;
|
|
49
|
+
exports.loadLiveRecords = loadLiveRecords;
|
|
50
|
+
const path_1 = __importDefault(require("path"));
|
|
51
|
+
const fs_1 = require("fs");
|
|
52
|
+
const ConfigManager = __importStar(require("./../config"));
|
|
53
|
+
const snClient_1 = require("./../snClient");
|
|
54
|
+
const appUtils_1 = require("./../appUtils");
|
|
55
|
+
const FileLogger_1 = require("./../FileLogger");
|
|
56
|
+
const fields_1 = require("./fields");
|
|
57
|
+
// Mirror the refresh chunk size so a large scope stays under ServiceNow's ~10 MB
|
|
58
|
+
// REST payload cap.
|
|
59
|
+
const BULK_DOWNLOAD_TABLE_CHUNK_SIZE = 5;
|
|
60
|
+
// Pull `sys_updated_on` out of a metaData.json blob. The instance side carries
|
|
61
|
+
// `sys_updated_on: { value }`; the on-disk side carries the stamped
|
|
62
|
+
// `_lastUpdatedOn`. Either resolves the baseline timestamp; missing -> "".
|
|
63
|
+
function extractUpdatedOn(metaContent) {
|
|
64
|
+
if (!metaContent) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(metaContent);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
const meta = parsed;
|
|
78
|
+
const updated = meta.sys_updated_on;
|
|
79
|
+
if (typeof updated === "object" &&
|
|
80
|
+
updated !== null &&
|
|
81
|
+
typeof updated.value === "string") {
|
|
82
|
+
return updated.value;
|
|
83
|
+
}
|
|
84
|
+
if (typeof meta._lastUpdatedOn === "string") {
|
|
85
|
+
return meta._lastUpdatedOn;
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
function applyWhitelist(manifest, scope) {
|
|
90
|
+
let allowed = [];
|
|
91
|
+
try {
|
|
92
|
+
allowed = ConfigManager.resolveConfigForScope(scope).tables || [];
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
allowed = [];
|
|
96
|
+
}
|
|
97
|
+
if (allowed.length === 0) {
|
|
98
|
+
return manifest;
|
|
99
|
+
}
|
|
100
|
+
const filtered = {};
|
|
101
|
+
for (const table of Object.keys(manifest.tables || {})) {
|
|
102
|
+
if (allowed.indexOf(table) !== -1) {
|
|
103
|
+
filtered[table] = manifest.tables[table];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { tables: filtered, scope: manifest.scope };
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Branch side (on-disk)
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
async function loadBranchRecords(scope) {
|
|
112
|
+
const manifest = await ConfigManager.loadScopeManifest(scope);
|
|
113
|
+
if (!manifest || !manifest.tables) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const whitelisted = applyWhitelist(manifest, scope);
|
|
117
|
+
const sourcePath = ConfigManager.getSourcePathForScope(scope);
|
|
118
|
+
const records = [];
|
|
119
|
+
for (const table of Object.keys(whitelisted.tables)) {
|
|
120
|
+
const tableRecords = whitelisted.tables[table].records || {};
|
|
121
|
+
for (const key of Object.keys(tableRecords)) {
|
|
122
|
+
const meta = tableRecords[key];
|
|
123
|
+
const folder = (0, appUtils_1.toSafeFolderName)(meta);
|
|
124
|
+
const recordDir = path_1.default.join(sourcePath, table, folder);
|
|
125
|
+
const fields = {};
|
|
126
|
+
let metaContent;
|
|
127
|
+
for (const file of meta.files || []) {
|
|
128
|
+
const filePath = path_1.default.join(recordDir, (0, fields_1.fieldKey)(file));
|
|
129
|
+
let content;
|
|
130
|
+
try {
|
|
131
|
+
content = await fs_1.promises.readFile(filePath, "utf8");
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
// A manifest file with no on-disk counterpart (partial checkout): skip
|
|
135
|
+
// it rather than fail the whole scope. It simply won't participate in
|
|
136
|
+
// the diff for this record.
|
|
137
|
+
FileLogger_1.fileLogger.debug("reconcile: missing on-disk field " + filePath);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (file.name === "metaData" && file.type === "json") {
|
|
141
|
+
metaContent = content;
|
|
142
|
+
}
|
|
143
|
+
if ((0, fields_1.isComparableField)(file)) {
|
|
144
|
+
fields[(0, fields_1.fieldKey)(file)] = content;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
records.push({
|
|
148
|
+
table,
|
|
149
|
+
scope,
|
|
150
|
+
sys_id: meta.sys_id,
|
|
151
|
+
name: meta.name || folder,
|
|
152
|
+
updatedOn: extractUpdatedOn(metaContent),
|
|
153
|
+
fields,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return records;
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Live side (instance)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
function buildAllFilesMap(manifest) {
|
|
163
|
+
const result = {};
|
|
164
|
+
const tables = manifest.tables || {};
|
|
165
|
+
for (const table of Object.keys(tables)) {
|
|
166
|
+
const records = tables[table].records || {};
|
|
167
|
+
const recMap = {};
|
|
168
|
+
for (const key of Object.keys(records)) {
|
|
169
|
+
const rec = records[key];
|
|
170
|
+
if (!rec.files || rec.files.length === 0) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
recMap[rec.sys_id] = rec.files.map((f) => ({ name: f.name, type: f.type }));
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(recMap).length > 0) {
|
|
176
|
+
result[table] = recMap;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
async function loadLiveRecords(scope) {
|
|
182
|
+
const client = (0, snClient_1.defaultClient)();
|
|
183
|
+
const config = ConfigManager.getConfig();
|
|
184
|
+
const liveManifest = applyWhitelist((0, appUtils_1.normalizeManifestKeys)(await (0, snClient_1.unwrapSNResponse)(client.getManifest(scope, config))), scope);
|
|
185
|
+
const allFiles = buildAllFilesMap(liveManifest);
|
|
186
|
+
const tableNames = Object.keys(allFiles);
|
|
187
|
+
if (tableNames.length === 0) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
// tableOptions feeds field decoding (e.g. JSON fields). Destructure exactly as
|
|
191
|
+
// the refresh path does so the typed config shape is preserved.
|
|
192
|
+
const { tableOptions = {} } = config;
|
|
193
|
+
const downloaded = {};
|
|
194
|
+
for (let i = 0; i < tableNames.length; i += BULK_DOWNLOAD_TABLE_CHUNK_SIZE) {
|
|
195
|
+
const chunkTables = tableNames.slice(i, i + BULK_DOWNLOAD_TABLE_CHUNK_SIZE);
|
|
196
|
+
const chunkMissing = {};
|
|
197
|
+
for (const t of chunkTables) {
|
|
198
|
+
chunkMissing[t] = allFiles[t];
|
|
199
|
+
}
|
|
200
|
+
const chunkResult = (await (0, snClient_1.unwrapSNResponse)(client.getMissingFiles(chunkMissing, tableOptions)));
|
|
201
|
+
for (const table of Object.keys(chunkResult)) {
|
|
202
|
+
downloaded[table] = chunkResult[table];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const records = [];
|
|
206
|
+
for (const table of Object.keys(downloaded)) {
|
|
207
|
+
const tableRecords = downloaded[table].records || {};
|
|
208
|
+
for (const key of Object.keys(tableRecords)) {
|
|
209
|
+
const rec = tableRecords[key];
|
|
210
|
+
const fields = {};
|
|
211
|
+
let metaContent;
|
|
212
|
+
for (const file of rec.files || []) {
|
|
213
|
+
if (file.name === "metaData" && file.type === "json") {
|
|
214
|
+
metaContent = file.content;
|
|
215
|
+
}
|
|
216
|
+
if ((0, fields_1.isComparableField)(file)) {
|
|
217
|
+
fields[(0, fields_1.fieldKey)(file)] = file.content || "";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
records.push({
|
|
221
|
+
table,
|
|
222
|
+
scope,
|
|
223
|
+
sys_id: rec.sys_id,
|
|
224
|
+
name: rec.name || (0, appUtils_1.toSafeFolderName)(rec),
|
|
225
|
+
updatedOn: extractUpdatedOn(metaContent),
|
|
226
|
+
fields,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return records;
|
|
231
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure renderer for the `dove reconcile` dry-run report. Takes the classified
|
|
3
|
+
// diff for every scope plus the optional schema arm and produces a grouped,
|
|
4
|
+
// deep-linked, plain-text report. No I/O, no color codes — so it is exactly
|
|
5
|
+
// assertable in tests and the command layer can print it verbatim.
|
|
6
|
+
//
|
|
7
|
+
// What the report communicates, in priority order:
|
|
8
|
+
// 1. DRIFT (refuse-if-dirty) — the dev's own instance edits since baseline.
|
|
9
|
+
// Phase 2 blocks apply on these; Phase 1 surfaces them up top.
|
|
10
|
+
// 2. RECORD DELTAS — create / update / delete the apply phases act on.
|
|
11
|
+
// 3. SCHEMA DELTAS — report-only (a ServiceNow ceiling), with the
|
|
12
|
+
// dictionary deep-links a human needs to apply them by hand.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.formatReconcileReport = formatReconcileReport;
|
|
15
|
+
function recordLink(host, table, sysId) {
|
|
16
|
+
if (!host) {
|
|
17
|
+
return table + ".do?sys_id=" + sysId;
|
|
18
|
+
}
|
|
19
|
+
return ("https://" + host + ".service-now.com/" + table + ".do?sys_id=" + sysId);
|
|
20
|
+
}
|
|
21
|
+
function dictionaryLink(host, table, element) {
|
|
22
|
+
const query = element
|
|
23
|
+
? "name=" + table + "^element=" + element
|
|
24
|
+
: "name=" + table;
|
|
25
|
+
const base = host ? "https://" + host + ".service-now.com/" : "";
|
|
26
|
+
return base + "sys_dictionary_list.do?sysparm_query=" + query;
|
|
27
|
+
}
|
|
28
|
+
function recordRow(host, glyph, change) {
|
|
29
|
+
const label = change.table + " > " + change.name;
|
|
30
|
+
return " " + glyph + " " + label + " " + recordLink(host, change.table, change.sys_id);
|
|
31
|
+
}
|
|
32
|
+
function fieldSubRows(change) {
|
|
33
|
+
const rows = [];
|
|
34
|
+
for (const delta of change.fieldDeltas) {
|
|
35
|
+
let note;
|
|
36
|
+
if (delta.changed) {
|
|
37
|
+
note = "changed";
|
|
38
|
+
}
|
|
39
|
+
else if (delta.onBranch && !delta.onLive) {
|
|
40
|
+
note = "missing on instance";
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
note = "extra on instance";
|
|
44
|
+
}
|
|
45
|
+
rows.push(" - " + delta.field + " (" + note + ")");
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
function partitionDeletes(deletes) {
|
|
50
|
+
const applyable = [];
|
|
51
|
+
const drift = [];
|
|
52
|
+
for (const change of deletes) {
|
|
53
|
+
if (change.deleteDisposition === "tracked") {
|
|
54
|
+
applyable.push(change);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
drift.push(change);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { applyable, drift };
|
|
61
|
+
}
|
|
62
|
+
function renderScope(host, scopeResult, lines) {
|
|
63
|
+
const { scope, diff, dirty } = scopeResult;
|
|
64
|
+
const deletes = partitionDeletes(diff.deletes);
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("Scope: " + scope);
|
|
67
|
+
if (!scopeResult.hasBaseline) {
|
|
68
|
+
lines.push(" ! no baseline for this instance — deletes are shown but cannot be " +
|
|
69
|
+
"applied (run a reconcile apply to establish one).");
|
|
70
|
+
}
|
|
71
|
+
// 1. Drift first — it is what blocks an apply.
|
|
72
|
+
if (dirty.length > 0) {
|
|
73
|
+
lines.push(" DRIFT — instance changed since baseline (apply would refuse):");
|
|
74
|
+
for (const record of dirty) {
|
|
75
|
+
lines.push(" ! " +
|
|
76
|
+
record.table +
|
|
77
|
+
" > " +
|
|
78
|
+
record.name +
|
|
79
|
+
" " +
|
|
80
|
+
recordLink(host, record.table, record.sys_id));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 2. Record deltas.
|
|
84
|
+
if (diff.creates.length > 0) {
|
|
85
|
+
lines.push(" CREATE (" + diff.creates.length + "):");
|
|
86
|
+
for (const change of diff.creates) {
|
|
87
|
+
lines.push(recordRow(host, "+", change));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (diff.updates.length > 0) {
|
|
91
|
+
lines.push(" UPDATE (" + diff.updates.length + "):");
|
|
92
|
+
for (const change of diff.updates) {
|
|
93
|
+
lines.push(recordRow(host, "~", change));
|
|
94
|
+
for (const sub of fieldSubRows(change)) {
|
|
95
|
+
lines.push(sub);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (deletes.applyable.length > 0) {
|
|
100
|
+
lines.push(" DELETE (" + deletes.applyable.length + "):");
|
|
101
|
+
for (const change of deletes.applyable) {
|
|
102
|
+
lines.push(recordRow(host, "-", change));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (deletes.drift.length > 0) {
|
|
106
|
+
lines.push(" INSTANCE-ONLY — present on instance, not in branch, not in baseline " +
|
|
107
|
+
"(kept, never deleted):");
|
|
108
|
+
for (const change of deletes.drift) {
|
|
109
|
+
lines.push(recordRow(host, "?", change));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 3. Schema — report-only.
|
|
113
|
+
renderSchema(host, scopeResult, lines);
|
|
114
|
+
const noRecordDeltas = diff.creates.length === 0 &&
|
|
115
|
+
diff.updates.length === 0 &&
|
|
116
|
+
deletes.applyable.length === 0;
|
|
117
|
+
if (noRecordDeltas && dirty.length === 0) {
|
|
118
|
+
lines.push(" in sync (" + diff.unchangedCount + " records match the instance)");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function renderSchema(host, scopeResult, lines) {
|
|
122
|
+
const schema = scopeResult.schema;
|
|
123
|
+
if (!schema) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const total = schema.summary.breaking + schema.summary.warn + schema.summary.info;
|
|
127
|
+
if (total === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
lines.push(" SCHEMA — report-only (" +
|
|
131
|
+
total +
|
|
132
|
+
" change(s); ServiceNow blocks headless dictionary writes — apply by hand):");
|
|
133
|
+
for (const table of schema.tables) {
|
|
134
|
+
lines.push(" " +
|
|
135
|
+
table.change +
|
|
136
|
+
" table " +
|
|
137
|
+
table.table +
|
|
138
|
+
" " +
|
|
139
|
+
dictionaryLink(host, table.table, ""));
|
|
140
|
+
}
|
|
141
|
+
for (const field of schema.fields) {
|
|
142
|
+
lines.push(" " +
|
|
143
|
+
field.change +
|
|
144
|
+
" " +
|
|
145
|
+
field.table +
|
|
146
|
+
"." +
|
|
147
|
+
field.field +
|
|
148
|
+
" " +
|
|
149
|
+
dictionaryLink(host, field.table, field.field));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function formatReconcileReport(input) {
|
|
153
|
+
const host = input.instanceHost;
|
|
154
|
+
const lines = [];
|
|
155
|
+
lines.push("dove reconcile — dry run (no changes applied)");
|
|
156
|
+
lines.push(" instance " + (host || "(unknown)"));
|
|
157
|
+
lines.push(" branch " + (input.branchRef || "(unknown)"));
|
|
158
|
+
let totalCreates = 0;
|
|
159
|
+
let totalUpdates = 0;
|
|
160
|
+
let totalDeletes = 0;
|
|
161
|
+
let totalDrift = 0;
|
|
162
|
+
let totalInstanceOnly = 0;
|
|
163
|
+
for (const scopeResult of input.scopes) {
|
|
164
|
+
renderScope(host, scopeResult, lines);
|
|
165
|
+
const deletes = partitionDeletes(scopeResult.diff.deletes);
|
|
166
|
+
totalCreates += scopeResult.diff.creates.length;
|
|
167
|
+
totalUpdates += scopeResult.diff.updates.length;
|
|
168
|
+
totalDeletes += deletes.applyable.length;
|
|
169
|
+
totalInstanceOnly += deletes.drift.length;
|
|
170
|
+
totalDrift += scopeResult.dirty.length;
|
|
171
|
+
}
|
|
172
|
+
if (input.schemaSkippedReason) {
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push("Schema diff skipped: " + input.schemaSkippedReason);
|
|
175
|
+
}
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push("Summary: " +
|
|
178
|
+
totalCreates +
|
|
179
|
+
" create, " +
|
|
180
|
+
totalUpdates +
|
|
181
|
+
" update, " +
|
|
182
|
+
totalDeletes +
|
|
183
|
+
" delete" +
|
|
184
|
+
(totalInstanceOnly > 0 ? ", " + totalInstanceOnly + " instance-only kept" : "") +
|
|
185
|
+
(totalDrift > 0 ? ", " + totalDrift + " drift" : ""));
|
|
186
|
+
if (totalDrift > 0) {
|
|
187
|
+
lines.push("Apply would REFUSE: " +
|
|
188
|
+
totalDrift +
|
|
189
|
+
" record(s) changed on the instance since baseline. " +
|
|
190
|
+
"Keep them with a refresh, or discard with --force.");
|
|
191
|
+
}
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Domain types for `dove reconcile` — the deliberate inverse of `dove watch`:
|
|
3
|
+
// make a developer's personal ServiceNow instance match the git branch they
|
|
4
|
+
// just checked out. Phase 1 is read-only (diff + report); these types are
|
|
5
|
+
// shared by the apply phases (2 + 3) as they land.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|