@rex0220/print-craft-authoring-tools 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/SECURITY.md +5 -1
- package/dist/cli.mjs +450 -150
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,13 +17,17 @@ Projects/
|
|
|
17
17
|
|
|
18
18
|
## 構成
|
|
19
19
|
|
|
20
|
-
- `src/cli.ts` … `pcraft-authoring <command>`(version / fields / record / pull / normalize / preview / diff / buttons。pull は API ラボのプラグインの設定の GET。fields と record の `--summary` は取得済みのファイルの要約で通信しない。record の要約は値を出さない)。読むのは cwd の中、書くのは fields/ records/ settings/ temp/ out/ の下だけ(`src/safe-path.ts
|
|
20
|
+
- `src/cli.ts` … `pcraft-authoring <command>`(version / fields / record / pull / normalize / preview / diff / buttons。pull は API ラボのプラグインの設定の GET。fields と record の `--summary` は取得済みのファイルの要約で通信しない。record の要約は値を出さない)。読むのは cwd の中、書くのは fields/ records/ settings/ temp/ out/ kintone/ の下だけ(`src/safe-path.ts`。kintone/ の下は `src/permission.ts` の許可も)。`.env` / policy / zip の場所は固定
|
|
21
21
|
- `src/plugin-zip.ts` … 印刷屋の zip(contents.zip の 2 層)を Node の zlib だけで読む。大きさ・entry 数・展開後の上限、CRC-32、名前の一致、重複を検査
|
|
22
22
|
- `src/engine.ts` … zip のエンジンと authoring API を happy-dom + スタブで動かす。照合(プラグイン ID、印刷屋の版、API の版と pluginVersion、API のキーと型)
|
|
23
23
|
- `src/kintone-url.ts` … 接続先の検証(`*.cybozu.com` / `*.kintone.com` / `*.cybozu.cn`、ユーザー情報・パス・ポート無し)
|
|
24
|
-
- `src/
|
|
24
|
+
- `src/context.ts` … 作業の文脈(作業フォルダーの実際のパスと環境変数)。CLI は起動時に一度だけ作り、中核には引数で渡す(print-craft MCP も同じ中核を呼ぶ)
|
|
25
|
+
- `src/paths.ts` / `src/dev-paths.ts` … tools の置き場所(環境変数を読まない。中核から使う)/ 開発とビルドのときだけ使う場所の探し方(隣の print-craft、kit、rexgrid。`PCRAFT_PRINT_CRAFT_ROOT` などの環境変数を読む。import するのは `cli.ts` と scripts/・試験だけ)
|
|
26
|
+
- `src/commit-file.ts` … ファイルの確定(新しいファイルはハードリンクで、上書きしない)、所有者の印付きのロック、片付け(save / pull / take が使う)
|
|
27
|
+
- `src/permission.ts` … `kintone/` の下を変える前の許可(`environments.json` の `role`。本番は読み取りだけ、未分類は何も変えない)
|
|
28
|
+
- `src/workspace.ts` … 開発と本番(environments.json。環境の `role`)、`kintone/<ホスト名>/<番号>-<アプリ名>/` のフォルダー、ダウンロードの名前(`src/commands/take.ts` が inbox から移す)
|
|
25
29
|
- `src/env.ts` / `src/kintone-rest.ts` … `.env`(kintone 公式 MCP と同じ `KINTONE_*` + `PCRAFT_PLUGIN_ZIP`)と GET 専用・許可 API 固定・送信先固定の REST
|
|
26
|
-
- `src/commands/` … 各コマンド。`src/normalize/` … 派生値の生成、検査(HTML / CSS の allowlist、policy、大きさ)、行の差分。`src/preview/` … 帳票 HTML(sandbox + CSP + DOM の無害化)
|
|
30
|
+
- `src/commands/` … 各コマンド。`save.ts` は保存の約束(新しい設定の保存、ボタン 1 つの差し替え。print-craft MCP の保存のツールの本体)、`records.ts` はレコードを数件・形だけで見る(print-craft MCP の kintone_list_records)。`src/normalize/` … 派生値の生成、検査(HTML / CSS の allowlist、policy、大きさ)、行の差分。`src/preview/` … 帳票 HTML(sandbox + CSP + DOM の無害化)
|
|
27
31
|
- `src/meta.ts` … tools の版、扱う印刷屋(プラグイン ID `PRINT_CRAFT_PLUGIN_ID`、版の下限 `MIN_PLUGIN_VERSION`)、API の版(`SUPPORTED_API_VERSIONS`)、API の契約(`REQUIRED_API`)
|
|
28
32
|
- `scripts/vendor.mjs` … moment 2.24.0 を CDN から `vendor/` に取る(`vendor/moment.json` の SHA-256 と照合)
|
|
29
33
|
- `scripts/build.mjs` … esbuild で `dist/cli.mjs`(Node 20、ESM。印刷屋 / kit のコードが入ったら失敗)
|
package/SECURITY.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
## tools が守ること
|
|
23
23
|
|
|
24
|
-
- **kintone には GET しか送らない。** 呼べる API は `app`、`app/form/fields`、`app/form/layout`、`record`、`app/plugin/config` と、その preview 版に固定している(ゲストスペースの `/k/guest/<id>/v1/` も同じ一覧)。それ以外のパスやメソッドは送信前に止まる。`app/plugin/config`(`pull`)は kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得する」で、印刷屋のプラグイン ID(zip の公開鍵から)の設定だけを読む。同じ API ラボの設定の変更(PUT)は呼ばない。`pull`
|
|
24
|
+
- **kintone には GET しか送らない。** 呼べる API は `app`、`app/form/fields`、`app/form/layout`、`record`、`records`、`app/plugin/config` と、その preview 版に固定している(ゲストスペースの `/k/guest/<id>/v1/` も同じ一覧)。それ以外のパスやメソッドは送信前に止まる。`app/plugin/config`(`pull`)は kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得する」で、印刷屋のプラグイン ID(zip の公開鍵から)の設定だけを読む。同じ API ラボの設定の変更(PUT)は呼ばない。`pull` は運用中の設定にレコード閲覧(API トークンでも可。資料は「閲覧と追加」だが閲覧だけのトークンで取れた)、`--preview` にアプリ管理の権限が要るので、`--preview` を使うときだけその権限のトークン(またはログインユーザー)にする。`records`(print-craft MCP の kintone_list_records)は query に limit / offset を許さず件数を 5 件に固定し、値・ファイル名・ユーザー名を返さず形だけを返す(返す JSON は UTF-8 で 64 KiB まで。超える前の件で打ち切る)。**受け取る本文に API ごとの上限**(アプリの情報 64 KiB、項目定義・レイアウト・レコード 8 MiB、プラグインの設定 2 MiB)があり、Content-Length か読みながらの大きさで超えたら読み切らずに止める(本文はストリームからだけ読む。ストリームの無い応答は読まない)
|
|
25
25
|
- **開発と本番(environments.json)でも、場所は CLI から指定できない。** `--env` で選べるのは `environments.json` の環境の名前だけで、認証のファイルの場所(`.env` か `env/<名前>.env` の形だけ)と接続先は `environments.json` に書く。テンプレートでは AI は `environments.json` と `env/` を書けない(`.claude/settings.json`)。このときの認証はその環境の認証のファイルだけから読み、OS の環境変数の `KINTONE_*` / `KSQL_*` は読まない(開発と本番の取り違えを防ぐ)。認証のファイルの `KINTONE_BASE_URL` が `environments.json` の接続先と違えば止まる。iframe の同一オリジンの判定には `environments.json` の接続先を使い、`kintone/<ホスト名>/` のフォルダー名(AI が作れる)は使わない
|
|
26
26
|
- **送信先は kintone のドメインだけ。** `KINTONE_BASE_URL` は `https://<サブドメイン>.cybozu.com` / `.kintone.com` / `.cybozu.cn`(`*.s.cybozu.com` を含む)だけを受け付け、ユーザー情報(`user@`)・ポート・パス・クエリが付いた URL は使わない。URL は `new URL(path, base)` で組み、送信直前にも origin が同じか確かめる
|
|
27
27
|
- **kintone 以外とは通信しない。** `normalize` / `preview` / `diff` / `buttons` / `version` と、`fields` / `record` の `--summary` はネットワークを使わない。利用状況の送信(テレメトリ)は無い
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
- **印刷屋の zip でなければ実行しない(fail closed)。** 外側の zip の `PUBKEY`(kintone のプラグインの公開鍵)から出るプラグイン ID が印刷屋のもの(`src/meta.ts` の `PRINT_CRAFT_PLUGIN_ID`)で、`manifest.json` の版が Ver.6 以上の整数で、authoring API があるときだけ zip のコードを実行する。違えば **コードを実行する前に止まる**。実行した後に、API の版(`SUPPORTED_API_VERSIONS`)、API の印刷屋の版と manifest の版の一致、tools が使う API のキーと型を確かめる。**`SIGNATURE`(kintone の署名)は検証しない**ので、本物の zip の `PUBKEY` を写して中身を差し替えた zip は止められない(1.1.0。2026-10-08。1.0.0 までは 4 ファイルの SHA-256 を既知のリリースと照合していたが、印刷屋の zip が変わるたびに tools の公開が要った)。zip の読み取りでは、外側と中身の大きさ・entry の数・展開後の大きさの上限、central directory と local header の名前の一致、CRC-32、同名 entry の重複を確かめる(zip bomb と改変の対策)。公開ビルド(npm の `dist/cli.mjs`)が読むのは `PCRAFT_PLUGIN_ZIP` の zip だけで、`node_modules` や隣のフォルダーのファイルは読まない(開発者がソースから動かし、環境変数 `PCRAFT_ALLOW_DEV_PLUGIN=1` を置いたときだけ、隣の print-craft の `prod/` を警告付きで読む)
|
|
31
31
|
- **印刷屋のコードを含まない。** エンジンと API は利用者の zip からメモリへ読むだけで、コピーや書き出しはしない。ビルドは、npm に入る `dist/cli.mjs` に印刷屋 / plugin-config-kit / rexgrid の実行コードが混ざっていないことを検査する
|
|
32
32
|
- **CLI が読む・書く場所を限る。** 読むのは作業フォルダーの中のファイルだけ(`.env` と `node_modules` は読まない)、書くのは `fields/` `records/` `settings/` `temp/` `out/` `kintone/` の下だけ(realpath で判定。junction / symlink で外へは出られない。Windows の予約名は使わない)。`.env`、`policy/authoring-policy.json`、印刷屋の zip の場所(`PCRAFT_PLUGIN_ZIP`)は作業フォルダーのものに固定で、`--env` / `--policy` / `--plugin-zip` のようなオプションは無い。iframe の同一オリジンの判定は `.env` の `KINTONE_BASE_URL` **だけ**を使い、AI が書き換えられる `fields/*.json` の値は使わない(`.env` に接続先が無ければ iframe は使えない)。パスの検査から書き込みまでの間にリンクが差し替えられる競合(TOCTOU)は残るが、AI と同じ OS ユーザーの範囲の話であり、この tools では防がない
|
|
33
|
+
- **本番の環境は読み取りだけ(`environments.json` の `role`)。** `kintone/` の下を変える直前に `environments.json` を読み直し、本番(`production`)のフォルダーは新しい名前のダウンロード / pull を足すことだけ、開発(`development`)はダウンロード / pull のファイルの書き換えだけを拒否、`role` の無い環境・`environments.json` が無いのに `kintone/` の下・どの環境か決まらないフォルダー(構成 2 で `apps` に無い番号)は何も変えない(`src/permission.ts`)。`--env`(AI が付けられる引数)では決めない
|
|
34
|
+
- **作業フォルダーと環境変数は起動時に一度だけ決める。** 作業フォルダーは実際のパス(symlink / junction を解いたもの)にしてから使い、中核は `process.cwd()` / `process.env` を既定の値として読まない(`src/context.ts`。印刷屋の zip と開発中の読み込み元も引数で受け取る)。環境変数を読むのは `src/cli.ts` と開発用の場所の探し方 `src/dev-paths.ts` だけで、`dev-paths.ts` を import するのは `cli.ts` だけ(`test/core-boundary.test.mjs` で確かめる)。OS の環境変数(print-craft MCP では設定項目)の `PCRAFT_PLUGIN_ZIP` は絶対パスだけ。外側の引用符を外すのは場所と URL だけ(認証情報は外さない)
|
|
35
|
+
- **設定の保存は normalize を通ったときだけ確定する**(`src/commands/save.ts`。print-craft MCP の保存のツールの本体)。normalize → 書く先のパス・`role`・ダウンロードのファイルを確かめる(normalize の間に本番に変わったフォルダーには一時ファイルもロックも作らない)→ 同じフォルダーの一時ファイル → 確定先のロック(隣の `.<名前>.pcraft-lock` に所有者の印。普通のファイルでなければ取らない。60 秒より古いものは固有の名前に付け替えてから回収する。外すのは自分の印のロックだけ)→ 確定の直前に書く先のパス(途中で symlink に差し替えられていないか)・`role`・ダウンロードのファイル・digest・ロックがまだ自分のものかを確かめ直す → 確定。新しい設定は `expectedAbsent: true` が要り、ハードリンクで確定する(同じ名前があれば失敗する = 確かめた後に作られたファイルも上書きしない)。既存の設定は名前の付け替えで確定し、読んだ時点の digest が違えば書かない。項目定義は、アプリのフォルダーの中なら同じフォルダーの `fields.json` だけ(別のファイルを渡すと拒否)。入力(`content` / `replacement`)は UTF-8 で 256 KiB まで。一時ファイルとロックの片付けは別々に試し(Windows の EPERM / EBUSY は短く待ってやり直す)、確定の後の片付けの失敗は保存の失敗にしない(結果に添える)(`src/commit-file.ts`)
|
|
36
|
+
- **ダウンロード / pull のファイルは新しいファイルとしてだけ置く。** `pull`(`environments.json` のときと、`--force` の無いとき。`environments.json` のときは `--force` を使えない)と `take` は、同じフォルダーの一時ファイルからハードリンクで確定し、確定の直前に書く先のパスと許可を確かめ直す(確かめた後に同じ名前のファイルができても上書きしない)。`take` は置けてから inbox の元を消す(消せなければ「置いたが inbox に残った」と返す)
|
|
33
37
|
- **`normalize` は許可した書き方だけ通す(allowlist)。** 要素は文章・表・画像の要素だけ、属性は共通の属性(class / id / style …)と要素ごとの属性(img の src / srcset / alt、td の colspan …)と `data-*` / `aria-*` だけで、一覧に無い要素・属性、`on*` 属性、インラインの `<svg>` / `<math>`、`<script>` `<object>` `<embed>` `<link>` `<base>` `<meta>` `<form>` 系、`<template>` などはエラー。URL は Chrome の URL パーサーと同じ前処理(タブ・改行・制御文字を捨てる、`\` は `/`)をしてから `https:` / `data:image/` / 置き換えタグ / 相対だけを通す(`a` の `href` は `data:` も不可、`iframe` は利用者の kintone と同じオリジンだけ)。CSS はコメントを外してエスケープを復号してから、`url()` / `image-set()` / `image()` / `src()` / `@import` の URL を分類し、`@import` / `expression(` / `behavior:` / `-moz-binding:` を止める。属性値の `${式}` はエラー。**kintone 以外への読み込みとスクリプトは印刷屋 Ver.6 が描画の前に除く**(共通の設定「外部参照」`externalRefs: "block"`。帳票の HTML / CSS を DOMParser で DOM 化して、外部へ向く URL 属性・CSS の `url()` / `@import`・`script` などの要素・`on*` 属性を外してから画面に入れる)ので、tools はそれを前提にする: `"block"` の設定の HTML / CSS のテンプレートにある外部 URL は**エラー**(帳票に出ない。添付ファイルか `data:image/` にする)。`"allow"`(Ver.5 と同じく何も除かない。自己責任。キーが無い Ver.5 の設定も印刷屋は `allow` で動くので tools も `allow` とみなす)は、利用者が AI の書けない `policy/authoring-policy.json` の `allowExternalRefs` にその設定ファイルを書いていなければ**エラー**。`allow` の設定の外部 URL と、外部参照の設定に関わらず Web フォント(印刷屋は除かない)は `allowExternal`(形を検証する)で承認し、承認済みは情報、無ければ**警告**。帳票の行の**計算式が作る HTML** は静的に追い切れない(計算式のインタープリターを tools に二重に持たない。Codex レビュー 5 回の結論)ので**警告だけ**: HTML を作る関数の使用、`TAG` の要素名や `ATTR` / `STYLE` / `BATTR` の値が定数でない、文字列の定数に HTML / CSS があれば HTML / CSS と同じ検査を警告として出す。`${式}` の安全判定は式全体で行い、`&` でつないだ項がすべて安全なときだけ警告しない。エラーがあれば書き戻さない
|
|
34
38
|
- **`preview` の HTML は閉じている。** 帳票は `sandbox=""` だけの iframe に `srcdoc` で入れ、外側と帳票の両方の文書に CSP(帳票は `default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'`)。CSS は `<` を CSS のエスケープ(`\3c`)にして `</style>` で文書を壊せないようにし、描いた DOM から `script` / `meta` / `link` / `form` 系 / `iframe` / `object` / `embed` / SVG のアニメーション要素と、`on*` 属性・`href`(文書内の `#` 以外)・`srcdoc` などを外す(レコードの値が `TABLE_HTML` などで HTML として入る経路があるため)。画像は印刷屋のダミー画像。Web フォントは、配信元が承認済み(Google Fonts は既定、他は `policy/authoring-policy.json` の `allowExternal`)のときだけ帳票の文書に `<link>` を入れ、CSP の `style-src` / `font-src` にその配信元(Google Fonts は `fonts.googleapis.com` と `fonts.gstatic.com`)を足す。これが preview の唯一の外部通信で、送るのは設定に書いた固定の URL だけ(レコードの値は入らない)。未承認なら読まない(OS の書体)。出力には**レコードの値が入る**ので `out/` はコミット対象外
|
|
35
39
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/*! @rex0220/print-craft-authoring-tools 1.
|
|
2
|
+
/*! @rex0220/print-craft-authoring-tools 1.2.0 (c) rex0220. MIT License. 計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む(このファイルには含まれない)。commit 390cbac. */
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import
|
|
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
|
-
|
|
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
|
|
70
|
-
const osEnv = opt.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
|
|
77
|
-
|
|
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
|
|
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
|
|
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
|
|
132
|
-
if (!
|
|
133
|
-
return path.resolve(path.dirname(envFileOf(opt)),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
383
|
-
import
|
|
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.
|
|
526
|
+
if (true) return JSON.parse('{"toolsVersion":"1.2.0","pluginId":"lcapkanpjdabgphknkabojmcfhonhkhp","minPluginVersion":6,"supportedApiVersions":[1,2],"commit":"390cbac","builtAt":"2026-10-09T12:21:22.862Z","mode":"build"}');
|
|
527
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,
|
|
1045
|
+
const walk = (el, path14) => {
|
|
1046
1046
|
const tag = el.tagName.toLowerCase();
|
|
1047
|
-
const here2 = `${
|
|
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
|
|
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
|
|
1213
|
-
if (!
|
|
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(
|
|
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(
|
|
1718
|
-
const
|
|
1719
|
-
const
|
|
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(
|
|
1752
|
-
const text =
|
|
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 (
|
|
1755
|
-
result.skipped.push({ file: rel, reason: `行き先に同じ名前の別の中身がある: ${
|
|
1903
|
+
if (readFileSync7(dest, "utf8") !== text) {
|
|
1904
|
+
result.skipped.push({ file: rel, reason: `行き先に同じ名前の別の中身がある: ${path10.relative(cwd, dest)}` });
|
|
1756
1905
|
continue;
|
|
1757
1906
|
}
|
|
1758
|
-
|
|
1759
|
-
result.moved.push({ file: rel, to:
|
|
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
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
2373
|
-
import
|
|
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
|
|
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(
|
|
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
|
|
2782
|
+
const zip = opt.pluginZip;
|
|
2544
2783
|
if (zip) {
|
|
2545
|
-
if (!
|
|
2784
|
+
if (!existsSync7(zip)) throw new PluginZipError(`印刷屋の zip が無い: ${zip}(.env の PCRAFT_PLUGIN_ZIP)`);
|
|
2546
2785
|
return { ...readPluginZip(zip), kind: "zip" };
|
|
2547
2786
|
}
|
|
2548
|
-
const
|
|
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
|
-
|
|
2553
|
-
|
|
2554
|
-
const
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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
|
|
2942
|
-
|
|
2943
|
-
|
|
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
|
|
2946
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
3028
|
-
if (!
|
|
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(
|
|
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
|
|
3310
|
+
if (arg) return resolveRead2(arg);
|
|
3037
3311
|
if (!folder) throw new UsageError("--fields <fields.json> が要る(fields コマンドの出力。kintone/<ホスト名>/<番号>-…/ の中のファイルなら同じフォルダーの fields.json を使う)");
|
|
3038
|
-
return existingFile(
|
|
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 ?
|
|
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 ?
|
|
3058
|
-
|
|
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 ?
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 ?
|
|
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(
|
|
3142
|
-
const b = readJsonLimited(
|
|
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 共通管理者がアップデートオプションの「検討中の新機能」で)、権限(運用中:
|
|
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 ?
|
|
3168
|
-
if (
|
|
3169
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
3504
|
+
src = path13.join(list.dir, list.snapshots[0]);
|
|
3208
3505
|
}
|
|
3209
|
-
const file =
|
|
3210
|
-
if (!SNAPSHOT_RE.test(
|
|
3211
|
-
const dest =
|
|
3212
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
3249
|
-
const outDir =
|
|
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 =
|
|
3266
|
-
|
|
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(
|
|
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(
|
|
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 {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rex0220/print-craft-authoring-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "rex0220 印刷屋プラグイン(kintone)の設定 JSON を AI で作る・確かめるための tools(fields / record / pull / normalize / preview / diff / buttons / version)。計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|