petari 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,25 @@ petari # 返答 (changes.md) を適用
48
48
  petari の更新で規約文が変わった場合は、`petari init` を再実行すると protocol.md の
49
49
  差分を検出して更新を提案します (slnmix 側の更新は不要)。
50
50
 
51
+ #### 新規 .vb ファイルの作成 (v0.9.0)
52
+
53
+ AI が `create` で `.vb` を新規作成すると、petari が次を自動で行います (Visual Studio での
54
+ 手作業をなくすため)。いずれも失敗しても create 自体は成功し、理由を結果表示に出します。
55
+
56
+ - **旧スタイル .vbproj への登録**: 作成先から上へ辿って最寄りの `.vbproj` を探し、
57
+ `<Compile Include="...">` を追加します (`Inherits Form` なら `<SubType>Form</SubType>`、
58
+ `X.Designer.vb` なら `<DependentUpon>X.vb</DependentUpon>` 付き)。追加行以外のバイト列は
59
+ 変えません (Shift_JIS / CRLF / BOM そのまま)。SDK スタイルの `.vbproj` は登録不要のため
60
+ 触りません。登録は履歴に含まれ `petari undo` で戻ります。無効化は `--no-vbproj` か
61
+ `.petari/config.json` の `"vbproj": { "register": false }`
62
+ - **エンコーディングの推定**: `.petari/config.json` の `"newFile": { "encoding": "auto" }`
63
+ (`petari init` の雛形の既定) で、同じディレクトリの既存ファイルの多数決で
64
+ 文字コード / BOM / 改行を決めます。`.vb` を BOM なし UTF-8 で作ることはありません
65
+ (vbc / VS で日本語が化けるため)。従来どおり `"utf8"` / `"shift_jis"` の明示指定も使えます
66
+
67
+ 規約文 v4 では「SEARCH は一意に特定できる範囲のうち最小 (目安 3〜8 行)」を追加しました。
68
+ `petari init` の再実行で protocol.md を更新してください。
69
+
51
70
  ### 適用が繰り返し失敗するとき
52
71
 
53
72
  失敗レポート (自動でクリップボードにコピーされます) をそのまま AI に貼り返すのが
@@ -74,7 +93,8 @@ slnmix の出力) が実ファイルとずれている**可能性が高いです
74
93
  | `petari init` | プロジェクト初回セットアップ (`--yes` で全提案に同意) |
75
94
 
76
95
  主なオプション: `--dry-run` (検証と差分プレビューのみ) / `--partial` (成功分のみ適用) /
77
- `--root <dir>` / `--yes` / `--clip-report` (失敗レポートをクリップボードへ。v0.5.0 から既定で自動コピーされるため、config で無効化した場合の個別指定用)
96
+ `--root <dir>` / `--yes` / `--clip-report` (失敗レポートをクリップボードへ。v0.5.0 から既定で自動コピーされるため、config で無効化した場合の個別指定用) /
97
+ `--no-vbproj` (create した .vb の .vbproj 自動登録を今回だけ無効化)
78
98
 
79
99
  ## 特徴
80
100
 
@@ -6,7 +6,7 @@
6
6
  * 制約: slnmix (v0.7.0+) はこのテキストを <instruction>...</instruction> で囲んで
7
7
  * 出力へ連結するため、規約文に <instruction> という文字列を含めないこと。
8
8
  */
