petari 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +23 -6
  2. package/dist/cli.js +4 -1
  3. package/dist/commands/show.js +205 -39
  4. package/dist/core/diff-html.js +272 -0
  5. package/dist/core/diff.js +177 -0
  6. package/dist/core/edit.js +83 -0
  7. package/dist/core/editor-html.js +301 -0
  8. package/dist/infra/browser.js +24 -0
  9. package/dist/infra/diff-server.js +437 -0
  10. package/package.json +7 -4
  11. package/vendor/monaco/LICENSE.txt +21 -0
  12. package/vendor/monaco/ThirdPartyNotices.txt +448 -0
  13. package/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
  14. package/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
  15. package/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
  16. package/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  17. package/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  18. package/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
  19. package/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
  20. package/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  21. package/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
  22. package/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  23. package/vendor/monaco/vs/bat-C397hTD6.js +1 -0
  24. package/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
  25. package/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
  26. package/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
  27. package/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
  28. package/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
  29. package/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
  30. package/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
  31. package/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
  32. package/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
  33. package/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
  34. package/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
  35. package/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
  36. package/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  37. package/vendor/monaco/vs/ecl-30fUercY.js +1 -0
  38. package/vendor/monaco/vs/editor/editor.main.css +1 -0
  39. package/vendor/monaco/vs/editor/editor.main.js +1 -0
  40. package/vendor/monaco/vs/editor/editor.worker.js +1 -0
  41. package/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
  42. package/vendor/monaco/vs/editor.js +1 -0
  43. package/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  44. package/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
  45. package/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
  46. package/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
  47. package/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  48. package/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
  49. package/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
  50. package/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
  51. package/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
  52. package/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
  53. package/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
  54. package/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
  55. package/vendor/monaco/vs/index-049xrXrp.js +1 -0
  56. package/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
  57. package/vendor/monaco/vs/index.js +1 -0
  58. package/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
  59. package/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
  60. package/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
  61. package/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
  62. package/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
  63. package/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
  64. package/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
  65. package/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  66. package/vendor/monaco/vs/language/css/css.worker.js +66 -0
  67. package/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
  68. package/vendor/monaco/vs/language/html/html.worker.js +443 -0
  69. package/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
  70. package/vendor/monaco/vs/language/json/json.worker.js +17 -0
  71. package/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
  72. package/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  73. package/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
  74. package/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
  75. package/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
  76. package/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
  77. package/vendor/monaco/vs/loader.js +1368 -0
  78. package/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  79. package/vendor/monaco/vs/lua-DtygF91M.js +1 -0
  80. package/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
  81. package/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
  82. package/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
  83. package/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
  84. package/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
  85. package/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
  86. package/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
  87. package/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
  88. package/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
  89. package/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
  90. package/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
  91. package/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
  92. package/vendor/monaco/vs/nls/lang/cs.d.ts +1 -0
  93. package/vendor/monaco/vs/nls/lang/cs.js +9 -0
  94. package/vendor/monaco/vs/nls/lang/de.d.ts +1 -0
  95. package/vendor/monaco/vs/nls/lang/de.js +9 -0
  96. package/vendor/monaco/vs/nls/lang/es.d.ts +1 -0
  97. package/vendor/monaco/vs/nls/lang/es.js +9 -0
  98. package/vendor/monaco/vs/nls/lang/fr.d.ts +1 -0
  99. package/vendor/monaco/vs/nls/lang/fr.js +9 -0
  100. package/vendor/monaco/vs/nls/lang/it.d.ts +1 -0
  101. package/vendor/monaco/vs/nls/lang/it.js +9 -0
  102. package/vendor/monaco/vs/nls/lang/ja.d.ts +1 -0
  103. package/vendor/monaco/vs/nls/lang/ja.js +9 -0
  104. package/vendor/monaco/vs/nls/lang/ko.d.ts +1 -0
  105. package/vendor/monaco/vs/nls/lang/ko.js +9 -0
  106. package/vendor/monaco/vs/nls/lang/pl.d.ts +1 -0
  107. package/vendor/monaco/vs/nls/lang/pl.js +9 -0
  108. package/vendor/monaco/vs/nls/lang/pt-br.d.ts +1 -0
  109. package/vendor/monaco/vs/nls/lang/pt-br.js +9 -0
  110. package/vendor/monaco/vs/nls/lang/ru.d.ts +1 -0
  111. package/vendor/monaco/vs/nls/lang/ru.js +9 -0
  112. package/vendor/monaco/vs/nls/lang/tr.d.ts +1 -0
  113. package/vendor/monaco/vs/nls/lang/tr.js +9 -0
  114. package/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
  115. package/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
  116. package/vendor/monaco/vs/nls/lang/zh-tw.d.ts +1 -0
  117. package/vendor/monaco/vs/nls/lang/zh-tw.js +9 -0
  118. package/vendor/monaco/vs/nls.messages-loader.js +1 -0
  119. package/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
  120. package/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
  121. package/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  122. package/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
  123. package/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
  124. package/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
  125. package/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
  126. package/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  127. package/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  128. package/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
  129. package/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  130. package/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
  131. package/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
  132. package/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  133. package/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
  134. package/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
  135. package/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
  136. package/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
  137. package/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  138. package/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
  139. package/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
  140. package/vendor/monaco/vs/sb-3GYllVck.js +1 -0
  141. package/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
  142. package/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
  143. package/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
  144. package/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
  145. package/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  146. package/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  147. package/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
  148. package/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
  149. package/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
  150. package/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
  151. package/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  152. package/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
  153. package/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  154. package/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
  155. package/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
  156. package/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
  157. package/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
  158. package/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
  159. package/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
  160. package/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  161. package/vendor/monaco/vs/workers-BBttULjf.js +1 -0
  162. package/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
  163. package/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
