@rex0220/print-craft-authoring-tools 1.2.0 → 1.3.0

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.
Files changed (41) hide show
  1. package/README.md +2 -1
  2. package/SECURITY.md +2 -2
  3. package/dist/cli.mjs +2 -2
  4. package/dist/core.mjs +3422 -0
  5. package/dist/types/_print-craft/src/authoring/api.d.ts +3 -0
  6. package/dist/types/_print-craft/src/config/load.d.ts +4 -0
  7. package/dist/types/_print-craft/src/config/schema.d.ts +5 -0
  8. package/dist/types/commands/buttons.d.ts +19 -0
  9. package/dist/types/commands/diff.d.ts +6 -0
  10. package/dist/types/commands/fields.d.ts +54 -0
  11. package/dist/types/commands/normalize.d.ts +40 -0
  12. package/dist/types/commands/preview.d.ts +39 -0
  13. package/dist/types/commands/pull.d.ts +36 -0
  14. package/dist/types/commands/record.d.ts +63 -0
  15. package/dist/types/commands/records.d.ts +36 -0
  16. package/dist/types/commands/save.d.ts +50 -0
  17. package/dist/types/commands/take.d.ts +19 -0
  18. package/dist/types/commit-file.d.ts +38 -0
  19. package/dist/types/context.d.ts +10 -0
  20. package/dist/types/core.d.ts +32 -0
  21. package/dist/types/engine.d.ts +64 -0
  22. package/dist/types/env.d.ts +50 -0
  23. package/dist/types/kintone-rest.d.ts +65 -0
  24. package/dist/types/kintone-url.d.ts +11 -0
  25. package/dist/types/meta.d.ts +30 -0
  26. package/dist/types/normalize/checks.d.ts +58 -0
  27. package/dist/types/normalize/css-check.d.ts +53 -0
  28. package/dist/types/normalize/derive.d.ts +30 -0
  29. package/dist/types/normalize/findings.d.ts +21 -0
  30. package/dist/types/normalize/html-check.d.ts +23 -0
  31. package/dist/types/normalize/line-diff.d.ts +8 -0
  32. package/dist/types/normalize/model.d.ts +36 -0
  33. package/dist/types/normalize/policy.d.ts +29 -0
  34. package/dist/types/normalize/size.d.ts +16 -0
  35. package/dist/types/paths.d.ts +7 -0
  36. package/dist/types/permission.d.ts +21 -0
  37. package/dist/types/plugin-zip.d.ts +53 -0
  38. package/dist/types/preview/render.d.ts +82 -0
  39. package/dist/types/safe-path.d.ts +24 -0
  40. package/dist/types/workspace.d.ts +84 -0
  41. package/package.json +10 -2
