@tenonhq/dovetail-servicenow 0.0.22 → 0.0.23

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/cli.js CHANGED
@@ -79,6 +79,7 @@ const editFlow_1 = require("./flowDesigner/editFlow");
79
79
  const editActionType_1 = require("./flowDesigner/editActionType");
80
80
  const testFlow_1 = require("./flowDesigner/testFlow");
81
81
  const table_1 = require("./table");
82
+ const hostAssets_1 = require("./hostAssets");
82
83
  const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
83
84
  function parseArgs(argv) {
84
85
  var command = argv[0] || "";
@@ -710,6 +711,9 @@ function printHelp() {
710
711
  " (--table <name|sys_id> --label <l> --type <t>\n" +
711
712
  " [--name <element>] [--max-length <n>] [--reference <table>]\n" +
712
713
  " [--scope <s>] [--update-set <sys_id>] [--dry-run] [--json])\n" +
714
+ " host-assets Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)\n" +
715
+ " (--dir <dist> --app <sys_id> --scope <namespace>\n" +
716
+ " [--update-set <sys_id>] [--max-bytes <n>] [--allow-oversize] [--dry-run] [--json])\n" +
713
717
  " test-flow Validate (default) or run a flow/subflow\n" +
714
718
  " (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
715
719
  " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
@@ -871,6 +875,48 @@ async function runAddColumn(flags) {
871
875
  return 2;
872
876
  return 0;
873
877
  }
878
+ /**
879
+ * dove-sn host-assets:
880
+ * --dir <dist> Required. Path to the pre-built dist/ directory.
881
+ * --app <sys_id> Required. Application record sys_id (m2m `application`).
882
+ * --scope <namespace> Required. Carrier scope, e.g. x_cadso_app_shell.
883
+ * --update-set <sys_id> Optional. Defaults to the scope's current update set.
884
+ * --max-bytes <n> Optional. Per-chunk serve cap (default ~5 MB).
885
+ * --allow-oversize Optional. Warn instead of failing on an oversize chunk.
886
+ * --dry-run Optional. Plan only; no writes/uploads/prunes.
887
+ * --json Optional. Emit the structured HostAssetsResult.
888
+ *
889
+ * Exit codes: 0 done/dry-run, 1 bad args, 2 a write landed but read-back is unverified.
890
+ */
891
+ async function runHostAssets(flags) {
892
+ var dir = flags.dir;
893
+ var app = flags.app;
894
+ var scope = flags.scope;
895
+ if (!dir || !app || !scope) {
896
+ process.stderr.write("host-assets: --dir, --app and --scope are required\n");
897
+ return 1;
898
+ }
899
+ var params = { dir: path.resolve(dir), app: app, scope: scope };
900
+ var us = flags["update-set"] || flags.updateSetSysId;
901
+ if (us)
902
+ params.updateSetSysId = us;
903
+ if (flags["max-bytes"])
904
+ params.maxBytes = Number(flags["max-bytes"]);
905
+ if (flags["allow-oversize"] === "true")
906
+ params.allowOversize = true;
907
+ if (flags["dry-run"] === "true")
908
+ params.dryRun = true;
909
+ var client = (0, client_1.createClient)({});
910
+ var result = await (0, hostAssets_1.hostAssets)(client, params);
911
+ if (flags.json === "true") {
912
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
913
+ }
914
+ else {
915
+ process.stdout.write((0, hostAssets_1.formatHostAssetsResult)(result) + "\n");
916
+ }
917
+ var unverified = !result.dryRun && result.chunks.some(function (c) { return !c.verified; });
918
+ return unverified ? 2 : 0;
919
+ }
874
920
  async function main() {
875
921
  var parsed = parseArgs(process.argv.slice(2));
876
922
  // Load credentials before any command runs. `--env`/`--env-file` (or the
@@ -905,6 +951,9 @@ async function main() {
905
951
  if (parsed.command === "add-column") {
906
952
  return await runAddColumn(parsed.flags);
907
953
  }
954
+ if (parsed.command === "host-assets") {
955
+ return await runHostAssets(parsed.flags);
956
+ }
908
957
  if (parsed.command === "test-flow") {
909
958
  return await runTestFlow(parsed.flags);
910
959
  }
package/dist/client.d.ts CHANGED
@@ -34,6 +34,15 @@ export interface TableSchema {
34
34
  fields: Array<TableSchemaField>;
35
35
  primary_key: string;
36
36
  }
37
+ /** A sys_attachment row, as returned by the native Attachment API. */
38
+ export interface AttachmentMeta {
39
+ sys_id: string;
40
+ file_name: string;
41
+ content_type: string;
42
+ /** SHA-256 hex of the file content, computed by ServiceNow. Absent on older instances. */
43
+ hash?: string;
44
+ size_bytes?: string;
45
+ }
37
46
  export interface ServiceNowClient {
38
47
  table: {
39
48
  /** GET /api/now/table/<t>?sysparm_query=...&sysparm_limit=N — returns result array. */
@@ -113,5 +122,31 @@ export interface ServiceNowClient {
113
122
  /** POST an arbitrary native ServiceNow REST path with a JSON body. See `get`. */
114
123
  post: <T = any>(path: string, body: any) => Promise<T>;
115
124
  };
125
+ attachment: {
126
+ /**
127
+ * GET /api/now/attachment?sysparm_query=table_name=<t>^table_sys_id=<id> —
128
+ * list the sys_attachment rows on a record. Read-only.
129
+ */
130
+ listFor: (params: {
131
+ table: string;
132
+ sysId: string;
133
+ }) => Promise<Array<AttachmentMeta>>;
134
+ /**
135
+ * POST /api/now/attachment/file — upload raw bytes as a sys_attachment on a record.
136
+ * The Buffer is sent verbatim with `contentType` as the request Content-Type (binary,
137
+ * not JSON). Reuses the shared auth/retry/throttle transport — not a bespoke HTTP path.
138
+ */
139
+ upload: (params: {
140
+ table: string;
141
+ sysId: string;
142
+ fileName: string;
143
+ contentType: string;
144
+ data: Buffer;
145
+ }) => Promise<AttachmentMeta>;
146
+ /** DELETE /api/now/attachment/<sysId> — remove a single attachment. */
147
+ remove: (params: {
148
+ sysId: string;
149
+ }) => Promise<void>;
150
+ };
116
151
  }
117
152
  export declare function createClient(config?: ServiceNowClientConfig): ServiceNowClient;
package/dist/client.js CHANGED
@@ -316,6 +316,37 @@ function createClient(config = {}) {
316
316
  post: function (path, body) {
317
317
  return request({ method: "POST", url: path, data: body }, "now.post(" + path + ")");
318
318
  }
319
+ },
320
+ attachment: {
321
+ listFor: async function (params) {
322
+ var data = await request({
323
+ method: "GET",
324
+ url: "/api/now/attachment",
325
+ params: {
326
+ sysparm_query: "table_name=" + params.table + "^table_sys_id=" + params.sysId,
327
+ sysparm_fields: "sys_id,file_name,content_type,hash,size_bytes"
328
+ }
329
+ }, "attachment.listFor(" + params.table + ")");
330
+ return (data && data.result) || [];
331
+ },
332
+ upload: async function (params) {
333
+ var data = await request({
334
+ method: "POST",
335
+ url: "/api/now/attachment/file",
336
+ params: {
337
+ table_name: params.table,
338
+ table_sys_id: params.sysId,
339
+ file_name: params.fileName
340
+ },
341
+ data: params.data,
342
+ // Binary upload: override the client's default application/json.
343
+ headers: { "content-type": params.contentType }
344
+ }, "attachment.upload(" + params.fileName + ")");
345
+ return (data && data.result) || data;
346
+ },
347
+ remove: async function (params) {
348
+ await request({ method: "DELETE", url: "/api/now/attachment/" + params.sysId }, "attachment.remove(" + params.sysId + ")");
349
+ }
319
350
  }
320
351
  };
321
352
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * hostAssets — deploy a pre-built front-end bundle to ServiceNow.
3
+ *
4
+ * For each chunk in a built dist/ (index.html + assets/*.{js,css}) this:
5
+ * 1. Upserts a carrier sys_ui_script named `app_shell_asset:<vite-relative-path>`.
6
+ * The name carries the rotating hash on purpose — the Scripted REST serving
7
+ * resource resolves an asset request to its carrier by this exact name, so the
8
+ * verb's naming MUST match the path the built index.html references, or the
9
+ * asset 404s on serve.
10
+ * 2. Stores the chunk's bytes as a sys_attachment on that record (the script field
11
+ * caps at 65 KB; real chunks are far larger). Identical bytes are detected by
12
+ * SHA-256 and left in place.
13
+ * 3. Wires an x_cadso_app_shell_m2m_app_script row (application, script, chunk_role,
14
+ * order) so the app shell loads the chunk.
15
+ * 4. Prunes carriers + m2m rows for chunks no longer in the build — hashes rotate
16
+ * every build, so last build's records would otherwise pile up.
17
+ *
18
+ * The sys_ui_script + m2m writes route through the Dovetail Scripted REST API and are
19
+ * captured in the target update set. Attachments are data, not customizations, so they
20
+ * are not part of the update set. Re-runnable per build, per instance; idempotent for
21
+ * an identical dist/.
22
+ *
23
+ * The serving layer streams each chunk via GlideSysAttachment.getContentStream(), capped
24
+ * at ~5 MB (glide.scriptable.excel.max_file_size). A chunk at/over the cap truncates on
25
+ * serve, so the verb fails fast on an oversize chunk unless allowOversize is set.
26
+ */
27
+ import type { ServiceNowClient } from "./client";
28
+ import type { ChunkInfo, HostAssetsParams, HostAssetsResult } from "./types";
29
+ /**
30
+ * Classify build files into ordered ChunkInfo. Pure — no filesystem access — so the
31
+ * naming + role + order logic is unit-testable in isolation.
32
+ */
33
+ export declare function classifyChunks(files: Array<{
34
+ viteRelPath: string;
35
+ ext: string;
36
+ isIndexHtml: boolean;
37
+ }>): Array<ChunkInfo>;
38
+ /**
39
+ * Deploy a built dist/ to ServiceNow as app-shell carrier scripts + attachments + m2m
40
+ * wiring. Idempotent; re-running an identical dist/ reports every chunk unchanged.
41
+ */
42
+ export declare function hostAssets(client: ServiceNowClient, params: HostAssetsParams): Promise<HostAssetsResult>;
43
+ /** Human-readable one-line-per-chunk summary for the CLI. */
44
+ export declare function formatHostAssetsResult(r: HostAssetsResult): string;
@@ -0,0 +1,579 @@
1
+ "use strict";
2
+ /**
3
+ * hostAssets — deploy a pre-built front-end bundle to ServiceNow.
4
+ *
5
+ * For each chunk in a built dist/ (index.html + assets/*.{js,css}) this:
6
+ * 1. Upserts a carrier sys_ui_script named `app_shell_asset:<vite-relative-path>`.
7
+ * The name carries the rotating hash on purpose — the Scripted REST serving
8
+ * resource resolves an asset request to its carrier by this exact name, so the
9
+ * verb's naming MUST match the path the built index.html references, or the
10
+ * asset 404s on serve.
11
+ * 2. Stores the chunk's bytes as a sys_attachment on that record (the script field
12
+ * caps at 65 KB; real chunks are far larger). Identical bytes are detected by
13
+ * SHA-256 and left in place.
14
+ * 3. Wires an x_cadso_app_shell_m2m_app_script row (application, script, chunk_role,
15
+ * order) so the app shell loads the chunk.
16
+ * 4. Prunes carriers + m2m rows for chunks no longer in the build — hashes rotate
17
+ * every build, so last build's records would otherwise pile up.
18
+ *
19
+ * The sys_ui_script + m2m writes route through the Dovetail Scripted REST API and are
20
+ * captured in the target update set. Attachments are data, not customizations, so they
21
+ * are not part of the update set. Re-runnable per build, per instance; idempotent for
22
+ * an identical dist/.
23
+ *
24
+ * The serving layer streams each chunk via GlideSysAttachment.getContentStream(), capped
25
+ * at ~5 MB (glide.scriptable.excel.max_file_size). A chunk at/over the cap truncates on
26
+ * serve, so the verb fails fast on an oversize chunk unless allowOversize is set.
27
+ */
28
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ var desc = Object.getOwnPropertyDescriptor(m, k);
31
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32
+ desc = { enumerable: true, get: function() { return m[k]; } };
33
+ }
34
+ Object.defineProperty(o, k2, desc);
35
+ }) : (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ o[k2] = m[k];
38
+ }));
39
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
40
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
41
+ }) : function(o, v) {
42
+ o["default"] = v;
43
+ });
44
+ var __importStar = (this && this.__importStar) || (function () {
45
+ var ownKeys = function(o) {
46
+ ownKeys = Object.getOwnPropertyNames || function (o) {
47
+ var ar = [];
48
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
49
+ return ar;
50
+ };
51
+ return ownKeys(o);
52
+ };
53
+ return function (mod) {
54
+ if (mod && mod.__esModule) return mod;
55
+ var result = {};
56
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
57
+ __setModuleDefault(result, mod);
58
+ return result;
59
+ };
60
+ })();
61
+ Object.defineProperty(exports, "__esModule", { value: true });
62
+ exports.classifyChunks = classifyChunks;
63
+ exports.hostAssets = hostAssets;
64
+ exports.formatHostAssetsResult = formatHostAssetsResult;
65
+ const fs = __importStar(require("fs"));
66
+ const path = __importStar(require("path"));
67
+ const crypto = __importStar(require("crypto"));
68
+ var SCRIPT_TABLE = "sys_ui_script";
69
+ var M2M_TABLE = "x_cadso_app_shell_m2m_app_script";
70
+ var ASSET_NAME_PREFIX = "app_shell_asset:";
71
+ /** ~5 MB serving cap (glide.scriptable.excel.max_file_size). Conservative MiB reading. */
72
+ var DEFAULT_MAX_CHUNK_BYTES = 5 * 1024 * 1024;
73
+ var CONTENT_TYPES = {
74
+ js: "application/javascript",
75
+ css: "text/css",
76
+ html: "text/html",
77
+ };
78
+ var ROLE_PRIORITY = {
79
+ index: 0,
80
+ entry: 1,
81
+ vendor: 2,
82
+ router: 3,
83
+ state: 4,
84
+ lazy: 5,
85
+ style: 6,
86
+ };
87
+ // A trailing `-<hash>` of 8+ url-safe chars (Vite's content hash). Used only to derive
88
+ // the base name for role inference — never to name the carrier record.
89
+ var HASH_RE = /^(.+)-[A-Za-z0-9_]{8,}$/;
90
+ function stripHash(nameNoExt) {
91
+ var m = HASH_RE.exec(nameNoExt);
92
+ return m ? m[1] : nameNoExt;
93
+ }
94
+ function roleForBase(base, ext, isIndexHtml) {
95
+ if (isIndexHtml)
96
+ return "index";
97
+ if (ext === "css")
98
+ return "style";
99
+ var b = base.toLowerCase();
100
+ if (b === "index" || b === "main" || b === "entry" || b === "app") {
101
+ return "entry";
102
+ }
103
+ if (b.indexOf("vendor") >= 0)
104
+ return "vendor";
105
+ if (b === "router" || b === "routes" || b === "route")
106
+ return "router";
107
+ if (b === "store" ||
108
+ b === "state" ||
109
+ b === "redux" ||
110
+ b === "pinia" ||
111
+ b === "vuex") {
112
+ return "state";
113
+ }
114
+ return "lazy";
115
+ }
116
+ /**
117
+ * Classify build files into ordered ChunkInfo. Pure — no filesystem access — so the
118
+ * naming + role + order logic is unit-testable in isolation.
119
+ */
120
+ function classifyChunks(files) {
121
+ var infos = files.map(function (f) {
122
+ var lowerExt = f.ext.toLowerCase();
123
+ var fileName = f.viteRelPath.split("/").pop() || f.viteRelPath;
124
+ var nameNoExt = fileName.slice(0, fileName.length - (lowerExt.length + 1));
125
+ var base = f.isIndexHtml ? "index" : stripHash(nameNoExt);
126
+ var role = roleForBase(base, lowerExt, f.isIndexHtml);
127
+ return {
128
+ viteRelPath: f.viteRelPath,
129
+ name: ASSET_NAME_PREFIX + f.viteRelPath,
130
+ fileName: fileName,
131
+ base: base,
132
+ ext: lowerExt,
133
+ role: role,
134
+ order: 0,
135
+ contentType: CONTENT_TYPES[lowerExt] || "application/octet-stream",
136
+ };
137
+ });
138
+ // Deterministic order: role priority, then relative path. Gap-free integers.
139
+ infos.sort(function (a, b) {
140
+ var pa = ROLE_PRIORITY[a.role];
141
+ var pb = ROLE_PRIORITY[b.role];
142
+ if (pa !== pb)
143
+ return pa - pb;
144
+ return a.viteRelPath < b.viteRelPath
145
+ ? -1
146
+ : a.viteRelPath > b.viteRelPath
147
+ ? 1
148
+ : 0;
149
+ });
150
+ infos.forEach(function (info, i) {
151
+ info.order = i;
152
+ });
153
+ return infos;
154
+ }
155
+ function readDistChunks(dir) {
156
+ if (!fs.existsSync(dir)) {
157
+ throw new Error("host-assets: dist directory not found: " + dir);
158
+ }
159
+ var found = [];
160
+ var indexHtml = path.join(dir, "index.html");
161
+ if (fs.existsSync(indexHtml)) {
162
+ found.push({
163
+ viteRelPath: "index.html",
164
+ ext: "html",
165
+ isIndexHtml: true,
166
+ abs: indexHtml,
167
+ });
168
+ }
169
+ var assetsDir = path.join(dir, "assets");
170
+ if (fs.existsSync(assetsDir)) {
171
+ fs.readdirSync(assetsDir).forEach(function (name) {
172
+ var ext = path.extname(name).replace(/^\./, "").toLowerCase();
173
+ if (ext !== "js" && ext !== "css")
174
+ return;
175
+ found.push({
176
+ viteRelPath: "assets/" + name,
177
+ ext: ext,
178
+ isIndexHtml: false,
179
+ abs: path.join(assetsDir, name),
180
+ });
181
+ });
182
+ }
183
+ if (found.length === 0) {
184
+ throw new Error("host-assets: no index.html or assets/*.{js,css} found under " + dir);
185
+ }
186
+ var infos = classifyChunks(found.map(function (f) {
187
+ return {
188
+ viteRelPath: f.viteRelPath,
189
+ ext: f.ext,
190
+ isIndexHtml: f.isIndexHtml,
191
+ };
192
+ }));
193
+ var absByPath = {};
194
+ found.forEach(function (f) {
195
+ absByPath[f.viteRelPath] = f.abs;
196
+ });
197
+ return infos.map(function (info) {
198
+ return { info: info, data: fs.readFileSync(absByPath[info.viteRelPath]) };
199
+ });
200
+ }
201
+ function guardOversize(chunks, maxBytes, allowOversize) {
202
+ var oversize = chunks.filter(function (c) {
203
+ return c.data.length >= maxBytes;
204
+ });
205
+ if (oversize.length === 0)
206
+ return;
207
+ var detail = oversize
208
+ .map(function (c) {
209
+ return c.info.viteRelPath + " (" + c.data.length + "B)";
210
+ })
211
+ .join(", ");
212
+ var msg = "host-assets: " +
213
+ oversize.length +
214
+ " chunk(s) at/over the " +
215
+ maxBytes +
216
+ "B serve cap (glide.scriptable.excel.max_file_size) — these truncate on serve: " +
217
+ detail +
218
+ ". Split deps via Vite manualChunks, or pass allowOversize to proceed anyway.";
219
+ if (!allowOversize)
220
+ throw new Error(msg);
221
+ // eslint-disable-next-line no-console
222
+ console.warn("[host-assets] WARNING — " + msg);
223
+ }
224
+ function sha256Hex(buf) {
225
+ return crypto.createHash("sha256").update(buf).digest("hex");
226
+ }
227
+ async function resolveScopeSysId(client, namespace) {
228
+ var rows = await client.table.query("sys_scope", "scope=" + namespace, 1);
229
+ if (rows.length === 0) {
230
+ throw new Error("host-assets: scope '" + namespace + "' not found in sys_scope.");
231
+ }
232
+ return rows[0].sys_id;
233
+ }
234
+ async function fetchUpdateSet(client, sysId) {
235
+ var rows = await client.table.query("sys_update_set", "sys_id=" + sysId, 1);
236
+ if (rows.length === 0) {
237
+ throw new Error("host-assets: update set " + sysId + " not found.");
238
+ }
239
+ var row = rows[0];
240
+ if (row.state && row.state !== "in progress" && row.state !== "in_progress") {
241
+ throw new Error("host-assets: update set " +
242
+ row.name +
243
+ " is '" +
244
+ row.state +
245
+ "' — only an in-progress update set can capture changes.");
246
+ }
247
+ return { sys_id: row.sys_id, name: row.name };
248
+ }
249
+ function scriptFields(info, scopeSysId) {
250
+ return {
251
+ name: info.name,
252
+ // Bytes live on the attachment; the script field caps at 65 KB.
253
+ script: "// Hosted front-end asset — bytes stored as a sys_attachment on this record.\n" +
254
+ "// path=" +
255
+ info.viteRelPath +
256
+ " role=" +
257
+ info.role,
258
+ active: "true",
259
+ sys_scope: scopeSysId,
260
+ };
261
+ }
262
+ function scriptUnchanged(existing, info) {
263
+ var fields = scriptFields(info, existing.sys_id);
264
+ return (existing.name === info.name &&
265
+ String(existing.active) === "true" &&
266
+ existing.script === fields.script);
267
+ }
268
+ /** Reconcile one chunk's carrier script + attachment + m2m row. */
269
+ async function reconcileChunk(opts) {
270
+ var client = opts.client;
271
+ var info = opts.chunk.info;
272
+ var data = opts.chunk.data;
273
+ var dryRun = opts.dryRun;
274
+ // ── carrier sys_ui_script ────────────────────────────────────────────────
275
+ var existingScript = opts.scriptByName[info.name];
276
+ var scriptSysId = existingScript ? existingScript.sys_id : "";
277
+ var scriptAction;
278
+ if (!existingScript) {
279
+ scriptAction = "created";
280
+ if (!dryRun) {
281
+ var created = await client.claude.createRecord({
282
+ table: SCRIPT_TABLE,
283
+ fields: scriptFields(info, opts.scopeSysId),
284
+ scope: opts.scope,
285
+ update_set_sys_id: opts.updateSetSysId,
286
+ });
287
+ scriptSysId = created.sys_id;
288
+ }
289
+ }
290
+ else if (scriptUnchanged(existingScript, info)) {
291
+ scriptAction = "unchanged";
292
+ }
293
+ else {
294
+ scriptAction = "updated";
295
+ if (!dryRun) {
296
+ await client.claude.pushWithUpdateSet({
297
+ update_set_sys_id: opts.updateSetSysId,
298
+ table: SCRIPT_TABLE,
299
+ record_sys_id: existingScript.sys_id,
300
+ fields: scriptFields(info, opts.scopeSysId),
301
+ });
302
+ }
303
+ }
304
+ // ── attachment (replace by content hash) ──────────────────────────────────
305
+ var attachmentSysId = "";
306
+ var attachmentAction = "uploaded";
307
+ if (dryRun || !scriptSysId) {
308
+ // Cannot read/write attachments without a concrete record sys_id.
309
+ attachmentAction = existingScript ? "replaced" : "uploaded";
310
+ }
311
+ else {
312
+ var newHash = sha256Hex(data);
313
+ var existingAtts = await client.attachment.listFor({
314
+ table: SCRIPT_TABLE,
315
+ sysId: scriptSysId,
316
+ });
317
+ var matching = existingAtts.filter(function (a) {
318
+ return a.hash && a.hash.toLowerCase() === newHash;
319
+ });
320
+ if (matching.length > 0) {
321
+ attachmentSysId = matching[0].sys_id;
322
+ attachmentAction = "unchanged";
323
+ // Drop any stale or duplicate attachments so exactly one carrier remains.
324
+ var extras = existingAtts.filter(function (a) {
325
+ return a.sys_id !== matching[0].sys_id;
326
+ });
327
+ for (var e = 0; e < extras.length; e += 1) {
328
+ await client.attachment.remove({ sysId: extras[e].sys_id });
329
+ }
330
+ }
331
+ else {
332
+ for (var d = 0; d < existingAtts.length; d += 1) {
333
+ await client.attachment.remove({ sysId: existingAtts[d].sys_id });
334
+ }
335
+ var uploaded = await client.attachment.upload({
336
+ table: SCRIPT_TABLE,
337
+ sysId: scriptSysId,
338
+ fileName: info.fileName,
339
+ contentType: info.contentType,
340
+ data: data,
341
+ });
342
+ attachmentSysId = uploaded.sys_id;
343
+ attachmentAction = existingAtts.length > 0 ? "replaced" : "uploaded";
344
+ }
345
+ }
346
+ // ── m2m wiring ────────────────────────────────────────────────────────────
347
+ var existingM2m = scriptSysId ? opts.m2mByScript[scriptSysId] : undefined;
348
+ var m2mSysId = existingM2m ? existingM2m.sys_id : "";
349
+ var m2mAction;
350
+ var m2mFields = {
351
+ application: opts.app,
352
+ script: scriptSysId,
353
+ chunk_role: info.role,
354
+ order: String(info.order),
355
+ sys_scope: opts.scopeSysId,
356
+ };
357
+ if (existingM2m) {
358
+ var m2mChanged = existingM2m.chunk_role !== info.role ||
359
+ String(existingM2m.order) !== String(info.order);
360
+ m2mAction = m2mChanged ? "updated" : "unchanged";
361
+ if (m2mChanged && !dryRun) {
362
+ await client.claude.pushWithUpdateSet({
363
+ update_set_sys_id: opts.updateSetSysId,
364
+ table: M2M_TABLE,
365
+ record_sys_id: existingM2m.sys_id,
366
+ fields: { chunk_role: info.role, order: String(info.order) },
367
+ });
368
+ }
369
+ }
370
+ else {
371
+ m2mAction = "created";
372
+ if (!dryRun && scriptSysId) {
373
+ var m2mCreated = await client.claude.createRecord({
374
+ table: M2M_TABLE,
375
+ fields: m2mFields,
376
+ scope: opts.scope,
377
+ update_set_sys_id: opts.updateSetSysId,
378
+ });
379
+ m2mSysId = m2mCreated.sys_id;
380
+ }
381
+ }
382
+ // ── read-back verify ──────────────────────────────────────────────────────
383
+ var verified = false;
384
+ if (!dryRun && scriptSysId) {
385
+ var back = await client.table.query(SCRIPT_TABLE, "sys_id=" + scriptSysId, 1);
386
+ var atts = await client.attachment.listFor({
387
+ table: SCRIPT_TABLE,
388
+ sysId: scriptSysId,
389
+ });
390
+ verified = back.length > 0 && atts.length > 0;
391
+ }
392
+ return {
393
+ name: info.name,
394
+ viteRelPath: info.viteRelPath,
395
+ base: info.base,
396
+ role: info.role,
397
+ order: info.order,
398
+ contentType: info.contentType,
399
+ bytes: data.length,
400
+ scriptSysId: scriptSysId,
401
+ scriptAction: scriptAction,
402
+ attachmentSysId: attachmentSysId,
403
+ attachmentAction: attachmentAction,
404
+ m2mSysId: m2mSysId,
405
+ m2mAction: m2mAction,
406
+ verified: verified,
407
+ };
408
+ }
409
+ /** Remove carriers + m2m rows for chunks no longer in the build. */
410
+ async function pruneRemoved(opts) {
411
+ var client = opts.client;
412
+ var pruned = [];
413
+ for (var i = 0; i < opts.ownedM2m.length; i += 1) {
414
+ var row = opts.ownedM2m[i];
415
+ var scriptId = String(row.script);
416
+ var script = opts.scriptById[scriptId];
417
+ var name = script ? script.name : "";
418
+ // Carrier still in the build (by name) — keep it.
419
+ if (name && opts.keepNames[name])
420
+ continue;
421
+ var scriptDeleted = false;
422
+ if (!opts.dryRun) {
423
+ // Unwire this app first.
424
+ await client.claude.deleteRecord({
425
+ table: M2M_TABLE,
426
+ sys_id: row.sys_id,
427
+ });
428
+ // Delete the carrier only if no other app's m2m still references it.
429
+ if (scriptId) {
430
+ var others = await client.table.query(M2M_TABLE, "script=" + scriptId + "^application!=" + opts.app, 1);
431
+ if (others.length === 0) {
432
+ var atts = await client.attachment.listFor({
433
+ table: SCRIPT_TABLE,
434
+ sysId: scriptId,
435
+ });
436
+ for (var a = 0; a < atts.length; a += 1) {
437
+ await client.attachment.remove({ sysId: atts[a].sys_id });
438
+ }
439
+ await client.claude.deleteRecord({
440
+ table: SCRIPT_TABLE,
441
+ sys_id: scriptId,
442
+ });
443
+ scriptDeleted = true;
444
+ }
445
+ }
446
+ }
447
+ pruned.push({
448
+ name: name || "(unknown)",
449
+ scriptSysId: scriptId,
450
+ m2mSysId: row.sys_id,
451
+ scriptDeleted: scriptDeleted,
452
+ });
453
+ }
454
+ return pruned;
455
+ }
456
+ /**
457
+ * Deploy a built dist/ to ServiceNow as app-shell carrier scripts + attachments + m2m
458
+ * wiring. Idempotent; re-running an identical dist/ reports every chunk unchanged.
459
+ */
460
+ async function hostAssets(client, params) {
461
+ if (!params.dir)
462
+ throw new Error("host-assets: dir is required.");
463
+ if (!/^[0-9a-f]{32}$/i.test(params.app || "")) {
464
+ throw new Error("host-assets: app must be a 32-character application record sys_id.");
465
+ }
466
+ if (!params.scope) {
467
+ throw new Error("host-assets: scope is required (e.g. x_cadso_app_shell).");
468
+ }
469
+ var dryRun = params.dryRun === true;
470
+ var maxBytes = typeof params.maxBytes === "number" && params.maxBytes > 0
471
+ ? params.maxBytes
472
+ : DEFAULT_MAX_CHUNK_BYTES;
473
+ var chunks = readDistChunks(params.dir);
474
+ guardOversize(chunks, maxBytes, params.allowOversize === true);
475
+ var scopeSysId = await resolveScopeSysId(client, params.scope);
476
+ var updateSetSysId = params.updateSetSysId || "";
477
+ var updateSetName = "";
478
+ if (updateSetSysId) {
479
+ var us = await fetchUpdateSet(client, updateSetSysId);
480
+ updateSetName = us.name;
481
+ }
482
+ else {
483
+ var cur = await client.claude.currentUpdateSet(params.scope);
484
+ updateSetSysId = cur.sys_id;
485
+ updateSetName = cur.name;
486
+ }
487
+ if (!updateSetSysId) {
488
+ throw new Error("host-assets: could not resolve a target update set for scope " +
489
+ params.scope +
490
+ ".");
491
+ }
492
+ // Existing state: every asset carrier (global, by name prefix) and this app's wiring.
493
+ var allCarriers = await client.table.query(SCRIPT_TABLE, "nameSTARTSWITH" + ASSET_NAME_PREFIX, 2000);
494
+ var scriptByName = {};
495
+ var scriptById = {};
496
+ allCarriers.forEach(function (s) {
497
+ scriptByName[s.name] = s;
498
+ scriptById[s.sys_id] = s;
499
+ });
500
+ var ownedM2m = await client.table.query(M2M_TABLE, "application=" + params.app, 2000);
501
+ var m2mByScript = {};
502
+ ownedM2m.forEach(function (r) {
503
+ m2mByScript[String(r.script)] = r;
504
+ });
505
+ var results = [];
506
+ for (var i = 0; i < chunks.length; i += 1) {
507
+ var r = await reconcileChunk({
508
+ client: client,
509
+ chunk: chunks[i],
510
+ app: params.app,
511
+ scope: params.scope,
512
+ scopeSysId: scopeSysId,
513
+ updateSetSysId: updateSetSysId,
514
+ dryRun: dryRun,
515
+ scriptByName: scriptByName,
516
+ m2mByScript: m2mByScript,
517
+ });
518
+ results.push(r);
519
+ }
520
+ var keepNames = {};
521
+ chunks.forEach(function (c) {
522
+ keepNames[c.info.name] = true;
523
+ });
524
+ var pruned = await pruneRemoved({
525
+ client: client,
526
+ app: params.app,
527
+ dryRun: dryRun,
528
+ keepNames: keepNames,
529
+ ownedM2m: ownedM2m,
530
+ scriptById: scriptById,
531
+ });
532
+ return {
533
+ scope: params.scope,
534
+ app: params.app,
535
+ updateSet: { sysId: updateSetSysId, name: updateSetName },
536
+ dryRun: dryRun,
537
+ chunks: results,
538
+ pruned: pruned,
539
+ };
540
+ }
541
+ /** Human-readable one-line-per-chunk summary for the CLI. */
542
+ function formatHostAssetsResult(r) {
543
+ var lines = [];
544
+ lines.push((r.dryRun ? "[dry-run] " : "") +
545
+ "host-assets → scope=" +
546
+ r.scope +
547
+ " app=" +
548
+ r.app +
549
+ " update-set=" +
550
+ r.updateSet.name +
551
+ " (" +
552
+ r.chunks.length +
553
+ " chunks)");
554
+ r.chunks.forEach(function (c) {
555
+ lines.push(" [" +
556
+ c.role +
557
+ "] " +
558
+ c.viteRelPath +
559
+ " — script:" +
560
+ c.scriptAction +
561
+ " attach:" +
562
+ c.attachmentAction +
563
+ " m2m:" +
564
+ c.m2mAction +
565
+ " (" +
566
+ c.bytes +
567
+ "B)" +
568
+ (r.dryRun ? "" : c.verified ? " ✓" : " ⚠ unverified"));
569
+ });
570
+ if (r.pruned.length > 0) {
571
+ lines.push(" pruned: " +
572
+ r.pruned
573
+ .map(function (p) {
574
+ return p.name + (p.scriptDeleted ? "" : " (m2m only)");
575
+ })
576
+ .join(", "));
577
+ }
578
+ return lines.join("\n");
579
+ }
package/dist/index.d.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  */
7
7
  export { createClient } from "./client";