@@ -0,0 +1,437 @@
1
+ /**
2
+ * ブラウザ編集用の一時 HTTP サーバー (§4.3 + §9)。
3
+ *
4
+ * セキュリティ設計 (2026-08-12 決定・同日 JS 解禁で更新):
5
+ * - 127.0.0.1 のみで待ち受け (OS 割当の一時ポート)。外部への通信は行わない
6
+ * - URL パス先頭に 128bit 乱数トークン。長さ検査の後 timingSafeEqual で比較し、
7
+ * 不一致はルート不明と区別せず 404 (トークンの探索を許さない)
8
+ * - Host ヘッダは 127.0.0.1:<port> 完全一致のみ (DNS rebinding 対策)。
9
+ * POST は Origin ヘッダが存在すれば自 origin 一致を要求
10
+ * - ルートは固定 (一覧 / 編集ページ / 保存 / 終了)。対象はサーバー起動時に確定した
11
+ * エントリ配列の整数インデックスのみで、クライアントからパスは一切受け取らない
12
+ * - POST は application/x-www-form-urlencoded のみ、ボディ上限 16 MiB
13
+ * - JS はエディタページに埋め込んだ自前スクリプトのみ (script-src 'nonce-<応答ごとの乱数>')。
14
+ * 外部リソース・外部ライブラリはゼロ。その他のページ/応答は JS 不可の基本 CSP
15
+ * - 保存は symlink 拒否・isInsideRoot 再検証・sha256 楽観ロック (409) を通す
16
+ * (フォーム POST と fetch (ajax=1 → JSON 応答) の両対応。JS 無効でも保存できる)
17
+ * - 無操作 idleTimeoutMs で自動終了 (textarea 入力はリクエストを発生させないため
18
+ * 既定 30 分。短くしすぎると編集内容を失わせる)
19
+ */
20
+ import { randomBytes, timingSafeEqual } from "node:crypto";
21
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
22
+ import { createServer } from "node:http";
23
+ import { dirname, join } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { CSP, buildIndexPage, buildMessagePage } from "../core/diff-html.js";
26
+ import { buildEditorPage, editorCsp } from "../core/editor-html.js";
27
+ import { browserEditBlockReason, findMergeUnencodable, mergeEditedText, renderTextareaValue, } from "../core/edit.js";
28
+ import { decodeFile, encodeDocument } from "../core/encoding.js";
29
+ import { isInsideRoot, readFileState, sha256, writeBytes } from "./files.js";
30
+ const BODY_LIMIT = 16 * 1024 * 1024;
31
+ const DEFAULT_IDLE_MS = 30 * 60_000;
32
+ const TOKEN_HEX_LEN = 32; // randomBytes(16) の hex
33
+ /** 同梱 monaco の場所 (src/ からも dist/ からも <パッケージ>/vendor/monaco) */
34
+ export function monacoVendorDir() {
35
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "vendor", "monaco");
36
+ }
37
+ /**
38
+ * 同梱資産の許可リストを起動時に列挙する (§9)。
39
+ * 要求パスはこの Set への完全一致でのみ配信するため、ディレクトリトラバーサルは
40
+ * 構造的に成立しない (".." を含むエントリは列挙に現れない)。
41
+ */
42
+ function listVendorAssets(dir) {
43
+ const set = new Set();
44
+ const walk = (rel) => {
45
+ for (const ent of readdirSync(rel === "" ? dir : join(dir, rel), { withFileTypes: true })) {
46
+ const relPath = rel === "" ? ent.name : `${rel}/${ent.name}`;
47
+ if (ent.isDirectory())
48
+ walk(relPath);
49
+ else if (ent.isFile())
50
+ set.add(relPath);
51
+ }
52
+ };
53
+ try {
54
+ walk("");
55
+ }
56
+ catch {
57
+ // 同梱なし (開発環境で gen:monaco 未実行) → 資産要求は 404
58
+ }
59
+ return set;
60
+ }
61
+ const ASSET_TYPES = {
62
+ ".js": "text/javascript; charset=utf-8",
63
+ ".css": "text/css; charset=utf-8",
64
+ ".json": "application/json; charset=utf-8",
65
+ ".ttf": "font/ttf",
66
+ ".svg": "image/svg+xml",
67
+ ".png": "image/png",
68
+ ".txt": "text/plain; charset=utf-8",
69
+ };
70
+ export async function startDiffServer(opts) {
71
+ const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_MS;
72
+ const idleMinutes = Math.max(1, Math.round(idleMs / 60_000));
73
+ const token = randomBytes(16).toString("hex");
74
+ const tokenBuf = Buffer.from(token);
75
+ const vendorDir = monacoVendorDir();
76
+ const vendorAssets = listVendorAssets(vendorDir);
77
+ let resolveClosed;
78
+ const closed = new Promise((resolve) => {
79
+ resolveClosed = resolve;
80
+ });
81
+ let finished = false;
82
+ let idleTimer = null;
83
+ const shutdown = (reason) => {
84
+ if (finished)
85
+ return;
86
+ finished = true;
87
+ if (idleTimer !== null)
88
+ clearTimeout(idleTimer);
89
+ server.close();
90
+ server.closeAllConnections();
91
+ resolveClosed(reason);
92
+ };
93
+ const resetIdle = () => {
94
+ if (finished)
95
+ return;
96
+ if (idleTimer !== null)
97
+ clearTimeout(idleTimer);
98
+ idleTimer = setTimeout(() => shutdown("idle"), idleMs);
99
+ };
100
+ const baseHeaders = (contentType, csp) => ({
101
+ "content-type": contentType,
102
+ "content-security-policy": csp,
103
+ "x-content-type-options": "nosniff",
104
+ "referrer-policy": "no-referrer",
105
+ "cache-control": "no-store",
106
+ });
107
+ const send = (res, status, html, csp) => {
108
+ res.writeHead(status, baseHeaders("text/html; charset=utf-8", csp ?? CSP));
109
+ res.end(html);
110
+ };
111
+ const sendJson = (res, status, value) => {
112
+ res.writeHead(status, baseHeaders("application/json; charset=utf-8", CSP));
113
+ res.end(JSON.stringify(value));
114
+ };
115
+ const encodingLabel = (doc) => doc.encoding === "shift_jis" ? "Shift_JIS" : doc.hasBom ? "UTF-8 (BOM)" : "UTF-8";
116
+ const eolLabel = (doc) => doc.eol === "crlf" ? "CRLF" : doc.eol === "lf" ? "LF" : "改行なし";
117
+ /** エントリの編集可否 (一覧の注記)。null = 編集可 */
118
+ const entryNote = (entry) => {
119
+ if (entry.op === "delete")
120
+ return "delete のため現在のファイルはありません";
121
+ const state = readFileState(entry.absPath);
122
+ if (state.symlink)
123
+ return "シンボリックリンクのため編集できません";
124
+ if (!state.exists)
125
+ return "ファイルが存在しません (undo 済み?)";
126
+ try {
127
+ return browserEditBlockReason(decodeFile(state.bytes));
128
+ }
129
+ catch {
130
+ return "テキストとして表示できません (バイナリまたはエンコーディング判定不能)";
131
+ }
132
+ };
133
+ /** 編集ページを組み立てる。text/banner は保存失敗時の再表示用 (入力を失わせない) */
134
+ const renderEditPage = (entry, saved, over) => {
135
+ const back = "../";
136
+ if (entry.op === "delete") {
137
+ return {
138
+ status: 200,
139
+ html: buildMessagePage(entry.path, "delete のため現在のファイルはありません", back),
140
+ };
141
+ }
142
+ const state = readFileState(entry.absPath);
143
+ if (state.symlink) {
144
+ return {
145
+ status: 200,
146
+ html: buildMessagePage(entry.path, "シンボリックリンクのため編集できません", back),
147
+ };
148
+ }
149
+ if (!state.exists) {
150
+ return {
151
+ status: 200,
152
+ html: buildMessagePage(entry.path, "ファイルが存在しません (undo 済み?)", back),
153
+ };
154
+ }
155
+ const bytes = state.bytes;
156
+ let doc;
157
+ try {
158
+ doc = decodeFile(bytes);
159
+ }
160
+ catch (e) {
161
+ const msg = e instanceof Error ? e.message : String(e);
162
+ return { status: 200, html: buildMessagePage(entry.path, `編集できません: ${msg}`, back) };
163
+ }
164
+ let snapshotText = null;
165
+ let snapshotNote = null;
166
+ if (entry.snapshotPath !== null && existsSync(entry.snapshotPath)) {
167
+ try {
168
+ snapshotText = renderTextareaValue(decodeFile(new Uint8Array(readFileSync(entry.snapshotPath))));
169
+ }
170
+ catch {
171
+ snapshotNote = "スナップショットをテキスト表示できません (空との比較になります)";
172
+ }
173
+ }
174
+ else {
175
+ snapshotNote = "比較元スナップショットがありません (新規作成ファイル)";
176
+ }
177
+ const nonce = randomBytes(16).toString("base64");
178
+ return {
179
+ status: 200,
180
+ csp: editorCsp(nonce),
181
+ html: buildEditorPage({
182
+ path: entry.path,
183
+ leftLabel: opts.leftLabel,
184
+ snapshotText,
185
+ snapshotNote,
186
+ currentText: over?.text ?? renderTextareaValue(doc),
187
+ baseSha256: sha256(bytes),
188
+ encodingLabel: encodingLabel(doc),
189
+ eolLabel: eolLabel(doc),
190
+ saved,
191
+ errorBanner: over?.banner ?? null,
192
+ blockReason: browserEditBlockReason(doc),
193
+ idleMinutes,
194
+ nonce,
195
+ }),
196
+ };
197
+ };
198
+ /** 保存の本体。応答形式 (フォーム/JSON) に依存しない意味的な結果を返す */
199
+ const doSave = (entry, n, params) => {
200
+ const text = params.get("text");
201
+ const base = params.get("baseSha256");
202
+ if (text === null || base === null || !/^[0-9a-f]{64}$/.test(base)) {
203
+ return { kind: "rejected", status: 400, message: "リクエストが不正です", back: "../" };
204
+ }
205
+ if (entry.op === "delete") {
206
+ return { kind: "rejected", status: 403, message: "delete のため保存できません", back: "../" };
207
+ }
208
+ // 起動後のすり替えに備えて保存直前にも検証する (symlink / ルート外)
209
+ if (!isInsideRoot(opts.root, entry.absPath)) {
210
+ return { kind: "rejected", status: 403, message: "保存先がプロジェクト外です", back: "../" };
211
+ }
212
+ const state = readFileState(entry.absPath);
213
+ if (state.symlink) {
214
+ return {
215
+ kind: "rejected",
216
+ status: 403,
217
+ message: "シンボリックリンクのため保存できません",
218
+ back: "../",
219
+ };
220
+ }
221
+ if (!state.exists) {
222
+ return {
223
+ kind: "rejected",
224
+ status: 409,
225
+ message: "ファイルが存在しません (削除された可能性)",
226
+ back: "../",
227
+ };
228
+ }
229
+ const bytes = state.bytes;
230
+ if (sha256(bytes) !== base) {
231
+ return {
232
+ kind: "rejected",
233
+ status: 409,
234
+ message: "ページを開いた後にファイルが外部で変更されています。開き直して編集を反映し直してください (今回の内容は保存されていません)",
235
+ // POST 先 URL (/<token>/f/<n>) 基準の相対参照 = 編集ページ自身
236
+ back: String(n),
237
+ };
238
+ }
239
+ let doc;
240
+ try {
241
+ doc = decodeFile(bytes);
242
+ }
243
+ catch (e) {
244
+ const msg = e instanceof Error ? e.message : String(e);
245
+ return { kind: "rejected", status: 403, message: `保存できません: ${msg}`, back: "../" };
246
+ }
247
+ const block = browserEditBlockReason(doc);
248
+ if (block !== null) {
249
+ return { kind: "rejected", status: 403, message: block, back: "../" };
250
+ }
251
+ const merged = mergeEditedText(doc, text);
252
+ const bad = findMergeUnencodable(merged);
253
+ if (bad.length > 0) {
254
+ return {
255
+ kind: "unencodable",
256
+ message: `Shift_JIS に変換できない文字が含まれています: ${bad.join(" ")}`,
257
+ text,
258
+ };
259
+ }
260
+ const outBytes = encodeDocument(merged, opts.fallbackEol);
261
+ const outSha = sha256(outBytes);
262
+ if (outSha !== base) {
263
+ writeBytes(entry.absPath, outBytes);
264
+ }
265
+ return { kind: "saved", sha: outSha };
266
+ };
267
+ const handle = async (req, res) => {
268
+ resetIdle();
269
+ const port = server.address().port;
270
+ const selfOrigin = `http://127.0.0.1:${port}`;
271
+ if (req.headers.host !== `127.0.0.1:${port}`) {
272
+ send(res, 400, buildMessagePage("エラー", "不正なリクエストです", null));
273
+ return;
274
+ }
275
+ let url;
276
+ try {
277
+ url = new URL(req.url ?? "/", selfOrigin);
278
+ }
279
+ catch {
280
+ send(res, 400, buildMessagePage("エラー", "不正なリクエストです", null));
281
+ return;
282
+ }
283
+ const segs = url.pathname.split("/").filter((s) => s !== "");
284
+ const first = segs[0] ?? "";
285
+ const firstBuf = Buffer.from(first);
286
+ if (firstBuf.length !== TOKEN_HEX_LEN || !timingSafeEqual(firstBuf, tokenBuf)) {
287
+ send(res, 404, buildMessagePage("Not Found", "ページが見つかりません", null));
288
+ return;
289
+ }
290
+ const rest = segs.slice(1);
291
+ if (req.method === "GET") {
292
+ if (rest.length === 0) {
293
+ if (!url.pathname.endsWith("/")) {
294
+ // 相対リンクの基準を保つため末尾スラッシュへ寄せる
295
+ res.writeHead(308, { location: `/${token}/`, "cache-control": "no-store" });
296
+ res.end();
297
+ return;
298
+ }
299
+ const items = opts.entries.map((e) => ({
300
+ path: e.path,
301
+ op: e.op,
302
+ note: entryNote(e),
303
+ }));
304
+ send(res, 200, buildIndexPage(opts.title, items, idleMinutes));
305
+ return;
306
+ }
307
+ // 同梱 monaco 資産 (起動時に列挙した許可リストへの完全一致のみ配信)
308
+ const assetKey = rest.join("/");
309
+ if (vendorAssets.has(assetKey)) {
310
+ const ext = assetKey.slice(assetKey.lastIndexOf("."));
311
+ res.writeHead(200, baseHeaders(ASSET_TYPES[ext] ?? "application/octet-stream", CSP));
312
+ res.end(readFileSync(join(vendorDir, ...rest)));
313
+ return;
314
+ }
315
+ const n = parseEntryIndex(rest, opts.entries.length);
316
+ if (n !== null) {
317
+ const page = renderEditPage(opts.entries[n], url.searchParams.get("saved") === "1");
318
+ send(res, page.status, page.html, page.csp);
319
+ return;
320
+ }
321
+ send(res, 404, buildMessagePage("Not Found", "ページが見つかりません", "../"));
322
+ return;
323
+ }
324
+ if (req.method === "POST") {
325
+ const origin = req.headers.origin;
326
+ if (origin !== undefined && origin !== selfOrigin) {
327
+ send(res, 403, buildMessagePage("エラー", "リクエスト元が不正です", null));
328
+ return;
329
+ }
330
+ const ct = req.headers["content-type"];
331
+ if (ct === undefined || !ct.startsWith("application/x-www-form-urlencoded")) {
332
+ send(res, 415, buildMessagePage("エラー", "Content-Type が不正です", null));
333
+ return;
334
+ }
335
+ const body = await readBody(req, BODY_LIMIT);
336
+ if (body === null) {
337
+ send(res, 413, buildMessagePage("エラー", "リクエストが大きすぎます", null));
338
+ res.once("finish", () => req.destroy());
339
+ return;
340
+ }
341
+ if (rest.length === 1 && rest[0] === "quit") {
342
+ send(res, 200, buildMessagePage("終了しました", "サーバーを終了しました。このタブは閉じてください", null));
343
+ // 応答を返してから閉じる
344
+ setImmediate(() => shutdown("quit"));
345
+ return;
346
+ }
347
+ const n = parseEntryIndex(rest, opts.entries.length);
348
+ if (n !== null) {
349
+ const entry = opts.entries[n];
350
+ const params = new URLSearchParams(body.toString("utf8"));
351
+ const result = doSave(entry, n, params);
352
+ if (params.get("ajax") === "1") {
353
+ // fetch (自前 JS) 向け: JSON で結果を返す
354
+ if (result.kind === "saved")
355
+ sendJson(res, 200, { ok: true, baseSha256: result.sha });
356
+ else if (result.kind === "unencodable")
357
+ sendJson(res, 422, { ok: false, error: result.message });
358
+ else
359
+ sendJson(res, result.status, { ok: false, error: result.message });
360
+ return;
361
+ }
362
+ // フォーム POST (JS 無効時のフォールバック) 向け
363
+ if (result.kind === "saved") {
364
+ res.writeHead(303, { location: `/${token}/f/${n}?saved=1`, "cache-control": "no-store" });
365
+ res.end();
366
+ }
367
+ else if (result.kind === "unencodable") {
368
+ const page = renderEditPage(entry, false, { text: result.text, banner: result.message });
369
+ send(res, 200, page.html, page.csp);
370
+ }
371
+ else {
372
+ send(res, result.status, buildMessagePage(result.status === 409 ? "競合" : "エラー", result.message, result.back));
373
+ }
374
+ return;
375
+ }
376
+ send(res, 404, buildMessagePage("Not Found", "ページが見つかりません", null));
377
+ return;
378
+ }
379
+ send(res, 405, buildMessagePage("エラー", "許可されていないメソッドです", null));
380
+ };
381
+ const server = createServer((req, res) => {
382
+ void handle(req, res).catch(() => {
383
+ try {
384
+ send(res, 500, buildMessagePage("エラー", "内部エラーが発生しました", null));
385
+ }
386
+ catch {
387
+ res.destroy();
388
+ }
389
+ });
390
+ });
391
+ await new Promise((resolve, reject) => {
392
+ server.once("error", reject);
393
+ server.listen(0, "127.0.0.1", () => resolve());
394
+ });
395
+ resetIdle();
396
+ const port = server.address().port;
397
+ return {
398
+ url: `http://127.0.0.1:${port}/${token}/`,
399
+ closed,
400
+ close: () => shutdown("quit"),
401
+ };
402
+ }
403
+ /** ルート ["f", "<n>"] の <n> を検証して返す (それ以外は null)。先頭ゼロや符号は拒否 */
404
+ function parseEntryIndex(rest, length) {
405
+ if (rest.length !== 2 || rest[0] !== "f")
406
+ return null;
407
+ const s = rest[1];
408
+ if (!/^(0|[1-9][0-9]*)$/.test(s))
409
+ return null;
410
+ const n = Number.parseInt(s, 10);
411
+ return n < length ? n : null;
412
+ }
413
+ /** ボディを上限付きで読む。超過・エラーは null */
414
+ function readBody(req, limit) {
415
+ return new Promise((resolve) => {
416
+ const chunks = [];
417
+ let total = 0;
418
+ let done = false;
419
+ const finish = (value) => {
420
+ if (!done) {
421
+ done = true;
422
+ resolve(value);
423
+ }
424
+ };
425
+ req.on("data", (c) => {
426
+ total += c.length;
427
+ if (total > limit) {
428
+ finish(null);
429
+ return;
430
+ }
431
+ if (!done)
432
+ chunks.push(c);
433
+ });
434
+ req.on("end", () => finish(Buffer.concat(chunks)));
435
+ req.on("error", () => finish(null));
436
+ });
437
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "petari",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "petari — paste AI chat patches onto your codebase",
5
5
  "keywords": [
6
6
  "cli",
@@ -24,22 +24,25 @@
24
24
  "petari": "./dist/cli.js"
25
25
  },
26
26
  "files": [
27
- "dist"
27
+ "dist",
28
+ "vendor"
28
29
  ],
29
30
  "engines": {
30
31
  "node": ">=20.19.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/node": "^24.3.0",
35
+ "monaco-editor": "0.56.0",
34
36
  "typescript": "^5.9.2",
35
37
  "vitest": "^3.2.4"
36
38
  },
37
39
  "scripts": {
38
- "build": "tsc -p tsconfig.build.json",
40
+ "build": "tsc -p tsconfig.build.json && node scripts/vendor-monaco.ts",
39
41
  "typecheck": "tsc --noEmit",
40
42
  "test": "vitest run",
41
43
  "test:watch": "vitest",
42
44
  "dev": "node src/cli.ts",
43
- "gen:sjis": "node scripts/generate-sjis-table.ts"
45
+ "gen:sjis": "node scripts/generate-sjis-table.ts",
46
+ "gen:monaco": "node scripts/vendor-monaco.ts"
44
47
  }
45
48
  }
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 - present Microsoft Corporation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.