package/dist/core.mjs ADDED
@@ -0,0 +1,3422 @@
1
+ /*! @rex0220/print-craft-authoring-tools/core 1.3.0 (c) rex0220. MIT License. 計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む(このファイルには含まれない)。commit 5abbe71. */
2
+
3
+ // src/safe-path.ts
4
+ import { lstatSync, realpathSync } from "node:fs";
5
+ import path from "node:path";
6
+ var PathError = class extends Error {
7
+ };
8
+ var WRITE_ROOTS = {
9
+ fields: ["fields", "kintone"],
10
+ records: ["records", "kintone"],
11
+ settings: ["settings", "temp", "kintone"],
12
+ out: ["out", "kintone"],
13
+ kintone: ["kintone"]
14
+ };
15
+ var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
16
+ function norm(p) {
17
+ const r = path.resolve(p);
18
+ return process.platform === "win32" ? r.toLowerCase() : r;
19
+ }
20
+ function realResolve(target, cwd) {
21
+ const abs = path.resolve(cwd, target);
22
+ let dir = abs;
23
+ const rest = [];
24
+ for (; ; ) {
25
+ try {
26
+ lstatSync(dir);
27
+ break;
28
+ } catch (e) {
29
+ const code = e.code;
30
+ if (code === "ENOENT" || code === "ENOTDIR") {
31
+ const parent = path.dirname(dir);
32
+ if (parent === dir) throw new PathError(`パスを解決できない: ${target}`);
33
+ rest.unshift(path.basename(dir));
34
+ dir = parent;
35
+ continue;
36
+ }
37
+ throw new PathError(`パスを解決できない(アクセスできない: ${code ?? "不明"}): ${target}`);
38
+ }
39
+ }
40
+ let real;
41
+ try {
42
+ real = realpathSync.native(dir);
43
+ } catch (e) {
44
+ throw new PathError(`パスを解決できない(リンク切れ・ループ・アクセスできない場所: ${e.code ?? "不明"}): ${target}`);
45
+ }
46
+ return rest.length ? path.join(real, ...rest) : real;
47
+ }
48
+ function isInside(child, parent) {
49
+ const c = norm(child);
50
+ const p = norm(parent);
51
+ return c === p || c.startsWith(p.endsWith(path.sep) ? p : p + path.sep);
52
+ }
53
+ function resolveRead(target, cwd) {
54
+ if (!target || typeof target !== "string") throw new PathError("ファイルのパスが要る");
55
+ const real = realResolve(target, cwd);
56
+ const cwdReal = realResolve(".", cwd);
57
+ if (!isInside(real, cwdReal)) throw new PathError(`読むファイルは作業フォルダーの中に置く: ${target}`);
58
+ const rel2 = path.relative(cwdReal, real).replace(/\\/g, "/");
59
+ if (/^\.env(\.|$)/i.test(path.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(rel2)) throw new PathError(`このファイルは tools では読まない: ${target}`);
60
+ return real;
61
+ }
62
+ function resolveWrite(target, roots, cwd) {
63
+ if (!target || typeof target !== "string") throw new PathError("書き込み先のパスが要る");
64
+ const real = realResolve(target, cwd);
65
+ const cwdReal = realResolve(".", cwd);
66
+ if (!isInside(real, cwdReal)) throw new PathError(`書き込み先は作業フォルダーの中: ${target}`);
67
+ const ok = roots.some((r) => {
68
+ const root = path.join(cwdReal, r);
69
+ return isInside(real, root) && norm(real) !== norm(root);
70
+ });
71
+ if (!ok) throw new PathError(`書き込み先は ${roots.map((r) => `${r}/`).join(" か ")} の下: ${target}`);
72
+ const base = path.basename(real);
73
+ if (WINDOWS_RESERVED.test(base) || /[. ]$/.test(base)) throw new PathError(`使えないファイル名: ${base}`);
74
+ return real;
75
+ }
76
+ function safeFileName(name, fallback) {
77
+ let s = name.replace(/[\\/:*?"<>|\u0000-\u001f\u007f]/g, "_").replace(/[. ]+$/g, "").trim();
78
+ if (!s || WINDOWS_RESERVED.test(s)) s = fallback;
79
+ return s.length > 120 ? s.slice(0, 120) : s;
80
+ }
81
+
82
+ // src/context.ts
83
+ function createContext(opt) {
84
+ return Object.freeze({ root: realResolve(".", opt.cwd), env: Object.freeze({ ...opt.env }) });
85
+ }
86
+
87
+ // src/permission.ts
88
+ import { existsSync as existsSync2 } from "node:fs";
89
+ import path3 from "node:path";
90
+
91
+ // src/workspace.ts
92
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
93
+ import path2 from "node:path";
94
+
95
+ // src/kintone-url.ts
96
+ var KINTONE_HOST_SUFFIXES = [".cybozu.com", ".kintone.com", ".cybozu.cn"];
97
+ var KintoneUrlError = class extends Error {
98
+ };
99
+ function normalizeKintoneBaseUrl(raw) {
100
+ const text = String(raw ?? "").trim();
101
+ let u;
102
+ try {
103
+ u = new URL(text);
104
+ } catch {
105
+ throw new KintoneUrlError(`kintone の URL として読めない: ${text.slice(0, 80)}`);
106
+ }
107
+ if (u.protocol !== "https:") throw new KintoneUrlError("kintone の URL は https で始める");
108
+ if (u.username || u.password) throw new KintoneUrlError("kintone の URL にユーザー名やパスワード(@)を入れない");
109
+ if (u.port) throw new KintoneUrlError("kintone の URL にポートを付けない");
110
+ if (u.search || u.hash) throw new KintoneUrlError("kintone の URL にクエリや # を付けない");
111
+ if (u.pathname !== "/" && u.pathname !== "") throw new KintoneUrlError("kintone の URL にパスを付けない(https://<サブドメイン>.cybozu.com だけ)");
112
+ const host = u.hostname.toLowerCase();
113
+ const wellFormed = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host);
114
+ const suffix = KINTONE_HOST_SUFFIXES.find((s) => host.endsWith(s) && host.length > s.length);
115
+ if (!wellFormed || !suffix) {
116
+ throw new KintoneUrlError(`kintone のドメインではない: ${host}(使えるのは ${KINTONE_HOST_SUFFIXES.map((s) => `*${s}`).join(" / ")})`);
117
+ }
118
+ return `https://${host}`;
119
+ }
120
+ function isKintoneBaseUrl(raw) {
121
+ try {
122
+ normalizeKintoneBaseUrl(raw);
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ // src/workspace.ts
130
+ var WORKSPACE_FILE = "environments.json";
131
+ var KINTONE_ROOT = "kintone";
132
+ var INBOX = "inbox";
133
+ var SNAPSHOT_PREFIX = "rex0220-print-craft";
134
+ var SNAPSHOT_RE = /^rex0220-print-craft-app(\d+)-(\d{8})-(\d{6})\.json$/;
135
+ var EDIT_RE = /^rex0220-print-craft-app(\d+)-(\d{8})-(\d{6})-edit\.json$/;
136
+ var ENV_NAME = /^[A-Za-z0-9_-]{1,32}$/;
137
+ var ENV_FILE = /^(\.env|env\/[A-Za-z0-9._-]{1,64}\.env)$/;
138
+ var MAX_BYTES = 256 * 1024;
139
+ var WorkspaceError = class extends Error {
140
+ };
141
+ var ENV_ROLES = ["development", "production"];
142
+ var isEnvName = (s) => ENV_NAME.test(s);
143
+ function parseWorkspace(text, where = WORKSPACE_FILE) {
144
+ let raw;
145
+ try {
146
+ raw = JSON.parse(text);
147
+ } catch (e) {
148
+ throw new WorkspaceError(`${where} を JSON として読めない: ${e.message}`);
149
+ }
150
+ const obj = (v, at) => {
151
+ if (!v || typeof v !== "object" || Array.isArray(v)) throw new WorkspaceError(`${where}: ${at} はオブジェクト`);
152
+ return v;
153
+ };
154
+ const root = obj(raw, "最上位");
155
+ for (const k of Object.keys(root)) if (!["default", "environments", "apps"].includes(k)) throw new WorkspaceError(`${where}: 使えるキーは default / environments / apps: ${k}`);
156
+ const envsRaw = obj(root.environments, "environments");
157
+ const environments = {};
158
+ for (const [name, v] of Object.entries(envsRaw)) {
159
+ if (!isEnvName(name)) throw new WorkspaceError(`${where}: 環境の名前は英数字と _ - の 32 文字まで: ${name}`);
160
+ const e = obj(v, `environments.${name}`);
161
+ for (const k of Object.keys(e)) if (!["baseUrl", "envFile", "role"].includes(k)) throw new WorkspaceError(`${where}: environments.${name} に使えるキーは baseUrl / envFile / role: ${k}`);
162
+ if (e.role !== void 0 && !ENV_ROLES.includes(e.role)) throw new WorkspaceError(`${where}: environments.${name}.role は development(開発)か production(本番): ${String(e.role)}`);
163
+ const role = e.role === void 0 ? "unclassified" : e.role;
164
+ let baseUrl;
165
+ try {
166
+ baseUrl = normalizeKintoneBaseUrl(String(e.baseUrl ?? ""));
167
+ } catch (err) {
168
+ throw new WorkspaceError(`${where}: environments.${name}.baseUrl が不正: ${err instanceof KintoneUrlError ? err.message : String(err)}`);
169
+ }
170
+ const envFile = e.envFile === void 0 ? ".env" : String(e.envFile).replace(/\\/g, "/");
171
+ if (!ENV_FILE.test(envFile)) throw new WorkspaceError(`${where}: environments.${name}.envFile は .env か env/<名前>.env: ${envFile}`);
172
+ environments[name] = { name, role, baseUrl, host: new URL(baseUrl).hostname, envFile };
173
+ }
174
+ if (Object.keys(environments).length === 0) throw new WorkspaceError(`${where}: environments に環境が 1 つも無い`);
175
+ const defaultEnv = root.default === void 0 ? Object.keys(environments)[0] : String(root.default);
176
+ if (!environments[defaultEnv]) throw new WorkspaceError(`${where}: default の環境が environments に無い: ${defaultEnv}`);
177
+ const apps = [];
178
+ if (root.apps !== void 0) {
179
+ if (!Array.isArray(root.apps)) throw new WorkspaceError(`${where}: apps は配列`);
180
+ const names = /* @__PURE__ */ new Set();
181
+ for (const [i, v] of root.apps.entries()) {
182
+ const a = obj(v, `apps[${i}]`);
183
+ const name = typeof a.name === "string" ? a.name.trim() : "";
184
+ if (!name || name.length > 100 || /^\d+$/.test(name)) throw new WorkspaceError(`${where}: apps[${i}].name は 100 文字までの名前(数字だけは不可)`);
185
+ if (names.has(name)) throw new WorkspaceError(`${where}: apps の名前が重複: ${name}`);
186
+ names.add(name);
187
+ const ids = {};
188
+ for (const [k, n] of Object.entries(a)) {
189
+ if (k === "name") continue;
190
+ if (!environments[k]) throw new WorkspaceError(`${where}: apps[${i}] の ${k} は environments に無い環境`);
191
+ if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) throw new WorkspaceError(`${where}: apps[${i}].${k} はアプリ番号(正の整数)`);
192
+ ids[k] = n;
193
+ }
194
+ apps.push({ name, ids });
195
+ }
196
+ }
197
+ return { defaultEnv, environments, apps };
198
+ }
199
+ function loadWorkspace(cwd) {
200
+ const file = path2.join(cwd, WORKSPACE_FILE);
201
+ if (!existsSync(file)) return null;
202
+ const size = statSync(file).size;
203
+ if (size > MAX_BYTES) throw new WorkspaceError(`${WORKSPACE_FILE} が大きすぎる(${size} バイト)`);
204
+ return parseWorkspace(readFileSync(file, "utf8"));
205
+ }
206
+ function pickEnv(ws, name) {
207
+ const n = name ?? ws.defaultEnv;
208
+ const e = ws.environments[n];
209
+ if (!e) throw new WorkspaceError(`環境「${n}」は ${WORKSPACE_FILE} に無い(${Object.keys(ws.environments).join(", ")})`);
210
+ return e;
211
+ }
212
+ function resolveApp(ws, env, app) {
213
+ if (/^\d+$/.test(app)) {
214
+ const appId2 = Number(app);
215
+ const def2 = ws.apps.find((a) => a.ids[env.name] === appId2);
216
+ return { appId: appId2, ...def2 ? { name: def2.name } : {} };
217
+ }
218
+ const def = ws.apps.find((a) => a.name === app);
219
+ if (!def) throw new WorkspaceError(`アプリ「${app}」は ${WORKSPACE_FILE} の apps に無い(番号で書くか、apps に足す)`);
220
+ const appId = def.ids[env.name];
221
+ if (!appId) throw new WorkspaceError(`アプリ「${app}」の環境「${env.name}」の番号が ${WORKSPACE_FILE} に無い`);
222
+ return { appId, name: def.name };
223
+ }
224
+ function folderNameOf(appId, appName) {
225
+ const name = safeFileName(appName, "");
226
+ return name ? `${appId}-${name}` : String(appId);
227
+ }
228
+ function hostDirOf(cwd, env) {
229
+ return path2.join(cwd, KINTONE_ROOT, env.host);
230
+ }
231
+ function findAppDir(cwd, env, appId) {
232
+ const hostDir = hostDirOf(cwd, env);
233
+ if (!existsSync(hostDir)) return null;
234
+ const hits = readdirSync(hostDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === String(appId) || d.name.startsWith(`${appId}-`))).map((d) => d.name);
235
+ if (hits.length > 1) throw new WorkspaceError(`アプリ ${appId} のフォルダーが 2 つある: ${hits.join(", ")}(kintone/${env.host}/ の下を 1 つにする)`);
236
+ return hits.length ? path2.join(hostDir, hits[0]) : null;
237
+ }
238
+ function appDirFor(cwd, env, appId, appName) {
239
+ return findAppDir(cwd, env, appId) ?? path2.join(hostDirOf(cwd, env), folderNameOf(appId, appName));
240
+ }
241
+ function appFolderOfFile(cwd, file) {
242
+ const rel2 = path2.relative(path2.join(cwd, KINTONE_ROOT), path2.resolve(cwd, file)).replace(/\\/g, "/");
243
+ if (!rel2 || rel2.startsWith("..")) return null;
244
+ const [host, folder, ...rest] = rel2.split("/");
245
+ if (!host || !folder || rest.length === 0) return null;
246
+ const m = folder.match(/^(\d+)(?:-|$)/);
247
+ if (!m) return null;
248
+ return { dir: path2.join(cwd, KINTONE_ROOT, host, folder), host, appId: Number(m[1]) };
249
+ }
250
+ var pad = (n) => String(n).padStart(2, "0");
251
+ function snapshotNameOf(appId, now = /* @__PURE__ */ new Date()) {
252
+ return `${SNAPSHOT_PREFIX}-app${appId}-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.json`;
253
+ }
254
+ function editNameOf(snapshotFile) {
255
+ return snapshotFile.replace(/\.json$/, "-edit.json");
256
+ }
257
+ var stampOf = (name) => {
258
+ const m = name.match(/-(\d{8})-(\d{6})(?:-edit)?\.json$/);
259
+ return m ? `${m[1]}${m[2]}` : "";
260
+ };
261
+ var newestFirst = (a, b) => stampOf(b).localeCompare(stampOf(a)) || b.localeCompare(a);
262
+ function listAppFolder(dir) {
263
+ const files = existsSync(dir) ? readdirSync(dir, { withFileTypes: true }) : [];
264
+ const names = files.filter((d) => d.isFile() && d.name.endsWith(".json")).map((d) => d.name);
265
+ const sub = (name, filter) => {
266
+ const p = path2.join(dir, name);
267
+ return existsSync(p) ? readdirSync(p).filter(filter).sort() : [];
268
+ };
269
+ return {
270
+ dir,
271
+ hasFields: names.includes("fields.json"),
272
+ snapshots: names.filter((n) => SNAPSHOT_RE.test(n)).sort(newestFirst),
273
+ edits: names.filter((n) => EDIT_RE.test(n)).sort(newestFirst),
274
+ reports: names.filter((n) => n !== "fields.json" && !SNAPSHOT_RE.test(n) && !EDIT_RE.test(n)).sort(),
275
+ records: sub("records", (n) => n.endsWith(".json")),
276
+ outs: sub("out", (n) => n.endsWith(".html")).length
277
+ };
278
+ }
279
+ function envsOfHost(ws, host) {
280
+ return Object.values(ws.environments).filter((e) => e.host === host);
281
+ }
282
+ function envOfAppFolder(ws, host, appId) {
283
+ const hits = envsOfHost(ws, host);
284
+ if (hits.length === 1) return hits[0];
285
+ const byApp = hits.filter((e) => ws.apps.some((a) => a.ids[e.name] === appId));
286
+ return byApp.length === 1 ? byApp[0] : void 0;
287
+ }
288
+
289
+ // src/permission.ts
290
+ var PermissionError = class extends Error {
291
+ };
292
+ var ALLOWED_CLASSES = {
293
+ settings: ["edit", "report"],
294
+ snapshot: ["snapshot"],
295
+ fields: ["fields"],
296
+ record: ["record"],
297
+ preview: ["out"]
298
+ };
299
+ var OP_LABEL = {
300
+ settings: "設定(-edit.json と新しい帳票)",
301
+ snapshot: "新しい名前のダウンロード / pull",
302
+ fields: "fields.json",
303
+ record: "records/ の下",
304
+ preview: "out/ の下"
305
+ };
306
+ function classOfAppPath(relInFolder) {
307
+ const rel2 = relInFolder.replace(/\\/g, "/");
308
+ if (rel2.startsWith("records/")) return rel2.toLowerCase().endsWith(".json") ? "record" : "other";
309
+ if (rel2.startsWith("out/")) return "out";
310
+ if (rel2.includes("/")) return "other";
311
+ if (rel2 === "fields.json") return "fields";
312
+ if (SNAPSHOT_RE.test(rel2)) return "snapshot";
313
+ if (EDIT_RE.test(rel2)) return "edit";
314
+ if (rel2.toLowerCase().endsWith(".json")) return "report";
315
+ return "other";
316
+ }
317
+ function roleOfTarget(root, file) {
318
+ const abs = path3.resolve(root, file);
319
+ if (!isInside(abs, path3.join(root, KINTONE_ROOT))) return null;
320
+ const ws = loadWorkspace(root);
321
+ const folder = appFolderOfFile(root, abs);
322
+ const cls = folder ? classOfAppPath(path3.relative(folder.dir, abs)) : void 0;
323
+ if (!ws) return { role: "unclassified", why: `${WORKSPACE_FILE} が無い`, cls };
324
+ if (!folder) return { role: "unclassified", why: "アプリのフォルダー(kintone/<ホスト名>/<番号>-…/)の中でない" };
325
+ const env = envOfAppFolder(ws, folder.host, folder.appId);
326
+ if (!env) return { role: "unclassified", why: `フォルダー(${folder.host} のアプリ ${folder.appId})がどの環境のものか ${WORKSPACE_FILE} から決まらない(同じホストの環境が 2 つ以上あるときは、apps にアプリの番号を足す)`, cls };
327
+ return env.role === "unclassified" ? { role: "unclassified", envName: env.name, why: `環境「${env.name}」に role が無い`, cls } : { role: env.role, envName: env.name, cls };
328
+ }
329
+ function assertChangeAllowed(root, file, op) {
330
+ const target = roleOfTarget(root, file);
331
+ if (!target) return;
332
+ const abs = path3.resolve(root, file);
333
+ if (target.role === "unclassified") {
334
+ throw new PermissionError(`${target.why}ので kintone/ の下を変更できない。${WORKSPACE_FILE} の環境に role(development か production)を書く(利用者が書く)`);
335
+ }
336
+ if (target.role === "production" && op !== "snapshot") {
337
+ throw new PermissionError(`環境「${target.envName}」は本番(role: production)なので変更できない。開発の環境で作る・直す(本番のフォルダーは pull / ダウンロードで今の設定を見るだけ)`);
338
+ }
339
+ const cls = target.cls ?? "other";
340
+ if (cls === "snapshot" && existsSync2(abs)) {
341
+ throw new PermissionError(`ダウンロード / pull のファイルは書き換えない: ${path3.relative(root, abs)}。edit で -edit.json に写して直す`);
342
+ }
343
+ if (!ALLOWED_CLASSES[op].includes(cls)) {
344
+ throw new PermissionError(`この操作でアプリのフォルダーに書けるのは ${OP_LABEL[op]} だけ: ${path3.relative(root, abs)}`);
345
+ }
346
+ }
347
+
348
+ // src/meta.ts
349
+ import { execFileSync } from "node:child_process";
350
+ import { readFileSync as readFileSync2 } from "node:fs";
351
+ import path5 from "node:path";
352
+
353
+ // src/paths.ts
354
+ import path4 from "node:path";
355
+ import { fileURLToPath } from "node:url";
356
+ var here = path4.dirname(fileURLToPath(import.meta.url));
357
+ var AUTHORING_ROOT = path4.resolve(here, "..");
358
+ var REPO_ROOT = path4.resolve(AUTHORING_ROOT, "..");
359
+ var VENDOR_DIR = path4.join(AUTHORING_ROOT, "vendor");
360
+ var MOMENT_FILE = path4.join(VENDOR_DIR, "moment-with-locales.min.js");
361
+
362
+ // src/meta.ts
363
+ var PRINT_CRAFT_PLUGIN_ID = "lcapkanpjdabgphknkabojmcfhonhkhp";
364
+ var MIN_PLUGIN_VERSION = 6;
365
+ var SUPPORTED_API_VERSIONS = [1, 2];
366
+ function isSupportedPluginVersion(version) {
367
+ return /^[1-9]\d{0,5}$/.test(version) && Number(version) >= MIN_PLUGIN_VERSION;
368
+ }
369
+ var REQUIRED_API = {
370
+ apiVersion: "number",
371
+ pluginVersion: "string",
372
+ pluginId: "string",
373
+ CONFIG_SCHEMA: "object",
374
+ CONFIG_LIMITS: "object",
375
+ PAGE_SIZES: "object",
376
+ DPI_OPTIONS: "object",
377
+ PAPER_NAMES: "object",
378
+ PRINT_MODES: "object",
379
+ buildMenuRows: "function",
380
+ normalizeTagsInfo: "function",
381
+ normalizeCssRows: "function",
382
+ toSavedRows: "function",
383
+ computeUsage: "function",
384
+ computePluginUOG: "function",
385
+ createCheckRecord: "function",
386
+ createFieldsInfo: "function",
387
+ isTargetType: "function",
388
+ stripComments: "function",
389
+ defaultCssRows: "function",
390
+ defaultMenuInfo: "function",
391
+ expandFields: "function",
392
+ buildReportCss: "function",
393
+ fileNameOf: "function",
394
+ formulaOf: "function",
395
+ mountReportHtml: "function",
396
+ replaceTags: "function",
397
+ getPaperSize: "function",
398
+ webFontOf: "function",
399
+ DUMMY_IMAGE: "string",
400
+ validate: "function",
401
+ parseJsonSafely: "function",
402
+ ValidationError: "function",
403
+ writeConfig: "function",
404
+ DEFAULT_LIMITS: "object",
405
+ ConfigStoreError: "function",
406
+ // pull(アプリのプラグインの設定を読んで封筒形式にする。2026-10-05)
407
+ readConfig: "function",
408
+ buildExportData: "function"
409
+ };
410
+ function gitCommit(dir) {
411
+ try {
412
+ const head = execFileSync("git", ["-C", dir, "rev-parse", "--short", "HEAD"], { encoding: "utf8" }).trim();
413
+ const dirty = execFileSync("git", ["-C", dir, "status", "--porcelain", "--untracked-files=no"], { encoding: "utf8" }).trim() !== "";
414
+ return dirty ? `${head}+dirty` : head;
415
+ } catch {
416
+ return "unknown";
417
+ }
418
+ }
419
+ function devMeta() {
420
+ const pkg = JSON.parse(readFileSync2(path5.join(AUTHORING_ROOT, "package.json"), "utf8"));
421
+ return { toolsVersion: pkg.version, pluginId: PRINT_CRAFT_PLUGIN_ID, minPluginVersion: MIN_PLUGIN_VERSION, supportedApiVersions: [...SUPPORTED_API_VERSIONS], commit: gitCommit(AUTHORING_ROOT), builtAt: "", mode: "dev" };
422
+ }
423
+ function toolsMeta() {
424
+ if (true) return JSON.parse('{"toolsVersion":"1.3.0","pluginId":"lcapkanpjdabgphknkabojmcfhonhkhp","minPluginVersion":6,"supportedApiVersions":[1,2],"commit":"5abbe71","builtAt":"2026-10-09T13:34:14.744Z","mode":"build"}');
425
+ return devMeta();
426
+ }
427
+
428
+ // src/engine.ts
429
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
430
+ import path6 from "node:path";
431
+ import vm from "node:vm";
432
+
433
+ // src/plugin-zip.ts
434
+ import { createHash } from "node:crypto";
435
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
436
+ import { inflateRawSync } from "node:zlib";
437
+ var SIG_EOCD = 101010256;
438
+ var SIG_CENTRAL = 33639248;
439
+ var SIG_LOCAL = 67324752;
440
+ var PluginZipError = class extends Error {
441
+ };
442
+ var ZIP_LIMITS = {
443
+ maxOuterBytes: 64 * 1024 * 1024,
444
+ maxEntries: 500,
445
+ maxEntryBytes: 32 * 1024 * 1024,
446
+ maxTotalBytes: 128 * 1024 * 1024
447
+ };
448
+ var CRC_TABLE = (() => {
449
+ const t = new Uint32Array(256);
450
+ for (let n = 0; n < 256; n++) {
451
+ let c = n;
452
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
453
+ t[n] = c >>> 0;
454
+ }
455
+ return t;
456
+ })();
457
+ function crc32(buf) {
458
+ let c = 4294967295;
459
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
460
+ return (c ^ 4294967295) >>> 0;
461
+ }
462
+ function u16(buf, p, what) {
463
+ if (p < 0 || p + 2 > buf.length) throw new PluginZipError(`zip が壊れている(${what} が範囲外)`);
464
+ return buf.readUInt16LE(p);
465
+ }
466
+ function u32(buf, p, what) {
467
+ if (p < 0 || p + 4 > buf.length) throw new PluginZipError(`zip が壊れている(${what} が範囲外)`);
468
+ return buf.readUInt32LE(p);
469
+ }
470
+ function unzip(buf, limits = ZIP_LIMITS) {
471
+ if (buf.length > limits.maxOuterBytes) throw new PluginZipError(`zip が大きすぎる(${buf.length} バイト。上限 ${limits.maxOuterBytes})`);
472
+ const minEocd = 22;
473
+ let eocd = -1;
474
+ for (let i = buf.length - minEocd; i >= Math.max(0, buf.length - minEocd - 65535); i--) {
475
+ if (buf.readUInt32LE(i) === SIG_EOCD) {
476
+ eocd = i;
477
+ break;
478
+ }
479
+ }
480
+ if (eocd < 0) throw new PluginZipError("zip の終端(EOCD)が無い");
481
+ const total = u16(buf, eocd + 10, "entry 数");
482
+ const cdSize = u32(buf, eocd + 12, "central directory の大きさ");
483
+ const cdOffset = u32(buf, eocd + 16, "central directory の位置");
484
+ if (total > limits.maxEntries) throw new PluginZipError(`zip の entry が多すぎる(${total}。上限 ${limits.maxEntries})`);
485
+ if (cdOffset + cdSize > eocd) throw new PluginZipError("zip が壊れている(central directory が終端を越える)");
486
+ const out = /* @__PURE__ */ new Map();
487
+ let p = cdOffset;
488
+ let totalBytes = 0;
489
+ for (let n = 0; n < total; n++) {
490
+ if (u32(buf, p, "central directory") !== SIG_CENTRAL) throw new PluginZipError(`zip の central directory が壊れている(entry ${n})`);
491
+ const method = u16(buf, p + 10, "圧縮方式");
492
+ const crc = u32(buf, p + 16, "CRC");
493
+ const compSize = u32(buf, p + 20, "圧縮後の大きさ");
494
+ const uncompSize = u32(buf, p + 24, "展開後の大きさ");
495
+ const nameLen = u16(buf, p + 28, "名前の長さ");
496
+ const extraLen = u16(buf, p + 30, "extra の長さ");
497
+ const commentLen = u16(buf, p + 32, "コメントの長さ");
498
+ const localOffset = u32(buf, p + 42, "local header の位置");
499
+ if (p + 46 + nameLen > eocd) throw new PluginZipError(`zip が壊れている(entry ${n} の名前が範囲外)`);
500
+ const name = buf.subarray(p + 46, p + 46 + nameLen).toString("utf8");
501
+ p += 46 + nameLen + extraLen + commentLen;
502
+ if (name.endsWith("/")) continue;
503
+ if (name.startsWith("/") || name.split("/").includes("..")) throw new PluginZipError(`zip の entry 名が不正: ${name}`);
504
+ if (out.has(name)) throw new PluginZipError(`zip に同じ名前の entry が 2 つある: ${name}`);
505
+ if (uncompSize > limits.maxEntryBytes) throw new PluginZipError(`zip の entry が大きすぎる: ${name}(${uncompSize} バイト。上限 ${limits.maxEntryBytes})`);
506
+ totalBytes += uncompSize;
507
+ if (totalBytes > limits.maxTotalBytes) throw new PluginZipError(`zip の展開後の合計が大きすぎる(上限 ${limits.maxTotalBytes} バイト)`);
508
+ if (u32(buf, localOffset, `local header(${name})`) !== SIG_LOCAL) throw new PluginZipError(`zip の local header が壊れている: ${name}`);
509
+ const localNameLen = u16(buf, localOffset + 26, "local の名前の長さ");
510
+ const localExtraLen = u16(buf, localOffset + 28, "local の extra の長さ");
511
+ const localName = buf.subarray(localOffset + 30, localOffset + 30 + localNameLen).toString("utf8");
512
+ if (localName !== name) throw new PluginZipError(`zip の entry 名が central directory と local header で違う: ${name} / ${localName}`);
513
+ const dataStart = localOffset + 30 + localNameLen + localExtraLen;
514
+ if (dataStart + compSize > cdOffset) throw new PluginZipError(`zip が壊れている(${name} のデータが central directory に重なる)`);
515
+ const data = buf.subarray(dataStart, dataStart + compSize);
516
+ let content;
517
+ if (method === 0) content = Buffer.from(data);
518
+ else if (method === 8) {
519
+ try {
520
+ content = inflateRawSync(data, { maxOutputLength: Math.max(1, uncompSize) });
521
+ } catch (e) {
522
+ throw new PluginZipError(`zip の entry を展開できない: ${name}(${e instanceof Error ? e.message : String(e)})`);
523
+ }
524
+ } else throw new PluginZipError(`zip の圧縮方式 ${method} には対応していない: ${name}`);
525
+ if (content.length !== uncompSize) throw new PluginZipError(`zip の entry の大きさが合わない: ${name}(${content.length} / ${uncompSize})`);
526
+ if (crc32(content) !== crc) throw new PluginZipError(`zip の entry の CRC が合わない(壊れているか改変されている): ${name}`);
527
+ out.set(name, content);
528
+ }
529
+ return out;
530
+ }
531
+ var ENGINE_ENTRY = "desktop_js/KintoneFormulaPCraft.min.js";
532
+ var BIGNUMBER_ENTRY = "desktop_js/bignumber.min.js";
533
+ var MOMENT_TZ_ENTRY = "desktop_js/moment-timezone-with-data.min.js";
534
+ var API_ENTRY = "config_js/print-craft-authoring-api.js";
535
+ var MANIFEST_ENTRY = "manifest.json";
536
+ function sha256Hex(data) {
537
+ return createHash("sha256").update(data).digest("hex");
538
+ }
539
+ function pluginIdOf(publicKey) {
540
+ return sha256Hex(publicKey).slice(0, 32).replace(/[0-9a-f]/g, (c) => "abcdefghijklmnop"["0123456789abcdef".indexOf(c)]);
541
+ }
542
+ function readPluginZip(file) {
543
+ let outer;
544
+ try {
545
+ const size = statSync2(file).size;
546
+ if (size > ZIP_LIMITS.maxOuterBytes) throw new PluginZipError(`大きすぎる(${size} バイト)`);
547
+ outer = unzip(readFileSync3(file));
548
+ } catch (e) {
549
+ throw new PluginZipError(`印刷屋の zip を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
550
+ }
551
+ const contents = outer.get("contents.zip");
552
+ if (!contents) throw new PluginZipError(`印刷屋の zip ではない(contents.zip が無い): ${file}`);
553
+ let inner;
554
+ try {
555
+ inner = unzip(contents);
556
+ } catch (e) {
557
+ throw new PluginZipError(`印刷屋の zip の contents.zip を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
558
+ }
559
+ const need = (name) => {
560
+ const b = inner.get(name);
561
+ if (!b) throw new PluginZipError(`印刷屋の zip に ${name} が無い: ${file}`);
562
+ return b;
563
+ };
564
+ let manifest;
565
+ try {
566
+ manifest = JSON.parse(need(MANIFEST_ENTRY).toString("utf8"));
567
+ } catch (e) {
568
+ throw new PluginZipError(`印刷屋の zip の manifest.json を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
569
+ }
570
+ if (!manifest || typeof manifest !== "object") throw new PluginZipError(`印刷屋の zip の manifest.json が不正: ${file}`);
571
+ const engine = need(ENGINE_ENTRY);
572
+ const bignumber = need(BIGNUMBER_ENTRY);
573
+ const momentTimezone = need(MOMENT_TZ_ENTRY);
574
+ const api = inner.get(API_ENTRY);
575
+ const pubkey = outer.get("PUBKEY");
576
+ return {
577
+ from: file,
578
+ ...pubkey ? { pluginId: pluginIdOf(pubkey) } : {},
579
+ manifest,
580
+ pluginVersion: String(manifest.version ?? ""),
581
+ engine: engine.toString("utf8"),
582
+ bignumber: bignumber.toString("utf8"),
583
+ momentTimezone: momentTimezone.toString("utf8"),
584
+ api: api?.toString("utf8"),
585
+ sha256: {
586
+ engine: sha256Hex(engine),
587
+ ...api ? { api: sha256Hex(api) } : {},
588
+ bignumber: sha256Hex(bignumber),
589
+ momentTimezone: sha256Hex(momentTimezone),
590
+ contents: sha256Hex(contents)
591
+ }
592
+ };
593
+ }
594
+
595
+ // src/engine.ts
596
+ var DEFAULT_CONTEXT_BASE_URL = "https://example.cybozu.com";
597
+ var DEFAULT_USER = { id: "1", code: "authoring", name: "authoring", email: "", language: "ja" };
598
+ var loaded = null;
599
+ function resolvePluginSources(opt = {}, mode = opt.mode ?? toolsMeta().mode) {
600
+ const zip = opt.pluginZip;
601
+ if (zip) {
602
+ if (!existsSync3(zip)) throw new PluginZipError(`印刷屋の zip が無い: ${zip}(.env の PCRAFT_PLUGIN_ZIP)`);
603
+ return { ...readPluginZip(zip), kind: "zip" };
604
+ }
605
+ const dev = opt.devPluginDir;
606
+ const devAllowed = mode === "dev" && !!dev;
607
+ if (!devAllowed) {
608
+ throw new PluginZipError(`印刷屋の zip の場所が分からない。.env に PCRAFT_PLUGIN_ZIP=<印刷屋プラグインの zip のパス> を書く${mode === "dev" ? "(開発中に隣の print-craft の prod/ を読むなら環境変数 PCRAFT_ALLOW_DEV_PLUGIN=1)" : ""}`);
609
+ }
610
+ if (!dev || !existsSync3(path6.join(dev, ENGINE_ENTRY))) throw new PluginZipError(`印刷屋の zip の場所が分からない(PCRAFT_ALLOW_DEV_PLUGIN=1 だが print-craft の prod/ が無い: ${dev})。.env に PCRAFT_PLUGIN_ZIP を書く`);
611
+ const read = (rel2) => readFileSync4(path6.join(dev, rel2), "utf8");
612
+ const apiFile = path6.join(dev, API_ENTRY);
613
+ const engine = read(ENGINE_ENTRY);
614
+ const bignumber = read(BIGNUMBER_ENTRY);
615
+ const momentTimezone = read(MOMENT_TZ_ENTRY);
616
+ const api = existsSync3(apiFile) ? readFileSync4(apiFile, "utf8") : void 0;
617
+ const manifest = JSON.parse(read(MANIFEST_ENTRY));
618
+ return {
619
+ kind: "dev",
620
+ from: dev,
621
+ manifest,
622
+ pluginVersion: String(manifest.version ?? ""),
623
+ engine,
624
+ bignumber,
625
+ momentTimezone,
626
+ api,
627
+ sha256: { engine: sha256Hex(engine), ...api ? { api: sha256Hex(api) } : {}, bignumber: sha256Hex(bignumber), momentTimezone: sha256Hex(momentTimezone) }
628
+ };
629
+ }
630
+ async function loadEngine(opt = {}) {
631
+ if (loaded) return loaded;
632
+ const meta = toolsMeta();
633
+ const src = resolvePluginSources(opt, opt.mode ?? meta.mode);
634
+ const warnings = [];
635
+ if (src.kind === "zip" && src.pluginId !== PRINT_CRAFT_PLUGIN_ID) {
636
+ throw new PluginZipError(`印刷屋プラグインの zip ではない(プラグイン ID ${src.pluginId ?? "不明(PUBKEY が無い)"}。印刷屋は ${PRINT_CRAFT_PLUGIN_ID}): ${src.from}。配布元から入手した印刷屋の zip を PCRAFT_PLUGIN_ZIP に書く`);
637
+ }
638
+ if (src.kind === "dev") warnings.push(`開発中の print-craft(${src.from}。PCRAFT_ALLOW_DEV_PLUGIN=1)から読む。プラグイン ID は確かめない`);
639
+ if (!isSupportedPluginVersion(src.pluginVersion)) {
640
+ throw new PluginZipError(`印刷屋プラグインの版 ${src.pluginVersion || "不明"}(${src.from})には対応していない。tools が扱うのは Ver.${MIN_PLUGIN_VERSION} 以降`);
641
+ }
642
+ if (!src.api) throw new PluginZipError(`印刷屋の zip に ${API_ENTRY} が無い(Ver.${MIN_PLUGIN_VERSION} 以降の zip が要る): ${src.from}`);
643
+ const g = globalThis;
644
+ const { Window } = await import("happy-dom");
645
+ const ctx = { baseUrl: DEFAULT_CONTEXT_BASE_URL, appId: 1, loginUser: { ...DEFAULT_USER } };
646
+ const window = new Window({ url: `${ctx.baseUrl}/k/${ctx.appId}/show` });
647
+ for (const k of ["document", "Node", "Element", "HTMLElement", "HTMLDivElement", "HTMLImageElement", "HTMLIFrameElement", "DOMParser", "Event", "CSS", "getComputedStyle", "Blob", "File", "URL", "DOMRect", "requestAnimationFrame", "cancelAnimationFrame"]) {
648
+ const v = window[k];
649
+ if (v !== void 0 && g[k] === void 0) g[k] = k === "getComputedStyle" && typeof v === "function" ? v.bind(window) : v;
650
+ }
651
+ g.window = window;
652
+ const kintone = {
653
+ getLoginUser: () => ({ ...ctx.loginUser, isGuest: false }),
654
+ app: { getId: () => ctx.appId },
655
+ mobile: { app: { getId: () => ctx.appId } },
656
+ api: Object.assign(() => Promise.reject(new Error("kintone.api is not available in authoring tools")), {
657
+ url: (p, _guest) => `${ctx.baseUrl}${p.startsWith("/") ? "" : "/"}${p}`
658
+ }),
659
+ $PLUGIN_ID: "authoring"
660
+ };
661
+ g.kintone = kintone;
662
+ g.cybozu = { data: { IS_MOBILE_DEVICE: false } };
663
+ const u = ctx.loginUser;
664
+ const infoUser = { id: u.id, code: u.code, name: u.name, email: u.email, primaryOrganization: null, organizations: [], groups: [] };
665
+ const usersInfo = {
666
+ users: [infoUser],
667
+ organizations: [],
668
+ groups: [],
669
+ userCodes: { [u.code]: infoUser },
670
+ orgCodes: {},
671
+ groupCodes: {},
672
+ userIds: { [u.id]: u.code },
673
+ orgIds: {},
674
+ groupIds: {}
675
+ };
676
+ g.rex0220_users_info3 = { request: () => void 0, getUsersInfo: async () => usersInfo, data: usersInfo };
677
+ window.kintone = g.kintone;
678
+ window.cybozu = g.cybozu;
679
+ window.rex0220_users_info3 = g.rex0220_users_info3;
680
+ if (!existsSync3(MOMENT_FILE)) throw new Error(`moment が無い: ${MOMENT_FILE}(tools の npm install 時に取る。手で取るなら npm run vendor)`);
681
+ const runScript = (code, filename) => {
682
+ vm.runInThisContext(code, { filename });
683
+ for (const k of ["moment", "rex0220_BigNumber", "rex0220_Decimal", "rex0220p", "rex0220PrintCraftAuthoring"]) {
684
+ if (g[k] === void 0 && window[k] !== void 0) g[k] = window[k];
685
+ if (window[k] === void 0 && g[k] !== void 0) window[k] = g[k];
686
+ }
687
+ };
688
+ runScript(readFileSync4(MOMENT_FILE, "utf8"), MOMENT_FILE);
689
+ runScript(src.momentTimezone, `${src.from}!${MOMENT_TZ_ENTRY}`);
690
+ runScript(src.bignumber, `${src.from}!${BIGNUMBER_ENTRY}`);
691
+ runScript(src.engine, `${src.from}!${ENGINE_ENTRY}`);
692
+ runScript(src.api, `${src.from}!${API_ENTRY}`);
693
+ const rex0220p = window.rex0220p;
694
+ const Ctor = rex0220p?.KintoneFormulaPCraft;
695
+ if (typeof Ctor !== "function") throw new Error("KintoneFormulaPCraft is not loaded");
696
+ const api = window.rex0220PrintCraftAuthoring;
697
+ if (!api || typeof api !== "object") throw new PluginZipError("印刷屋の authoring API(rex0220PrintCraftAuthoring)が読めない");
698
+ if (!SUPPORTED_API_VERSIONS.includes(api.apiVersion)) throw new PluginZipError(`印刷屋の authoring API の版 ${String(api.apiVersion)} には対応していない(tools は ${SUPPORTED_API_VERSIONS.join(", ")})。tools を新しい版にする`);
699
+ if (String(api.pluginVersion) !== src.pluginVersion) throw new PluginZipError(`authoring API の印刷屋の版 ${String(api.pluginVersion)} が zip の manifest の版 ${src.pluginVersion} と違う(組み替えられた zip)`);
700
+ const bad = Object.entries(REQUIRED_API).filter(([k, t]) => typeof api[k] !== t).map(([k]) => k);
701
+ if (bad.length) throw new PluginZipError(`印刷屋の authoring API に tools が使うものが無い、または型が違う: ${bad.join(", ")}(tools と印刷屋の版を合わせる)`);
702
+ loaded = {
703
+ window,
704
+ Ctor,
705
+ api,
706
+ source: { kind: src.kind, from: src.from, pluginVersion: src.pluginVersion, engineSha256: src.sha256.engine, apiSha256: src.sha256.api, ...src.pluginId ? { pluginId: src.pluginId } : {} },
707
+ warnings,
708
+ setContext(next) {
709
+ if (next.baseUrl) ctx.baseUrl = next.baseUrl.replace(/\/+$/, "");
710
+ if (next.appId) ctx.appId = next.appId;
711
+ if (next.loginUser) ctx.loginUser = { ...ctx.loginUser, ...next.loginUser };
712
+ },
713
+ checker: (pp, crec) => new Ctor("formula", pp, crec, true, true),
714
+ runner: (pp, record) => new Ctor("formula", pp, record, true),
715
+ functionNames: () => Object.keys(new Ctor("formula", {}, {}, true, true).funs).sort()
716
+ };
717
+ return loaded;
718
+ }
719
+
720
+ // src/env.ts
721
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
722
+ import path7 from "node:path";
723
+ var NAMES = {
724
+ baseUrl: ["KINTONE_BASE_URL", "KSQL_BASE_URL"],
725
+ token: ["KINTONE_API_TOKEN", "KSQL_TOKEN"],
726
+ username: ["KINTONE_USERNAME", "KSQL_USERNAME"],
727
+ password: ["KINTONE_PASSWORD", "KSQL_PASSWORD"],
728
+ pluginZip: ["PCRAFT_PLUGIN_ZIP"]
729
+ };
730
+ function unquote(raw) {
731
+ const value = raw.trim();
732
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
733
+ return value;
734
+ }
735
+ function parseDotEnv(text) {
736
+ const out = {};
737
+ for (const raw of text.split(/\r?\n/)) {
738
+ const line = raw.trim();
739
+ if (!line || line.startsWith("#")) continue;
740
+ const m = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
741
+ if (!m) continue;
742
+ out[m[1]] = unquote(m[2]);
743
+ }
744
+ return out;
745
+ }
746
+ var AuthError = class extends Error {
747
+ };
748
+ function pickerWithSource(opt, unquoteProcess = false) {
749
+ const osEnv = opt.env;
750
+ const file = envFileOf(opt);
751
+ const fromFile = existsSync4(file) ? parseDotEnv(readFileSync5(file, "utf8")) : {};
752
+ return (names) => {
753
+ for (const [src, source] of [[osEnv, "process"], [fromFile, "file"]]) {
754
+ for (const n of names) {
755
+ const raw = src[n] ?? "";
756
+ const v = source === "process" && unquoteProcess ? unquote(raw) : raw.trim();
757
+ if (v) return { value: v, source, ...source === "file" ? { file } : {} };
758
+ }
759
+ }
760
+ return void 0;
761
+ };
762
+ }
763
+ function picker(opt, unquoteProcess = false) {
764
+ const pick = pickerWithSource(opt, unquoteProcess);
765
+ return (names) => pick(names)?.value;
766
+ }
767
+ function envFileOf(opt) {
768
+ return opt.envFile ?? path7.join(opt.cwd, ".env");
769
+ }
770
+ function baseUrlFromEnv(opt) {
771
+ const raw = picker(opt, true)(NAMES.baseUrl);
772
+ if (!raw) return void 0;
773
+ try {
774
+ return normalizeKintoneBaseUrl(raw);
775
+ } catch (e) {
776
+ throw new AuthError(`KINTONE_BASE_URL が不正: ${e instanceof KintoneUrlError ? e.message : String(e)}(https://<サブドメイン>.cybozu.com の形で書く)`);
777
+ }
778
+ }
779
+ function loadAuth(opt) {
780
+ const pick = picker(opt);
781
+ const file = envFileOf(opt);
782
+ const baseUrl = baseUrlFromEnv(opt);
783
+ if (!baseUrl) throw new AuthError(`KINTONE_BASE_URL が無い(OS の環境変数か .env: ${file}。kintone 公式 MCP と同じ変数。dashboard の KSQL_BASE_URL でもよい)`);
784
+ const token = pick(NAMES.token);
785
+ const username = pick(NAMES.username);
786
+ const password = pick(NAMES.password);
787
+ if (!token && !(username && password)) throw new AuthError("KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(OS の環境変数か .env)");
788
+ return { baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
789
+ }
790
+ function loadAuthForEnv(env, cwd) {
791
+ const file = path7.join(cwd, env.envFile);
792
+ if (!existsSync4(file)) throw new AuthError(`環境「${env.name}」の認証のファイルが無い: ${env.envFile}(${env.envFile === ".env" ? ".env.example を写して作る" : "env/ に作る。.env と同じ書き方"})`);
793
+ const vars = parseDotEnv(readFileSync5(file, "utf8"));
794
+ const pick = (names) => {
795
+ for (const n of names) if (vars[n] && vars[n].trim()) return vars[n].trim();
796
+ return void 0;
797
+ };
798
+ const fileBase = pick(NAMES.baseUrl);
799
+ if (fileBase) {
800
+ let normalized = "";
801
+ try {
802
+ normalized = normalizeKintoneBaseUrl(fileBase);
803
+ } catch {
804
+ normalized = "";
805
+ }
806
+ if (normalized !== env.baseUrl) throw new AuthError(`${env.envFile} の KINTONE_BASE_URL が environments.json の環境「${env.name}」の baseUrl(${env.baseUrl})と違う。取り違えを防ぐため止める(どちらかを直す)`);
807
+ }
808
+ const token = pick(NAMES.token);
809
+ const username = pick(NAMES.username);
810
+ const password = pick(NAMES.password);
811
+ if (!token && !(username && password)) throw new AuthError(`${env.envFile} に KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(環境「${env.name}」。OS の環境変数は読まない)`);
812
+ return { baseUrl: env.baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
813
+ }
814
+ function pluginZipPath(opt) {
815
+ const picked = pickerWithSource(opt, true)(NAMES.pluginZip);
816
+ if (!picked) return void 0;
817
+ if (picked.source === "file") return path7.resolve(path7.dirname(picked.file ?? envFileOf(opt)), picked.value);
818
+ if (!path7.isAbsolute(picked.value)) throw new AuthError(`PCRAFT_PLUGIN_ZIP(OS の環境変数)は絶対パスで書く: ${picked.value}(.env に書くなら .env のフォルダーからの相対パスでもよい)`);
819
+ return path7.normalize(picked.value);
820
+ }
821
+ function describeAuth(auth) {
822
+ return auth.token ? "API トークン" : "ログインユーザー";
823
+ }
824
+
825
+ // src/kintone-rest.ts
826
+ var ALLOWED_APIS = ["app", "app/form/fields", "app/form/layout", "record", "records", "preview/app/form/fields", "preview/app/form/layout", "app/plugin/config", "preview/app/plugin/config"];
827
+ var RECEIVE_LIMITS = {
828
+ app: 64 * 1024,
829
+ "app/form/fields": 8 * 1024 * 1024,
830
+ "app/form/layout": 8 * 1024 * 1024,
831
+ "preview/app/form/fields": 8 * 1024 * 1024,
832
+ "preview/app/form/layout": 8 * 1024 * 1024,
833
+ record: 8 * 1024 * 1024,
834
+ records: 8 * 1024 * 1024,
835
+ "app/plugin/config": 2 * 1024 * 1024,
836
+ "preview/app/plugin/config": 2 * 1024 * 1024
837
+ };
838
+ var RestError = class extends Error {
839
+ status;
840
+ code;
841
+ apiPath;
842
+ // Node の型の除去で動かすため、コンストラクターのパラメータープロパティは使わない
843
+ constructor(message, status, code, apiPath) {
844
+ super(message);
845
+ this.status = status;
846
+ this.code = code;
847
+ this.apiPath = apiPath;
848
+ }
849
+ };
850
+ var NotAllowedError = class extends Error {
851
+ };
852
+ async function readCapped(res, limit, apiPath) {
853
+ const tooLarge2 = () => new RestError(`kintone ${apiPath} の応答が大きすぎる(上限 ${limit} バイト)`, res.status, "LimitError", apiPath);
854
+ const quietCancel = async (reader2) => {
855
+ try {
856
+ await reader2.cancel();
857
+ } catch {
858
+ }
859
+ };
860
+ if (res.body === null) return "";
861
+ if (!res.body || typeof res.body.getReader !== "function") throw new RestError(`kintone ${apiPath} の応答に本文のストリームが無い`, res.status, "ResponseError", apiPath);
862
+ const reader = res.body.getReader();
863
+ const declared = Number(res.headers?.get("content-length") ?? Number.NaN);
864
+ if (Number.isFinite(declared) && declared > limit) {
865
+ await quietCancel(reader);
866
+ throw tooLarge2();
867
+ }
868
+ const chunks = [];
869
+ let total = 0;
870
+ for (; ; ) {
871
+ const { done, value } = await reader.read();
872
+ if (done) break;
873
+ if (!value) continue;
874
+ total += value.length;
875
+ if (total > limit) {
876
+ await quietCancel(reader);
877
+ throw tooLarge2();
878
+ }
879
+ chunks.push(value);
880
+ }
881
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
882
+ }
883
+ function apiPathOf(api, guestSpaceId) {
884
+ if (!ALLOWED_APIS.includes(api)) throw new NotAllowedError(`この API は authoring tools では呼ばない: ${api}(許可: ${ALLOWED_APIS.join(", ")})`);
885
+ if (guestSpaceId !== void 0 && (!Number.isInteger(guestSpaceId) || guestSpaceId <= 0)) throw new NotAllowedError(`ゲストスペースの ID が不正: ${String(guestSpaceId)}`);
886
+ const prefix = guestSpaceId ? `/k/guest/${guestSpaceId}/v1/` : "/k/v1/";
887
+ return `${prefix}${api}.json`;
888
+ }
889
+ function authHeaders(auth) {
890
+ if (auth.token) return { "X-Cybozu-API-Token": auth.token };
891
+ if (auth.username && auth.password) return { "X-Cybozu-Authorization": Buffer.from(`${auth.username}:${auth.password}`, "utf8").toString("base64") };
892
+ throw new NotAllowedError("認証情報が無い");
893
+ }
894
+ function createRestClient(auth, fetchImpl = fetch) {
895
+ let baseUrl;
896
+ try {
897
+ baseUrl = normalizeKintoneBaseUrl(auth.baseUrl);
898
+ } catch (e) {
899
+ throw new NotAllowedError(`接続先が kintone ではないので送らない: ${e instanceof KintoneUrlError ? e.message : String(e)}`);
900
+ }
901
+ const headers = { ...authHeaders(auth), Accept: "application/json" };
902
+ return {
903
+ baseUrl,
904
+ async get(api, params, guestSpaceId) {
905
+ const apiPath = apiPathOf(api, guestSpaceId);
906
+ const url = new URL(apiPath, baseUrl);
907
+ for (const [k, v] of Object.entries(params)) if (v !== void 0) url.searchParams.set(k, String(v));
908
+ if (url.origin !== baseUrl || url.username || url.password) throw new NotAllowedError(`送信先が kintone の接続先と違うので送らない: ${url.origin}`);
909
+ const res = await fetchImpl(url.href, { method: "GET", headers });
910
+ const text = await readCapped(res, RECEIVE_LIMITS[api], apiPath);
911
+ if (!res.ok) {
912
+ let code;
913
+ try {
914
+ const body = JSON.parse(text);
915
+ code = typeof body.code === "string" ? body.code.slice(0, 40) : void 0;
916
+ } catch {
917
+ }
918
+ const hint = res.status === 401 ? "(認証情報かアクセス権を確かめる。API トークンならそのアプリで有効か)" : res.status === 403 ? "(アクセス権が無い)" : res.status === 404 ? "(アプリかレコードが無い)" : "";
919
+ throw new RestError(`kintone ${apiPath} が HTTP ${res.status}${code ? ` ${code}` : ""}${hint}`, res.status, code, apiPath);
920
+ }
921
+ try {
922
+ return JSON.parse(text);
923
+ } catch {
924
+ throw new RestError(`kintone ${apiPath} の応答が JSON でない${text ? "" : "(本文が空)"}`, res.status, "ResponseError", apiPath);
925
+ }
926
+ }
927
+ };
928
+ }
929
+
930
+ // src/commands/normalize.ts
931
+ import { readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
932
+ import path9 from "node:path";
933
+
934
+ // src/normalize/findings.ts
935
+ var Findings = class {
936
+ items = [];
937
+ add(level, rule, where, message) {
938
+ this.items.push({ level, rule, where, message });
939
+ }
940
+ error(rule, where, message) {
941
+ this.add("error", rule, where, message);
942
+ }
943
+ warning(rule, where, message) {
944
+ this.add("warning", rule, where, message);
945
+ }
946
+ info(rule, where, message) {
947
+ this.add("info", rule, where, message);
948
+ }
949
+ count(level) {
950
+ return this.items.filter((f) => f.level === level).length;
951
+ }
952
+ get hasErrors() {
953
+ return this.count("error") > 0;
954
+ }
955
+ /** 画面に出す表(レベル → 場所 → 文言) */
956
+ format() {
957
+ const label = { error: "エラー", warning: "警告", info: "情報" };
958
+ const lines = [`エラー ${this.count("error")} / 警告 ${this.count("warning")} / 情報 ${this.count("info")}`];
959
+ for (const level of ["error", "warning", "info"]) {
960
+ for (const f of this.items.filter((x) => x.level === level)) lines.push(`[${label[level]}] ${f.where}: ${f.message}(${f.rule})`);
961
+ }
962
+ return lines.join("\n");
963
+ }
964
+ };
965
+
966
+ // src/normalize/model.ts
967
+ var PSEUDO_FIELDS = /* @__PURE__ */ new Set(["$id", "$revision", "$out", "$html", "$rseq"]);
968
+ var UOG_MARK = "$UGO$";
969
+ var EXEC_CONDITION_LABEL = "実行条件";
970
+ var clone = (v) => JSON.parse(JSON.stringify(v));
971
+ function buildModel(file, api, trustedBaseUrl) {
972
+ const pp = { ...clone(file.properties) };
973
+ pp["$id"] = { type: "RECORD_NUMBER", code: "$id", label: "$id" };
974
+ pp["$revision"] = { type: "__REVISION__", code: "$revision", label: "$revision" };
975
+ pp["$html"] = { type: "MULTI_LINE_TEXT", code: "$html", label: "$html" };
976
+ const crec = api.createCheckRecord(pp);
977
+ const ppRun = api.expandFields(clone(file.properties), EXEC_CONDITION_LABEL);
978
+ const calcCandidates = api.createFieldsInfo(pp, clone(file.layout)).filter((c) => !c.ptcode && c.type !== "SUBTABLE");
979
+ const calcByCode = /* @__PURE__ */ new Map();
980
+ for (const c of calcCandidates) calcByCode.set(c.fieldcode, c);
981
+ const baseUrl = trustedBaseUrl ?? "";
982
+ const fieldsBaseUrl = typeof file.baseUrl === "string" && isKintoneBaseUrl(file.baseUrl) ? normalizeKintoneBaseUrl(file.baseUrl) : "";
983
+ return { api, file, baseUrl, fieldsBaseUrl, appId: file.appId, pp, crec, ppRun, calcCandidates, calcByCode };
984
+ }
985
+ function fieldExists(model, code) {
986
+ if (PSEUDO_FIELDS.has(code)) return true;
987
+ const p = model.pp[code];
988
+ if (!p) return false;
989
+ if (p.ptcode) return !!model.pp[p.ptcode];
990
+ return true;
991
+ }
992
+ function calcIneligibleReason(model, code) {
993
+ if (model.calcByCode.has(code)) return null;
994
+ const p = model.pp[code];
995
+ if (!p) return "fields に無い";
996
+ if (p.ptcode) return `テーブル ${p.ptcode} の中`;
997
+ if (!model.api.isTargetType(p.type)) return `更新できない型(${p.type})`;
998
+ if (p.expression) return "計算項目";
999
+ if (p.copyfield) return "ルックアップのコピー先";
1000
+ return "レイアウトに無い";
1001
+ }
1002
+
1003
+ // src/normalize/derive.ts
1004
+ var ENVELOPE_KEYS = ["date", "pluginName", "pluginID", "PluginVersion", "appId", "appName"];
1005
+ var SHELL_KEYS = ["pluginProductEnv", "pluginLastUpdate", "pluginUpdater", "startDate", "name", "version", "ldate"];
1006
+ var META_CALC_KEYS = ["row_type", "type", "lookup", "ptcode", "fieldlabel", "required"];
1007
+ function evaluateFormula(engine, model, formulaSet) {
1008
+ const formula = model.api.stripComments(formulaSet.replace(/\r\n?/g, "\n"));
1009
+ if (!formula.trim()) return { formula, usedFields: {} };
1010
+ const kf = engine.checker(model.pp, model.crec);
1011
+ kf.usedFields({});
1012
+ try {
1013
+ kf.dq(formula);
1014
+ } catch (e) {
1015
+ return { formula, usedFields: {}, error: String(e?.message ?? e) };
1016
+ }
1017
+ return { formula, usedFields: kf.usedFields() };
1018
+ }
1019
+ function rowLabel(row, index) {
1020
+ return row.menu || `ボタン ${index + 1}`;
1021
+ }
1022
+ function tagRowLabel(tag, index) {
1023
+ const kind = index === 0 ? "ボタン表示条件" : index === 1 ? "ファイル名" : tag.desc || tag.fieldcode || "本文";
1024
+ return `HTML 設定 ${index + 1} 行目 (${kind})`;
1025
+ }
1026
+ function bodyOf(envelope) {
1027
+ const skip = /* @__PURE__ */ new Set([...ENVELOPE_KEYS, ...SHELL_KEYS]);
1028
+ const out = {};
1029
+ for (const k of Object.keys(envelope)) if (!skip.has(k)) out[k] = envelope[k];
1030
+ return out;
1031
+ }
1032
+ function deriveBody(input, model, engine, f) {
1033
+ const api = model.api;
1034
+ const rows = api.buildMenuRows(input, "ja");
1035
+ rows.forEach((row, i) => {
1036
+ const label = rowLabel(row, i);
1037
+ const tags = api.normalizeTagsInfo(row.tagsInfo);
1038
+ if (tags) {
1039
+ tags.fieldsInfo = tags.fieldsInfo.map((t, j) => {
1040
+ const base = { ...t, id: j + 1, formulaSet: String(t.formulaSet ?? "").replace(/\r\n?/g, "\n") };
1041
+ if (!t.state) return { ...base, formula: "", usedFields: {} };
1042
+ const r = evaluateFormula(engine, model, base.formulaSet);
1043
+ if (r.error) f.error("formula.syntax", `${label} / ${tagRowLabel(t, j)}`, `計算式を評価できない: ${r.error}`);
1044
+ return { ...base, formula: r.formula, usedFields: r.usedFields };
1045
+ });
1046
+ row.tagsInfo = tags;
1047
+ }
1048
+ const given = row.calcInfo.fieldsInfo;
1049
+ if (given.length === 0) {
1050
+ row.calcInfo.usedFields = {};
1051
+ row.calcInfo.flinkage = false;
1052
+ return;
1053
+ }
1054
+ const outRow = given.find((x) => x.fieldcode === "$out");
1055
+ const rebuilt = [];
1056
+ rebuilt.push(
1057
+ outRow ? { ...outRow, row_type: outRow.row_type || "更新条件", type: "BOOL", lookup: false, ptcode: "", fieldlabel: "", fieldcode: "$out", required: false } : { id: 1, state: false, row_type: "更新条件", type: "BOOL", lookup: false, ptcode: "", fieldlabel: "", fieldcode: "$out", required: false, remark: "", formulaSet: "1", formula: "1", usedFields: {} }
1058
+ );
1059
+ if (!outRow) f.info("calc.out", `${label} / 更新項目`, "先頭の $out(更新条件)が無いので既定(1 = 常に更新)を足した");
1060
+ const seen = /* @__PURE__ */ new Set(["$out"]);
1061
+ for (const x of given) {
1062
+ if (x.fieldcode === "$out") continue;
1063
+ if (seen.has(x.fieldcode)) {
1064
+ f.error("calc.duplicate", `${label} / 更新項目 ${x.fieldcode}`, "同じ項目が 2 回ある");
1065
+ continue;
1066
+ }
1067
+ seen.add(x.fieldcode);
1068
+ const cand = model.calcByCode.get(x.fieldcode);
1069
+ if (!cand) {
1070
+ f.error("calc.ineligible", `${label} / 更新項目 ${x.fieldcode}`, `${x.fieldcode} は更新項目にできない: ${calcIneligibleReason(model, x.fieldcode)}`);
1071
+ continue;
1072
+ }
1073
+ const diffs = META_CALC_KEYS.filter((k) => x[k] !== void 0 && x[k] !== "" && String(x[k]) !== String(cand[k] ?? ""));
1074
+ if (diffs.length) f.info("calc.meta", `${label} / 更新項目 ${x.fieldcode}`, `fields の定義で上書き: ${diffs.map((k) => `${k} ${JSON.stringify(x[k])} → ${JSON.stringify(cand[k] ?? "")}`).join(", ")}`);
1075
+ rebuilt.push({
1076
+ id: 0,
1077
+ state: !!x.state,
1078
+ row_type: cand.row_type,
1079
+ type: cand.type,
1080
+ lookup: cand.lookup,
1081
+ ptcode: cand.ptcode || "",
1082
+ fieldlabel: cand.fieldlabel,
1083
+ fieldcode: cand.fieldcode,
1084
+ required: !!cand.required,
1085
+ remark: String(x.remark ?? ""),
1086
+ formulaSet: String(x.formulaSet ?? "").replace(/\r\n?/g, "\n"),
1087
+ formula: "",
1088
+ usedFields: {}
1089
+ });
1090
+ }
1091
+ let used = {};
1092
+ row.calcInfo.fieldsInfo = rebuilt.map((c, j) => {
1093
+ const base = { ...c, id: j + 1 };
1094
+ if (!c.state) return { ...base, formula: "", usedFields: {} };
1095
+ const r = evaluateFormula(engine, model, base.formulaSet);
1096
+ if (r.error) f.error("formula.syntax", `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, `計算式を評価できない: ${r.error}`);
1097
+ used = { ...used, ...r.usedFields };
1098
+ return { ...base, formula: r.formula, usedFields: r.usedFields };
1099
+ });
1100
+ row.calcInfo.usedFields = used;
1101
+ row.calcInfo.flinkage = false;
1102
+ });
1103
+ const saved = api.toSavedRows(rows);
1104
+ const usage = api.computeUsage(rows, model.pp);
1105
+ const body = {
1106
+ pluginEnable: input.pluginEnable === void 0 ? true : !!input.pluginEnable,
1107
+ menuInfo: input.menuInfo && typeof input.menuInfo === "object" ? input.menuInfo : api.defaultMenuInfo("ja"),
1108
+ // ゲストの行番号は tools が振る(docs/設定ファイル仕様.md の guestsInfo[].id)
1109
+ guestsInfo: Array.isArray(input.guestsInfo) ? input.guestsInfo.map((g, i) => g && typeof g === "object" && !Array.isArray(g) ? { ...g, id: i + 1 } : g) : [],
1110
+ pluginComment: String(input.pluginComment ?? ""),
1111
+ pluginDescription: String(input.pluginDescription ?? ""),
1112
+ commonCssEnable: input.commonCssEnable === void 0 ? true : !!input.commonCssEnable,
1113
+ // 共通 CSS は書いたときだけ出す。印刷屋 Ver.6 のアップロードの一部置換・追加は、ファイルに cssInfo があれば共通 CSS を丸ごと置き換えるかを
1114
+ // 利用者が選ぶ(既定は置き換えない。Takashi 2026-10-05)。既定の 4 行を足しても意味がなく、置き換えを選ぶとアプリの共通 CSS が既定に戻る。無ければ印刷屋は既定の 4 行を使う
1115
+ ...Array.isArray(input.cssInfo) ? { cssInfo: api.normalizeCssRows(input.cssInfo) } : {},
1116
+ ...input.fontInfo && typeof input.fontInfo === "object" ? { fontInfo: input.fontInfo } : {},
1117
+ // 外部参照(Ver.6 の共通の設定。設定画面は常に明示して保存する)。キーが無ければ印刷屋は Ver.5 の設定として「許可」で動く(Takashi 2026-10-04)ので
1118
+ // "allow" を明示する(checks.ts が利用者の承認を求める)。"block" / "allow" 以外はそのまま残して checks.ts がエラーにする
1119
+ externalRefs: input.externalRefs === void 0 ? "allow" : input.externalRefs,
1120
+ pluginInfos: saved,
1121
+ usedFields: usage.usedFields,
1122
+ pluginUOG: usage.pluginUOG
1123
+ };
1124
+ for (const k of Object.keys(input)) if (!(k in body)) body[k] = input[k];
1125
+ if (input.externalRefs === void 0) f.info("externalRefs.legacy", "共通の設定", `外部参照(externalRefs)が無い(Ver.5 の設定か、書き忘れ)。印刷屋はキーが無い設定を「許可」(何も除かない)として動かすので "allow" を明示した。新しい設定なら "block" を書く`);
1126
+ return body;
1127
+ }
1128
+
1129
+ // src/normalize/css-check.ts
1130
+ var PLACEHOLDER_RE = /^#\{&(?:p|n|f\([^{}]*\)|q\([^{}]*\))\}$/s;
1131
+ var STR_MARK = "";
1132
+ var STR_TOKEN = /\u0001(\d+)\u0001/g;
1133
+ function cleanUrl(raw) {
1134
+ let v = String(raw ?? "").replace(/[\t\n\r]/g, "");
1135
+ v = v.replace(/^[\u0000- ]+|[\u0000- ]+$/g, "");
1136
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1);
1137
+ return v.replace(/^[\u0000- ]+|[\u0000- ]+$/g, "");
1138
+ }
1139
+ function classifyUrl(raw) {
1140
+ const v = cleanUrl(raw);
1141
+ if (v === "") return "relative";
1142
+ if (v.startsWith("#{&")) return PLACEHOLDER_RE.test(v) ? "placeholder" : "bad";
1143
+ if (v.startsWith("#")) return "fragment";
1144
+ if (/[\u0000-\u001f\u007f]/.test(v)) return "bad";
1145
+ const scheme = v.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/)?.[1]?.toLowerCase();
1146
+ if (!scheme) return v.replace(/\\/g, "/").startsWith("//") ? "bad" : "relative";
1147
+ if (scheme === "https") return "https";
1148
+ if (scheme === "data") {
1149
+ if (/^data:image\/svg\+xml[;,]/i.test(v)) return "data-svg";
1150
+ return /^data:image\/[a-z0-9.+-]+[;,]/i.test(v) ? "data-image" : "bad";
1151
+ }
1152
+ return "bad";
1153
+ }
1154
+ function parseSrcsetUrls(value) {
1155
+ const urls = [];
1156
+ const n = value.length;
1157
+ let i = 0;
1158
+ while (i < n) {
1159
+ while (i < n && /[\s,]/.test(value[i])) i++;
1160
+ if (i >= n) break;
1161
+ const start = i;
1162
+ while (i < n && !/\s/.test(value[i])) i++;
1163
+ let url = value.slice(start, i);
1164
+ if (url.endsWith(",")) {
1165
+ url = url.replace(/,+$/, "");
1166
+ if (url) urls.push(url);
1167
+ continue;
1168
+ }
1169
+ if (url) urls.push(url);
1170
+ let depth = 0;
1171
+ while (i < n) {
1172
+ const c = value[i];
1173
+ if (c === "(") depth++;
1174
+ else if (c === ")") depth--;
1175
+ else if (c === "," && depth <= 0) {
1176
+ i++;
1177
+ break;
1178
+ }
1179
+ i++;
1180
+ }
1181
+ }
1182
+ return urls;
1183
+ }
1184
+ function tokenizeCss(css) {
1185
+ const strings = [];
1186
+ let code = "";
1187
+ let i = 0;
1188
+ const n = css.length;
1189
+ const readEscape = () => {
1190
+ const hex = css.slice(i + 1, i + 7).match(/^[0-9a-fA-F]{1,6}/);
1191
+ if (hex) {
1192
+ i += 1 + hex[0].length;
1193
+ if (css.startsWith("\r\n", i)) i += 2;
1194
+ else if (i < n && /[ \t\r\n\f]/.test(css[i])) i++;
1195
+ const cp = parseInt(hex[0], 16);
1196
+ return cp === 0 || cp > 1114111 || cp >= 55296 && cp <= 57343 ? "�" : String.fromCodePoint(cp);
1197
+ }
1198
+ const c = css[i + 1];
1199
+ if (c === void 0) {
1200
+ i += 1;
1201
+ return "�";
1202
+ }
1203
+ if (c === "\r" && css[i + 2] === "\n") {
1204
+ i += 3;
1205
+ return "";
1206
+ }
1207
+ if (c === "\n" || c === "\r" || c === "\f") {
1208
+ i += 2;
1209
+ return "";
1210
+ }
1211
+ i += 2;
1212
+ return c;
1213
+ };
1214
+ while (i < n) {
1215
+ const c = css[i];
1216
+ if (c === "/" && css[i + 1] === "*") {
1217
+ const end = css.indexOf("*/", i + 2);
1218
+ i = end < 0 ? n : end + 2;
1219
+ code += " ";
1220
+ continue;
1221
+ }
1222
+ if (c === '"' || c === "'") {
1223
+ const q = c;
1224
+ i++;
1225
+ let s = "";
1226
+ while (i < n) {
1227
+ const d = css[i];
1228
+ if (d === "\\") {
1229
+ s += readEscape();
1230
+ continue;
1231
+ }
1232
+ if (d === q) {
1233
+ i++;
1234
+ break;
1235
+ }
1236
+ if (d === "\n" || d === "\r" || d === "\f") break;
1237
+ s += d;
1238
+ i++;
1239
+ }
1240
+ strings.push(s);
1241
+ code += `${STR_MARK}${strings.length - 1}${STR_MARK}`;
1242
+ continue;
1243
+ }
1244
+ if (c === "\\") {
1245
+ code += readEscape();
1246
+ continue;
1247
+ }
1248
+ code += c;
1249
+ i++;
1250
+ }
1251
+ return { code, strings };
1252
+ }
1253
+ function extractUrls(css) {
1254
+ const out = [];
1255
+ const t = tokenizeCss(css);
1256
+ const code = t.code;
1257
+ const str = (token) => t.strings[Number(token.slice(1, -1))] ?? "";
1258
+ const re = /\burl\(\s*/gi;
1259
+ let m;
1260
+ while (m = re.exec(code)) {
1261
+ const i = m.index + m[0].length;
1262
+ const ph = code.slice(i).match(/^\u0001\d+\u0001/);
1263
+ let value = "";
1264
+ if (ph) value = str(ph[0]);
1265
+ else if (code.startsWith("#{&", i)) {
1266
+ const end = code.indexOf(")}", i);
1267
+ value = code.slice(i, end < 0 ? code.length : end + 2);
1268
+ } else {
1269
+ const end = code.indexOf(")", i);
1270
+ value = code.slice(i, end < 0 ? code.length : end);
1271
+ }
1272
+ out.push({ url: value.trim(), via: "url()" });
1273
+ }
1274
+ const fns = /\b(?:-webkit-)?(image-set|image|src)\(((?:[^()]|\([^()]*\))*)\)/gi;
1275
+ while (m = fns.exec(code)) {
1276
+ for (const s of m[2].matchAll(STR_TOKEN)) out.push({ url: str(s[0]).trim(), via: m[1].toLowerCase() });
1277
+ }
1278
+ const imp = /@import\s+(?:url\([^)]*\)|(\u0001\d+\u0001))/gi;
1279
+ while (m = imp.exec(code)) if (m[1] !== void 0) out.push({ url: str(m[1]).trim(), via: "@import" });
1280
+ return out;
1281
+ }
1282
+ function checkCss(css) {
1283
+ const errors = [];
1284
+ const externals = [];
1285
+ const { code } = tokenizeCss(css);
1286
+ if (/@import\b/i.test(code)) errors.push("@import は使えない(外部の CSS を読む)");
1287
+ if (/\bexpression\s*\(/i.test(code)) errors.push("expression( は使えない");
1288
+ if (/\bbehavior\s*:/i.test(code)) errors.push("behavior: は使えない");
1289
+ if (/-moz-binding\s*:/i.test(code)) errors.push("-moz-binding: は使えない");
1290
+ for (const u of extractUrls(css)) {
1291
+ const kind = classifyUrl(u.url);
1292
+ if (kind === "https") externals.push({ url: cleanUrl(u.url), via: u.via });
1293
+ else if (kind === "bad") errors.push(`${u.via} の URL が使えない形: ${u.url.slice(0, 80)}(https:、data:image/、#{&f(…)} だけ)`);
1294
+ }
1295
+ return { errors, externals };
1296
+ }
1297
+
1298
+ // src/normalize/html-check.ts
1299
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
1300
+ "div",
1301
+ "span",
1302
+ "p",
1303
+ "br",
1304
+ "hr",
1305
+ "img",
1306
+ "table",
1307
+ "thead",
1308
+ "tbody",
1309
+ "tfoot",
1310
+ "tr",
1311
+ "th",
1312
+ "td",
1313
+ "caption",
1314
+ "colgroup",
1315
+ "col",
1316
+ "ul",
1317
+ "ol",
1318
+ "li",
1319
+ "dl",
1320
+ "dt",
1321
+ "dd",
1322
+ "h1",
1323
+ "h2",
1324
+ "h3",
1325
+ "h4",
1326
+ "h5",
1327
+ "h6",
1328
+ "strong",
1329
+ "b",
1330
+ "em",
1331
+ "i",
1332
+ "u",
1333
+ "s",
1334
+ "small",
1335
+ "sub",
1336
+ "sup",
1337
+ "mark",
1338
+ "del",
1339
+ "ins",
1340
+ "a",
1341
+ "section",
1342
+ "article",
1343
+ "header",
1344
+ "footer",
1345
+ "main",
1346
+ "nav",
1347
+ "aside",
1348
+ "figure",
1349
+ "figcaption",
1350
+ "pre",
1351
+ "code",
1352
+ "blockquote",
1353
+ "q",
1354
+ "cite",
1355
+ "abbr",
1356
+ "time",
1357
+ "address",
1358
+ "label",
1359
+ "wbr",
1360
+ "iframe",
1361
+ "style",
1362
+ "ruby",
1363
+ "rt",
1364
+ "rp",
1365
+ "bdi",
1366
+ "bdo",
1367
+ "big",
1368
+ "center",
1369
+ "font",
1370
+ "tt",
1371
+ "kbd",
1372
+ "samp",
1373
+ "var",
1374
+ "dfn"
1375
+ ]);
1376
+ var DENIED_REASON = {
1377
+ script: "スクリプト",
1378
+ object: "外部の内容を埋め込む",
1379
+ embed: "外部の内容を埋め込む",
1380
+ applet: "外部の内容を埋め込む",
1381
+ link: "外部の CSS を読む",
1382
+ base: "リンクの基準を変える",
1383
+ meta: "文書の動きを変える(refresh など)",
1384
+ noscript: "",
1385
+ template: "",
1386
+ slot: "",
1387
+ portal: "",
1388
+ form: "送信の経路",
1389
+ input: "入力欄",
1390
+ button: "入力欄",
1391
+ select: "入力欄",
1392
+ textarea: "入力欄",
1393
+ option: "入力欄",
1394
+ optgroup: "入力欄",
1395
+ frame: "",
1396
+ frameset: "",
1397
+ svg: 'インラインの SVG は使わない(図は <img src="data:image/svg+xml,…"> か添付ファイルの #{&f(…)} で入れる)',
1398
+ math: "MathML は使わない",
1399
+ video: "動画は PDF にならない",
1400
+ audio: "音声は PDF にならない",
1401
+ source: "",
1402
+ track: "",
1403
+ picture: "<img src> だけ使う",
1404
+ canvas: "",
1405
+ map: "",
1406
+ area: "",
1407
+ dialog: "",
1408
+ details: "",
1409
+ summary: "",
1410
+ marquee: "",
1411
+ xmp: "",
1412
+ plaintext: "",
1413
+ listing: "",
1414
+ html: "",
1415
+ head: "",
1416
+ body: "",
1417
+ title: ""
1418
+ };
1419
+ var GLOBAL_ATTRS = /* @__PURE__ */ new Set(["class", "id", "style", "title", "lang", "dir", "hidden", "translate", "role", "align", "valign", "bgcolor", "border", "width", "height", "color", "nowrap", "char", "charoff"]);
1420
+ var TAG_ATTRS = {
1421
+ img: ["src", "srcset", "alt", "loading", "decoding", "sizes"],
1422
+ a: ["href", "target", "rel", "name"],
1423
+ td: ["colspan", "rowspan", "headers", "scope", "abbr"],
1424
+ th: ["colspan", "rowspan", "headers", "scope", "abbr"],
1425
+ col: ["span"],
1426
+ colgroup: ["span"],
1427
+ table: ["cellpadding", "cellspacing", "summary", "rules", "frame"],
1428
+ ol: ["start", "reversed", "type"],
1429
+ ul: ["type"],
1430
+ li: ["value"],
1431
+ time: ["datetime"],
1432
+ q: ["cite"],
1433
+ blockquote: ["cite"],
1434
+ ins: ["cite", "datetime"],
1435
+ del: ["cite", "datetime"],
1436
+ iframe: ["src", "name", "frameborder", "scrolling"],
1437
+ label: ["for"],
1438
+ style: ["media", "type"],
1439
+ font: ["face", "size"]
1440
+ };
1441
+ var URL_ATTRS = /* @__PURE__ */ new Set(["src", "srcset", "href", "cite"]);
1442
+ function tagError(tag, where = "") {
1443
+ const t = tag.toLowerCase();
1444
+ if (ALLOWED_TAGS.has(t)) return null;
1445
+ const reason = DENIED_REASON[t];
1446
+ return `<${t}> は使えない(${where}${where ? "。" : ""}${reason ? reason : "許可の一覧に無い要素"})`;
1447
+ }
1448
+ function attrAllowed(tag, name) {
1449
+ if (GLOBAL_ATTRS.has(name)) return true;
1450
+ if (name.startsWith("data-") || name.startsWith("aria-")) return true;
1451
+ if (tag === void 0) return Object.values(TAG_ATTRS).some((list) => list.includes(name));
1452
+ return (TAG_ATTRS[tag] ?? []).includes(name);
1453
+ }
1454
+ var windowPromise = null;
1455
+ async function dom() {
1456
+ if (!windowPromise) {
1457
+ windowPromise = (async () => {
1458
+ const { Window } = await import("happy-dom");
1459
+ return new Window({ url: "https://authoring.local/" });
1460
+ })();
1461
+ }
1462
+ return windowPromise;
1463
+ }
1464
+ function sameOrigin(url, baseUrl) {
1465
+ if (!baseUrl) return false;
1466
+ try {
1467
+ return new URL(url).origin === new URL(baseUrl).origin;
1468
+ } catch {
1469
+ return false;
1470
+ }
1471
+ }
1472
+ async function checkHtml(html, opt = {}) {
1473
+ const errors = [];
1474
+ const warnings = [];
1475
+ const externals = [];
1476
+ if (!html.trim()) return { errors, warnings, externals };
1477
+ const w = await dom();
1478
+ const doc = new w.DOMParser().parseFromString(`<!doctype html><html><body>${html}</body></html>`, "text/html");
1479
+ const root = doc.body ?? doc.documentElement;
1480
+ if (!root) return { errors, warnings, externals };
1481
+ const walk = (el, path13) => {
1482
+ const tag = el.tagName.toLowerCase();
1483
+ const here2 = `${path13}>${tag}`;
1484
+ const denied = tagError(tag, here2);
1485
+ if (denied) {
1486
+ errors.push(denied);
1487
+ return;
1488
+ }
1489
+ if (tag === "style") {
1490
+ const r = checkCss(el.textContent ?? "");
1491
+ for (const e of r.errors) errors.push(`<style>: ${e}`);
1492
+ for (const u of r.externals) externals.push({ url: u.url, via: `<style> ${u.via}` });
1493
+ }
1494
+ for (const a of Array.from(el.attributes)) {
1495
+ const name = a.name.toLowerCase();
1496
+ const value = a.value ?? "";
1497
+ if (name.startsWith("on")) {
1498
+ errors.push(`${name} 属性(イベント)は使えない(${here2})`);
1499
+ continue;
1500
+ }
1501
+ if (!attrAllowed(tag, name)) {
1502
+ errors.push(`${name} 属性は使えない(${here2}。許可の一覧に無い属性)`);
1503
+ continue;
1504
+ }
1505
+ if (value.includes("${")) {
1506
+ errors.push(`属性値に \${式} を置けない(${here2}[${name}]。ESC_HTML は引用符を逃がさないので属性が壊れる。属性の値は固定にする)`);
1507
+ continue;
1508
+ }
1509
+ if (name === "style") {
1510
+ const r = checkCss(`x{${value}}`);
1511
+ for (const e of r.errors) errors.push(`style 属性: ${e}(${here2})`);
1512
+ for (const u of r.externals) externals.push({ url: u.url, via: `${here2}[style] ${u.via}` });
1513
+ continue;
1514
+ }
1515
+ if (URL_ATTRS.has(name)) {
1516
+ const urls = name === "srcset" ? parseSrcsetUrls(value) : [value];
1517
+ for (const u of urls) {
1518
+ const kind = classifyUrl(u);
1519
+ const shown = cleanUrl(u).slice(0, 80);
1520
+ if (kind === "bad") {
1521
+ errors.push(`${name} の URL が使えない形: ${shown}(${here2}。https:、data:image/、#{&f(…)}、相対だけ)`);
1522
+ continue;
1523
+ }
1524
+ if (tag === "a" && (kind === "data-image" || kind === "data-svg")) {
1525
+ errors.push(`href に data: は使えない(${here2})`);
1526
+ continue;
1527
+ }
1528
+ if (tag === "iframe" && name === "src") {
1529
+ if (kind !== "https" || !sameOrigin(cleanUrl(u), opt.baseUrl)) errors.push(`iframe の src は利用者の kintone(${opt.baseUrl ?? ".env の KINTONE_BASE_URL。未設定なので iframe は使えない"})だけ: ${shown}(${here2})`);
1530
+ continue;
1531
+ }
1532
+ if (kind === "https") externals.push({ url: cleanUrl(u), via: `${here2}[${name}]` });
1533
+ }
1534
+ }
1535
+ }
1536
+ for (const c of Array.from(el.children)) walk(c, here2);
1537
+ };
1538
+ for (const c of Array.from(root.children)) walk(c, "");
1539
+ return { errors, warnings, externals };
1540
+ }
1541
+ function expressionsOf(html) {
1542
+ return [...html.matchAll(/\$\{(.*?)\}/g)].map((m) => m[1].trim());
1543
+ }
1544
+
1545
+ // src/normalize/policy.ts
1546
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
1547
+ import path8 from "node:path";
1548
+ var PolicyError = class extends Error {
1549
+ };
1550
+ var POLICY_FILE = path8.join("policy", "authoring-policy.json");
1551
+ var DEFAULT_ALLOWED_ORIGINS = ["https://fonts.googleapis.com", "https://fonts.gstatic.com"];
1552
+ var MAX_POLICY_BYTES = 1024 * 1024;
1553
+ var MAX_RULES = 200;
1554
+ var MAX_FILES = 100;
1555
+ function normalizeRel(p) {
1556
+ let s = String(p).replace(/\\/g, "/").replace(/\/{2,}/g, "/");
1557
+ while (s.startsWith("./")) s = s.slice(2);
1558
+ if (s.startsWith("/") || /^[a-zA-Z]:/.test(s) || s.split("/").includes("..")) throw new PolicyError(`files は作業フォルダーからの相対パスで書く(.. や絶対パスは使えない): ${p}`);
1559
+ return process.platform === "win32" ? s.toLowerCase() : s;
1560
+ }
1561
+ function fail(file, where, msg) {
1562
+ throw new PolicyError(`${file} の形が不正(${where}): ${msg}`);
1563
+ }
1564
+ function parsePolicy(text, file) {
1565
+ let raw;
1566
+ try {
1567
+ raw = JSON.parse(text);
1568
+ } catch (e) {
1569
+ fail(file, "$", `JSON として読めない: ${e.message}`);
1570
+ }
1571
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) fail(file, "$", "最上位はオブジェクト");
1572
+ const obj = raw;
1573
+ for (const k of Object.keys(obj)) if (k !== "allowExternal" && k !== "allowExternalRefs") fail(file, `$.${k}`, "使えるキーは allowExternal と allowExternalRefs だけ");
1574
+ let allowExternalRefs;
1575
+ if (obj.allowExternalRefs !== void 0) {
1576
+ const files = obj.allowExternalRefs;
1577
+ if (!Array.isArray(files) || files.some((f) => typeof f !== "string")) fail(file, "$.allowExternalRefs", "設定ファイルの相対パスの配列で書く");
1578
+ if (files.length > MAX_FILES) fail(file, "$.allowExternalRefs", `${MAX_FILES} 件まで`);
1579
+ allowExternalRefs = files.map((f) => {
1580
+ try {
1581
+ return normalizeRel(f);
1582
+ } catch (e) {
1583
+ return fail(file, "$.allowExternalRefs", e.message);
1584
+ }
1585
+ });
1586
+ }
1587
+ const list = obj.allowExternal ?? [];
1588
+ if (!Array.isArray(list)) fail(file, "$.allowExternal", "配列で書く");
1589
+ if (list.length > MAX_RULES) fail(file, "$.allowExternal", `${MAX_RULES} 件まで`);
1590
+ const rules = [];
1591
+ list.forEach((r, i) => {
1592
+ const at = `$.allowExternal[${i}]`;
1593
+ if (!r || typeof r !== "object" || Array.isArray(r)) fail(file, at, "オブジェクトで書く");
1594
+ const o = r;
1595
+ for (const k of Object.keys(o)) if (!["origin", "url", "files", "note"].includes(k)) fail(file, `${at}.${k}`, "使えるキーは origin / url / files / note");
1596
+ const rule = {};
1597
+ if (o.origin !== void 0) {
1598
+ if (typeof o.origin !== "string") fail(file, `${at}.origin`, "文字列で書く");
1599
+ let u;
1600
+ try {
1601
+ u = new URL(o.origin);
1602
+ } catch {
1603
+ fail(file, `${at}.origin`, `URL として読めない: ${o.origin}`);
1604
+ }
1605
+ if (u.protocol !== "https:" || u.username || u.password || u.origin !== o.origin.replace(/\/$/, "")) fail(file, `${at}.origin`, `https のオリジンだけを書く(例 https://cdn.example.com): ${o.origin}`);
1606
+ rule.origin = u.origin;
1607
+ }
1608
+ if (o.url !== void 0) {
1609
+ if (typeof o.url !== "string") fail(file, `${at}.url`, "文字列で書く");
1610
+ let u;
1611
+ try {
1612
+ u = new URL(o.url);
1613
+ } catch {
1614
+ fail(file, `${at}.url`, `URL として読めない: ${o.url}`);
1615
+ }
1616
+ if (u.protocol !== "https:" || u.username || u.password) fail(file, `${at}.url`, `https の完全な URL を書く: ${o.url}`);
1617
+ rule.url = u.href;
1618
+ }
1619
+ if (rule.origin === void 0 && rule.url === void 0) fail(file, at, "origin か url のどちらかが要る");
1620
+ if (o.files !== void 0) {
1621
+ if (!Array.isArray(o.files) || o.files.some((f) => typeof f !== "string")) fail(file, `${at}.files`, "文字列の配列で書く");
1622
+ if (o.files.length > MAX_FILES) fail(file, `${at}.files`, `${MAX_FILES} 件まで`);
1623
+ rule.files = o.files.map((f) => {
1624
+ try {
1625
+ return normalizeRel(f);
1626
+ } catch (e) {
1627
+ return fail(file, `${at}.files`, e.message);
1628
+ }
1629
+ });
1630
+ }
1631
+ if (o.note !== void 0) {
1632
+ if (typeof o.note !== "string") fail(file, `${at}.note`, "文字列で書く");
1633
+ rule.note = o.note;
1634
+ }
1635
+ rules.push(rule);
1636
+ });
1637
+ return { allowExternal: rules, ...allowExternalRefs ? { allowExternalRefs } : {}, file };
1638
+ }
1639
+ function isExternalRefsAllowed(policy, settingsFile) {
1640
+ if (!policy.allowExternalRefs?.length || !settingsFile) return false;
1641
+ try {
1642
+ return policy.allowExternalRefs.includes(normalizeRel(settingsFile));
1643
+ } catch {
1644
+ return false;
1645
+ }
1646
+ }
1647
+ function loadPolicy(opt) {
1648
+ const file = path8.join(opt.cwd, POLICY_FILE);
1649
+ if (!existsSync5(file)) return { allowExternal: [] };
1650
+ if (statSync3(file).size > MAX_POLICY_BYTES) throw new PolicyError(`${file} が大きすぎる(${MAX_POLICY_BYTES} バイトまで)`);
1651
+ return parsePolicy(readFileSync6(file, "utf8"), file);
1652
+ }
1653
+ function originOf(url) {
1654
+ try {
1655
+ return new URL(url).origin;
1656
+ } catch {
1657
+ return null;
1658
+ }
1659
+ }
1660
+ function hrefOf(url) {
1661
+ try {
1662
+ return new URL(url).href;
1663
+ } catch {
1664
+ return null;
1665
+ }
1666
+ }
1667
+ function isAllowed(policy, url, settingsFile) {
1668
+ const u = cleanUrl(url);
1669
+ const origin = originOf(u);
1670
+ const href = hrefOf(u);
1671
+ if (origin && DEFAULT_ALLOWED_ORIGINS.includes(origin)) return true;
1672
+ let rel2;
1673
+ if (settingsFile) {
1674
+ try {
1675
+ rel2 = normalizeRel(settingsFile);
1676
+ } catch {
1677
+ rel2 = void 0;
1678
+ }
1679
+ }
1680
+ for (const r of policy.allowExternal) {
1681
+ const hit = r.url && href && r.url === href || r.origin && origin && r.origin === origin;
1682
+ if (!hit) continue;
1683
+ if (r.files && r.files.length) {
1684
+ if (!rel2) continue;
1685
+ if (!r.files.includes(rel2)) continue;
1686
+ }
1687
+ return true;
1688
+ }
1689
+ return false;
1690
+ }
1691
+
1692
+ // src/normalize/checks.ts
1693
+ var RAW_HTML_FUNCTIONS = ["HTML", "FVAL", "TABLE_HTML", "RECS_HTML", "FIELDS_HTML", "PAGE_HTML", "TAGS_HTML", "UNESC_HTML", "ADD_TAGS", "TAG", "VTAG", "ATTR", "BATTR", "STYLE"];
1694
+ var SAFE_VALUE_TYPES = /* @__PURE__ */ new Set(["NUMBER", "CALC", "DATE", "DATETIME", "TIME", "CREATED_TIME", "UPDATED_TIME", "RECORD_NUMBER", "__REVISION__", "__ID__"]);
1695
+ var FORMAT_FUNCTIONS = /* @__PURE__ */ new Set(["FVAL", "DATE_FORMAT", "DATE_ADD", "YEN", "FIXED", "ROUND", "ROUNDUP", "ROUNDDOWN", "INT", "FLOOR", "CEIL", "ABS", "FSIZE", "DURATION_FORMAT", "TODAY", "NOW"]);
1696
+ var STRING_LITERAL = /^"(?:[^"\\]|\\.)*"$/s;
1697
+ var NUMBER_LITERAL = /^-?\d+(?:\.\d+)?$/;
1698
+ var IDENT = /^[\p{L}_$][\p{L}\p{N}_$]*$/u;
1699
+ var isPlainLiteral = (s) => STRING_LITERAL.test(s) && !/[<>&"']/.test(s.slice(1, -1));
1700
+ var isSafeReplacement = (s) => isPlainLiteral(s) || /^"<br\s*\/?>"$/i.test(s);
1701
+ var isSafeSearch = (s) => STRING_LITERAL.test(s) || /^NEWLINE\(\s*\)$/.test(s);
1702
+ function backslashEscapes(formula) {
1703
+ return [...new Set([...formula.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[0]).filter((lit) => /\\[nrt]/.test(lit)))];
1704
+ }
1705
+ function splitTopLevel(s, sep = ",") {
1706
+ const out = [];
1707
+ let depth = 0;
1708
+ let cur = "";
1709
+ let quote = null;
1710
+ for (let i = 0; i < s.length; i++) {
1711
+ const c = s[i];
1712
+ if (quote) {
1713
+ cur += c;
1714
+ if (c === "\\" && i + 1 < s.length) {
1715
+ cur += s[++i];
1716
+ continue;
1717
+ }
1718
+ if (c === quote) quote = null;
1719
+ continue;
1720
+ }
1721
+ if (c === '"' || c === "'") {
1722
+ quote = c;
1723
+ cur += c;
1724
+ continue;
1725
+ }
1726
+ if (c === "(") depth++;
1727
+ else if (c === ")") {
1728
+ depth--;
1729
+ if (depth < 0) return null;
1730
+ }
1731
+ if (c === sep && depth === 0) {
1732
+ out.push(cur.trim());
1733
+ cur = "";
1734
+ continue;
1735
+ }
1736
+ cur += c;
1737
+ }
1738
+ if (depth !== 0 || quote) return null;
1739
+ if (cur.trim() !== "" || out.length) out.push(cur.trim());
1740
+ return out;
1741
+ }
1742
+ function parseCall(e) {
1743
+ const m = e.match(/^([A-Z_][A-Z0-9_]*)\s*\(([\s\S]*)\)$/);
1744
+ if (!m) return null;
1745
+ const args = splitTopLevel(m[2]);
1746
+ if (!args) return null;
1747
+ return { name: m[1], args: args.length === 1 && args[0] === "" ? [] : args };
1748
+ }
1749
+ function commentInsideString(formulaSet) {
1750
+ let inString = false;
1751
+ for (let i = 0; i < formulaSet.length; i++) {
1752
+ const c = formulaSet[i];
1753
+ if (inString) {
1754
+ if (c === "\\" && formulaSet[i + 1] === '"') i++;
1755
+ else if (c === '"') inString = false;
1756
+ else if (c === "/" && formulaSet[i + 1] === "/") return true;
1757
+ continue;
1758
+ }
1759
+ if (c === '"') inString = true;
1760
+ else if (c === "/" && formulaSet[i + 1] === "/") {
1761
+ const nl = formulaSet.indexOf("\n", i);
1762
+ if (nl < 0) return false;
1763
+ i = nl;
1764
+ }
1765
+ }
1766
+ return false;
1767
+ }
1768
+ function stringLiterals(formula) {
1769
+ return [...formula.matchAll(/"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'/g)].map((m) => (m[1] ?? m[2] ?? "").replace(/\\(.)/g, "$1"));
1770
+ }
1771
+ function callsOf(formula, name) {
1772
+ const out = [];
1773
+ const re = new RegExp(`\\b${name}\\s*\\(`, "g");
1774
+ let m;
1775
+ while (m = re.exec(formula)) {
1776
+ let depth = 1;
1777
+ let i = m.index + m[0].length;
1778
+ let quote = null;
1779
+ const start = i;
1780
+ for (; i < formula.length; i++) {
1781
+ const c = formula[i];
1782
+ if (quote) {
1783
+ if (c === "\\") i++;
1784
+ else if (c === quote) quote = null;
1785
+ continue;
1786
+ }
1787
+ if (c === '"' || c === "'") quote = c;
1788
+ else if (c === "(") depth++;
1789
+ else if (c === ")") {
1790
+ depth--;
1791
+ if (depth === 0) break;
1792
+ }
1793
+ }
1794
+ const args = splitTopLevel(formula.slice(start, i));
1795
+ if (args) out.push(args.length === 1 && args[0] === "" ? [] : args);
1796
+ }
1797
+ return out;
1798
+ }
1799
+ function isNumericSafe(arg, pp) {
1800
+ const a = arg.trim();
1801
+ if (!a) return false;
1802
+ const call = parseCall(a);
1803
+ if (call && FORMAT_FUNCTIONS.has(call.name)) return isSafeExpression(a, pp);
1804
+ const tokens = a.match(/[\p{L}_$][\p{L}\p{N}_$]*|\d+(?:\.\d+)?|[+\-*/()]|\s+|[\s\S]/gu) ?? [];
1805
+ for (const t of tokens) {
1806
+ if (/^\s+$/.test(t) || /^[+\-*/()]$/.test(t) || NUMBER_LITERAL.test(t)) continue;
1807
+ if (IDENT.test(t)) {
1808
+ if (!SAFE_VALUE_TYPES.has(pp[t]?.type ?? "")) return false;
1809
+ continue;
1810
+ }
1811
+ return false;
1812
+ }
1813
+ return true;
1814
+ }
1815
+ function isSafeExpression(expr, pp) {
1816
+ const e = expr.trim();
1817
+ if (!e) return false;
1818
+ const terms = splitTopLevel(e, "&");
1819
+ if (terms && terms.length > 1) return terms.every((t) => t !== "" && isSafeExpression(t, pp));
1820
+ if (NUMBER_LITERAL.test(e)) return true;
1821
+ if (STRING_LITERAL.test(e)) return isPlainLiteral(e);
1822
+ if (IDENT.test(e)) return SAFE_VALUE_TYPES.has(pp[e]?.type ?? "");
1823
+ const call = parseCall(e);
1824
+ if (!call) return false;
1825
+ if (call.name === "ESC_HTML") return call.args.length >= 1;
1826
+ if (call.name === "REPLACE") return call.args.length === 3 && parseCall(call.args[0])?.name === "ESC_HTML" && isSafeSearch(call.args[1]) && isSafeReplacement(call.args[2]);
1827
+ if (FORMAT_FUNCTIONS.has(call.name)) {
1828
+ if (call.name === "TODAY" || call.name === "NOW") return call.args.length === 0;
1829
+ if (call.args.length === 0 || !isNumericSafe(call.args[0], pp)) return false;
1830
+ return call.args.slice(1).every((a) => STRING_LITERAL.test(a) ? !/[<>&]/.test(a) : NUMBER_LITERAL.test(a));
1831
+ }
1832
+ return false;
1833
+ }
1834
+ function approvedFinding(f, opt, where, url, via) {
1835
+ if (isAllowed(opt.policy, url, opt.settingsFile)) {
1836
+ f.info("external.allowed", where, `外部の URL(承認済み): ${url}(${via})`);
1837
+ } else {
1838
+ f.warning("external.url", where, `外部の URL: ${url}(${via})。利用者が承認するなら policy/authoring-policy.json の allowExternal に origin か url を書く(AI は書かない)`);
1839
+ }
1840
+ }
1841
+ function externalFinding(f, ctx, where, url, via, certain = true) {
1842
+ if (ctx.mode === "block") {
1843
+ const msg = `外部の URL: ${url}(${via})。外部参照が「除く」(externalRefs: "block")の設定では、印刷屋は描画の前に kintone 以外への読み込みを除くので帳票に出ない。添付ファイルの #{&f(…)} か data:image/ にする。外部をそのまま使うなら、利用者が externalRefs を "allow" にして policy/authoring-policy.json の allowExternalRefs にこの設定ファイルを書く(AI は書かない)`;
1844
+ if (certain) f.error("external.blocked", where, msg);
1845
+ else f.warning("external.blocked", where, `${msg}。計算式の文字列なので警告(帳票に入れば除かれる)`);
1846
+ return;
1847
+ }
1848
+ approvedFinding(f, ctx, where, url, via);
1849
+ }
1850
+ function escapeFinding(f, where, formulas) {
1851
+ const lits = [...new Set(formulas.flatMap(backslashEscapes))];
1852
+ if (lits.length) f.warning("formula.escape", where, `計算式の文字列は \\ を解釈しない: ${lits.slice(0, 3).join(" ")}("\\n" は改行でなく \\ と n の 2 文字なので、REPLACE(…, "\\n", "<br>") は何も置き換えない)。改行は NEWLINE()(例 REPLACE(ESC_HTML(備考), NEWLINE(), "<br>"))`);
1853
+ }
1854
+ var HTML_LIKE = /<\s*[a-zA-Z!/]/;
1855
+ var CSS_LIKE = /url\s*\(|@import|image-set\s*\(|expression\s*\(|behavior\s*:/i;
1856
+ async function checkFormulaHtml(formula, where, f, ctx) {
1857
+ const raw = RAW_HTML_FUNCTIONS.filter((fn) => new RegExp(`\\b${fn}\\s*\\(`).test(formula));
1858
+ if (raw.length) f.warning("formula.rawHtml", where, `HTML を作る・入れる関数: ${raw.join(", ")}(結果がそのまま帳票の HTML になる。印刷屋が描画の前に外部への読み込みとスクリプトを除くが、崩れは防げない。差分で人が見る)`);
1859
+ const dynamic = [];
1860
+ for (const name of ["TAG", "VTAG"]) {
1861
+ for (const args of callsOf(formula, name)) if (args[0] !== void 0 && !STRING_LITERAL.test(args[0])) dynamic.push(`${name} の要素名 ${args[0]}`);
1862
+ }
1863
+ for (const name of ["ATTR", "STYLE"]) {
1864
+ for (const args of callsOf(formula, name)) {
1865
+ args.forEach((a, i) => {
1866
+ if (a !== "" && !STRING_LITERAL.test(a)) dynamic.push(`${name} の${i % 2 === 0 ? "名前" : "値"} ${a}`);
1867
+ });
1868
+ }
1869
+ }
1870
+ for (const args of callsOf(formula, "BATTR")) for (const a of args) if (a !== "" && !STRING_LITERAL.test(a)) dynamic.push(`BATTR の属性名 ${a}`);
1871
+ if (dynamic.length) {
1872
+ const shown = [...new Set(dynamic)];
1873
+ f.warning("formula.attr", where, `要素名や属性にレコードの値や式を入れている: ${shown.slice(0, 5).join(" / ")}${shown.length > 5 ? ` 他 ${shown.length - 5}` : ""}(定数だけにする。印刷屋は描画の前に外部 URL と on 属性を除くが、属性の崩れは防げない)`);
1874
+ }
1875
+ const seen = /* @__PURE__ */ new Set();
1876
+ for (const lit of stringLiterals(formula)) {
1877
+ if (seen.has(lit)) continue;
1878
+ seen.add(lit);
1879
+ if (HTML_LIKE.test(lit)) {
1880
+ const h = await checkHtml(lit, { baseUrl: ctx.baseUrl });
1881
+ for (const e of h.errors) f.warning("formula.html", where, `計算式の文字列の HTML: ${e}(印刷屋が描画の前に外部への読み込みとスクリプトを除く。差分で人が見る)`);
1882
+ for (const u of h.externals) externalFinding(f, ctx, where, u.url, `計算式の文字列 ${u.via}`, false);
1883
+ } else if (CSS_LIKE.test(lit)) {
1884
+ const c = checkCss(/[{}]/.test(lit) ? lit : `x{${lit}}`);
1885
+ for (const e of c.errors) f.warning("formula.html", where, `計算式の文字列の CSS: ${e}(印刷屋が描画の前に外部への読み込みを除く)`);
1886
+ for (const u of c.externals) externalFinding(f, ctx, where, u.url, `計算式の文字列 CSS ${u.via}`, false);
1887
+ }
1888
+ }
1889
+ }
1890
+ async function checkBody(body, model, f, opt) {
1891
+ const { PAPER_NAMES, PAGE_SIZES, DPI_OPTIONS, PRINT_MODES } = model.api;
1892
+ const rows = body.pluginInfos ?? [];
1893
+ const pp = model.pp;
1894
+ const ext = body.externalRefs;
1895
+ const mode = ext === "allow" ? "allow" : "block";
1896
+ const ctx = { ...opt, mode, baseUrl: model.baseUrl || void 0 };
1897
+ if (ext !== "block" && ext !== "allow") {
1898
+ f.error("externalRefs.value", "共通の設定", `externalRefs は "block"(印刷屋が描画の前に kintone 以外への読み込みとスクリプトを除く)か "allow"(何も除かない。自己責任): ${JSON.stringify(ext)}`);
1899
+ } else if (ext === "allow") {
1900
+ if (isExternalRefsAllowed(opt.policy, opt.settingsFile)) {
1901
+ f.info("externalRefs.allowed", "共通の設定", `外部参照は「許可」(利用者が policy/authoring-policy.json の allowExternalRefs で承認済み)。印刷屋は帳票の HTML / CSS から何も除かない(Ver.5 と同じ。自己責任)。外部の URL は allowExternal で承認する`);
1902
+ } else {
1903
+ f.error("externalRefs.unapproved", "共通の設定", `外部参照が「許可」(externalRefs: "allow"。キーが無い Ver.5 の設定も印刷屋は許可で動く)。印刷屋は帳票の HTML / CSS から kintone 以外への読み込みとスクリプトを除かない(自己責任)ので利用者の承認が要る: 利用者が policy/authoring-policy.json の allowExternalRefs にこの設定ファイル(${opt.settingsFile ?? "ファイル名が分からない"})を書く(AI は書かない)。新しい設定は "block"(描画の前に除く)を明示する`);
1904
+ }
1905
+ }
1906
+ for (const [i, c] of (body.cssInfo ?? []).entries()) {
1907
+ if (!c.state || !c.css) continue;
1908
+ const where = `共通 CSS ${i + 1} 行目 (${c.name})`;
1909
+ const r = checkCss(c.css);
1910
+ for (const e of r.errors) f.error("css.rule", where, e);
1911
+ for (const u of r.externals) externalFinding(f, ctx, where, u.url, `CSS ${u.via}`);
1912
+ }
1913
+ const font = body.fontInfo;
1914
+ if (font?.enabled && font.cssUrl) {
1915
+ if (!/^https:\/\//.test(font.cssUrl)) f.error("font.url", "Web フォント", `cssUrl は https の URL: ${font.cssUrl.slice(0, 80)}`);
1916
+ else approvedFinding(f, opt, "Web フォント", font.cssUrl, "fontInfo.cssUrl");
1917
+ }
1918
+ const seenMenus = /* @__PURE__ */ new Map();
1919
+ for (const [i, row] of rows.entries()) {
1920
+ const label = rowLabel(row, i);
1921
+ if (row.state) {
1922
+ const prev = seenMenus.get(row.menu);
1923
+ if (prev !== void 0) f.error("menu.duplicate", label, `有効なボタンの名前が ${prev + 1} 番目と同じ: ${row.menu}(diff と preview の出力が区別できない。名前を変える)`);
1924
+ else seenMenus.set(row.menu, i);
1925
+ }
1926
+ const tags = row.tagsInfo;
1927
+ if (!tags) {
1928
+ if (row.state) f.error("tags.rows", label, "有効なボタンに HTML 設定(tagsInfo)が無い(ボタンは出るが帳票を作れない。無効にするか HTML 設定を書く)");
1929
+ else f.info("tags.missing", label, "無効なボタンに HTML 設定が無い");
1930
+ continue;
1931
+ }
1932
+ if (!PAPER_NAMES.includes(tags.pageSize)) f.error("tags.pageSize", label, `pageSize が使えない値: ${tags.pageSize}(${PAPER_NAMES.join(", ")})`);
1933
+ else if (!PAGE_SIZES.includes(tags.pageSize)) f.warning("tags.pageSize", label, `pageSize ${tags.pageSize} は設定画面の候補に無い(動くが画面で選べない)`);
1934
+ if (tags.orientation !== "p" && tags.orientation !== "l") f.error("tags.orientation", label, `orientation は p か l: ${tags.orientation}`);
1935
+ if (!DPI_OPTIONS.includes(String(tags.dpi))) f.error("tags.dpi", label, `dpi は "96" か "150": ${JSON.stringify(tags.dpi)}`);
1936
+ if (tags.printMode !== void 0 && !PRINT_MODES.includes(tags.printMode)) f.error("tags.printMode", label, `printMode は ${PRINT_MODES.join(" / ")}: ${tags.printMode}`);
1937
+ if (row.list && tags.printMode && tags.printMode !== "preview") f.info("tags.printMode", label, "一覧帳票では printMode は効かない(常にプレビュー)");
1938
+ if (tags.filecode) {
1939
+ const p = model.pp[tags.filecode];
1940
+ if (!p) f.error("tags.filecode", label, `filecode の項目が fields に無い: ${tags.filecode}`);
1941
+ else if (p.type !== "FILE") f.error("tags.filecode", label, `filecode は添付ファイル項目: ${tags.filecode} は ${p.type}`);
1942
+ else if (p.ptcode) f.error("tags.filecode", label, `filecode はテーブルの外の添付ファイル項目: ${tags.filecode} はテーブル ${p.ptcode} の中`);
1943
+ } else if (row.list === false && row.state) {
1944
+ f.info("tags.filecode", label, "保存先が無い(PC はダウンロード、iPhone は共有シート、Android の kintone アプリは案内)");
1945
+ }
1946
+ const fieldsInfo = tags.fieldsInfo;
1947
+ if (fieldsInfo.length < 2) f.error("tags.rows", label, `HTML 設定は 1 行目 $out(ボタン表示条件)、2 行目 $fname(ファイル名)、3 行目以降が帳票。行が ${fieldsInfo.length} しか無い`);
1948
+ if (fieldsInfo[0] && fieldsInfo[0].fieldcode !== "$out") f.error("tags.rows", label, `1 行目の fieldcode は $out: ${fieldsInfo[0].fieldcode}`);
1949
+ if (fieldsInfo[1] && fieldsInfo[1].fieldcode !== "$fname") f.error("tags.rows", label, `2 行目の fieldcode は $fname: ${fieldsInfo[1].fieldcode}`);
1950
+ if (!fieldsInfo.slice(2).some((t) => t.state && (t.html || t.formulaSet))) f.warning("tags.rows", label, "有効な帳票の行(3 行目以降の html か計算式)が無い");
1951
+ for (const [j, t] of fieldsInfo.entries()) {
1952
+ const where = `${label} / ${tagRowLabel(t, j)}`;
1953
+ for (const code of Object.keys(t.usedFields ?? {})) {
1954
+ if (code === UOG_MARK) continue;
1955
+ if (!fieldExists(model, code)) f.error("field.unknown", where, `項目が fields に無い: ${code}(ラベルで書いた、別アプリの項目、綴りの違い)`);
1956
+ }
1957
+ if (!t.state) continue;
1958
+ if (t.formulaSet && commentInsideString(t.formulaSet)) f.error("formula.comment", where, '計算式の文字列の中に // がある("https://…" など)。印刷屋は文字列の中でも // 以降をコメントとして捨てるので式が壊れる。URL は HTML の属性か ##目印## に置く');
1959
+ escapeFinding(f, where, [t.formula || "", ...t.html ? expressionsOf(t.html) : []]);
1960
+ const formula = t.formula || "";
1961
+ if (formula && j >= 2) await checkFormulaHtml(formula, where, f, ctx);
1962
+ if (t.html) {
1963
+ const r = await checkHtml(t.html, { baseUrl: ctx.baseUrl });
1964
+ for (const e of r.errors) f.error("html.rule", where, e);
1965
+ for (const w of r.warnings) f.warning("html.rule", where, w);
1966
+ for (const u of r.externals) externalFinding(f, ctx, where, u.url, u.via);
1967
+ const rawExprs = expressionsOf(t.html).filter((e) => !isSafeExpression(e, pp));
1968
+ if (rawExprs.length) f.warning("html.rawExpression", where, `\${式} が文字列をエスケープせずに差し込んでいる: ${rawExprs.slice(0, 5).map((e) => `\${${e}}`).join(" ")}${rawExprs.length > 5 ? ` 他 ${rawExprs.length - 5}` : ""}(文字列の項目は \${ESC_HTML(項目)}、複数行は \${REPLACE(ESC_HTML(項目), NEWLINE(), "<br>")}。数値・日付の項目と FVAL / DATE_FORMAT はそのままでよい)`);
1969
+ }
1970
+ if (t.css) {
1971
+ const r = checkCss(t.css);
1972
+ for (const e of r.errors) f.error("css.rule", where, e);
1973
+ for (const u of r.externals) externalFinding(f, ctx, where, u.url, `CSS ${u.via}`);
1974
+ }
1975
+ }
1976
+ const outRow = fieldsInfo[0];
1977
+ if (outRow?.state && outRow.usedFields && UOG_MARK in outRow.usedFields && !body.pluginUOG) {
1978
+ f.info("uog.condition", label, "ボタン表示条件で UINFO / OINFO / GINFO を使っているが pluginUOG は偽(更新項目だけで決まる仕様。実機では評価できる)");
1979
+ }
1980
+ for (const [j, c] of row.calcInfo.fieldsInfo.entries()) {
1981
+ if (!c.state) continue;
1982
+ if (c.formulaSet && commentInsideString(c.formulaSet)) f.error("formula.comment", `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, "計算式の文字列の中に // がある。印刷屋は文字列の中でも // 以降をコメントとして捨てるので式が壊れる");
1983
+ escapeFinding(f, `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, [c.formula || ""]);
1984
+ for (const code of Object.keys(c.usedFields ?? {})) {
1985
+ if (code === UOG_MARK) continue;
1986
+ if (!fieldExists(model, code)) f.error("field.unknown", `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, `項目が fields に無い: ${code}`);
1987
+ }
1988
+ }
1989
+ }
1990
+ }
1991
+
1992
+ // src/normalize/size.ts
1993
+ async function measureStored(data, api) {
1994
+ const limits = api.DEFAULT_LIMITS;
1995
+ try {
1996
+ const stored = await api.writeConfig(data, "compressed");
1997
+ const entries = Object.entries(stored);
1998
+ const totalBytes = entries.reduce((n, [k, v]) => n + Buffer.byteLength(k, "utf8") + Buffer.byteLength(v, "utf8"), 0);
1999
+ const split = entries.filter(([k]) => /^json\d*$/.test(k)).length;
2000
+ return { ok: true, totalBytes, maxTotalBytes: limits.maxTotalBytes, splitCount: split, maxValueLength: Math.max(0, ...entries.map(([, v]) => v.length)), maxValueLimit: limits.maxValueLength };
2001
+ } catch (e) {
2002
+ const message = e instanceof api.ConfigStoreError ? e.message : String(e?.message ?? e);
2003
+ return { ok: false, totalBytes: 0, maxTotalBytes: limits.maxTotalBytes, splitCount: 0, maxValueLength: 0, maxValueLimit: limits.maxValueLength, error: message };
2004
+ }
2005
+ }
2006
+ function formatSize(s) {
2007
+ if (!s.ok) return `保存値が kintone の制限を超える: ${s.error}`;
2008
+ const pct = Math.round(s.totalBytes / s.maxTotalBytes * 100);
2009
+ return `保存値(compressed)${s.totalBytes.toLocaleString()} bytes / 上限 ${s.maxTotalBytes.toLocaleString()}(${pct}%。残り ${(s.maxTotalBytes - s.totalBytes).toLocaleString()})、分割 ${s.splitCount}`;
2010
+ }
2011
+
2012
+ // src/commands/normalize.ts
2013
+ var PLUGIN_NAME = "印刷屋プラグイン";
2014
+ var MAX_INPUT_BYTES = 16 * 1024 * 1024;
2015
+ var InputError = class extends Error {
2016
+ };
2017
+ var pad2 = (n) => String(n).padStart(2, "0");
2018
+ function formatDate(d) {
2019
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
2020
+ }
2021
+ function jsonDiff(a, b, pathStr, out) {
2022
+ if (JSON.stringify(a) === JSON.stringify(b)) return;
2023
+ if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
2024
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) jsonDiff(a[k], b[k], `${pathStr}.${k}`, out);
2025
+ return;
2026
+ }
2027
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
2028
+ a.forEach((x, i) => jsonDiff(x, b[i], `${pathStr}[${i}]`, out));
2029
+ return;
2030
+ }
2031
+ const short = (v) => {
2032
+ const s = JSON.stringify(v);
2033
+ return s === void 0 ? "undefined" : s.length > 100 ? s.slice(0, 100) + "…" : s;
2034
+ };
2035
+ out.push(`${pathStr}: ${short(a)} → ${short(b)}`);
2036
+ }
2037
+ async function normalizeSettings(input) {
2038
+ const f = new Findings();
2039
+ const api = input.engine.api;
2040
+ const pluginVersion = api.pluginVersion;
2041
+ const where = input.settingsFile ?? "settings";
2042
+ let envelope;
2043
+ try {
2044
+ envelope = api.parseJsonSafely(input.settingsText, api.CONFIG_LIMITS);
2045
+ } catch (e) {
2046
+ f.error("json", where, `JSON として読めない、または大きすぎる(${api.CONFIG_LIMITS.maxBytes.toLocaleString()} バイトまで): ${e.message}`);
2047
+ return { findings: f, summary: "JSON を読めない" };
2048
+ }
2049
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
2050
+ f.error("json", where, "JSON の最上位はオブジェクト(封筒形式)");
2051
+ return { findings: f, summary: "封筒形式でない" };
2052
+ }
2053
+ if (envelope.pluginID !== api.pluginId) f.error("envelope.pluginID", where, `pluginID は "${api.pluginId}": ${JSON.stringify(envelope.pluginID)}(封筒なしの素の設定は作らない)`);
2054
+ const envVersion = String(envelope.PluginVersion ?? "");
2055
+ if (!isSupportedPluginVersion(envVersion)) f.error("envelope.version", where, `PluginVersion は印刷屋の版(Ver.${MIN_PLUGIN_VERSION} 以降、zip は ${pluginVersion}): ${JSON.stringify(envelope.PluginVersion)}`);
2056
+ else if (Number(envVersion) > Number(pluginVersion)) f.error("envelope.version", where, `PluginVersion ${envVersion} の設定を、それより古い印刷屋の zip(版 ${pluginVersion})で検査しようとしている。アプリに入れた版の zip を .env の PCRAFT_PLUGIN_ZIP に書く`);
2057
+ else if (envVersion !== pluginVersion) f.info("envelope.version.upgrade", where, `PluginVersion ${envVersion} の設定を印刷屋の版 ${pluginVersion}(zip)で検査し、書き出す封筒は ${pluginVersion} にする`);
2058
+ if (envelope.appId !== void 0) {
2059
+ if (typeof envelope.appId !== "number" || !Number.isInteger(envelope.appId) || envelope.appId <= 0) f.error("envelope.appId", where, `appId は正の整数(数値): ${JSON.stringify(envelope.appId)}`);
2060
+ else if (envelope.appId !== input.fields.appId) f.warning("envelope.appId", where, `appId ${envelope.appId} が fields のアプリ ${input.fields.appId} と違う`);
2061
+ }
2062
+ if (f.hasErrors) return { findings: f, summary: "封筒の誤り" };
2063
+ const inputBody = bodyOf(envelope);
2064
+ let inputClean;
2065
+ try {
2066
+ inputClean = api.validate(inputBody, api.CONFIG_SCHEMA, api.CONFIG_LIMITS);
2067
+ } catch (e) {
2068
+ if (e instanceof api.ValidationError) {
2069
+ f.error("schema", where, `設定のスキーマに合わない: ${e.message}`);
2070
+ return { findings: f, summary: "スキーマの誤り" };
2071
+ }
2072
+ throw e;
2073
+ }
2074
+ const model = buildModel(input.fields, api, input.baseUrl);
2075
+ if (input.baseUrl && typeof input.fields.baseUrl === "string" && input.fields.baseUrl && model.fieldsBaseUrl !== input.baseUrl) {
2076
+ f.warning("fields.baseUrl", where, `fields の baseUrl(${input.fields.baseUrl.slice(0, 60)})が .env の KINTONE_BASE_URL と違う。iframe の判定は .env の接続先で行う。fields を取り直すなら npx @rex0220/print-craft-authoring-tools fields --app ${input.fields.appId}`);
2077
+ }
2078
+ const body = deriveBody(JSON.parse(JSON.stringify(inputClean)), model, input.engine, f);
2079
+ const policy = input.policy ?? { allowExternal: [] };
2080
+ await checkBody(body, model, f, { policy, settingsFile: input.settingsFile });
2081
+ let cleaned;
2082
+ try {
2083
+ cleaned = api.validate(body, api.CONFIG_SCHEMA, api.CONFIG_LIMITS);
2084
+ } catch (e) {
2085
+ if (e instanceof api.ValidationError) {
2086
+ f.error("schema", where, `正規化した設定がスキーマに合わない: ${e.message}`);
2087
+ return { findings: f, body, summary: "スキーマの誤り" };
2088
+ }
2089
+ throw e;
2090
+ }
2091
+ const size = await measureStored(cleaned, api);
2092
+ if (!size.ok) f.error("size", where, formatSize(size));
2093
+ else f.info("size", where, formatSize(size));
2094
+ let checkDiffs;
2095
+ if (input.check) {
2096
+ checkDiffs = [];
2097
+ jsonDiff(inputClean, cleaned, "$", checkDiffs);
2098
+ }
2099
+ const rows = cleaned.pluginInfos ?? [];
2100
+ const summary = `アプリ ${input.fields.appId} ${input.fields.appName}、ボタン ${rows.length}(有効 ${rows.filter((r) => r.state).length})`;
2101
+ if (f.hasErrors) return { findings: f, body: cleaned, size, checkDiffs, summary };
2102
+ const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
2103
+ const output = {
2104
+ date: formatDate(now),
2105
+ pluginName: typeof envelope.pluginName === "string" && envelope.pluginName ? envelope.pluginName : PLUGIN_NAME,
2106
+ pluginID: api.pluginId,
2107
+ PluginVersion: pluginVersion,
2108
+ appId: input.fields.appId,
2109
+ appName: typeof envelope.appName === "string" && envelope.appName ? envelope.appName : input.fields.appName
2110
+ };
2111
+ for (const k of ENVELOPE_KEYS) if (!(k in output)) output[k] = envelope[k];
2112
+ Object.assign(output, cleaned);
2113
+ return { findings: f, output, body: cleaned, size, checkDiffs, summary };
2114
+ }
2115
+ function readTextLimited(file, maxBytes = MAX_INPUT_BYTES) {
2116
+ const size = statSync4(file).size;
2117
+ if (size > maxBytes) throw new InputError(`${file} が大きすぎる(${size.toLocaleString()} バイト。上限 ${maxBytes.toLocaleString()})`);
2118
+ return readFileSync7(file, "utf8");
2119
+ }
2120
+ function readJsonLimited(file, maxBytes = MAX_INPUT_BYTES) {
2121
+ let v;
2122
+ try {
2123
+ v = JSON.parse(readTextLimited(file, maxBytes));
2124
+ } catch (e) {
2125
+ if (e instanceof InputError) throw e;
2126
+ throw new InputError(`${file} を JSON として読めない: ${e.message}`);
2127
+ }
2128
+ if (!v || typeof v !== "object" || Array.isArray(v)) throw new InputError(`${file} の最上位はオブジェクト`);
2129
+ return v;
2130
+ }
2131
+ async function readFieldsFile(file) {
2132
+ const fields = readJsonLimited(file);
2133
+ if (!fields.properties || typeof fields.properties !== "object") throw new InputError(`${file} は fields コマンドの出力ではない(properties が無い)`);
2134
+ if (typeof fields.appId !== "number" || !Number.isInteger(fields.appId) || fields.appId <= 0) throw new InputError(`${file} の appId が正の整数でない`);
2135
+ return fields;
2136
+ }
2137
+ function relativeSettingsPath(file, cwd) {
2138
+ const rel2 = path9.relative(cwd, path9.resolve(cwd, file));
2139
+ return rel2.startsWith("..") ? file : rel2.replace(/\\/g, "/");
2140
+ }
2141
+
2142
+ // src/commands/save.ts
2143
+ import { createHash as createHash2 } from "node:crypto";
2144
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
2145
+ import path11 from "node:path";
2146
+
2147
+ // src/commit-file.ts
2148
+ import { randomBytes } from "node:crypto";
2149
+ import { constants, copyFileSync, linkSync, lstatSync as lstatSync2, readFileSync as readFileSync8, renameSync, rmSync, writeFileSync } from "node:fs";
2150
+ import path10 from "node:path";
2151
+ var LOCK_STALE_MS = 6e4;
2152
+ var FileExistsError = class extends Error {
2153
+ };
2154
+ var LockBusyError = class extends Error {
2155
+ };
2156
+ var errCode = (e) => e?.code;
2157
+ var rand = () => randomBytes(6).toString("hex");
2158
+ function sleepSync(ms) {
2159
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
2160
+ }
2161
+ function withRetry(fn, tries = 3) {
2162
+ for (let i = 1; ; i++) {
2163
+ try {
2164
+ return fn();
2165
+ } catch (e) {
2166
+ const code = errCode(e);
2167
+ if (i < tries && (code === "EPERM" || code === "EBUSY")) {
2168
+ sleepSync(20 * i);
2169
+ continue;
2170
+ }
2171
+ throw e;
2172
+ }
2173
+ }
2174
+ }
2175
+ function cleanupAll(steps, tries = 3) {
2176
+ const errors = [];
2177
+ for (const step of steps) {
2178
+ try {
2179
+ withRetry(step, tries);
2180
+ } catch (e) {
2181
+ errors.push(e instanceof Error ? e.message : String(e));
2182
+ }
2183
+ }
2184
+ return errors;
2185
+ }
2186
+ function tempPathFor(target) {
2187
+ return path10.join(path10.dirname(target), `.${path10.basename(target)}.${rand()}.tmp`);
2188
+ }
2189
+ function placeNew(tmp, target, link = linkSync) {
2190
+ try {
2191
+ link(tmp, target);
2192
+ return;
2193
+ } catch (e) {
2194
+ const code = errCode(e);
2195
+ if (code === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path10.basename(target)}`);
2196
+ if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "ENOSYS") throw e;
2197
+ }
2198
+ try {
2199
+ copyFileSync(tmp, target, constants.COPYFILE_EXCL);
2200
+ } catch (e) {
2201
+ if (errCode(e) === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path10.basename(target)}`);
2202
+ throw e;
2203
+ }
2204
+ }
2205
+ function writeNewFile(target, text, recheck = () => {
2206
+ }) {
2207
+ const tmp = tempPathFor(target);
2208
+ let placed = false;
2209
+ let failure;
2210
+ try {
2211
+ recheck();
2212
+ writeFileSync(tmp, text, { encoding: "utf8", flag: "wx" });
2213
+ recheck();
2214
+ placeNew(tmp, target);
2215
+ placed = true;
2216
+ } catch (e) {
2217
+ failure = e;
2218
+ }
2219
+ const cleanup = cleanupAll([() => rmSync(tmp, { force: true })]);
2220
+ if (!placed) throw failure;
2221
+ return { cleanup };
2222
+ }
2223
+ function lockPathFor(target) {
2224
+ return path10.join(path10.dirname(target), `.${path10.basename(target)}.pcraft-lock`);
2225
+ }
2226
+ function lstatOrNull(p) {
2227
+ try {
2228
+ return lstatSync2(p);
2229
+ } catch (e) {
2230
+ if (errCode(e) === "ENOENT") return null;
2231
+ throw e;
2232
+ }
2233
+ }
2234
+ function acquireLock(target, staleMs = LOCK_STALE_MS, hooks = {}) {
2235
+ const file = lockPathFor(target);
2236
+ const token = randomBytes(16).toString("hex");
2237
+ const busy = (why = "同じファイルへの別の保存が進行中。少し待ってからやり直す") => new LockBusyError(why);
2238
+ const tryCreate = () => {
2239
+ try {
2240
+ writeFileSync(file, token, { flag: "wx" });
2241
+ return true;
2242
+ } catch (e) {
2243
+ if (errCode(e) === "EEXIST") return false;
2244
+ throw e;
2245
+ }
2246
+ };
2247
+ const isStale = (st) => Date.now() - st.mtimeMs > staleMs;
2248
+ if (!tryCreate()) {
2249
+ const st = lstatOrNull(file);
2250
+ if (st && !st.isFile()) throw busy(`ロックのファイルが普通のファイルでない: ${path10.basename(file)}(利用者が確かめて消す)`);
2251
+ if (st && !isStale(st)) throw busy();
2252
+ if (st) {
2253
+ const aside = `${file}.${rand()}.stale`;
2254
+ hooks.beforeClaim?.();
2255
+ try {
2256
+ withRetry(() => renameSync(file, aside));
2257
+ } catch (e) {
2258
+ if (errCode(e) !== "ENOENT") throw e;
2259
+ }
2260
+ const moved = lstatOrNull(aside);
2261
+ if (moved) {
2262
+ if (!moved.isFile()) throw busy(`ロックのファイルが普通のファイルでない: ${path10.basename(aside)}(利用者が確かめて消す)`);
2263
+ if (!isStale(moved)) {
2264
+ hooks.beforeRestore?.();
2265
+ try {
2266
+ withRetry(() => placeNew(aside, file));
2267
+ } catch (e) {
2268
+ if (!(e instanceof FileExistsError)) throw busy(`新しいロックを戻せなかった(${errCode(e) ?? e.message}): ${path10.basename(aside)} を確かめて ${path10.basename(file)} に戻すか消す`);
2269
+ }
2270
+ cleanupAll([() => rmSync(aside, { force: true })]);
2271
+ throw busy();
2272
+ }
2273
+ cleanupAll([() => rmSync(aside, { force: true })]);
2274
+ }
2275
+ }
2276
+ if (!tryCreate()) throw busy();
2277
+ }
2278
+ const owned = () => {
2279
+ try {
2280
+ const st = lstatSync2(file);
2281
+ return st.isFile() && readFileSync8(file, "utf8") === token;
2282
+ } catch {
2283
+ return false;
2284
+ }
2285
+ };
2286
+ return {
2287
+ owned,
2288
+ release: () => {
2289
+ if (!owned()) return false;
2290
+ withRetry(() => rmSync(file, { force: true }));
2291
+ return true;
2292
+ }
2293
+ };
2294
+ }
2295
+
2296
+ // src/commands/save.ts
2297
+ var MAX_SAVE_INPUT_BYTES = 256 * 1024;
2298
+ function digestOf(text) {
2299
+ return createHash2("sha256").update(text).digest("hex");
2300
+ }
2301
+ var rel = (root, abs) => path11.relative(root, abs).replace(/\\/g, "/");
2302
+ var SaveDenied = class extends Error {
2303
+ };
2304
+ var ConflictError = class extends Error {
2305
+ };
2306
+ function fieldsFileFor(ctx, target, fields) {
2307
+ const folder = appFolderOfFile(ctx.root, target);
2308
+ if (folder) {
2309
+ const own = resolveRead(path11.join(folder.dir, "fields.json"), ctx.root);
2310
+ if (!existsSync6(own)) throw new InputError(`アプリのフォルダーに fields.json が無い: ${rel(ctx.root, own)}(fields コマンドか pcraft_fields で取る)`);
2311
+ if (fields !== void 0 && resolveRead(fields, ctx.root) !== own) throw new SaveDenied(`アプリのフォルダーの中の設定は、同じフォルダーの fields.json で検査する(fields は渡さないか、${rel(ctx.root, own)} にする)`);
2312
+ return own;
2313
+ }
2314
+ if (!fields) throw new InputError("fields(項目定義のファイル)が要る(アプリのフォルダー kintone/<ホスト名>/<番号>-…/ の中なら同じフォルダーの fields.json を使う)");
2315
+ const file = resolveRead(fields, ctx.root);
2316
+ if (!existsSync6(file)) throw new InputError(`項目定義のファイルが無い: ${rel(ctx.root, file)}(fields コマンドか pcraft_fields で取る)`);
2317
+ return file;
2318
+ }
2319
+ function tooLarge(text) {
2320
+ return Buffer.byteLength(text, "utf8") > MAX_SAVE_INPUT_BYTES;
2321
+ }
2322
+ function commit(ctx, target, text, mode, recheck) {
2323
+ const tmp = tempPathFor(target);
2324
+ let lock;
2325
+ let digest;
2326
+ let failure;
2327
+ const checkTarget = () => {
2328
+ if (resolveWrite(rel(ctx.root, target), WRITE_ROOTS.settings, ctx.root) !== target) throw new SaveDenied("書く先のフォルダーが途中で変わった(symlink など)");
2329
+ assertChangeAllowed(ctx.root, target, "settings");
2330
+ if (SNAPSHOT_RE.test(path11.basename(target))) throw new SaveDenied("ダウンロード / pull のファイルは書き換えない。edit で -edit.json に写して直す");
2331
+ };
2332
+ try {
2333
+ checkTarget();
2334
+ mkdirSync(path11.dirname(target), { recursive: true });
2335
+ writeFileSync2(tmp, text, { encoding: "utf8", flag: "wx" });
2336
+ lock = acquireLock(target);
2337
+ checkTarget();
2338
+ recheck();
2339
+ if (!lock.owned()) throw new ConflictError("ロックを失った(保存が 60 秒より長く止まった)。pcraft_buttons で読み直してやり直す");
2340
+ if (mode === "new") withRetry(() => placeNew(tmp, target));
2341
+ else withRetry(() => renameSync2(tmp, target));
2342
+ digest = digestOf(text);
2343
+ ctx.afterPlace?.();
2344
+ } catch (e) {
2345
+ failure = e;
2346
+ }
2347
+ const cleanup = cleanupAll([...lock ? [() => void lock.release()] : [], () => rmSync2(tmp, { force: true })]);
2348
+ if (digest === void 0) throw failure;
2349
+ return { digest, cleanup };
2350
+ }
2351
+ async function normalizeText(ctx, target, text, fields) {
2352
+ const fieldsData = await readFieldsFile(fieldsFileFor(ctx, target, fields));
2353
+ ctx.engine.setContext({ baseUrl: ctx.baseUrl ?? "https://example.cybozu.com", appId: fieldsData.appId });
2354
+ return normalizeSettings({ settingsText: text, settingsFile: rel(ctx.root, target), fields: fieldsData, engine: ctx.engine, policy: ctx.policy, baseUrl: ctx.baseUrl });
2355
+ }
2356
+ function statusOf(e) {
2357
+ if (e instanceof ConflictError || e instanceof FileExistsError || e instanceof LockBusyError) return "conflict";
2358
+ if (e instanceof SaveDenied || e instanceof PermissionError || e instanceof PathError) return "denied";
2359
+ if (e instanceof InputError || e instanceof SyntaxError) return "invalid";
2360
+ return "failed";
2361
+ }
2362
+ function codeOf(e) {
2363
+ if (e instanceof ConflictError || e instanceof FileExistsError || e instanceof LockBusyError) return "ConflictError";
2364
+ if (e instanceof SaveDenied || e instanceof PermissionError) return "PermissionError";
2365
+ return e instanceof Error ? e.constructor.name : "Error";
2366
+ }
2367
+ function errorResult(root, target, e, findings = []) {
2368
+ return { status: statusOf(e), code: codeOf(e), path: rel(root, target), findings, message: e instanceof Error ? e.message : String(e) };
2369
+ }
2370
+ function finish(ctx, target, findings, run) {
2371
+ try {
2372
+ const { digest, cleanup } = run();
2373
+ return { status: "ok", path: rel(ctx.root, target), digest, findings, ...cleanup.length ? { cleanup, message: `確定したが、一時ファイルかロックを消せなかった(.${path11.basename(target)}.* を消してよい)` } : {} };
2374
+ } catch (e) {
2375
+ return errorResult(ctx.root, target, e, findings);
2376
+ }
2377
+ }
2378
+ async function saveNewSettings(ctx, opt) {
2379
+ if (opt.expectedAbsent !== true) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: "新しい設定の保存は expectedAbsent: true が要る(既存のファイルを直すなら pcraft_update_button)" };
2380
+ if (tooLarge(opt.content)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `content は ${MAX_SAVE_INPUT_BYTES} バイトまで` };
2381
+ let target;
2382
+ try {
2383
+ target = resolveWrite(opt.path, WRITE_ROOTS.settings, ctx.root);
2384
+ if (SNAPSHOT_RE.test(path11.basename(target))) throw new SaveDenied("ダウンロード / pull の名前(rex0220-print-craft-app<番号>-<日時>.json)では新しく保存しない");
2385
+ assertChangeAllowed(ctx.root, target, "settings");
2386
+ } catch (e) {
2387
+ return errorResult(ctx.root, path11.resolve(ctx.root, opt.path), e);
2388
+ }
2389
+ if (existsSync6(target)) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "同じ名前のファイルがある(新しい設定の保存は上書きしない。直すなら pcraft_update_button)" };
2390
+ let result;
2391
+ try {
2392
+ result = await normalizeText(ctx, target, opt.content, opt.fields);
2393
+ } catch (e) {
2394
+ return errorResult(ctx.root, target, e);
2395
+ }
2396
+ if (!result.output) return { status: "invalid", code: "NormalizeError", path: rel(ctx.root, target), findings: result.findings.items, message: "normalize のエラーがあるので保存しない" };
2397
+ const text = JSON.stringify(result.output, null, 2) + "\n";
2398
+ return finish(
2399
+ ctx,
2400
+ target,
2401
+ result.findings.items,
2402
+ () => commit(ctx, target, text, "new", () => {
2403
+ if (existsSync6(target)) throw new ConflictError("確定の直前に同じ名前のファイルができた");
2404
+ })
2405
+ );
2406
+ }
2407
+ async function updateButton(ctx, opt) {
2408
+ if (tooLarge(opt.replacement)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `replacement は ${MAX_SAVE_INPUT_BYTES} バイトまで` };
2409
+ let target;
2410
+ try {
2411
+ target = resolveRead(opt.path, ctx.root);
2412
+ resolveWrite(opt.path, WRITE_ROOTS.settings, ctx.root);
2413
+ if (SNAPSHOT_RE.test(path11.basename(target))) throw new SaveDenied("ダウンロード / pull のファイルは書き換えない。edit で -edit.json に写して直す");
2414
+ assertChangeAllowed(ctx.root, target, "settings");
2415
+ } catch (e) {
2416
+ return errorResult(ctx.root, path11.resolve(ctx.root, opt.path), e);
2417
+ }
2418
+ if (!existsSync6(target)) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "ファイルが無い(新しい設定なら pcraft_save_settings)" };
2419
+ const current = readFileSync9(target);
2420
+ if (digestOf(current) !== opt.expectedDigest) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "読んだ後にファイルが変わった(digest が違う)。pcraft_buttons で読み直す" };
2421
+ let settings;
2422
+ let replacement;
2423
+ try {
2424
+ settings = JSON.parse(current.toString("utf8"));
2425
+ replacement = JSON.parse(opt.replacement);
2426
+ } catch (e) {
2427
+ return { status: "invalid", code: "InputError", path: rel(ctx.root, target), findings: [], message: `JSON として読めない: ${e.message}` };
2428
+ }
2429
+ if (!replacement || typeof replacement !== "object" || Array.isArray(replacement) || replacement.menu !== opt.button) {
2430
+ return { status: "invalid", code: "InputError", path: rel(ctx.root, target), findings: [], message: "replacement はボタン 1 つ(pluginInfos の 1 行)のオブジェクトで、menu が button と同じ" };
2431
+ }
2432
+ const rows = Array.isArray(settings.pluginInfos) ? [...settings.pluginInfos] : [];
2433
+ const i = rows.findIndex((r) => r && r.menu === opt.button);
2434
+ if (i >= 0) rows[i] = replacement;
2435
+ else rows.push(replacement);
2436
+ let result;
2437
+ try {
2438
+ result = await normalizeText(ctx, target, JSON.stringify({ ...settings, pluginInfos: rows }), opt.fields);
2439
+ } catch (e) {
2440
+ return errorResult(ctx.root, target, e);
2441
+ }
2442
+ if (!result.output) return { status: "invalid", code: "NormalizeError", path: rel(ctx.root, target), findings: result.findings.items, message: "normalize のエラーがあるので保存しない" };
2443
+ const text = JSON.stringify(result.output, null, 2) + "\n";
2444
+ return finish(
2445
+ ctx,
2446
+ target,
2447
+ result.findings.items,
2448
+ () => commit(ctx, target, text, "replace", () => {
2449
+ if (digestOf(readFileSync9(target)) !== opt.expectedDigest) throw new ConflictError("確定の直前にファイルが変わった(digest が違う)。pcraft_buttons で読み直す");
2450
+ })
2451
+ );
2452
+ }
2453
+
2454
+ // src/commands/buttons.ts
2455
+ import { createHash as createHash3 } from "node:crypto";
2456
+ var ButtonNotFoundError = class extends Error {
2457
+ };
2458
+ var kb = (v) => `${(Buffer.byteLength(JSON.stringify(v) ?? "", "utf8") / 1024).toFixed(1)} KB`;
2459
+ var oneLine = (s, max = 160) => {
2460
+ const t = s.replace(/\s+/g, " ").trim();
2461
+ return t.length > max ? `${t.slice(0, max)}…` : t;
2462
+ };
2463
+ var chars = (s) => [...s ?? ""].length;
2464
+ function shortenDataUrls(s, opt = {}) {
2465
+ return s.replace(/data:[^"'\s)<>]{120,}/g, (m) => `${m.slice(0, 40)}…(data URL ${m.length.toLocaleString()} 文字${opt.hash ? `、sha256 ${createHash3("sha256").update(m).digest("hex").slice(0, 8)}` : ""})`);
2466
+ }
2467
+ function screensOf(b) {
2468
+ const ids = (b.viewsCsv ?? "").split(",").map((s) => s.trim()).filter(Boolean);
2469
+ const lists = ids.filter((x) => x !== "-");
2470
+ const listText = ids.length === 0 ? "すべての一覧" : lists.length ? `一覧 ${lists.join(",")}` : "";
2471
+ if (b.list) return `一覧帳票(${listText || "どの一覧にも出ない"})`;
2472
+ const detail2 = ids.length === 0 || ids.includes("-");
2473
+ return [detail2 ? "詳細画面" : "", listText ? `${listText}で一括処理` : ""].filter(Boolean).join(" + ") || "どの画面にも出ない";
2474
+ }
2475
+ function headline(b, i) {
2476
+ const t = b.tagsInfo ?? {};
2477
+ const parts = [
2478
+ b.state ? "有効" : "無効",
2479
+ screensOf(b),
2480
+ t.filecode ? `保存先 ${t.filecode}` : "保存先なし(ダウンロード)",
2481
+ `${t.pageSize ?? "?"} ${t.orientation === "l" ? "横" : t.orientation === "p" ? "縦" : t.orientation ?? "?"} ${t.dpi ?? "?"} dpi`,
2482
+ `押したとき ${t.printMode ?? "preview"}`,
2483
+ ...b.authority ? ["権限で絞る"] : [],
2484
+ kb(b)
2485
+ ];
2486
+ return `${i + 1}. ${b.menu ?? "(名前なし)"}(${parts.join("、")})`;
2487
+ }
2488
+ function overview(b) {
2489
+ const rows = b.tagsInfo?.fieldsInfo ?? [];
2490
+ const [out, fname, ...body] = rows;
2491
+ const lines = [];
2492
+ lines.push(` 表示条件: ${out ? out.state && out.formulaSet?.trim() ? oneLine(out.formulaSet) : "なし(常に出す)" : "(1 行目が無い)"}`);
2493
+ lines.push(` ファイル名: ${fname ? fname.state && fname.formulaSet?.trim() ? oneLine(fname.formulaSet) : "なし" : "(2 行目が無い)"}`);
2494
+ const bodies = body.map((r, j) => `${j + 3} 行目 ${r.desc || "(説明なし)"}${r.state ? "" : "(無効)"}: HTML ${chars(r.html).toLocaleString()} 文字、CSS ${chars(r.css).toLocaleString()} 文字、計算式 ${(r.formulaSet ?? "").trim() ? (r.formulaSet ?? "").split("\n").length : 0} 行`);
2495
+ lines.push(` 帳票: ${bodies.length ? bodies.join(" / ") : "なし"}`);
2496
+ const calc = (b.calcInfo?.fieldsInfo ?? []).filter((c) => c.fieldcode !== "$out");
2497
+ lines.push(` 更新項目: ${calc.length ? calc.map((c) => `${c.fieldcode}${c.state ? "" : "(無効)"}`).join(", ") : "なし"}`);
2498
+ return lines;
2499
+ }
2500
+ function detail(b, i) {
2501
+ const lines = [headline(b, i)];
2502
+ for (const k of ["menu_en", "menu_ja", "menu_zh", "desc", "desc_en", "desc_ja", "desc_zh", "remark", "users", "organizations", "groups", "guest"]) {
2503
+ const v = b[k];
2504
+ if (typeof v === "string" && v) lines.push(`${k}: ${oneLine(v, 400)}`);
2505
+ }
2506
+ for (const [j, r] of (b.tagsInfo?.fieldsInfo ?? []).entries()) {
2507
+ const role = j === 0 ? "$out ボタン表示条件" : j === 1 ? "$fname ファイル名" : "帳票";
2508
+ lines.push(`--- HTML 設定 ${j + 1} 行目 ${role}(${r.state ? "有効" : "無効"})${r.desc ? ` ${r.desc}` : ""}${r.remark ? ` 備考: ${oneLine(r.remark, 200)}` : ""}`);
2509
+ if (j >= 2 && r.css) lines.push("CSS:", shortenDataUrls(r.css));
2510
+ if (j >= 2 && r.html) lines.push("HTML:", shortenDataUrls(r.html));
2511
+ if (r.formulaSet) lines.push("計算式:", shortenDataUrls(r.formulaSet));
2512
+ }
2513
+ const calc = b.calcInfo?.fieldsInfo ?? [];
2514
+ lines.push(`--- 更新項目 ${calc.length ? `${calc.length} 行` : "なし"}`);
2515
+ for (const [j, c] of calc.entries()) lines.push(`${j + 1} 行目 ${c.fieldcode}(${c.state ? "有効" : "無効"}): ${shortenDataUrls(c.formulaSet ?? "")}${c.remark ? ` 備考: ${oneLine(c.remark, 200)}` : ""}`);
2516
+ return lines;
2517
+ }
2518
+ function listButtons(settings, opt) {
2519
+ const buttons = Array.isArray(settings.pluginInfos) ? settings.pluginInfos : [];
2520
+ if (opt.button !== void 0) {
2521
+ const hits = buttons.map((b, i) => ({ b, i })).filter(({ b }) => b.menu === opt.button);
2522
+ if (!hits.length) throw new ButtonNotFoundError(`ボタン「${opt.button}」は ${opt.file} に無い(${buttons.map((b) => b.menu).join(", ") || "ボタンなし"})`);
2523
+ return [...hits.flatMap(({ b, i }) => detail(b, i)), "(data: の URL は先頭と長さだけ。ファイルには元のまま)"].join("\n");
2524
+ }
2525
+ const normalized = buttons.some((b) => (b.tagsInfo?.fieldsInfo ?? []).some((r) => typeof r.formula === "string"));
2526
+ const font = settings.fontInfo;
2527
+ const css = Array.isArray(settings.cssInfo) ? settings.cssInfo.length : 0;
2528
+ const ext = settings.externalRefs;
2529
+ const header = [
2530
+ settings.appId !== void 0 ? `アプリ ${String(settings.appId)} ${typeof settings.appName === "string" ? settings.appName : ""}`.trim() : "アプリ不明",
2531
+ `ボタン ${buttons.length}(有効 ${buttons.filter((b) => b.state).length})`,
2532
+ `外部参照 ${ext === "block" || ext === "allow" ? ext : ext === void 0 ? "キー無し(印刷屋は allow で動く)" : JSON.stringify(ext)}`,
2533
+ `共通 CSS ${settings.commonCssEnable === false ? "無効" : css ? `${css} 行` : "既定(省略)"}`,
2534
+ `Web フォント ${font?.enabled ? font.family ?? "有効" : "なし"}`,
2535
+ normalized ? "派生値あり(normalize 済み)" : "派生値なし",
2536
+ kb(settings)
2537
+ ];
2538
+ const lines = [`${opt.file}(${header.join("、")})`];
2539
+ for (const [i, b] of buttons.entries()) lines.push(headline(b, i), ...overview(b));
2540
+ lines.push("中身は --button <名前>");
2541
+ return lines.join("\n");
2542
+ }
2543
+
2544
+ // src/normalize/line-diff.ts
2545
+ var MAX_CELLS = 4e6;
2546
+ function lineDiff(a, b) {
2547
+ const A = a.split("\n");
2548
+ const B = b.split("\n");
2549
+ if (A.length * B.length > MAX_CELLS) {
2550
+ return [...A.map((t) => ({ kind: "-", text: t })), ...B.map((t) => ({ kind: "+", text: t }))];
2551
+ }
2552
+ const n = A.length;
2553
+ const m = B.length;
2554
+ const dp = [];
2555
+ for (let i2 = 0; i2 <= n; i2++) dp.push(new Uint32Array(m + 1));
2556
+ for (let i2 = n - 1; i2 >= 0; i2--) for (let j2 = m - 1; j2 >= 0; j2--) dp[i2][j2] = A[i2] === B[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
2557
+ const out = [];
2558
+ let i = 0;
2559
+ let j = 0;
2560
+ while (i < n && j < m) {
2561
+ if (A[i] === B[j]) {
2562
+ out.push({ kind: " ", text: A[i] });
2563
+ i++;
2564
+ j++;
2565
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
2566
+ out.push({ kind: "-", text: A[i] });
2567
+ i++;
2568
+ } else {
2569
+ out.push({ kind: "+", text: B[j] });
2570
+ j++;
2571
+ }
2572
+ }
2573
+ while (i < n) out.push({ kind: "-", text: A[i++] });
2574
+ while (j < m) out.push({ kind: "+", text: B[j++] });
2575
+ return out;
2576
+ }
2577
+ function formatDiff(lines, context = 2) {
2578
+ const keep = new Array(lines.length).fill(false);
2579
+ lines.forEach((l, i) => {
2580
+ if (l.kind === " ") return;
2581
+ for (let k = Math.max(0, i - context); k <= Math.min(lines.length - 1, i + context); k++) keep[k] = true;
2582
+ });
2583
+ const out = [];
2584
+ let skipping = false;
2585
+ lines.forEach((l, i) => {
2586
+ if (!keep[i]) {
2587
+ if (!skipping) out.push(" …");
2588
+ skipping = true;
2589
+ return;
2590
+ }
2591
+ skipping = false;
2592
+ out.push(`${l.kind} ${l.text}`);
2593
+ });
2594
+ return out.join("\n");
2595
+ }
2596
+
2597
+ // src/commands/diff.ts
2598
+ var DERIVED = /* @__PURE__ */ new Set(["formula", "usedFields", "id", "views", "pluginUOG"]);
2599
+ var ROW_SCALARS = ["state", "menu", "menu_en", "menu_ja", "menu_zh", "mcheck", "list", "authority", "guest", "users", "organizations", "groups", "desc", "desc_en", "desc_ja", "desc_zh", "remark", "viewsCsv"];
2600
+ var TAGS_SCALARS = ["filecode", "pageSize", "orientation", "dpi", "printMode"];
2601
+ var TAG_ROW_SCALARS = ["state", "desc", "remark", "fieldcode"];
2602
+ var TAG_ROW_TEXTS = ["css", "html", "formulaSet"];
2603
+ var show = (v) => v === void 0 ? "(無し)" : JSON.stringify(v);
2604
+ function scalarDiffs(a, b, keys, out, prefix) {
2605
+ for (const k of keys) {
2606
+ const x = a?.[k];
2607
+ const y = b?.[k];
2608
+ if (JSON.stringify(x) !== JSON.stringify(y)) out.push(`${prefix}${k}: ${show(x)} → ${show(y)}`);
2609
+ }
2610
+ }
2611
+ function textDiff(label, a, b, out) {
2612
+ if (a === b) return;
2613
+ out.push(`${label}:`);
2614
+ out.push(formatDiff(lineDiff(a, b)));
2615
+ }
2616
+ function diffSettings(beforeEnvelope, afterEnvelope, opt = {}) {
2617
+ const out = [];
2618
+ const a = bodyOf(beforeEnvelope);
2619
+ const b = bodyOf(afterEnvelope);
2620
+ for (const k of ["appId", "appName", "PluginVersion"]) if (JSON.stringify(beforeEnvelope[k]) !== JSON.stringify(afterEnvelope[k])) out.push(`封筒 ${k}: ${show(beforeEnvelope[k])} → ${show(afterEnvelope[k])}`);
2621
+ scalarDiffs(a, b, ["pluginEnable", "pluginComment", "pluginDescription", "commonCssEnable", "externalRefs"], out, "");
2622
+ if (JSON.stringify(a.menuInfo) !== JSON.stringify(b.menuInfo)) out.push(`menuInfo: ${show(a.menuInfo)} → ${show(b.menuInfo)}`);
2623
+ if (JSON.stringify(a.fontInfo) !== JSON.stringify(b.fontInfo)) out.push(`Web フォント: ${show(a.fontInfo)} → ${show(b.fontInfo)}`);
2624
+ if (JSON.stringify(a.guestsInfo) !== JSON.stringify(b.guestsInfo)) out.push(`ゲスト: ${show(a.guestsInfo)} → ${show(b.guestsInfo)}`);
2625
+ const cssA = new Map((a.cssInfo ?? []).map((c, i) => [String(c.name ?? i), c]));
2626
+ const cssB = new Map((b.cssInfo ?? []).map((c, i) => [String(c.name ?? i), c]));
2627
+ for (const name of /* @__PURE__ */ new Set([...cssA.keys(), ...cssB.keys()])) {
2628
+ const x = cssA.get(name);
2629
+ const y = cssB.get(name);
2630
+ if (!x) out.push(`共通 CSS ${name}: 追加`);
2631
+ else if (!y) out.push(`共通 CSS ${name}: 削除`);
2632
+ else {
2633
+ scalarDiffs(x, y, ["state", "desc"], out, `共通 CSS ${name} / `);
2634
+ textDiff(`共通 CSS ${name} / css`, String(x.css ?? ""), String(y.css ?? ""), out);
2635
+ }
2636
+ }
2637
+ const key = (r, i, seen) => {
2638
+ const base = String(r.menu ?? `#${i + 1}`);
2639
+ const n = (seen.get(base) ?? 0) + 1;
2640
+ seen.set(base, n);
2641
+ return n === 1 ? base : `${base} (${n})`;
2642
+ };
2643
+ const seenA = /* @__PURE__ */ new Map();
2644
+ const seenB = /* @__PURE__ */ new Map();
2645
+ const rowsA = new Map((a.pluginInfos ?? []).map((r, i) => [key(r, i, seenA), r]));
2646
+ const rowsB = new Map((b.pluginInfos ?? []).map((r, i) => [key(r, i, seenB), r]));
2647
+ for (const name of /* @__PURE__ */ new Set([...rowsA.keys(), ...rowsB.keys()])) {
2648
+ const x = rowsA.get(name);
2649
+ const y = rowsB.get(name);
2650
+ const p = `ボタン ${name} / `;
2651
+ if (!x) {
2652
+ out.push(`ボタン ${name}: 追加`);
2653
+ continue;
2654
+ }
2655
+ if (!y) {
2656
+ out.push(`ボタン ${name}: 削除`);
2657
+ continue;
2658
+ }
2659
+ scalarDiffs(x, y, ROW_SCALARS, out, p);
2660
+ const tx = x.tagsInfo;
2661
+ const ty = y.tagsInfo;
2662
+ if (!!tx !== !!ty) out.push(`${p}HTML 設定: ${tx ? "削除" : "追加"}`);
2663
+ else if (tx && ty) {
2664
+ scalarDiffs(tx, ty, TAGS_SCALARS, out, `${p}HTML 設定 / `);
2665
+ const fa = tx.fieldsInfo ?? [];
2666
+ const fb = ty.fieldsInfo ?? [];
2667
+ for (let i = 0; i < Math.max(fa.length, fb.length); i++) {
2668
+ const ra = fa[i];
2669
+ const rb = fb[i];
2670
+ const q = `${p}HTML 設定 ${i + 1} 行目 / `;
2671
+ if (!ra) {
2672
+ out.push(`${q}追加 (${show(rb?.desc)})`);
2673
+ continue;
2674
+ }
2675
+ if (!rb) {
2676
+ out.push(`${q}削除 (${show(ra.desc)})`);
2677
+ continue;
2678
+ }
2679
+ scalarDiffs(ra, rb, TAG_ROW_SCALARS, out, q);
2680
+ for (const k of TAG_ROW_TEXTS) textDiff(`${q}${k}`, String(ra[k] ?? ""), String(rb[k] ?? ""), out);
2681
+ if (opt.derived) scalarDiffs(ra, rb, ["id", "formula", "usedFields"], out, `${q}派生 `);
2682
+ }
2683
+ }
2684
+ const ca = new Map((x.calcInfo?.fieldsInfo ?? []).map((c) => [String(c.fieldcode), c]));
2685
+ const cb = new Map((y.calcInfo?.fieldsInfo ?? []).map((c) => [String(c.fieldcode), c]));
2686
+ for (const code of /* @__PURE__ */ new Set([...ca.keys(), ...cb.keys()])) {
2687
+ const u = ca.get(code);
2688
+ const v = cb.get(code);
2689
+ const q = `${p}更新項目 ${code} / `;
2690
+ if (!u) {
2691
+ if (v?.state || v?.formulaSet) out.push(`${q}追加(state ${show(v?.state)})`);
2692
+ continue;
2693
+ }
2694
+ if (!v) {
2695
+ if (u.state || u.formulaSet) out.push(`${q}削除`);
2696
+ continue;
2697
+ }
2698
+ scalarDiffs(u, v, ["state", "remark"], out, q);
2699
+ textDiff(`${q}formulaSet`, String(u.formulaSet ?? ""), String(v.formulaSet ?? ""), out);
2700
+ if (opt.derived) scalarDiffs(u, v, ["formula", "usedFields", "type", "ptcode"], out, `${q}派生 `);
2701
+ }
2702
+ if (opt.derived) scalarDiffs(x, y, ["id", "views"], out, `${p}派生 `);
2703
+ }
2704
+ if (opt.derived) scalarDiffs(a, b, ["usedFields", "pluginUOG"], out, "派生 ");
2705
+ const derivedNote = opt.derived ? "" : "(派生値 formula / usedFields / id / views / pluginUOG は除く。--derived で含める)";
2706
+ const body = shortenDataUrls(out.join("\n"), { hash: true });
2707
+ const dataNote = body.includes("…(data URL ") ? "(data: の URL は先頭・長さ・sha256 の先頭だけ)" : "";
2708
+ return out.length ? `${body}
2709
+ ${derivedNote}${dataNote}`.trim() : `差分なし${derivedNote}`;
2710
+ }
2711
+ var DERIVED_KEYS = DERIVED;
2712
+
2713
+ // src/commands/fields.ts
2714
+ async function fetchFields(client, opt) {
2715
+ const lang = opt.lang ?? "ja";
2716
+ const app = await client.get("app", { id: opt.app }, opt.guestSpaceId);
2717
+ const fields = await client.get(opt.preview ? "preview/app/form/fields" : "app/form/fields", { app: opt.app, lang }, opt.guestSpaceId);
2718
+ const layout = await client.get(opt.preview ? "preview/app/form/layout" : "app/form/layout", { app: opt.app }, opt.guestSpaceId);
2719
+ return {
2720
+ tool: "pcraft-authoring fields",
2721
+ fetchedAt: (opt.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
2722
+ baseUrl: client.baseUrl,
2723
+ appId: Number(app.appId),
2724
+ appName: app.name,
2725
+ ...opt.guestSpaceId ? { guestSpaceId: opt.guestSpaceId } : {},
2726
+ preview: !!opt.preview,
2727
+ lang,
2728
+ revision: String(fields.revision ?? layout.revision ?? ""),
2729
+ properties: fields.properties ?? {},
2730
+ layout: layout.layout ?? []
2731
+ };
2732
+ }
2733
+ function layoutOrder(layout) {
2734
+ const out = [];
2735
+ const walk = (rows) => {
2736
+ for (const r of rows ?? []) {
2737
+ if (r.type === "GROUP") {
2738
+ if (r.code) out.push(r.code);
2739
+ walk(r.layout);
2740
+ } else if (r.type === "SUBTABLE") {
2741
+ if (r.code) out.push(r.code);
2742
+ } else {
2743
+ for (const f of r.fields ?? []) if (f.code) out.push(f.code);
2744
+ }
2745
+ }
2746
+ };
2747
+ walk(layout);
2748
+ return out;
2749
+ }
2750
+ var SELECT_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "RADIO_BUTTON", "DROP_DOWN", "MULTI_SELECT"]);
2751
+ function fieldNotes(p, inTable, copyTargets) {
2752
+ const notes = [];
2753
+ if (p.type === "CALC" && typeof p.format === "string") notes.push(`書式 ${p.format}`);
2754
+ if (p.type === "SINGLE_LINE_TEXT" && typeof p.expression === "string" && p.expression) notes.push("自動計算");
2755
+ if (p.type === "NUMBER" || p.type === "CALC") {
2756
+ if (p.digit === true) notes.push("桁区切り");
2757
+ if (p.displayScale !== void 0 && p.displayScale !== "") notes.push(`小数 ${p.displayScale} 桁`);
2758
+ if (typeof p.unit === "string" && p.unit) notes.push(`単位「${p.unit}」${p.unitPosition === "BEFORE" ? "前" : "後"}`);
2759
+ }
2760
+ if (SELECT_TYPES.has(p.type) && p.options && typeof p.options === "object") {
2761
+ const labels = Object.values(p.options).sort((a, b) => Number(a.index) - Number(b.index)).map((o) => o.label);
2762
+ notes.push(`選択肢 ${labels.slice(0, 8).join(" / ")}${labels.length > 8 ? ` 他 ${labels.length - 8}` : ""}`);
2763
+ }
2764
+ const lookup = p.lookup;
2765
+ if (lookup && typeof lookup === "object") notes.push(`ルックアップ(アプリ ${lookup.relatedApp?.app ?? "?"} の ${lookup.relatedKeyField ?? "?"})`);
2766
+ if (copyTargets.has(p.code)) notes.push("ルックアップのコピー先");
2767
+ if (p.type === "FILE" && !inTable) notes.push("保存先 filecode にできる");
2768
+ return notes.length ? `(${notes.join("、")})` : "";
2769
+ }
2770
+ function listFields(file) {
2771
+ const props = file.properties;
2772
+ const copyTargets = /* @__PURE__ */ new Set();
2773
+ const collect = (p) => {
2774
+ const maps = p.lookup?.fieldMappings;
2775
+ for (const m of maps ?? []) if (m.field) copyTargets.add(m.field);
2776
+ for (const c of Object.values(p.fields ?? {})) collect(c);
2777
+ };
2778
+ for (const p of Object.values(props)) collect(p);
2779
+ const line = (p, indent, inTable) => `${indent}${p.code}${p.label && p.label !== p.code ? `「${p.label}」` : ""} ${p.type}${fieldNotes(p, inTable, copyTargets)}`;
2780
+ const lines = [];
2781
+ const shown = /* @__PURE__ */ new Set();
2782
+ for (const code of layoutOrder(file.layout)) {
2783
+ const p = props[code];
2784
+ if (!p || shown.has(code)) continue;
2785
+ shown.add(code);
2786
+ lines.push(line(p, "", false));
2787
+ if (p.type === "SUBTABLE") for (const c of Object.values(p.fields ?? {})) lines.push(line(c, " ", true));
2788
+ }
2789
+ const rest = Object.values(props).filter((p) => !shown.has(p.code));
2790
+ const header = `アプリ ${file.appId} ${file.appName}(${file.preview ? "preview" : "運用中"}、lang=${file.lang}、取得 ${file.fetchedAt})項目 ${Object.keys(props).length}。レイアウトの順、テーブルの子は字下げ(TABLE_HTML と計算式で使う。更新項目にはできない)`;
2791
+ const tail = rest.length ? [`レイアウトに無い項目: ${rest.map((p) => `${p.code} ${p.type}`).join(", ")}`] : [];
2792
+ return [header, ...lines, ...tail].join("\n");
2793
+ }
2794
+ function summarizeFields(file) {
2795
+ const props = Object.values(file.properties);
2796
+ const tables = props.filter((p) => p.type === "SUBTABLE");
2797
+ const byType = /* @__PURE__ */ new Map();
2798
+ for (const p of props) byType.set(p.type, (byType.get(p.type) ?? 0) + 1);
2799
+ const types = [...byType.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} ${n}`).join(", ");
2800
+ const tableLines = tables.map((t) => ` テーブル ${t.code}: ${Object.keys(t.fields ?? {}).join(", ")}`);
2801
+ return [`アプリ ${file.appId} ${file.appName}(${file.preview ? "preview" : "運用中"}、lang=${file.lang})`, `項目 ${props.length}(${types})`, ...tableLines].join("\n");
2802
+ }
2803
+
2804
+ // src/commands/record.ts
2805
+ var ALWAYS = /* @__PURE__ */ new Set(["$id", "$revision"]);
2806
+ function identifiersOf(expr) {
2807
+ return expr.replace(/"[^"]*"/g, " ").split(/[^\p{L}\p{N}_・_$¥$]+/u).filter((t) => t && !/^\d+$/.test(t));
2808
+ }
2809
+ function usedFieldCodes(settings) {
2810
+ const codes = /* @__PURE__ */ new Set();
2811
+ const s = settings;
2812
+ if (!s || typeof s !== "object") return null;
2813
+ const addKeys = (o) => {
2814
+ if (o && typeof o === "object") for (const k of Object.keys(o)) codes.add(k);
2815
+ };
2816
+ const addWords = (text) => {
2817
+ if (typeof text === "string") for (const w of identifiersOf(text)) codes.add(w);
2818
+ };
2819
+ const addFormula = (f) => addWords(typeof f.formula === "string" && f.formula ? f.formula : f.formulaSet);
2820
+ addKeys(s.usedFields);
2821
+ for (const row of s.pluginInfos ?? []) {
2822
+ const tags = row.tagsInfo;
2823
+ for (const f of tags?.fieldsInfo ?? []) {
2824
+ addKeys(f.usedFields);
2825
+ addFormula(f);
2826
+ if (typeof f.html === "string") for (const e of expressionsOf(f.html)) addWords(e);
2827
+ }
2828
+ if (tags?.filecode) codes.add(tags.filecode);
2829
+ const calc = row.calcInfo;
2830
+ addKeys(calc?.usedFields);
2831
+ for (const f of calc?.fieldsInfo ?? []) {
2832
+ if (f.state && typeof f.fieldcode === "string" && !f.fieldcode.startsWith("$")) codes.add(f.fieldcode);
2833
+ addKeys(f.usedFields);
2834
+ addFormula(f);
2835
+ }
2836
+ }
2837
+ for (const c of [...codes]) if (c.startsWith("$")) codes.delete(c);
2838
+ return codes.size ? codes : null;
2839
+ }
2840
+ function narrowRecord(record, keep) {
2841
+ const out = {};
2842
+ const kept = [];
2843
+ for (const [code, fv] of Object.entries(record)) {
2844
+ if (ALWAYS.has(code)) {
2845
+ out[code] = fv;
2846
+ continue;
2847
+ }
2848
+ if (fv?.type === "SUBTABLE" && Array.isArray(fv.value)) {
2849
+ const rows = fv.value;
2850
+ const childCodes = new Set(rows.flatMap((r) => Object.keys(r.value ?? {})));
2851
+ const wholeTable = keep.has(code);
2852
+ const usedChildren = [...childCodes].filter((c) => keep.has(c));
2853
+ if (!wholeTable && usedChildren.length === 0) continue;
2854
+ out[code] = {
2855
+ type: "SUBTABLE",
2856
+ value: rows.map((r) => {
2857
+ if (wholeTable) return r;
2858
+ const v = {};
2859
+ for (const c of usedChildren) if (r.value[c]) v[c] = r.value[c];
2860
+ return { ...r.id ? { id: r.id } : {}, value: v };
2861
+ })
2862
+ };
2863
+ kept.push(wholeTable ? code : `${code}(${usedChildren.join(",")})`);
2864
+ continue;
2865
+ }
2866
+ if (keep.has(code)) {
2867
+ out[code] = fv;
2868
+ kept.push(code);
2869
+ }
2870
+ }
2871
+ return { record: out, kept };
2872
+ }
2873
+ function missingInRecord(settings, fields, record) {
2874
+ const used = usedFieldCodes(settings);
2875
+ if (!used) return [];
2876
+ const missing = [];
2877
+ for (const [code, p] of Object.entries(fields.properties ?? {})) {
2878
+ if (p.type === "SUBTABLE") {
2879
+ const table = record[code];
2880
+ const rows = Array.isArray(table?.value) ? table.value : null;
2881
+ for (const child of Object.keys(p.fields ?? {})) {
2882
+ if (!used.has(child)) continue;
2883
+ if (!rows || rows.length > 0 && !rows.some((r) => r.value && child in r.value)) missing.push(`${code}.${child}`);
2884
+ }
2885
+ if (used.has(code) && !table) missing.push(code);
2886
+ continue;
2887
+ }
2888
+ if (used.has(code) && !(code in record)) missing.push(code);
2889
+ }
2890
+ return [...new Set(missing)];
2891
+ }
2892
+ async function fetchRecord(client, opt) {
2893
+ const res = await client.get("record", { app: opt.app, id: opt.id }, opt.guestSpaceId);
2894
+ let record = res.record ?? {};
2895
+ let keptFields;
2896
+ if (opt.keep) {
2897
+ const n = narrowRecord(record, opt.keep);
2898
+ record = n.record;
2899
+ keptFields = n.kept;
2900
+ }
2901
+ return {
2902
+ tool: "pcraft-authoring record",
2903
+ fetchedAt: (opt.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
2904
+ baseUrl: client.baseUrl,
2905
+ appId: opt.app,
2906
+ id: opt.id,
2907
+ ...opt.guestSpaceId ? { guestSpaceId: opt.guestSpaceId } : {},
2908
+ ...keptFields ? { keptFields } : {},
2909
+ record
2910
+ };
2911
+ }
2912
+ var MULTI_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT", "CATEGORY"]);
2913
+ var ENTITY_TYPES = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT", "STATUS_ASSIGNEE"]);
2914
+ var DATE_TYPES = { DATE: "日付", DATETIME: "日時", CREATED_TIME: "日時", UPDATED_TIME: "日時", TIME: "時刻" };
2915
+ var NUMERIC = /^-?\d+(\.\d+)?$/;
2916
+ function shapeOf(fv) {
2917
+ const v = fv.value;
2918
+ if (v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0) return "空";
2919
+ if (fv.type === "FILE" && Array.isArray(v)) {
2920
+ const byType = /* @__PURE__ */ new Map();
2921
+ for (const f of v) byType.set(f.contentType ?? "?", (byType.get(f.contentType ?? "?") ?? 0) + 1);
2922
+ return `${v.length} 件(${[...byType].map(([t, n]) => `${t} ${n}`).join("、")})`;
2923
+ }
2924
+ if (MULTI_TYPES.has(fv.type) && Array.isArray(v)) return `${v.length} 個`;
2925
+ if (ENTITY_TYPES.has(fv.type) && Array.isArray(v)) return `${v.length} 件`;
2926
+ if (DATE_TYPES[fv.type]) return DATE_TYPES[fv.type];
2927
+ if (fv.type === "RECORD_NUMBER") return "あり";
2928
+ if (typeof v === "string") {
2929
+ if ((fv.type === "NUMBER" || fv.type === "CALC") && NUMERIC.test(v)) {
2930
+ const [int, dec] = v.replace("-", "").split(".");
2931
+ return `数値 ${v.startsWith("-") ? "負 " : ""}整数 ${int.length} 桁${dec ? `、小数 ${dec.length} 桁` : ""}`;
2932
+ }
2933
+ const lines = v.split("\n").length;
2934
+ return `${fv.type === "RICH_TEXT" ? "HTML " : ""}${[...v].length} 文字${lines > 1 ? `、${lines} 行` : ""}`;
2935
+ }
2936
+ if (typeof v === "object") return "あり";
2937
+ return typeof v;
2938
+ }
2939
+ function columnShape(type, values) {
2940
+ const shapes = values.map((value) => shapeOf({ type, value }));
2941
+ const filled = shapes.filter((s) => s !== "空");
2942
+ if (!filled.length) return "全行 空";
2943
+ const longest = filled.reduce((a, b) => Number(b.match(/\d+/)?.[0] ?? 0) > Number(a.match(/\d+/)?.[0] ?? 0) ? b : a);
2944
+ const multiLine = filled.filter((s) => /\d+ 行$/.test(s)).length;
2945
+ return `最大 ${longest}${multiLine ? `(複数行の値 ${multiLine} 行)` : ""}${filled.length < shapes.length ? `、空 ${shapes.length - filled.length} 行` : ""}`;
2946
+ }
2947
+ function describeRecord(file) {
2948
+ const header = `アプリ ${file.appId} レコード ${file.id}(取得 ${file.fetchedAt}${file.keptFields ? `、--fields-from で ${file.keptFields.length} 項目に絞った` : ""})。値は出さない(形だけ)`;
2949
+ return [header, ...shapeLines(file.record)].join("\n");
2950
+ }
2951
+ function shapeLines(record) {
2952
+ const lines = [];
2953
+ for (const [code, fv] of Object.entries(record)) {
2954
+ if (code === "$id" || code === "$revision" || !fv) continue;
2955
+ if (fv.type === "SUBTABLE" && Array.isArray(fv.value)) {
2956
+ const rows = fv.value;
2957
+ lines.push(`${code} SUBTABLE ${rows.length} 行`);
2958
+ const children = /* @__PURE__ */ new Map();
2959
+ for (const r of rows) {
2960
+ for (const [c, cv] of Object.entries(r.value ?? {})) {
2961
+ const e = children.get(c) ?? { type: cv.type, values: [] };
2962
+ e.values.push(cv.value);
2963
+ children.set(c, e);
2964
+ }
2965
+ }
2966
+ for (const [c, e] of children) lines.push(` ${c} ${e.type} ${columnShape(e.type, e.values)}`);
2967
+ continue;
2968
+ }
2969
+ lines.push(`${code} ${fv.type} ${shapeOf(fv)}`);
2970
+ }
2971
+ return lines;
2972
+ }
2973
+ function summarizeRecord(file) {
2974
+ const codes = Object.keys(file.record);
2975
+ const tables = codes.filter((c) => file.record[c]?.type === "SUBTABLE");
2976
+ const rows = tables.map((t) => `${t} ${Array.isArray(file.record[t].value) ? file.record[t].value.length : 0} 行`);
2977
+ return `アプリ ${file.appId} レコード ${file.id}: 項目 ${codes.length}${tables.length ? `(テーブル ${rows.join(", ")})` : ""}${file.keptFields ? `。--fields-from で ${file.keptFields.length} 項目に絞った` : ""}。値は表示しない`;
2978
+ }
2979
+
2980
+ // src/commands/records.ts
2981
+ var LIST_LIMIT = 5;
2982
+ var QUERY_MAX = 500;
2983
+ var SHAPE_TEXT_MAX = 64 * 1024;
2984
+ var QueryError = class extends Error {
2985
+ };
2986
+ function withoutQuoted(q) {
2987
+ return q.replace(/"(?:[^"\\]|\\.)*"/g, '""');
2988
+ }
2989
+ function listQueryOf(query) {
2990
+ const q = (query ?? "").trim();
2991
+ if ([...q].length > QUERY_MAX) throw new QueryError(`query は ${QUERY_MAX} 文字まで`);
2992
+ if (/\b(limit|offset)\b/i.test(withoutQuoted(q))) throw new QueryError("query に limit / offset は書かない(件数はサーバーが決める)");
2993
+ return q ? `${q} limit ${LIST_LIMIT}` : `limit ${LIST_LIMIT}`;
2994
+ }
2995
+ async function listRecordShapes(client, opt) {
2996
+ const max = opt.maxBytes ?? SHAPE_TEXT_MAX;
2997
+ const query = listQueryOf(opt.query);
2998
+ const res = await client.get("records", { app: opt.app, query }, opt.guestSpaceId);
2999
+ const out = [];
3000
+ let truncated = false;
3001
+ for (const record of (res.records ?? []).slice(0, LIST_LIMIT)) {
3002
+ const idValue = record.$id?.value;
3003
+ const shape = { id: typeof idValue === "string" ? idValue : String(idValue ?? ""), lines: shapeLines(record) };
3004
+ if (Buffer.byteLength(JSON.stringify({ appId: opt.app, query, records: [...out, shape], truncated: false }), "utf8") > max) {
3005
+ truncated = true;
3006
+ break;
3007
+ }
3008
+ out.push(shape);
3009
+ }
3010
+ return { appId: opt.app, query, records: out, truncated };
3011
+ }
3012
+
3013
+ // src/preview/render.ts
3014
+ var PREVIEW_CSP = "default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
3015
+ var OUTER_CSP = "script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'";
3016
+ function fontOrigins(cssUrl) {
3017
+ let origin;
3018
+ try {
3019
+ const u = new URL(cssUrl);
3020
+ if (u.protocol !== "https:") return null;
3021
+ origin = u.origin;
3022
+ } catch {
3023
+ return null;
3024
+ }
3025
+ const font = [origin];
3026
+ if (origin === "https://fonts.googleapis.com") font.push("https://fonts.gstatic.com");
3027
+ return { style: [origin], font };
3028
+ }
3029
+ function previewCsp(font) {
3030
+ const o = font ? fontOrigins(font.cssUrl) : null;
3031
+ if (!o) return PREVIEW_CSP;
3032
+ return `default-src 'none'; img-src data:; style-src 'unsafe-inline' ${o.style.join(" ")}; font-src data: ${o.font.join(" ")}; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'`;
3033
+ }
3034
+ var PREVIEW_REMOVE_SELECTOR = "script,meta,link,base,form,input,button,select,textarea,iframe,frame,object,embed,applet,noscript,template,video,audio,source,track,canvas,map,area,animate,animatemotion,animatetransform,animatecolor,set,foreignobject,math";
3035
+ var REMOVE_ATTRS = /* @__PURE__ */ new Set(["srcdoc", "formaction", "action", "ping", "target", "download", "background", "poster", "manifest"]);
3036
+ var PAGES_CSS = `body { margin: 0; background: #f7f7f7; }
3037
+ .xp-rex0220-print-craft-overlay-pages { display: block; width: fit-content; font-size: 16px; line-height: 1.5; font-family: "メイリオ", Meiryo, "Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN W3", "MS Pゴシック", "Lucida Grande", "Lucida Sans Unicode", Arial, Verdana, sans-serif; -webkit-text-size-adjust: 100%; }
3038
+ .pcraft-authoring-error { color: #b00020; background: #fde7e9; border: 1px solid #b00020; border-radius: 3px; padding: 0 4px; font-size: 12px; font-family: monospace; }`;
3039
+ function escapeHtml(s) {
3040
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3041
+ }
3042
+ function escapeCssText(css) {
3043
+ return css.replace(/</g, "\\3c ").replace(/-->/g, "--\\3e ");
3044
+ }
3045
+ function errorSpan(expression, e) {
3046
+ const message = e instanceof Error ? e.message : String(e);
3047
+ return `<span class="pcraft-authoring-error">式のエラー: ${escapeHtml(expression)} — ${escapeHtml(message)}</span>`;
3048
+ }
3049
+ function renderRows(api, row, record, kf, errors) {
3050
+ const rows = row.tagsInfo?.fieldsInfo ?? [];
3051
+ let html = "";
3052
+ for (let index = 2; index < rows.length; index++) {
3053
+ const info = rows[index];
3054
+ if (!info.state) continue;
3055
+ let tmp = info.html ? info.html.replace(/\$\{(.*?)\}/g, (_m, key) => {
3056
+ try {
3057
+ const v = kf.dq(key);
3058
+ return v == null ? "" : String(v);
3059
+ } catch (e) {
3060
+ errors.push(`HTML 設定 ${index + 1} 行目 \${${key}}: ${e instanceof Error ? e.message : String(e)}`);
3061
+ return errorSpan(`\${${key}}`, e);
3062
+ }
3063
+ }) : "";
3064
+ const formula = api.formulaOf(info);
3065
+ if (formula) {
3066
+ record.$html = { value: tmp, type: "MULTI_LINE_TEXT" };
3067
+ try {
3068
+ const v = kf.dq(formula);
3069
+ tmp = v == null ? "" : String(v);
3070
+ } catch (e) {
3071
+ errors.push(`HTML 設定 ${index + 1} 行目の計算式: ${e instanceof Error ? e.message : String(e)}`);
3072
+ tmp += errorSpan(formula.length > 120 ? formula.slice(0, 120) + "…" : formula, e);
3073
+ }
3074
+ }
3075
+ html += tmp;
3076
+ }
3077
+ return html.replace(/\\/g, "&yen;");
3078
+ }
3079
+ function sanitizePreviewDom(host) {
3080
+ let removedElements = 0;
3081
+ let removedAttributes = 0;
3082
+ for (const el of Array.from(host.querySelectorAll(PREVIEW_REMOVE_SELECTOR))) {
3083
+ el.remove();
3084
+ removedElements++;
3085
+ }
3086
+ for (const el of Array.from(host.querySelectorAll("*"))) {
3087
+ for (const a of Array.from(el.attributes)) {
3088
+ const name = a.name.toLowerCase();
3089
+ const value = String(a.value ?? "").replace(/[\t\n\r]/g, "").trim();
3090
+ const isLink = name === "href" || name === "xlink:href";
3091
+ if (name.startsWith("on") || REMOVE_ATTRS.has(name) || isLink && !value.startsWith("#") || /^[\u0000- ]*javascript:/i.test(value)) {
3092
+ el.removeAttribute(a.name);
3093
+ removedAttributes++;
3094
+ }
3095
+ }
3096
+ if (el.tagName.toLowerCase() === "style") el.textContent = escapeCssText(el.textContent ?? "");
3097
+ }
3098
+ return { removedElements, removedAttributes };
3099
+ }
3100
+ function renderButton(input) {
3101
+ const { row, model, engine, body } = input;
3102
+ const api = model.api;
3103
+ const tags = row.tagsInfo;
3104
+ if (!tags) throw new Error(`${row.menu}: HTML 設定が無い`);
3105
+ const errors = [];
3106
+ const record = JSON.parse(JSON.stringify(input.record));
3107
+ const paper = api.getPaperSize(tags.pageSize || "A4", tags.orientation || "p", Number(tags.dpi) || 96);
3108
+ const kf = engine.runner(model.ppRun, record);
3109
+ const fileName = api.fileNameOf(row, kf);
3110
+ const font = api.webFontOf(body.fontInfo);
3111
+ const cssBody = Array.isArray(body.cssInfo) ? body : { ...body, cssInfo: api.defaultCssRows() };
3112
+ const css = api.buildReportCss(cssBody, row, paper.scr, font);
3113
+ const html = renderRows(api, row, record, kf, errors);
3114
+ const doc = engine.window.document;
3115
+ const host = doc.createElement("div");
3116
+ const pages = api.mountReportHtml(host, html, { sanitize: body.externalRefs !== "allow" });
3117
+ api.replaceTags(pages, {}, {}, api.DUMMY_IMAGE);
3118
+ if (pages.length === 0) errors.push('ページ要素(class="rex0220-pcraft-page")が無い。印刷屋は 0 ページの PDF を作る');
3119
+ sanitizePreviewDom(host);
3120
+ const wf = input.webFont ?? null;
3121
+ const useFont = wf && wf.approved && fontOrigins(wf.font.cssUrl) ? wf.font : null;
3122
+ const fontLink = useFont ? `
3123
+ <link rel="stylesheet" href="${escapeHtml(useFont.cssUrl)}">` : "";
3124
+ const inner = `<!doctype html>
3125
+ <html lang="ja"><head><meta charset="utf-8">
3126
+ <meta http-equiv="Content-Security-Policy" content="${previewCsp(useFont)}">${fontLink}
3127
+ <title>${escapeHtml(row.menu)}</title>
3128
+ <style>${PAGES_CSS}</style>
3129
+ <style class="xp-rex0220-print-craft-page-style">${escapeCssText(css)}</style>
3130
+ </head><body><div class="xp-rex0220-print-craft-overlay-pages">${host.innerHTML}</div></body></html>`;
3131
+ const width = paper.scr.width + 40;
3132
+ const height = Math.max(1, pages.length) * (paper.scr.height + 40) + 40;
3133
+ const errorList = errors.length ? `<ul class="errors">${errors.map((e) => `<li>${escapeHtml(e)}</li>`).join("")}</ul>` : "";
3134
+ const fontNote = useFont ? `Web フォント「${escapeHtml(useFont.family)}」は ${escapeHtml(fontOrigins(useFont.cssUrl).style[0])} から読みます(承認済み。preview の外部通信はこれだけ)。` : wf ? `Web フォント「${escapeHtml(wf.font.family)}」は配信元が未承認のため読みません(OS の書体で代替。利用者が policy/authoring-policy.json の allowExternal に書けば読みます)。` : "Web フォントは使いません。";
3135
+ const outer = `<!doctype html>
3136
+ <html lang="ja"><head><meta charset="utf-8">
3137
+ <meta http-equiv="Content-Security-Policy" content="${OUTER_CSP}">
3138
+ <title>preview: ${escapeHtml(row.menu)}</title>
3139
+ <style>
3140
+ body { margin: 0; font-family: system-ui, sans-serif; background: #e9ecef; color: #222; }
3141
+ header { padding: 12px 16px; background: #fff; border-bottom: 1px solid #ccc; }
3142
+ h1 { margin: 0 0 4px; font-size: 18px; }
3143
+ header p { margin: 2px 0; font-size: 13px; color: #555; }
3144
+ .errors { margin: 8px 0 0; padding: 8px 12px 8px 28px; background: #fde7e9; border: 1px solid #b00020; color: #b00020; font-size: 13px; }
3145
+ iframe { display: block; margin: 12px auto; border: 0; background: #f7f7f7; }
3146
+ </style></head><body>
3147
+ <header>
3148
+ <h1>${escapeHtml(row.menu)}</h1>
3149
+ <p>ファイル名: ${escapeHtml(fileName)} / 用紙: ${escapeHtml(tags.pageSize)} ${tags.orientation === "l" ? "横" : "縦"} ${escapeHtml(String(tags.dpi))} dpi / ページ: ${pages.length}</p>
3150
+ <p>これは近似のプレビューです。添付ファイルの画像と QR はダミーです。${fontNote} PDF の見た目は印刷屋プラグインで確かめてください。帳票は sandbox の iframe の中に置き、スクリプトと外部への通信(承認した Web フォントの配信元を除く)は CSP と sandbox で止め、リンクと埋め込みの要素は外してあります(文字だけ残る)。</p>
3151
+ ${errorList}
3152
+ </header>
3153
+ <iframe sandbox="" title="${escapeHtml(row.menu)}" width="${width}" height="${height}" srcdoc="${escapeHtml(inner)}"></iframe>
3154
+ </body></html>
3155
+ `;
3156
+ return { menu: row.menu, fileName, pageSize: tags.pageSize, orientation: tags.orientation, dpi: String(tags.dpi), pages: pages.length, errors, webFont: useFont ? useFont.cssUrl : null, inner, html: outer };
3157
+ }
3158
+
3159
+ // src/commands/preview.ts
3160
+ function extractRecord(file) {
3161
+ if (!file || typeof file !== "object") throw new Error("レコードの JSON がオブジェクトでない");
3162
+ const o = file;
3163
+ if (o.record && typeof o.record === "object") return o.record;
3164
+ const values = Object.values(o);
3165
+ if (values.length && values.every((v) => v && typeof v === "object" && "type" in v && "value" in v)) return o;
3166
+ throw new Error("レコードの JSON の形が分からない(record コマンドの出力か、/k/v1/record の応答か、{ 項目: { type, value } } の形)");
3167
+ }
3168
+ function previewFileNames(rows) {
3169
+ const used = /* @__PURE__ */ new Map();
3170
+ return rows.map((row, i) => {
3171
+ const base = safeFileName(row.menu, `button-${i + 1}`);
3172
+ const n = used.get(base.toLowerCase()) ?? 0;
3173
+ used.set(base.toLowerCase(), n + 1);
3174
+ return n ? `${base}-${n + 1}.html` : `${base}.html`;
3175
+ });
3176
+ }
3177
+ async function runPreview(input) {
3178
+ const normalized = await normalizeSettings({ settingsText: input.settingsText, settingsFile: input.settingsFile, fields: input.fields, engine: input.engine, policy: input.policy, baseUrl: input.baseUrl });
3179
+ const findings = normalized.findings;
3180
+ const results = [];
3181
+ const skipped = [];
3182
+ if (!normalized.body) return { findings, results, skipped, summary: normalized.summary };
3183
+ const record = extractRecord(input.recordFile);
3184
+ const missing = missingInRecord(normalized.body, input.fields, record);
3185
+ if (missing.length) findings.warning("preview.record", "レコード", `設定が使う項目がプレビューのレコードに無い: ${missing.slice(0, 10).join(", ")}${missing.length > 10 ? ` 他 ${missing.length - 10}` : ""}(帳票では空になる。record --fields-from <この設定> で取り直す)`);
3186
+ const model = buildModel(input.fields, input.engine.api, input.baseUrl);
3187
+ input.engine.setContext({ baseUrl: model.baseUrl || DEFAULT_CONTEXT_BASE_URL, appId: input.fields.appId });
3188
+ const rows = normalized.body.pluginInfos ?? [];
3189
+ const targets = rows.filter((row) => row.state && row.tagsInfo && (input.button === void 0 || row.menu === input.button));
3190
+ const names = previewFileNames(targets);
3191
+ const font = input.engine.api.webFontOf(normalized.body.fontInfo);
3192
+ const webFont = font ? { font: { family: font.family, cssUrl: font.cssUrl }, approved: isAllowed(input.policy ?? { allowExternal: [] }, font.cssUrl, input.settingsFile) } : null;
3193
+ targets.forEach((row, i) => {
3194
+ if (row.list) {
3195
+ skipped.push(`${row.menu}: 一覧帳票は 1 レコードのプレビューの対象外(段階 2)`);
3196
+ return;
3197
+ }
3198
+ const r = renderButton({ body: normalized.body, row, model, engine: input.engine, record, webFont });
3199
+ results.push({ ...r, file: names[i] });
3200
+ });
3201
+ if (input.button !== void 0 && results.length === 0 && !skipped.length) findings.error("preview.button", input.button, "その名前の有効なボタンが無い");
3202
+ const summary = `${normalized.summary}。プレビュー ${results.length} 件${skipped.length ? `、対象外 ${skipped.length}` : ""}、式のエラー ${results.reduce((n, r) => n + r.errors.length, 0)}`;
3203
+ return { findings, results, skipped, summary };
3204
+ }
3205
+
3206
+ // src/commands/pull.ts
3207
+ var PLUGIN_ID_PATTERN = /^[a-p]{32}$/;
3208
+ async function pullSettings(client, engine, opt) {
3209
+ if (!PLUGIN_ID_PATTERN.test(opt.pluginId)) throw new InputError(`プラグイン ID の形が違う: ${opt.pluginId}(a〜p の 32 文字)`);
3210
+ const app = await client.get("app", { id: opt.app }, opt.guestSpaceId);
3211
+ const res = await client.get(opt.preview ? "preview/app/plugin/config" : "app/plugin/config", { app: opt.app, id: opt.pluginId }, opt.guestSpaceId);
3212
+ const stored = res.config && typeof res.config === "object" && !Array.isArray(res.config) ? res.config : {};
3213
+ if (Object.keys(stored).length === 0) throw new InputError(`アプリ ${opt.app} の印刷屋の設定が空(${opt.preview ? "動作テスト環境" : "運用中"}。設定画面でまだ保存していないか、${opt.preview ? "" : "保存して運用環境に反映していないか、"}別のプラグイン ID)`);
3214
+ for (const [k, v] of Object.entries(stored)) if (typeof v !== "string") throw new InputError(`プラグインの設定の値が文字列でない: ${k}`);
3215
+ const api = engine.api;
3216
+ const { data, format } = await api.readConfig(stored);
3217
+ const raw = data && typeof data === "object" && !Array.isArray(data) ? data : {};
3218
+ const body = api.validate(raw, api.CONFIG_SCHEMA, api.CONFIG_LIMITS);
3219
+ const envelope = api.buildExportData({ pluginName: PLUGIN_NAME, pluginId: api.pluginId, pluginVersion: engine.source.pluginVersion, appId: opt.app, appName: app.name }, body, (opt.now ?? (() => /* @__PURE__ */ new Date()))());
3220
+ return { envelope, appName: app.name, revision: String(res.revision ?? ""), format };
3221
+ }
3222
+ function defaultPullName(appName, appId) {
3223
+ const name = safeFileName(appName, "");
3224
+ return name ? `APP${appId}-${name}.json` : `APP${appId}.json`;
3225
+ }
3226
+
3227
+ // src/commands/take.ts
3228
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync10, unlinkSync, mkdirSync as mkdirSync2 } from "node:fs";
3229
+ import path12 from "node:path";
3230
+ var PRINT_CRAFT_PLUGIN_ID2 = "rex0220 Print craft plugin";
3231
+ function removeSource(src, warnings, rel2) {
3232
+ try {
3233
+ withRetry(() => unlinkSync(src));
3234
+ return true;
3235
+ } catch (e) {
3236
+ warnings.push(`${rel2} は行き先に置いたが、inbox から消せなかった(${e.code ?? e.message})。消してよい`);
3237
+ return false;
3238
+ }
3239
+ }
3240
+ function envFor(ws, appId, envName) {
3241
+ if (envName) return ws.environments[envName] ?? `環境「${envName}」は environments.json に無い`;
3242
+ const hits = Object.values(ws.environments).filter((e) => ws.apps.some((a) => a.ids[e.name] === appId));
3243
+ if (hits.length === 1) return hits[0];
3244
+ if (hits.length > 1) return `アプリ ${appId} が ${hits.map((e) => e.name).join(" と ")} の両方にある。--env で選ぶ`;
3245
+ const all = Object.values(ws.environments);
3246
+ if (all.length === 1) return all[0];
3247
+ return `アプリ ${appId} は environments.json の apps に無く、環境が 2 つ以上ある。apps に足すか --env で選ぶ`;
3248
+ }
3249
+ function takeInbox(cwdIn, ws, envName) {
3250
+ const cwd = realResolve(".", cwdIn);
3251
+ const result = { moved: [], skipped: [], warnings: [] };
3252
+ const inbox = path12.join(cwd, INBOX);
3253
+ if (!existsSync7(inbox)) return result;
3254
+ for (const name of readdirSync2(inbox).filter((n) => n.toLowerCase().endsWith(".json")).sort()) {
3255
+ const rel2 = `${INBOX}/${name}`;
3256
+ const src = resolveRead(rel2, cwd);
3257
+ let data;
3258
+ try {
3259
+ data = readJsonLimited(src);
3260
+ } catch (e) {
3261
+ result.skipped.push({ file: rel2, reason: e.message });
3262
+ continue;
3263
+ }
3264
+ if (data.pluginID !== PRINT_CRAFT_PLUGIN_ID2) {
3265
+ result.skipped.push({ file: rel2, reason: "印刷屋の設定のファイルではない(pluginID が違う)" });
3266
+ continue;
3267
+ }
3268
+ const appId = data.appId;
3269
+ if (typeof appId !== "number" || !Number.isInteger(appId) || appId <= 0) {
3270
+ result.skipped.push({ file: rel2, reason: "封筒の appId が無い" });
3271
+ continue;
3272
+ }
3273
+ if (!SNAPSHOT_RE.test(name)) {
3274
+ result.skipped.push({ file: rel2, reason: "ダウンロードの名前(rex0220-print-craft-app<番号>-<日時>.json)ではない。設定画面のダウンロードの名前のまま inbox/ に置く" });
3275
+ continue;
3276
+ }
3277
+ const m = name.match(/-app(\d+)-/);
3278
+ if (m && Number(m[1]) !== appId) {
3279
+ result.skipped.push({ file: rel2, reason: `ファイル名のアプリ ${m[1]} と封筒の appId ${appId} が違う` });
3280
+ continue;
3281
+ }
3282
+ const env = envFor(ws, appId, envName);
3283
+ if (typeof env === "string") {
3284
+ result.skipped.push({ file: rel2, reason: env });
3285
+ continue;
3286
+ }
3287
+ const dir = appDirFor(cwd, env, appId, typeof data.appName === "string" ? data.appName : "");
3288
+ const dest = resolveWrite(path12.join(dir, name), WRITE_ROOTS.kintone, cwd);
3289
+ const text = readFileSync10(src, "utf8");
3290
+ if (existsSync7(dest)) {
3291
+ if (readFileSync10(dest, "utf8") !== text) {
3292
+ result.skipped.push({ file: rel2, reason: `行き先に同じ名前の別の中身がある: ${path12.relative(cwd, dest)}` });
3293
+ continue;
3294
+ }
3295
+ const removed2 = removeSource(src, result.warnings, rel2);
3296
+ result.moved.push({ file: rel2, to: path12.relative(cwd, dest), same: true, ...removed2 ? {} : { leftInInbox: true } });
3297
+ continue;
3298
+ }
3299
+ try {
3300
+ assertChangeAllowed(cwd, dest, "snapshot");
3301
+ mkdirSync2(path12.dirname(dest), { recursive: true });
3302
+ const { cleanup } = writeNewFile(dest, text, () => {
3303
+ if (resolveWrite(path12.join(dir, name), WRITE_ROOTS.kintone, cwd) !== dest) throw new PermissionError(`書く先のフォルダーが途中で変わった(symlink など): ${path12.relative(cwd, dest)}`);
3304
+ assertChangeAllowed(cwd, dest, "snapshot");
3305
+ });
3306
+ for (const c of cleanup) result.warnings.push(`${rel2} は行き先に置いたが、一時ファイルを消せなかった(${c})。${path12.relative(cwd, path12.dirname(dest))} の .${name}.*.tmp を消してよい`);
3307
+ } catch (e) {
3308
+ if (e instanceof PermissionError) {
3309
+ result.skipped.push({ file: rel2, reason: e.message });
3310
+ continue;
3311
+ }
3312
+ if (e instanceof FileExistsError) {
3313
+ result.skipped.push({ file: rel2, reason: `行き先に同じ名前のファイルができた(移す途中で): ${path12.relative(cwd, dest)}。もう一度 take する` });
3314
+ continue;
3315
+ }
3316
+ throw e;
3317
+ }
3318
+ const removed = removeSource(src, result.warnings, rel2);
3319
+ result.moved.push({ file: rel2, to: path12.relative(cwd, dest), same: false, ...removed ? {} : { leftInInbox: true } });
3320
+ }
3321
+ return result;
3322
+ }
3323
+
3324
+ // src/core.ts
3325
+ var CORE_API_VERSION = 1;
3326
+ export {
3327
+ ALLOWED_APIS,
3328
+ AuthError,
3329
+ ButtonNotFoundError,
3330
+ CORE_API_VERSION,
3331
+ DEFAULT_CONTEXT_BASE_URL,
3332
+ DERIVED_KEYS,
3333
+ EDIT_RE,
3334
+ FileExistsError,
3335
+ INBOX,
3336
+ InputError,
3337
+ KINTONE_ROOT,
3338
+ KintoneUrlError,
3339
+ LIST_LIMIT,
3340
+ LockBusyError,
3341
+ MAX_INPUT_BYTES,
3342
+ MAX_SAVE_INPUT_BYTES,
3343
+ MIN_PLUGIN_VERSION,
3344
+ NotAllowedError,
3345
+ PRINT_CRAFT_PLUGIN_ID,
3346
+ PathError,
3347
+ PermissionError,
3348
+ PluginZipError,
3349
+ QUERY_MAX,
3350
+ QueryError,
3351
+ RECEIVE_LIMITS,
3352
+ RestError,
3353
+ SHAPE_TEXT_MAX,
3354
+ SNAPSHOT_RE,
3355
+ SUPPORTED_API_VERSIONS,
3356
+ WORKSPACE_FILE,
3357
+ WRITE_ROOTS,
3358
+ WorkspaceError,
3359
+ appDirFor,
3360
+ appFolderOfFile,
3361
+ assertChangeAllowed,
3362
+ baseUrlFromEnv,
3363
+ classOfAppPath,
3364
+ createContext,
3365
+ createRestClient,
3366
+ defaultPullName,
3367
+ describeAuth,
3368
+ describeRecord,
3369
+ diffSettings,
3370
+ digestOf,
3371
+ editNameOf,
3372
+ envOfAppFolder,
3373
+ extractRecord,
3374
+ fetchFields,
3375
+ fetchRecord,
3376
+ findAppDir,
3377
+ folderNameOf,
3378
+ isInside,
3379
+ isKintoneBaseUrl,
3380
+ isSupportedPluginVersion,
3381
+ listAppFolder,
3382
+ listButtons,
3383
+ listFields,
3384
+ listQueryOf,
3385
+ listRecordShapes,
3386
+ loadAuth,
3387
+ loadAuthForEnv,
3388
+ loadEngine,
3389
+ loadPolicy,
3390
+ loadWorkspace,
3391
+ missingInRecord,
3392
+ narrowRecord,
3393
+ normalizeKintoneBaseUrl,
3394
+ normalizeSettings,
3395
+ parseWorkspace,
3396
+ pickEnv,
3397
+ pluginZipPath,
3398
+ previewFileNames,
3399
+ pullSettings,
3400
+ readFieldsFile,
3401
+ readJsonLimited,
3402
+ readTextLimited,
3403
+ realResolve,
3404
+ relativeSettingsPath,
3405
+ resolveApp,
3406
+ resolveRead,
3407
+ resolveWrite,
3408
+ roleOfTarget,
3409
+ runPreview,
3410
+ saveNewSettings,
3411
+ shapeLines,
3412
+ shortenDataUrls,
3413
+ snapshotNameOf,
3414
+ summarizeFields,
3415
+ summarizeRecord,
3416
+ takeInbox,
3417
+ toolsMeta,
3418
+ unquote,
3419
+ updateButton,
3420
+ usedFieldCodes,
3421
+ writeNewFile
3422
+ };