9
- export const PROTOCOL_VERSION = 3;
9
+ export const PROTOCOL_VERSION = 4;
10
10
  export const PROTOCOL_TEXT = `<!-- petari protocol v${PROTOCOL_VERSION} -->
11
11
  # コード変更の出力規約 (changes.md)
12
12
 
@@ -69,7 +69,9 @@ new code
69
69
  2. SEARCH ブロックの内容は、**現在のファイルから一字一句そのまま** (インデント・空白を含めて) コピーする。
70
70
  記憶に頼って書き換えたり要約したりしない
71
71
  3. SEARCH ブロックには **ファイル内で一意に特定できる範囲** を含める
72
- (同じコードが複数箇所にある場合は、前後の行を含めて一意にする)
72
+ (同じコードが複数箇所にある場合は、前後の行を含めて一意にする)。
73
+ ただし一意に特定できる範囲のうち **最小** (目安 3〜8 行) にする。大きな範囲は
74
+ 一致に失敗しやすい。ファイルの過半が変わるなら rewrite を使う
73
75
  4. パスはプロジェクトルートからの相対パスを / 区切りで書く。絶対パスや .. は使わない
74
76
  5. 同じファイルへの変更は 1 つの FILE セクションにまとめる (セクションを重複させない)
75
77
  6. FILE セクションの間に説明文を書かない。説明はすべて冒頭の CHANGES セクションに書く
@@ -8,6 +8,7 @@ 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 { fixedNewFile, resolveNewFileStyle } from "../core/new-file-style.js";
11
12
  import { parseChangesRecovering } from "../core/parser.js";
12
13
  import { buildBlankInsensitiveNote, buildFailureReport, buildParseErrorReport, } from "../core/report.js";
13
14
  import { readClipboard, writeClipboard } from "../infra/clipboard.js";
@@ -15,10 +16,12 @@ import { loadConfig } from "../infra/config.js";
15
16
  import { findRecentChangesFiles, resolveDownloadsDir, } from "../infra/downloads.js";
16
17
  import { deleteFile, isInsideRoot, readFileState, sha256, writeBytes } from "../infra/files.js";
17
18
  import { gitDirtyFiles } from "../infra/git.js";
19
+ import { collectStyleSampleGroups } from "../infra/new-file-style.js";
18
20
  import { err, out, outEmphasis } from "../infra/term.js";
19
21
  import { beginHistory, createHistoryId, finishHistory, pruneHistory, } from "../infra/history.js";
20
22
  import { confirm } from "../infra/prompt.js";
21
23
  import { findProjectRoot } from "../infra/root.js";
24
+ import { formatVbprojPlan, planVbprojRegistrations } from "./vbproj-register.js";
22
25
  /** このファイルは書き込み対象か (適用済みのみのファイルは書き込み不要なので含めない) */
23
26
  function isApplicable(o) {
24
27
  if (o.alreadyApplied)
@@ -78,6 +81,8 @@ export async function applyCommand(argv) {
78
81
  yes: { type: "boolean", short: "y", default: false },
79
82
  clip: { type: "boolean", default: false },
80
83
  "clip-report": { type: "boolean", default: false },
84
+ // create した .vb の .vbproj 自動登録を今回だけ無効化 (config vbproj.register の一時上書き)
85
+ "no-vbproj": { type: "boolean", default: false },
81
86
  },
82
87
  allowPositionals: true,
83
88
  });
@@ -200,7 +205,22 @@ export async function applyCommand(argv) {
200
205
  }
201
206
  states.set(f.path, readFileState(abs));
202
207
  }
203
- const plan = planChangeSet(changeSet, states, config.newFile);
208
+ // 新規ファイルの形式: encoding "auto" なら作成先の既存ファイルから推定する (設計書 §11.2)
209
+ const newFileBasis = new Map();
210
+ const resolveNewFile = (change) => {
211
+ if (config.newFile.encoding !== "auto" || change.op !== "create") {
212
+ return fixedNewFile(config.newFile);
213
+ }
214
+ // 不正パス・symlink 経由でルート外を指すパスは手本を探さない (検証段階で失敗する)
215
+ if (invalidPathReason(change.path) !== null || states.get(change.path)?.escapesRoot === true) {
216
+ return fixedNewFile(config.newFile);
217
+ }
218
+ const r = resolveNewFileStyle(change.path, config.newFile, collectStyleSampleGroups(root, change.path));
219
+ if (r.basis !== null)
220
+ newFileBasis.set(change.path, r.basis);
221
+ return r.config;
222
+ };
223
+ const plan = planChangeSet(changeSet, states, resolveNewFile);
204
224
  // 3. 失敗があれば何も書き込まずレポート (§4.1, §7)
205
225
  if (!plan.ok && !values.partial) {
206
226
  err(`petari: 検証に失敗しました (${plan.failures.length} 件)。何も書き込んでいません。\n`);
@@ -208,6 +228,20 @@ export async function applyCommand(argv) {
208
228
  return 1;
209
229
  }
210
230
  const applicable = plan.outcomes.filter(isApplicable);
231
+ // create した .vb の旧スタイル .vbproj への登録計画 (設計書 §11.1)。失敗しても create は成功扱い
232
+ const vbprojEnabled = config.vbproj.register && !values["no-vbproj"];
233
+ const vbprojPlan = vbprojEnabled
234
+ ? planVbprojRegistrations(root, applicable)
235
+ : { writes: new Map(), registrations: [], notes: [] };
236
+ const printExtras = () => {
237
+ for (const o of applicable) {
238
+ const basis = newFileBasis.get(o.change.path);
239
+ if (basis !== undefined)
240
+ out(` 新規ファイルの形式: ${o.change.path} → ${basis}`);
241
+ }
242
+ for (const line of formatVbprojPlan(vbprojPlan))
243
+ out(line);
244
+ };
211
245
  if (applicable.length === 0) {
212
246
  // 冪等性: 全変更が適用済みなら成功として終了 (書き込み・履歴なし)
213
247
  if (plan.ok && plan.outcomes.some((o) => o.alreadyApplied)) {
@@ -223,6 +257,7 @@ export async function applyCommand(argv) {
223
257
  if (values["dry-run"]) {
224
258
  out(`dry-run: 適用予定 ${applicable.length} ファイル (書き込みなし)`);
225
259
  printPreview(applicable);
260
+ printExtras();
226
261
  printAlreadyApplied(plan.outcomes);
227
262
  const dryRunNote = buildBlankInsensitiveNote(plan.outcomes);
228
263
  if (dryRunNote !== null)
@@ -238,6 +273,7 @@ export async function applyCommand(argv) {
238
273
  out(`適用予定 ${applicable.length} ファイル:`);
239
274
  for (const o of applicable)
240
275
  out(` ${opLabel(o)}`);
276
+ printExtras();
241
277
  printAlreadyApplied(plan.outcomes);
242
278
  if (plan.failures.length > 0) {
243
279
  out(` (検証失敗 ${plan.failures.length} 件は --partial によりスキップ)`);
@@ -258,6 +294,11 @@ export async function applyCommand(argv) {
258
294
  for (const o of applicable) {
259
295
  before.set(o.change.path, states.get(o.change.path)?.bytes ?? null);
260
296
  }
297
+ // .vbproj 登録も履歴・undo の対象。changes.md の変更対象でもある .vbproj は元の before を保つ
298
+ for (const [path, w] of vbprojPlan.writes) {
299
+ if (!before.has(path))
300
+ before.set(path, w.before);
301
+ }
261
302
  beginHistory(root, id, changesText, before);
262
303
  const after = new Map();
263
304
  for (const o of applicable) {
@@ -267,10 +308,18 @@ export async function applyCommand(argv) {
267
308
  after.set(o.change.path, null);
268
309
  }
269
310
  else {
270
- writeBytes(abs, o.afterBytes);
271
- after.set(o.change.path, o.afterBytes);
311
+ // 同じ .vbproj への登録があれば登録後の内容 (変更後に登録を重ねたもの) を書く
312
+ const bytes = vbprojPlan.writes.get(o.change.path)?.after ?? o.afterBytes;
313
+ writeBytes(abs, bytes);
314
+ after.set(o.change.path, bytes);
272
315
  }
273
316
  }
317
+ for (const [path, w] of vbprojPlan.writes) {
318
+ if (after.has(path))
319
+ continue;
320
+ writeBytes(join(root, path), w.after);
321
+ after.set(path, w.after);
322
+ }
274
323
  const entries = plan.outcomes.map((o) => {
275
324
  const applied = isApplicable(o);
276
325
  const beforeBytes = states.get(o.change.path)?.bytes ?? null;
@@ -294,8 +343,25 @@ export async function applyCommand(argv) {
294
343
  }
295
344
  if (o.alreadyApplied)
296
345
  entry.alreadyApplied = true;
346
+ const registered = vbprojPlan.writes.get(o.change.path)?.registered;
347
+ if (registered !== undefined)
348
+ entry.registered = registered;
297
349
  return entry;
298
350
  });
351
+ for (const [path, w] of vbprojPlan.writes) {
352
+ if (entries.some((e) => e.path === path))
353
+ continue;
354
+ entries.push({
355
+ path,
356
+ op: "vbproj",
357
+ applied: true,
358
+ blocks: 0,
359
+ appliedBlocks: 0,
360
+ registered: w.registered,
361
+ beforeSha256: w.before !== null ? sha256(w.before) : null,
362
+ afterSha256: sha256(w.after),
363
+ });
364
+ }
299
365
  const manifest = {
300
366
  id,
301
367
  appliedAt: new Date().toISOString(),
@@ -314,6 +380,7 @@ export async function applyCommand(argv) {
314
380
  out(`適用しました (履歴 ID: ${id})`);
315
381
  for (const o of applicable)
316
382
  out(` ${opLabel(o)}`);
383
+ printExtras();
317
384
  printAlreadyApplied(plan.outcomes);
318
385
  const appliedNote = buildBlankInsensitiveNote(plan.outcomes);
319
386
  if (appliedNote !== null)
@@ -11,10 +11,11 @@ import { findProjectRoot } from "../infra/root.js";
11
11
  import { err, out } from "../infra/term.js";
12
12
  const CONFIG_TEMPLATE = `{
13
13
  "downloadsDir": null,
14
- "newFile": { "encoding": "utf8", "eol": "lf" },
14
+ "newFile": { "encoding": "auto" },
15
15
  "historyLimit": null,
16
16
  "vscodeCommand": "code",
17
- "clipReportOnFailure": true
17
+ "clipReportOnFailure": true,
18
+ "vbproj": { "register": true }
18
19
  }
