@tenonhq/dovetail-core 0.0.99 → 0.0.100
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.
|
@@ -36,6 +36,7 @@ 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.processManifestForScope = processManifestForScope;
|
|
39
40
|
exports.processScope = processScope;
|
|
40
41
|
exports.initScopesCommand = initScopesCommand;
|
|
41
42
|
exports.watchAllScopesCommand = watchAllScopesCommand;
|
|
@@ -51,7 +52,8 @@ const fs = __importStar(require("fs"));
|
|
|
51
52
|
const child_process_1 = require("child_process");
|
|
52
53
|
const progress_1 = __importDefault(require("progress"));
|
|
53
54
|
const fsp = fs.promises;
|
|
54
|
-
// Custom function to process manifest with specific source directory
|
|
55
|
+
// Custom function to process manifest with specific source directory.
|
|
56
|
+
// Exported for regression testing of filesystem-safe folder writing.
|
|
55
57
|
async function processManifestForScope(manifest, sourceDirectory, forceWrite = false) {
|
|
56
58
|
try {
|
|
57
59
|
// Ensure the source directory exists
|
|
@@ -62,6 +64,13 @@ async function processManifestForScope(manifest, sourceDirectory, forceWrite = f
|
|
|
62
64
|
Logger_1.logger.error("Failed to create directory: " + sourceDirectory);
|
|
63
65
|
throw dirError;
|
|
64
66
|
}
|
|
67
|
+
// Re-key the manifest to filesystem-safe folder names before writing.
|
|
68
|
+
// Idempotent, so it is safe even when the caller already normalized.
|
|
69
|
+
// Without this, a Windows-illegal display name (trailing dot/space, or
|
|
70
|
+
// any of <>:"|?*) would be written as a raw folder and abort `git clone`
|
|
71
|
+
// on Windows. Keeps the folder ≡ manifest-key invariant `dove push` relies
|
|
72
|
+
// on. See toSafeFolderName / normalizeManifestKeys (PR #117, #133).
|
|
73
|
+
AppUtils.normalizeManifestKeys(manifest);
|
|
65
74
|
const tables = manifest.tables || {};
|
|
66
75
|
const tableNames = Object.keys(tables);
|
|
67
76
|
var totalRecords = 0;
|
|
@@ -86,8 +95,10 @@ async function processManifestForScope(manifest, sourceDirectory, forceWrite = f
|
|
|
86
95
|
FileLogger_1.fileLogger.debug("Table " + tableName + ": " + recordNames.length + " records");
|
|
87
96
|
for (const recordName of recordNames) {
|
|
88
97
|
const record = tableRecords.records[recordName];
|
|
89
|
-
//
|
|
90
|
-
|
|
98
|
+
// Derive the folder via toSafeFolderName (never the raw record.name):
|
|
99
|
+
// normalizeManifestKeys above already re-keyed unsafe names to sys_id,
|
|
100
|
+
// and this keeps the writer defensive if that ever changes.
|
|
101
|
+
const recordDirName = AppUtils.toSafeFolderName(record);
|
|
91
102
|
const recordPath = path.join(tablePath, recordDirName);
|
|
92
103
|
// Check if metadata file exists in the files from server
|
|
93
104
|
const hasMetadataFromServer = record.files && record.files.some((f) => f.name === 'metaData' && f.type === 'json');
|
|
@@ -195,8 +206,9 @@ async function processScope(scopeName, scopeConfig, apiDelay = 0) {
|
|
|
195
206
|
else {
|
|
196
207
|
Logger_1.logger.warn("No _tables whitelist defined — writing ALL tables for " + scopeName);
|
|
197
208
|
}
|
|
198
|
-
//
|
|
199
|
-
|
|
209
|
+
// Record-key normalization now happens inside processManifestForScope (the
|
|
210
|
+
// writer owns the folder ≡ key invariant). The missing-files structure
|
|
211
|
+
// below keys by record.sys_id, so it does not depend on normalized names.
|
|
200
212
|
// Build the missing files structure from the filtered manifest
|
|
201
213
|
var manifestTables = manifest.tables || {};
|
|
202
214
|
var allMissingFiles = {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Regression: the download / initScopes writer must produce filesystem-SAFE
|
|
4
|
+
* folder names — even when its caller forgets to normalize the manifest first.
|
|
5
|
+
*
|
|
6
|
+
* `processManifestForScope` (allScopesCommands.ts) builds one folder per record.
|
|
7
|
+
* It used to do `record.name || recordName` with no sanitization, so a record
|
|
8
|
+
* whose ServiceNow display name is Windows-illegal — e.g. a sys_ui_script named
|
|
9
|
+
* `x_cadso_core.` (trailing dot) — was written as a literal `x_cadso_core.`
|
|
10
|
+
* folder. Windows forbids trailing dots/spaces in path segments, so Git aborts
|
|
11
|
+
* the whole checkout (`error: invalid path`) and the clone fails for every
|
|
12
|
+
* Windows dev. It only avoided the bug because one caller happened to call
|
|
13
|
+
* normalizeManifestKeys first; any other caller re-introduced it.
|
|
14
|
+
*
|
|
15
|
+
* The writer now normalizes the manifest itself and routes folder names through
|
|
16
|
+
* toSafeFolderName, so an unsafe name falls back to its sys_id (and the manifest
|
|
17
|
+
* is re-keyed to match — preserving the folder ≡ key invariant `dove push`
|
|
18
|
+
* relies on), while a safe name stays human-readable.
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const os = __importStar(require("os"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
// ---------- mocks (must come before importing the module under test) ----------
|
|
58
|
+
var mockLogger = {
|
|
59
|
+
info: jest.fn(),
|
|
60
|
+
debug: jest.fn(),
|
|
61
|
+
warn: jest.fn(),
|
|
62
|
+
error: jest.fn(),
|
|
63
|
+
success: jest.fn(),
|
|
64
|
+
getLogLevel: function () { return "warn"; },
|
|
65
|
+
};
|
|
66
|
+
var mockFileLogger = {
|
|
67
|
+
debug: jest.fn(),
|
|
68
|
+
info: jest.fn(),
|
|
69
|
+
warn: jest.fn(),
|
|
70
|
+
error: jest.fn(),
|
|
71
|
+
};
|
|
72
|
+
jest.mock("../Logger", function () { return { logger: mockLogger }; });
|
|
73
|
+
jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
|
|
74
|
+
jest.mock("../config", function () { return {}; });
|
|
75
|
+
jest.mock("../snClient", function () {
|
|
76
|
+
return {
|
|
77
|
+
defaultClient: function () { return {}; },
|
|
78
|
+
unwrapSNResponse: function (p) { return Promise.resolve(p); },
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
jest.mock("../wizard", function () {
|
|
82
|
+
return { setupDotEnv: jest.fn(), getLoginInfo: jest.fn() };
|
|
83
|
+
});
|
|
84
|
+
jest.mock("../commands", function () { return { setLogLevel: jest.fn() }; });
|
|
85
|
+
jest.mock("progress", function () {
|
|
86
|
+
return jest.fn().mockImplementation(function () {
|
|
87
|
+
return { tick: jest.fn() };
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const AllScopes = __importStar(require("../allScopesCommands"));
|
|
91
|
+
// ---------- helpers ----------
|
|
92
|
+
var tmpRoot;
|
|
93
|
+
var UI_SYS_ID = "89295b86334983d07b18bc534d5c7b8a";
|
|
94
|
+
var SAFE_SYS_ID = "aa11bb22cc33dd44ee55ff6600112233";
|
|
95
|
+
function listFolders(table) {
|
|
96
|
+
try {
|
|
97
|
+
return fs.readdirSync(path.join(tmpRoot, table)).sort();
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
describe("download — folder names are filesystem-safe (self-sufficient writer)", function () {
|
|
104
|
+
beforeEach(function () {
|
|
105
|
+
jest.clearAllMocks();
|
|
106
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dove-dl-safe-"));
|
|
107
|
+
});
|
|
108
|
+
afterEach(function () {
|
|
109
|
+
try {
|
|
110
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
catch (e) { }
|
|
113
|
+
});
|
|
114
|
+
test("a trailing-dot record name is written to its sys_id folder, never `x_cadso_core.`", async function () {
|
|
115
|
+
// Manifest keyed by the raw, Windows-illegal display name — i.e. a caller
|
|
116
|
+
// that did NOT pre-normalize. The writer must defend itself.
|
|
117
|
+
var manifest = {
|
|
118
|
+
tables: {
|
|
119
|
+
sys_ui_script: {
|
|
120
|
+
records: {
|
|
121
|
+
"x_cadso_core.": {
|
|
122
|
+
name: "x_cadso_core.",
|
|
123
|
+
sys_id: UI_SYS_ID,
|
|
124
|
+
files: [{ name: "script", type: "js", content: "// ui" }],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
await AllScopes.processManifestForScope(manifest, tmpRoot, true);
|
|
131
|
+
var folders = listFolders("sys_ui_script");
|
|
132
|
+
expect(folders).toContain(UI_SYS_ID);
|
|
133
|
+
expect(folders).not.toContain("x_cadso_core.");
|
|
134
|
+
// File landed inside the sys_id folder.
|
|
135
|
+
expect(fs.readFileSync(path.join(tmpRoot, "sys_ui_script", UI_SYS_ID, "script.js"), "utf8")).toBe("// ui");
|
|
136
|
+
// Folder ≡ manifest-key invariant: the manifest was re-keyed to the sys_id
|
|
137
|
+
// so `dove push` (which looks records up by folder name) still resolves it.
|
|
138
|
+
expect(Object.keys(manifest.tables.sys_ui_script.records)).toEqual([UI_SYS_ID]);
|
|
139
|
+
});
|
|
140
|
+
test("other Windows-illegal names also fall back to sys_id (wildcard ACL, trailing space)", async function () {
|
|
141
|
+
var ACL_SYS_ID = "bc91c2a5f334621048dff31590812771";
|
|
142
|
+
var SPACE_SYS_ID = "cc22dd33ee44ff5500112233445566aa";
|
|
143
|
+
var manifest = {
|
|
144
|
+
tables: {
|
|
145
|
+
sys_security_acl: {
|
|
146
|
+
records: {
|
|
147
|
+
// A `<table>.*` wildcard ACL — the `*` (and trailing `.*`) is illegal on Windows.
|
|
148
|
+
"x_cadso_work_campaign.*": {
|
|
149
|
+
name: "x_cadso_work_campaign.*",
|
|
150
|
+
sys_id: ACL_SYS_ID,
|
|
151
|
+
files: [{ name: "script", type: "js", content: "// acl" }],
|
|
152
|
+
},
|
|
153
|
+
// A trailing space is silently stripped by Win32 — also unsafe.
|
|
154
|
+
"Trailing Space ": {
|
|
155
|
+
name: "Trailing Space ",
|
|
156
|
+
sys_id: SPACE_SYS_ID,
|
|
157
|
+
files: [{ name: "script", type: "js", content: "// space" }],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
await AllScopes.processManifestForScope(manifest, tmpRoot, true);
|
|
164
|
+
var folders = listFolders("sys_security_acl");
|
|
165
|
+
expect(folders).toContain(ACL_SYS_ID);
|
|
166
|
+
expect(folders).toContain(SPACE_SYS_ID);
|
|
167
|
+
expect(folders).not.toContain("x_cadso_work_campaign.*");
|
|
168
|
+
expect(folders).not.toContain("Trailing Space ");
|
|
169
|
+
});
|
|
170
|
+
test("a filesystem-safe name stays human-readable (no needless sys_id fallback)", async function () {
|
|
171
|
+
var manifest = {
|
|
172
|
+
tables: {
|
|
173
|
+
sys_ui_script: {
|
|
174
|
+
records: {
|
|
175
|
+
"Tenon Shell": {
|
|
176
|
+
name: "Tenon Shell",
|
|
177
|
+
sys_id: SAFE_SYS_ID,
|
|
178
|
+
files: [{ name: "script", type: "js", content: "// shell" }],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
await AllScopes.processManifestForScope(manifest, tmpRoot, true);
|
|
185
|
+
var folders = listFolders("sys_ui_script");
|
|
186
|
+
expect(folders).toContain("Tenon Shell");
|
|
187
|
+
expect(folders).not.toContain(SAFE_SYS_ID);
|
|
188
|
+
});
|
|
189
|
+
});
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-06-
|
|
3
|
+
"generated_at": "2026-06-02T18:16:07.004Z",
|
|
4
4
|
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-core@0.0.99",
|
|
8
|
+
"package": "@tenonhq/dovetail-core",
|
|
9
|
+
"version": "0.0.99",
|
|
10
|
+
"prev_version": "0.0.98",
|
|
11
|
+
"published_at": "2026-06-02T18:16:06.996Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "bad6776e9a426207b700d39dd139438c8a934d49",
|
|
14
|
+
"tag": "dovetail-core@0.0.99"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "bad6776e9",
|
|
20
|
+
"subject": "fix(core): write instance-relative _record_link in metaData.json (#139)",
|
|
21
|
+
"pr": 139
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"sha": "e0cf83a8c",
|
|
25
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.13 [skip ci]"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"sha": "bd127c726",
|
|
29
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.12 [skip ci]"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"sha": "448696846",
|
|
33
|
+
"subject": "chore(release): publish 3 packages [skip ci]"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"exports_delta": null,
|
|
37
|
+
"narrative": null
|
|
38
|
+
},
|
|
5
39
|
{
|
|
6
40
|
"$schema": "dovetail-release-event/v1",
|
|
7
41
|
"event_id": "dovetail-servicenow@0.0.13",
|