8
8
  export { createClientFromEnvFile, resolveConfigFromEnvFile } from "./createClientFromEnvFile";
9
- export type { ServiceNowClient, TableQueryOptions, TableSchema, TableSchemaField } from "./client";
9
+ export type { ServiceNowClient, TableQueryOptions, TableSchema, TableSchemaField, AttachmentMeta } from "./client";
10
10
  export { addChoicesToField } from "./choices";
11
+ export { hostAssets, classifyChunks, formatHostAssetsResult } from "./hostAssets";
11
12
  export { formatAddChoicesResult } from "./formatter";
12
13
  export { createView } from "./layout/views";
13
14
  export { setListLayout } from "./layout/listLayout";
@@ -17,6 +18,6 @@ export { formatLayoutResult, formatCreateViewResult } from "./layout/formatter";
17
18
  export { sincPlugin } from "./plugin";
18
19
  export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, triggerPublication, publishActionType, editActionType, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError } from "./flowDesigner";
19
20
  export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, EditActionTypeParams, EditActionTypeResult, EditActionTypeOps, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, CreateFlowParams, CreateFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult } from "./flowDesigner";
20
- export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams } from "./types";
21
+ export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult } from "./types";
21
22
  export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, applyAddColumnOverlay } from "./table";
22
23
  export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec } from "./table";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * REST API so every change lands in the target update set and scope.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.applyAddColumnOverlay = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
