@rex0220/print-craft-authoring-tools 1.0.0 → 1.2.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.
package/dist/cli.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- /*! @rex0220/print-craft-authoring-tools 1.0.0 (c) rex0220. MIT License. 計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む(このファイルには含まれない)。commit d662e0b. */
2
+ /*! @rex0220/print-craft-authoring-tools 1.2.0 (c) rex0220. MIT License. 計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む(このファイルには含まれない)。commit 390cbac. */
3
3
 
4
4
  // src/cli.ts
5
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
6
- import path10 from "node:path";
5
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync2 } from "node:fs";
6
+ import path13 from "node:path";
7
7
 
8
8
  // src/env.ts
9
9
  import { existsSync, readFileSync } from "node:fs";
@@ -49,9 +49,13 @@ var NAMES = {
49
49
  token: ["KINTONE_API_TOKEN", "KSQL_TOKEN"],
50
50
  username: ["KINTONE_USERNAME", "KSQL_USERNAME"],
51
51
  password: ["KINTONE_PASSWORD", "KSQL_PASSWORD"],
52
- pluginZip: ["PCRAFT_PLUGIN_ZIP"],
53
- allowUnknown: ["PCRAFT_ALLOW_UNKNOWN_PLUGIN"]
52
+ pluginZip: ["PCRAFT_PLUGIN_ZIP"]
54
53
  };
