@rex0220/formula-authoring-tools 0.1.0 → 0.2.1

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/core.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @rex0220/formula-authoring-tools/core 0.1.0 (c) rex0220. MIT License. 計算式エンジンと計算式プラグインのコードは利用者の計算式プラグインプラグインの zip から読む(このファイルには含まれない)。commit 69de71e. */
1
+ /*! @rex0220/formula-authoring-tools/core 0.2.1 (c) rex0220. MIT License. 計算式エンジンと計算式プラグインのコードは利用者の計算式プラグインプラグインの zip から読む(このファイルには含まれない)。commit f2e46ef. */
2
2
 
3
3
  // src/safe-path.ts
4
4
  import { lstatSync, realpathSync } from "node:fs";
@@ -56,8 +56,8 @@ function resolveRead(target, cwd) {
56
56
  const real = realResolve(target, cwd);
57
57
  const cwdReal = realResolve(".", cwd);
58
58
  if (!isInside(real, cwdReal)) throw new PathError(`読むファイルは作業フォルダーの中に置く: ${target}`);
59
- const rel = path.relative(cwdReal, real).replace(/\\/g, "/");
60
- if (/^\.env(\.|$)/i.test(path.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(rel)) throw new PathError(`このファイルは tools では読まない: ${target}`);
59
+ const rel2 = path.relative(cwdReal, real).replace(/\\/g, "/");
60
+ if (/^\.env(\.|$)/i.test(path.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(rel2)) throw new PathError(`このファイルは tools では読まない: ${target}`);
61
61
  return real;
62
62
  }
63
63
  function resolveWrite(target, roots, cwd) {
@@ -74,6 +74,14 @@ function resolveWrite(target, roots, cwd) {
74
74
  if (WINDOWS_RESERVED.test(base) || /[. ]$/.test(base)) throw new PathError(`使えないファイル名: ${base}`);
75
75
  return real;
76
76
  }
77
+ function resolveWriteDir(target, roots, cwd) {
78
+ if (!target || typeof target !== "string") throw new PathError("フォルダーのパスが要る");
79
+ const real = realResolve(target, cwd);
80
+ const cwdReal = realResolve(".", cwd);
81
+ if (!isInside(real, cwdReal)) throw new PathError(`出力フォルダーは作業フォルダーの中: ${target}`);
82
+ if (!roots.some((r) => isInside(real, path.join(cwdReal, r)))) throw new PathError(`出力フォルダーは ${roots.map((r) => `${r}/`).join(" か ")}(またはその下): ${target}`);
83
+ return real;
84
+ }
77
85
  function safeFileName(name, fallback) {
78
86
  let s = name.replace(/[\\/:*?"<>|\u0000-\u001f\u007f]/g, "_").replace(/[. ]+$/g, "").trim();
79
87
  if (!s || WINDOWS_RESERVED.test(s)) s = fallback;
@@ -182,7 +190,7 @@ function devMeta() {
182
190
  return { toolsVersion: pkg.version, pluginId: FORMULA_PLUGIN_ID, minPluginVersion: MIN_PLUGIN_VERSION, supportedApiVersions: [...SUPPORTED_API_VERSIONS], commit: gitCommit(AUTHORING_ROOT), builtAt: "", mode: "dev" };
183
191
  }
184
192
  function toolsMeta() {
185
- if (true) return JSON.parse('{"toolsVersion":"0.1.0","pluginId":"dhgihghfgfmibkdldkeibboabfnmdpke","minPluginVersion":101,"supportedApiVersions":[1],"commit":"69de71e","builtAt":"2026-10-18T00:25:51.113Z","mode":"build"}');
193
+ if (true) return JSON.parse('{"toolsVersion":"0.2.1","pluginId":"dhgihghfgfmibkdldkeibboabfnmdpke","minPluginVersion":101,"supportedApiVersions":[1],"commit":"f2e46ef","builtAt":"2026-10-18T03:55:45.474Z","mode":"build"}');
186
194
  return devMeta();
187
195
  }
188
196
 
@@ -491,6 +499,9 @@ function summarizeUsers(src, origin, user) {
491
499
  }
492
500
 
493
501
  // src/engine.ts
502
+ function momentFileOf(opt) {
503
+ return opt.vendorDir ? path4.join(opt.vendorDir, path4.basename(MOMENT_FILE)) : MOMENT_FILE;
504
+ }
494
505
  var DEFAULT_CONTEXT_BASE_URL = "https://example.cybozu.com";
495
506
  var SENTINEL_USER = { id: "0", code: "__sentinel__", name: "", email: "", timezone: "Asia/Tokyo", language: "ja", isGuest: false };
496
507
  var loaded = null;
@@ -523,7 +534,7 @@ function resolvePluginSources(opt = {}, mode = opt.mode ?? toolsMeta().mode) {
523
534
  throw new PluginZipError(`計算式プラグインの zip の場所が分からない。.env に FORMULA_PLUGIN_ZIP=<計算式プラグインの zip のパス> を書く${mode === "dev" ? "(開発中に隣の formula の prod/ を読むなら環境変数 FORMULA_ALLOW_DEV_PLUGIN=1)" : ""}`, "not-configured");
524
535
  }
525
536
  if (!dev || !existsSync(path4.join(dev, ENGINE_ENTRY))) throw new PluginZipError(`計算式プラグインの zip の場所が分からない(FORMULA_ALLOW_DEV_PLUGIN=1 だが formula の prod/ が無い: ${dev})`, "not-configured");
526
- const read = (rel) => readFileSync3(path4.join(dev, rel), "utf8");
537
+ const read = (rel2) => readFileSync3(path4.join(dev, rel2), "utf8");
527
538
  const apiFile = path4.join(dev, API_ENTRY);
528
539
  const decimalFile = path4.join(dev, DECIMAL_ENTRY);
529
540
  const engine = read(ENGINE_ENTRY);
@@ -590,15 +601,16 @@ async function loadOnce(opt) {
590
601
  }
591
602
  if (!src.api) throw new PluginZipError(`計算式プラグインの zip に ${API_ENTRY} が無い(Ver.${MIN_PLUGIN_VERSION} 以降の zip が要る): ${src.from}`, "no-api", known);
592
603
  if (!src.decimal) warnings.push(`zip に ${DECIMAL_ENTRY} が無い(べき乗の小数の指数がエラーになる)`);
593
- if (!existsSync(MOMENT_FILE)) throw new Error(`moment が無い: ${MOMENT_FILE}(tools の npm install 時に取る。手で取るなら npm run vendor)`);
604
+ const momentFile = momentFileOf(opt);
605
+ if (!existsSync(momentFile)) throw new Error(`moment が無い: ${momentFile}(tools の npm install 時に取る。手で取るなら npm run vendor)`);
594
606
  try {
595
- return await startEngine({ ...src, api: src.api }, known, warnings, stamp);
607
+ return await startEngine({ ...src, api: src.api }, known, warnings, stamp, momentFile);
596
608
  } catch (e) {
597
609
  poisoned = { code: e instanceof PluginZipError ? e.code : "unexpected", info: e instanceof PluginZipError ? e.info : known };
598
610
  throw e;
599
611
  }
600
612
  }
601
- async function startEngine(src, known, warnings, stamp) {
613
+ async function startEngine(src, known, warnings, stamp, momentFile) {
602
614
  const g = globalThis;
603
615
  const { Window } = await import("happy-dom");
604
616
  const ctx = {
@@ -674,7 +686,7 @@ async function startEngine(src, known, warnings, stamp) {
674
686
  throw new PluginZipError(`計算式プラグインの zip のコード(${entry})を実行できない: ${e instanceof Error ? e.message : String(e)}`, "engine-unreadable", known);
675
687
  }
676
688
  };
677
- runScript(readFileSync3(MOMENT_FILE, "utf8"), MOMENT_FILE);
689
+ runScript(readFileSync3(momentFile, "utf8"), momentFile);
678
690
  runScript(src.momentTimezone, `${src.from}!${MOMENT_TZ_ENTRY}`, MOMENT_TZ_ENTRY);
679
691
  runScript(src.bignumber, `${src.from}!${BIGNUMBER_ENTRY}`, BIGNUMBER_ENTRY);
680
692
  if (src.decimal) runScript(src.decimal, `${src.from}!${DECIMAL_ENTRY}`, DECIMAL_ENTRY);
@@ -853,6 +865,17 @@ function loadAuth(opt) {
853
865
  if (!token && !(username && password)) throw new AuthError("KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(OS の環境変数か .env)");
854
866
  return { baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
855
867
  }
868
+ function loadUserApiAuth(opt) {
869
+ const pick = picker(opt);
870
+ const baseUrl = baseUrlFromEnv(opt);
871
+ if (!baseUrl) throw new AuthError(`KINTONE_BASE_URL が無い(OS の環境変数か .env: ${envFileOf(opt)})`);
872
+ const bearer = pick(NAMES.userApiToken);
873
+ if (bearer) return { baseUrl, bearer };
874
+ const username = pick(NAMES.username);
875
+ const password = pick(NAMES.password);
876
+ if (username && password) return { baseUrl, username, password };
877
+ throw new AuthError("User API(users --pull)は FORMULA_USER_API_TOKEN(cybozu.com 共通管理の API トークン)か、KINTONE_USERNAME と KINTONE_PASSWORD(2 要素認証のないアカウント)で呼ぶ。kintone アプリの API トークン(KINTONE_API_TOKEN)では呼べない");
878
+ }
856
879
  function pluginZipPath(opt) {
857
880
  const picked = pickerWithSource(opt, true)(NAMES.pluginZip);
858
881
  if (!picked) return void 0;
@@ -890,6 +913,11 @@ function jsonErrorWhere(e) {
890
913
  if (!m) return "";
891
914
  return m[2] ? `(${m[2]} 行 ${m[3]} 列)` : `(位置 ${m[1]})`;
892
915
  }
916
+ function jsonErrorText(e) {
917
+ const msg = e instanceof Error ? e.message : String(e);
918
+ if (e instanceof SyntaxError || /is not valid JSON|in JSON at position|Unexpected (token|end|non-whitespace)|Bad control character|Unterminated string/.test(msg)) return `JSON の構文の誤り${jsonErrorWhere(e)}(中身は表示しない)`;
919
+ return msg;
920
+ }
893
921
 
894
922
  // src/input.ts
895
923
  var PLUGIN_NAME = "計算式プラグイン";
@@ -939,8 +967,8 @@ function readFieldsFile(file) {
939
967
  return fields;
940
968
  }
941
969
  function relativeSettingsPath(file, cwd) {
942
- const rel = path6.relative(cwd, path6.resolve(cwd, file));
943
- return rel.startsWith("..") ? file : rel.replace(/\\/g, "/");
970
+ const rel2 = path6.relative(cwd, path6.resolve(cwd, file));
971
+ return rel2.startsWith("..") ? file : rel2.replace(/\\/g, "/");
944
972
  }
945
973
  function quiet(fn) {
946
974
  const saved = { log: console.log, warn: console.warn, error: console.error };
@@ -988,7 +1016,7 @@ var RestError = class extends Error {
988
1016
  var NotAllowedError = class extends Error {
989
1017
  };
990
1018
  async function readCapped(res, limit, apiPath) {
991
- const tooLarge = () => new RestError(`kintone ${apiPath} の応答が大きすぎる(上限 ${limit} バイト)`, res.status, "LimitError", apiPath);
1019
+ const tooLarge2 = () => new RestError(`kintone ${apiPath} の応答が大きすぎる(上限 ${limit} バイト)`, res.status, "LimitError", apiPath);
992
1020
  const quietCancel = async (reader2) => {
993
1021
  try {
994
1022
  await reader2.cancel();
@@ -1001,7 +1029,7 @@ async function readCapped(res, limit, apiPath) {
1001
1029
  const declared = Number(res.headers?.get("content-length") ?? Number.NaN);
1002
1030
  if (Number.isFinite(declared) && declared > limit) {
1003
1031
  await quietCancel(reader);
1004
- throw tooLarge();
1032
+ throw tooLarge2();
1005
1033
  }
1006
1034
  const chunks = [];
1007
1035
  let total = 0;
@@ -1012,7 +1040,7 @@ async function readCapped(res, limit, apiPath) {
1012
1040
  total += value.length;
1013
1041
  if (total > limit) {
1014
1042
  await quietCancel(reader);
1015
- throw tooLarge();
1043
+ throw tooLarge2();
1016
1044
  }
1017
1045
  chunks.push(value);
1018
1046
  }
@@ -1651,6 +1679,7 @@ function diffSettings(before, after, names) {
1651
1679
 
1652
1680
  // src/commands/users.ts
1653
1681
  var PAGE = 100;
1682
+ var CODES_PER_CALL = 100;
1654
1683
  var DEFAULT_MAX_CALLS = 300;
1655
1684
  var UsersPullError = class extends Error {
1656
1685
  };
@@ -1708,6 +1737,95 @@ function scanUserInfoCalls(text) {
1708
1737
  }
1709
1738
  return out;
1710
1739
  }
1740
+ function referencedCodesOf(settings, strip) {
1741
+ const out = { users: [], orgs: [], groups: [], dynamic: [], fields: 0 };
1742
+ const rows = Array.isArray(settings.pluginInfos) ? settings.pluginInfos : [];
1743
+ const add = (kind, code) => {
1744
+ if (!out[kind].includes(code)) out[kind].push(code);
1745
+ };
1746
+ for (const r of rows) {
1747
+ if (!r || typeof r.formulaSet !== "string" || !r.formulaSet.trim()) continue;
1748
+ out.fields++;
1749
+ const fieldcode = typeof r.fieldcode === "string" ? r.fieldcode : "?";
1750
+ const calls = scanUserInfoCalls(strip(r.formulaSet));
1751
+ for (const d of calls.dynamic) out.dynamic.push(`${fieldcode}: ${d}`);
1752
+ for (const kind of ["users", "orgs", "groups"]) for (const c of calls[kind]) add(kind, c);
1753
+ }
1754
+ return out;
1755
+ }
1756
+ async function byCodes(client, api, key, codes, budget) {
1757
+ const out = [];
1758
+ for (let i = 0; i < codes.length; i += CODES_PER_CALL) {
1759
+ if (++budget.calls > budget.max) throw new UsersPullError(`User API の呼び出しが ${budget.max} 回を超えた(--max で上げる)`);
1760
+ const res = await client.getUser(api, { codes: codes.slice(i, i + CODES_PER_CALL), size: PAGE });
1761
+ out.push(...res[key] ?? []);
1762
+ }
1763
+ return out;
1764
+ }
1765
+ var uniq = (a) => [...new Set(a)];
1766
+ var missingOf = (want, have) => {
1767
+ const got = new Set([...have].map((x) => x.code));
1768
+ return want.filter((c) => !got.has(c));
1769
+ };
1770
+ async function pullReferencedUsers(client, ref, opt = {}) {
1771
+ const budget = { calls: 0, max: opt.maxCalls ?? DEFAULT_MAX_CALLS };
1772
+ const log = opt.log ?? (() => void 0);
1773
+ const one = async (api, key, code) => all(client, api, key, { code }, budget);
1774
+ const orgMembers = /* @__PURE__ */ new Map();
1775
+ for (const o of uniq(ref.orgs)) orgMembers.set(o, (await one("organization/users", "userTitles", o)).map((m) => m.user?.code).filter((c) => !!c));
1776
+ const groupMembers = /* @__PURE__ */ new Map();
1777
+ for (const g of uniq(ref.groups)) groupMembers.set(g, (await one("group/users", "users", g)).map((m) => m.code).filter((c) => !!c));
1778
+ const userCodes = uniq([...ref.users, ...[...orgMembers.values()].flat(), ...[...groupMembers.values()].flat()]);
1779
+ const rawUsers = userCodes.length ? await byCodes(client, "users", "users", userCodes, budget) : [];
1780
+ const missingUsers = missingOf(ref.users, rawUsers);
1781
+ if (missingUsers.length) throw new UsersPullError(`式が参照しているユーザーが無い: ${missingUsers.slice(0, 5).join(", ")}(コードを確かめる。全部取るなら --only-referenced を外す)`);
1782
+ const valid = rawUsers.filter((u2) => u2.valid !== false);
1783
+ log(`ユーザー ${valid.length}(参照 ${ref.users.length}、組織 ${ref.orgs.length} とグループ ${ref.groups.length} の所属を含む。ここまで ${budget.calls} 回)。所属をあと ${valid.length * 2} 回`);
1784
+ const userOrgs = /* @__PURE__ */ new Map();
1785
+ const userGroups = /* @__PURE__ */ new Map();
1786
+ for (const u2 of valid) {
1787
+ if ((budget.calls += 2) > budget.max) throw new UsersPullError(`User API の呼び出しが ${budget.max} 回を超えた(--max で上げる)`);
1788
+ const o = await client.getUser("user/organizations", { code: u2.code });
1789
+ userOrgs.set(u2.code, (o.organizationTitles ?? []).map((t) => t.organization).filter((x) => !!x && typeof x.code === "string"));
1790
+ const g = await client.getUser("user/groups", { code: u2.code });
1791
+ userGroups.set(u2.code, (g.groups ?? []).filter((x) => !!x && typeof x.code === "string"));
1792
+ }
1793
+ const orgDetail = /* @__PURE__ */ new Map();
1794
+ for (const list of userOrgs.values()) for (const o of list) orgDetail.set(o.code, o);
1795
+ const parentsOf = (orgs) => uniq([...orgs].map((o) => o.parentCode).filter((c) => !!c));
1796
+ let pending = uniq([...ref.orgs, ...parentsOf(orgDetail.values())]).filter((c) => !orgDetail.has(c));
1797
+ while (pending.length) {
1798
+ const got = await byCodes(client, "organizations", "organizations", pending, budget);
1799
+ for (const o of got) orgDetail.set(o.code, o);
1800
+ const missing = pending.filter((c) => !orgDetail.has(c));
1801
+ if (missing.length) throw new UsersPullError(`式が参照している組織が無い: ${missing.slice(0, 5).join(", ")}(コードを確かめる。全部取るなら --only-referenced を外す)`);
1802
+ pending = parentsOf(got).filter((c) => !orgDetail.has(c));
1803
+ }
1804
+ const groupDetail = /* @__PURE__ */ new Map();
1805
+ for (const list of userGroups.values()) for (const g of list) groupDetail.set(g.code, g);
1806
+ const gPending = uniq(ref.groups).filter((c) => !groupDetail.has(c));
1807
+ if (gPending.length) {
1808
+ for (const g of await byCodes(client, "groups", "groups", gPending, budget)) groupDetail.set(g.code, g);
1809
+ const missing = gPending.filter((c) => !groupDetail.has(c));
1810
+ if (missing.length) throw new UsersPullError(`式が参照しているグループが無い: ${missing.slice(0, 5).join(", ")}(コードを確かめる。全部取るなら --only-referenced を外す)`);
1811
+ }
1812
+ const byId = (a, b) => Number(a.id) - Number(b.id) || String(a.id).localeCompare(String(b.id));
1813
+ const orgRecords = [...orgDetail.values()].map((o) => ({
1814
+ id: String(o.id),
1815
+ code: o.code,
1816
+ name: o.name,
1817
+ parentCode: o.parentCode ?? null,
1818
+ users: valid.filter((u2) => (orgMembers.get(o.code) ?? []).includes(u2.code) || (userOrgs.get(u2.code) ?? []).some((x) => x.code === o.code)).map((u2) => u2.code)
1819
+ })).sort(byId);
1820
+ const groupRecords = [...groupDetail.values()].map((g) => ({
1821
+ id: String(g.id),
1822
+ code: g.code,
1823
+ name: g.name,
1824
+ users: valid.filter((u2) => (groupMembers.get(g.code) ?? []).includes(u2.code) || (userGroups.get(u2.code) ?? []).some((x) => x.code === g.code)).map((u2) => u2.code)
1825
+ })).sort(byId);
1826
+ const userRecords = valid.map((u2) => ({ ...u2, id: String(u2.id), customItemValues: u2.customItemValues ?? [] })).sort(byId);
1827
+ return { source: { users: userRecords, organizations: orgRecords, groups: groupRecords }, calls: budget.calls };
1828
+ }
1711
1829
  function usersFileNameOf(baseUrl) {
1712
1830
  try {
1713
1831
  return `${new URL(baseUrl).hostname}.json`;
@@ -2171,7 +2289,7 @@ function calcSettings(input) {
2171
2289
  const usersLabel = !table ? userInfoOn ? "なし" : "各種設定「ユーザー情報」OFF" : input.usersOrigin.kind === "file" ? input.usersOrigin.file : "既定のテストデータ(架空)";
2172
2290
  const contextLine = `文脈: アプリ ${input.appId}、ログインユーザー ${loginUser ? loginUser.code : "指定なし(番兵)"}、mobile ${input.mobile ? "true" : "false"}、イベント ${evType}${event ? `(${event.action.value} / ${event.status.value} → ${event.nextStatus.value})` : ""}、ユーザー情報 ${usersLabel}、ルックアップ先 ${Object.keys(targets.lookupTargets).length}、関連レコード一覧の先 ${Object.keys(targets.relatedTargets).length}`;
2173
2291
  const notice = `更新していません(模擬)。EV_INFO の文脈を与えた 3 段(項目 → テーブルの行 → 2 段目)の計算で、${evType === "allUpdate" ? "設定画面の「既存レコードを変更」の文脈" : evType === "batch" ? "一覧の「一括計算」の文脈" : "画面のイベントの処理の完全な再現ではありません"}。`;
2174
- const markOf = (fieldcode) => {
2292
+ const markOf2 = (fieldcode) => {
2175
2293
  const scan = normalized.scans.get(fieldcode);
2176
2294
  if (!scan) return [];
2177
2295
  const used = Object.keys(scan.functions);
@@ -2194,7 +2312,7 @@ function calcSettings(input) {
2194
2312
  const a = r.after[code]?.value;
2195
2313
  const raw = record[code]?.value;
2196
2314
  const b = isTable(raw) && isTable(rb) && raw.length !== rb.length ? raw : rb;
2197
- lines.push({ fieldcode: code, before: b, after: a, changed: JSON.stringify(b) !== JSON.stringify(a), marks: markOf(code) });
2315
+ lines.push({ fieldcode: code, before: b, after: a, changed: JSON.stringify(b) !== JSON.stringify(a), marks: markOf2(code) });
2198
2316
  }
2199
2317
  const byField = /* @__PURE__ */ new Map();
2200
2318
  for (const e of r.errors) {
@@ -2249,15 +2367,884 @@ function calcSettings(input) {
2249
2367
  return { contextLine, notice, records: results, summary, text: text.join("\n") };
2250
2368
  }
2251
2369
 
2370
+ // src/commands/take.ts
2371
+ import { closeSync as closeSync2, existsSync as existsSync6, fstatSync as fstatSync2, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync9, readSync as readSync2, unlinkSync } from "node:fs";
2372
+ import path12 from "node:path";
2373
+
2374
+ // src/permission.ts
2375
+ import { existsSync as existsSync5 } from "node:fs";
2376
+ import path10 from "node:path";
2377
+
2378
+ // src/connections.ts
2379
+ import { createHash as createHash3, createHmac, randomBytes } from "node:crypto";
2380
+ import * as nodeFs from "node:fs";
2381
+ import { realpathSync as realpathSync3 } from "node:fs";
2382
+ import path8 from "node:path";
2383
+ var CONNECTION_MAX_BYTES = 256 * 1024;
2384
+ var REAL_FS = {
2385
+ openSync: (p, flags) => nodeFs.openSync(p, flags),
2386
+ fstatSync: (fd) => nodeFs.fstatSync(fd),
2387
+ readSync: (fd, buf, offset, length, position) => nodeFs.readSync(fd, buf, offset, length, position),
2388
+ closeSync: (fd) => nodeFs.closeSync(fd),
2389
+ statSync: (p) => nodeFs.statSync(p)
2390
+ };
2391
+ var fsOps = REAL_FS;
2392
+ var PROFILE_NAME_RE = /^[A-Za-z0-9_-]{1,40}$/;
2393
+ var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2394
+ var FALLBACK_PROFILE = "dev";
2395
+ var ConnectionError = class extends Error {
2396
+ code;
2397
+ constructor(message, code) {
2398
+ super(message);
2399
+ this.code = code;
2400
+ }
2401
+ };
2402
+ var SECRETS = /* @__PURE__ */ new WeakMap();
2403
+ var FINGERPRINT_KEY = randomBytes(32);
2404
+ var fingerprint = (parts) => createHmac("sha256", FINGERPRINT_KEY).update(JSON.stringify(parts)).digest("hex");
2405
+ var nonblank = (v) => typeof v === "string" && v.trim() ? v : void 0;
2406
+ var isPlainObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2407
+ function appKeyOf(key) {
2408
+ const m = /^(?:APP)?([1-9][0-9]*)$/i.exec(key);
2409
+ if (!m) return void 0;
2410
+ const n = Number(m[1]);
2411
+ return Number.isSafeInteger(n) ? n : void 0;
2412
+ }
2413
+ function readProfile(name, raw, env) {
2414
+ if (!PROFILE_NAME_RE.test(name)) return { reason: "profile の名前は英数字と - _ の 40 文字まで(フォルダーの名前になる)" };
2415
+ if (!isPlainObject(raw)) return { reason: "profile がオブジェクトでない" };
2416
+ if (typeof raw.baseUrl !== "string") return { reason: "baseUrl が無い、または文字列でない" };
2417
+ for (const k of ["username", "password", "passwordEnv"]) {
2418
+ if (raw[k] !== void 0 && typeof raw[k] !== "string") return { reason: `${k} が文字列でない` };
2419
+ }
2420
+ let baseUrl;
2421
+ try {
2422
+ baseUrl = normalizeKintoneBaseUrl(raw.baseUrl);
2423
+ } catch {
2424
+ return { reason: "baseUrl が kintone の URL でない(https://<サブドメイン>.cybozu.com / .kintone.com / .cybozu.cn だけ。パス・ポート・ユーザー情報なし)" };
2425
+ }
2426
+ let guestSpaceId = null;
2427
+ if (raw.guestSpaceId !== void 0 && raw.guestSpaceId !== null) {
2428
+ if (typeof raw.guestSpaceId !== "number" || !Number.isSafeInteger(raw.guestSpaceId) || raw.guestSpaceId <= 0) return { reason: "guestSpaceId は正の整数" };
2429
+ guestSpaceId = raw.guestSpaceId;
2430
+ }
2431
+ const mode = raw.auth ?? "auto";
2432
+ if (mode !== "token" && mode !== "userpass" && mode !== "auto") return { reason: "auth は token / userpass / auto のどれか" };
2433
+ if (raw.passwordEnv !== void 0 && (typeof raw.passwordEnv !== "string" || !ENV_NAME_RE.test(raw.passwordEnv))) return { reason: "passwordEnv は環境変数の名前(英字か _ で始まる英数字と _)" };
2434
+ if (raw.userApiToken !== void 0) {
2435
+ if (typeof raw.userApiToken !== "string" || !raw.userApiToken.trim()) return { reason: "userApiToken が文字列でない、または空" };
2436
+ if (raw.userApiToken.startsWith("env:") && !ENV_NAME_RE.test(raw.userApiToken.slice(4))) return { reason: "userApiToken の env: の後ろが環境変数の名前でない" };
2437
+ }
2438
+ const userApiToken = typeof raw.userApiToken === "string" ? raw.userApiToken : void 0;
2439
+ const username = nonblank(raw.username);
2440
+ const password = (typeof raw.passwordEnv === "string" ? nonblank(env[raw.passwordEnv]) : void 0) ?? nonblank(raw.password);
2441
+ const tokens = /* @__PURE__ */ new Map();
2442
+ if (raw.tokenMap !== void 0) {
2443
+ if (!isPlainObject(raw.tokenMap)) return { reason: "tokenMap がオブジェクトでない" };
2444
+ for (const key of Object.keys(raw.tokenMap)) {
2445
+ const appId = appKeyOf(key);
2446
+ if (appId === void 0) return { reason: `tokenMap のキー「${key.slice(0, 40)}」はアプリの番号でない(APP101 か 101。先頭に 0 を付けない)` };
2447
+ if (tokens.has(appId)) return { reason: `tokenMap にアプリ ${appId} が 2 つある(APP${appId} と ${appId} など)` };
2448
+ const value = raw.tokenMap[key];
2449
+ if (typeof value !== "string" || !value.trim()) return { reason: `tokenMap の APP${appId} の値が文字列でない、または空` };
2450
+ if (value.startsWith("env:") && !ENV_NAME_RE.test(value.slice(4))) return { reason: `tokenMap の APP${appId} の env: の後ろが環境変数の名前でない` };
2451
+ tokens.set(appId, value);
2452
+ }
2453
+ }
2454
+ const auth = mode === "auto" ? username && password ? "userpass" : "token" : mode;
2455
+ const tokenApps = [...tokens.keys()].sort((a, b) => a - b);
2456
+ const tokenReady = {};
2457
+ for (const [appId, value] of tokens) tokenReady[appId] = value.startsWith("env:") ? !!nonblank(env[value.slice(4)]) : true;
2458
+ const host = new URL(baseUrl).hostname;
2459
+ const userApiTokenReady = userApiToken === void 0 ? false : userApiToken.startsWith("env:") ? !!nonblank(env[userApiToken.slice(4)]) : true;
2460
+ return {
2461
+ def: { profile: name, baseUrl, host, guestSpaceId, auth, tokenApps, tokenReady, userpassReady: !!(username && password), userApiTokenReady },
2462
+ secrets: {
2463
+ ...username ? { username } : {},
2464
+ ...password ? { password } : {},
2465
+ tokens,
2466
+ ...userApiToken !== void 0 ? { userApiToken } : {}
2467
+ }
2468
+ };
2469
+ }
2470
+ function realOf(p) {
2471
+ return realpathSync3.native(p);
2472
+ }
2473
+ var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
2474
+ var changed = (name) => new ConnectionError(`接続のファイル(${name})が読み込みの途中で変わった`, "unreadable");
2475
+ function readStable(real, name) {
2476
+ let fd;
2477
+ try {
2478
+ fd = fsOps.openSync(real, "r");
2479
+ const before = fsOps.fstatSync(fd);
2480
+ if (!before.isFile()) throw new ConnectionError(`接続のファイル(${name})が普通のファイルでない(フォルダーなど)`, "unreadable");
2481
+ if (before.size > CONNECTION_MAX_BYTES) throw new ConnectionError(`接続のファイル(${name})が大きすぎる(上限 ${CONNECTION_MAX_BYTES} バイト)`, "unreadable");
2482
+ const buf = Buffer.alloc(CONNECTION_MAX_BYTES + 1);
2483
+ let off = 0;
2484
+ for (; ; ) {
2485
+ const n = fsOps.readSync(fd, buf, off, buf.length - off, off);
2486
+ if (n === 0) break;
2487
+ off += n;
2488
+ if (off > CONNECTION_MAX_BYTES) throw new ConnectionError(`接続のファイル(${name})が大きすぎる(上限 ${CONNECTION_MAX_BYTES} バイト)`, "unreadable");
2489
+ }
2490
+ const after = fsOps.fstatSync(fd);
2491
+ let onPath;
2492
+ try {
2493
+ onPath = fsOps.statSync(real);
2494
+ } catch {
2495
+ throw changed(name);
2496
+ }
2497
+ if (!sameFile(before, after) || !sameFile(before, onPath) || off !== before.size) throw changed(name);
2498
+ return { bytes: Buffer.from(buf.subarray(0, off)), stat: before };
2499
+ } catch (e) {
2500
+ if (e instanceof ConnectionError) throw e;
2501
+ throw new ConnectionError(`接続のファイル(${name})を読めない`, "unreadable");
2502
+ } finally {
2503
+ if (fd !== void 0) fsOps.closeSync(fd);
2504
+ }
2505
+ }
2506
+ function readRegularFile(real, name) {
2507
+ const first = readStable(real, name);
2508
+ if (first.stat.ino === 0) {
2509
+ const second = readStable(real, name);
2510
+ if (!first.bytes.equals(second.bytes)) throw changed(name);
2511
+ }
2512
+ return { bytes: first.bytes, mode: first.stat.mode };
2513
+ }
2514
+ function hasInlineSecrets(rawProfiles) {
2515
+ for (const key of Object.keys(rawProfiles)) {
2516
+ const p = rawProfiles[key];
2517
+ if (!isPlainObject(p)) continue;
2518
+ if (nonblank(p.password)) return true;
2519
+ if (typeof p.userApiToken === "string" && p.userApiToken.trim() !== "" && !p.userApiToken.startsWith("env:")) return true;
2520
+ if (isPlainObject(p.tokenMap) && Object.values(p.tokenMap).some((v) => typeof v === "string" && v.trim() !== "" && !v.startsWith("env:"))) return true;
2521
+ }
2522
+ return false;
2523
+ }
2524
+ function loadConnections(file, opt) {
2525
+ if (!file || !file.trim()) throw new ConnectionError("kintone の接続のファイルが設定されていない(ksql.config.json と同じ形のファイル)", "not-configured");
2526
+ const name = path8.basename(file);
2527
+ if (!path8.isAbsolute(file)) throw new ConnectionError(`接続のファイル(${name})は絶対パスで指定する`, "unreadable");
2528
+ let real;
2529
+ try {
2530
+ real = realOf(file);
2531
+ } catch {
2532
+ throw new ConnectionError(`接続のファイル(${name})が無い、または読めない`, "unreadable");
2533
+ }
2534
+ const given = path8.resolve(file);
2535
+ for (const root of opt.workspaceRoots) {
2536
+ let rootReal;
2537
+ try {
2538
+ rootReal = realOf(root);
2539
+ } catch {
2540
+ continue;
2541
+ }
2542
+ if (isInside(real, rootReal) || isInside(given, rootReal) || isInside(given, path8.resolve(root))) {
2543
+ throw new ConnectionError(`接続のファイル(${name})が作業フォルダーの中にある。作業フォルダーの外に置く`, "inside-workspace");
2544
+ }
2545
+ }
2546
+ const { bytes, mode } = readRegularFile(real, name);
2547
+ let again;
2548
+ try {
2549
+ again = realOf(file);
2550
+ } catch {
2551
+ again = void 0;
2552
+ }
2553
+ if (again !== real) throw new ConnectionError(`接続のファイル(${name})が読み込みの途中で変わった`, "unreadable");
2554
+ let data;
2555
+ try {
2556
+ data = JSON.parse(bytes.toString("utf8"));
2557
+ } catch {
2558
+ throw new ConnectionError(`接続のファイル(${name})を JSON として読めない`, "unreadable");
2559
+ }
2560
+ if (!isPlainObject(data)) throw new ConnectionError(`接続のファイル(${name})の最上位がオブジェクトでない`, "unreadable");
2561
+ const rawProfiles = data.profiles ?? {};
2562
+ if (!isPlainObject(rawProfiles)) throw new ConnectionError(`接続のファイル(${name})の profiles がオブジェクトでない`, "unreadable");
2563
+ const profiles = /* @__PURE__ */ new Map();
2564
+ const byProfile = /* @__PURE__ */ new Map();
2565
+ const invalid = [];
2566
+ for (const key of Object.keys(rawProfiles)) {
2567
+ const r = readProfile(key, rawProfiles[key], opt.env);
2568
+ if ("reason" in r) invalid.push({ name: key.slice(0, 40), reason: r.reason });
2569
+ else {
2570
+ profiles.set(key, r.def);
2571
+ byProfile.set(key, r.secrets);
2572
+ }
2573
+ }
2574
+ const lower = /* @__PURE__ */ new Map();
2575
+ for (const key of profiles.keys()) lower.set(key.toLowerCase(), [...lower.get(key.toLowerCase()) ?? [], key]);
2576
+ for (const keys of lower.values()) {
2577
+ if (keys.length < 2) continue;
2578
+ for (const key of keys) {
2579
+ profiles.delete(key);
2580
+ byProfile.delete(key);
2581
+ invalid.push({ name: key, reason: `大文字小文字だけが違う profile がある(${keys.join("、")})` });
2582
+ }
2583
+ }
2584
+ if (data.defaultProfile !== void 0 && typeof data.defaultProfile !== "string") throw new ConnectionError(`接続のファイル(${name})の defaultProfile が文字列でない`, "unreadable");
2585
+ const defaultProfile = data.defaultProfile;
2586
+ const warnings = [];
2587
+ if (process.platform !== "win32" && hasInlineSecrets(rawProfiles) && (mode & 63) !== 0) warnings.push(`接続のファイル(${name})に秘密の値が直接書いてあり、ほかの利用者も読める。chmod 600 を勧める`);
2588
+ const set = { fileName: name, digest: createHash3("sha256").update(bytes).digest("hex"), ...defaultProfile !== void 0 ? { defaultProfile } : {}, profiles, invalid, warnings };
2589
+ SECRETS.set(set, { env: opt.env, byProfile });
2590
+ return set;
2591
+ }
2592
+ function pickProfile(set, name) {
2593
+ const bad = (n) => set.invalid.find((p) => p.name === n)?.reason;
2594
+ if (name !== void 0) {
2595
+ const def2 = set.profiles.get(name);
2596
+ if (def2) return def2;
2597
+ const reason2 = bad(name);
2598
+ if (reason2) throw new ConnectionError(`profile「${name.slice(0, 40)}」は使えない: ${reason2}`, "profile-invalid");
2599
+ throw new ConnectionError(`profile「${name.slice(0, 40)}」は接続のファイル(${set.fileName})に無い`, "unknown-profile");
2600
+ }
2601
+ if (set.defaultProfile !== void 0) {
2602
+ const def2 = set.profiles.get(set.defaultProfile);
2603
+ if (def2) return def2;
2604
+ const reason2 = bad(set.defaultProfile);
2605
+ throw new ConnectionError(`defaultProfile の「${set.defaultProfile.slice(0, 40)}」が${reason2 ? `使えない: ${reason2}` : "接続のファイルの profiles に無い"}`, "profile-invalid");
2606
+ }
2607
+ const def = set.profiles.get(FALLBACK_PROFILE);
2608
+ if (def) return def;
2609
+ const reason = bad(FALLBACK_PROFILE);
2610
+ if (reason) throw new ConnectionError(`profile「${FALLBACK_PROFILE}」は使えない: ${reason}`, "profile-invalid");
2611
+ throw new ConnectionError(`profile を指定するか、接続のファイル(${set.fileName})に defaultProfile を書く(無ければ "${FALLBACK_PROFILE}" を使うが、それも無い)`, "no-default-profile");
2612
+ }
2613
+ function snapshotOf(set, def) {
2614
+ return { ...identityOf(def), auth: def.auth, fileName: set.fileName };
2615
+ }
2616
+ function semanticDigest(set, def, appId) {
2617
+ const held = SECRETS.get(set);
2618
+ const secrets = held?.byProfile.get(def.profile);
2619
+ let credential = "none";
2620
+ if (def.auth === "userpass") credential = fingerprint(["userpass", secrets?.username ?? "", secrets?.password ?? ""]);
2621
+ else if (appId !== void 0) {
2622
+ const v = secrets?.tokens.get(appId);
2623
+ const value = v === void 0 ? "" : v.startsWith("env:") ? nonblank(held?.env[v.slice(4)]) ?? "" : v;
2624
+ credential = fingerprint(["token", v?.startsWith("env:") ? v : "inline", value]);
2625
+ }
2626
+ return createHash3("sha256").update(JSON.stringify([def.profile, def.baseUrl, def.guestSpaceId, def.auth, credential, appId ?? null])).digest("hex");
2627
+ }
2628
+ function assertSameConnection(before, reload, appId) {
2629
+ const want = semanticDigest(before.set, before.def, appId);
2630
+ let now;
2631
+ try {
2632
+ const set = reload();
2633
+ now = semanticDigest(set, pickProfile(set, before.def.profile), appId);
2634
+ } catch (e) {
2635
+ if (e instanceof ConnectionError && (e.code === "unknown-profile" || e.code === "profile-invalid")) throw new ConnectionError(`profile「${before.def.profile}」の接続が途中で変わった(使えなくなった)。もう一度やり直す`, "connection-changed");
2636
+ throw e;
2637
+ }
2638
+ if (now !== want) throw new ConnectionError(`profile「${before.def.profile}」の接続が途中で変わった。もう一度やり直す`, "connection-changed");
2639
+ }
2640
+ function authFor(set, profile, appId) {
2641
+ const held = SECRETS.get(set);
2642
+ const secrets = held?.byProfile.get(profile.profile);
2643
+ if (!held || !secrets) throw new ConnectionError(`profile「${profile.profile}」の接続が読み込まれていない`, "profile-invalid");
2644
+ if (profile.auth === "userpass") {
2645
+ if (!secrets.username || !secrets.password) throw new ConnectionError(`profile「${profile.profile}」のログイン名とパスワードがそろわない(username と password か passwordEnv)`, "no-userpass");
2646
+ return { baseUrl: profile.baseUrl, username: secrets.username, password: secrets.password };
2647
+ }
2648
+ const value = secrets.tokens.get(appId);
2649
+ if (value === void 0) throw new ConnectionError(`profile「${profile.profile}」の tokenMap に APP${appId} が無い(そのアプリの API トークンを足す)`, "no-token");
2650
+ if (value.startsWith("env:")) {
2651
+ const token = nonblank(held.env[value.slice(4)]);
2652
+ if (!token) throw new ConnectionError(`profile「${profile.profile}」の APP${appId} のトークンの環境変数が無い(環境変数を置いてから実行し直す。MCP なら再接続する: Claude Code は /mcp、Desktop は起動し直す)`, "env-missing");
2653
+ return { baseUrl: profile.baseUrl, token };
2654
+ }
2655
+ return { baseUrl: profile.baseUrl, token: value };
2656
+ }
2657
+ function userApiAuthFor(set, profile) {
2658
+ const held = SECRETS.get(set);
2659
+ const secrets = held?.byProfile.get(profile.profile);
2660
+ if (!held || !secrets) throw new ConnectionError(`profile「${profile.profile}」の接続が読み込まれていない`, "profile-invalid");
2661
+ const raw = secrets.userApiToken;
2662
+ if (raw !== void 0) {
2663
+ const bearer = raw.startsWith("env:") ? nonblank(held.env[raw.slice(4)]) : raw;
2664
+ if (!bearer) throw new ConnectionError(`profile「${profile.profile}」の userApiToken の環境変数が無い(設定した後、起動し直す)`, "env-missing");
2665
+ return { baseUrl: profile.baseUrl, bearer };
2666
+ }
2667
+ if (secrets.username && secrets.password) return { baseUrl: profile.baseUrl, username: secrets.username, password: secrets.password };
2668
+ throw new ConnectionError(`profile「${profile.profile}」に User API(users --pull)の認証が無い。userApiToken(cybozu.com 共通管理の API トークン。env:<環境変数> か値)か、username と password(passwordEnv)を書く。アプリの API トークン(tokenMap)では呼べない`, "no-user-api-auth");
2669
+ }
2670
+ function identityOf(def) {
2671
+ return { profile: def.profile, baseUrl: def.baseUrl, host: def.host, guestSpaceId: def.guestSpaceId };
2672
+ }
2673
+ function sameIdentity(a, b) {
2674
+ return a.profile === b.profile && a.baseUrl === b.baseUrl && a.guestSpaceId === b.guestSpaceId;
2675
+ }
2676
+
2677
+ // src/permission-error.ts
2678
+ var PermissionError = class extends Error {
2679
+ };
2680
+
2681
+ // src/workspace.ts
2682
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync, readdirSync, readFileSync as readFileSync7, renameSync, rmSync, writeFileSync } from "node:fs";
2683
+ import { randomBytes as randomBytes2 } from "node:crypto";
2684
+ import path9 from "node:path";
2685
+ var KINTONE_ROOT = "kintone";
2686
+ var INBOX = "inbox";
2687
+ var LEGACY_WORKSPACE_FILE = "environments.json";
2688
+ var APP_MARKER = ".formula-app.json";
2689
+ var SNAPSHOT_PREFIX = "rex0220-formula";
2690
+ var SNAPSHOT_RE = /^rex0220-formula-app(\d+)-(\d{8})-(\d{6})\.json$/;
2691
+ var EDIT_RE = /^rex0220-formula-app(\d+)-(\d{8})-(\d{6})-edit\.json$/;
2692
+ var MARKER_MAX_BYTES = 4 * 1024;
2693
+ var WorkspaceError = class extends Error {
2694
+ };
2695
+ function hasLegacyConfig(cwd) {
2696
+ try {
2697
+ lstatSync2(path9.join(cwd, LEGACY_WORKSPACE_FILE));
2698
+ return true;
2699
+ } catch {
2700
+ return false;
2701
+ }
2702
+ }
2703
+ function legacyBlockedError() {
2704
+ return new ConnectionError(
2705
+ `${LEGACY_WORKSPACE_FILE} は tools 2.0.0 から使わない。kintone の接続のファイル(ksql.config.json と同じ形)を書いて設定し(CLI は FORMULA_KINTONE_CONFIG)、${LEGACY_WORKSPACE_FILE} を消す(バージョンアップ手順)`,
2706
+ "legacy-config-present"
2707
+ );
2708
+ }
2709
+ function modeOf(cwd, opt) {
2710
+ if (opt.configFile !== void 0) {
2711
+ if (!opt.configFile.trim()) throw new ConnectionError("kintone の接続のファイル(FORMULA_KINTONE_CONFIG)が空。ファイルのパスを書くか、設定を消す", "unreadable");
2712
+ const load = () => loadConnections(opt.configFile, { workspaceRoots: opt.workspaceRoots, env: opt.env });
2713
+ return { kind: "profiles", connections: { set: load(), reload: load } };
2714
+ }
2715
+ if (hasLegacyConfig(cwd)) return { kind: "legacy-blocked" };
2716
+ return opt.surface === "cli" ? { kind: "single" } : { kind: "not-configured" };
2717
+ }
2718
+ function requireProfiles(mode, what) {
2719
+ if (mode.kind === "profiles") return mode.connections;
2720
+ if (mode.kind === "legacy-blocked") throw legacyBlockedError();
2721
+ if (mode.kind === "not-configured") throw new ConnectionError("kintone の接続のファイルが設定されていない(ksql.config.json と同じ形のファイルを設定に選ぶ)", "not-configured");
2722
+ throw new ConnectionError(`${what} は kintone の接続のファイル(FORMULA_KINTONE_CONFIG)があるときだけ使える`, "not-configured");
2723
+ }
2724
+ function folderNameOf(appId, appName) {
2725
+ const name = safeFileName(appName, "");
2726
+ return name ? `${appId}-${name}` : String(appId);
2727
+ }
2728
+ function profileDirOf(cwd, profile) {
2729
+ if (!PROFILE_NAME_RE.test(profile)) throw new WorkspaceError(`profile の名前が使えない: ${profile.slice(0, 40)}`);
2730
+ return path9.join(cwd, KINTONE_ROOT, profile);
2731
+ }
2732
+ function assertInsideWorkspace(cwd, p) {
2733
+ const rel2 = path9.relative(cwd, p);
2734
+ const root = realResolve(".", cwd);
2735
+ if (!isInside(realResolve(rel2, cwd), root)) throw new PathError(`${rel2} の実体が作業フォルダーの外を指している(symlink など)`);
2736
+ }
2737
+ function findAppDir(cwd, profile, appId) {
2738
+ const dir = profileDirOf(cwd, profile);
2739
+ if (!existsSync4(dir)) return null;
2740
+ assertInsideWorkspace(cwd, path9.join(cwd, KINTONE_ROOT));
2741
+ assertInsideWorkspace(cwd, dir);
2742
+ const hits = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && (d.name === String(appId) || d.name.startsWith(`${appId}-`))).map((d) => d.name);
2743
+ if (hits.length > 1) throw new WorkspaceError(`アプリ ${appId} のフォルダーが 2 つある: ${hits.join(", ")}(kintone/${profile}/ の下を 1 つにする)`);
2744
+ return hits.length ? path9.join(dir, hits[0]) : null;
2745
+ }
2746
+ function appDirFor(cwd, profile, appId, appName) {
2747
+ return findAppDir(cwd, profile, appId) ?? path9.join(profileDirOf(cwd, profile), folderNameOf(appId, appName));
2748
+ }
2749
+ function appFolderOfFile(cwd, file) {
2750
+ const rel2 = path9.relative(path9.join(cwd, KINTONE_ROOT), path9.resolve(cwd, file)).replace(/\\/g, "/");
2751
+ if (!rel2 || rel2.startsWith("..") || path9.isAbsolute(rel2)) return null;
2752
+ const [profile, folder, ...rest] = rel2.split("/");
2753
+ if (!profile || !folder || rest.length === 0 || !PROFILE_NAME_RE.test(profile)) return null;
2754
+ const m = folder.match(/^([1-9]\d*)(?:-|$)/);
2755
+ if (!m) return null;
2756
+ return { dir: path9.join(cwd, KINTONE_ROOT, profile, folder), profile, appId: Number(m[1]) };
2757
+ }
2758
+ var pad2 = (n) => String(n).padStart(2, "0");
2759
+ function snapshotNameOf(appId, now = /* @__PURE__ */ new Date()) {
2760
+ return `${SNAPSHOT_PREFIX}-app${appId}-${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}.json`;
2761
+ }
2762
+ function editNameOf(snapshotFile) {
2763
+ return snapshotFile.replace(/\.json$/, "-edit.json");
2764
+ }
2765
+ function markOf(def, appId) {
2766
+ return { schemaVersion: 1, profile: def.profile, origin: def.baseUrl, guestSpaceId: def.guestSpaceId, appId };
2767
+ }
2768
+ function readAppMark(dir) {
2769
+ const file = path9.join(dir, APP_MARKER);
2770
+ let st;
2771
+ try {
2772
+ st = lstatSync2(file);
2773
+ } catch {
2774
+ return { state: "missing" };
2775
+ }
2776
+ if (!st.isFile() || st.size > MARKER_MAX_BYTES) return { state: "invalid" };
2777
+ try {
2778
+ const v = JSON.parse(readFileSync7(file, "utf8"));
2779
+ const ok = v && v.schemaVersion === 1 && typeof v.profile === "string" && typeof v.origin === "string" && (v.guestSpaceId === null || typeof v.guestSpaceId === "number" && Number.isSafeInteger(v.guestSpaceId)) && typeof v.appId === "number" && Number.isSafeInteger(v.appId);
2780
+ return ok ? { state: "ok", mark: v } : { state: "invalid" };
2781
+ } catch {
2782
+ return { state: "invalid" };
2783
+ }
2784
+ }
2785
+ function assertAppMark(dir, def, appId) {
2786
+ const shownDir = `kintone/${def.profile}/${path9.basename(dir)}`;
2787
+ const r = readAppMark(dir);
2788
+ if (r.state === "missing") {
2789
+ throw new ConnectionError(`${shownDir} に印(${APP_MARKER})が無い。tools が作ったフォルダーでないので使わない(名前を変えて退け、fields か pull で作り直す。中のファイルは後で移す)`, "app-marker-missing");
2790
+ }
2791
+ if (r.state === "invalid") throw new ConnectionError(`${shownDir} の印(${APP_MARKER})が壊れている。名前を変えて退け、fields か pull で作り直す`, "app-marker-invalid");
2792
+ const m = r.mark;
2793
+ if (m.appId !== appId || !sameIdentity({ profile: m.profile, baseUrl: m.origin, host: "", guestSpaceId: m.guestSpaceId }, { profile: def.profile, baseUrl: def.baseUrl, host: "", guestSpaceId: def.guestSpaceId })) {
2794
+ throw new ConnectionError(`${shownDir} は別の接続(profile・接続先・ゲストスペース・アプリのどれかが違う)のフォルダー。profile の接続を変えたなら、名前を変えて退け、fields か pull で作り直す`, "app-identity-conflict");
2795
+ }
2796
+ }
2797
+ function ensureAppFolder(dir, def, appId) {
2798
+ if (existsSync4(dir)) {
2799
+ assertAppMark(dir, def, appId);
2800
+ return;
2801
+ }
2802
+ const parent = path9.dirname(dir);
2803
+ mkdirSync(parent, { recursive: true });
2804
+ const tmp = path9.join(parent, `.${path9.basename(dir)}.${randomBytes2(6).toString("hex")}.tmp`);
2805
+ mkdirSync(tmp);
2806
+ try {
2807
+ writeFileSync(path9.join(tmp, APP_MARKER), JSON.stringify(markOf(def, appId), null, 2) + "\n", { encoding: "utf8", flag: "wx" });
2808
+ if (existsSync4(dir)) {
2809
+ rmSync(tmp, { recursive: true, force: true });
2810
+ } else {
2811
+ try {
2812
+ renameSync(tmp, dir);
2813
+ } catch (e) {
2814
+ const code = e.code;
2815
+ if (code !== "EEXIST" && code !== "ENOTEMPTY" && code !== "EPERM") throw e;
2816
+ rmSync(tmp, { recursive: true, force: true });
2817
+ }
2818
+ }
2819
+ } catch (e) {
2820
+ rmSync(tmp, { recursive: true, force: true });
2821
+ throw e;
2822
+ }
2823
+ assertAppMark(dir, def, appId);
2824
+ }
2825
+ function assertUsableInKintone(cwd, file, mode) {
2826
+ const abs = path9.resolve(cwd, file);
2827
+ if (!isInside(abs, path9.join(cwd, KINTONE_ROOT))) return;
2828
+ if (mode.kind !== "profiles") {
2829
+ if (mode.kind === "legacy-blocked") throw legacyBlockedError();
2830
+ throw new PermissionError("kintone/ の下は、kintone の接続のファイルがあるときだけ使う(今の形では settings/ などを使う)");
2831
+ }
2832
+ const folder = appFolderOfFile(cwd, abs);
2833
+ if (!folder) throw new PermissionError(`アプリのフォルダー(kintone/<profile>/<番号>-…/)の中でない: ${path9.relative(cwd, abs)}`);
2834
+ const def = mode.connections.set.profiles.get(folder.profile);
2835
+ if (!def) throw new PermissionError(`profile「${folder.profile}」は接続のファイル(${mode.connections.set.fileName})に無い(使われていないフォルダー)`);
2836
+ assertAppMark(folder.dir, def, folder.appId);
2837
+ }
2838
+ var stampOf = (name) => {
2839
+ const m = name.match(/-(\d{8})-(\d{6})(?:-edit)?\.json$/);
2840
+ return m ? `${m[1]}${m[2]}` : "";
2841
+ };
2842
+ var newestFirst = (a, b) => stampOf(b).localeCompare(stampOf(a)) || b.localeCompare(a);
2843
+ function listAppFolder(dir, cwd) {
2844
+ if (cwd !== void 0) {
2845
+ for (const d of [dir, path9.join(dir, "records")]) if (existsSync4(d)) assertInsideWorkspace(cwd, d);
2846
+ }
2847
+ const files = existsSync4(dir) ? readdirSync(dir, { withFileTypes: true }) : [];
2848
+ const names = files.filter((d) => d.isFile() && d.name.endsWith(".json") && !d.name.startsWith(".")).map((d) => d.name);
2849
+ const sub = (name, filter) => {
2850
+ const p = path9.join(dir, name);
2851
+ return existsSync4(p) ? readdirSync(p).filter((n) => !n.startsWith(".") && filter(n)).sort() : [];
2852
+ };
2853
+ return {
2854
+ dir,
2855
+ hasFields: names.includes("fields.json"),
2856
+ snapshots: names.filter((n) => SNAPSHOT_RE.test(n)).sort(newestFirst),
2857
+ edits: names.filter((n) => EDIT_RE.test(n)).sort(newestFirst),
2858
+ others: names.filter((n) => n !== "fields.json" && !SNAPSHOT_RE.test(n) && !EDIT_RE.test(n)).sort(),
2859
+ records: sub("records", (n) => n.endsWith(".json"))
2860
+ };
2861
+ }
2862
+ function appFoldersOf(cwd, def) {
2863
+ const dir = profileDirOf(cwd, def.profile);
2864
+ if (!existsSync4(dir)) return [];
2865
+ assertInsideWorkspace(cwd, path9.join(cwd, KINTONE_ROOT));
2866
+ assertInsideWorkspace(cwd, dir);
2867
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => {
2868
+ const m = d.name.match(/^([1-9]\d*)(?:-|$)/);
2869
+ const appId = m ? Number(m[1]) : null;
2870
+ const r = readAppMark(path9.join(dir, d.name));
2871
+ if (r.state !== "ok") return { name: d.name, appId, mark: r.state };
2872
+ const ok = appId !== null && r.mark.appId === appId && r.mark.profile === def.profile && r.mark.origin === def.baseUrl && r.mark.guestSpaceId === def.guestSpaceId;
2873
+ return { name: d.name, appId, mark: ok ? "ok" : "conflict" };
2874
+ }).sort((a, b) => a.name.localeCompare(b.name));
2875
+ }
2876
+ function unusedProfileDirs(cwd, set) {
2877
+ const root = path9.join(cwd, KINTONE_ROOT);
2878
+ if (!existsSync4(root)) return [];
2879
+ assertInsideWorkspace(cwd, root);
2880
+ return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !set.profiles.has(d.name)).map((d) => d.name).sort();
2881
+ }
2882
+
2883
+ // src/permission.ts
2884
+ var ALLOWED_CLASSES = {
2885
+ settings: ["edit", "newsettings"],
2886
+ snapshot: ["snapshot"],
2887
+ fields: ["fields"],
2888
+ record: ["record"]
2889
+ };
2890
+ var OP_LABEL = {
2891
+ settings: "設定(-edit.json と新しい設定)",
2892
+ snapshot: "新しい名前のダウンロード / pull",
2893
+ fields: "fields.json",
2894
+ record: "records/ の下"
2895
+ };
2896
+ var CREATES_FOLDER = ["fields", "snapshot"];
2897
+ function classOfAppPath(relInFolder) {
2898
+ const rel2 = relInFolder.replace(/\\/g, "/");
2899
+ if (rel2.split("/").some((seg) => seg.startsWith("."))) return "other";
2900
+ if (rel2.startsWith("records/")) return rel2.toLowerCase().endsWith(".json") && rel2.split("/").length === 2 ? "record" : "other";
2901
+ if (rel2.includes("/")) return "other";
2902
+ if (rel2 === "fields.json") return "fields";
2903
+ if (SNAPSHOT_RE.test(rel2)) return "snapshot";
2904
+ if (EDIT_RE.test(rel2)) return "edit";
2905
+ if (rel2.toLowerCase().endsWith(".json")) return "newsettings";
2906
+ return "other";
2907
+ }
2908
+ function assertChangeAllowed(root, file, op, mode) {
2909
+ const abs = path10.resolve(root, file);
2910
+ if (!isInside(abs, path10.join(root, KINTONE_ROOT))) return;
2911
+ const rel2 = path10.relative(root, abs);
2912
+ if (mode.kind !== "profiles") {
2913
+ if (mode.kind === "legacy-blocked") throw legacyBlockedError();
2914
+ throw new PermissionError("kintone/ の下は、kintone の接続のファイルがあるときだけ変える(今の形では settings/ などを使う)");
2915
+ }
2916
+ const folder = appFolderOfFile(root, abs);
2917
+ if (!folder) throw new PermissionError(`アプリのフォルダー(kintone/<profile>/<番号>-…/)の中でない: ${rel2}`);
2918
+ const { set, reload } = mode.connections;
2919
+ const def = set.profiles.get(folder.profile);
2920
+ if (!def) throw new PermissionError(`profile「${folder.profile}」は接続のファイル(${set.fileName})に無い(使われていないフォルダーは変えない)`);
2921
+ const cls = classOfAppPath(path10.relative(folder.dir, abs));
2922
+ if (cls === "snapshot" && existsSync5(abs)) throw new PermissionError(`ダウンロード / pull のファイルは書き換えない: ${rel2}。edit で -edit.json に写して直す`);
2923
+ if (!ALLOWED_CLASSES[op].includes(cls)) throw new PermissionError(`この操作でアプリのフォルダーに書けるのは ${OP_LABEL[op]} だけ: ${rel2}`);
2924
+ if (existsSync5(folder.dir)) assertAppMark(folder.dir, def, folder.appId);
2925
+ else if (!CREATES_FOLDER.includes(op)) throw new PermissionError(`アプリのフォルダーが無い: ${path10.relative(root, folder.dir)}(先に fields か pull で作る)`);
2926
+ assertSameConnection({ set, def }, reload, folder.appId);
2927
+ }
2928
+
2929
+ // src/commit-file.ts
2930
+ import { randomBytes as randomBytes3 } from "node:crypto";
2931
+ import { constants, copyFileSync, linkSync, lstatSync as lstatSync3, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
2932
+ import path11 from "node:path";
2933
+ var LOCK_STALE_MS = 6e4;
2934
+ var FileExistsError = class extends Error {
2935
+ };
2936
+ var LockBusyError = class extends Error {
2937
+ };
2938
+ var errCode = (e) => e?.code;
2939
+ var rand = () => randomBytes3(6).toString("hex");
2940
+ function sleepSync(ms) {
2941
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
2942
+ }
2943
+ function withRetry(fn, tries = 3) {
2944
+ for (let i = 1; ; i++) {
2945
+ try {
2946
+ return fn();
2947
+ } catch (e) {
2948
+ const code = errCode(e);
2949
+ if (i < tries && (code === "EPERM" || code === "EBUSY")) {
2950
+ sleepSync(20 * i);
2951
+ continue;
2952
+ }
2953
+ throw e;
2954
+ }
2955
+ }
2956
+ }
2957
+ function cleanupAll(steps, tries = 3) {
2958
+ const errors = [];
2959
+ for (const step of steps) {
2960
+ try {
2961
+ withRetry(step, tries);
2962
+ } catch (e) {
2963
+ errors.push(e instanceof Error ? e.message : String(e));
2964
+ }
2965
+ }
2966
+ return errors;
2967
+ }
2968
+ function tempPathFor(target) {
2969
+ return path11.join(path11.dirname(target), `.${path11.basename(target)}.${rand()}.tmp`);
2970
+ }
2971
+ function placeNew(tmp, target, link = linkSync) {
2972
+ try {
2973
+ link(tmp, target);
2974
+ return;
2975
+ } catch (e) {
2976
+ const code = errCode(e);
2977
+ if (code === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path11.basename(target)}`);
2978
+ if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "ENOSYS") throw e;
2979
+ }
2980
+ try {
2981
+ copyFileSync(tmp, target, constants.COPYFILE_EXCL);
2982
+ } catch (e) {
2983
+ if (errCode(e) === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path11.basename(target)}`);
2984
+ throw e;
2985
+ }
2986
+ }
2987
+ function writeNewFile(target, text, recheck = () => {
2988
+ }) {
2989
+ const tmp = tempPathFor(target);
2990
+ let placed = false;
2991
+ let failure;
2992
+ try {
2993
+ recheck();
2994
+ writeFileSync2(tmp, text, { encoding: "utf8", flag: "wx" });
2995
+ recheck();
2996
+ placeNew(tmp, target);
2997
+ placed = true;
2998
+ } catch (e) {
2999
+ failure = e;
3000
+ }
3001
+ const cleanup = cleanupAll([() => rmSync2(tmp, { force: true })]);
3002
+ if (!placed) throw failure;
3003
+ return { cleanup };
3004
+ }
3005
+ function lockPathFor(target) {
3006
+ return path11.join(path11.dirname(target), `.${path11.basename(target)}.formula-lock`);
3007
+ }
3008
+ function lstatOrNull(p) {
3009
+ try {
3010
+ return lstatSync3(p);
3011
+ } catch (e) {
3012
+ if (errCode(e) === "ENOENT") return null;
3013
+ throw e;
3014
+ }
3015
+ }
3016
+ function acquireLock(target, staleMs = LOCK_STALE_MS, hooks = {}) {
3017
+ const file = lockPathFor(target);
3018
+ const token = randomBytes3(16).toString("hex");
3019
+ const busy = (why = "同じファイルへの別の保存が進行中。少し待ってからやり直す") => new LockBusyError(why);
3020
+ const tryCreate = () => {
3021
+ try {
3022
+ writeFileSync2(file, token, { flag: "wx" });
3023
+ return true;
3024
+ } catch (e) {
3025
+ if (errCode(e) === "EEXIST") return false;
3026
+ throw e;
3027
+ }
3028
+ };
3029
+ const isStale = (st) => Date.now() - st.mtimeMs > staleMs;
3030
+ if (!tryCreate()) {
3031
+ const st = lstatOrNull(file);
3032
+ if (st && !st.isFile()) throw busy(`ロックのファイルが普通のファイルでない: ${path11.basename(file)}(利用者が確かめて消す)`);
3033
+ if (st && !isStale(st)) throw busy();
3034
+ if (st) {
3035
+ const aside = `${file}.${rand()}.stale`;
3036
+ hooks.beforeClaim?.();
3037
+ try {
3038
+ withRetry(() => renameSync2(file, aside));
3039
+ } catch (e) {
3040
+ if (errCode(e) !== "ENOENT") throw e;
3041
+ }
3042
+ const moved = lstatOrNull(aside);
3043
+ if (moved) {
3044
+ if (!moved.isFile()) throw busy(`ロックのファイルが普通のファイルでない: ${path11.basename(aside)}(利用者が確かめて消す)`);
3045
+ if (!isStale(moved)) {
3046
+ hooks.beforeRestore?.();
3047
+ try {
3048
+ withRetry(() => placeNew(aside, file));
3049
+ } catch (e) {
3050
+ if (!(e instanceof FileExistsError)) throw busy(`新しいロックを戻せなかった(${errCode(e) ?? e.message}): ${path11.basename(aside)} を確かめて ${path11.basename(file)} に戻すか消す`);
3051
+ }
3052
+ cleanupAll([() => rmSync2(aside, { force: true })]);
3053
+ throw busy();
3054
+ }
3055
+ cleanupAll([() => rmSync2(aside, { force: true })]);
3056
+ }
3057
+ }
3058
+ if (!tryCreate()) throw busy();
3059
+ }
3060
+ const owned = () => {
3061
+ try {
3062
+ const st = lstatSync3(file);
3063
+ return st.isFile() && readFileSync8(file, "utf8") === token;
3064
+ } catch {
3065
+ return false;
3066
+ }
3067
+ };
3068
+ return {
3069
+ owned,
3070
+ release: () => {
3071
+ if (!owned()) return false;
3072
+ withRetry(() => rmSync2(file, { force: true }));
3073
+ return true;
3074
+ }
3075
+ };
3076
+ }
3077
+
3078
+ // src/commands/take.ts
3079
+ function removeSource(src, warnings, rel2) {
3080
+ try {
3081
+ withRetry(() => unlinkSync(src));
3082
+ return true;
3083
+ } catch (e) {
3084
+ warnings.push(`${rel2} は行き先に置いたが、inbox から消せなかった(${e.code ?? "理由は不明"})。消してよい`);
3085
+ return false;
3086
+ }
3087
+ }
3088
+ function readInbox(src) {
3089
+ let bytes;
3090
+ let fd;
3091
+ try {
3092
+ fd = openSync2(src, "r");
3093
+ if (!fstatSync2(fd).isFile()) return { reason: "普通のファイルでない" };
3094
+ const buf = Buffer.alloc(MAX_INPUT_BYTES + 1);
3095
+ let off = 0;
3096
+ for (; ; ) {
3097
+ const n = readSync2(fd, buf, off, buf.length - off, off);
3098
+ if (n === 0) break;
3099
+ off += n;
3100
+ if (off > MAX_INPUT_BYTES) return { reason: `大きすぎる(上限 ${MAX_INPUT_BYTES.toLocaleString()} バイト)` };
3101
+ }
3102
+ bytes = buf.subarray(0, off);
3103
+ } catch (e) {
3104
+ return { reason: `読めない(${e.code ?? "理由は不明"})` };
3105
+ } finally {
3106
+ if (fd !== void 0) closeSync2(fd);
3107
+ }
3108
+ const text = bytes.toString("utf8");
3109
+ let data;
3110
+ try {
3111
+ data = JSON.parse(text);
3112
+ } catch (e) {
3113
+ return { reason: `JSON として読めない${jsonErrorWhere(e)}(中身は返さない)` };
3114
+ }
3115
+ if (!data || typeof data !== "object" || Array.isArray(data)) return { reason: "JSON の最上位がオブジェクトでない" };
3116
+ return { text, data };
3117
+ }
3118
+ function takeInbox(cwdIn, mode, profile) {
3119
+ const connections = requireProfiles(mode, "take");
3120
+ const def = pickProfile(connections.set, profile);
3121
+ const cwd = realResolve(".", cwdIn);
3122
+ const result = { moved: [], skipped: [], warnings: [] };
3123
+ const inbox = path12.join(cwd, INBOX);
3124
+ if (!existsSync6(inbox)) return result;
3125
+ assertInsideWorkspace(cwd, inbox);
3126
+ const candidates = [];
3127
+ for (const name of readdirSync2(inbox).filter((n) => n.toLowerCase().endsWith(".json")).sort()) {
3128
+ const rel2 = `${INBOX}/${name}`;
3129
+ const src = resolveRead(rel2, cwd);
3130
+ const read = readInbox(src);
3131
+ if ("reason" in read) {
3132
+ result.skipped.push({ file: rel2, reason: read.reason });
3133
+ continue;
3134
+ }
3135
+ const data = read.data;
3136
+ if (data.pluginID !== FORMULA_CONFIG_PLUGIN_ID) {
3137
+ result.skipped.push({ file: rel2, reason: "計算式プラグインの設定のファイルではない(pluginID が違う)" });
3138
+ continue;
3139
+ }
3140
+ const appId = data.appId;
3141
+ if (typeof appId !== "number" || !Number.isSafeInteger(appId) || appId <= 0) {
3142
+ result.skipped.push({ file: rel2, reason: "封筒の appId が無い" });
3143
+ continue;
3144
+ }
3145
+ if (!SNAPSHOT_RE.test(name)) {
3146
+ result.skipped.push({ file: rel2, reason: "ダウンロードの名前(rex0220-formula-app<番号>-<日時>.json)ではない。設定画面のダウンロードの名前のまま inbox/ に置く" });
3147
+ continue;
3148
+ }
3149
+ const m = name.match(/-app(\d+)-/);
3150
+ if (m && Number(m[1]) !== appId) {
3151
+ result.skipped.push({ file: rel2, reason: `ファイル名のアプリ ${m[1]} と封筒の appId ${appId} が違う` });
3152
+ continue;
3153
+ }
3154
+ candidates.push({ rel: rel2, src, name, appId, appName: typeof data.appName === "string" ? data.appName : "", text: read.text });
3155
+ }
3156
+ const dirByApp = /* @__PURE__ */ new Map();
3157
+ for (const appId of new Set(candidates.map((c) => c.appId))) {
3158
+ const newest = candidates.filter((c) => c.appId === appId).reduce((a, b) => b.name > a.name ? b : a);
3159
+ dirByApp.set(appId, appDirFor(cwd, def.profile, appId, newest.appName));
3160
+ }
3161
+ const plans = candidates.map((c) => {
3162
+ const dir = dirByApp.get(c.appId);
3163
+ return { ...c, dir, dest: resolveWrite(path12.join(dir, c.name), WRITE_ROOTS.kintone, cwd) };
3164
+ });
3165
+ const checked = /* @__PURE__ */ new Set();
3166
+ for (const p of plans) {
3167
+ if (checked.has(p.dir) || !existsSync6(p.dir)) continue;
3168
+ assertAppMark(p.dir, def, p.appId);
3169
+ checked.add(p.dir);
3170
+ }
3171
+ for (const p of plans) {
3172
+ try {
3173
+ if (existsSync6(p.dest)) {
3174
+ if (readFileSync9(p.dest, "utf8") !== p.text) {
3175
+ result.skipped.push({ file: p.rel, reason: `行き先に同じ名前の別の中身がある: ${path12.relative(cwd, p.dest)}` });
3176
+ continue;
3177
+ }
3178
+ assertAppMark(p.dir, def, p.appId);
3179
+ assertSameConnection({ set: connections.set, def }, connections.reload, p.appId);
3180
+ const removed2 = removeSource(p.src, result.warnings, p.rel);
3181
+ result.moved.push({ file: p.rel, to: path12.relative(cwd, p.dest), same: true, ...removed2 ? {} : { leftInInbox: true } });
3182
+ continue;
3183
+ }
3184
+ if (!existsSync6(p.dir)) {
3185
+ const found = findAppDir(cwd, def.profile, p.appId);
3186
+ if (found !== null && found !== p.dir) {
3187
+ result.skipped.push({ file: p.rel, reason: `同じアプリのフォルダーが移す途中で別の名前で作られた: ${path12.relative(cwd, found)}。もう一度 take する` });
3188
+ continue;
3189
+ }
3190
+ }
3191
+ assertChangeAllowed(cwd, p.dest, "snapshot", mode);
3192
+ ensureAppFolder(p.dir, def, p.appId);
3193
+ const { cleanup } = writeNewFile(p.dest, p.text, () => {
3194
+ if (resolveWrite(path12.join(p.dir, p.name), WRITE_ROOTS.kintone, cwd) !== p.dest) throw new PermissionError(`書く先のフォルダーが途中で変わった(symlink など): ${path12.relative(cwd, p.dest)}`);
3195
+ assertChangeAllowed(cwd, p.dest, "snapshot", mode);
3196
+ });
3197
+ for (const c of cleanup) result.warnings.push(`${p.rel} は行き先に置いたが、一時ファイルを消せなかった(${c})。${path12.relative(cwd, path12.dirname(p.dest))} の .${p.name}.*.tmp を消してよい`);
3198
+ } catch (e) {
3199
+ if (e instanceof ConnectionError) {
3200
+ throw new ConnectionError(`${e.message}(この呼び出しで移したのは ${result.moved.length} 件。inbox に残ったものは、直した後にもう一度 take する)`, e.code);
3201
+ }
3202
+ if (e instanceof PermissionError) {
3203
+ result.skipped.push({ file: p.rel, reason: e.message });
3204
+ continue;
3205
+ }
3206
+ if (e instanceof FileExistsError) {
3207
+ result.skipped.push({ file: p.rel, reason: `行き先に同じ名前のファイルができた(移す途中で): ${path12.relative(cwd, p.dest)}。もう一度 take する` });
3208
+ continue;
3209
+ }
3210
+ throw e;
3211
+ }
3212
+ const removed = removeSource(p.src, result.warnings, p.rel);
3213
+ result.moved.push({ file: p.rel, to: path12.relative(cwd, p.dest), same: false, ...removed ? {} : { leftInInbox: true } });
3214
+ }
3215
+ return result;
3216
+ }
3217
+
3218
+ // src/commands/save.ts
3219
+ import { createHash as createHash4 } from "node:crypto";
3220
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
3221
+ import path14 from "node:path";
3222
+
2252
3223
  // src/worker/run.ts
2253
- import { existsSync as existsSync4 } from "node:fs";
3224
+ import { existsSync as existsSync7 } from "node:fs";
2254
3225
  import { Worker } from "node:worker_threads";
2255
3226
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2256
- import path8 from "node:path";
3227
+ import path13 from "node:path";
2257
3228
 
2258
3229
  // src/worker/eval-run.ts
3230
+ function classifyEvalError(e) {
3231
+ const message = String(e?.message ?? e);
3232
+ if (e instanceof InputError) return { kind: "input", name: "InputError", message };
3233
+ if (e instanceof PluginZipError) return { kind: "zip", name: "PluginZipError", message, code: e.code };
3234
+ if (e instanceof UsersDataError) return { kind: "users", name: "UsersDataError", message };
3235
+ if (e instanceof PolicyError) return { kind: "policy", name: "PolicyError", message };
3236
+ return { kind: "other", name: builtinErrorName(e), message };
3237
+ }
3238
+ function builtinErrorName(e) {
3239
+ if (e instanceof TypeError) return "TypeError";
3240
+ if (e instanceof RangeError) return "RangeError";
3241
+ if (e instanceof ReferenceError) return "ReferenceError";
3242
+ if (e instanceof SyntaxError) return "SyntaxError";
3243
+ if (e instanceof URIError) return "URIError";
3244
+ return "Error";
3245
+ }
2259
3246
  async function runEvalJob(job) {
2260
- const engine = await loadEngine({ pluginZip: job.pluginZip, devPluginDir: job.devPluginDir, mode: job.mode });
3247
+ const engine = await loadEngine({ pluginZip: job.pluginZip, devPluginDir: job.devPluginDir, mode: job.mode, vendorDir: job.vendorDir });
2261
3248
  if (job.baseUrl) engine.setContext({ baseUrl: job.baseUrl });
2262
3249
  const usersInfo = buildUsersInfo(job.usersSource);
2263
3250
  const usersLabel = job.usersOrigin.kind === "file" ? job.usersOrigin.file : "既定のテストデータ(架空)";
@@ -2274,7 +3261,16 @@ async function runEvalJob(job) {
2274
3261
  usersOrigin: job.usersOrigin
2275
3262
  });
2276
3263
  const outcome = {
2277
- normalize: { summary: res.summary, hasErrors: res.findings.hasErrors, warnings: res.findings.count("warning"), output: res.output, checkDiffs: res.checkDiffs },
3264
+ normalize: {
3265
+ summary: res.summary,
3266
+ hasErrors: res.findings.hasErrors,
3267
+ warnings: res.findings.count("warning"),
3268
+ findings: [...res.findings.items],
3269
+ output: res.output,
3270
+ checkDiffs: res.checkDiffs,
3271
+ appId: res.appId,
3272
+ size: res.size
3273
+ },
2278
3274
  engineWarnings: [...engine.warnings]
2279
3275
  };
2280
3276
  if (job.kind === "calc" && job.calc && !res.findings.hasErrors) {
@@ -2292,7 +3288,7 @@ async function runEvalJob(job) {
2292
3288
  usersInfo,
2293
3289
  usersOrigin: job.usersOrigin
2294
3290
  });
2295
- outcome.calc = { text: r.text, failed: r.records.some((x) => x.errors.length > 0 || x.failed) };
3291
+ outcome.calc = { text: r.text, failed: r.records.some((x) => x.errors.length > 0 || x.failed), records: r.records, contextLine: r.contextLine, notice: r.notice, summary: r.summary };
2296
3292
  }
2297
3293
  return outcome;
2298
3294
  }
@@ -2305,79 +3301,388 @@ var EvalTimeoutError = class extends Error {
2305
3301
  var EvalError = class extends Error {
2306
3302
  innerName;
2307
3303
  code;
2308
- constructor(message, innerName, code) {
3304
+ kind;
3305
+ constructor(message, innerName, code, kind = "other") {
2309
3306
  super(message);
2310
3307
  this.innerName = innerName;
2311
3308
  this.code = code;
3309
+ this.kind = kind;
2312
3310
  }
2313
3311
  };
2314
3312
  function workerEntry() {
2315
3313
  const here2 = fileURLToPath2(import.meta.url);
2316
- const dir = path8.dirname(here2);
2317
- const built = path8.join(dir, "eval-worker.mjs");
2318
- if (here2.endsWith(".mjs") && existsSync4(built)) return built;
2319
- const src = path8.join(dir, "eval-worker.ts");
2320
- if (existsSync4(src)) return src;
3314
+ const dir = path13.dirname(here2);
3315
+ const built = path13.join(dir, "eval-worker.mjs");
3316
+ if (here2.endsWith(".mjs") && existsSync7(built)) return built;
3317
+ const src = path13.join(dir, "eval-worker.ts");
3318
+ if (existsSync7(src)) return src;
2321
3319
  throw new Error(`Worker の入口が無い: ${built}`);
2322
3320
  }
2323
3321
  async function runEval(job, opt = {}) {
2324
3322
  const timeoutSec = Math.min(Math.max(opt.timeoutSec ?? DEFAULT_TIMEOUT_SEC, 1), MAX_TIMEOUT_SEC);
2325
- if (opt.inline) return runEvalJob(job);
2326
- const entry = workerEntry();
3323
+ if (opt.inline) {
3324
+ try {
3325
+ return await runEvalJob(job);
3326
+ } catch (e) {
3327
+ const c = classifyEvalError(e);
3328
+ throw new EvalError(c.message, c.name, c.code, c.kind);
3329
+ }
3330
+ }
3331
+ let entry;
3332
+ if (opt.workerFile !== void 0) {
3333
+ if (!path13.isAbsolute(opt.workerFile) || !existsSync7(opt.workerFile)) throw new Error(`Worker の入口が無い(絶対パスで指定する): ${opt.workerFile}`);
3334
+ entry = opt.workerFile;
3335
+ } else entry = workerEntry();
3336
+ const capture = opt.captureOutput;
2327
3337
  return new Promise((resolve, reject) => {
2328
- const worker = new Worker(entry, { workerData: job, stdout: false, stderr: false });
3338
+ const worker = new Worker(entry, { workerData: job, stdout: capture !== void 0, stderr: capture !== void 0 });
3339
+ if (capture) {
3340
+ worker.stdout.on("data", (d) => capture(String(d), "stdout"));
3341
+ worker.stderr.on("data", (d) => capture(String(d), "stderr"));
3342
+ }
2329
3343
  let settled = false;
2330
- const finish = (fn) => {
3344
+ const finish2 = (fn) => {
2331
3345
  if (settled) return;
2332
3346
  settled = true;
2333
3347
  clearTimeout(timer);
2334
3348
  fn();
2335
3349
  };
2336
3350
  const timer = setTimeout(() => {
2337
- finish(() => {
3351
+ finish2(() => {
2338
3352
  void worker.terminate();
2339
3353
  reject(new EvalTimeoutError(`評価が ${timeoutSec} 秒を超えたので止めた(式が重いか無限に近い繰り返し。--timeout <秒> で上げられる。上限 ${MAX_TIMEOUT_SEC})`));
2340
3354
  });
2341
3355
  }, timeoutSec * 1e3);
2342
3356
  worker.on("message", (m) => {
2343
- finish(() => {
3357
+ finish2(() => {
2344
3358
  if (m.ok) resolve(m.outcome);
2345
- else reject(new EvalError(m.error.message, m.error.name, m.error.code));
3359
+ else reject(new EvalError(m.error.message, m.error.name, m.error.code, m.error.kind ?? "other"));
2346
3360
  });
2347
3361
  });
2348
- worker.on("error", (e) => finish(() => reject(e)));
2349
- worker.on("exit", (code) => finish(() => reject(new Error(`評価の Worker が結果を返さずに終わった(exit ${code})`))));
3362
+ worker.on("error", (e) => finish2(() => reject(new EvalError(e?.message ?? String(e), builtinErrorName(e), void 0, "other"))));
3363
+ worker.on("exit", (code) => finish2(() => reject(new Error(`評価の Worker が結果を返さずに終わった(exit ${code})`))));
2350
3364
  });
2351
3365
  }
2352
3366
 
3367
+ // src/commands/save.ts
3368
+ var MAX_SAVE_INPUT_BYTES = 256 * 1024;
3369
+ var MAX_UPDATE_ROWS = 50;
3370
+ var COMMON_KEYS2 = ["pluginEnable", "pluginProductEnv", "toolEnable", "pluginComment", "pluginDescription", "pluginBtnInfo", "pluginListInfo", "authority", "pluginMiscInfo"];
3371
+ function digestOf(text) {
3372
+ return createHash4("sha256").update(text).digest("hex");
3373
+ }
3374
+ var rel = (root, abs) => path14.relative(root, abs).replace(/\\/g, "/");
3375
+ var SaveDenied = class extends Error {
3376
+ };
3377
+ var ConflictError = class extends Error {
3378
+ };
3379
+ var isPlainObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3380
+ function parseInput(text, what) {
3381
+ try {
3382
+ return JSON.parse(text);
3383
+ } catch (e) {
3384
+ throw new InputError(`${what} を JSON として読めない${jsonErrorWhere(e)}(中身は返さない)`);
3385
+ }
3386
+ }
3387
+ function tooLarge(text) {
3388
+ return Buffer.byteLength(text, "utf8") > MAX_SAVE_INPUT_BYTES;
3389
+ }
3390
+ function fieldsFileFor(ctx, target, fields, envelope) {
3391
+ const folder = appFolderOfFile(ctx.root, target);
3392
+ if (folder) {
3393
+ const own = resolveRead(path14.join(folder.dir, "fields.json"), ctx.root);
3394
+ assertUsableInKintone(ctx.root, own, ctx.mode);
3395
+ if (!existsSync8(own)) throw new InputError(`アプリのフォルダーに fields.json が無い: ${rel(ctx.root, own)}(fields コマンドか fml_fields で取る)`);
3396
+ if (fields !== void 0 && resolveRead(fields, ctx.root) !== own) throw new SaveDenied(`アプリのフォルダーの中の設定は、同じフォルダーの fields.json で検査する(fields は渡さないか、${rel(ctx.root, own)} にする)`);
3397
+ return own;
3398
+ }
3399
+ const appId = typeof envelope.appId === "number" && Number.isInteger(envelope.appId) && envelope.appId > 0 ? envelope.appId : void 0;
3400
+ const relFile = fields ?? (appId !== void 0 ? path14.join("fields", `${appId}.json`) : void 0);
3401
+ if (!relFile) throw new InputError("fields(項目定義のファイル)が要る(設定の封筒に appId があれば fields/<appId>.json を既定にする。アプリのフォルダー kintone/<profile>/<番号>-…/ の中なら同じフォルダーの fields.json を使う)");
3402
+ const file = resolveRead(relFile, ctx.root);
3403
+ assertUsableInKintone(ctx.root, file, ctx.mode);
3404
+ if (!existsSync8(file)) throw new InputError(`項目定義のファイルが無い: ${rel(ctx.root, file)}(fields コマンドか fml_fields で取る)`);
3405
+ return file;
3406
+ }
3407
+ async function normalizeText(ctx, target, text, fieldsFile) {
3408
+ const job = {
3409
+ kind: "normalize",
3410
+ pluginZip: ctx.source.pluginZip,
3411
+ devPluginDir: ctx.source.devPluginDir,
3412
+ mode: ctx.source.mode,
3413
+ vendorDir: ctx.source.vendorDir,
3414
+ baseUrl: ctx.baseUrl,
3415
+ settingsText: text,
3416
+ settingsFile: rel(ctx.root, target),
3417
+ fields: readFieldsFile(fieldsFile),
3418
+ usersSource: ctx.usersSource ?? DEFAULT_USERS,
3419
+ usersOrigin: ctx.usersOrigin ?? { kind: "default" },
3420
+ policy: ctx.policy,
3421
+ lang: ctx.lang
3422
+ };
3423
+ return runEval(job, ctx.run);
3424
+ }
3425
+ function commit(ctx, target, text, mode, recheck) {
3426
+ const tmp = tempPathFor(target);
3427
+ let lock;
3428
+ let digest;
3429
+ let failure;
3430
+ const checkTarget = () => {
3431
+ if (resolveWrite(rel(ctx.root, target), WRITE_ROOTS.settings, ctx.root) !== target) throw new SaveDenied("書く先のフォルダーが途中で変わった(symlink など)");
3432
+ assertChangeAllowed(ctx.root, target, "settings", ctx.mode);
3433
+ if (SNAPSHOT_RE.test(path14.basename(target))) throw new SaveDenied("ダウンロード / pull のファイルは書き換えない。edit で -edit.json に写して直す");
3434
+ };
3435
+ try {
3436
+ checkTarget();
3437
+ mkdirSync2(path14.dirname(target), { recursive: true });
3438
+ writeFileSync3(tmp, text, { encoding: "utf8", flag: "wx" });
3439
+ lock = acquireLock(target);
3440
+ checkTarget();
3441
+ recheck();
3442
+ if (!lock.owned()) throw new ConflictError("ロックを失った(保存が 60 秒より長く止まった)。fml_summary で読み直してやり直す");
3443
+ if (mode === "new") withRetry(() => placeNew(tmp, target));
3444
+ else withRetry(() => renameSync3(tmp, target));
3445
+ digest = digestOf(text);
3446
+ ctx.afterPlace?.();
3447
+ } catch (e) {
3448
+ failure = e;
3449
+ }
3450
+ const cleanup = cleanupAll([...lock ? [() => void lock.release()] : [], () => rmSync3(tmp, { force: true })]);
3451
+ if (digest === void 0) throw failure;
3452
+ return { digest, cleanup };
3453
+ }
3454
+ function statusOf(e) {
3455
+ if (e instanceof ConnectionError) return connectionStatusOf(e);
3456
+ if (e instanceof ConflictError || e instanceof FileExistsError || e instanceof LockBusyError) return "conflict";
3457
+ if (e instanceof SaveDenied || e instanceof PermissionError || e instanceof PathError) return "denied";
3458
+ if (e instanceof InputError || e instanceof SyntaxError) return "invalid";
3459
+ if (e instanceof EvalError && e.kind === "input") return "invalid";
3460
+ return "failed";
3461
+ }
3462
+ function connectionStatusOf(e) {
3463
+ switch (e.code) {
3464
+ case "inside-workspace":
3465
+ return "denied";
3466
+ case "unknown-profile":
3467
+ case "guest-mismatch":
3468
+ return "invalid";
3469
+ case "app-marker-missing":
3470
+ case "app-marker-invalid":
3471
+ case "app-identity-conflict":
3472
+ case "connection-changed":
3473
+ return "conflict";
3474
+ default:
3475
+ return "failed";
3476
+ }
3477
+ }
3478
+ function codeOf(e) {
3479
+ if (e instanceof ConnectionError) return e.code;
3480
+ if (e instanceof ConflictError || e instanceof FileExistsError || e instanceof LockBusyError) return "ConflictError";
3481
+ if (e instanceof SaveDenied || e instanceof PermissionError) return "PermissionError";
3482
+ if (e instanceof EvalError) return { input: "InputError", zip: "PluginZipError", users: "UsersDataError", policy: "PolicyError", other: e.innerName }[e.kind];
3483
+ if (e instanceof EvalTimeoutError) return "EvalTimeoutError";
3484
+ if (e instanceof InputError) return "InputError";
3485
+ if (e instanceof PathError) return "PathError";
3486
+ return builtinErrorName(e);
3487
+ }
3488
+ function safeMessageOf(e) {
3489
+ if (e instanceof ConnectionError || e instanceof ConflictError || e instanceof FileExistsError || e instanceof LockBusyError || e instanceof SaveDenied || e instanceof PermissionError || e instanceof PathError || e instanceof InputError || e instanceof EvalTimeoutError) return e.message;
3490
+ if (e instanceof EvalError) return e.kind === "other" ? `評価の途中で想定外の誤り(${e.innerName})。誤りの文は値やパスを含みうるので返さない` : e.message;
3491
+ if (e instanceof SyntaxError) return "JSON の構文の誤り(中身は返さない)";
3492
+ return `想定外の誤り(${builtinErrorName(e)})。誤りの文は値やパスを含みうるので返さない`;
3493
+ }
3494
+ function errorResult(root, target, e, findings = []) {
3495
+ return { status: statusOf(e), code: codeOf(e), path: rel(root, target), findings, message: safeMessageOf(e), ...e instanceof EvalError && e.kind === "zip" && e.code ? { zipCode: e.code } : {} };
3496
+ }
3497
+ function finish(ctx, target, outcome, run, extra = {}) {
3498
+ const findings = outcome.normalize.findings;
3499
+ try {
3500
+ const { digest, cleanup } = run();
3501
+ return {
3502
+ status: "ok",
3503
+ path: rel(ctx.root, target),
3504
+ digest,
3505
+ findings,
3506
+ summary: outcome.normalize.summary,
3507
+ ...extra,
3508
+ ...cleanup.length ? { cleanup, message: `確定したが、一時ファイルかロックを消せなかった(.${path14.basename(target)}.* を消してよい)` } : {}
3509
+ };
3510
+ } catch (e) {
3511
+ return { ...errorResult(ctx.root, target, e, findings), summary: outcome.normalize.summary };
3512
+ }
3513
+ }
3514
+ async function saveNewSettings(ctx, opt) {
3515
+ if (opt.expectedAbsent !== true) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: "新しい設定の保存は expectedAbsent: true が要る(既存のファイルを直すなら fml_update_fields)" };
3516
+ if (tooLarge(opt.content)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `content は ${MAX_SAVE_INPUT_BYTES.toLocaleString()} バイトまで` };
3517
+ let target;
3518
+ try {
3519
+ target = resolveWrite(opt.path, WRITE_ROOTS.settings, ctx.root);
3520
+ const name = path14.basename(target);
3521
+ if (SNAPSHOT_RE.test(name)) throw new SaveDenied("ダウンロード / pull の名前(rex0220-formula-app<番号>-<日時>.json)では新しく保存しない");
3522
+ if (EDIT_RE.test(name)) throw new SaveDenied("-edit.json の名前では新しく保存しない(ダウンロード / pull を直すなら edit(fml_edit)で写してから fml_update_fields)");
3523
+ assertChangeAllowed(ctx.root, target, "settings", ctx.mode);
3524
+ } catch (e) {
3525
+ return errorResult(ctx.root, path14.resolve(ctx.root, opt.path), e);
3526
+ }
3527
+ if (existsSync8(target)) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "同じ名前のファイルがある(新しい設定の保存は上書きしない。直すなら fml_update_fields)" };
3528
+ let outcome;
3529
+ try {
3530
+ const envelope = parseInput(opt.content, "content");
3531
+ if (!isPlainObject2(envelope)) throw new InputError("content の最上位はオブジェクト(封筒形式の設定 JSON)");
3532
+ outcome = await normalizeText(ctx, target, opt.content, fieldsFileFor(ctx, target, opt.fields, envelope));
3533
+ } catch (e) {
3534
+ return errorResult(ctx.root, target, e);
3535
+ }
3536
+ if (outcome.normalize.hasErrors || !outcome.normalize.output) return { status: "invalid", code: "NormalizeError", path: rel(ctx.root, target), findings: outcome.normalize.findings, summary: outcome.normalize.summary, message: "normalize のエラーがあるので保存しない" };
3537
+ const text = JSON.stringify(outcome.normalize.output, null, 2) + "\n";
3538
+ return finish(
3539
+ ctx,
3540
+ target,
3541
+ outcome,
3542
+ () => commit(ctx, target, text, "new", () => {
3543
+ if (existsSync8(target)) throw new ConflictError("確定の直前に同じ名前のファイルができた");
3544
+ })
3545
+ );
3546
+ }
3547
+ function parseRows(text, current) {
3548
+ const v = parseInput(text, "rows");
3549
+ if (!Array.isArray(v)) throw new InputError("rows は行(fieldcode を持つオブジェクト)の配列");
3550
+ if (v.length === 0) throw new InputError("rows が空");
3551
+ if (v.length > MAX_UPDATE_ROWS) throw new InputError(`rows は 1 回に ${MAX_UPDATE_ROWS} 行まで(${v.length} 行)`);
3552
+ const known = new Set(ROW_SOURCE_KEYS);
3553
+ const codes = new Set(current.map((r) => isPlainObject2(r) ? String(r.fieldcode ?? "") : ""));
3554
+ const out = /* @__PURE__ */ new Map();
3555
+ v.forEach((row, i) => {
3556
+ if (!isPlainObject2(row)) throw new InputError(`rows[${i}] はオブジェクト`);
3557
+ const code = row.fieldcode;
3558
+ if (typeof code !== "string" || !code) throw new InputError(`rows[${i}] に fieldcode(文字列)が要る`);
3559
+ const unknown = Object.keys(row).filter((k) => !known.has(k));
3560
+ if (unknown.length) throw new InputError(`rows[${i}](${code})に行の元の値でないキーがある: ${unknown.join(", ")}(使えるのは ${ROW_SOURCE_KEYS.join(", ")}。画面の値(type、fieldlabel など)は normalize が項目定義から補う)`);
3561
+ if (out.has(code)) throw new InputError(`rows の fieldcode が重複: ${code}`);
3562
+ if (!codes.has(code)) throw new InputError(`設定に無い fieldcode: ${code}(項目はアプリの項目定義で決まる。fml_fields で項目定義を取り直し、fml_summary で設定の項目を確かめる)`);
3563
+ out.set(code, row);
3564
+ });
3565
+ return out;
3566
+ }
3567
+ function parseCommon(text) {
3568
+ const v = parseInput(text, "common");
3569
+ if (!isPlainObject2(v)) throw new InputError(`common は共通の設定のキー(${COMMON_KEYS2.join("、")})を持つオブジェクト`);
3570
+ const fixed = Object.keys(v).filter((k) => k === "pluginInfos" || ENVELOPE_KEYS2.includes(k));
3571
+ if (fixed.length) throw new InputError(`common で変えられないキー: ${fixed.join(", ")}(項目の行は rows、封筒(appId など)は変えない)`);
3572
+ const unknown = Object.keys(v).filter((k) => !COMMON_KEYS2.includes(k));
3573
+ if (unknown.length) throw new InputError(`common に共通の設定でないキーがある: ${unknown.join(", ")}(使えるのは ${COMMON_KEYS2.join("、")})`);
3574
+ if (!Object.keys(v).length) throw new InputError("common が空(変えないなら渡さない)");
3575
+ return v;
3576
+ }
3577
+ async function updateFields(ctx, opt) {
3578
+ if (tooLarge(opt.rows)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `rows は ${MAX_SAVE_INPUT_BYTES.toLocaleString()} バイトまで` };
3579
+ if (opt.common !== void 0 && tooLarge(opt.common)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `common は ${MAX_SAVE_INPUT_BYTES.toLocaleString()} バイトまで` };
3580
+ let target;
3581
+ try {
3582
+ target = resolveRead(opt.path, ctx.root);
3583
+ resolveWrite(opt.path, WRITE_ROOTS.settings, ctx.root);
3584
+ if (SNAPSHOT_RE.test(path14.basename(target))) throw new SaveDenied("ダウンロード / pull のファイルは書き換えない。edit(fml_edit)で -edit.json に写して直す");
3585
+ assertUsableInKintone(ctx.root, target, ctx.mode);
3586
+ assertChangeAllowed(ctx.root, target, "settings", ctx.mode);
3587
+ } catch (e) {
3588
+ return errorResult(ctx.root, path14.resolve(ctx.root, opt.path), e);
3589
+ }
3590
+ if (!existsSync8(target)) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "ファイルが無い(新しい設定なら fml_save_settings)" };
3591
+ const current = readFileSync10(target);
3592
+ if (digestOf(current) !== opt.expectedDigest) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "読んだ後にファイルが変わった(digest が違う)。fml_summary で読み直す" };
3593
+ let settings;
3594
+ try {
3595
+ const v = parseInput(current.toString("utf8").replace(/^/, ""), "設定のファイル");
3596
+ if (!isPlainObject2(v)) throw new InputError("設定のファイルの最上位はオブジェクト");
3597
+ if (v.pluginID !== FORMULA_CONFIG_PLUGIN_ID) throw new InputError(`計算式プラグインの設定のファイルではない(封筒の pluginID が "${FORMULA_CONFIG_PLUGIN_ID}" でない)`);
3598
+ if (!Array.isArray(v.pluginInfos)) throw new InputError("設定のファイルに pluginInfos(項目の行)が無い");
3599
+ settings = v;
3600
+ } catch (e) {
3601
+ return errorResult(ctx.root, target, e);
3602
+ }
3603
+ const rows = settings.pluginInfos;
3604
+ let next;
3605
+ try {
3606
+ const replace = parseRows(opt.rows, rows);
3607
+ const common = opt.common !== void 0 ? parseCommon(opt.common) : {};
3608
+ const merged = rows.map((r) => {
3609
+ const code = isPlainObject2(r) ? String(r.fieldcode ?? "") : "";
3610
+ const rep = replace.get(code);
3611
+ return rep ? { ...r, ...rep } : r;
3612
+ });
3613
+ next = { ...settings, ...common, pluginInfos: merged };
3614
+ } catch (e) {
3615
+ return errorResult(ctx.root, target, e);
3616
+ }
3617
+ let outcome;
3618
+ try {
3619
+ outcome = await normalizeText(ctx, target, JSON.stringify(next), fieldsFileFor(ctx, target, opt.fields, settings));
3620
+ } catch (e) {
3621
+ return errorResult(ctx.root, target, e);
3622
+ }
3623
+ if (outcome.normalize.hasErrors || !outcome.normalize.output) return { status: "invalid", code: "NormalizeError", path: rel(ctx.root, target), findings: outcome.normalize.findings, summary: outcome.normalize.summary, message: "normalize のエラーがあるので保存しない(ファイルは変えていない)" };
3624
+ const output = outcome.normalize.output;
3625
+ const text = JSON.stringify(output, null, 2) + "\n";
3626
+ const name = rel(ctx.root, target);
3627
+ const d = diffSettings(settings, output, { before: `${name}(前)`, after: name });
3628
+ return finish(
3629
+ ctx,
3630
+ target,
3631
+ outcome,
3632
+ () => commit(ctx, target, text, "replace", () => {
3633
+ if (digestOf(readFileSync10(target)) !== opt.expectedDigest) throw new ConflictError("確定の直前にファイルが変わった(digest が違う)。fml_summary で読み直す");
3634
+ }),
3635
+ { diff: { fields: d.fields, common: d.common } }
3636
+ );
3637
+ }
3638
+
2353
3639
  // src/core.ts
2354
- var CORE_API_VERSION = 1;
3640
+ var CORE_API_VERSION = 2;
2355
3641
  export {
2356
3642
  ALLOWED_APIS,
2357
3643
  ALLOWED_USER_APIS,
3644
+ APP_MARKER,
2358
3645
  AuthError,
3646
+ COMMON_KEYS2 as COMMON_KEYS,
3647
+ CONNECTION_MAX_BYTES,
2359
3648
  CORE_API_VERSION,
3649
+ ConnectionError,
2360
3650
  DEFAULT_CONTEXT_BASE_URL,
2361
3651
  DEFAULT_EVENT,
2362
3652
  DEFAULT_MAX_CALLS,
2363
3653
  DEFAULT_POLICY,
2364
3654
  DEFAULT_TIMEOUT_SEC,
2365
3655
  DEFAULT_USERS,
3656
+ EDIT_RE,
3657
+ ENVELOPE_KEYS2 as ENVELOPE_KEYS,
2366
3658
  EVENT_RE,
2367
3659
  EvalError,
2368
3660
  EvalTimeoutError,
3661
+ FALLBACK_PROFILE,
3662
+ FORMULA_CONFIG_PLUGIN_ID,
2369
3663
  FORMULA_PLUGIN_ID,
3664
+ FileExistsError,
2370
3665
  Findings,
3666
+ INBOX,
2371
3667
  InputError,
3668
+ KINTONE_ROOT,
2372
3669
  KintoneUrlError,
3670
+ LEGACY_WORKSPACE_FILE,
2373
3671
  LIST_LIMIT,
3672
+ LOCK_STALE_MS,
3673
+ LockBusyError,
2374
3674
  MAX_INPUT_BYTES,
3675
+ MAX_SAVE_INPUT_BYTES,
2375
3676
  MAX_TIMEOUT_SEC,
3677
+ MAX_UPDATE_ROWS,
2376
3678
  MIN_PLUGIN_VERSION,
2377
3679
  NotAllowedError,
3680
+ OUTER_KEYS,
2378
3681
  PLUGIN_NAME,
2379
3682
  POLICY_FILE,
3683
+ PROFILE_NAME_RE,
2380
3684
  PathError,
3685
+ PermissionError,
2381
3686
  PluginZipError,
2382
3687
  PolicyError,
2383
3688
  QUERY_MAX,
@@ -2385,17 +3690,38 @@ export {
2385
3690
  RECEIVE_LIMITS,
2386
3691
  RECORDS_MAX,
2387
3692
  REQUIRED_API,
3693
+ ROW_SCREEN_KEYS,
3694
+ ROW_SOURCE_KEYS,
2388
3695
  RestError,
2389
3696
  SENTINEL_USER,
2390
3697
  SHAPE_TEXT_MAX,
3698
+ SNAPSHOT_PREFIX,
3699
+ SNAPSHOT_RE,
2391
3700
  SUPPORTED_API_VERSIONS,
3701
+ USER_INFO_FUNCTIONS,
2392
3702
  UsersDataError,
2393
3703
  UsersPullError,
2394
3704
  WRITE_ROOTS,
3705
+ WorkspaceError,
3706
+ acquireLock,
3707
+ appDirFor,
3708
+ appFolderOfFile,
3709
+ appFoldersOf,
3710
+ assertAppMark,
3711
+ assertChangeAllowed,
3712
+ assertInsideWorkspace,
3713
+ assertSameConnection,
3714
+ assertUsableInKintone,
3715
+ authFor,
2395
3716
  baseUrlFromEnv,
2396
3717
  bodyOf,
2397
3718
  buildUsersInfo,
3719
+ builtinErrorName,
2398
3720
  calcSettings,
3721
+ classOfAppPath,
3722
+ classifyEvalError,
3723
+ cleanupAll,
3724
+ connectionStatusOf,
2399
3725
  contextTargetsOf,
2400
3726
  createContext,
2401
3727
  createRestClient,
@@ -2404,57 +3730,96 @@ export {
2404
3730
  describeAuth,
2405
3731
  describeRecord,
2406
3732
  diffSettings,
3733
+ digestOf,
3734
+ editNameOf,
3735
+ ensureAppFolder,
2407
3736
  fetchFields,
2408
3737
  fetchRecord,
3738
+ findAppDir,
2409
3739
  findCycles,
3740
+ folderNameOf,
2410
3741
  formatDate,
3742
+ hasLegacyConfig,
3743
+ identityOf,
2411
3744
  isInside,
2412
3745
  isKintoneBaseUrl,
2413
3746
  isSupportedPluginVersion,
2414
3747
  jsonDiff,
3748
+ jsonErrorText,
3749
+ jsonErrorWhere,
2415
3750
  kintoneConfigPath,
3751
+ legacyBlockedError,
3752
+ listAppFolder,
2416
3753
  listFields,
2417
3754
  listQueryOf,
2418
3755
  listRecordShapes,
2419
3756
  loadAuth,
3757
+ loadConnections,
2420
3758
  loadEngine,
2421
3759
  loadPolicy,
3760
+ loadUserApiAuth,
3761
+ lockPathFor,
3762
+ markOf,
2422
3763
  materialsOf,
3764
+ modeOf,
3765
+ momentFileOf,
2423
3766
  narrowRecord,
2424
3767
  normalizeKintoneBaseUrl,
2425
3768
  normalizeSettings,
2426
3769
  parsePolicy,
3770
+ pickProfile,
3771
+ placeNew,
2427
3772
  pluginIdOf,
2428
3773
  pluginZipPath,
3774
+ profileDirOf,
3775
+ pullReferencedUsers,
2429
3776
  pullSettings,
2430
3777
  pullUsers,
3778
+ readAppMark,
2431
3779
  readFieldsFile,
2432
3780
  readJsonLimited,
2433
3781
  readPluginZip,
2434
3782
  readTextLimited,
2435
3783
  realResolve,
2436
3784
  referenceTablesOf,
3785
+ referencedCodesOf,
2437
3786
  relativeSettingsPath,
3787
+ requireProfiles,
2438
3788
  resolvePluginSources,
2439
3789
  resolveRead,
2440
3790
  resolveWrite,
3791
+ resolveWriteDir,
2441
3792
  rowsOf,
2442
3793
  runEval,
2443
3794
  runEvalJob,
3795
+ safeFileName,
3796
+ sameIdentity,
3797
+ saveNewSettings,
2444
3798
  scanFormula,
2445
3799
  scanFormulas,
3800
+ scanUserInfoCalls,
2446
3801
  schemaRevisionOf,
3802
+ semanticDigest,
2447
3803
  shapeLines,
3804
+ snapshotNameOf,
3805
+ snapshotOf,
2448
3806
  stronglyConnected,
2449
3807
  summarizeFields,
2450
3808
  summarizeRecord,
2451
3809
  summarizeSettings,
2452
3810
  summarizeUsers,
3811
+ takeInbox,
3812
+ tempPathFor,
2453
3813
  toolsMeta,
2454
3814
  unquote,
3815
+ unusedProfileDirs,
3816
+ updateFields,
2455
3817
  usedFieldCodes,
3818
+ userApiAuthFor,
2456
3819
  usersFileNameOf,
2457
3820
  validateEvent,
2458
3821
  validateUsersSource,
2459
- viewsOf
3822
+ viewsOf,
3823
+ withRetry,
3824
+ writeNewFile
2460
3825
  };