9
+ exports.applyAddColumnOverlay = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
10
10
  var client_1 = require("./client");
11
11
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
12
12
  var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
@@ -14,6 +14,10 @@ Object.defineProperty(exports, "createClientFromEnvFile", { enumerable: true, ge
14
14
  Object.defineProperty(exports, "resolveConfigFromEnvFile", { enumerable: true, get: function () { return createClientFromEnvFile_1.resolveConfigFromEnvFile; } });
15
15
  var choices_1 = require("./choices");
16
16
  Object.defineProperty(exports, "addChoicesToField", { enumerable: true, get: function () { return choices_1.addChoicesToField; } });
17
+ var hostAssets_1 = require("./hostAssets");
18
+ Object.defineProperty(exports, "hostAssets", { enumerable: true, get: function () { return hostAssets_1.hostAssets; } });
19
+ Object.defineProperty(exports, "classifyChunks", { enumerable: true, get: function () { return hostAssets_1.classifyChunks; } });
20
+ Object.defineProperty(exports, "formatHostAssetsResult", { enumerable: true, get: function () { return hostAssets_1.formatHostAssetsResult; } });
17
21
  var formatter_1 = require("./formatter");
18
22
  Object.defineProperty(exports, "formatAddChoicesResult", { enumerable: true, get: function () { return formatter_1.formatAddChoicesResult; } });
19
23
  var views_1 = require("./layout/views");
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
12
12
  import type { ServiceNowClient } from "../client";
13
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "host_assets"];
14
14
  export type ToolName = typeof TOOL_NAMES[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -26,6 +26,7 @@ const createFlow_1 = require("../flowDesigner/createFlow");
26
26
  const editFlow_1 = require("../flowDesigner/editFlow");
27
27
  const testFlow_1 = require("../flowDesigner/testFlow");
28
28
  const table_1 = require("../table");
29
+ const hostAssets_1 = require("../hostAssets");
29
30
  const schemas_1 = require("./schemas");
30
31
  exports.TOOL_NAMES = [
31
32
  "create_view",
@@ -41,7 +42,8 @@ exports.TOOL_NAMES = [
41
42
  "flow_test",
42
43
  "flow_edit",
43
44
  "create_table",
44
- "add_column"
45
+ "add_column",
46
+ "host_assets"
45
47
  ];
46
48
  // Annotation presets (READ_ONLY / WRITE_ADDITIVE_IDEMPOTENT / WRITE_CREATE /
47
49
  // WRITE_OVERWRITE / WRITE_EXECUTE) come from @tenonhq/dovetail-mcp-kit.
@@ -293,6 +295,24 @@ function buildDescriptors(deps = {}) {
293
295
  debug: p.debug
294
296
  });
295
297
  }
298
+ },
299
+ {
300
+ name: "host_assets",
301
+ annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
302
+ description: "Deploy a pre-built front-end dist/ bundle to ServiceNow. For each chunk (index.html + "
303
+ + "assets/*.{js,css}) upserts a carrier sys_ui_script named app_shell_asset:<vite-relative-path> "
304
+ + "(the rotating hash is part of the name on purpose — the Scripted REST serving resource resolves "
305
+ + "an asset by this exact name), stores the chunk bytes as a sys_attachment (the script field caps "
306
+ + "at 65 KB), and wires an x_cadso_app_shell_m2m_app_script row (application, script, chunk_role, "
307
+ + "order). PRUNES carriers + m2m rows for chunks no longer in the build (hashes rotate per build). "
308
+ + "Idempotent — identical bytes (by SHA-256) are left in place. Fails fast on any chunk at/over the "
309
+ + "~5 MB serve cap (glide.scriptable.excel.max_file_size) unless allowOversize. app is the application "
310
+ + "record sys_id; dir is a local dist path on the server running this tool; script + m2m writes are "
311
+ + "captured in the update set; dryRun previews without writing.",
312
+ shape: schemas_1.hostAssetsSchema.shape,
313
+ handler: async function (args) {
314
+ return (0, hostAssets_1.hostAssets)(client(), schemas_1.hostAssetsSchema.parse(args));
315
+ }
296
316
  }