54
+ function unquote(raw) {
55
+ const value = raw.trim();
56
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
57
+ return value;
58
+ }
55
59
  function parseDotEnv(text) {
56
60
  const out = {};
57
61
  for (const raw of text.split(/\r?\n/)) {
@@ -59,33 +63,36 @@ function parseDotEnv(text) {
59
63
  if (!line || line.startsWith("#")) continue;
60
64
  const m = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
61
65
  if (!m) continue;
62
- let value = m[2].trim();
63
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
64
- out[m[1]] = value;
66
+ out[m[1]] = unquote(m[2]);
65
67
  }
66
68
  return out;
67
69
  }
68
70
  var AuthError = class extends Error {
69
71
  };
70
- function picker(opt) {
71
- const osEnv = opt.env ?? process.env;
72
+ function pickerWithSource(opt, unquoteProcess = false) {
73
+ const osEnv = opt.env;
72
74
  const file = envFileOf(opt);
73
75
  const fromFile = existsSync(file) ? parseDotEnv(readFileSync(file, "utf8")) : {};
74
76
  return (names) => {
75
- for (const src of [osEnv, fromFile]) {
77
+ for (const [src, source] of [[osEnv, "process"], [fromFile, "file"]]) {
76
78
  for (const n of names) {
77
- const v = src[n];
78
- if (v && v.trim()) return v.trim();
79
+ const raw = src[n] ?? "";
80
+ const v = source === "process" && unquoteProcess ? unquote(raw) : raw.trim();
81
+ if (v) return { value: v, source, ...source === "file" ? { file } : {} };
79
82
  }
80
83
  }
81
84
  return void 0;
82
85
  };
83
86
  }
87
+ function picker(opt, unquoteProcess = false) {
88
+ const pick = pickerWithSource(opt, unquoteProcess);
89
+ return (names) => pick(names)?.value;
90
+ }
84
91
  function envFileOf(opt) {
85
- return opt.envFile ?? path.join(opt.cwd ?? process.cwd(), ".env");
92
+ return opt.envFile ?? path.join(opt.cwd, ".env");
86
93
  }
87
- function baseUrlFromEnv(opt = {}) {
88
- const raw = picker(opt)(NAMES.baseUrl);
94
+ function baseUrlFromEnv(opt) {
95
+ const raw = picker(opt, true)(NAMES.baseUrl);
89
96
  if (!raw) return void 0;
90
97
  try {
91
98
  return normalizeKintoneBaseUrl(raw);
@@ -93,7 +100,7 @@ function baseUrlFromEnv(opt = {}) {
93
100
  throw new AuthError(`KINTONE_BASE_URL が不正: ${e instanceof KintoneUrlError ? e.message : String(e)}(https://<サブドメイン>.cybozu.com の形で書く)`);
94
101
  }
95
102
  }
96
- function loadAuth(opt = {}) {
103
+ function loadAuth(opt) {
97
104
  const pick = picker(opt);
98
105
  const file = envFileOf(opt);
99
106
  const baseUrl = baseUrlFromEnv(opt);
@@ -104,7 +111,7 @@ function loadAuth(opt = {}) {
104
111
  if (!token && !(username && password)) throw new AuthError("KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(OS の環境変数か .env)");
105
112
  return { baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
106
113
  }
107
- function loadAuthForEnv(env, cwd = process.cwd()) {
114
+ function loadAuthForEnv(env, cwd) {
108
115
  const file = path.join(cwd, env.envFile);
109
116
  if (!existsSync(file)) throw new AuthError(`環境「${env.name}」の認証のファイルが無い: ${env.envFile}(${env.envFile === ".env" ? ".env.example を写して作る" : "env/ に作る。.env と同じ書き方"})`);
110
117
  const vars = parseDotEnv(readFileSync(file, "utf8"));
@@ -128,13 +135,12 @@ function loadAuthForEnv(env, cwd = process.cwd()) {
128
135
  if (!token && !(username && password)) throw new AuthError(`${env.envFile} に KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(環境「${env.name}」。OS の環境変数は読まない)`);
129
136
  return { baseUrl: env.baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
130
137
  }
131
- function pluginZipPath(opt = {}) {
132
- const v = picker(opt)(NAMES.pluginZip);
133
- if (!v) return void 0;
134
- return path.resolve(path.dirname(envFileOf(opt)), v);
135
- }
136
- function allowUnknownPlugin(opt = {}) {
137
- return picker(opt)(NAMES.allowUnknown) === "1";
138
+ function pluginZipPath(opt) {
139
+ const picked = pickerWithSource(opt, true)(NAMES.pluginZip);
140
+ if (!picked) return void 0;
141
+ if (picked.source === "file") return path.resolve(path.dirname(picked.file ?? envFileOf(opt)), picked.value);
142
+ if (!path.isAbsolute(picked.value)) throw new AuthError(`PCRAFT_PLUGIN_ZIP(OS の環境変数)は絶対パスで書く: ${picked.value}(.env に書くなら .env のフォルダーからの相対パスでもよい)`);
143
+ return path.normalize(picked.value);
138
144
  }
139
145
  function describeAuth(auth) {
140
146
  return auth.token ? "API トークン" : "ログインユーザー";
@@ -161,7 +167,7 @@ function norm(p) {
161
167
  const r = path2.resolve(p);
162
168
  return process.platform === "win32" ? r.toLowerCase() : r;
163
169
  }
164
- function realResolve(target, cwd = process.cwd()) {
170
+ function realResolve(target, cwd) {
165
171
  const abs = path2.resolve(cwd, target);
166
172
  let dir = abs;
167
173
  const rest = [];
@@ -194,7 +200,7 @@ function isInside(child, parent) {
194
200
  const p = norm(parent);
195
201
  return c === p || c.startsWith(p.endsWith(path2.sep) ? p : p + path2.sep);
196
202
  }
197
- function resolveRead(target, cwd = process.cwd()) {
203
+ function resolveRead(target, cwd) {
198
204
  if (!target || typeof target !== "string") throw new PathError("ファイルのパスが要る");
199
205
  const real = realResolve(target, cwd);
200
206
  const cwdReal = realResolve(".", cwd);
@@ -203,7 +209,7 @@ function resolveRead(target, cwd = process.cwd()) {
203
209
  if (/^\.env(\.|$)/i.test(path2.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(rel)) throw new PathError(`このファイルは tools では読まない: ${target}`);
204
210
  return real;
205
211
  }
206
- function resolveWrite(target, roots, cwd = process.cwd()) {
212
+ function resolveWrite(target, roots, cwd) {
207
213
  if (!target || typeof target !== "string") throw new PathError("書き込み先のパスが要る");
208
214
  const real = realResolve(target, cwd);
209
215
  const cwdReal = realResolve(".", cwd);
@@ -217,7 +223,7 @@ function resolveWrite(target, roots, cwd = process.cwd()) {
217
223
  if (WINDOWS_RESERVED.test(base) || /[. ]$/.test(base)) throw new PathError(`使えないファイル名: ${base}`);
218
224
  return real;
219
225
  }
220
- function resolveWriteDir(target, roots, cwd = process.cwd()) {
226
+ function resolveWriteDir(target, roots, cwd) {
221
227
  if (!target || typeof target !== "string") throw new PathError("フォルダーのパスが要る");
222
228
  const real = realResolve(target, cwd);
223
229
  const cwdReal = realResolve(".", cwd);
@@ -243,6 +249,7 @@ var ENV_FILE = /^(\.env|env\/[A-Za-z0-9._-]{1,64}\.env)$/;
243
249
  var MAX_BYTES = 256 * 1024;
244
250
  var WorkspaceError = class extends Error {
245
251
  };
252
+ var ENV_ROLES = ["development", "production"];
246
253
  var isEnvName = (s) => ENV_NAME.test(s);
247
254
  function parseWorkspace(text, where = WORKSPACE_FILE) {
248
255
  let raw;
@@ -262,7 +269,9 @@ function parseWorkspace(text, where = WORKSPACE_FILE) {
262
269
  for (const [name, v] of Object.entries(envsRaw)) {
263
270
  if (!isEnvName(name)) throw new WorkspaceError(`${where}: 環境の名前は英数字と _ - の 32 文字まで: ${name}`);
264
271
  const e = obj(v, `environments.${name}`);
265
- for (const k of Object.keys(e)) if (!["baseUrl", "envFile"].includes(k)) throw new WorkspaceError(`${where}: environments.${name} に使えるキーは baseUrl / envFile: ${k}`);
272
+ for (const k of Object.keys(e)) if (!["baseUrl", "envFile", "role"].includes(k)) throw new WorkspaceError(`${where}: environments.${name} に使えるキーは baseUrl / envFile / role: ${k}`);
273
+ if (e.role !== void 0 && !ENV_ROLES.includes(e.role)) throw new WorkspaceError(`${where}: environments.${name}.role は development(開発)か production(本番): ${String(e.role)}`);
274
+ const role = e.role === void 0 ? "unclassified" : e.role;
266
275
  let baseUrl;
267
276
  try {
268
277
  baseUrl = normalizeKintoneBaseUrl(String(e.baseUrl ?? ""));
@@ -271,7 +280,7 @@ function parseWorkspace(text, where = WORKSPACE_FILE) {
271
280
  }
272
281
  const envFile = e.envFile === void 0 ? ".env" : String(e.envFile).replace(/\\/g, "/");
273
282
  if (!ENV_FILE.test(envFile)) throw new WorkspaceError(`${where}: environments.${name}.envFile は .env か env/<名前>.env: ${envFile}`);
274
- environments[name] = { name, baseUrl, host: new URL(baseUrl).hostname, envFile };
283
+ environments[name] = { name, role, baseUrl, host: new URL(baseUrl).hostname, envFile };
275
284
  }
276
285
  if (Object.keys(environments).length === 0) throw new WorkspaceError(`${where}: environments に環境が 1 つも無い`);
277
286
  const defaultEnv = root.default === void 0 ? Object.keys(environments)[0] : String(root.default);
@@ -298,7 +307,7 @@ function parseWorkspace(text, where = WORKSPACE_FILE) {
298
307
  }
299
308
  return { defaultEnv, environments, apps };
300
309
  }
301
- function loadWorkspace(cwd = process.cwd()) {
310
+ function loadWorkspace(cwd) {
302
311
  const file = path3.join(cwd, WORKSPACE_FILE);
303
312
  if (!existsSync2(file)) return null;
304
313
  const size = statSync(file).size;
@@ -381,14 +390,20 @@ function listAppFolder(dir) {
381
390
  function envsOfHost(ws, host) {
382
391
  return Object.values(ws.environments).filter((e) => e.host === host);
383
392
  }
393
+ function envOfAppFolder(ws, host, appId) {
394
+ const hits = envsOfHost(ws, host);
395
+ if (hits.length === 1) return hits[0];
396
+ const byApp = hits.filter((e) => ws.apps.some((a) => a.ids[e.name] === appId));
397
+ return byApp.length === 1 ? byApp[0] : void 0;
398
+ }
384
399
 
385
400
  // src/commands/take.ts
386
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
387
- import path6 from "node:path";
401
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, unlinkSync, mkdirSync } from "node:fs";
402
+ import path10 from "node:path";
388
403
 
389
404
  // src/commands/normalize.ts
390
- import { readFileSync as readFileSync4, statSync as statSync3 } from "node:fs";
391
- import path5 from "node:path";
405
+ import { readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
406
+ import path7 from "node:path";
392
407
 
393
408
  // src/normalize/findings.ts
394
409
  var Findings = class {
@@ -422,6 +437,96 @@ var Findings = class {
422
437
  }
423
438
  };
424
439
 
440
+ // src/meta.ts
441
+ import { execFileSync } from "node:child_process";
442
+ import { createHash } from "node:crypto";
443
+ import { readFileSync as readFileSync3 } from "node:fs";
444
+ import path5 from "node:path";
445
+
446
+ // src/paths.ts
447
+ import path4 from "node:path";
448
+ import { fileURLToPath } from "node:url";
449
+ var here = path4.dirname(fileURLToPath(import.meta.url));
450
+ var AUTHORING_ROOT = path4.resolve(here, "..");
451
+ var REPO_ROOT = path4.resolve(AUTHORING_ROOT, "..");
452
+ var VENDOR_DIR = path4.join(AUTHORING_ROOT, "vendor");
453
+ var MOMENT_FILE = path4.join(VENDOR_DIR, "moment-with-locales.min.js");
454
+
455
+ // src/meta.ts
456
+ var PRINT_CRAFT_PLUGIN_ID = "lcapkanpjdabgphknkabojmcfhonhkhp";
457
+ var MIN_PLUGIN_VERSION = 6;
458
+ var SUPPORTED_API_VERSIONS = [1, 2];
459
+ function isSupportedPluginVersion(version2) {
460
+ return /^[1-9]\d{0,5}$/.test(version2) && Number(version2) >= MIN_PLUGIN_VERSION;
461
+ }
462
+ var REQUIRED_API = {
463
+ apiVersion: "number",
464
+ pluginVersion: "string",
465
+ pluginId: "string",
466
+ CONFIG_SCHEMA: "object",
467
+ CONFIG_LIMITS: "object",
468
+ PAGE_SIZES: "object",
469
+ DPI_OPTIONS: "object",
470
+ PAPER_NAMES: "object",
471
+ PRINT_MODES: "object",
472
+ buildMenuRows: "function",
473
+ normalizeTagsInfo: "function",
474
+ normalizeCssRows: "function",
475
+ toSavedRows: "function",
476
+ computeUsage: "function",
477
+ computePluginUOG: "function",
478
+ createCheckRecord: "function",
479
+ createFieldsInfo: "function",
480
+ isTargetType: "function",
481
+ stripComments: "function",
482
+ defaultCssRows: "function",
483
+ defaultMenuInfo: "function",
484
+ expandFields: "function",
485
+ buildReportCss: "function",
486
+ fileNameOf: "function",
487
+ formulaOf: "function",
488
+ mountReportHtml: "function",
489
+ replaceTags: "function",
490
+ getPaperSize: "function",
491
+ webFontOf: "function",
492
+ DUMMY_IMAGE: "string",
493
+ validate: "function",
494
+ parseJsonSafely: "function",
495
+ ValidationError: "function",
496
+ writeConfig: "function",
497
+ DEFAULT_LIMITS: "object",
498
+ ConfigStoreError: "function",
499
+ // pull(アプリのプラグインの設定を読んで封筒形式にする。2026-10-05)
500
+ readConfig: "function",
501
+ buildExportData: "function"
502
+ };
503
+ function stableJson(v) {
504
+ return JSON.stringify(v, (_k, x) => x instanceof RegExp ? `/${x.source}/${x.flags}` : x);
505
+ }
506
+ function sha256Hex(data) {
507
+ return createHash("sha256").update(data).digest("hex");
508
+ }
509
+ function schemaRevisionOf(schema) {
510
+ return sha256Hex(stableJson(schema)).slice(0, 12);
511
+ }
512
+ function gitCommit(dir) {
513
+ try {
514
+ const head = execFileSync("git", ["-C", dir, "rev-parse", "--short", "HEAD"], { encoding: "utf8" }).trim();
515
+ const dirty = execFileSync("git", ["-C", dir, "status", "--porcelain", "--untracked-files=no"], { encoding: "utf8" }).trim() !== "";
516
+ return dirty ? `${head}+dirty` : head;
517
+ } catch {
518
+ return "unknown";
519
+ }
520
+ }
521
+ function devMeta() {
522
+ const pkg = JSON.parse(readFileSync3(path5.join(AUTHORING_ROOT, "package.json"), "utf8"));
523
+ return { toolsVersion: pkg.version, pluginId: PRINT_CRAFT_PLUGIN_ID, minPluginVersion: MIN_PLUGIN_VERSION, supportedApiVersions: [...SUPPORTED_API_VERSIONS], commit: gitCommit(AUTHORING_ROOT), builtAt: "", mode: "dev" };
524
+ }
525
+ function toolsMeta() {
526
+ if (true) return JSON.parse('{"toolsVersion":"1.2.0","pluginId":"lcapkanpjdabgphknkabojmcfhonhkhp","minPluginVersion":6,"supportedApiVersions":[1,2],"commit":"390cbac","builtAt":"2026-10-09T12:21:22.862Z","mode":"build"}');
527
+ return devMeta();
528
+ }
529
+
425
530
  // src/normalize/model.ts
426
531
  var PSEUDO_FIELDS = /* @__PURE__ */ new Set(["$id", "$revision", "$out", "$html", "$rseq"]);
427
532
  var UOG_MARK = "$UGO$";
@@ -937,9 +1042,9 @@ async function checkHtml(html, opt = {}) {
937
1042
  const doc = new w.DOMParser().parseFromString(`<!doctype html><html><body>${html}</body></html>`, "text/html");
938
1043
  const root = doc.body ?? doc.documentElement;
939
1044
  if (!root) return { errors, warnings, externals };
940
- const walk = (el, path11) => {
1045
+ const walk = (el, path14) => {
941
1046
  const tag = el.tagName.toLowerCase();
942
- const here2 = `${path11}>${tag}`;
1047
+ const here2 = `${path14}>${tag}`;
943
1048
  const denied = tagError(tag, here2);
944
1049
  if (denied) {
945
1050
  errors.push(denied);
@@ -1002,11 +1107,11 @@ function expressionsOf(html) {
1002
1107
  }
1003
1108
 
1004
1109
  // src/normalize/policy.ts
1005
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
1006
- import path4 from "node:path";
1110
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
1111
+ import path6 from "node:path";
1007
1112
  var PolicyError = class extends Error {
1008
1113
  };
1009
- var POLICY_FILE = path4.join("policy", "authoring-policy.json");
1114
+ var POLICY_FILE = path6.join("policy", "authoring-policy.json");
1010
1115
  var DEFAULT_ALLOWED_ORIGINS = ["https://fonts.googleapis.com", "https://fonts.gstatic.com"];
1011
1116
  var MAX_POLICY_BYTES = 1024 * 1024;
1012
1117
  var MAX_RULES = 200;
@@ -1103,11 +1208,11 @@ function isExternalRefsAllowed(policy, settingsFile) {
1103
1208
  return false;
1104
1209
  }
1105
1210
  }
1106
- function loadPolicy(opt = {}) {
1107
- const file = path4.join(opt.cwd ?? process.cwd(), POLICY_FILE);
1211
+ function loadPolicy(opt) {
1212
+ const file = path6.join(opt.cwd, POLICY_FILE);
1108
1213
  if (!existsSync3(file)) return { allowExternal: [] };
1109
1214
  if (statSync2(file).size > MAX_POLICY_BYTES) throw new PolicyError(`${file} が大きすぎる(${MAX_POLICY_BYTES} バイトまで)`);
1110
- return parsePolicy(readFileSync3(file, "utf8"), file);
1215
+ return parsePolicy(readFileSync4(file, "utf8"), file);
1111
1216
  }
1112
1217
  function originOf(url) {
1113
1218
  try {
@@ -1510,7 +1615,10 @@ async function normalizeSettings(input) {
1510
1615
  return { findings: f, summary: "封筒形式でない" };
1511
1616
  }
1512
1617
  if (envelope.pluginID !== api.pluginId) f.error("envelope.pluginID", where, `pluginID は "${api.pluginId}": ${JSON.stringify(envelope.pluginID)}(封筒なしの素の設定は作らない)`);
1513
- if (String(envelope.PluginVersion ?? "") !== pluginVersion) f.error("envelope.version", where, `PluginVersion は印刷屋の版 ${pluginVersion}(zip の manifest): ${JSON.stringify(envelope.PluginVersion)}`);
1618
+ const envVersion = String(envelope.PluginVersion ?? "");
1619
+ if (!isSupportedPluginVersion(envVersion)) f.error("envelope.version", where, `PluginVersion は印刷屋の版(Ver.${MIN_PLUGIN_VERSION} 以降、zip は ${pluginVersion}): ${JSON.stringify(envelope.PluginVersion)}`);
1620
+ else if (Number(envVersion) > Number(pluginVersion)) f.error("envelope.version", where, `PluginVersion ${envVersion} の設定を、それより古い印刷屋の zip(版 ${pluginVersion})で検査しようとしている。アプリに入れた版の zip を .env の PCRAFT_PLUGIN_ZIP に書く`);
1621
+ else if (envVersion !== pluginVersion) f.info("envelope.version.upgrade", where, `PluginVersion ${envVersion} の設定を印刷屋の版 ${pluginVersion}(zip)で検査し、書き出す封筒は ${pluginVersion} にする`);
1514
1622
  if (envelope.appId !== void 0) {
1515
1623
  if (typeof envelope.appId !== "number" || !Number.isInteger(envelope.appId) || envelope.appId <= 0) f.error("envelope.appId", where, `appId は正の整数(数値): ${JSON.stringify(envelope.appId)}`);
1516
1624
  else if (envelope.appId !== input.fields.appId) f.warning("envelope.appId", where, `appId ${envelope.appId} が fields のアプリ ${input.fields.appId} と違う`);
@@ -1571,7 +1679,7 @@ async function normalizeSettings(input) {
1571
1679
  function readTextLimited(file, maxBytes = MAX_INPUT_BYTES) {
1572
1680
  const size = statSync3(file).size;
1573
1681
  if (size > maxBytes) throw new InputError(`${file} が大きすぎる(${size.toLocaleString()} バイト。上限 ${maxBytes.toLocaleString()})`);
1574
- return readFileSync4(file, "utf8");
1682
+ return readFileSync5(file, "utf8");
1575
1683
  }
1576
1684
  function readJsonLimited(file, maxBytes = MAX_INPUT_BYTES) {
1577
1685
  let v;
@@ -1590,13 +1698,157 @@ async function readFieldsFile(file) {
1590
1698
  if (typeof fields2.appId !== "number" || !Number.isInteger(fields2.appId) || fields2.appId <= 0) throw new InputError(`${file} の appId が正の整数でない`);
1591
1699
  return fields2;
1592
1700
  }
1593
- function relativeSettingsPath(file) {
1594
- const rel = path5.relative(process.cwd(), path5.resolve(file));
1701
+ function relativeSettingsPath(file, cwd) {
1702
+ const rel = path7.relative(cwd, path7.resolve(cwd, file));
1595
1703
  return rel.startsWith("..") ? file : rel.replace(/\\/g, "/");
1596
1704
  }
1597
1705
 
1706
+ // src/permission.ts
1707
+ import { existsSync as existsSync4 } from "node:fs";
1708
+ import path8 from "node:path";
1709
+ var PermissionError = class extends Error {
1710
+ };
1711
+ var ALLOWED_CLASSES = {
1712
+ settings: ["edit", "report"],
1713
+ snapshot: ["snapshot"],
1714
+ fields: ["fields"],
1715
+ record: ["record"],
1716
+ preview: ["out"]
1717
+ };
1718
+ var OP_LABEL = {
1719
+ settings: "設定(-edit.json と新しい帳票)",
1720
+ snapshot: "新しい名前のダウンロード / pull",
1721
+ fields: "fields.json",
1722
+ record: "records/ の下",
1723
+ preview: "out/ の下"
1724
+ };
1725
+ function classOfAppPath(relInFolder) {
1726
+ const rel = relInFolder.replace(/\\/g, "/");
1727
+ if (rel.startsWith("records/")) return rel.toLowerCase().endsWith(".json") ? "record" : "other";
1728
+ if (rel.startsWith("out/")) return "out";
1729
+ if (rel.includes("/")) return "other";
1730
+ if (rel === "fields.json") return "fields";
1731
+ if (SNAPSHOT_RE.test(rel)) return "snapshot";
1732
+ if (EDIT_RE.test(rel)) return "edit";
1733
+ if (rel.toLowerCase().endsWith(".json")) return "report";
1734
+ return "other";
1735
+ }
1736
+ function roleOfTarget(root, file) {
1737
+ const abs = path8.resolve(root, file);
1738
+ if (!isInside(abs, path8.join(root, KINTONE_ROOT))) return null;
1739
+ const ws = loadWorkspace(root);
1740
+ const folder = appFolderOfFile(root, abs);
1741
+ const cls = folder ? classOfAppPath(path8.relative(folder.dir, abs)) : void 0;
1742
+ if (!ws) return { role: "unclassified", why: `${WORKSPACE_FILE} が無い`, cls };
1743
+ if (!folder) return { role: "unclassified", why: "アプリのフォルダー(kintone/<ホスト名>/<番号>-…/)の中でない" };
1744
+ const env = envOfAppFolder(ws, folder.host, folder.appId);
1745
+ if (!env) return { role: "unclassified", why: `フォルダー(${folder.host} のアプリ ${folder.appId})がどの環境のものか ${WORKSPACE_FILE} から決まらない(同じホストの環境が 2 つ以上あるときは、apps にアプリの番号を足す)`, cls };
1746
+ return env.role === "unclassified" ? { role: "unclassified", envName: env.name, why: `環境「${env.name}」に role が無い`, cls } : { role: env.role, envName: env.name, cls };
1747
+ }
1748
+ function assertChangeAllowed(root, file, op) {
1749
+ const target = roleOfTarget(root, file);
1750
+ if (!target) return;
1751
+ const abs = path8.resolve(root, file);
1752
+ if (target.role === "unclassified") {
1753
+ throw new PermissionError(`${target.why}ので kintone/ の下を変更できない。${WORKSPACE_FILE} の環境に role(development か production)を書く(利用者が書く)`);
1754
+ }
1755
+ if (target.role === "production" && op !== "snapshot") {
1756
+ throw new PermissionError(`環境「${target.envName}」は本番(role: production)なので変更できない。開発の環境で作る・直す(本番のフォルダーは pull / ダウンロードで今の設定を見るだけ)`);
1757
+ }
1758
+ const cls = target.cls ?? "other";
1759
+ if (cls === "snapshot" && existsSync4(abs)) {
1760
+ throw new PermissionError(`ダウンロード / pull のファイルは書き換えない: ${path8.relative(root, abs)}。edit で -edit.json に写して直す`);
1761
+ }
1762
+ if (!ALLOWED_CLASSES[op].includes(cls)) {
1763
+ throw new PermissionError(`この操作でアプリのフォルダーに書けるのは ${OP_LABEL[op]} だけ: ${path8.relative(root, abs)}`);
1764
+ }
1765
+ }
1766
+
1767
+ // src/commit-file.ts
1768
+ import { randomBytes } from "node:crypto";
1769
+ import { constants, copyFileSync, linkSync, lstatSync as lstatSync2, readFileSync as readFileSync6, renameSync, rmSync, writeFileSync } from "node:fs";
1770
+ import path9 from "node:path";
1771
+ var FileExistsError = class extends Error {
1772
+ };
1773
+ var errCode = (e) => e?.code;
1774
+ var rand = () => randomBytes(6).toString("hex");
1775
+ function sleepSync(ms) {
1776
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1777
+ }
1778
+ function withRetry(fn, tries = 3) {
1779
+ for (let i = 1; ; i++) {
1780
+ try {
1781
+ return fn();
1782
+ } catch (e) {
1783
+ const code = errCode(e);
1784
+ if (i < tries && (code === "EPERM" || code === "EBUSY")) {
1785
+ sleepSync(20 * i);
1786
+ continue;
1787
+ }
1788
+ throw e;
1789
+ }
1790
+ }
1791
+ }
1792
+ function cleanupAll(steps, tries = 3) {
1793
+ const errors = [];
1794
+ for (const step of steps) {
1795
+ try {
1796
+ withRetry(step, tries);
1797
+ } catch (e) {
1798
+ errors.push(e instanceof Error ? e.message : String(e));
1799
+ }
1800
+ }
1801
+ return errors;
1802
+ }
1803
+ function tempPathFor(target) {
1804
+ return path9.join(path9.dirname(target), `.${path9.basename(target)}.${rand()}.tmp`);
1805
+ }
1806
+ function placeNew(tmp, target, link = linkSync) {
1807
+ try {
1808
+ link(tmp, target);
1809
+ return;
1810
+ } catch (e) {
1811
+ const code = errCode(e);
1812
+ if (code === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path9.basename(target)}`);
1813
+ if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "ENOSYS") throw e;
1814
+ }
1815
+ try {
1816
+ copyFileSync(tmp, target, constants.COPYFILE_EXCL);
1817
+ } catch (e) {
1818
+ if (errCode(e) === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path9.basename(target)}`);
1819
+ throw e;
1820
+ }
1821
+ }
1822
+ function writeNewFile(target, text, recheck = () => {
1823
+ }) {
1824
+ const tmp = tempPathFor(target);
1825
+ let placed = false;
1826
+ let failure;
1827
+ try {
1828
+ recheck();
1829
+ writeFileSync(tmp, text, { encoding: "utf8", flag: "wx" });
1830
+ recheck();
1831
+ placeNew(tmp, target);
1832
+ placed = true;
1833
+ } catch (e) {
1834
+ failure = e;
1835
+ }
1836
+ const cleanup = cleanupAll([() => rmSync(tmp, { force: true })]);
1837
+ if (!placed) throw failure;
1838
+ return { cleanup };
1839
+ }
1840
+
1598
1841
  // src/commands/take.ts
1599
- var PRINT_CRAFT_PLUGIN_ID = "rex0220 Print craft plugin";
1842
+ var PRINT_CRAFT_PLUGIN_ID2 = "rex0220 Print craft plugin";
1843
+ function removeSource(src, warnings, rel) {
1844
+ try {
1845
+ withRetry(() => unlinkSync(src));
1846
+ return true;
1847
+ } catch (e) {
1848
+ warnings.push(`${rel} は行き先に置いたが、inbox から消せなかった(${e.code ?? e.message})。消してよい`);
1849
+ return false;
1850
+ }
1851
+ }
1600
1852
  function envFor(ws, appId, envName) {
1601
1853
  if (envName) return ws.environments[envName] ?? `環境「${envName}」は environments.json に無い`;
1602
1854
  const hits = Object.values(ws.environments).filter((e) => ws.apps.some((a) => a.ids[e.name] === appId));
@@ -1606,10 +1858,11 @@ function envFor(ws, appId, envName) {
1606
1858
  if (all.length === 1) return all[0];
1607
1859
  return `アプリ ${appId} は environments.json の apps に無く、環境が 2 つ以上ある。apps に足すか --env で選ぶ`;
1608
1860
  }
1609
- function takeInbox(cwd, ws, envName) {
1610
- const result = { moved: [], skipped: [] };
1611
- const inbox = path6.join(cwd, INBOX);
1612
- if (!existsSync4(inbox)) return result;
1861
+ function takeInbox(cwdIn, ws, envName) {
1862
+ const cwd = realResolve(".", cwdIn);
1863
+ const result = { moved: [], skipped: [], warnings: [] };
1864
+ const inbox = path10.join(cwd, INBOX);
1865
+ if (!existsSync5(inbox)) return result;
1613
1866
  for (const name of readdirSync2(inbox).filter((n) => n.toLowerCase().endsWith(".json")).sort()) {
1614
1867
  const rel = `${INBOX}/${name}`;
1615
1868
  const src = resolveRead(rel, cwd);
@@ -1620,7 +1873,7 @@ function takeInbox(cwd, ws, envName) {
1620
1873
  result.skipped.push({ file: rel, reason: e.message });
1621
1874
  continue;
1622
1875
  }
1623
- if (data.pluginID !== PRINT_CRAFT_PLUGIN_ID) {
1876
+ if (data.pluginID !== PRINT_CRAFT_PLUGIN_ID2) {
1624
1877
  result.skipped.push({ file: rel, reason: "印刷屋の設定のファイルではない(pluginID が違う)" });
1625
1878
  continue;
1626
1879
  }
@@ -1629,6 +1882,10 @@ function takeInbox(cwd, ws, envName) {
1629
1882
  result.skipped.push({ file: rel, reason: "封筒の appId が無い" });
1630
1883
  continue;
1631
1884
  }
1885
+ if (!SNAPSHOT_RE.test(name)) {
1886
+ result.skipped.push({ file: rel, reason: "ダウンロードの名前(rex0220-print-craft-app<番号>-<日時>.json)ではない。設定画面のダウンロードの名前のまま inbox/ に置く" });
1887
+ continue;
1888
+ }
1632
1889
  const m = name.match(/-app(\d+)-/);
1633
1890
  if (m && Number(m[1]) !== appId) {
1634
1891
  result.skipped.push({ file: rel, reason: `ファイル名のアプリ ${m[1]} と封筒の appId ${appId} が違う` });
@@ -1640,27 +1897,55 @@ function takeInbox(cwd, ws, envName) {
1640
1897
  continue;
1641
1898
  }
1642
1899
  const dir = appDirFor(cwd, env, appId, typeof data.appName === "string" ? data.appName : "");
1643
- const dest = resolveWrite(path6.join(dir, name), WRITE_ROOTS.kintone, cwd);
1644
- const text = readFileSync5(src, "utf8");
1645
- if (existsSync4(dest)) {
1646
- if (readFileSync5(dest, "utf8") !== text) {
1647
- result.skipped.push({ file: rel, reason: `行き先に同じ名前の別の中身がある: ${path6.relative(cwd, dest)}` });
1900
+ const dest = resolveWrite(path10.join(dir, name), WRITE_ROOTS.kintone, cwd);
1901
+ const text = readFileSync7(src, "utf8");
1902
+ if (existsSync5(dest)) {
1903
+ if (readFileSync7(dest, "utf8") !== text) {
1904
+ result.skipped.push({ file: rel, reason: `行き先に同じ名前の別の中身がある: ${path10.relative(cwd, dest)}` });
1648
1905
  continue;
1649
1906
  }
1650
- unlinkSync(src);
1651
- result.moved.push({ file: rel, to: path6.relative(cwd, dest), same: true });
1907
+ const removed2 = removeSource(src, result.warnings, rel);
1908
+ result.moved.push({ file: rel, to: path10.relative(cwd, dest), same: true, ...removed2 ? {} : { leftInInbox: true } });
1652
1909
  continue;
1653
1910
  }
1654
- mkdirSync(path6.dirname(dest), { recursive: true });
1655
- writeFileSync(dest, text, "utf8");
1656
- unlinkSync(src);
1657
- result.moved.push({ file: rel, to: path6.relative(cwd, dest), same: false });
1911
+ try {
1912
+ assertChangeAllowed(cwd, dest, "snapshot");
1913
+ mkdirSync(path10.dirname(dest), { recursive: true });
1914
+ const { cleanup } = writeNewFile(dest, text, () => {
1915
+ if (resolveWrite(path10.join(dir, name), WRITE_ROOTS.kintone, cwd) !== dest) throw new PermissionError(`書く先のフォルダーが途中で変わった(symlink など): ${path10.relative(cwd, dest)}`);
1916
+ assertChangeAllowed(cwd, dest, "snapshot");
1917
+ });
1918
+ for (const c of cleanup) result.warnings.push(`${rel} は行き先に置いたが、一時ファイルを消せなかった(${c})。${path10.relative(cwd, path10.dirname(dest))} の .${name}.*.tmp を消してよい`);
1919
+ } catch (e) {
1920
+ if (e instanceof PermissionError) {
1921
+ result.skipped.push({ file: rel, reason: e.message });
1922
+ continue;
1923
+ }
1924
+ if (e instanceof FileExistsError) {
1925
+ result.skipped.push({ file: rel, reason: `行き先に同じ名前のファイルができた(移す途中で): ${path10.relative(cwd, dest)}。もう一度 take する` });
1926
+ continue;
1927
+ }
1928
+ throw e;
1929
+ }
1930
+ const removed = removeSource(src, result.warnings, rel);
1931
+ result.moved.push({ file: rel, to: path10.relative(cwd, dest), same: false, ...removed ? {} : { leftInInbox: true } });
1658
1932
  }
1659
1933
  return result;
1660
1934
  }
1661
1935
 
1662
1936
  // src/kintone-rest.ts
1663
- var ALLOWED_APIS = ["app", "app/form/fields", "app/form/layout", "record", "preview/app/form/fields", "preview/app/form/layout", "app/plugin/config", "preview/app/plugin/config"];
1937
+ 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"];
1938
+ var RECEIVE_LIMITS = {
1939
+ app: 64 * 1024,
1940
+ "app/form/fields": 8 * 1024 * 1024,
1941
+ "app/form/layout": 8 * 1024 * 1024,
1942
+ "preview/app/form/fields": 8 * 1024 * 1024,
1943
+ "preview/app/form/layout": 8 * 1024 * 1024,
1944
+ record: 8 * 1024 * 1024,
1945
+ records: 8 * 1024 * 1024,
1946
+ "app/plugin/config": 2 * 1024 * 1024,
1947
+ "preview/app/plugin/config": 2 * 1024 * 1024
1948
+ };
1664
1949
  var RestError = class extends Error {
1665
1950
  status;
1666
1951
  code;
@@ -1675,6 +1960,37 @@ var RestError = class extends Error {
1675
1960
  };
1676
1961
  var NotAllowedError = class extends Error {
1677
1962
  };
1963
+ async function readCapped(res, limit, apiPath) {
1964
+ const tooLarge = () => new RestError(`kintone ${apiPath} の応答が大きすぎる(上限 ${limit} バイト)`, res.status, "LimitError", apiPath);
1965
+ const quietCancel = async (reader2) => {
1966
+ try {
1967
+ await reader2.cancel();
1968
+ } catch {
1969
+ }
1970
+ };
1971
+ if (res.body === null) return "";
1972
+ if (!res.body || typeof res.body.getReader !== "function") throw new RestError(`kintone ${apiPath} の応答に本文のストリームが無い`, res.status, "ResponseError", apiPath);
1973
+ const reader = res.body.getReader();
1974
+ const declared = Number(res.headers?.get("content-length") ?? Number.NaN);
1975
+ if (Number.isFinite(declared) && declared > limit) {
1976
+ await quietCancel(reader);
1977
+ throw tooLarge();
1978
+ }
1979
+ const chunks = [];
1980
+ let total = 0;
1981
+ for (; ; ) {
1982
+ const { done, value } = await reader.read();
1983
+ if (done) break;
1984
+ if (!value) continue;
1985
+ total += value.length;
1986
+ if (total > limit) {
1987
+ await quietCancel(reader);
1988
+ throw tooLarge();
1989
+ }
1990
+ chunks.push(value);
1991
+ }
1992
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
1993
+ }
1678
1994
  function apiPathOf(api, guestSpaceId) {
1679
1995
  if (!ALLOWED_APIS.includes(api)) throw new NotAllowedError(`この API は authoring tools では呼ばない: ${api}(許可: ${ALLOWED_APIS.join(", ")})`);
1680
1996
  if (guestSpaceId !== void 0 && (!Number.isInteger(guestSpaceId) || guestSpaceId <= 0)) throw new NotAllowedError(`ゲストスペースの ID が不正: ${String(guestSpaceId)}`);
@@ -1702,7 +2018,7 @@ function createRestClient(auth, fetchImpl = fetch) {
1702
2018
  for (const [k, v] of Object.entries(params)) if (v !== void 0) url.searchParams.set(k, String(v));
1703
2019
  if (url.origin !== baseUrl || url.username || url.password) throw new NotAllowedError(`送信先が kintone の接続先と違うので送らない: ${url.origin}`);
1704
2020
  const res = await fetchImpl(url.href, { method: "GET", headers });
1705
- const text = await res.text();
2021
+ const text = await readCapped(res, RECEIVE_LIMITS[api], apiPath);
1706
2022
  if (!res.ok) {
1707
2023
  let code;
1708
2024
  try {
@@ -1713,7 +2029,11 @@ function createRestClient(auth, fetchImpl = fetch) {
1713
2029
  const hint = res.status === 401 ? "(認証情報かアクセス権を確かめる。API トークンならそのアプリで有効か)" : res.status === 403 ? "(アクセス権が無い)" : res.status === 404 ? "(アプリかレコードが無い)" : "";
1714
2030
  throw new RestError(`kintone ${apiPath} が HTTP ${res.status}${code ? ` ${code}` : ""}${hint}`, res.status, code, apiPath);
1715
2031
  }
1716
- return JSON.parse(text);
2032
+ try {
2033
+ return JSON.parse(text);
2034
+ } catch {
2035
+ throw new RestError(`kintone ${apiPath} の応答が JSON でない${text ? "" : "(本文が空)"}`, res.status, "ResponseError", apiPath);
2036
+ }
1717
2037
  }
1718
2038
  };
1719
2039
  }
@@ -1953,8 +2273,12 @@ function columnShape(type, values) {
1953
2273
  return `最大 ${longest}${multiLine ? `(複数行の値 ${multiLine} 行)` : ""}${filled.length < shapes.length ? `、空 ${shapes.length - filled.length} 行` : ""}`;
1954
2274
  }
1955
2275
  function describeRecord(file) {
2276
+ const header = `アプリ ${file.appId} レコード ${file.id}(取得 ${file.fetchedAt}${file.keptFields ? `、--fields-from で ${file.keptFields.length} 項目に絞った` : ""})。値は出さない(形だけ)`;
2277
+ return [header, ...shapeLines(file.record)].join("\n");
2278
+ }
2279
+ function shapeLines(record2) {
1956
2280
  const lines = [];
1957
- for (const [code, fv] of Object.entries(file.record)) {
2281
+ for (const [code, fv] of Object.entries(record2)) {
1958
2282
  if (code === "$id" || code === "$revision" || !fv) continue;
1959
2283
  if (fv.type === "SUBTABLE" && Array.isArray(fv.value)) {
1960
2284
  const rows = fv.value;
@@ -1972,8 +2296,7 @@ function describeRecord(file) {
1972
2296
  }
1973
2297
  lines.push(`${code} ${fv.type} ${shapeOf(fv)}`);
1974
2298
  }
1975
- const header = `アプリ ${file.appId} レコード ${file.id}(取得 ${file.fetchedAt}${file.keptFields ? `、--fields-from で ${file.keptFields.length} 項目に絞った` : ""})。値は出さない(形だけ)`;
1976
- return [header, ...lines].join("\n");
2299
+ return lines;
1977
2300
  }
1978
2301
  function summarizeRecord(file) {
1979
2302
  const codes = Object.keys(file.record);
@@ -1982,6 +2305,30 @@ function summarizeRecord(file) {
1982
2305
  return `アプリ ${file.appId} レコード ${file.id}: 項目 ${codes.length}${tables.length ? `(テーブル ${rows.join(", ")})` : ""}${file.keptFields ? `。--fields-from で ${file.keptFields.length} 項目に絞った` : ""}。値は表示しない`;
1983
2306
  }
1984
2307
 
2308
+ // src/dev-paths.ts
2309
+ import { existsSync as existsSync6, realpathSync as realpathSync2 } from "node:fs";
2310
+ import path11 from "node:path";
2311
+ function linkedRoot(env, name, envVar, fallback) {
2312
+ const v = env[envVar];
2313
+ if (v) return v;
2314
+ const linked = path11.join(AUTHORING_ROOT, "node_modules", name);
2315
+ if (existsSync6(path11.join(linked, "package.json"))) {
2316
+ try {
2317
+ return realpathSync2(linked);
2318
+ } catch {
2319
+ return linked;
2320
+ }
2321
+ }
2322
+ return fallback;
2323
+ }
2324
+ function printCraftRootOf(env) {
2325
+ return linkedRoot(env, "print-craft", "PCRAFT_PRINT_CRAFT_ROOT", path11.resolve(REPO_ROOT, "..", "print-craft"));
2326
+ }
2327
+ function printCraftProdDir(env) {
2328
+ return path11.join(printCraftRootOf(env), "prod");
2329
+ }
2330
+ var PRINT_CRAFT_ROOT = printCraftRootOf(process.env);
2331
+
1985
2332
  // src/normalize/line-diff.ts
1986
2333
  var MAX_CELLS = 4e6;
1987
2334
  function lineDiff(a, b) {
@@ -2036,7 +2383,7 @@ function formatDiff(lines, context2 = 2) {
2036
2383
  }
2037
2384
 
2038
2385
  // src/commands/buttons.ts
2039
- import { createHash } from "node:crypto";
2386
+ import { createHash as createHash2 } from "node:crypto";
2040
2387
  var ButtonNotFoundError = class extends Error {
2041
2388
  };
2042
2389
  var kb = (v) => `${(Buffer.byteLength(JSON.stringify(v) ?? "", "utf8") / 1024).toFixed(1)} KB`;
@@ -2046,7 +2393,7 @@ var oneLine = (s, max = 160) => {
2046
2393
  };
2047
2394
  var chars = (s) => [...s ?? ""].length;
2048
2395
  function shortenDataUrls(s, opt = {}) {
2049
- return s.replace(/data:[^"'\s)<>]{120,}/g, (m) => `${m.slice(0, 40)}…(data URL ${m.length.toLocaleString()} 文字${opt.hash ? `、sha256 ${createHash("sha256").update(m).digest("hex").slice(0, 8)}` : ""})`);
2396
+ return s.replace(/data:[^"'\s)<>]{120,}/g, (m) => `${m.slice(0, 40)}…(data URL ${m.length.toLocaleString()} 文字${opt.hash ? `、sha256 ${createHash2("sha256").update(m).digest("hex").slice(0, 8)}` : ""})`);
2050
2397
  }
2051
2398
  function screensOf(b) {
2052
2399
  const ids = (b.viewsCsv ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -2261,41 +2608,13 @@ function defaultPullName(appName, appId) {
2261
2608
  }
2262
2609
 
2263
2610
  // src/engine.ts
2264
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
2265
- import path9 from "node:path";
2611
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
2612
+ import path12 from "node:path";
2266
2613
  import vm from "node:vm";
2267
2614
 
2268
- // src/paths.ts
2269
- import { existsSync as existsSync5, realpathSync as realpathSync2 } from "node:fs";
2270
- import path7 from "node:path";
2271
- import { fileURLToPath } from "node:url";
2272
- var here = path7.dirname(fileURLToPath(import.meta.url));
2273
- var AUTHORING_ROOT = path7.resolve(here, "..");
2274
- var REPO_ROOT = path7.resolve(AUTHORING_ROOT, "..");
2275
- var VENDOR_DIR = path7.join(AUTHORING_ROOT, "vendor");
2276
- var MOMENT_FILE = path7.join(VENDOR_DIR, "moment-with-locales.min.js");
2277
- function linkedRoot(name, envVar, fallback) {
2278
- const env = process.env[envVar];
2279
- if (env) return env;
2280
- const linked = path7.join(AUTHORING_ROOT, "node_modules", name);
2281
- if (existsSync5(path7.join(linked, "package.json"))) {
2282
- try {
2283
- return realpathSync2(linked);
2284
- } catch {
2285
- return linked;
2286
- }
2287
- }
2288
- return fallback;
2289
- }
2290
- var PRINT_CRAFT_ROOT = linkedRoot("print-craft", "PCRAFT_PRINT_CRAFT_ROOT", path7.resolve(REPO_ROOT, "..", "print-craft"));
2291
- function devPluginDir() {
2292
- const dir = path7.join(PRINT_CRAFT_ROOT, "prod");
2293
- return existsSync5(path7.join(dir, "desktop_js", "KintoneFormulaPCraft.min.js")) ? dir : null;
2294
- }
2295
-
2296
2615
  // src/plugin-zip.ts
2297
- import { createHash as createHash2 } from "node:crypto";
2298
- import { readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
2616
+ import { createHash as createHash3 } from "node:crypto";
2617
+ import { readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
2299
2618
  import { inflateRawSync } from "node:zlib";
2300
2619
  var SIG_EOCD = 101010256;
2301
2620
  var SIG_CENTRAL = 33639248;
@@ -2396,18 +2715,18 @@ var BIGNUMBER_ENTRY = "desktop_js/bignumber.min.js";
2396
2715
  var MOMENT_TZ_ENTRY = "desktop_js/moment-timezone-with-data.min.js";
2397
2716
  var API_ENTRY = "config_js/print-craft-authoring-api.js";
2398
2717
  var MANIFEST_ENTRY = "manifest.json";
2399
- function sha256Hex(data) {
2400
- return createHash2("sha256").update(data).digest("hex");
2718
+ function sha256Hex2(data) {
2719
+ return createHash3("sha256").update(data).digest("hex");
2401
2720
  }
2402
2721
  function pluginIdOf(publicKey) {
2403
- return sha256Hex(publicKey).slice(0, 32).replace(/[0-9a-f]/g, (c) => "abcdefghijklmnop"["0123456789abcdef".indexOf(c)]);
2722
+ return sha256Hex2(publicKey).slice(0, 32).replace(/[0-9a-f]/g, (c) => "abcdefghijklmnop"["0123456789abcdef".indexOf(c)]);
2404
2723
  }
2405
2724
  function readPluginZip(file) {
2406
2725
  let outer;
2407
2726
  try {
2408
2727
  const size = statSync4(file).size;
2409
2728
  if (size > ZIP_LIMITS.maxOuterBytes) throw new PluginZipError(`大きすぎる(${size} バイト)`);
2410
- outer = unzip(readFileSync6(file));
2729
+ outer = unzip(readFileSync8(file));
2411
2730
  } catch (e) {
2412
2731
  throw new PluginZipError(`印刷屋の zip を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
2413
2732
  }
@@ -2446,131 +2765,37 @@ function readPluginZip(file) {
2446
2765
  momentTimezone: momentTimezone.toString("utf8"),
2447
2766
  api: api?.toString("utf8"),
2448
2767
  sha256: {
2449
- engine: sha256Hex(engine),
2450
- ...api ? { api: sha256Hex(api) } : {},
2451
- bignumber: sha256Hex(bignumber),
2452
- momentTimezone: sha256Hex(momentTimezone),
2453
- contents: sha256Hex(contents)
2768
+ engine: sha256Hex2(engine),
2769
+ ...api ? { api: sha256Hex2(api) } : {},
2770
+ bignumber: sha256Hex2(bignumber),
2771
+ momentTimezone: sha256Hex2(momentTimezone),
2772
+ contents: sha256Hex2(contents)
2454
2773
  }
2455
2774
  };
2456
2775
  }
2457
2776
 
2458
- // src/meta.ts
2459
- import { execFileSync } from "node:child_process";
2460
- import { createHash as createHash3 } from "node:crypto";
2461
- import { readFileSync as readFileSync7 } from "node:fs";
2462
- import path8 from "node:path";
2463
- var SUPPORTED_PLUGIN_VERSIONS = ["6"];
2464
- var SUPPORTED_API_VERSION = 1;
2465
- var KNOWN_PLUGIN_RELEASES = {
2466
- "6": [
2467
- {
2468
- engine: "8a5f78f78f51e04b5daaa435b0e8a8c0ea4d08d388d84d279f0cd1b50a857140",
2469
- api: "59c27e480f419b9d3480c24426ae93dadf0ce7c46c940bcf7457d8252b26dea3",
2470
- bignumber: "eca7c1c71fee589d7b5c58bd3df3f31d56fbd234821ed8c60ec4e2050ec50129",
2471
- momentTimezone: "31b9bea01ffef2e8f311eafdbbcdd944a12194fa216d8f54489e15a7188d47dc",
2472
- note: "Ver.6(2026-10-04 の PR #2 マージ時点。print-craft 73097d2。描画前の掃除が入る前)"
2473
- },
2474
- {
2475
- engine: "8a5f78f78f51e04b5daaa435b0e8a8c0ea4d08d388d84d279f0cd1b50a857140",
2476
- api: "3c029ba0bb2b2c869de9dc074e588cf3e5cae8caac91a83e3da196ef2aeee673",
2477
- bignumber: "eca7c1c71fee589d7b5c58bd3df3f31d56fbd234821ed8c60ec4e2050ec50129",
2478
- momentTimezone: "31b9bea01ffef2e8f311eafdbbcdd944a12194fa216d8f54489e15a7188d47dc",
2479
- note: "Ver.6(2026-10-04〜05 描画前の掃除 sanitize.ts と共通の設定「外部参照」externalRefs、load.ts の derivedOf(無効な行と空の計算式は formula / usedFields を持たない)を入れた後。API に sanitizeReportCss / sanitizeReportNodes / isAllowedReportUrl / EXTERNAL_REFS / externalRefsOf を追加。途中の API 9cc7daf8… / b22564da… / 6c2e447d… は配っていないので載せない)"
2480
- }
2481
- ]
2482
- };
2483
- var KNOWN_ENGINE_SHA256 = Object.fromEntries(Object.entries(KNOWN_PLUGIN_RELEASES).map(([v, rs]) => [v, rs.map((r) => r.engine)]));
2484
- var REQUIRED_API = {
2485
- apiVersion: "number",
2486
- pluginVersion: "string",
2487
- pluginId: "string",
2488
- CONFIG_SCHEMA: "object",
2489
- CONFIG_LIMITS: "object",
2490
- PAGE_SIZES: "object",
2491
- DPI_OPTIONS: "object",
2492
- PAPER_NAMES: "object",
2493
- PRINT_MODES: "object",
2494
- buildMenuRows: "function",
2495
- normalizeTagsInfo: "function",
2496
- normalizeCssRows: "function",
2497
- toSavedRows: "function",
2498
- computeUsage: "function",
2499
- computePluginUOG: "function",
2500
- createCheckRecord: "function",
2501
- createFieldsInfo: "function",
2502
- isTargetType: "function",
2503
- stripComments: "function",
2504
- defaultCssRows: "function",
2505
- defaultMenuInfo: "function",
2506
- expandFields: "function",
2507
- buildReportCss: "function",
2508
- fileNameOf: "function",
2509
- formulaOf: "function",
2510
- mountReportHtml: "function",
2511
- replaceTags: "function",
2512
- getPaperSize: "function",
2513
- webFontOf: "function",
2514
- DUMMY_IMAGE: "string",
2515
- validate: "function",
2516
- parseJsonSafely: "function",
2517
- ValidationError: "function",
2518
- writeConfig: "function",
2519
- DEFAULT_LIMITS: "object",
2520
- ConfigStoreError: "function",
2521
- // pull(アプリのプラグインの設定を読んで封筒形式にする。2026-10-05)
2522
- readConfig: "function",
2523
- buildExportData: "function"
2524
- };
2525
- function stableJson(v) {
2526
- return JSON.stringify(v, (_k, x) => x instanceof RegExp ? `/${x.source}/${x.flags}` : x);
2527
- }
2528
- function sha256Hex2(data) {
2529
- return createHash3("sha256").update(data).digest("hex");
2530
- }
2531
- function schemaRevisionOf(schema) {
2532
- return sha256Hex2(stableJson(schema)).slice(0, 12);
2533
- }
2534
- function gitCommit(dir) {
2535
- try {
2536
- const head = execFileSync("git", ["-C", dir, "rev-parse", "--short", "HEAD"], { encoding: "utf8" }).trim();
2537
- const dirty = execFileSync("git", ["-C", dir, "status", "--porcelain", "--untracked-files=no"], { encoding: "utf8" }).trim() !== "";
2538
- return dirty ? `${head}+dirty` : head;
2539
- } catch {
2540
- return "unknown";
2541
- }
2542
- }
2543
- function devMeta() {
2544
- const pkg = JSON.parse(readFileSync7(path8.join(AUTHORING_ROOT, "package.json"), "utf8"));
2545
- return { toolsVersion: pkg.version, supportedPluginVersions: SUPPORTED_PLUGIN_VERSIONS, supportedApiVersion: SUPPORTED_API_VERSION, commit: gitCommit(AUTHORING_ROOT), builtAt: "", mode: "dev" };
2546
- }
2547
- function toolsMeta() {
2548
- if (true) return JSON.parse('{"toolsVersion":"1.0.0","supportedPluginVersions":["6"],"supportedApiVersion":1,"commit":"d662e0b","builtAt":"2026-10-06T05:47:45.315Z","mode":"build"}');
2549
- return devMeta();
2550
- }
2551
-
2552
2777
  // src/engine.ts
2553
2778
  var DEFAULT_CONTEXT_BASE_URL = "https://example.cybozu.com";
2554
2779
  var DEFAULT_USER = { id: "1", code: "authoring", name: "authoring", email: "", language: "ja" };
2555
2780
  var loaded = null;
2556
2781
  function resolvePluginSources(opt = {}, mode = opt.mode ?? toolsMeta().mode) {
2557
- const zip = opt.pluginZip ?? process.env.PCRAFT_PLUGIN_ZIP;
2782
+ const zip = opt.pluginZip;
2558
2783
  if (zip) {
2559
- if (!existsSync6(zip)) throw new PluginZipError(`印刷屋の zip が無い: ${zip}(.env の PCRAFT_PLUGIN_ZIP)`);
2784
+ if (!existsSync7(zip)) throw new PluginZipError(`印刷屋の zip が無い: ${zip}(.env の PCRAFT_PLUGIN_ZIP)`);
2560
2785
  return { ...readPluginZip(zip), kind: "zip" };
2561
2786
  }
2562
- const devAllowed = mode === "dev" && process.env.PCRAFT_ALLOW_DEV_PLUGIN === "1";
2787
+ const dev = opt.devPluginDir;
2788
+ const devAllowed = mode === "dev" && !!dev;
2563
2789
  if (!devAllowed) {
2564
2790
  throw new PluginZipError(`印刷屋の zip の場所が分からない。.env に PCRAFT_PLUGIN_ZIP=<印刷屋プラグインの zip のパス> を書く${mode === "dev" ? "(開発中に隣の print-craft の prod/ を読むなら環境変数 PCRAFT_ALLOW_DEV_PLUGIN=1)" : ""}`);
2565
2791
  }
2566
- const dev = devPluginDir();
2567
- if (!dev) throw new PluginZipError("印刷屋の zip の場所が分からない(PCRAFT_ALLOW_DEV_PLUGIN=1 だが隣の print-craft の prod/ が無い)。.env に PCRAFT_PLUGIN_ZIP を書く");
2568
- const read = (rel) => readFileSync8(path9.join(dev, rel), "utf8");
2569
- const apiFile = path9.join(dev, API_ENTRY);
2792
+ if (!dev || !existsSync7(path12.join(dev, ENGINE_ENTRY))) throw new PluginZipError(`印刷屋の zip の場所が分からない(PCRAFT_ALLOW_DEV_PLUGIN=1 だが print-craft の prod/ が無い: ${dev})。.env に PCRAFT_PLUGIN_ZIP を書く`);
2793
+ const read = (rel) => readFileSync9(path12.join(dev, rel), "utf8");
2794
+ const apiFile = path12.join(dev, API_ENTRY);
2570
2795
  const engine = read(ENGINE_ENTRY);
2571
2796
  const bignumber = read(BIGNUMBER_ENTRY);
2572
2797
  const momentTimezone = read(MOMENT_TZ_ENTRY);
2573
- const api = existsSync6(apiFile) ? readFileSync8(apiFile, "utf8") : void 0;
2798
+ const api = existsSync7(apiFile) ? readFileSync9(apiFile, "utf8") : void 0;
2574
2799
  const manifest = JSON.parse(read(MANIFEST_ENTRY));
2575
2800
  return {
2576
2801
  kind: "dev",
@@ -2581,47 +2806,22 @@ function resolvePluginSources(opt = {}, mode = opt.mode ?? toolsMeta().mode) {
2581
2806
  bignumber,
2582
2807
  momentTimezone,
2583
2808
  api,
2584
- sha256: { engine: sha256Hex(engine), ...api ? { api: sha256Hex(api) } : {}, bignumber: sha256Hex(bignumber), momentTimezone: sha256Hex(momentTimezone) }
2809
+ sha256: { engine: sha256Hex2(engine), ...api ? { api: sha256Hex2(api) } : {}, bignumber: sha256Hex2(bignumber), momentTimezone: sha256Hex2(momentTimezone) }
2585
2810
  };
2586
2811
  }
2587
- var PARTS = [
2588
- ["計算式エンジン", "engine"],
2589
- ["authoring API", "api"],
2590
- ["bignumber", "bignumber"],
2591
- ["moment-timezone", "momentTimezone"]
2592
- ];
2593
- function unknownParts(src, releases = KNOWN_PLUGIN_RELEASES) {
2594
- const list = releases[src.pluginVersion];
2595
- if (!list?.length) return ["版の一覧が無い"];
2596
- const matches = (r) => PARTS.filter(([, k]) => src.sha256[k] && r[k] === src.sha256[k]).length;
2597
- if (list.some((r) => matches(r) === PARTS.length)) return [];
2598
- const show2 = ([label, k]) => `${label} ${src.sha256[k] ? src.sha256[k].slice(0, 12) + "…" : "無し"}`;
2599
- const bestScore = Math.max(...list.map(matches));
2600
- if (bestScore === 0) return PARTS.map(show2);
2601
- const candidates = list.filter((r) => matches(r) === bestScore);
2602
- const diffs = candidates.map((r) => PARTS.filter(([, k]) => !src.sha256[k] || r[k] !== src.sha256[k]).map(([label]) => label).join("|"));
2603
- if (new Set(diffs).size !== 1) return PARTS.map(show2);
2604
- const best = candidates[0];
2605
- const diff2 = PARTS.filter(([, k]) => !src.sha256[k] || best[k] !== src.sha256[k]).map(show2);
2606
- return diff2.length ? diff2 : PARTS.map(show2);
2607
- }
2608
2812
  async function loadEngine(opt = {}) {
2609
2813
  if (loaded) return loaded;
2610
2814
  const meta = toolsMeta();
2611
2815
  const src = resolvePluginSources(opt, opt.mode ?? meta.mode);
2612
2816
  const warnings = [];
2613
- if (!SUPPORTED_PLUGIN_VERSIONS.includes(src.pluginVersion)) {
2614
- throw new PluginZipError(`印刷屋プラグインの版 ${src.pluginVersion || "不明"}(${src.from})には対応していない。tools が対応する版: ${SUPPORTED_PLUGIN_VERSIONS.join(", ")}`);
2615
- }
2616
- if (!src.api) throw new PluginZipError(`印刷屋の zip に ${API_ENTRY} が無い(Ver.6 以降の zip が要る): ${src.from}`);
2617
- const unknown = unknownParts(src);
2618
- const engineKnown = unknown.length === 0;
2619
- if (!engineKnown) {
2620
- const head = `印刷屋の zip の中身が tools の既知のリリースと違う: ${unknown.join("、")}(${src.from})`;
2621
- if (src.kind === "dev") warnings.push(`${head}。開発中の print-craft(PCRAFT_ALLOW_DEV_PLUGIN=1)なので続ける`);
2622
- else if (opt.allowUnknown) warnings.push(`${head}。PCRAFT_ALLOW_UNKNOWN_PLUGIN=1 なので続ける(結果が設定画面と違うことがある)`);
2623
- else throw new PluginZipError(`${head}。配布元から取り直した zip を使うか、tools を新しい版にする。新しい修正版の zip だと分かっていて続けるなら、利用者が .env に PCRAFT_ALLOW_UNKNOWN_PLUGIN=1 を書く(zip の中のコードはこの PC の権限で動く。AI は .env を書けない)`);
2817
+ if (src.kind === "zip" && src.pluginId !== PRINT_CRAFT_PLUGIN_ID) {
2818
+ throw new PluginZipError(`印刷屋プラグインの zip ではない(プラグイン ID ${src.pluginId ?? "不明(PUBKEY が無い)"}。印刷屋は ${PRINT_CRAFT_PLUGIN_ID}): ${src.from}。配布元から入手した印刷屋の zip を PCRAFT_PLUGIN_ZIP に書く`);
2624
2819
  }
2820
+ if (src.kind === "dev") warnings.push(`開発中の print-craft(${src.from}。PCRAFT_ALLOW_DEV_PLUGIN=1)から読む。プラグイン ID は確かめない`);
2821
+ if (!isSupportedPluginVersion(src.pluginVersion)) {
2822
+ throw new PluginZipError(`印刷屋プラグインの版 ${src.pluginVersion || "不明"}(${src.from})には対応していない。tools が扱うのは Ver.${MIN_PLUGIN_VERSION} 以降`);
2823
+ }
2824
+ if (!src.api) throw new PluginZipError(`印刷屋の zip に ${API_ENTRY} が無い(Ver.${MIN_PLUGIN_VERSION} 以降の zip が要る): ${src.from}`);
2625
2825
  const g = globalThis;
2626
2826
  const { Window } = await import("happy-dom");
2627
2827
  const ctx = { baseUrl: DEFAULT_CONTEXT_BASE_URL, appId: 1, loginUser: { ...DEFAULT_USER } };
@@ -2659,7 +2859,7 @@ async function loadEngine(opt = {}) {
2659
2859
  window.kintone = g.kintone;
2660
2860
  window.cybozu = g.cybozu;
2661
2861
  window.rex0220_users_info3 = g.rex0220_users_info3;
2662
- if (!existsSync6(MOMENT_FILE)) throw new Error(`moment が無い: ${MOMENT_FILE}(tools の npm install 時に取る。手で取るなら npm run vendor)`);
2862
+ if (!existsSync7(MOMENT_FILE)) throw new Error(`moment が無い: ${MOMENT_FILE}(tools の npm install 時に取る。手で取るなら npm run vendor)`);
2663
2863
  const runScript = (code, filename) => {
2664
2864
  vm.runInThisContext(code, { filename });
2665
2865
  for (const k of ["moment", "rex0220_BigNumber", "rex0220_Decimal", "rex0220p", "rex0220PrintCraftAuthoring"]) {
@@ -2667,7 +2867,7 @@ async function loadEngine(opt = {}) {
2667
2867
  if (window[k] === void 0 && g[k] !== void 0) window[k] = g[k];
2668
2868
  }
2669
2869
  };
2670
- runScript(readFileSync8(MOMENT_FILE, "utf8"), MOMENT_FILE);
2870
+ runScript(readFileSync9(MOMENT_FILE, "utf8"), MOMENT_FILE);
2671
2871
  runScript(src.momentTimezone, `${src.from}!${MOMENT_TZ_ENTRY}`);
2672
2872
  runScript(src.bignumber, `${src.from}!${BIGNUMBER_ENTRY}`);
2673
2873
  runScript(src.engine, `${src.from}!${ENGINE_ENTRY}`);
@@ -2677,7 +2877,7 @@ async function loadEngine(opt = {}) {
2677
2877
  if (typeof Ctor !== "function") throw new Error("KintoneFormulaPCraft is not loaded");
2678
2878
  const api = window.rex0220PrintCraftAuthoring;
2679
2879
  if (!api || typeof api !== "object") throw new PluginZipError("印刷屋の authoring API(rex0220PrintCraftAuthoring)が読めない");
2680
- if (api.apiVersion !== SUPPORTED_API_VERSION) throw new PluginZipError(`印刷屋の authoring API の版 ${String(api.apiVersion)} には対応していない(tools は ${SUPPORTED_API_VERSION})。tools を印刷屋の版に合わせて更新する`);
2880
+ if (!SUPPORTED_API_VERSIONS.includes(api.apiVersion)) throw new PluginZipError(`印刷屋の authoring API の版 ${String(api.apiVersion)} には対応していない(tools は ${SUPPORTED_API_VERSIONS.join(", ")})。tools を新しい版にする`);
2681
2881
  if (String(api.pluginVersion) !== src.pluginVersion) throw new PluginZipError(`authoring API の印刷屋の版 ${String(api.pluginVersion)} が zip の manifest の版 ${src.pluginVersion} と違う(組み替えられた zip)`);
2682
2882
  const bad = Object.entries(REQUIRED_API).filter(([k, t]) => typeof api[k] !== t).map(([k]) => k);
2683
2883
  if (bad.length) throw new PluginZipError(`印刷屋の authoring API に tools が使うものが無い、または型が違う: ${bad.join(", ")}(tools と印刷屋の版を合わせる)`);
@@ -2685,7 +2885,7 @@ async function loadEngine(opt = {}) {
2685
2885
  window,
2686
2886
  Ctor,
2687
2887
  api,
2688
- source: { kind: src.kind, from: src.from, pluginVersion: src.pluginVersion, engineSha256: src.sha256.engine, apiSha256: src.sha256.api, engineKnown, ...src.pluginId ? { pluginId: src.pluginId } : {} },
2888
+ source: { kind: src.kind, from: src.from, pluginVersion: src.pluginVersion, engineSha256: src.sha256.engine, apiSha256: src.sha256.api, ...src.pluginId ? { pluginId: src.pluginId } : {} },
2689
2889
  warnings,
2690
2890
  setContext(next) {
2691
2891
  if (next.baseUrl) ctx.baseUrl = next.baseUrl.replace(/\/+$/, "");
@@ -2892,6 +3092,18 @@ async function runPreview(input) {
2892
3092
  return { findings, results, skipped, summary };
2893
3093
  }
2894
3094
 
3095
+ // src/context.ts
3096
+ function createContext(opt) {
3097
+ return Object.freeze({ root: realResolve(".", opt.cwd), env: Object.freeze({ ...opt.env }) });
3098
+ }
3099
+
3100
+ // src/commands/save.ts
3101
+ import { createHash as createHash4 } from "node:crypto";
3102
+ var MAX_SAVE_INPUT_BYTES = 256 * 1024;
3103
+ function digestOf(text) {
3104
+ return createHash4("sha256").update(text).digest("hex");
3105
+ }
3106
+
2895
3107
  // src/cli.ts
2896
3108
  var USAGE = `使い方: npx @rex0220/print-craft-authoring-tools <command> [options]
2897
3109
  (短い npx pcraft-authoring は使わない。npm ci の前だと npm の公開レジストリの同じ名前のパッケージを取りに行く)
@@ -2905,14 +3117,16 @@ var USAGE = `使い方: npx @rex0220/print-craft-authoring-tools <command> [opti
2905
3117
  取得済みの fields/<N>.json を 1 項目 1 行で(レイアウトの順。型・ラベル・書式・単位・選択肢・ルックアップ)。通信しない。
2906
3118
  pull --app N [--preview] [--guest <spaceId>] [--out settings/<file>] [--force] [--plugin-id <ID>]
2907
3119
  アプリに入っている印刷屋の今の設定を取って、設定画面の「設定をダウンロード」と同じ封筒形式で settings/APP<N>-<アプリ名>.json に保存(GET だけ)。
2908
- kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得する」を有効にした環境だけ。権限は運用中の設定がレコード閲覧+追加、
2909
- --preview(動作テスト環境 = 保存して未反映の設定)がアプリ管理。プラグイン ID は印刷屋の zip から。既にあるファイルは --force で上書き。
3120
+ kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得する」を有効にした環境だけ。権限は運用中の設定がレコード閲覧(API トークンでも可)、
3121
+ --preview(動作テスト環境 = 保存して未反映の設定)がアプリ管理。プラグイン ID は印刷屋の zip から。既にあるファイルは --force で上書き
3122
+ (--force は settings/ に置くときだけ。environments.json があるときはダウンロードと同じ日時の名前で新しく置き、上書きしない)。
2910
3123
  record --app N --id R [--fields-from <settings.json>] [--guest <spaceId>] [--out records/<file>]
2911
3124
  レコードを records/<N>-<R>.json に保存。--fields-from で設定が使う項目だけ残す。
2912
3125
  record --app N --id R --summary
2913
3126
  取得済みの records/<N>-<R>.json を 1 項目 1 行で。値は出さず形だけ(文字数・行数・数値の桁・テーブルの行数・添付の件数と種類)。通信しない。
2914
- buttons <settings.json> [--button <名前>]
3127
+ buttons <settings.json> [--button <名前>] [--json]
2915
3128
  設定のボタン一覧(画面・保存先・用紙・表示条件・ファイル名・帳票の行・更新項目)。--button でそのボタンの HTML / CSS / 計算式(data: の URL は長さだけ)。
3129
+ --json はファイルの digest(sha256)とボタンの名前の一覧も(直すときの照合に使う)。
2916
3130
  normalize <settings.json> --fields <fields.json> [--out settings/<file>] [--dry-run] [--check] [--json]
2917
3131
  設定画面と同じ手順で派生値を作り直し、検査して、エラーが無ければ書き戻す(既定は同じファイルに上書き。--dry-run は書かない)。
2918
3132
  --check は入力の派生値と生成した値の差を出す。外部 URL の承認は policy/authoring-policy.json(場所は固定。利用者が書く)。
@@ -2945,6 +3159,12 @@ kintone には GET しか送らない。
2945
3159
  var FLAGS = /* @__PURE__ */ new Set(["preview", "check", "json", "dry-run", "derived", "summary", "force"]);
2946
3160
  var UsageError = class extends Error {
2947
3161
  };
3162
+ var W;
3163
+ var resolveRead2 = (target) => resolveRead(target, W.root);
3164
+ var resolveWrite2 = (target, roots) => resolveWrite(target, roots, W.root);
3165
+ var resolveWriteDir2 = (target, roots) => resolveWriteDir(target, roots, W.root);
3166
+ var authOpt = () => ({ cwd: W.root, env: W.env });
3167
+ var allow = (file, op) => assertChangeAllowed(W.root, file, op);
2948
3168
  function option(args, name) {
2949
3169
  const i = args.indexOf(`--${name}`);
2950
3170
  return i >= 0 ? args[i + 1] : void 0;
@@ -2977,20 +3197,35 @@ function rejectRemovedOptions(args) {
2977
3197
  if (args.includes(`--${name}`)) throw new UsageError(`--${name} は使えない。.env と policy/authoring-policy.json と PCRAFT_PLUGIN_ZIP は作業フォルダーのものに固定(AI が書けるファイルを読ませないため)`);
2978
3198
  }
2979
3199
  }
2980
- function writeJson(file, data) {
2981
- mkdirSync2(path10.dirname(file), { recursive: true });
2982
- writeFileSync2(file, JSON.stringify(data, null, 2) + "\n", "utf8");
3200
+ function writeText(file, text, roots, op, opt = {}) {
3201
+ const real = resolveWrite2(file, roots);
3202
+ allow(real, op);
3203
+ mkdirSync2(path13.dirname(real), { recursive: true });
3204
+ if (opt.exclusive ?? op === "snapshot") {
3205
+ try {
3206
+ const { cleanup } = writeNewFile(real, text, () => {
3207
+ if (resolveWrite2(file, roots) !== real) throw new InputError(`書く先のフォルダーが途中で変わった(symlink など): ${shown(real)}`);
3208
+ allow(real, op);
3209
+ });
3210
+ for (const c of cleanup) console.error(`注意: 書いたが一時ファイルを消せなかった(${c})`);
3211
+ } catch (e) {
3212
+ if (e instanceof FileExistsError) throw new InputError(`${shown(real)} は既にある(確かめた後に作られた)。上書きしない`);
3213
+ throw e;
3214
+ }
3215
+ return real;
3216
+ }
3217
+ writeFileSync2(real, text, "utf8");
3218
+ return real;
2983
3219
  }
2984
- function writeText(file, text) {
2985
- mkdirSync2(path10.dirname(file), { recursive: true });
2986
- writeFileSync2(file, text, "utf8");
3220
+ function writeJson(file, data, roots, op, opt = {}) {
3221
+ return writeText(file, JSON.stringify(data, null, 2) + "\n", roots, op, opt);
2987
3222
  }
2988
3223
  function shown(file) {
2989
- const rel = path10.relative(process.cwd(), file);
3224
+ const rel = path13.relative(W.root, file);
2990
3225
  return rel && !rel.startsWith("..") ? rel : file;
2991
3226
  }
2992
3227
  async function engineFor() {
2993
- const engine = await loadEngine({ pluginZip: pluginZipPath(), allowUnknown: allowUnknownPlugin() });
3228
+ const engine = await loadEngine({ pluginZip: pluginZipPath(authOpt()), devPluginDir: W.env.PCRAFT_ALLOW_DEV_PLUGIN === "1" ? printCraftProdDir(W.env) : void 0 });
2994
3229
  for (const w of engine.warnings) console.error(`注意: ${w}`);
2995
3230
  return engine;
2996
3231
  }
@@ -3007,12 +3242,12 @@ async function version(args) {
3007
3242
  if (expect !== void 0 && !expect) throw new UsageError("--expect には印刷屋の版(例: 6)を続ける");
3008
3243
  let expectError;
3009
3244
  if (expect !== void 0 && engine) {
3010
- if (!meta.supportedPluginVersions.includes(expect)) expectError = `--expect ${expect}: tools ${meta.toolsVersion} が対応する印刷屋の版は ${meta.supportedPluginVersions.join(", ")}(tools を更新する)`;
3245
+ if (!isSupportedPluginVersion(expect)) expectError = `--expect ${expect}: tools ${meta.toolsVersion} が扱う印刷屋の版は Ver.${meta.minPluginVersion} 以降`;
3011
3246
  else if (engine.source.pluginVersion !== expect) expectError = `--expect ${expect}: zip の印刷屋の版は ${engine.source.pluginVersion}(${engine.source.from}。アプリに入れた版の zip を PCRAFT_PLUGIN_ZIP に書く)`;
3012
3247
  }
3013
3248
  const info = {
3014
3249
  ...meta,
3015
- plugin: engine ? { source: engine.source.kind, from: engine.source.from, pluginVersion: engine.source.pluginVersion, apiVersion: engine.api.apiVersion, engineSha256: engine.source.engineSha256, apiSha256: engine.source.apiSha256, engineKnown: engine.source.engineKnown, schemaRevision: schemaRevisionOf(engine.api.CONFIG_SCHEMA) } : null,
3250
+ plugin: engine ? { source: engine.source.kind, from: engine.source.from, pluginVersion: engine.source.pluginVersion, apiVersion: engine.api.apiVersion, engineSha256: engine.source.engineSha256, apiSha256: engine.source.apiSha256, pluginId: engine.source.pluginId ?? null, schemaRevision: schemaRevisionOf(engine.api.CONFIG_SCHEMA) } : null,
3016
3251
  expected: expect,
3017
3252
  ok: !!engine && !expectError,
3018
3253
  error: error ?? expectError
@@ -3020,10 +3255,11 @@ async function version(args) {
3020
3255
  if (flag(args, "json")) {
3021
3256
  console.log(JSON.stringify(info, null, 2));
3022
3257
  } else {
3023
- console.log(`@rex0220/print-craft-authoring-tools ${meta.toolsVersion}${meta.mode === "dev" ? "(開発中)" : ""}(対応する印刷屋の版 ${meta.supportedPluginVersions.join(", ")}、API ${meta.supportedApiVersion}。commit ${meta.commit}${meta.builtAt ? `、ビルド ${meta.builtAt}` : ""})`);
3258
+ console.log(`@rex0220/print-craft-authoring-tools ${meta.toolsVersion}${meta.mode === "dev" ? "(開発中)" : ""}(扱う印刷屋 Ver.${meta.minPluginVersion} 以降、API ${meta.supportedApiVersions.join(", ")}。commit ${meta.commit}${meta.builtAt ? `、ビルド ${meta.builtAt}` : ""})`);
3024
3259
  if (engine) {
3025
3260
  console.log(`印刷屋プラグイン: 版 ${engine.source.pluginVersion}、authoring API ${engine.api.apiVersion}(${engine.source.kind === "zip" ? "zip" : "開発中の print-craft"}: ${engine.source.from})`);
3026
- console.log(`計算式エンジン: sha256 ${engine.source.engineSha256}${engine.source.engineKnown ? "(zip の中身は既知)" : "(既知の一覧と違う中身を含む)"}`);
3261
+ console.log(`プラグイン ID: ${engine.source.pluginId ?? "(開発中の print-craft。確かめない)"}`);
3262
+ console.log(`計算式エンジン: sha256 ${engine.source.engineSha256}`);
3027
3263
  console.log(`設定スキーマの版: ${schemaRevisionOf(engine.api.CONFIG_SCHEMA)}`);
3028
3264
  if (expectError) console.error(expectError);
3029
3265
  else if (expect !== void 0) console.log(`--expect ${expect}: 一致`);
@@ -3034,7 +3270,7 @@ async function version(args) {
3034
3270
  return info.ok ? 0 : 1;
3035
3271
  }
3036
3272
  function context(args) {
3037
- const ws = loadWorkspace();
3273
+ const ws = loadWorkspace(W.root);
3038
3274
  const name = option(args, "env");
3039
3275
  if (args.includes("--env")) {
3040
3276
  if (!name || !isEnvName(name)) throw new UsageError("--env には environments.json の環境の名前を続ける(ファイルの場所は指定できない)");
@@ -3043,7 +3279,7 @@ function context(args) {
3043
3279
  return ws ? { ws, env: pickEnv(ws, name) } : { ws: null };
3044
3280
  }
3045
3281
  function client(ctx) {
3046
- const auth = ctx?.ws && ctx.env ? loadAuthForEnv(ctx.env) : loadAuth();
3282
+ const auth = ctx?.ws && ctx.env ? loadAuthForEnv(ctx.env, W.root) : loadAuth(authOpt());
3047
3283
  return { client: createRestClient(auth), authLabel: describeAuth(auth) };
3048
3284
  }
3049
3285
  var envFlag = (ctx) => ctx.ws && ctx.env && ctx.env.name !== ctx.ws.defaultEnv ? ` --env ${ctx.env.name}` : "";
@@ -3054,7 +3290,7 @@ function appOf(args, ctx) {
3054
3290
  return resolveApp(ctx.ws, ctx.env, v).appId;
3055
3291
  }
3056
3292
  function existingAppDir(ctx, appId) {
3057
- const dir = findAppDir(process.cwd(), ctx.env, appId);
3293
+ const dir = findAppDir(W.root, ctx.env, appId);
3058
3294
  if (!dir) throw new InputError(`アプリ ${appId} のフォルダー(kintone/${ctx.env.host}/${appId}-…)が無い。先に npx @rex0220/print-craft-authoring-tools fields --app ${appId}${envFlag(ctx)}`);
3059
3295
  return dir;
3060
3296
  }
@@ -3062,38 +3298,39 @@ function noOutInWorkspace(args, ctx) {
3062
3298
  if (ctx.ws && args.includes("--out")) throw new UsageError("environments.json があるときは --out を使わない(保存先はアプリのフォルダー kintone/<ホスト名>/<番号>-<アプリ名>/)");
3063
3299
  }
3064
3300
  function existingFile(rel, howToFetch) {
3065
- const file = resolveRead(rel);
3066
- if (!existsSync7(file)) throw new InputError(`${shown(file)} が無い。先に npx @rex0220/print-craft-authoring-tools ${howToFetch}`);
3301
+ const file = resolveRead2(rel);
3302
+ if (!existsSync8(file)) throw new InputError(`${shown(file)} が無い。先に npx @rex0220/print-craft-authoring-tools ${howToFetch}`);
3067
3303
  return file;
3068
3304
  }
3069
3305
  function folderDefaults(settingsFile) {
3070
- return loadWorkspace() ? appFolderOfFile(process.cwd(), settingsFile) : null;
3306
+ return loadWorkspace(W.root) ? appFolderOfFile(W.root, settingsFile) : null;
3071
3307
  }
3072
3308
  function fieldsFileOf(args, folder) {
3073
3309
  const arg = option(args, "fields");
3074
- if (arg) return resolveRead(arg);
3310
+ if (arg) return resolveRead2(arg);
3075
3311
  if (!folder) throw new UsageError("--fields <fields.json> が要る(fields コマンドの出力。kintone/<ホスト名>/<番号>-…/ の中のファイルなら同じフォルダーの fields.json を使う)");
3076
- return existingFile(path10.join(folder.dir, "fields.json"), `fields --app ${folder.appId}`);
3312
+ return existingFile(path13.join(folder.dir, "fields.json"), `fields --app ${folder.appId}`);
3077
3313
  }
3078
3314
  function trustedBaseUrl(folder) {
3079
- const ws = loadWorkspace();
3315
+ const ws = loadWorkspace(W.root);
3080
3316
  if (ws && folder) return envsOfHost(ws, folder.host)[0]?.baseUrl;
3081
- return baseUrlFromEnv();
3317
+ return baseUrlFromEnv(authOpt());
3082
3318
  }
3083
3319
  async function fields(args) {
3084
3320
  const ctx = context(args);
3085
3321
  const app = appOf(args, ctx);
3086
3322
  noOutInWorkspace(args, ctx);
3087
3323
  if (flag(args, "summary")) {
3088
- const rel = ctx.ws ? path10.join(existingAppDir(ctx, app), "fields.json") : path10.join("fields", `${app}.json`);
3324
+ const rel = ctx.ws ? path13.join(existingAppDir(ctx, app), "fields.json") : path13.join("fields", `${app}.json`);
3089
3325
  console.log(listFields(await readFieldsFile(existingFile(rel, `fields --app ${app}${envFlag(ctx)}`))));
3090
3326
  return 0;
3091
3327
  }
3092
3328
  const guestSpaceId = intOption(args, "guest", false);
3093
3329
  const { client: c, authLabel } = client(ctx);
3094
3330
  const file = await fetchFields(c, { app, lang: option(args, "lang"), preview: flag(args, "preview"), guestSpaceId });
3095
- const out = ctx.ws ? resolveWrite(path10.join(appDirFor(process.cwd(), ctx.env, app, file.appName), "fields.json"), WRITE_ROOTS.kintone) : resolveWrite(option(args, "out") ?? path10.join("fields", `${app}.json`), WRITE_ROOTS.fields);
3096
- writeJson(out, file);
3331
+ const out = ctx.ws ? resolveWrite2(path13.join(appDirFor(W.root, ctx.env, app, file.appName), "fields.json"), WRITE_ROOTS.kintone) : resolveWrite2(option(args, "out") ?? path13.join("fields", `${app}.json`), WRITE_ROOTS.fields);
3332
+ allow(out, "fields");
3333
+ writeJson(out, file, ctx.ws ? WRITE_ROOTS.kintone : WRITE_ROOTS.fields, "fields");
3097
3334
  console.log(`${summarizeFields(file)}
3098
3335
  ${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}`);
3099
3336
  return 0;
@@ -3103,7 +3340,7 @@ async function record(args) {
3103
3340
  const app = appOf(args, ctx);
3104
3341
  const id = intOption(args, "id", true);
3105
3342
  noOutInWorkspace(args, ctx);
3106
- const defaultRel = () => ctx.ws ? path10.join(existingAppDir(ctx, app), "records", `${id}.json`) : path10.join("records", `${app}-${id}.json`);
3343
+ const defaultRel = () => ctx.ws ? path13.join(existingAppDir(ctx, app), "records", `${id}.json`) : path13.join("records", `${app}-${id}.json`);
3107
3344
  if (flag(args, "summary")) {
3108
3345
  const file2 = existingFile(defaultRel(), `record --app ${app} --id ${id}${envFlag(ctx)}`);
3109
3346
  const data = readJsonLimited(file2);
@@ -3112,18 +3349,19 @@ async function record(args) {
3112
3349
  return 0;
3113
3350
  }
3114
3351
  const guestSpaceId = intOption(args, "guest", false);
3115
- const out = resolveWrite(option(args, "out") ?? defaultRel(), WRITE_ROOTS.records);
3352
+ const out = resolveWrite2(option(args, "out") ?? defaultRel(), WRITE_ROOTS.records);
3116
3353
  const from = option(args, "fields-from");
3117
3354
  let keep;
3118
3355
  if (from) {
3119
- const settings = readJsonLimited(resolveRead(from));
3356
+ const settings = readJsonLimited(resolveRead2(from));
3120
3357
  const codes = usedFieldCodes(settings);
3121
3358
  if (!codes) console.log(`${from} に使う項目の情報が無いので絞らない(normalize した設定なら usedFields から絞れる)`);
3122
3359
  else keep = codes;
3123
3360
  }
3124
3361
  const { client: c, authLabel } = client(ctx);
3362
+ allow(out, "record");
3125
3363
  const file = await fetchRecord(c, { app, id, guestSpaceId, keep });
3126
- writeJson(out, file);
3364
+ writeJson(out, file, WRITE_ROOTS.records, "record");
3127
3365
  console.log(`${summarizeRecord(file)}
3128
3366
  ${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}(個人情報を含む。コミットしない)`);
3129
3367
  return 0;
@@ -3131,24 +3369,25 @@ ${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}(個人情報を含
3131
3369
  async function normalize(args) {
3132
3370
  const [settingsArg] = positional(args);
3133
3371
  if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
3134
- const settingsFile = resolveRead(settingsArg);
3372
+ const settingsFile = resolveRead2(settingsArg);
3135
3373
  const folder = folderDefaults(settingsFile);
3136
3374
  const fieldsFile = fieldsFileOf(args, folder);
3137
3375
  const write = !flag(args, "dry-run");
3138
- if (write && folder && !option(args, "out") && SNAPSHOT_RE.test(path10.basename(settingsFile))) {
3376
+ if (write && folder && !option(args, "out") && SNAPSHOT_RE.test(path13.basename(settingsFile))) {
3139
3377
  throw new UsageError(`ダウンロード / pull のファイルは書き換えない。npx @rex0220/print-craft-authoring-tools edit ${shown(settingsFile)} で -edit.json に写してから直す(検査だけなら --dry-run)`);
3140
3378
  }
3141
- const out = write ? resolveWrite(option(args, "out") ?? settingsFile, WRITE_ROOTS.settings) : void 0;
3379
+ const out = write ? resolveWrite2(option(args, "out") ?? settingsFile, WRITE_ROOTS.settings) : void 0;
3380
+ if (out) allow(out, "settings");
3142
3381
  const fieldsData = await readFieldsFile(fieldsFile);
3143
3382
  const baseUrl = trustedBaseUrl(folder);
3144
3383
  const engine = await engineFor();
3145
3384
  engine.setContext({ baseUrl: baseUrl ?? DEFAULT_CONTEXT_BASE_URL, appId: fieldsData.appId });
3146
3385
  const result = await normalizeSettings({
3147
3386
  settingsText: readTextLimited(settingsFile),
3148
- settingsFile: relativeSettingsPath(settingsArg),
3387
+ settingsFile: relativeSettingsPath(settingsArg, W.root),
3149
3388
  fields: fieldsData,
3150
3389
  engine,
3151
- policy: loadPolicy(),
3390
+ policy: loadPolicy({ cwd: W.root }),
3152
3391
  check: flag(args, "check"),
3153
3392
  baseUrl
3154
3393
  });
@@ -3168,7 +3407,7 @@ async function normalize(args) {
3168
3407
  return 1;
3169
3408
  }
3170
3409
  if (out) {
3171
- writeJson(out, result.output);
3410
+ writeJson(out, result.output, WRITE_ROOTS.settings, "settings");
3172
3411
  if (!flag(args, "json")) console.log(`出力: ${shown(out)}(封筒の date を更新。派生値を生成)`);
3173
3412
  }
3174
3413
  return 0;
@@ -3176,8 +3415,8 @@ async function normalize(args) {
3176
3415
  async function diff(args) {
3177
3416
  const [before, after] = positional(args);
3178
3417
  if (!before || !after) throw new UsageError("diff <before.json> <after.json>");
3179
- const a = readJsonLimited(resolveRead(before));
3180
- const b = readJsonLimited(resolveRead(after));
3418
+ const a = readJsonLimited(resolveRead2(before));
3419
+ const b = readJsonLimited(resolveRead2(after));
3181
3420
  console.log(diffSettings(a, b, { derived: flag(args, "derived") }));
3182
3421
  return 0;
3183
3422
  }
@@ -3185,6 +3424,7 @@ async function pull(args) {
3185
3424
  const ctx = context(args);
3186
3425
  const app = appOf(args, ctx);
3187
3426
  noOutInWorkspace(args, ctx);
3427
+ if (ctx.ws && flag(args, "force")) throw new UsageError("environments.json があるときの pull は、ダウンロードと同じ日時の名前で新しく置く(ダウンロード / pull のファイルは上書きしない)ので --force は使えない");
3188
3428
  const guestSpaceId = intOption(args, "guest", false);
3189
3429
  const preview2 = flag(args, "preview");
3190
3430
  const outArg = option(args, "out");
@@ -3198,13 +3438,15 @@ async function pull(args) {
3198
3438
  result = await pullSettings(c, engine, { app, preview: preview2, guestSpaceId, pluginId });
3199
3439
  } catch (e) {
3200
3440
  if (e instanceof RestError && /plugin\/config/.test(e.apiPath ?? "")) {
3201
- throw new RestError(`${e.message}。確かめること: kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得または更新するREST API」が有効か(cybozu.com 共通管理者がアップデートオプションの「検討中の新機能」で)、権限(運用中: レコード閲覧+レコード追加 / --preview: アプリ管理)、アプリに印刷屋プラグイン(ID ${pluginId})が入っているか`, e.status, e.code, e.apiPath);
3441
+ throw new RestError(`${e.message}。確かめること: kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得または更新するREST API」が有効か(cybozu.com 共通管理者がアップデートオプションの「検討中の新機能」で)、権限(運用中: レコード閲覧。API トークンでも可 / --preview: アプリ管理)、アプリに印刷屋プラグイン(ID ${pluginId})が入っているか`, e.status, e.code, e.apiPath);
3202
3442
  }
3203
3443
  throw e;
3204
3444
  }
3205
- const out = ctx.ws ? resolveWrite(path10.join(appDirFor(process.cwd(), ctx.env, app, result.appName), snapshotNameOf(app)), WRITE_ROOTS.kintone) : resolveWrite(outArg ?? path10.join("settings", defaultPullName(result.appName, app)), WRITE_ROOTS.settings);
3206
- if (existsSync7(out) && !flag(args, "force")) throw new InputError(`${shown(out)} は既にある。上書きするなら --force(上書きの前の内容は git の差分で確かめる)、別の名前なら --out settings/<ファイル>`);
3207
- writeJson(out, result.envelope);
3445
+ const out = ctx.ws ? resolveWrite2(path13.join(appDirFor(W.root, ctx.env, app, result.appName), snapshotNameOf(app)), WRITE_ROOTS.kintone) : resolveWrite2(outArg ?? path13.join("settings", defaultPullName(result.appName, app)), WRITE_ROOTS.settings);
3446
+ if (existsSync8(out) && ctx.ws) throw new InputError(`${shown(out)} は既にある(同じ日時の名前)。少し待ってからやり直す(ダウンロード / pull のファイルは上書きしない)`);
3447
+ if (existsSync8(out) && !flag(args, "force")) throw new InputError(`${shown(out)} は既にある。上書きするなら --force(上書きの前の内容は git の差分で確かめる)、別の名前なら --out settings/<ファイル>`);
3448
+ allow(out, ctx.ws ? "snapshot" : "settings");
3449
+ writeJson(out, result.envelope, ctx.ws ? WRITE_ROOTS.kintone : WRITE_ROOTS.settings, ctx.ws ? "snapshot" : "settings", { exclusive: !!ctx.ws || !flag(args, "force") });
3208
3450
  console.log(`pull: アプリ ${app} ${result.appName}(${preview2 ? "動作テスト環境" : "運用中"}の設定、revision ${result.revision}、保存形式 ${result.format})を${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}`);
3209
3451
  console.log(listButtons(result.envelope, { file: shown(out) }));
3210
3452
  return 0;
@@ -3217,20 +3459,37 @@ async function buttons(args) {
3217
3459
  const app = appOf(args, ctx);
3218
3460
  const list = listAppFolder(existingAppDir(ctx, app));
3219
3461
  if (!list.snapshots.length) throw new InputError(`${shown(list.dir)} にダウンロード / pull のファイルが無い(設定画面でダウンロードして inbox/ に置き take、または pull --app ${app}${envFlag(ctx)})`);
3220
- settingsArg = shown(path10.join(list.dir, list.snapshots[0]));
3462
+ settingsArg = shown(path13.join(list.dir, list.snapshots[0]));
3221
3463
  }
3222
3464
  if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
3223
3465
  const button = option(args, "button");
3224
3466
  if (args.includes("--button") && !button) throw new UsageError("--button にはボタン名を続ける");
3225
- console.log(listButtons(readJsonLimited(resolveRead(settingsArg)), { file: settingsArg, button }));
3467
+ const file = resolveRead2(settingsArg);
3468
+ const bytes = readFileSync10(file);
3469
+ if (bytes.length > MAX_INPUT_BYTES) throw new InputError(`${shown(file)} が大きすぎる(${bytes.length.toLocaleString()} バイト。上限 ${MAX_INPUT_BYTES.toLocaleString()})`);
3470
+ let settings;
3471
+ try {
3472
+ settings = JSON.parse(bytes.toString("utf8"));
3473
+ } catch (e) {
3474
+ throw new InputError(`${shown(file)} を JSON として読めない: ${e.message}`);
3475
+ }
3476
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new InputError(`${shown(file)} の最上位はオブジェクト`);
3477
+ const text = listButtons(settings, { file: settingsArg, button });
3478
+ if (flag(args, "json")) {
3479
+ const menus = (Array.isArray(settings.pluginInfos) ? settings.pluginInfos : []).map((r) => String(r.menu ?? ""));
3480
+ console.log(JSON.stringify({ file: shown(file), digest: digestOf(bytes), menus, text }, null, 2));
3481
+ } else {
3482
+ console.log(text);
3483
+ }
3226
3484
  return 0;
3227
3485
  }
3228
3486
  async function take(args) {
3229
3487
  const ctx = context(args);
3230
3488
  if (!ctx.ws) throw new UsageError("take は environments.json があるときだけ使える(inbox/ のダウンロードをアプリのフォルダーへ移す)");
3231
- const r = takeInbox(process.cwd(), ctx.ws, args.includes("--env") ? ctx.env.name : void 0);
3232
- for (const m of r.moved) console.log(`${m.file} → ${m.to}${m.same ? "(同じものが既にあったので inbox から消した)" : ""}`);
3489
+ const r = takeInbox(W.root, ctx.ws, args.includes("--env") ? ctx.env.name : void 0);
3490
+ for (const m of r.moved) console.log(`${m.file} → ${m.to}${m.leftInInbox ? "(置いたが inbox に残った)" : m.same ? "(同じものが既にあったので inbox から消した)" : ""}`);
3233
3491
  for (const s of r.skipped) console.error(`移さない: ${s.file}(${s.reason})`);
3492
+ for (const w of r.warnings) console.error(`注意: ${w}`);
3234
3493
  if (!r.moved.length && !r.skipped.length) console.log(`${INBOX}/ に設定のファイルが無い`);
3235
3494
  return r.skipped.length ? 1 : 0;
3236
3495
  }
@@ -3242,15 +3501,16 @@ async function edit(args) {
3242
3501
  const app = appOf(args, ctx);
3243
3502
  const list = listAppFolder(existingAppDir(ctx, app));
3244
3503
  if (!list.snapshots.length) throw new InputError(`${shown(list.dir)} にダウンロード / pull のファイルが無い`);
3245
- src = path10.join(list.dir, list.snapshots[0]);
3504
+ src = path13.join(list.dir, list.snapshots[0]);
3246
3505
  }
3247
- const file = resolveRead(src);
3248
- if (!SNAPSHOT_RE.test(path10.basename(file)) || !appFolderOfFile(process.cwd(), file)) throw new UsageError(`edit に渡すのはアプリのフォルダーのダウンロード / pull のファイル(rex0220-print-craft-app<番号>-<日時>.json): ${src}`);
3249
- const dest = resolveWrite(editNameOf(file), WRITE_ROOTS.kintone);
3250
- if (existsSync7(dest)) {
3506
+ const file = resolveRead2(src);
3507
+ if (!SNAPSHOT_RE.test(path13.basename(file)) || !appFolderOfFile(W.root, file)) throw new UsageError(`edit に渡すのはアプリのフォルダーのダウンロード / pull のファイル(rex0220-print-craft-app<番号>-<日時>.json): ${src}`);
3508
+ const dest = resolveWrite2(editNameOf(file), WRITE_ROOTS.kintone);
3509
+ if (existsSync8(dest)) {
3251
3510
  console.log(`既にある(続けて直す): ${shown(dest)}`);
3252
3511
  } else {
3253
- writeText(dest, readTextLimited(file));
3512
+ allow(dest, "settings");
3513
+ writeText(dest, readTextLimited(file), WRITE_ROOTS.kintone, "settings");
3254
3514
  console.log(`${shown(file)} → ${shown(dest)}(ここを直す。ダウンロードのファイルは書き換えない)`);
3255
3515
  }
3256
3516
  console.log(`次: 直したら npx @rex0220/print-craft-authoring-tools normalize ${shown(dest)} → diff ${shown(file)} ${shown(dest)}`);
@@ -3278,40 +3538,41 @@ async function files(args) {
3278
3538
  async function preview(args) {
3279
3539
  const [settingsArg] = positional(args);
3280
3540
  if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
3281
- const settingsFile = resolveRead(settingsArg);
3541
+ const settingsFile = resolveRead2(settingsArg);
3282
3542
  const folder = folderDefaults(settingsFile);
3283
3543
  const fieldsFile = fieldsFileOf(args, folder);
3284
3544
  const recordArg = option(args, "record");
3285
3545
  if (!recordArg) throw new UsageError("--record <record.json> が要る(record コマンドの出力。アプリのフォルダーの中のファイルならレコード番号だけでよい: --record 3)");
3286
- const recordFile = folder && /^\d+$/.test(recordArg) ? existingFile(path10.join(folder.dir, "records", `${recordArg}.json`), `record --app ${folder.appId} --id ${recordArg}`) : resolveRead(recordArg);
3287
- const outDir = resolveWriteDir(option(args, "out-dir") ?? (folder ? path10.join(folder.dir, "out") : "out"), WRITE_ROOTS.out);
3546
+ const recordFile = folder && /^\d+$/.test(recordArg) ? existingFile(path13.join(folder.dir, "records", `${recordArg}.json`), `record --app ${folder.appId} --id ${recordArg}`) : resolveRead2(recordArg);
3547
+ const outDir = resolveWriteDir2(option(args, "out-dir") ?? (folder ? path13.join(folder.dir, "out") : "out"), WRITE_ROOTS.out);
3288
3548
  const fieldsData = await readFieldsFile(fieldsFile);
3289
3549
  const engine = await engineFor();
3290
3550
  const result = await runPreview({
3291
3551
  settingsText: readTextLimited(settingsFile),
3292
- settingsFile: relativeSettingsPath(settingsArg),
3552
+ settingsFile: relativeSettingsPath(settingsArg, W.root),
3293
3553
  fields: fieldsData,
3294
3554
  recordFile: readJsonLimited(recordFile),
3295
3555
  engine,
3296
- policy: loadPolicy(),
3556
+ policy: loadPolicy({ cwd: W.root }),
3297
3557
  button: option(args, "button"),
3298
3558
  baseUrl: trustedBaseUrl(folder)
3299
3559
  });
3300
3560
  const written = [];
3301
3561
  if (!result.findings.hasErrors) {
3302
3562
  for (const r of result.results) {
3303
- const file = resolveWrite(path10.join(outDir, r.file), WRITE_ROOTS.out);
3304
- writeText(file, r.html);
3563
+ const file = resolveWrite2(path13.join(outDir, r.file), WRITE_ROOTS.out);
3564
+ allow(file, "preview");
3565
+ writeText(file, r.html, WRITE_ROOTS.out, "preview");
3305
3566
  written.push(shown(file));
3306
3567
  }
3307
3568
  }
3308
3569
  if (flag(args, "json")) {
3309
- console.log(JSON.stringify({ summary: result.summary, findings: result.findings.items, results: result.results.map((r) => ({ menu: r.menu, file: shown(path10.join(outDir, r.file)), fileName: r.fileName, pages: r.pages, errors: r.errors })), skipped: result.skipped, written }, null, 2));
3570
+ console.log(JSON.stringify({ summary: result.summary, findings: result.findings.items, results: result.results.map((r) => ({ menu: r.menu, file: shown(path13.join(outDir, r.file)), fileName: r.fileName, pages: r.pages, errors: r.errors })), skipped: result.skipped, written }, null, 2));
3310
3571
  } else {
3311
3572
  console.log(`preview: ${settingsArg}(${result.summary})`);
3312
3573
  console.log(result.findings.format());
3313
3574
  for (const r of result.results) {
3314
- console.log(`${r.menu}: ${r.pages} ページ、ファイル名 ${r.fileName}${r.errors.length ? `、式のエラー ${r.errors.length}` : ""} → ${shown(path10.join(outDir, r.file))}`);
3575
+ console.log(`${r.menu}: ${r.pages} ページ、ファイル名 ${r.fileName}${r.errors.length ? `、式のエラー ${r.errors.length}` : ""} → ${shown(path13.join(outDir, r.file))}`);
3315
3576
  for (const e of r.errors) console.log(` ${e}`);
3316
3577
  }
3317
3578
  for (const s of result.skipped) console.log(`対象外: ${s}`);
@@ -3322,6 +3583,7 @@ async function preview(args) {
3322
3583
  }
3323
3584
  async function main(argv) {
3324
3585
  const [command, ...args] = argv;
3586
+ W = createContext({ cwd: process.cwd(), env: process.env });
3325
3587
  rejectRemovedOptions(args);
3326
3588
  if (args.includes("--env")) context(args);
3327
3589
  switch (command) {
@@ -3368,7 +3630,7 @@ main(process.argv.slice(2)).then(
3368
3630
  console.error(`${e.message}
3369
3631
  ${USAGE}`);
3370
3632
  process.exitCode = 2;
3371
- } else if (e instanceof AuthError || e instanceof RestError || e instanceof NotAllowedError || e instanceof PluginZipError || e instanceof PolicyError || e instanceof InputError || e instanceof KintoneUrlError || e instanceof ButtonNotFoundError || e instanceof WorkspaceError) {
3633
+ } else if (e instanceof AuthError || e instanceof RestError || e instanceof NotAllowedError || e instanceof PluginZipError || e instanceof PolicyError || e instanceof InputError || e instanceof KintoneUrlError || e instanceof ButtonNotFoundError || e instanceof WorkspaceError || e instanceof PermissionError) {
3372
3634
  console.error(e.message);
3373
3635
  process.exitCode = 1;
3374
3636
  } else {