petari 0.4.1 → 0.5.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 +8 -2
- package/dist/assets/protocol.js +12 -1
- package/dist/commands/apply.js +19 -7
- package/dist/commands/init.js +2 -1
- package/dist/commands/show.js +3 -2
- package/dist/core/parser.js +91 -13
- package/dist/core/report.js +14 -2
- package/dist/infra/clipboard.js +16 -9
- package/dist/infra/config.js +4 -0
- package/dist/infra/term.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ petari の更新で規約文が変わった場合は、`petari init` を再実
|
|
|
60
60
|
| `petari init` | プロジェクト初回セットアップ (`--yes` で全提案に同意) |
|
|
61
61
|
|
|
62
62
|
主なオプション: `--dry-run` (検証と差分プレビューのみ) / `--partial` (成功分のみ適用) /
|
|
63
|
-
`--root <dir>` / `--yes` / `--clip-report` (
|
|
63
|
+
`--root <dir>` / `--yes` / `--clip-report` (失敗レポートをクリップボードへ。v0.5.0 から既定で自動コピーされるため、config で無効化した場合の個別指定用)
|
|
64
64
|
|
|
65
65
|
## 特徴
|
|
66
66
|
|
|
@@ -71,7 +71,13 @@ petari の更新で規約文が変わった場合は、`petari init` を再実
|
|
|
71
71
|
結果は `適用 / 済み / 失敗` の 3 状態で表示
|
|
72
72
|
- **エンコーディング保全**: Shift_JIS / UTF-8 (BOM 有無)・CRLF / LF・末尾改行を完全維持。
|
|
73
73
|
変更していない行は元のバイト列をそのまま書き戻す (レガシー VB.NET 資産でも安全)
|
|
74
|
-
-
|
|
74
|
+
- **規約逸脱への耐性 (寛容パース)**: AI がマーカーの記号数・見出しレベルを崩したり
|
|
75
|
+
出力全体をコードフェンスで包んでも、厳密パース失敗時に限り安全な範囲で自動補正して
|
|
76
|
+
解釈する (補正内容は適用前に全件表示。曖昧さが残る場合は採用せずエラーに戻す)
|
|
77
|
+
- **失敗レポート**: そのまま AI チャットに貼り返せる形式で出力し、**既定でクリップボードへ
|
|
78
|
+
自動コピー** (失敗した時点で貼り返し 1 ペーストの状態。config の
|
|
79
|
+
`clipReportOnFailure: false` で無効化可)。構文エラー時のレポートには規約フォーマットの
|
|
80
|
+
要点を同梱 (チャット側で規約文が失われていてもレポート 1 枚で再出力を依頼できる)
|
|
75
81
|
- **git / VS Code がない端末でも差分確認・手修正**: `show` は VS Code がなければ自己完結
|
|
76
82
|
HTML レポートをブラウザで開き、`show --edit` は同梱の Monaco Editor (VS Code と同じ
|
|
77
83
|
差分エディタ) で現在のファイルを差分を見ながら直接編集・保存できる
|
package/dist/assets/protocol.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 制約: slnmix (v0.7.0+) はこのテキストを <instruction>...</instruction> で囲んで
|
|
7
7
|
* 出力へ連結するため、規約文に <instruction> という文字列を含めないこと。
|
|
8
8
|
*/
|
|
9
|
-
export const PROTOCOL_VERSION =
|
|
9
|
+
export const PROTOCOL_VERSION = 3;
|
|
10
10
|
export const PROTOCOL_TEXT = `<!-- petari protocol v${PROTOCOL_VERSION} -->
|
|
11
11
|
# コード変更の出力規約 (changes.md)
|
|
12
12
|
|
|
@@ -76,4 +76,15 @@ new code
|
|
|
76
76
|
7. 対象ファイルが日本語 Shift_JIS の場合、絵文字など Shift_JIS で表現できない文字を
|
|
77
77
|
コード中に入れない
|
|
78
78
|
8. changes.md 以外の出力 (前置き・後書き) は最小限にする
|
|
79
|
+
|
|
80
|
+
## 出力前の自己チェック
|
|
81
|
+
|
|
82
|
+
出力を確定する前に、次の 4 点を必ず確認してください。
|
|
83
|
+
|
|
84
|
+
1. 出力は「## CHANGES」の行から始まっているか
|
|
85
|
+
2. すべてのマーカー行が行頭にあり、綴りが正確か
|
|
86
|
+
(<<<<<<< と >>>>>>> は記号 7 個 + 半角スペース + 語、区切りは = 7 個のみの行)
|
|
87
|
+
3. 各 SEARCH の内容は、提供された現在のファイルからの正確なコピーか
|
|
88
|
+
(記憶をもとに再構成していないか)
|
|
89
|
+
4. 同じファイルの FILE セクションが重複していないか
|
|
79
90
|
`;
|
package/dist/commands/apply.js
CHANGED
|
@@ -8,14 +8,14 @@ import { join, resolve } from "node:path";
|
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
import { invalidPathReason, planChangeSet, } from "../core/applier.js";
|
|
10
10
|
import { PRESENCE_STAGE_LABEL } from "../core/matcher.js";
|
|
11
|
-
import {
|
|
11
|
+
import { parseChangesRecovering } from "../core/parser.js";
|
|
12
12
|
import { buildFailureReport, buildParseErrorReport } from "../core/report.js";
|
|
13
13
|
import { readClipboard, writeClipboard } from "../infra/clipboard.js";
|
|
14
14
|
import { loadConfig } from "../infra/config.js";
|
|
15
15
|
import { findRecentChangesFiles, resolveDownloadsDir, } from "../infra/downloads.js";
|
|
16
16
|
import { deleteFile, isInsideRoot, readFileState, sha256, writeBytes } from "../infra/files.js";
|
|
17
17
|
import { gitDirtyFiles } from "../infra/git.js";
|
|
18
|
-
import { err, out } from "../infra/term.js";
|
|
18
|
+
import { err, out, outEmphasis } from "../infra/term.js";
|
|
19
19
|
import { beginHistory, createHistoryId, finishHistory, pruneHistory, } from "../infra/history.js";
|
|
20
20
|
import { confirm } from "../infra/prompt.js";
|
|
21
21
|
import { findProjectRoot } from "../infra/root.js";
|
|
@@ -86,13 +86,19 @@ export async function applyCommand(argv) {
|
|
|
86
86
|
if (!existsSync(join(root, ".petari"))) {
|
|
87
87
|
out("ヒント: 初回は `petari init` を実行すると protocol.md や設定の雛形が整います");
|
|
88
88
|
}
|
|
89
|
-
//
|
|
89
|
+
// レポートを標準出力へ出し、クリップボードにもコピーする (§7)。
|
|
90
|
+
// 既定 (clipReportOnFailure: true) で自動コピーし、config で無効化できる。--clip-report は常に有効
|
|
90
91
|
const emitReport = async (report) => {
|
|
91
92
|
out(report);
|
|
92
|
-
if (values["clip-report"]) {
|
|
93
|
+
if (values["clip-report"] || config.clipReportOnFailure) {
|
|
93
94
|
try {
|
|
94
95
|
await writeClipboard(report);
|
|
95
|
-
|
|
96
|
+
// 貼り返しを忘れないよう、コピー済みであることを最後に強調表示する
|
|
97
|
+
const rule = "━".repeat(56);
|
|
98
|
+
outEmphasis(rule);
|
|
99
|
+
outEmphasis(" 失敗レポートをクリップボードにコピーしました ", true);
|
|
100
|
+
outEmphasis(" → そのまま AI チャットに貼り付けて再依頼できます (Ctrl+V)");
|
|
101
|
+
outEmphasis(rule);
|
|
96
102
|
}
|
|
97
103
|
catch (e) {
|
|
98
104
|
err(`petari: クリップボードへのコピーに失敗: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -166,13 +172,19 @@ export async function applyCommand(argv) {
|
|
|
166
172
|
}
|
|
167
173
|
source = { type: newest.origin, path: newest.path };
|
|
168
174
|
}
|
|
169
|
-
// 1. パース (構文エラーは即時失敗・§4.1)
|
|
170
|
-
const { changeSet, issues } =
|
|
175
|
+
// 1. パース (構文エラーは即時失敗・§4.1)。strict で失敗したら寛容パースを試す (§3.5)
|
|
176
|
+
const { changeSet, issues, repairs } = parseChangesRecovering(changesText);
|
|
171
177
|
if (issues.length > 0) {
|
|
172
178
|
err("petari: changes.md の構文エラーです。何も書き込んでいません。\n");
|
|
173
179
|
await emitReport(buildParseErrorReport(issues));
|
|
174
180
|
return 1;
|
|
175
181
|
}
|
|
182
|
+
if (repairs.length > 0) {
|
|
183
|
+
out(`規約からの軽微な逸脱 ${repairs.length} 件を自動補正して解釈しました:`);
|
|
184
|
+
for (const r of repairs)
|
|
185
|
+
out(` ${r.line} 行目: ${r.message}`);
|
|
186
|
+
out("");
|
|
187
|
+
}
|
|
176
188
|
// 2. 全ブロックのドライラン検証
|
|
177
189
|
const states = new Map();
|
|
178
190
|
for (const f of changeSet.files) {
|
package/dist/commands/init.js
CHANGED
|
@@ -13,7 +13,8 @@ const CONFIG_TEMPLATE = `{
|
|
|
13
13
|
"downloadsDir": null,
|
|
14
14
|
"newFile": { "encoding": "utf8", "eol": "lf" },
|
|
15
15
|
"historyLimit": null,
|
|
16
|
-
"vscodeCommand": "code"
|
|
16
|
+
"vscodeCommand": "code",
|
|
17
|
+
"clipReportOnFailure": true
|
|
17
18
|
}
|
|
18
19
|
`;
|
|
19
20
|
export async function initCommand(argv) {
|
package/dist/commands/show.js
CHANGED
|
@@ -21,7 +21,7 @@ import { invalidPathReason } from "../core/applier.js";
|
|
|
21
21
|
import { diffLines, toSideBySideRows } from "../core/diff.js";
|
|
22
22
|
import { buildReportPage } from "../core/diff-html.js";
|
|
23
23
|
import { EncodingError, decodeFile } from "../core/encoding.js";
|
|
24
|
-
import {
|
|
24
|
+
import { parseChangesRecovering } from "../core/parser.js";
|
|
25
25
|
import { openInBrowser } from "../infra/browser.js";
|
|
26
26
|
import { loadConfig } from "../infra/config.js";
|
|
27
27
|
import { monacoVendorDir, startDiffServer } from "../infra/diff-server.js";
|
|
@@ -273,7 +273,8 @@ export async function showCommand(argv) {
|
|
|
273
273
|
// --changes: CHANGES セクション (概要・影響一覧・Mermaid) の表示 (§13-4)
|
|
274
274
|
if (values.changes) {
|
|
275
275
|
const text = readFileSync(join(hdir, "changes.md"), "utf8");
|
|
276
|
-
|
|
276
|
+
// 寛容パースで適用した履歴の changes.md も表示できるようにする (§3.5)
|
|
277
|
+
const { changeSet } = parseChangesRecovering(text);
|
|
277
278
|
out(changeSet.header !== "" ? changeSet.header : "(CHANGES セクションがありません)");
|
|
278
279
|
return 0;
|
|
279
280
|
}
|
package/dist/core/parser.js
CHANGED
|
@@ -6,6 +6,28 @@ const CONTENT_END = ">>>>>>> END";
|
|
|
6
6
|
const CHANGES_HEADING = "## CHANGES";
|
|
7
7
|
const FILE_PREFIX = "### FILE:";
|
|
8
8
|
const FILE_RE = /^### FILE:\s*(.+?)\s*\((replace|create|rewrite|delete)\)$/;
|
|
9
|
+
const EXACT_MARKER = {
|
|
10
|
+
search: SEARCH,
|
|
11
|
+
divider: DIVIDER,
|
|
12
|
+
"replace-end": REPLACE_END,
|
|
13
|
+
content: CONTENT,
|
|
14
|
+
"content-end": CONTENT_END,
|
|
15
|
+
};
|
|
16
|
+
// 寛容モード (§3.5) の許容パターン。strict パースが失敗したときだけ試す「惜しい」逸脱で、
|
|
17
|
+
// AI チャットの実出力で観測された崩れ (記号の個数ずれ・スペース欠落・見出しレベルずれ・
|
|
18
|
+
// 全体のコードフェンス包み) を対象にする。誤検知しても lenient 結果は issues が 1 件でも
|
|
19
|
+
// あれば破棄して strict のエラーに戻すため、静かな誤解釈は採用されない。
|
|
20
|
+
const LENIENT_MARKER = {
|
|
21
|
+
search: /^<{5,16}\s*SEARCH$/,
|
|
22
|
+
divider: /^={6,10}$/,
|
|
23
|
+
"replace-end": /^>{5,16}\s*REPLACE$/,
|
|
24
|
+
content: /^<{5,16}\s*CONTENT$/,
|
|
25
|
+
"content-end": /^>{5,16}\s*END$/,
|
|
26
|
+
};
|
|
27
|
+
const LENIENT_CHANGES_RE = /^#{1,6}\s*CHANGES$/;
|
|
28
|
+
const LENIENT_FILE_ATTEMPT_RE = /^#{1,6}\s*FILE\s*:/;
|
|
29
|
+
const LENIENT_FILE_RE = /^#{1,6}\s*FILE\s*:\s*(.+?)\s*\((replace|create|rewrite|delete)\)$/;
|
|
30
|
+
const FENCE_RE = /^`{3,}[A-Za-z0-9_-]*$/;
|
|
9
31
|
/**
|
|
10
32
|
* changes.md をパースする (§3)。
|
|
11
33
|
*
|
|
@@ -15,6 +37,27 @@ const FILE_RE = /^### FILE:\s*(.+?)\s*\((replace|create|rewrite|delete)\)$/;
|
|
|
15
37
|
* - 構文の問題は issues に集約する。issues が空でなければ適用してはならない。
|
|
16
38
|
*/
|
|
17
39
|
export function parseChanges(text) {
|
|
40
|
+
return parseCore(text, null);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 寛容パース (§3.5)。strict で成功すればそのまま返し、失敗した場合のみ lenient で
|
|
44
|
+
* 再パースする。lenient 結果は issues がゼロかつ補正が 1 件以上のときだけ採用し、
|
|
45
|
+
* それ以外は strict の issues を返す (エラーメッセージの分かりやすさを優先)。
|
|
46
|
+
* ブロック内部 (SEARCH/REPLACE/CONTENT の本文) ではフェンス行を除去しないため、
|
|
47
|
+
* Markdown ファイル自体を変更対象にしても本文は壊れない。
|
|
48
|
+
*/
|
|
49
|
+
export function parseChangesRecovering(text) {
|
|
50
|
+
const strict = parseCore(text, null);
|
|
51
|
+
if (strict.issues.length === 0)
|
|
52
|
+
return { ...strict, repairs: [] };
|
|
53
|
+
const repairs = [];
|
|
54
|
+
const lenient = parseCore(text, repairs);
|
|
55
|
+
if (lenient.issues.length === 0 && repairs.length > 0)
|
|
56
|
+
return { ...lenient, repairs };
|
|
57
|
+
return { ...strict, repairs: [] };
|
|
58
|
+
}
|
|
59
|
+
/** repairs が null なら strict、配列なら lenient (補正内容を追記していく) */
|
|
60
|
+
function parseCore(text, repairs) {
|
|
18
61
|
const rawLines = text.split(/\r?\n/);
|
|
19
62
|
const issues = [];
|
|
20
63
|
const files = [];
|
|
@@ -26,6 +69,19 @@ export function parseChanges(text) {
|
|
|
26
69
|
let replaceBuf = [];
|
|
27
70
|
let contentBuf = [];
|
|
28
71
|
let blockLine = 0;
|
|
72
|
+
// 該当行が指定マーカーか。lenient では許容パターンも試し、一致したら補正として記録する。
|
|
73
|
+
// 呼び出し側は true を返した分岐で必ずそのマーカーとして処理するため、記録と動作は一致する
|
|
74
|
+
const isMarker = (line, no, kind) => {
|
|
75
|
+
if (line === EXACT_MARKER[kind])
|
|
76
|
+
return true;
|
|
77
|
+
if (repairs === null || !LENIENT_MARKER[kind].test(line))
|
|
78
|
+
return false;
|
|
79
|
+
repairs.push({
|
|
80
|
+
line: no,
|
|
81
|
+
message: `マーカー "${line}" を "${EXACT_MARKER[kind]}" として解釈しました`,
|
|
82
|
+
});
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
29
85
|
// クロージャ経由の代入は TS の narrowing が追えないため、cur へのアクセスはここを通す
|
|
30
86
|
function requireCur() {
|
|
31
87
|
if (cur === null)
|
|
@@ -76,13 +132,28 @@ export function parseChanges(text) {
|
|
|
76
132
|
const line = raw.trimEnd();
|
|
77
133
|
const no = idx + 1;
|
|
78
134
|
if (state === "preamble") {
|
|
79
|
-
if (line === CHANGES_HEADING)
|
|
135
|
+
if (line === CHANGES_HEADING) {
|
|
136
|
+
state = "header";
|
|
137
|
+
}
|
|
138
|
+
else if (repairs !== null && LENIENT_CHANGES_RE.test(line)) {
|
|
139
|
+
repairs.push({ line: no, message: `見出し "${line}" を "${CHANGES_HEADING}" として解釈しました` });
|
|
80
140
|
state = "header";
|
|
141
|
+
}
|
|
81
142
|
continue;
|
|
82
143
|
}
|
|
83
144
|
if (state === "header" || state === "fileTop") {
|
|
84
|
-
|
|
85
|
-
|
|
145
|
+
const fileAttempt = line.startsWith(FILE_PREFIX) || (repairs !== null && LENIENT_FILE_ATTEMPT_RE.test(line));
|
|
146
|
+
if (fileAttempt) {
|
|
147
|
+
let m = FILE_RE.exec(line);
|
|
148
|
+
if (m === null && repairs !== null) {
|
|
149
|
+
m = LENIENT_FILE_RE.exec(line);
|
|
150
|
+
if (m !== null) {
|
|
151
|
+
repairs.push({
|
|
152
|
+
line: no,
|
|
153
|
+
message: `FILE 行 "${line}" を "### FILE: ${m[1]} (${m[2]})" として解釈しました`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
86
157
|
if (m !== null) {
|
|
87
158
|
startSection(m[1], m[2], no);
|
|
88
159
|
}
|
|
@@ -95,7 +166,10 @@ export function parseChanges(text) {
|
|
|
95
166
|
continue;
|
|
96
167
|
}
|
|
97
168
|
if (state === "header") {
|
|
98
|
-
if (line
|
|
169
|
+
if (isMarker(line, no, "search") ||
|
|
170
|
+
isMarker(line, no, "content") ||
|
|
171
|
+
isMarker(line, no, "replace-end") ||
|
|
172
|
+
isMarker(line, no, "content-end")) {
|
|
99
173
|
issues.push({
|
|
100
174
|
line: no,
|
|
101
175
|
message: `${line} が最初の FILE 行より前に現れました (### FILE: 行が壊れている可能性があります)`,
|
|
@@ -109,9 +183,13 @@ export function parseChanges(text) {
|
|
|
109
183
|
// fileTop: FILE セクション内・ブロック外
|
|
110
184
|
if (line === "")
|
|
111
185
|
continue;
|
|
186
|
+
if (repairs !== null && FENCE_RE.test(line)) {
|
|
187
|
+
repairs.push({ line: no, message: `コードフェンス行 "${line}" を無視しました` });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
112
190
|
const section = requireCur();
|
|
113
191
|
if (section.op === "replace") {
|
|
114
|
-
if (line
|
|
192
|
+
if (isMarker(line, no, "search")) {
|
|
115
193
|
state = "search";
|
|
116
194
|
searchBuf = [];
|
|
117
195
|
replaceBuf = [];
|
|
@@ -125,7 +203,7 @@ export function parseChanges(text) {
|
|
|
125
203
|
}
|
|
126
204
|
}
|
|
127
205
|
else if (section.op === "create" || section.op === "rewrite") {
|
|
128
|
-
if (line
|
|
206
|
+
if (isMarker(line, no, "content")) {
|
|
129
207
|
if (section.content !== null) {
|
|
130
208
|
issues.push({
|
|
131
209
|
line: no,
|
|
@@ -153,17 +231,17 @@ export function parseChanges(text) {
|
|
|
153
231
|
continue;
|
|
154
232
|
}
|
|
155
233
|
if (state === "search") {
|
|
156
|
-
if (line
|
|
234
|
+
if (isMarker(line, no, "divider")) {
|
|
157
235
|
state = "replace";
|
|
158
236
|
}
|
|
159
|
-
else if (line
|
|
237
|
+
else if (isMarker(line, no, "replace-end")) {
|
|
160
238
|
issues.push({
|
|
161
239
|
line: no,
|
|
162
240
|
message: `${requireCur().path}: ======= (区切り) がないまま >>>>>>> REPLACE が現れました`,
|
|
163
241
|
});
|
|
164
242
|
state = "fileTop";
|
|
165
243
|
}
|
|
166
|
-
else if (line
|
|
244
|
+
else if (isMarker(line, no, "search")) {
|
|
167
245
|
issues.push({
|
|
168
246
|
line: no,
|
|
169
247
|
message: `${requireCur().path}: SEARCH ブロックが閉じられないまま次の <<<<<<< SEARCH が現れました`,
|
|
@@ -178,7 +256,7 @@ export function parseChanges(text) {
|
|
|
178
256
|
}
|
|
179
257
|
if (state === "replace") {
|
|
180
258
|
const section = requireCur();
|
|
181
|
-
if (line
|
|
259
|
+
if (isMarker(line, no, "replace-end")) {
|
|
182
260
|
if (searchBuf.length === 0) {
|
|
183
261
|
issues.push({ line: blockLine, message: `${section.path}: SEARCH ブロックが空です` });
|
|
184
262
|
}
|
|
@@ -190,14 +268,14 @@ export function parseChanges(text) {
|
|
|
190
268
|
});
|
|
191
269
|
state = "fileTop";
|
|
192
270
|
}
|
|
193
|
-
else if (line
|
|
271
|
+
else if (isMarker(line, no, "divider")) {
|
|
194
272
|
issues.push({
|
|
195
273
|
line: no,
|
|
196
274
|
message: `${section.path}: ======= (区切り) が 1 ブロック内に複数あります`,
|
|
197
275
|
});
|
|
198
276
|
state = "fileTop";
|
|
199
277
|
}
|
|
200
|
-
else if (line
|
|
278
|
+
else if (isMarker(line, no, "search")) {
|
|
201
279
|
issues.push({
|
|
202
280
|
line: no,
|
|
203
281
|
message: `${section.path}: >>>>>>> REPLACE で閉じられないまま次の <<<<<<< SEARCH が現れました`,
|
|
@@ -213,7 +291,7 @@ export function parseChanges(text) {
|
|
|
213
291
|
continue;
|
|
214
292
|
}
|
|
215
293
|
// state === "content"
|
|
216
|
-
if (line
|
|
294
|
+
if (isMarker(line, no, "content-end")) {
|
|
217
295
|
requireCur().content = contentBuf;
|
|
218
296
|
state = "fileTop";
|
|
219
297
|
}
|
package/dist/core/report.js
CHANGED
|
@@ -20,7 +20,8 @@ export function buildFailureReport(failures) {
|
|
|
20
20
|
parts.push("", RE_REQUEST, "");
|
|
21
21
|
return parts.join("\n");
|
|
22
22
|
}
|
|
23
|
-
/** 構文エラー (パース失敗)
|
|
23
|
+
/** 構文エラー (パース失敗) のレポート。規約文がチャット側で失われていても
|
|
24
|
+
* このレポート単体で再出力を依頼できるよう、フォーマットの要点を再掲する */
|
|
24
25
|
export function buildParseErrorReport(issues) {
|
|
25
26
|
const parts = [
|
|
26
27
|
"受け取った changes.md が規約フォーマットとして解釈できませんでした。",
|
|
@@ -29,11 +30,22 @@ export function buildParseErrorReport(issues) {
|
|
|
29
30
|
"",
|
|
30
31
|
...issues.map((i) => `- ${i.line} 行目: ${i.message}`),
|
|
31
32
|
"",
|
|
33
|
+
"## 規約フォーマットの要点 (再掲)",
|
|
34
|
+
"",
|
|
35
|
+
"- 出力は必ず行頭の「## CHANGES」の行から始め、変更概要のあとに各ファイルのセクションを置く",
|
|
36
|
+
"- 各ファイルは「### FILE: 相対パス (replace|create|rewrite|delete)」の見出し行で始める",
|
|
37
|
+
"- replace は <<<<<<< SEARCH / ======= / >>>>>>> REPLACE のブロックで書き、",
|
|
38
|
+
" SEARCH の内容は現在のファイルから一字一句そのままコピーする",
|
|
39
|
+
"- create / rewrite は <<<<<<< CONTENT / >>>>>>> END のブロックにファイル全文を書く。delete は本文なし",
|
|
40
|
+
"- マーカー行は必ず行頭から書き、前後に他の文字を付けない (< > = はいずれも 7 個)",
|
|
41
|
+
"- 出力全体や各ブロックをコードフェンス (```) で包まない",
|
|
42
|
+
"- FILE セクションの間に説明文を書かない (説明は冒頭の CHANGES セクションへ)",
|
|
43
|
+
"",
|
|
32
44
|
"## 依頼",
|
|
33
45
|
"",
|
|
34
46
|
"上記の構文エラーを修正し、changes.md 全体を規約フォーマット (## CHANGES から始まり、",
|
|
35
47
|
"### FILE: 行と <<<<<<< SEARCH / ======= / >>>>>>> REPLACE 等のマーカーを行頭に置く形式) で",
|
|
36
|
-
"
|
|
48
|
+
"再出力してください。失敗していないファイル・ブロックも含めた完全な changes.md を出力してください。",
|
|
37
49
|
"",
|
|
38
50
|
];
|
|
39
51
|
return parts.join("\n");
|
package/dist/infra/clipboard.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Linux: xclip (インストールされている場合のみ)
|
|
7
7
|
*/
|
|
8
8
|
import { execFile, spawn } from "node:child_process";
|
|
9
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { promisify } from "node:util";
|
|
@@ -48,14 +48,21 @@ export async function writeClipboard(text) {
|
|
|
48
48
|
if (process.platform === "win32") {
|
|
49
49
|
// コンソールのコードページに依存しないよう UTF-8 の一時ファイル経由で渡す。
|
|
50
50
|
// パスは PowerShell の単一引用符で渡す (' は '' にエスケープ。展開・注入が起きない)
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), "petari-clip-"));
|
|
52
|
+
const file = join(dir, "report.txt");
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(file, text, "utf8");
|
|
55
|
+
const quoted = `'${file.replaceAll("'", "''")}'`;
|
|
56
|
+
await execFileP("powershell", [
|
|
57
|
+
"-NoProfile",
|
|
58
|
+
"-Command",
|
|
59
|
+
`Get-Content -Raw -Encoding UTF8 -LiteralPath ${quoted} | Set-Clipboard`,
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
// レポートはコードの断片を含むため一時ファイルを残さない
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
59
66
|
return;
|
|
60
67
|
}
|
|
61
68
|
await viaStdin("xclip", ["-selection", "clipboard"], text);
|
package/dist/infra/config.js
CHANGED
|
@@ -6,6 +6,7 @@ export const DEFAULT_CONFIG = {
|
|
|
6
6
|
newFile: { encoding: "utf8", eol: "lf" },
|
|
7
7
|
historyLimit: null,
|
|
8
8
|
vscodeCommand: "code",
|
|
9
|
+
clipReportOnFailure: true,
|
|
9
10
|
};
|
|
10
11
|
/** グローバル設定のパス (§10)。Windows は %APPDATA%、他は XDG (~/.config) */
|
|
11
12
|
export function globalConfigPath() {
|
|
@@ -45,6 +46,9 @@ function validateConfig(c) {
|
|
|
45
46
|
if (c.downloadsDir !== null && typeof c.downloadsDir !== "string") {
|
|
46
47
|
throw new Error("config の downloadsDir が不正です (null または文字列を指定)");
|
|
47
48
|
}
|
|
49
|
+
if (typeof c.clipReportOnFailure !== "boolean") {
|
|
50
|
+
throw new Error("config の clipReportOnFailure が不正です (true | false)");
|
|
51
|
+
}
|
|
48
52
|
return c;
|
|
49
53
|
}
|
|
50
54
|
/** 既定 < グローバル < プロジェクトの順でマージする (プロジェクト優先・§10) */
|
package/dist/infra/term.js
CHANGED
|
@@ -12,3 +12,17 @@ export function sanitizeForTerminal(s) {
|
|
|
12
12
|
}
|
|
13
13
|
export const out = (s) => void process.stdout.write(sanitizeForTerminal(s) + "\n");
|
|
14
14
|
export const err = (s) => void process.stderr.write(sanitizeForTerminal(s) + "\n");
|
|
15
|
+
const RESET = "\u001B[0m";
|
|
16
|
+
const BOLD_YELLOW = "\u001B[1;33m";
|
|
17
|
+
const BOLD_REVERSE_YELLOW = "\u001B[1;7;33m";
|
|
18
|
+
/**
|
|
19
|
+
* 目立たせたい行の出力。装飾コードはこのファイルのリテラル定数のみで、
|
|
20
|
+
* 本文はサニタイズ後に着色するため ANSI 注入対策 (上記) は保たれる。
|
|
21
|
+
* 非 TTY (パイプ・リダイレクト) と NO_COLOR 指定時は装飾なしで出力する。
|
|
22
|
+
*/
|
|
23
|
+
export const outEmphasis = (s, reverse = false) => {
|
|
24
|
+
const clean = sanitizeForTerminal(s);
|
|
25
|
+
const decorate = process.stdout.isTTY === true && process.env["NO_COLOR"] === undefined;
|
|
26
|
+
const colored = decorate ? `${reverse ? BOLD_REVERSE_YELLOW : BOLD_YELLOW}${clean}${RESET}` : clean;
|
|
27
|
+
process.stdout.write(colored + "\n");
|
|
28
|
+
};
|