297
317
  ];
298
318
  }
@@ -382,6 +382,31 @@ export declare var editFlowSchema: z.ZodObject<{
382
382
  updateSetSysId?: string | undefined;
383
383
  apply?: boolean | undefined;
384
384
  }>;
385
+ export declare var hostAssetsSchema: z.ZodObject<{
386
+ dir: z.ZodString;
387
+ app: z.ZodString;
388
+ scope: z.ZodString;
389
+ updateSetSysId: z.ZodOptional<z.ZodString>;
390
+ maxBytes: z.ZodOptional<z.ZodNumber>;
391
+ allowOversize: z.ZodOptional<z.ZodBoolean>;
392
+ dryRun: z.ZodOptional<z.ZodBoolean>;
393
+ }, "strip", z.ZodTypeAny, {
394
+ dir: string;
395
+ scope: string;
396
+ app: string;
397
+ dryRun?: boolean | undefined;
398
+ updateSetSysId?: string | undefined;
399
+ maxBytes?: number | undefined;
400
+ allowOversize?: boolean | undefined;
401
+ }, {
402
+ dir: string;
403
+ scope: string;
404
+ app: string;
405
+ dryRun?: boolean | undefined;
406
+ updateSetSysId?: string | undefined;
407
+ maxBytes?: number | undefined;
408
+ allowOversize?: boolean | undefined;
409
+ }>;
385
410
  export declare var columnSpecSchema: z.ZodObject<{
386
411
  label: z.ZodString;
387
412
  type: z.ZodString;
@@ -4,7 +4,7 @@
4
4
  * own file so registry.ts stays focused on wiring.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
8
8
  const zod_1 = require("zod");
9
9
  exports.createViewSchema = zod_1.z.object({
10
10
  name: zod_1.z.string().min(1),
@@ -113,6 +113,15 @@ exports.editFlowSchema = zod_1.z.object({
113
113
  scopeSysId: zod_1.z.string().optional(),
114
114
  updateSetSysId: zod_1.z.string().optional()
115
115
  });
116
+ exports.hostAssetsSchema = zod_1.z.object({
117
+ dir: zod_1.z.string().min(1),
118
+ app: zod_1.z.string().min(1),
119
+ scope: zod_1.z.string().min(1),
120
+ updateSetSysId: zod_1.z.string().min(1).optional(),
121
+ maxBytes: zod_1.z.number().int().positive().optional(),
122
+ allowOversize: zod_1.z.boolean().optional(),
123
+ dryRun: zod_1.z.boolean().optional()
124
+ });
116
125
  exports.columnSpecSchema = zod_1.z.object({
117
126
  label: zod_1.z.string().min(1),
118
127
  type: zod_1.z.string().min(1),
package/dist/types.d.ts CHANGED
@@ -179,3 +179,84 @@ export interface SetRelatedListsParams {
179
179
  /** Plan the writes without performing them. Defaults to false. */
180
180
  dryRun?: boolean;
181
181
  }
182
+ /** Role of a built chunk, inferred from its base name. Drives m2m chunk_role + load order. */
183
+ export type ChunkRole = "index" | "entry" | "vendor" | "router" | "state" | "lazy" | "style";
184
+ /**
185
+ * A classified build chunk — pure metadata, derived before any bytes are read.
186
+ * The carrier sys_ui_script is named `app_shell_asset:<viteRelPath>` (hash included),
187
+ * because the Scripted REST serving resource resolves an asset by that exact name.
188
+ */
189
+ export interface ChunkInfo {
190
+ /** Dist-relative path with forward slashes, e.g. "assets/index-a1b2c3d4.js" or "index.html". */
191
+ viteRelPath: string;
192
+ /** Carrier sys_ui_script name: "app_shell_asset:" + viteRelPath. Stable per build, rotates with the hash. */
193
+ name: string;
194
+ /** Filename only, e.g. "index-a1b2c3d4.js". */
195
+ fileName: string;
196
+ /** Hash-stripped base used only for role inference, e.g. "index". */
197
+ base: string;
198
+ /** Lowercased extension without the dot, e.g. "js". */
199
+ ext: string;
200
+ role: ChunkRole;
201
+ /** Gap-free load order across all chunks (ascending). */
202
+ order: number;
203
+ /** MIME type written to the attachment. */
204
+ contentType: string;
205
+ }
206
+ export interface HostAssetsParams {
207
+ /** Path to the pre-built dist/ directory. */
208
+ dir: string;
209
+ /** Application record sys_id (32 hex) — written verbatim to the m2m `application` field. */
210
+ app: string;
211
+ /** Scope namespace the carrier records live in, e.g. "x_cadso_app_shell". */
212
+ scope: string;
213
+ /** Update set sys_id that captures every metadata write. Resolved from the scope's current update set when omitted. */
214
+ updateSetSysId?: string;
215
+ /**
216
+ * Max bytes per chunk. The serving layer streams each chunk via
217
+ * GlideSysAttachment.getContentStream(), capped by glide.scriptable.excel.max_file_size
218
+ * (~5 MB) — a larger chunk truncates on serve. Defaults to 5 * 1024 * 1024.
219
+ */
220
+ maxBytes?: number;
221
+ /** Warn instead of failing when a chunk meets/exceeds maxBytes. Defaults to false (fail). */
222
+ allowOversize?: boolean;
223
+ /** Plan the deploy (read + classify) without writing/uploading/pruning. Defaults to false. */
224
+ dryRun?: boolean;
225
+ }
226
+ export interface ChunkResult {
227
+ /** Carrier sys_ui_script name ("app_shell_asset:<viteRelPath>"). */
228
+ name: string;
229
+ viteRelPath: string;
230
+ base: string;
231
+ role: ChunkRole;
232
+ order: number;
233
+ contentType: string;
234
+ bytes: number;
235
+ scriptSysId: string;
236
+ scriptAction: "created" | "updated" | "unchanged";
237
+ attachmentSysId: string;
238
+ attachmentAction: "uploaded" | "replaced" | "unchanged";
239
+ m2mSysId: string;
240
+ m2mAction: "created" | "updated" | "unchanged";
241
+ /** True once the script record + an attachment were read back from the instance. */
242
+ verified: boolean;
243
+ }
244
+ export interface PrunedResult {
245
+ /** Carrier sys_ui_script name that was removed (or planned for removal). */
246
+ name: string;
247
+ scriptSysId: string;
248
+ m2mSysId: string;
249
+ /** False when the carrier script was kept because another app's m2m still references it. */
250
+ scriptDeleted: boolean;
251
+ }
252
+ export interface HostAssetsResult {
253
+ scope: string;
254
+ app: string;
255
+ updateSet: {
256
+ sysId: string;
257
+ name: string;
258
+ };
259
+ dryRun: boolean;
260
+ chunks: Array<ChunkResult>;
261
+ pruned: Array<PrunedResult>;
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },