create-einja-app 0.3.3 → 0.3.4

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.
@@ -0,0 +1,596 @@
1
+ # JSON 配布メカニズムの統一実装(ブラックリスト方式 + 3方向マージ)
2
+
3
+ ## Context
4
+
5
+ テンプレートリポジトリの JSON ファイル変更が配布先に反映されない。
6
+ 全JSONファイルでテンプレート側と配布先側の双方向変更が発生しうるが、適切なマージ戦略が設定されていない。
7
+
8
+ ### 現状の問題
9
+
10
+ | JSONファイル | copy-presetsコピー | einja syncカテゴリ | jsonPaths設定 |
11
+ |---|---|---|---|
12
+ | `package.json` | **No** | **なし** | なし |
13
+ | `.claude/settings.json` | Yes | **なし** | なし |
14
+ | `.vscode/settings.json` | Yes | `tools` | なし |
15
+ | `.mcp.json` | Yes (optional) | **なし** | なし |
16
+
17
+ - `sync.ts:435` の `fileName` がファイル名のみ(`.split("/").pop()`)で、フルパスの jsonPaths キーとマッチしない
18
+ - 全JSONの `jsonPaths` が空 → デフォルトのディープマージ(新規キー追加のみ、既存キー保持)
19
+ - 現在の `project-private` とデフォルトの動作が実質同じ(`json-processor.ts:131-160`)→ 除外が効いていない
20
+ - 現在のマージは template vs local の2点比較で、true な3方向マージ(base比較)がない
21
+
22
+ ### 変更後の状態
23
+
24
+ | JSONファイル | copy-presetsコピー | einja syncカテゴリ | jsonPaths設定 |
25
+ |---|---|---|---|
26
+ | `package.json` | **Yes** (Step 5) | **root-config** (Step 6) | project-private: name, version, private, workspaces, packageManager, volta |
27
+ | `.claude/settings.json` | Yes | **claude-config** (Step 6) | managed: plansDirectory, includeCoAuthoredBy |
28
+ | `.vscode/settings.json` | Yes | `tools` (既存) | managed: editor.*, eslint.*, prettier.*, [json], [jsonc] |
29
+ | `.mcp.json` | Yes | **root-config** (Step 6) | デフォルト(全キー3方向マージ) |
30
+
31
+ ## 設計方針: ブラックリスト方式
32
+
33
+ ### 旧設計(ホワイトリスト)の問題
34
+
35
+ ```
36
+ tracked: ["scripts", "lint-staged"] ← これだけsync
37
+ project-private: ["*"] ← 残り全部除外
38
+ ```
39
+ → `devDependencies`(husky, turbo 等の共通ツール依存)がsync対象外
40
+ → 新セクション追加時に tracked リスト更新が必要
41
+
42
+ ### 新設計(ブラックリスト)
43
+
44
+ ```
45
+ managed: { ... } ← テンプレート強制上書き
46
+ project-private: { ... } ← ブラックリスト(完全除外)
47
+ デフォルト: 3方向マージ ← 上記以外は全て3方向マージ
48
+ ```
49
+
50
+ - `tracked` モードは廃止。デフォルト動作が3方向マージ
51
+ - `project-private` = **完全除外**(テンプレートにあっても追加しない。初回syncでも追加しない)
52
+ - `*` ワイルドカードは不要(ブラックリストなので除外対象を個別指定)
53
+
54
+ ### jsonPaths 設定
55
+
56
+ ```
57
+ jsonPaths:
58
+ managed: {
59
+ ".claude/settings.json": ["plansDirectory", "includeCoAuthoredBy"],
60
+ ".vscode/settings.json": [
61
+ "editor.codeActionsOnSave", "editor.defaultFormatter", "editor.formatOnSave",
62
+ "eslint.enable", "prettier.enable", "prettier.useEditorConfig", "[json]", "[jsonc]"
63
+ ],
64
+ },
65
+ "project-private": {
66
+ "package.json": ["name", "version", "private", "workspaces", "packageManager", "volta"],
67
+ }
68
+ ```
69
+
70
+ ### セクション別マージ戦略(ファイル別)
71
+
72
+ #### package.json
73
+
74
+ | セクション | モード | 動作 |
75
+ |-----------|--------|------|
76
+ | `name`, `version`, `private`, `workspaces`, `packageManager`, `volta` | project-private | 完全除外(テンプレートから追加しない) |
77
+ | `scripts`, `lint-staged`, `devDependencies`, `dependencies` 等 | デフォルト | 3方向マージ |
78
+
79
+ #### .claude/settings.json
80
+
81
+ | セクション | モード | 動作 |
82
+ |-----------|--------|------|
83
+ | `plansDirectory`, `includeCoAuthoredBy` | managed | テンプレート強制上書き |
84
+ | `permissions`, `hooks`, `env`, `enabledPlugins` 等 | デフォルト | 3方向マージ |
85
+
86
+ #### .vscode/settings.json
87
+
88
+ | セクション | モード | 動作 |
89
+ |-----------|--------|------|
90
+ | `editor.*`, `eslint.*`, `prettier.*`, `[json]`, `[jsonc]` | managed | テンプレート強制上書き |
91
+ | ユーザー追加の拡張設定 | デフォルト | 3方向マージ |
92
+
93
+ #### .mcp.json
94
+
95
+ | セクション | モード | 動作 |
96
+ |-----------|--------|------|
97
+ | 全セクション | デフォルト | 3方向マージ(テンプレートが新MCP追加、ユーザーが独自MCP追加・ポート変更等) |
98
+
99
+ ## 3方向マージのルール
100
+
101
+ base(前回sync時のテンプレート)・local・template の3つを比較し、キー別に判定:
102
+
103
+ ```
104
+ キー別判定:
105
+ base→local 変更なし + base→template 変更あり → テンプレート適用
106
+ base→local 変更あり + base→template 変更なし → ローカル保持
107
+ 両方変更 + 同じ値 → コンフリクトなし
108
+ 両方変更 + 異なる値 → コンフリクト ⚠️(ローカル保持)
109
+ ローカルで削除 + テンプレート変更なし → 削除を維持
110
+ ローカルで削除 + テンプレート変更あり → コンフリクト ⚠️
111
+ テンプレートで新キー追加 → 追加
112
+ ローカルで新キー追加 → 保持
113
+ ```
114
+
115
+ ### 配列値のマージ
116
+
117
+ 配列は**値として丸ごと扱う**(要素単位のマージは対象外)。
118
+ base/local/template の値全体を `deepEqual` で比較し、コンフリクト判定する。
119
+
120
+ ### 初回sync(base なし)の動作
121
+
122
+ base がない場合は **ローカル優先 + テンプレートの新規キーのみ追加**:
123
+ - ローカルに既存のキー → ローカル値を保持
124
+ - テンプレートにしかないキー → 追加
125
+ - `project-private` のキー → 追加しない(完全除外)
126
+
127
+ ### コンフリクト時の動作
128
+
129
+ - **ローカル値を保持**(安全側にデフォルト)
130
+ - コンソールに警告出力
131
+ - `mergeJson` の戻り値に `conflicts` 配列を追加
132
+
133
+ ## 配布タイミング
134
+
135
+ ```
136
+ ルート JSONファイル群(Single Source of Truth)
137
+
138
+ ├─【ビルド時】copy-presets.mjs
139
+ │ ├─→ presets/default/package.json (新規: フルコピー)
140
+ │ ├─→ presets/default/.claude/settings.json (既存: フルコピー)
141
+ │ ├─→ presets/default/.vscode/settings.json (既存: フルコピー)
142
+ │ ├─→ presets/default/.mcp.json (既存: フルコピー)
143
+ │ └─→ templates/default/package.json (create-einja-app 用: scripts同期)
144
+
145
+ ├─【einja sync】JsonProcessor 3方向マージ
146
+ │ └─→ 配布先の各JSONを managed/project-private/デフォルト(3way) でマージ
147
+
148
+ └─【create-einja-app】テンプレートからコピー
149
+ └─→ 新規プロジェクトの各JSONファイル
150
+ ```
151
+
152
+ ## 実装ステップ
153
+
154
+ ### Step 0: 型定義の更新
155
+
156
+ **ファイル**: `packages/cli/src/types/sync.ts`
157
+
158
+ 1. `FileMetadataSchema` に `baseContent` フィールド追加(前回sync時のテンプレートコンテンツ保存用)
159
+ 2. `JsonPathsConfigSchema` から `tracked` を**削除**(ブラックリスト方式では不要)
160
+
161
+ ```typescript
162
+ export const FileMetadataSchema = z.object({
163
+ hash: z.string(),
164
+ syncedAt: z.string(),
165
+ conflicts: z.array(z.string()).optional(),
166
+ baseContent: z.string().optional(), // 前回sync時のテンプレートコンテンツ(3方向マージ用)
167
+ });
168
+
169
+ export const JsonPathsConfigSchema = z.object({
170
+ managed: z.record(z.string(), z.array(z.string())),
171
+ "project-private": z.record(z.string(), z.array(z.string())),
172
+ });
173
+ ```
174
+
175
+ ### Step 1: `sync.ts` の `fileName` → フルパス修正
176
+
177
+ **ファイル**: `packages/cli/src/commands/sync.ts`
178
+
179
+ `sync.ts:435` と dry-run パス(`sync.ts:287`付近)の両方を修正:
180
+
181
+ ```typescript
182
+ // 変更前
183
+ const fileName = target.path.split("/").pop() || target.path;
184
+
185
+ // 変更後
186
+ const filePath = target.path;
187
+ ```
188
+
189
+ ### Step 2: `JsonProcessor` の全面改修
190
+
191
+ **ファイル**: `packages/cli/src/lib/sync/json-processor.ts`
192
+
193
+ #### 2a. 戻り値の型を追加
194
+
195
+ ```typescript
196
+ export interface JsonConflict {
197
+ keyPath: string;
198
+ baseValue: unknown;
199
+ localValue: unknown;
200
+ templateValue: unknown;
201
+ }
202
+
203
+ export interface JsonMergeResult {
204
+ result: Record<string, unknown>;
205
+ conflicts: JsonConflict[];
206
+ }
207
+ ```
208
+
209
+ #### 2b. `mergeJson` シグネチャ変更
210
+
211
+ ```typescript
212
+ mergeJson(
213
+ templateJson: Record<string, unknown>,
214
+ localJson: Record<string, unknown> | null,
215
+ jsonPaths: JsonPathsConfig,
216
+ filePath: string,
217
+ baseJson?: Record<string, unknown> | null
218
+ ): JsonMergeResult
219
+ ```
220
+
221
+ - `localJson === null`(ファイル未存在): project-private 以外のテンプレートキーを採用
222
+ - `baseJson` なし(初回sync): ローカル優先 + テンプレート新規キーのみ追加
223
+
224
+ #### 2c. `deepMergeWithPaths` に3方向マージを統合
225
+
226
+ `merge3WaySection` は廃止。`deepMergeWithPaths` 自体が base を受け取り、各キーで:
227
+ 1. managed チェック → テンプレート強制上書き
228
+ 2. project-private チェック → ローカル保持(テンプレートから追加しない)
229
+ 3. デフォルト → 3方向マージ(base があれば)/ 新規キー追加(base なし)
230
+
231
+ ```typescript
232
+ private deepMergeWithPaths(
233
+ template: Record<string, unknown>,
234
+ existing: Record<string, unknown>,
235
+ jsonPaths: JsonPathsConfig,
236
+ filePath: string,
237
+ currentPath: string,
238
+ conflicts: JsonConflict[],
239
+ base?: Record<string, unknown> | null
240
+ ): Record<string, unknown> {
241
+ const result = { ...existing }; // ローカルをベースに開始
242
+
243
+ // テンプレート側のキーを処理
244
+ for (const [key, templateValue] of Object.entries(template)) {
245
+ const keyPath = currentPath ? `${currentPath}.${key}` : key;
246
+
247
+ if (this.isPathManaged(filePath, keyPath, jsonPaths)) {
248
+ // managed: テンプレート強制上書き
249
+ result[key] = this.deepClone(templateValue);
250
+
251
+ } else if (this.isPathProjectPrivate(filePath, keyPath, jsonPaths)) {
252
+ // project-private: ローカル保持、テンプレートから追加しない
253
+ // existing に key があればそのまま(result = { ...existing } で既に含まれる)
254
+ // existing に key がなければ何もしない(追加しない = 完全除外)
255
+ continue;
256
+
257
+ } else if (base) {
258
+ // デフォルト(base あり): 3方向マージ
259
+ const baseValue = base[key];
260
+ const localValue = existing[key];
261
+
262
+ if (this.deepEqual(localValue, baseValue)) {
263
+ // ローカル未変更 → テンプレートの値を採用
264
+ if (templateValue !== undefined) {
265
+ result[key] = this.isObject(templateValue) && this.isObject(localValue)
266
+ ? this.deepMergeWithPaths(
267
+ templateValue as Record<string, unknown>,
268
+ localValue as Record<string, unknown>,
269
+ jsonPaths, filePath, keyPath, conflicts,
270
+ baseValue as Record<string, unknown> | null
271
+ )
272
+ : this.deepClone(templateValue);
273
+ } else {
274
+ delete result[key]; // テンプレートで削除
275
+ }
276
+ } else if (this.deepEqual(templateValue, baseValue)) {
277
+ // テンプレート未変更 → ローカルの値を保持(何もしない)
278
+ } else if (this.deepEqual(localValue, templateValue)) {
279
+ // 同じ値に変更 → コンフリクトなし
280
+ } else {
281
+ // 両方変更、異なる値 → コンフリクト(ローカル保持)
282
+ conflicts.push({ keyPath, baseValue, localValue, templateValue });
283
+ }
284
+
285
+ } else {
286
+ // デフォルト(base なし = 初回sync): ローカル優先 + テンプレート新規キーのみ追加
287
+ if (!(key in existing)) {
288
+ result[key] = this.deepClone(templateValue);
289
+ } else if (this.isObject(existing[key]) && this.isObject(templateValue)) {
290
+ // オブジェクト型: 再帰(新規キーのみ追加)
291
+ result[key] = this.deepMergeWithPaths(
292
+ templateValue as Record<string, unknown>,
293
+ existing[key] as Record<string, unknown>,
294
+ jsonPaths, filePath, keyPath, conflicts, null
295
+ );
296
+ }
297
+ // プリミティブ型で既存あり: ローカル保持(何もしない)
298
+ }
299
+ }
300
+
301
+ // base にあってテンプレートから削除されたキーの処理(base あり時のみ)
302
+ if (base) {
303
+ for (const key of Object.keys(base)) {
304
+ if (!(key in template) && key in existing) {
305
+ const keyPath = currentPath ? `${currentPath}.${key}` : key;
306
+ if (this.isPathProjectPrivate(filePath, keyPath, jsonPaths)) continue;
307
+ const baseValue = base[key];
308
+ const localValue = existing[key];
309
+ if (this.deepEqual(localValue, baseValue)) {
310
+ // ローカル未変更 + テンプレート削除 → 削除
311
+ delete result[key];
312
+ } else {
313
+ // ローカル変更あり + テンプレート削除 → コンフリクト(ローカル保持)
314
+ conflicts.push({ keyPath, baseValue, localValue, templateValue: undefined });
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ return result;
321
+ }
322
+ ```
323
+
324
+ **ネスト指定サポート**: `deepMergeWithPaths` が再帰するため、`devDependencies.@types/node` のようなネスト指定の project-private が自然に動く。
325
+
326
+ #### 2d. `isPathProjectPrivate` の動作修正
327
+
328
+ 現在の実装は「既存にない場合はテンプレートから追加」でデフォルトと同じ(`json-processor.ts:131-146`)。
329
+ ブラックリスト方式では**完全除外**に修正:
330
+ - テンプレートに存在してもローカルに追加しない
331
+ - 初回syncでも追加しない
332
+ - `deepMergeWithPaths` 内で `continue` するだけ(上記 2c 参照)
333
+
334
+ #### 2e. `isPathTracked` を削除(不要)
335
+
336
+ #### 2f. `deepEqual` ユーティリティ(キー順序非依存の深い比較)
337
+
338
+ ```typescript
339
+ private deepEqual(a: unknown, b: unknown): boolean {
340
+ if (a === b) return true;
341
+ if (a === null || b === null || typeof a !== typeof b) return false;
342
+ if (Array.isArray(a) && Array.isArray(b)) {
343
+ if (a.length !== b.length) return false;
344
+ return a.every((item, i) => this.deepEqual(item, b[i]));
345
+ }
346
+ if (typeof a === "object" && typeof b === "object") {
347
+ const keysA = Object.keys(a as Record<string, unknown>).sort();
348
+ const keysB = Object.keys(b as Record<string, unknown>).sort();
349
+ if (keysA.length !== keysB.length) return false;
350
+ return keysA.every((key, i) =>
351
+ key === keysB[i] && this.deepEqual(
352
+ (a as Record<string, unknown>)[key],
353
+ (b as Record<string, unknown>)[key]
354
+ )
355
+ );
356
+ }
357
+ return false;
358
+ }
359
+ ```
360
+
361
+ ### Step 3: `sync.ts` の JSON 処理フロー変更
362
+
363
+ **ファイル**: `packages/cli/src/commands/sync.ts`
364
+
365
+ 本処理パス + dry-run パスの**両方**:
366
+
367
+ 1. `mergeJson` に base を渡す
368
+ 2. 戻り値を `JsonMergeResult` に変更
369
+ 3. コンフリクト警告出力
370
+
371
+ ```typescript
372
+ const fileMetadata = metadata.files[target.path];
373
+ const baseJson = fileMetadata?.baseContent
374
+ ? JSON.parse(fileMetadata.baseContent) as Record<string, unknown>
375
+ : null;
376
+ const mergeResult = jsonProcessor.mergeJson(templateJson, localJson, jsonPaths, filePath, baseJson);
377
+ const mergedContent = `${JSON.stringify(mergeResult.result, null, 2)}\n`;
378
+
379
+ if (mergeResult.conflicts.length > 0) {
380
+ for (const conflict of mergeResult.conflicts) {
381
+ console.warn(` ⚠️ JSON コンフリクト: ${target.path} → ${conflict.keyPath}`);
382
+ console.warn(` ローカル値を保持(テンプレート値: ${JSON.stringify(conflict.templateValue)})`);
383
+ }
384
+ }
385
+ ```
386
+
387
+ ### Step 4: `MetadataManager` の更新
388
+
389
+ **ファイル**: `packages/cli/src/lib/sync/metadata-manager.ts`
390
+
391
+ 1. `updateFileHash` に `baseContent` パラメータ追加(一元管理)
392
+ 2. デフォルト jsonPaths を設定(ブラックリスト方式)
393
+
394
+ ```typescript
395
+ updateFileHash(filePath: string, hash: string, conflicts?: string[], baseContent?: string): void {
396
+ this.metadata.files[filePath] = {
397
+ hash,
398
+ syncedAt: new Date().toISOString(),
399
+ ...(conflicts && { conflicts }),
400
+ ...(baseContent && { baseContent }),
401
+ };
402
+ }
403
+
404
+ private getDefaultMetadata(): SyncMetadata {
405
+ return {
406
+ // ...
407
+ jsonPaths: {
408
+ managed: {
409
+ ".claude/settings.json": ["plansDirectory", "includeCoAuthoredBy"],
410
+ ".vscode/settings.json": [
411
+ "editor.codeActionsOnSave", "editor.defaultFormatter", "editor.formatOnSave",
412
+ "eslint.enable", "prettier.enable", "prettier.useEditorConfig", "[json]", "[jsonc]"
413
+ ],
414
+ },
415
+ "project-private": {
416
+ "package.json": ["name", "version", "private", "workspaces", "packageManager", "volta"],
417
+ },
418
+ },
419
+ };
420
+ }
421
+ ```
422
+
423
+ ### Step 5: `copy-presets.mjs` に package.json コピー + テンプレート同期
424
+
425
+ **ファイル**: `packages/cli/scripts/copy-presets.mjs`
426
+
427
+ 1. `fileMappings` に package.json フルコピーを追加
428
+ 2. `create-einja-app` テンプレートの scripts 同期関数を追加(scripts + lint-staged をルートから同期)
429
+
430
+ ### Step 6: `file-filter.ts` にカテゴリ追加
431
+
432
+ **ファイル**: `packages/cli/src/lib/sync/file-filter.ts`
433
+
434
+ `CATEGORY_MAPPING` に追加:
435
+
436
+ ```typescript
437
+ "root-config": ".", // package.json, .mcp.json
438
+ "claude-config": ".claude", // .claude/settings.json
439
+ ```
440
+
441
+ 各カテゴリの特別処理(`env` / `tools` と同じパターンで、対象ファイルをフィルタ)。
442
+
443
+ ### Step 7: ドキュメント更新
444
+
445
+ #### 7a. `docs/einja/instructions/setup-flow.md`
446
+
447
+ 1. sync カテゴリ一覧に追加:
448
+ ```
449
+ | `root-config` | `package.json`, `.mcp.json` | ルート設定ファイル |
450
+ | `claude-config` | `.claude/settings.json` | Claude Code設定 |
451
+ ```
452
+
453
+ 2. JSON マージ方式テーブルを更新(3モード構成に書き換え):
454
+ ```
455
+ | モード | 動作 | 設定方法 |
456
+ |--------|------|---------|
457
+ | `managed` | テンプレート値で強制上書き | jsonPaths.managed にパス指定 |
458
+ | `project-private` | 完全除外(テンプレートから追加しない) | jsonPaths["project-private"] にパス指定 |
459
+ | デフォルト | 3方向マージ(base/local/template比較、コンフリクト検出) | 上記以外の全パス |
460
+ ```
461
+
462
+ 3. JSON同期動作テーブルを追加:
463
+ ```
464
+ #### JSON ファイルの同期動作(3方向マージ)
465
+
466
+ | 操作 | 結果 |
467
+ |------|------|
468
+ | テンプレートに新キーが追加された(利用者は未変更) | sync時に利用者のファイルに追加される |
469
+ | 利用者が独自キーを追加した | 保持される |
470
+ | 利用者がテンプレート由来のキーを削除(テンプレート側は未変更) | 削除が維持される |
471
+ | 利用者がテンプレート由来のキーを変更(テンプレート側は未変更) | 利用者の変更が保持される |
472
+ | テンプレートがキーを更新(利用者側は未変更) | テンプレートの更新が自動適用される |
473
+ | 両方が同じキーを異なる値に変更 | コンフリクト警告(利用者の値を保持) |
474
+ | project-private 指定のキー | テンプレートから一切追加・変更されない |
475
+ | managed 指定のキー | テンプレート値で常に上書き |
476
+ ```
477
+
478
+ 4. ファイル別の jsonPaths 設定テーブルを追加:
479
+ ```
480
+ #### ファイル別 jsonPaths 設定
481
+
482
+ | ファイル | managed | project-private | 残り |
483
+ |---------|---------|----------------|------|
484
+ | `package.json` | — | name, version, private, workspaces, packageManager, volta | 3方向マージ |
485
+ | `.claude/settings.json` | plansDirectory, includeCoAuthoredBy | — | 3方向マージ |
486
+ | `.vscode/settings.json` | editor.*, eslint.*, prettier.*, [json], [jsonc] | — | 3方向マージ |
487
+ | `.mcp.json` | — | — | 3方向マージ |
488
+ ```
489
+
490
+ 5. 初回sync・base保存の仕組みを説明:
491
+ ```
492
+ #### base スナップショット
493
+
494
+ 3方向マージには「前回sync時のテンプレート内容」(base)が必要。
495
+ `.einja-sync.json` の `baseContent` フィールドに保存される。
496
+
497
+ - 初回sync(baseなし): ローカル優先 + テンプレートの新規キーのみ追加
498
+ - 2回目以降: base/local/template の3方向比較でマージ
499
+ ```
500
+
501
+ #### 7b. `.claude/skills/cli-package-specs/SKILL.md`
502
+
503
+ セクション 4 と 5 の間に「4.5 JSON マージ仕様」を追加:
504
+
505
+ ```markdown
506
+ ### 4.5 JSON マージ仕様
507
+
508
+ **実装**: `packages/cli/src/lib/sync/json-processor.ts`
509
+ **設定**: `.einja-sync.json` の `jsonPaths` フィールド
510
+
511
+ #### マージモード(ブラックリスト方式)
512
+
513
+ | モード | 動作 | 用途 |
514
+ |--------|------|------|
515
+ | `managed` | テンプレート値で強制上書き | テンプレートが完全管理するセクション |
516
+ | `project-private` | 完全除外(テンプレートから追加・更新しない) | プロジェクト固有のセクション |
517
+ | デフォルト | base/local/templateの3方向マージ | 上記以外の全パス |
518
+
519
+ #### ネスト指定
520
+
521
+ パスはドット区切りでネスト指定可能。`deepMergeWithPaths` の再帰により
522
+ 各レベルで jsonPaths チェックが行われる。
523
+
524
+ 例: `"project-private": { "package.json": ["devDependencies.@types/node"] }`
525
+ → devDependencies 全体は3方向マージ、@types/node のみ除外
526
+
527
+ #### 設定例
528
+
529
+ jsonPaths:
530
+ managed: { ".claude/settings.json": ["plansDirectory"] }
531
+ project-private: { "package.json": ["name", "version", "private", "workspaces"] }
532
+
533
+ → plansDirectory はテンプレート強制上書き
534
+ → name, version 等は完全除外
535
+ → scripts, devDependencies 等は3方向マージ
536
+
537
+ #### base スナップショット
538
+
539
+ 3方向マージには前回sync時のテンプレート内容(base)が必要。
540
+ `.einja-sync.json` の各ファイルメタデータに `baseContent` として保存。
541
+ 初回sync(base なし)はローカル優先 + テンプレート新規キーのみ追加。
542
+
543
+ #### コンフリクト
544
+
545
+ 両方が同じキーを異なる値に変更した場合:
546
+ - ローカル値を保持(安全側)
547
+ - コンソールに警告出力
548
+ - `mergeJson` の戻り値 `conflicts` 配列で呼び出し元にも通知
549
+ ```
550
+
551
+ ファイルマッピングテーブルに追加:
552
+ ```
553
+ | `package.json`(ルート) | `package.json` | root-config |
554
+ ```
555
+
556
+ #### 7c. `CLAUDE.md`
557
+
558
+ 二重管理禁止テーブルに追加:
559
+ ```
560
+ | `package.json`(ルート) | `presets/default/package.json` | フルコピー |
561
+ ```
562
+
563
+ ## 変更対象ファイル
564
+
565
+ | ファイル | 変更内容 |
566
+ |---------|---------|
567
+ | `packages/cli/src/types/sync.ts` | `tracked` 削除、`baseContent` 追加 |
568
+ | `packages/cli/src/commands/sync.ts` | fileName→フルパス、base渡し(本処理+dry-run両方)、戻り値型変更、コンフリクト出力 |
569
+ | `packages/cli/src/lib/sync/json-processor.ts` | `deepMergeWithPaths` に3方向マージ統合、`project-private` を完全除外に修正、`deepEqual` 追加、`isPathTracked` 削除 |
570
+ | `packages/cli/src/lib/sync/metadata-manager.ts` | `updateFileHash` に baseContent追加、デフォルト jsonPaths をブラックリスト方式に |
571
+ | `packages/cli/scripts/copy-presets.mjs` | package.json フルコピー + テンプレート scripts 同期 |
572
+ | `packages/cli/src/lib/sync/file-filter.ts` | `root-config` + `claude-config` カテゴリ追加 |
573
+ | `docs/einja/instructions/setup-flow.md` | カテゴリ・マージ方式ドキュメント更新 |
574
+ | `.claude/skills/cli-package-specs/SKILL.md` | JSON マージ仕様セクション追加 |
575
+ | `CLAUDE.md` | 二重管理禁止テーブルに package.json 追加 |
576
+
577
+ ## 検証方法
578
+
579
+ 1. **ビルド検証**: `pnpm --filter @einja/dev-cli build` → `presets/default/package.json` 生成
580
+ 2. **テンプレート同期検証**: ビルド後に `templates/default/package.json` の scripts がルートと一致
581
+ 3. **JsonProcessor ユニットテスト**:
582
+ - 3方向マージ: base→template変更のみ→テンプレート適用
583
+ - 3方向マージ: base→local変更のみ→ローカル保持
584
+ - 3方向マージ: 両方変更(異なる値)→コンフリクト検出、ローカル保持
585
+ - 3方向マージ: ローカル削除+テンプレート未変更→削除維持
586
+ - 3方向マージ: テンプレート新キー追加→追加
587
+ - 3方向マージ: ローカル新キー追加→保持
588
+ - 初回sync(baseなし)+ローカルにユーザー変更あり→ローカル保持
589
+ - project-private: テンプレートにあってもローカルに追加しない(完全除外)
590
+ - project-private ネスト指定: `devDependencies.@types/node` が除外される
591
+ - managed: テンプレート値で強制上書き
592
+ - フルパスでの jsonPaths ルックアップ: `.claude/settings.json` 等が正しくマッチ
593
+ 4. **file-filter テスト**: `root-config` + `claude-config` カテゴリのスキャン
594
+ 5. **全JSONファイル統合テスト**: 各ファイルのマージ動作
595
+ 6. **既存テスト**: `pnpm --filter @einja/dev-cli test` 全パス
596
+ 7. **品質チェック**: `pnpm prepush` 全パス
@@ -0,0 +1,20 @@
1
+ # TODO: JSON 配布メカニズムの統一実装
2
+
3
+ ## Phase 1(並列実行)
4
+
5
+ - [x] **TG-A**: コアSync Engine(Steps 0+2+4: 型定義 + JsonProcessor全面改修 + MetadataManager更新)
6
+ - [x] **TG-B**: copy-presets.mjs(Step 5: package.jsonフルコピー + テンプレートscripts同期)
7
+ - [x] **TG-C**: file-filter.ts(Step 6: root-config + claude-configカテゴリ追加)
8
+
9
+ ## Phase 2(TG-A完了後)
10
+
11
+ - [x] **TG-D**: sync.ts変更(Steps 1+3: fileName→フルパス + JSON処理フロー変更)
12
+
13
+ ## Phase 3(コード変更完了後、並列)
14
+
15
+ - [x] **TG-E**: ドキュメント更新(Step 7: setup-flow.md, SKILL.md, CLAUDE.md)
16
+ - [x] **TG-F**: JsonProcessorユニットテスト作成(バグ修正含む)
17
+
18
+ ## Phase 4
19
+
20
+ - [x] **検証**: ビルド・テスト・prepush(全パス: 607 tests, 29 files, 7 turbo tasks)
@@ -0,0 +1,23 @@
1
+ # TODO: Skill命名規則の文書化 + 配布制御のプレフィックスベース化
2
+
3
+ ## 進捗
4
+
5
+ | # | タスク | 担当 | 状態 |
6
+ |---|--------|------|------|
7
+ | 1 | einja-skill-creator/SKILL.md に命名規則セクション追加 | Agent-1 | ✅ |
8
+ | 2 | CLAUDE.md 更新(命名規則追加 + L139参照パス + L252注釈更新) | Agent-2 | ✅ |
9
+ | 3 | Skill name フィールド統一 + ディレクトリリネーム + 参照パス更新 | Agent-3 | ✅ |
10
+ | 4 | copy-presets.mjs + file-copier.ts のプレフィックスベース改修 | Agent-4 | ✅ |
11
+ | 5 | ビルド検証(`pnpm --filter @einja/dev-cli build`) | 親 | ✅ |
12
+ | 6 | 全体検証(`pnpm prepush`) | 親 | ✅ |
13
+ | + | 既存バグ修正: setup-flow.md マーカーバリデーションエラー | 親 | ✅ |
14
+ | 7 | Codexレビュー | codex-agent | ⏳ |
15
+
16
+ ## 検証結果
17
+
18
+ - ビルド成功(`pnpm --filter @einja/dev-cli build`)
19
+ - `presets/default/.claude/skills/` に `_einja-*` 4個 + `einja-*` 15個が配布 ✅
20
+ - `cli-package-specs` は配布されていない ✅
21
+ - 旧ディレクトリ名の残留なし ✅
22
+ - `pnpm prepush` 全パス(lint + typecheck + test: 7 tasks successful)✅
23
+ - 参照パスの漏れなし ✅