@rex0220/print-craft-authoring-tools 1.1.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 +9 -4
  2. package/SECURITY.md +6 -2
  3. package/dist/cli.mjs +450 -150
  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/cli.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- /*! @rex0220/print-craft-authoring-tools 1.1.0 (c) rex0220. MIT License. 計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む(このファイルには含まれない)。commit eb915ae. */
2
+ /*! @rex0220/print-craft-authoring-tools 1.3.0 (c) rex0220. MIT License. 計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む(このファイルには含まれない)。commit 5abbe71. */
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";
@@ -51,6 +51,11 @@ var NAMES = {
51
51
  password: ["KINTONE_PASSWORD", "KSQL_PASSWORD"],
52
52
  pluginZip: ["PCRAFT_PLUGIN_ZIP"]
53
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
+ }
54
59
  function parseDotEnv(text) {
55
60
  const out = {};
56
61
  for (const raw of text.split(/\r?\n/)) {
@@ -58,33 +63,36 @@ function parseDotEnv(text) {
58
63
  if (!line || line.startsWith("#")) continue;
59
64
  const m = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
60
65
  if (!m) continue;
61
- let value = m[2].trim();
62
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
63
- out[m[1]] = value;
66
+ out[m[1]] = unquote(m[2]);
64
67
  }
65
68
  return out;
66
69
  }
67
70
  var AuthError = class extends Error {
68
71
  };
69
- function picker(opt) {
70
- const osEnv = opt.env ?? process.env;
72
+ function pickerWithSource(opt, unquoteProcess = false) {
73
+ const osEnv = opt.env;
71
74
  const file = envFileOf(opt);
72
75
  const fromFile = existsSync(file) ? parseDotEnv(readFileSync(file, "utf8")) : {};
73
76
  return (names) => {
74
- for (const src of [osEnv, fromFile]) {
77
+ for (const [src, source] of [[osEnv, "process"], [fromFile, "file"]]) {
75
78
  for (const n of names) {
76
- const v = src[n];
77
- 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 } : {} };
78
82
  }
79
83
  }
80
84
  return void 0;
81
85
  };
82
86
  }
87
+ function picker(opt, unquoteProcess = false) {
88
+ const pick = pickerWithSource(opt, unquoteProcess);
89
+ return (names) => pick(names)?.value;
90
+ }
83
91
  function envFileOf(opt) {
84
- return opt.envFile ?? path.join(opt.cwd ?? process.cwd(), ".env");
92
+ return opt.envFile ?? path.join(opt.cwd, ".env");
85
93
  }
86
- function baseUrlFromEnv(opt = {}) {
87
- const raw = picker(opt)(NAMES.baseUrl);
94
+ function baseUrlFromEnv(opt) {
95
+ const raw = picker(opt, true)(NAMES.baseUrl);
88
96
  if (!raw) return void 0;
89
97
  try {
90
98
  return normalizeKintoneBaseUrl(raw);
@@ -92,7 +100,7 @@ function baseUrlFromEnv(opt = {}) {
92
100
  throw new AuthError(`KINTONE_BASE_URL が不正: ${e instanceof KintoneUrlError ? e.message : String(e)}(https://<サブドメイン>.cybozu.com の形で書く)`);
93
101
  }
94
102
  }
95
- function loadAuth(opt = {}) {
103
+ function loadAuth(opt) {
96
104
  const pick = picker(opt);
97
105
  const file = envFileOf(opt);
98
106
  const baseUrl = baseUrlFromEnv(opt);
@@ -103,7 +111,7 @@ function loadAuth(opt = {}) {
103
111
  if (!token && !(username && password)) throw new AuthError("KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(OS の環境変数か .env)");
104
112
  return { baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
105
113
  }
106
- function loadAuthForEnv(env, cwd = process.cwd()) {
114
+ function loadAuthForEnv(env, cwd) {
107
115
  const file = path.join(cwd, env.envFile);
108
116
  if (!existsSync(file)) throw new AuthError(`環境「${env.name}」の認証のファイルが無い: ${env.envFile}(${env.envFile === ".env" ? ".env.example を写して作る" : "env/ に作る。.env と同じ書き方"})`);
109
117
  const vars = parseDotEnv(readFileSync(file, "utf8"));
@@ -127,10 +135,12 @@ function loadAuthForEnv(env, cwd = process.cwd()) {
127
135
  if (!token && !(username && password)) throw new AuthError(`${env.envFile} に KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(環境「${env.name}」。OS の環境変数は読まない)`);
128
136
  return { baseUrl: env.baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
129
137
  }
130
- function pluginZipPath(opt = {}) {
131
- const v = picker(opt)(NAMES.pluginZip);
132
- if (!v) return void 0;
133
- return path.resolve(path.dirname(envFileOf(opt)), v);
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);
134
144
  }
135
145
  function describeAuth(auth) {
136
146
  return auth.token ? "API トークン" : "ログインユーザー";
@@ -157,7 +167,7 @@ function norm(p) {
157
167
  const r = path2.resolve(p);
158
168
  return process.platform === "win32" ? r.toLowerCase() : r;
159
169
  }
160
- function realResolve(target, cwd = process.cwd()) {
170
+ function realResolve(target, cwd) {
161
171
  const abs = path2.resolve(cwd, target);
162
172
  let dir = abs;
163
173
  const rest = [];
@@ -190,7 +200,7 @@ function isInside(child, parent) {
190
200
  const p = norm(parent);
191
201
  return c === p || c.startsWith(p.endsWith(path2.sep) ? p : p + path2.sep);
192
202
  }
193
- function resolveRead(target, cwd = process.cwd()) {
203
+ function resolveRead(target, cwd) {
194
204
  if (!target || typeof target !== "string") throw new PathError("ファイルのパスが要る");
195
205
  const real = realResolve(target, cwd);
196
206
  const cwdReal = realResolve(".", cwd);
@@ -199,7 +209,7 @@ function resolveRead(target, cwd = process.cwd()) {
199
209
  if (/^\.env(\.|$)/i.test(path2.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(rel)) throw new PathError(`このファイルは tools では読まない: ${target}`);
200
210
  return real;
201
211
  }
202
- function resolveWrite(target, roots, cwd = process.cwd()) {
212
+ function resolveWrite(target, roots, cwd) {
203
213
  if (!target || typeof target !== "string") throw new PathError("書き込み先のパスが要る");
204
214
  const real = realResolve(target, cwd);
205
215
  const cwdReal = realResolve(".", cwd);
@@ -213,7 +223,7 @@ function resolveWrite(target, roots, cwd = process.cwd()) {
213
223
  if (WINDOWS_RESERVED.test(base) || /[. ]$/.test(base)) throw new PathError(`使えないファイル名: ${base}`);
214
224
  return real;
215
225
  }
216
- function resolveWriteDir(target, roots, cwd = process.cwd()) {
226
+ function resolveWriteDir(target, roots, cwd) {
217
227
  if (!target || typeof target !== "string") throw new PathError("フォルダーのパスが要る");
218
228
  const real = realResolve(target, cwd);
219
229
  const cwdReal = realResolve(".", cwd);
@@ -239,6 +249,7 @@ var ENV_FILE = /^(\.env|env\/[A-Za-z0-9._-]{1,64}\.env)$/;
239
249
  var MAX_BYTES = 256 * 1024;
240
250
  var WorkspaceError = class extends Error {
241
251
  };
252
+ var ENV_ROLES = ["development", "production"];
242
253
  var isEnvName = (s) => ENV_NAME.test(s);
243
254
  function parseWorkspace(text, where = WORKSPACE_FILE) {
244
255
  let raw;
@@ -258,7 +269,9 @@ function parseWorkspace(text, where = WORKSPACE_FILE) {
258
269
  for (const [name, v] of Object.entries(envsRaw)) {
259
270
  if (!isEnvName(name)) throw new WorkspaceError(`${where}: 環境の名前は英数字と _ - の 32 文字まで: ${name}`);
260
271
  const e = obj(v, `environments.${name}`);
261
- 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;
262
275
  let baseUrl;
263
276
  try {
264
277
  baseUrl = normalizeKintoneBaseUrl(String(e.baseUrl ?? ""));
@@ -267,7 +280,7 @@ function parseWorkspace(text, where = WORKSPACE_FILE) {
267
280
  }
268
281
  const envFile = e.envFile === void 0 ? ".env" : String(e.envFile).replace(/\\/g, "/");
269
282
  if (!ENV_FILE.test(envFile)) throw new WorkspaceError(`${where}: environments.${name}.envFile は .env か env/<名前>.env: ${envFile}`);
270
- environments[name] = { name, baseUrl, host: new URL(baseUrl).hostname, envFile };
283
+ environments[name] = { name, role, baseUrl, host: new URL(baseUrl).hostname, envFile };
271
284
  }
272
285
  if (Object.keys(environments).length === 0) throw new WorkspaceError(`${where}: environments に環境が 1 つも無い`);
273
286
  const defaultEnv = root.default === void 0 ? Object.keys(environments)[0] : String(root.default);
@@ -294,7 +307,7 @@ function parseWorkspace(text, where = WORKSPACE_FILE) {
294
307
  }
295
308
  return { defaultEnv, environments, apps };
296
309
  }
297
- function loadWorkspace(cwd = process.cwd()) {
310
+ function loadWorkspace(cwd) {
298
311
  const file = path3.join(cwd, WORKSPACE_FILE);
299
312
  if (!existsSync2(file)) return null;
300
313
  const size = statSync(file).size;
@@ -377,10 +390,16 @@ function listAppFolder(dir) {
377
390
  function envsOfHost(ws, host) {
378
391
  return Object.values(ws.environments).filter((e) => e.host === host);
379
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
+ }
380
399
 
381
400
  // src/commands/take.ts
382
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync6, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
383
- import path8 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";
384
403
 
385
404
  // src/commands/normalize.ts
386
405
  import { readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
@@ -425,7 +444,6 @@ import { readFileSync as readFileSync3 } from "node:fs";
425
444
  import path5 from "node:path";
426
445
 
427
446
  // src/paths.ts
428
- import { existsSync as existsSync3, realpathSync as realpathSync2 } from "node:fs";
429
447
  import path4 from "node:path";
430
448
  import { fileURLToPath } from "node:url";
431
449
  var here = path4.dirname(fileURLToPath(import.meta.url));
@@ -433,24 +451,6 @@ var AUTHORING_ROOT = path4.resolve(here, "..");
433
451
  var REPO_ROOT = path4.resolve(AUTHORING_ROOT, "..");
434
452
  var VENDOR_DIR = path4.join(AUTHORING_ROOT, "vendor");
435
453
  var MOMENT_FILE = path4.join(VENDOR_DIR, "moment-with-locales.min.js");
436
- function linkedRoot(name, envVar, fallback) {
437
- const env = process.env[envVar];
438
- if (env) return env;
439
- const linked = path4.join(AUTHORING_ROOT, "node_modules", name);
440
- if (existsSync3(path4.join(linked, "package.json"))) {
441
- try {
442
- return realpathSync2(linked);
443
- } catch {
444
- return linked;
445
- }
446
- }
447
- return fallback;
448
- }
449
- var PRINT_CRAFT_ROOT = linkedRoot("print-craft", "PCRAFT_PRINT_CRAFT_ROOT", path4.resolve(REPO_ROOT, "..", "print-craft"));
450
- function devPluginDir() {
451
- const dir = path4.join(PRINT_CRAFT_ROOT, "prod");
452
- return existsSync3(path4.join(dir, "desktop_js", "KintoneFormulaPCraft.min.js")) ? dir : null;
453
- }
454
454
 
455
455
  // src/meta.ts
456
456
  var PRINT_CRAFT_PLUGIN_ID = "lcapkanpjdabgphknkabojmcfhonhkhp";
@@ -523,7 +523,7 @@ function devMeta() {
523
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
524
  }
525
525
  function toolsMeta() {
526
- if (true) return JSON.parse('{"toolsVersion":"1.1.0","pluginId":"lcapkanpjdabgphknkabojmcfhonhkhp","minPluginVersion":6,"supportedApiVersions":[1,2],"commit":"eb915ae","builtAt":"2026-10-08T00:26:18.929Z","mode":"build"}');
526
+ 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"}');
527
527
  return devMeta();
528
528
  }
529
529
 
@@ -1042,9 +1042,9 @@ async function checkHtml(html, opt = {}) {
1042
1042
  const doc = new w.DOMParser().parseFromString(`<!doctype html><html><body>${html}</body></html>`, "text/html");
1043
1043
  const root = doc.body ?? doc.documentElement;
1044
1044
  if (!root) return { errors, warnings, externals };
1045
- const walk = (el, path11) => {
1045
+ const walk = (el, path14) => {
1046
1046
  const tag = el.tagName.toLowerCase();
1047
- const here2 = `${path11}>${tag}`;
1047
+ const here2 = `${path14}>${tag}`;
1048
1048
  const denied = tagError(tag, here2);
1049
1049
  if (denied) {
1050
1050
  errors.push(denied);
@@ -1107,7 +1107,7 @@ function expressionsOf(html) {
1107
1107
  }
1108
1108
 
1109
1109
  // src/normalize/policy.ts
1110
- import { existsSync as existsSync4, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
1110
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
1111
1111
  import path6 from "node:path";
1112
1112
  var PolicyError = class extends Error {
1113
1113
  };
@@ -1208,9 +1208,9 @@ function isExternalRefsAllowed(policy, settingsFile) {
1208
1208
  return false;
1209
1209
  }
1210
1210
  }
1211
- function loadPolicy(opt = {}) {
1212
- const file = path6.join(opt.cwd ?? process.cwd(), POLICY_FILE);
1213
- if (!existsSync4(file)) return { allowExternal: [] };
1211
+ function loadPolicy(opt) {
1212
+ const file = path6.join(opt.cwd, POLICY_FILE);
1213
+ if (!existsSync3(file)) return { allowExternal: [] };
1214
1214
  if (statSync2(file).size > MAX_POLICY_BYTES) throw new PolicyError(`${file} が大きすぎる(${MAX_POLICY_BYTES} バイトまで)`);
1215
1215
  return parsePolicy(readFileSync4(file, "utf8"), file);
1216
1216
  }
@@ -1698,13 +1698,157 @@ async function readFieldsFile(file) {
1698
1698
  if (typeof fields2.appId !== "number" || !Number.isInteger(fields2.appId) || fields2.appId <= 0) throw new InputError(`${file} の appId が正の整数でない`);
1699
1699
  return fields2;
1700
1700
  }
1701
- function relativeSettingsPath(file) {
1702
- const rel = path7.relative(process.cwd(), path7.resolve(file));
1701
+ function relativeSettingsPath(file, cwd) {
1702
+ const rel = path7.relative(cwd, path7.resolve(cwd, file));
1703
1703
  return rel.startsWith("..") ? file : rel.replace(/\\/g, "/");
1704
1704
  }
1705
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
+
1706
1841
  // src/commands/take.ts
1707
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
+ }
1708
1852
  function envFor(ws, appId, envName) {
1709
1853
  if (envName) return ws.environments[envName] ?? `環境「${envName}」は environments.json に無い`;
1710
1854
  const hits = Object.values(ws.environments).filter((e) => ws.apps.some((a) => a.ids[e.name] === appId));
@@ -1714,9 +1858,10 @@ function envFor(ws, appId, envName) {
1714
1858
  if (all.length === 1) return all[0];
1715
1859
  return `アプリ ${appId} は environments.json の apps に無く、環境が 2 つ以上ある。apps に足すか --env で選ぶ`;
1716
1860
  }
1717
- function takeInbox(cwd, ws, envName) {
1718
- const result = { moved: [], skipped: [] };
1719
- const inbox = path8.join(cwd, INBOX);
1861
+ function takeInbox(cwdIn, ws, envName) {
1862
+ const cwd = realResolve(".", cwdIn);
1863
+ const result = { moved: [], skipped: [], warnings: [] };
1864
+ const inbox = path10.join(cwd, INBOX);
1720
1865
  if (!existsSync5(inbox)) return result;
1721
1866
  for (const name of readdirSync2(inbox).filter((n) => n.toLowerCase().endsWith(".json")).sort()) {
1722
1867
  const rel = `${INBOX}/${name}`;
@@ -1737,6 +1882,10 @@ function takeInbox(cwd, ws, envName) {
1737
1882
  result.skipped.push({ file: rel, reason: "封筒の appId が無い" });
1738
1883
  continue;
1739
1884
  }
1885
+ if (!SNAPSHOT_RE.test(name)) {
1886
+ result.skipped.push({ file: rel, reason: "ダウンロードの名前(rex0220-print-craft-app<番号>-<日時>.json)ではない。設定画面のダウンロードの名前のまま inbox/ に置く" });
1887
+ continue;
1888
+ }
1740
1889
  const m = name.match(/-app(\d+)-/);
1741
1890
  if (m && Number(m[1]) !== appId) {
1742
1891
  result.skipped.push({ file: rel, reason: `ファイル名のアプリ ${m[1]} と封筒の appId ${appId} が違う` });
@@ -1748,27 +1897,55 @@ function takeInbox(cwd, ws, envName) {
1748
1897
  continue;
1749
1898
  }
1750
1899
  const dir = appDirFor(cwd, env, appId, typeof data.appName === "string" ? data.appName : "");
1751
- const dest = resolveWrite(path8.join(dir, name), WRITE_ROOTS.kintone, cwd);
1752
- const text = readFileSync6(src, "utf8");
1900
+ const dest = resolveWrite(path10.join(dir, name), WRITE_ROOTS.kintone, cwd);
1901
+ const text = readFileSync7(src, "utf8");
1753
1902
  if (existsSync5(dest)) {
1754
- if (readFileSync6(dest, "utf8") !== text) {
1755
- result.skipped.push({ file: rel, reason: `行き先に同じ名前の別の中身がある: ${path8.relative(cwd, dest)}` });
1903
+ if (readFileSync7(dest, "utf8") !== text) {
1904
+ result.skipped.push({ file: rel, reason: `行き先に同じ名前の別の中身がある: ${path10.relative(cwd, dest)}` });
1756
1905
  continue;
1757
1906
  }
1758
- unlinkSync(src);
1759
- result.moved.push({ file: rel, to: path8.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 } });
1760
1909
  continue;
1761
1910
  }
1762
- mkdirSync(path8.dirname(dest), { recursive: true });
1763
- writeFileSync(dest, text, "utf8");
1764
- unlinkSync(src);
1765
- result.moved.push({ file: rel, to: path8.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 } });
1766
1932
  }
1767
1933
  return result;
1768
1934
  }
1769
1935
 
1770
1936
  // src/kintone-rest.ts
1771
- 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
+ };
1772
1949
  var RestError = class extends Error {
1773
1950
  status;
1774
1951
  code;
@@ -1783,6 +1960,37 @@ var RestError = class extends Error {
1783
1960
  };
1784
1961
  var NotAllowedError = class extends Error {
1785
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
+ }
1786
1994
  function apiPathOf(api, guestSpaceId) {
1787
1995
  if (!ALLOWED_APIS.includes(api)) throw new NotAllowedError(`この API は authoring tools では呼ばない: ${api}(許可: ${ALLOWED_APIS.join(", ")})`);
1788
1996
  if (guestSpaceId !== void 0 && (!Number.isInteger(guestSpaceId) || guestSpaceId <= 0)) throw new NotAllowedError(`ゲストスペースの ID が不正: ${String(guestSpaceId)}`);
@@ -1810,7 +2018,7 @@ function createRestClient(auth, fetchImpl = fetch) {
1810
2018
  for (const [k, v] of Object.entries(params)) if (v !== void 0) url.searchParams.set(k, String(v));
1811
2019
  if (url.origin !== baseUrl || url.username || url.password) throw new NotAllowedError(`送信先が kintone の接続先と違うので送らない: ${url.origin}`);
1812
2020
  const res = await fetchImpl(url.href, { method: "GET", headers });
1813
- const text = await res.text();
2021
+ const text = await readCapped(res, RECEIVE_LIMITS[api], apiPath);
1814
2022
  if (!res.ok) {
1815
2023
  let code;
1816
2024
  try {
@@ -1821,7 +2029,11 @@ function createRestClient(auth, fetchImpl = fetch) {
1821
2029
  const hint = res.status === 401 ? "(認証情報かアクセス権を確かめる。API トークンならそのアプリで有効か)" : res.status === 403 ? "(アクセス権が無い)" : res.status === 404 ? "(アプリかレコードが無い)" : "";
1822
2030
  throw new RestError(`kintone ${apiPath} が HTTP ${res.status}${code ? ` ${code}` : ""}${hint}`, res.status, code, apiPath);
1823
2031
  }
1824
- 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
+ }
1825
2037
  }
1826
2038
  };
1827
2039
  }
@@ -2061,8 +2273,12 @@ function columnShape(type, values) {
2061
2273
  return `最大 ${longest}${multiLine ? `(複数行の値 ${multiLine} 行)` : ""}${filled.length < shapes.length ? `、空 ${shapes.length - filled.length} 行` : ""}`;
2062
2274
  }
2063
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) {
2064
2280
  const lines = [];
2065
- for (const [code, fv] of Object.entries(file.record)) {
2281
+ for (const [code, fv] of Object.entries(record2)) {
2066
2282
  if (code === "$id" || code === "$revision" || !fv) continue;
2067
2283
  if (fv.type === "SUBTABLE" && Array.isArray(fv.value)) {
2068
2284
  const rows = fv.value;
@@ -2080,8 +2296,7 @@ function describeRecord(file) {
2080
2296
  }
2081
2297
  lines.push(`${code} ${fv.type} ${shapeOf(fv)}`);
2082
2298
  }
2083
- const header = `アプリ ${file.appId} レコード ${file.id}(取得 ${file.fetchedAt}${file.keptFields ? `、--fields-from で ${file.keptFields.length} 項目に絞った` : ""})。値は出さない(形だけ)`;
2084
- return [header, ...lines].join("\n");
2299
+ return lines;
2085
2300
  }
2086
2301
  function summarizeRecord(file) {
2087
2302
  const codes = Object.keys(file.record);
@@ -2090,6 +2305,30 @@ function summarizeRecord(file) {
2090
2305
  return `アプリ ${file.appId} レコード ${file.id}: 項目 ${codes.length}${tables.length ? `(テーブル ${rows.join(", ")})` : ""}${file.keptFields ? `。--fields-from で ${file.keptFields.length} 項目に絞った` : ""}。値は表示しない`;
2091
2306
  }
2092
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
+
2093
2332
  // src/normalize/line-diff.ts
2094
2333
  var MAX_CELLS = 4e6;
2095
2334
  function lineDiff(a, b) {
@@ -2369,13 +2608,13 @@ function defaultPullName(appName, appId) {
2369
2608
  }
2370
2609
 
2371
2610
  // src/engine.ts
2372
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
2373
- import path9 from "node:path";
2611
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
2612
+ import path12 from "node:path";
2374
2613
  import vm from "node:vm";
2375
2614
 
2376
2615
  // src/plugin-zip.ts
2377
2616
  import { createHash as createHash3 } from "node:crypto";
2378
- import { readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
2617
+ import { readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
2379
2618
  import { inflateRawSync } from "node:zlib";
2380
2619
  var SIG_EOCD = 101010256;
2381
2620
  var SIG_CENTRAL = 33639248;
@@ -2487,7 +2726,7 @@ function readPluginZip(file) {
2487
2726
  try {
2488
2727
  const size = statSync4(file).size;
2489
2728
  if (size > ZIP_LIMITS.maxOuterBytes) throw new PluginZipError(`大きすぎる(${size} バイト)`);
2490
- outer = unzip(readFileSync7(file));
2729
+ outer = unzip(readFileSync8(file));
2491
2730
  } catch (e) {
2492
2731
  throw new PluginZipError(`印刷屋の zip を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
2493
2732
  }
@@ -2540,23 +2779,23 @@ var DEFAULT_CONTEXT_BASE_URL = "https://example.cybozu.com";
2540
2779
  var DEFAULT_USER = { id: "1", code: "authoring", name: "authoring", email: "", language: "ja" };
2541
2780
  var loaded = null;
2542
2781
  function resolvePluginSources(opt = {}, mode = opt.mode ?? toolsMeta().mode) {
2543
- const zip = opt.pluginZip ?? process.env.PCRAFT_PLUGIN_ZIP;
2782
+ const zip = opt.pluginZip;
2544
2783
  if (zip) {
2545
- 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)`);
2546
2785
  return { ...readPluginZip(zip), kind: "zip" };
2547
2786
  }
2548
- const devAllowed = mode === "dev" && process.env.PCRAFT_ALLOW_DEV_PLUGIN === "1";
2787
+ const dev = opt.devPluginDir;
2788
+ const devAllowed = mode === "dev" && !!dev;
2549
2789
  if (!devAllowed) {
2550
2790
  throw new PluginZipError(`印刷屋の zip の場所が分からない。.env に PCRAFT_PLUGIN_ZIP=<印刷屋プラグインの zip のパス> を書く${mode === "dev" ? "(開発中に隣の print-craft の prod/ を読むなら環境変数 PCRAFT_ALLOW_DEV_PLUGIN=1)" : ""}`);
2551
2791
  }
2552
- const dev = devPluginDir();
2553
- if (!dev) throw new PluginZipError("印刷屋の zip の場所が分からない(PCRAFT_ALLOW_DEV_PLUGIN=1 だが隣の print-craft の prod/ が無い)。.env に PCRAFT_PLUGIN_ZIP を書く");
2554
- const read = (rel) => readFileSync8(path9.join(dev, rel), "utf8");
2555
- 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);
2556
2795
  const engine = read(ENGINE_ENTRY);
2557
2796
  const bignumber = read(BIGNUMBER_ENTRY);
2558
2797
  const momentTimezone = read(MOMENT_TZ_ENTRY);
2559
- const api = existsSync6(apiFile) ? readFileSync8(apiFile, "utf8") : void 0;
2798
+ const api = existsSync7(apiFile) ? readFileSync9(apiFile, "utf8") : void 0;
2560
2799
  const manifest = JSON.parse(read(MANIFEST_ENTRY));
2561
2800
  return {
2562
2801
  kind: "dev",
@@ -2620,7 +2859,7 @@ async function loadEngine(opt = {}) {
2620
2859
  window.kintone = g.kintone;
2621
2860
  window.cybozu = g.cybozu;
2622
2861
  window.rex0220_users_info3 = g.rex0220_users_info3;
2623
- 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)`);
2624
2863
  const runScript = (code, filename) => {
2625
2864
  vm.runInThisContext(code, { filename });
2626
2865
  for (const k of ["moment", "rex0220_BigNumber", "rex0220_Decimal", "rex0220p", "rex0220PrintCraftAuthoring"]) {
@@ -2628,7 +2867,7 @@ async function loadEngine(opt = {}) {
2628
2867
  if (window[k] === void 0 && g[k] !== void 0) window[k] = g[k];
2629
2868
  }
2630
2869
  };
2631
- runScript(readFileSync8(MOMENT_FILE, "utf8"), MOMENT_FILE);
2870
+ runScript(readFileSync9(MOMENT_FILE, "utf8"), MOMENT_FILE);
2632
2871
  runScript(src.momentTimezone, `${src.from}!${MOMENT_TZ_ENTRY}`);
2633
2872
  runScript(src.bignumber, `${src.from}!${BIGNUMBER_ENTRY}`);
2634
2873
  runScript(src.engine, `${src.from}!${ENGINE_ENTRY}`);
@@ -2853,6 +3092,18 @@ async function runPreview(input) {
2853
3092
  return { findings, results, skipped, summary };
2854
3093
  }
2855
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
+
2856
3107
  // src/cli.ts
2857
3108
  var USAGE = `使い方: npx @rex0220/print-craft-authoring-tools <command> [options]
2858
3109
  (短い npx pcraft-authoring は使わない。npm ci の前だと npm の公開レジストリの同じ名前のパッケージを取りに行く)
@@ -2866,14 +3117,16 @@ var USAGE = `使い方: npx @rex0220/print-craft-authoring-tools <command> [opti
2866
3117
  取得済みの fields/<N>.json を 1 項目 1 行で(レイアウトの順。型・ラベル・書式・単位・選択肢・ルックアップ)。通信しない。
2867
3118
  pull --app N [--preview] [--guest <spaceId>] [--out settings/<file>] [--force] [--plugin-id <ID>]
2868
3119
  アプリに入っている印刷屋の今の設定を取って、設定画面の「設定をダウンロード」と同じ封筒形式で settings/APP<N>-<アプリ名>.json に保存(GET だけ)。
2869
- kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得する」を有効にした環境だけ。権限は運用中の設定がレコード閲覧+追加、
2870
- --preview(動作テスト環境 = 保存して未反映の設定)がアプリ管理。プラグイン ID は印刷屋の zip から。既にあるファイルは --force で上書き。
3120
+ kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得する」を有効にした環境だけ。権限は運用中の設定がレコード閲覧(API トークンでも可)、
3121
+ --preview(動作テスト環境 = 保存して未反映の設定)がアプリ管理。プラグイン ID は印刷屋の zip から。既にあるファイルは --force で上書き
3122
+ (--force は settings/ に置くときだけ。environments.json があるときはダウンロードと同じ日時の名前で新しく置き、上書きしない)。
2871
3123
  record --app N --id R [--fields-from <settings.json>] [--guest <spaceId>] [--out records/<file>]
2872
3124
  レコードを records/<N>-<R>.json に保存。--fields-from で設定が使う項目だけ残す。
2873
3125
  record --app N --id R --summary
2874
3126
  取得済みの records/<N>-<R>.json を 1 項目 1 行で。値は出さず形だけ(文字数・行数・数値の桁・テーブルの行数・添付の件数と種類)。通信しない。
2875
- buttons <settings.json> [--button <名前>]
3127
+ buttons <settings.json> [--button <名前>] [--json]
2876
3128
  設定のボタン一覧(画面・保存先・用紙・表示条件・ファイル名・帳票の行・更新項目)。--button でそのボタンの HTML / CSS / 計算式(data: の URL は長さだけ)。
3129
+ --json はファイルの digest(sha256)とボタンの名前の一覧も(直すときの照合に使う)。
2877
3130
  normalize <settings.json> --fields <fields.json> [--out settings/<file>] [--dry-run] [--check] [--json]
2878
3131
  設定画面と同じ手順で派生値を作り直し、検査して、エラーが無ければ書き戻す(既定は同じファイルに上書き。--dry-run は書かない)。
2879
3132
  --check は入力の派生値と生成した値の差を出す。外部 URL の承認は policy/authoring-policy.json(場所は固定。利用者が書く)。
@@ -2906,6 +3159,12 @@ kintone には GET しか送らない。
2906
3159
  var FLAGS = /* @__PURE__ */ new Set(["preview", "check", "json", "dry-run", "derived", "summary", "force"]);
2907
3160
  var UsageError = class extends Error {
2908
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);
2909
3168
  function option(args, name) {
2910
3169
  const i = args.indexOf(`--${name}`);
2911
3170
  return i >= 0 ? args[i + 1] : void 0;
@@ -2938,20 +3197,35 @@ function rejectRemovedOptions(args) {
2938
3197
  if (args.includes(`--${name}`)) throw new UsageError(`--${name} は使えない。.env と policy/authoring-policy.json と PCRAFT_PLUGIN_ZIP は作業フォルダーのものに固定(AI が書けるファイルを読ませないため)`);
2939
3198
  }
2940
3199
  }
2941
- function writeJson(file, data) {
2942
- mkdirSync2(path10.dirname(file), { recursive: true });
2943
- 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;
2944
3219
  }
2945
- function writeText(file, text) {
2946
- mkdirSync2(path10.dirname(file), { recursive: true });
2947
- 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);
2948
3222
  }
2949
3223
  function shown(file) {
2950
- const rel = path10.relative(process.cwd(), file);
3224
+ const rel = path13.relative(W.root, file);
2951
3225
  return rel && !rel.startsWith("..") ? rel : file;
2952
3226
  }
2953
3227
  async function engineFor() {
2954
- const engine = await loadEngine({ pluginZip: pluginZipPath() });
3228
+ const engine = await loadEngine({ pluginZip: pluginZipPath(authOpt()), devPluginDir: W.env.PCRAFT_ALLOW_DEV_PLUGIN === "1" ? printCraftProdDir(W.env) : void 0 });
2955
3229
  for (const w of engine.warnings) console.error(`注意: ${w}`);
2956
3230
  return engine;
2957
3231
  }
@@ -2996,7 +3270,7 @@ async function version(args) {
2996
3270
  return info.ok ? 0 : 1;
2997
3271
  }
2998
3272
  function context(args) {
2999
- const ws = loadWorkspace();
3273
+ const ws = loadWorkspace(W.root);
3000
3274
  const name = option(args, "env");
3001
3275
  if (args.includes("--env")) {
3002
3276
  if (!name || !isEnvName(name)) throw new UsageError("--env には environments.json の環境の名前を続ける(ファイルの場所は指定できない)");
@@ -3005,7 +3279,7 @@ function context(args) {
3005
3279
  return ws ? { ws, env: pickEnv(ws, name) } : { ws: null };
3006
3280
  }
3007
3281
  function client(ctx) {
3008
- const auth = ctx?.ws && ctx.env ? loadAuthForEnv(ctx.env) : loadAuth();
3282
+ const auth = ctx?.ws && ctx.env ? loadAuthForEnv(ctx.env, W.root) : loadAuth(authOpt());
3009
3283
  return { client: createRestClient(auth), authLabel: describeAuth(auth) };
3010
3284
  }
3011
3285
  var envFlag = (ctx) => ctx.ws && ctx.env && ctx.env.name !== ctx.ws.defaultEnv ? ` --env ${ctx.env.name}` : "";
@@ -3016,7 +3290,7 @@ function appOf(args, ctx) {
3016
3290
  return resolveApp(ctx.ws, ctx.env, v).appId;
3017
3291
  }
3018
3292
  function existingAppDir(ctx, appId) {
3019
- const dir = findAppDir(process.cwd(), ctx.env, appId);
3293
+ const dir = findAppDir(W.root, ctx.env, appId);
3020
3294
  if (!dir) throw new InputError(`アプリ ${appId} のフォルダー(kintone/${ctx.env.host}/${appId}-…)が無い。先に npx @rex0220/print-craft-authoring-tools fields --app ${appId}${envFlag(ctx)}`);
3021
3295
  return dir;
3022
3296
  }
@@ -3024,38 +3298,39 @@ function noOutInWorkspace(args, ctx) {
3024
3298
  if (ctx.ws && args.includes("--out")) throw new UsageError("environments.json があるときは --out を使わない(保存先はアプリのフォルダー kintone/<ホスト名>/<番号>-<アプリ名>/)");
3025
3299
  }
3026
3300
  function existingFile(rel, howToFetch) {
3027
- const file = resolveRead(rel);
3028
- 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}`);
3029
3303
  return file;
3030
3304
  }
3031
3305
  function folderDefaults(settingsFile) {
3032
- return loadWorkspace() ? appFolderOfFile(process.cwd(), settingsFile) : null;
3306
+ return loadWorkspace(W.root) ? appFolderOfFile(W.root, settingsFile) : null;
3033
3307
  }
3034
3308
  function fieldsFileOf(args, folder) {
3035
3309
  const arg = option(args, "fields");
3036
- if (arg) return resolveRead(arg);
3310
+ if (arg) return resolveRead2(arg);
3037
3311
  if (!folder) throw new UsageError("--fields <fields.json> が要る(fields コマンドの出力。kintone/<ホスト名>/<番号>-…/ の中のファイルなら同じフォルダーの fields.json を使う)");
3038
- 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}`);
3039
3313
  }
3040
3314
  function trustedBaseUrl(folder) {
3041
- const ws = loadWorkspace();
3315
+ const ws = loadWorkspace(W.root);
3042
3316
  if (ws && folder) return envsOfHost(ws, folder.host)[0]?.baseUrl;
3043
- return baseUrlFromEnv();
3317
+ return baseUrlFromEnv(authOpt());
3044
3318
  }
3045
3319
  async function fields(args) {
3046
3320
  const ctx = context(args);
3047
3321
  const app = appOf(args, ctx);
3048
3322
  noOutInWorkspace(args, ctx);
3049
3323
  if (flag(args, "summary")) {
3050
- 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`);
3051
3325
  console.log(listFields(await readFieldsFile(existingFile(rel, `fields --app ${app}${envFlag(ctx)}`))));
3052
3326
  return 0;
3053
3327
  }
3054
3328
  const guestSpaceId = intOption(args, "guest", false);
3055
3329
  const { client: c, authLabel } = client(ctx);
3056
3330
  const file = await fetchFields(c, { app, lang: option(args, "lang"), preview: flag(args, "preview"), guestSpaceId });
3057
- 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);
3058
- 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");
3059
3334
  console.log(`${summarizeFields(file)}
3060
3335
  ${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}`);
3061
3336
  return 0;
@@ -3065,7 +3340,7 @@ async function record(args) {
3065
3340
  const app = appOf(args, ctx);
3066
3341
  const id = intOption(args, "id", true);
3067
3342
  noOutInWorkspace(args, ctx);
3068
- 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`);
3069
3344
  if (flag(args, "summary")) {
3070
3345
  const file2 = existingFile(defaultRel(), `record --app ${app} --id ${id}${envFlag(ctx)}`);
3071
3346
  const data = readJsonLimited(file2);
@@ -3074,18 +3349,19 @@ async function record(args) {
3074
3349
  return 0;
3075
3350
  }
3076
3351
  const guestSpaceId = intOption(args, "guest", false);
3077
- const out = resolveWrite(option(args, "out") ?? defaultRel(), WRITE_ROOTS.records);
3352
+ const out = resolveWrite2(option(args, "out") ?? defaultRel(), WRITE_ROOTS.records);
3078
3353
  const from = option(args, "fields-from");
3079
3354
  let keep;
3080
3355
  if (from) {
3081
- const settings = readJsonLimited(resolveRead(from));
3356
+ const settings = readJsonLimited(resolveRead2(from));
3082
3357
  const codes = usedFieldCodes(settings);
3083
3358
  if (!codes) console.log(`${from} に使う項目の情報が無いので絞らない(normalize した設定なら usedFields から絞れる)`);
3084
3359
  else keep = codes;
3085
3360
  }
3086
3361
  const { client: c, authLabel } = client(ctx);
3362
+ allow(out, "record");
3087
3363
  const file = await fetchRecord(c, { app, id, guestSpaceId, keep });
3088
- writeJson(out, file);
3364
+ writeJson(out, file, WRITE_ROOTS.records, "record");
3089
3365
  console.log(`${summarizeRecord(file)}
3090
3366
  ${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}(個人情報を含む。コミットしない)`);
3091
3367
  return 0;
@@ -3093,24 +3369,25 @@ ${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}(個人情報を含
3093
3369
  async function normalize(args) {
3094
3370
  const [settingsArg] = positional(args);
3095
3371
  if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
3096
- const settingsFile = resolveRead(settingsArg);
3372
+ const settingsFile = resolveRead2(settingsArg);
3097
3373
  const folder = folderDefaults(settingsFile);
3098
3374
  const fieldsFile = fieldsFileOf(args, folder);
3099
3375
  const write = !flag(args, "dry-run");
3100
- 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))) {
3101
3377
  throw new UsageError(`ダウンロード / pull のファイルは書き換えない。npx @rex0220/print-craft-authoring-tools edit ${shown(settingsFile)} で -edit.json に写してから直す(検査だけなら --dry-run)`);
3102
3378
  }
3103
- 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");
3104
3381
  const fieldsData = await readFieldsFile(fieldsFile);
3105
3382
  const baseUrl = trustedBaseUrl(folder);
3106
3383
  const engine = await engineFor();
3107
3384
  engine.setContext({ baseUrl: baseUrl ?? DEFAULT_CONTEXT_BASE_URL, appId: fieldsData.appId });
3108
3385
  const result = await normalizeSettings({
3109
3386
  settingsText: readTextLimited(settingsFile),
3110
- settingsFile: relativeSettingsPath(settingsArg),
3387
+ settingsFile: relativeSettingsPath(settingsArg, W.root),
3111
3388
  fields: fieldsData,
3112
3389
  engine,
3113
- policy: loadPolicy(),
3390
+ policy: loadPolicy({ cwd: W.root }),
3114
3391
  check: flag(args, "check"),
3115
3392
  baseUrl
3116
3393
  });
@@ -3130,7 +3407,7 @@ async function normalize(args) {
3130
3407
  return 1;
3131
3408
  }
3132
3409
  if (out) {
3133
- writeJson(out, result.output);
3410
+ writeJson(out, result.output, WRITE_ROOTS.settings, "settings");
3134
3411
  if (!flag(args, "json")) console.log(`出力: ${shown(out)}(封筒の date を更新。派生値を生成)`);
3135
3412
  }
3136
3413
  return 0;
@@ -3138,8 +3415,8 @@ async function normalize(args) {
3138
3415
  async function diff(args) {
3139
3416
  const [before, after] = positional(args);
3140
3417
  if (!before || !after) throw new UsageError("diff <before.json> <after.json>");
3141
- const a = readJsonLimited(resolveRead(before));
3142
- const b = readJsonLimited(resolveRead(after));
3418
+ const a = readJsonLimited(resolveRead2(before));
3419
+ const b = readJsonLimited(resolveRead2(after));
3143
3420
  console.log(diffSettings(a, b, { derived: flag(args, "derived") }));
3144
3421
  return 0;
3145
3422
  }
@@ -3147,6 +3424,7 @@ async function pull(args) {
3147
3424
  const ctx = context(args);
3148
3425
  const app = appOf(args, ctx);
3149
3426
  noOutInWorkspace(args, ctx);
3427
+ if (ctx.ws && flag(args, "force")) throw new UsageError("environments.json があるときの pull は、ダウンロードと同じ日時の名前で新しく置く(ダウンロード / pull のファイルは上書きしない)ので --force は使えない");
3150
3428
  const guestSpaceId = intOption(args, "guest", false);
3151
3429
  const preview2 = flag(args, "preview");
3152
3430
  const outArg = option(args, "out");
@@ -3160,13 +3438,15 @@ async function pull(args) {
3160
3438
  result = await pullSettings(c, engine, { app, preview: preview2, guestSpaceId, pluginId });
3161
3439
  } catch (e) {
3162
3440
  if (e instanceof RestError && /plugin\/config/.test(e.apiPath ?? "")) {
3163
- 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);
3164
3442
  }
3165
3443
  throw e;
3166
3444
  }
3167
- 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);
3168
- if (existsSync7(out) && !flag(args, "force")) throw new InputError(`${shown(out)} は既にある。上書きするなら --force(上書きの前の内容は git の差分で確かめる)、別の名前なら --out settings/<ファイル>`);
3169
- 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") });
3170
3450
  console.log(`pull: アプリ ${app} ${result.appName}(${preview2 ? "動作テスト環境" : "運用中"}の設定、revision ${result.revision}、保存形式 ${result.format})を${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}`);
3171
3451
  console.log(listButtons(result.envelope, { file: shown(out) }));
3172
3452
  return 0;
@@ -3179,20 +3459,37 @@ async function buttons(args) {
3179
3459
  const app = appOf(args, ctx);
3180
3460
  const list = listAppFolder(existingAppDir(ctx, app));
3181
3461
  if (!list.snapshots.length) throw new InputError(`${shown(list.dir)} にダウンロード / pull のファイルが無い(設定画面でダウンロードして inbox/ に置き take、または pull --app ${app}${envFlag(ctx)})`);
3182
- settingsArg = shown(path10.join(list.dir, list.snapshots[0]));
3462
+ settingsArg = shown(path13.join(list.dir, list.snapshots[0]));
3183
3463
  }
3184
3464
  if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
3185
3465
  const button = option(args, "button");
3186
3466
  if (args.includes("--button") && !button) throw new UsageError("--button にはボタン名を続ける");
3187
- 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
+ }
3188
3484
  return 0;
3189
3485
  }
3190
3486
  async function take(args) {
3191
3487
  const ctx = context(args);
3192
3488
  if (!ctx.ws) throw new UsageError("take は environments.json があるときだけ使える(inbox/ のダウンロードをアプリのフォルダーへ移す)");
3193
- const r = takeInbox(process.cwd(), ctx.ws, args.includes("--env") ? ctx.env.name : void 0);
3194
- 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 から消した)" : ""}`);
3195
3491
  for (const s of r.skipped) console.error(`移さない: ${s.file}(${s.reason})`);
3492
+ for (const w of r.warnings) console.error(`注意: ${w}`);
3196
3493
  if (!r.moved.length && !r.skipped.length) console.log(`${INBOX}/ に設定のファイルが無い`);
3197
3494
  return r.skipped.length ? 1 : 0;
3198
3495
  }
@@ -3204,15 +3501,16 @@ async function edit(args) {
3204
3501
  const app = appOf(args, ctx);
3205
3502
  const list = listAppFolder(existingAppDir(ctx, app));
3206
3503
  if (!list.snapshots.length) throw new InputError(`${shown(list.dir)} にダウンロード / pull のファイルが無い`);
3207
- src = path10.join(list.dir, list.snapshots[0]);
3504
+ src = path13.join(list.dir, list.snapshots[0]);
3208
3505
  }
3209
- const file = resolveRead(src);
3210
- if (!SNAPSHOT_RE.test(path10.basename(file)) || !appFolderOfFile(process.cwd(), file)) throw new UsageError(`edit に渡すのはアプリのフォルダーのダウンロード / pull のファイル(rex0220-print-craft-app<番号>-<日時>.json): ${src}`);
3211
- const dest = resolveWrite(editNameOf(file), WRITE_ROOTS.kintone);
3212
- 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)) {
3213
3510
  console.log(`既にある(続けて直す): ${shown(dest)}`);
3214
3511
  } else {
3215
- writeText(dest, readTextLimited(file));
3512
+ allow(dest, "settings");
3513
+ writeText(dest, readTextLimited(file), WRITE_ROOTS.kintone, "settings");
3216
3514
  console.log(`${shown(file)} → ${shown(dest)}(ここを直す。ダウンロードのファイルは書き換えない)`);
3217
3515
  }
3218
3516
  console.log(`次: 直したら npx @rex0220/print-craft-authoring-tools normalize ${shown(dest)} → diff ${shown(file)} ${shown(dest)}`);
@@ -3240,40 +3538,41 @@ async function files(args) {
3240
3538
  async function preview(args) {
3241
3539
  const [settingsArg] = positional(args);
3242
3540
  if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
3243
- const settingsFile = resolveRead(settingsArg);
3541
+ const settingsFile = resolveRead2(settingsArg);
3244
3542
  const folder = folderDefaults(settingsFile);
3245
3543
  const fieldsFile = fieldsFileOf(args, folder);
3246
3544
  const recordArg = option(args, "record");
3247
3545
  if (!recordArg) throw new UsageError("--record <record.json> が要る(record コマンドの出力。アプリのフォルダーの中のファイルならレコード番号だけでよい: --record 3)");
3248
- const recordFile = folder && /^\d+$/.test(recordArg) ? existingFile(path10.join(folder.dir, "records", `${recordArg}.json`), `record --app ${folder.appId} --id ${recordArg}`) : resolveRead(recordArg);
3249
- 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);
3250
3548
  const fieldsData = await readFieldsFile(fieldsFile);
3251
3549
  const engine = await engineFor();
3252
3550
  const result = await runPreview({
3253
3551
  settingsText: readTextLimited(settingsFile),
3254
- settingsFile: relativeSettingsPath(settingsArg),
3552
+ settingsFile: relativeSettingsPath(settingsArg, W.root),
3255
3553
  fields: fieldsData,
3256
3554
  recordFile: readJsonLimited(recordFile),
3257
3555
  engine,
3258
- policy: loadPolicy(),
3556
+ policy: loadPolicy({ cwd: W.root }),
3259
3557
  button: option(args, "button"),
3260
3558
  baseUrl: trustedBaseUrl(folder)
3261
3559
  });
3262
3560
  const written = [];
3263
3561
  if (!result.findings.hasErrors) {
3264
3562
  for (const r of result.results) {
3265
- const file = resolveWrite(path10.join(outDir, r.file), WRITE_ROOTS.out);
3266
- 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");
3267
3566
  written.push(shown(file));
3268
3567
  }
3269
3568
  }
3270
3569
  if (flag(args, "json")) {
3271
- 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));
3272
3571
  } else {
3273
3572
  console.log(`preview: ${settingsArg}(${result.summary})`);
3274
3573
  console.log(result.findings.format());
3275
3574
  for (const r of result.results) {
3276
- 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))}`);
3277
3576
  for (const e of r.errors) console.log(` ${e}`);
3278
3577
  }
3279
3578
  for (const s of result.skipped) console.log(`対象外: ${s}`);
@@ -3284,6 +3583,7 @@ async function preview(args) {
3284
3583
  }
3285
3584
  async function main(argv) {
3286
3585
  const [command, ...args] = argv;
3586
+ W = createContext({ cwd: process.cwd(), env: process.env });
3287
3587
  rejectRemovedOptions(args);
3288
3588
  if (args.includes("--env")) context(args);
3289
3589
  switch (command) {
@@ -3330,7 +3630,7 @@ main(process.argv.slice(2)).then(
3330
3630
  console.error(`${e.message}
3331
3631
  ${USAGE}`);
3332
3632
  process.exitCode = 2;
3333
- } 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) {
3334
3634
  console.error(e.message);
3335
3635
  process.exitCode = 1;
3336
3636
  } else {