19
20
  `;
20
21
  export async function initCommand(argv) {
@@ -22,6 +22,7 @@ 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
24
  import { parseChangesRecovering } from "../core/parser.js";
25
+ import { fixedNewFile } from "../core/new-file-style.js";
25
26
  import { openInBrowser } from "../infra/browser.js";
26
27
  import { loadConfig } from "../infra/config.js";
27
28
  import { monacoVendorDir, startDiffServer } from "../infra/diff-server.js";
@@ -225,7 +226,7 @@ async function runEditServer(root, hdir, id, entries, mine, config, noOpen) {
225
226
  entries: editEntries,
226
227
  root,
227
228
  leftLabel: mine ? "適用直後 (after)" : "適用前 (before)",
228
- fallbackEol: config.newFile.eol,
229
+ fallbackEol: fixedNewFile(config.newFile).eol,
229
230
  });
230
231
  out(`ブラウザ編集サーバーを起動しました: ${handle.url}`);
231
232
  out(" 終了はページ内の「サーバーを終了」ボタンか Ctrl+C (無操作 30 分で自動終了)");
@@ -0,0 +1,159 @@
1
+ /**
2
+ * create した .vb の旧スタイル .vbproj への自動登録 (設計書 §11.1 P1-a) — apply の結線部。
3
+ * 判定・挿入は core/vbproj.ts、探索は infra/vbproj.ts。
4
+ *
5
+ * - 登録の失敗は create 自体の成否に影響しない (レポートに載せるだけ)
6
+ * - 同じ changes.md が .vbproj 自体を変更する場合は、その変更後の内容に対して登録する
7
+ * - 挿入行以外は元バイト列を書き戻す (行単位ドキュメント・§8)
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { basename, dirname, join } from "node:path";
11
+ import { EncodingError, decodeFile, encodeDocument, findUnencodable, } from "../core/encoding.js";
12
+ import { designerParentName, detectSubType, registerCompileItem, toIncludePath, } from "../core/vbproj.js";
13
+ import { isInsideRoot, readFileState } from "../infra/files.js";
14
+ import { findNearestVbproj } from "../infra/vbproj.js";
15
+ const EMPTY = { writes: new Map(), registrations: [], notes: [] };
16
+ function relDirOf(path) {
17
+ const d = dirname(path);
18
+ return d === "." ? "" : d;
19
+ }
20
+ /**
21
+ * 書き込み対象 (applicable) の outcomes から create された .vb を拾い、登録計画を立てる。
22
+ * 書き込みは行わない。
23
+ */
24
+ export function planVbprojRegistrations(root, applicable) {
25
+ const creates = applicable
26
+ .filter((o) => o.change.op === "create" && /\.vb$/i.test(o.change.path))
27
+ // X.vb を X.Designer.vb より先に登録する (VS の並びに合わせる。安定ソート)
28
+ .map((o, i) => ({ o, i, designer: designerParentName(basename(o.change.path)) !== null }))
29
+ .sort((a, b) => Number(a.designer) - Number(b.designer) || a.i - b.i)
30
+ .map((x) => x.o);
31
+ if (creates.length === 0)
32
+ return EMPTY;
33
+ const createdPaths = new Set(applicable.filter((o) => o.change.op === "create").map((o) => o.change.path.toLowerCase()));
34
+ const loaded = new Map();
35
+ const plan = { writes: new Map(), registrations: [], notes: [] };
36
+ const load = (vbproj) => {
37
+ const cached = loaded.get(vbproj);
38
+ if (cached !== undefined)
39
+ return cached;
40
+ const result = loadVbproj(root, vbproj, applicable);
41
+ loaded.set(vbproj, result);
42
+ return result;
43
+ };
44
+ for (const o of creates) {
45
+ const file = o.change.path;
46
+ const lookup = findNearestVbproj(root, relDirOf(file));
47
+ if (lookup.kind === "none") {
48
+ plan.notes.push({ file, message: "vbproj が見つからないため未登録" });
49
+ continue;
50
+ }
51
+ if (lookup.kind === "multiple") {
52
+ plan.notes.push({
53
+ file,
54
+ message: `${lookup.dir} に複数の .vbproj があるため未登録 (${lookup.names.join(", ")})。手動で登録してください`,
55
+ });
56
+ continue;
57
+ }
58
+ const target = load(lookup.path);
59
+ if ("error" in target) {
60
+ plan.notes.push({ file, message: `${lookup.path} に未登録 (${target.error})` });
61
+ continue;
62
+ }
63
+ const include = toIncludePath(relDirOf(lookup.path), file);
64
+ const parent = designerParentName(basename(file));
65
+ const opts = {};
66
+ if (parent !== null) {
67
+ // 親が同じ changes.md で create されるか、既にディスクにあれば DependentUpon を付ける
68
+ const parentPath = relDirOf(file) === "" ? parent : `${relDirOf(file)}/${parent}`;
69
+ if (createdPaths.has(parentPath.toLowerCase()) || existsSync(join(root, parentPath))) {
70
+ opts.dependentUpon = parent;
71
+ }
72
+ }
73
+ else if (o.change.op === "create") {
74
+ const subType = detectSubType(o.change.content);
75
+ if (subType !== undefined)
76
+ opts.subType = subType;
77
+ }
78
+ const r = registerCompileItem(target.lines.map((l) => l.text), include, opts);
79
+ if (r.status === "registered") {
80
+ plan.notes.push({ file, message: `${lookup.path} に登録済み (変更なし)` });
81
+ continue;
82
+ }
83
+ if (r.status === "sdk-style") {
84
+ plan.notes.push({ file, message: `${lookup.path} は SDK スタイルのため登録不要` });
85
+ continue;
86
+ }
87
+ if (r.status === "unsupported") {
88
+ plan.notes.push({
89
+ file,
90
+ message: `${lookup.path} は登録できない形式のため未登録 (${r.reason})。手動で登録してください`,
91
+ });
92
+ continue;
93
+ }
94
+ const bad = findUnencodable(r.inserted.join("\n"), target.doc.encoding);
95
+ if (bad.length > 0) {
96
+ plan.notes.push({
97
+ file,
98
+ message: `${lookup.path} (Shift_JIS) に変換できない文字がパスに含まれるため未登録: ${bad.join(" ")}`,
99
+ });
100
+ continue;
101
+ }
102
+ const inserted = r.inserted.map((text) => ({ text, raw: null, eol: null }));
103
+ target.lines = [...target.lines.slice(0, r.at), ...inserted, ...target.lines.slice(r.at)];
104
+ target.registered.push(file);
105
+ plan.registrations.push({ vbproj: lookup.path, file, include, ...opts });
106
+ }
107
+ for (const [vbproj, t] of loaded) {
108
+ if ("error" in t || t.registered.length === 0)
109
+ continue;
110
+ plan.writes.set(vbproj, {
111
+ before: t.before,
112
+ after: encodeDocument({ ...t.doc, lines: t.lines }),
113
+ registered: t.registered,
114
+ });
115
+ }
116
+ return plan;
117
+ }
118
+ /** .vbproj の現在の内容 (changes.md が同じ .vbproj を変更するならその変更後) を読む */
119
+ function loadVbproj(root, vbproj, applicable) {
120
+ const own = applicable.find((o) => o.change.path === vbproj);
121
+ let bytes;
122
+ if (own !== undefined) {
123
+ if (own.change.op === "delete")
124
+ return { error: "changes.md がこの .vbproj を削除するため" };
125
+ bytes = own.afterBytes;
126
+ }
127
+ else {
128
+ const abs = join(root, vbproj);
129
+ if (!isInsideRoot(root, abs))
130
+ return { error: "プロジェクトルートの外を指しているため" };
131
+ const state = readFileState(abs);
132
+ if (state.symlink)
133
+ return { error: "シンボリックリンクのため" };
134
+ bytes = state.bytes;
135
+ }
136
+ if (bytes === null)
137
+ return { error: "内容を読めないため" };
138
+ try {
139
+ const doc = decodeFile(bytes);
140
+ return { doc, lines: doc.lines, before: bytes, registered: [] };
141
+ }
142
+ catch (e) {
143
+ return { error: e instanceof EncodingError ? e.message : String(e) };
144
+ }
145
+ }
146
+ /** 端末表示 (プレビュー・確認・サマリ共通)。該当がなければ何も出さない */
147
+ export function formatVbprojPlan(plan) {
148
+ const lines = [];
149
+ for (const r of plan.registrations) {
150
+ const extra = [
151
+ r.subType !== undefined ? `SubType: ${r.subType}` : null,
152
+ r.dependentUpon !== undefined ? `DependentUpon: ${r.dependentUpon}` : null,
153
+ ].filter((x) => x !== null);
154
+ lines.push(` vbproj に登録: ${r.vbproj} ← ${r.file}${extra.length > 0 ? ` (${extra.join(", ")})` : ""}`);
155
+ }
156
+ for (const n of plan.notes)
157
+ lines.push(` vbproj: ${n.file} — ${n.message}`);
158
+ return lines;
159
+ }
@@ -242,9 +242,10 @@ function planFile(change, state, newFile) {
242
242
  }
243
243
  /** 全ファイルをドライラン検証し適用計画を立てる (§4.1 手順 2)。書き込みは行わない */
244
244
  export function planChangeSet(changeSet, states, newFile) {
245
+ const resolve = typeof newFile === "function" ? newFile : () => newFile;
245
246
  const outcomes = changeSet.files.map((change) => {
246
247
  const state = states.get(change.path) ?? { exists: false, symlink: false, bytes: null };
247
- return planFile(change, state, newFile);
248
+ return planFile(change, state, resolve(change));
248
249
  });
249
250
  const failures = outcomes.flatMap((o) => o.failures);
250
251
  return { outcomes, failures, ok: failures.length === 0 };
@@ -70,6 +70,7 @@ section.file{border:1px solid var(--border);border-radius:var(--radius);margin:1
70
70
  .op-create{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}
71
71
  .op-delete{color:var(--err);background:color-mix(in srgb,var(--err) 14%,transparent)}
72
72
  .op-rewrite{color:var(--rewrite);background:color-mix(in srgb,var(--rewrite) 14%,transparent)}
73
+ .op-vbproj{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}
73
74
  .dlabels{display:grid;grid-template-columns:1fr 1fr;font-size:.74rem;color:var(--muted);
74
75
  background:var(--surface);border-bottom:1px solid var(--border)}
75
76
  .dlabels div{padding:.28rem .7rem}
@@ -0,0 +1,73 @@
1
+ function majority(values, tieBreak) {
2
+ const counts = new Map();
3
+ for (const v of values)
4
+ counts.set(v, (counts.get(v) ?? 0) + 1);
5
+ let best = null;
6
+ let bestCount = -1;
7
+ for (const [v, n] of counts) {
8
+ if (n > bestCount || (n === bestCount && v === tieBreak)) {
9
+ best = v;
10
+ bestCount = n;
11
+ }
12
+ }
13
+ return best ?? tieBreak;
14
+ }
15
+ function extensionOf(path) {
16
+ const name = path.slice(path.lastIndexOf("/") + 1);
17
+ const dot = name.lastIndexOf(".");
18
+ return dot <= 0 ? "" : name.slice(dot).toLowerCase();
19
+ }
20
+ /** auto 以外: 従来どおり明示値 (eol 既定 lf / bom 既定なし) */
21
+ export function fixedNewFile(setting) {
22
+ const encoding = setting.encoding === "auto" ? "utf8" : setting.encoding;
23
+ return { encoding, bom: setting.bom ?? false, eol: setting.eol ?? "lf" };
24
+ }
25
+ function describe(c) {
26
+ return `${c.encoding}${c.encoding === "utf8" ? (c.bom === true ? " (BOM あり)" : " (BOM なし)") : ""} / ${c.eol}`;
27
+ }
28
+ /**
29
+ * 作成先に近いディレクトリから順に手本を探し、最初に見つかったディレクトリの多数決で決める。
30
+ * 手本がなければ拡張子別の既定 (.vb → utf8 + BOM + crlf、それ以外 → utf8 / BOM なし / lf)。
31
+ * .vb で utf8 かつ BOM なしになった場合は BOM を付ける (BOM なし UTF-8 の日本語は vbc で化ける。
32
+ * ASCII のみの Shift_JIS ファイルが UTF-8 判定される既知の限界への対処でもある)。
33
+ */
34
+ export function resolveNewFileStyle(path, setting, groups) {
35
+ if (setting.encoding !== "auto")
36
+ return { config: fixedNewFile(setting), basis: null };
37
+ const ext = extensionOf(path);
38
+ const isVb = ext === ".vb";
39
+ let config;
40
+ let basis;
41
+ const found = groups
42
+ .map((g) => ({ g, samples: g.sameExt.length > 0 ? g.sameExt : g.others, same: g.sameExt.length > 0 }))
43
+ .find((x) => x.samples.length > 0);
44
+ if (found === undefined) {
45
+ config = isVb
46
+ ? { encoding: "utf8", bom: true, eol: "crlf" }
47
+ : { encoding: "utf8", bom: false, eol: "lf" };
48
+ basis = `手本となる既存ファイルなし → ${isVb ? ".vb" : "拡張子"} の既定`;
49
+ }
50
+ else {
51
+ const { g, samples, same } = found;
52
+ const encoding = majority(samples.map((s) => s.encoding), "utf8");
53
+ const utf8Samples = samples.filter((s) => s.encoding === "utf8");
54
+ const bom = encoding === "utf8" ? majority(utf8Samples.map((s) => s.bom), true) : false;
55
+ const eols = samples.map((s) => s.eol).filter((e) => e !== null);
56
+ const eol = eols.length > 0 ? majority(eols, "crlf") : isVb ? "crlf" : "lf";
57
+ config = { encoding, bom, eol };
58
+ const what = same
59
+ ? `${ext === "" ? "拡張子なし" : ext} ${samples.length} ファイル`
60
+ : `テキスト ${samples.length} ファイル (同じ拡張子の手本なし)`;
61
+ basis = `${g.dir === "." ? "プロジェクト直下" : g.dir} の ${what}の多数決`;
62
+ }
63
+ // 明示指定があれば推定より優先する
64
+ if (setting.bom !== undefined)
65
+ config.bom = setting.bom;
66
+ if (setting.eol !== undefined)
67
+ config.eol = setting.eol;
68
+ if (isVb && config.encoding === "utf8" && config.bom !== true && setting.bom === undefined) {
69
+ config.bom = true;
70
+ basis += " → .vb のため BOM を付与 (BOM なし UTF-8 は vbc で日本語が化ける)";
71
+ }
72
+ return { config, basis: `${describe(config)} (${basis})` };
73
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * 旧スタイル .vbproj への Compile 項目登録 (設計書 §11.1 P1-a)。
3
+ *
4
+ * 旧スタイル .vbproj は <Compile Include> に載っていないファイルをコンパイルしない。
5
+ * AI にこれをやらせるとパックに .vbproj 本体がなく SEARCH が一致しないため、
6
+ * petari が機械的に登録する。実行時依存ゼロの方針により XML パーサは使わず、
7
+ * 行単位の正規表現処理で <ItemGroup> / <Compile Include="..."> を扱う。
8
+ *
9
+ * 純粋関数のみ (ファイル I/O なし)。行配列 in → 挿入位置と挿入行 out。
10
+ * 挿入行以外には触れないため、呼び出し側は既存の行単位ドキュメント (encoding.ts) に
11
+ * 挿入行だけを差し込めばエンコーディング・改行・BOM が保たれる (§8)。
12
+ *
13
+ * 整形が特殊な .vbproj (1 行に複数要素、CDATA、閉じタグ欠落等) は「登録できない形式」として
14
+ * 未登録扱いにし、壊さない。
15
+ */
16
+ /** `<Project Sdk="...">` または Sdk.props の Import があれば SDK スタイル */
17
+ export function isSdkStyleVbproj(lines) {
18
+ const text = lines.join("\n");
19
+ if (/<Project\b[^>]*\bSdk\s*=/i.test(text))
20
+ return true;
21
+ if (/<Import\b[^>]*\bProject\s*=\s*"[^"]*Sdk\.props"/i.test(text))
22
+ return true;
23
+ return /<Sdk\b[^>]*\bName\s*=/i.test(text);
24
+ }
25
+ /** Include 属性値の比較用正規化 (区切り記号と大文字小文字の差は同じファイルとみなす) */
26
+ function normalizeInclude(include) {
27
+ return unescapeXml(include).replace(/\//g, "\\").toLowerCase();
28
+ }
29
+ function escapeXmlAttr(value) {
30
+ return value
31
+ .replace(/&/g, "&amp;")
32
+ .replace(/</g, "&lt;")
33
+ .replace(/>/g, "&gt;")
34
+ .replace(/"/g, "&quot;");
35
+ }
36
+ function unescapeXml(value) {
37
+ return value
38
+ .replace(/&quot;/g, '"')
39
+ .replace(/&apos;/g, "'")
40
+ .replace(/&lt;/g, "<")
41
+ .replace(/&gt;/g, ">")
42
+ .replace(/&amp;/g, "&");
43
+ }
44
+ const RE_ITEMGROUP_OPEN = /^(\s*)<ItemGroup(\s[^<>]*)?>\s*$/i;
45
+ const RE_ITEMGROUP_CLOSE = /^\s*<\/ItemGroup>\s*$/i;
46
+ const RE_COMPILE_SELF = /^(\s*)<Compile\s[^<>]*\/>\s*$/i;
47
+ const RE_COMPILE_OPEN = /^(\s*)<Compile\s[^<>]*[^/]>\s*$/i;
48
+ const RE_COMPILE_CLOSE = /^\s*<\/Compile>\s*$/i;
49
+ const RE_COMPILE_INCLUDE = /<Compile\s[^<>]*\bInclude\s*=\s*"([^"]*)"/i;
50
+ const RE_PROJECT_CLOSE = /^\s*<\/Project>\s*$/i;
51
+ /** 行構造を走査し ItemGroup の一覧を返す。形式が想定外なら文字列で理由を返す */
52
+ function scanItemGroups(lines) {
53
+ const groups = [];
54
+ let current = null;
55
+ let openCompile = false;
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i];
58
+ if (line.includes("<![CDATA["))
59
+ return "CDATA を含む";
60
+ const hasGroupOpen = /<ItemGroup\b/i.test(line);
61
+ const hasGroupClose = /<\/ItemGroup>/i.test(line);
62
+ const hasCompile = /<Compile\b/i.test(line);
63
+ const hasCompileClose = /<\/Compile>/i.test(line);
64
+ if (hasGroupOpen) {
65
+ const m = RE_ITEMGROUP_OPEN.exec(line);
66
+ if (m === null || hasGroupClose || hasCompile)
67
+ return `${i + 1} 行目: <ItemGroup> が単独の行ではない`;
68
+ if (current !== null)
69
+ return `${i + 1} 行目: <ItemGroup> が入れ子になっている`;
70
+ current = {
71
+ open: i,
72
+ close: -1,
73
+ conditional: /\bCondition\s*=/i.test(m[2] ?? ""),
74
+ firstCompile: null,
75
+ indent: m[1] ?? "",
76
+ };
77
+ continue;
78
+ }
79
+ if (hasGroupClose) {
80
+ if (!RE_ITEMGROUP_CLOSE.test(line))
81
+ return `${i + 1} 行目: </ItemGroup> が単独の行ではない`;
82
+ if (current === null)
83
+ return `${i + 1} 行目: 対応する <ItemGroup> がない`;
84
+ if (openCompile)
85
+ return `${i + 1} 行目: <Compile> が閉じられていない`;
86
+ current.close = i;
87
+ groups.push(current);
88
+ current = null;
89
+ continue;
90
+ }
91
+ if (hasCompile) {
92
+ if (current === null)
93
+ return `${i + 1} 行目: <Compile> が <ItemGroup> の外にある`;
94
+ if (openCompile)
95
+ return `${i + 1} 行目: <Compile> が閉じられていない`;
96
+ if (RE_COMPILE_SELF.test(line)) {
97
+ if (current.firstCompile === null)
98
+ current.firstCompile = i;
99
+ continue;
100
+ }
101
+ if (RE_COMPILE_OPEN.test(line) && !hasCompileClose) {
102
+ if (current.firstCompile === null)
103
+ current.firstCompile = i;
104
+ openCompile = true;
105
+ continue;
106
+ }
107
+ return `${i + 1} 行目: <Compile> 要素が 1 行 1 要素になっていない`;
108
+ }
109
+ if (hasCompileClose) {
110
+ if (!openCompile || !RE_COMPILE_CLOSE.test(line))
111
+ return `${i + 1} 行目: </Compile> の位置が不正`;
112
+ openCompile = false;
113
+ }
114
+ }
115
+ if (current !== null)
116
+ return "</ItemGroup> が欠けている";
117
+ return groups;
118
+ }
119
+ function leadingWhitespace(line) {
120
+ return /^\s*/.exec(line)?.[0] ?? "";
121
+ }
122
+ /**
123
+ * 既存の複数行 <Compile> から子要素のインデント増分を推定する。なければ 2 スペース
124
+ * (Visual Studio の既定整形: ` <ItemGroup>` / ` <Compile>` / ` <SubType>`)
125
+ */
126
+ function childIndentDelta(lines) {
127
+ for (let i = 0; i < lines.length - 1; i++) {
128
+ const m = RE_COMPILE_OPEN.exec(lines[i]);
129
+ if (m === null)
130
+ continue;
131
+ const next = lines[i + 1];
132
+ if (RE_COMPILE_CLOSE.test(next))
133
+ continue;
134
+ const child = leadingWhitespace(next);
135
+ const parent = m[1] ?? "";
136
+ if (child.startsWith(parent) && child.length > parent.length)
137
+ return child.slice(parent.length);
138
+ }
139
+ return " ";
140
+ }
141
+ /**
142
+ * relInclude (.vbproj からの相対パス・`\` 区切り) を Compile 項目として登録する位置と行を返す。
143
+ * 判定順: SDK スタイル → 登録済み → 形式検査 → 挿入位置計算。
144
+ */
145
+ export function registerCompileItem(lines, relInclude, opts = {}) {
146
+ if (isSdkStyleVbproj(lines))
147
+ return { status: "sdk-style" };
148
+ const wanted = normalizeInclude(relInclude);
149
+ for (const line of lines) {
150
+ const m = RE_COMPILE_INCLUDE.exec(line);
151
+ if (m !== null && normalizeInclude(m[1] ?? "") === wanted)
152
+ return { status: "registered" };
153
+ }
154
+ if (!lines.some((l) => /<Project\b/i.test(l))) {
155
+ return { status: "unsupported", reason: "<Project> 要素が見つからない" };
156
+ }
157
+ const groups = scanItemGroups(lines);
158
+ if (typeof groups === "string")
159
+ return { status: "unsupported", reason: groups };
160
+ const delta = childIndentDelta(lines);
161
+ const item = (indent) => {
162
+ const include = escapeXmlAttr(relInclude);
163
+ const children = [];
164
+ if (opts.subType !== undefined)
165
+ children.push(`<SubType>${escapeXmlText(opts.subType)}</SubType>`);
166
+ if (opts.dependentUpon !== undefined) {
167
+ children.push(`<DependentUpon>${escapeXmlText(opts.dependentUpon)}</DependentUpon>`);
168
+ }
169
+ if (children.length === 0)
170
+ return [`${indent}<Compile Include="${include}" />`];
171
+ return [
172
+ `${indent}<Compile Include="${include}">`,
173
+ ...children.map((c) => `${indent}${delta}${c}`),
174
+ `${indent}</Compile>`,
175
+ ];
176
+ };
177
+ // Compile 項目を含む最初の (無条件の) ItemGroup の末尾に追加する
178
+ const target = groups.find((g) => g.firstCompile !== null && !g.conditional);
179
+ if (target !== undefined) {
180
+ const indent = leadingWhitespace(lines[target.firstCompile]);
181
+ return { status: "insert", at: target.close, inserted: item(indent) };
182
+ }
183
+ // なければ最後の ItemGroup の後に新しい ItemGroup を作る (ItemGroup が皆無なら </Project> の直前)
184
+ const last = groups[groups.length - 1];
185
+ const projectClose = lines.findIndex((l) => RE_PROJECT_CLOSE.test(l));
186
+ if (last === undefined && projectClose < 0) {
187
+ return { status: "unsupported", reason: "</Project> が見つからない" };
188
+ }
189
+ const groupIndent = last?.indent ?? " ";
190
+ const at = last !== undefined ? last.close + 1 : projectClose;
191
+ return {
192
+ status: "insert",
193
+ at,
194
+ inserted: [`${groupIndent}<ItemGroup>`, ...item(groupIndent + delta), `${groupIndent}</ItemGroup>`],
195
+ };
196
+ }
197
+ function escapeXmlText(value) {
198
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
199
+ }
200
+ /** テスト・表示用: 挿入結果を行配列に反映する */
201
+ export function applyInsertion(lines, result) {
202
+ return [...lines.slice(0, result.at), ...result.inserted, ...lines.slice(result.at)];
203
+ }
204
+ const RE_INHERITS = /^\s*Inherits\s+(?:(?:System\.)?Windows\.Forms\.)?(Form|UserControl)\s*(?:'.*)?$/i;
205
+ /** 新規 .vb の内容から <SubType> を推定する (Inherits Form / UserControl)。該当なしは undefined */
206
+ export function detectSubType(contentLines) {
207
+ for (const line of contentLines) {
208
+ const m = RE_INHERITS.exec(line);
209
+ if (m === null)
210
+ continue;
211
+ const name = m[1].toLowerCase();
212
+ return name === "form" ? "Form" : "UserControl";
213
+ }
214
+ return undefined;
215
+ }
216
+ /** `X.Designer.vb` なら親ファイル名 `X.vb` を返す (大文字小文字は区別しない)。それ以外は null */
217
+ export function designerParentName(fileName) {
218
+ const m = /^(.+)\.designer\.vb$/i.exec(fileName);
219
+ return m === null ? null : `${m[1]}.vb`;
220
+ }
221
+ /** ルート相対パス (/ 区切り) を .vbproj のディレクトリからの相対 Include (\ 区切り) にする */
222
+ export function toIncludePath(vbprojDir, filePath) {
223
+ const base = vbprojDir === "" ? [] : vbprojDir.split("/");
224
+ const target = filePath.split("/");
225
+ let common = 0;
226
+ while (common < base.length && common < target.length && base[common] === target[common])
227
+ common++;
228
+ const ups = base.slice(common).map(() => "..");
229
+ return [...ups, ...target.slice(common)].join("\\");
230
+ }
@@ -7,6 +7,7 @@ export const DEFAULT_CONFIG = {
7
7
  historyLimit: null,
8
8
  vscodeCommand: "code",
9
9
  clipReportOnFailure: true,
10
+ vbproj: { register: true },
10
11
  };
11
12
  /** グローバル設定のパス (§10)。Windows は %APPDATA%、他は XDG (~/.config) */
12
13
  export function globalConfigPath() {
@@ -29,13 +30,21 @@ function readConfigFile(path) {
29
30
  /** JSON 由来の値は型注釈を素通りするため、使う前に実行時検証する */
30
31
  function validateConfig(c) {
31
32
  const enc = c.newFile.encoding;
32
- if (enc !== "utf8" && enc !== "shift_jis") {
33
- throw new Error(`config の newFile.encoding が不正です: ${String(enc)} ("utf8" | "shift_jis")`);
33
+ if (enc !== "utf8" && enc !== "shift_jis" && enc !== "auto") {
34
+ throw new Error(`config の newFile.encoding が不正です: ${String(enc)} ("utf8" | "shift_jis" | "auto")`);
34
35
  }
35
36
  const eol = c.newFile.eol;
36
- if (eol !== "lf" && eol !== "crlf") {
37
+ if (eol !== undefined && eol !== "lf" && eol !== "crlf") {
37
38
  throw new Error(`config の newFile.eol が不正です: ${String(eol)} ("lf" | "crlf")`);
38
39
  }
40
+ const bom = c.newFile.bom;
41
+ if (bom !== undefined && typeof bom !== "boolean") {
42
+ throw new Error(`config の newFile.bom が不正です: ${String(bom)} (true | false)`);
43
+ }
44
+ const register = c.vbproj?.register;
45
+ if (typeof c.vbproj !== "object" || c.vbproj === null || typeof register !== "boolean") {
46
+ throw new Error('config の vbproj が不正です ({ "register": true | false } を指定)');
47
+ }
39
48
  const limit = c.historyLimit;
40
49
  if (limit !== null && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1)) {
41
50
  throw new Error(`config の historyLimit が不正です: ${String(limit)} (null または 1 以上の整数)`);
@@ -59,10 +68,21 @@ export function loadConfig(root) {
59
68
  ...DEFAULT_CONFIG,
60
69
  ...global,
61
70
  ...project,
62
- newFile: {
63
- ...DEFAULT_CONFIG.newFile,
64
- ...(global.newFile ?? {}),
65
- ...(project.newFile ?? {}),
71
+ newFile: mergeNewFile(global.newFile, project.newFile),
72
+ vbproj: {
73
+ ...DEFAULT_CONFIG.vbproj,
74
+ ...(global.vbproj ?? {}),
75
+ ...(project.vbproj ?? {}),
66
76
  },
67
77
  });
68
78
  }
79
+ /**
80
+ * newFile の合成。encoding "auto" のときは既定の eol: "lf" / bom を混ぜない
81
+ * (混ぜると eol / BOM の推定が効かなくなる)。ユーザーが明示した eol / bom は auto でも残る
82
+ */
83
+ function mergeNewFile(global, project) {
84
+ const user = { ...(global ?? {}), ...(project ?? {}) };
85
+ if (user.encoding === "auto")
86
+ return user;
87
+ return { ...DEFAULT_CONFIG.newFile, ...user };
88
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * 新規ファイルの形式推定に使う手本の収集 (設計書 §11.2)。ファイル I/O のみ。
3
+ * 多数決と既定の決定は core/new-file-style.ts。
4
+ */
5
+ import { readdirSync, readFileSync, statSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { decodeFile } from "../core/encoding.js";
8
+ /** 1 ディレクトリで読む手本の上限と 1 ファイルの上限 (I/O を抑える) */
9
+ const MAX_FILES_PER_DIR = 100;
10
+ const MAX_FILE_BYTES = 1024 * 1024;
11
+ function extensionOf(name) {
12
+ const dot = name.lastIndexOf(".");
13
+ return dot <= 0 ? "" : name.slice(dot).toLowerCase();
14
+ }
15
+ function sampleOf(absPath) {
16
+ let bytes;
17
+ try {
18
+ if (statSync(absPath).size > MAX_FILE_BYTES)
19
+ return null;
20
+ bytes = new Uint8Array(readFileSync(absPath));
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ if (bytes.length === 0 || bytes.includes(0))
26
+ return null; // 空 / バイナリは手本にしない
27
+ try {
28
+ const doc = decodeFile(bytes);
29
+ return { encoding: doc.encoding, bom: doc.hasBom, eol: doc.eol };
30
+ }
31
+ catch {
32
+ return null; // UTF-8 でも Shift_JIS でもないものは対象外
33
+ }
34
+ }
35
+ function collectDir(absDir, dir, ext) {
36
+ const group = { dir, sameExt: [], others: [] };
37
+ let entries;
38
+ try {
39
+ entries = readdirSync(absDir, { withFileTypes: true });
40
+ }
41
+ catch {
42
+ return group; // 未作成ディレクトリ (create 先)
43
+ }
44
+ const files = entries
45
+ .filter((e) => e.isFile() && !e.name.startsWith("."))
46
+ .map((e) => e.name)
47
+ .sort()
48
+ .slice(0, MAX_FILES_PER_DIR);
49
+ for (const name of files) {
50
+ const s = sampleOf(join(absDir, name));
51
+ if (s === null)
52
+ continue;
53
+ if (extensionOf(name) === ext)
54
+ group.sameExt.push(s);
55
+ else
56
+ group.others.push(s);
57
+ }
58
+ return group;
59
+ }
60
+ /**
61
+ * ルート相対パス path の作成先ディレクトリからルートまで、各ディレクトリの手本を近い順に集める。
62
+ * 手本が見つかったディレクトリで打ち切る (core 側は最初の非空グループを採用する)。
63
+ */
64
+ export function collectStyleSampleGroups(root, path) {
65
+ const ext = extensionOf(path.slice(path.lastIndexOf("/") + 1));
66
+ const groups = [];
67
+ let dir = dirname(path);
68
+ if (dir === ".")
69
+ dir = "";
70
+ for (;;) {
71
+ const group = collectDir(dir === "" ? root : join(root, dir), dir === "" ? "." : dir, ext);
72
+ groups.push(group);
73
+ if (group.sameExt.length > 0 || group.others.length > 0)
74
+ break;
75
+ if (dir === "")
76
+ break;
77
+ const parent = dirname(dir);
78
+ dir = parent === "." ? "" : parent;
79
+ }
80
+ return groups;
81
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 最寄り .vbproj の探索 (設計書 §11.1)。ファイル I/O のみ。判定・挿入は core/vbproj.ts。
3
+ */
4
+ import { readdirSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ function listVbproj(absDir) {
7
+ try {
8
+ return readdirSync(absDir, { withFileTypes: true })
9
+ // 書き込み対象になるため symlink は除外する (Dirent 判定・§9)
10
+ .filter((e) => e.isFile() && /\.vbproj$/i.test(e.name))
11
+ .map((e) => e.name)
12
+ .sort();
13
+ }
14
+ catch {
15
+ return null; // 未作成ディレクトリ (create 先) は手本なしとして上へ辿る
16
+ }
17
+ }
18
+ /**
19
+ * ルート相対ディレクトリ relDir から上へ辿り、プロジェクトルートまでの間で最寄りの .vbproj を探す。
20
+ * ルートの外には出ない。
21
+ */
22
+ export function findNearestVbproj(root, relDir) {
23
+ let dir = relDir === "." ? "" : relDir;
24
+ for (;;) {
25
+ const names = listVbproj(dir === "" ? root : join(root, dir));
26
+ if (names !== null && names.length === 1) {
27
+ return { kind: "found", path: dir === "" ? names[0] : `${dir}/${names[0]}` };
28
+ }
29
+ if (names !== null && names.length > 1)
30
+ return { kind: "multiple", dir: dir === "" ? "." : dir, names };
31
+ if (dir === "")
32
+ return { kind: "none" };
33
+ const parent = dirname(dir);
34
+ dir = parent === "." ? "" : parent;
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "petari",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "petari — paste AI chat patches onto your codebase",
5
5
  "keywords": [
6
6
  "cli",