create-einja-app 0.3.2 → 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.
- package/README.md +33 -0
- package/dist/cli.js +60 -64
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.claude/hooks/einja/plan-mode-skill-loader.sh +5 -1
- package/templates/default/.claude/settings.json +14 -4
- package/templates/default/.claude/skills/_einja-general-context-loader/SKILL.md +258 -0
- package/templates/default/.claude/skills/_einja-output-format/SKILL.md +211 -0
- package/templates/default/.claude/skills/_einja-project-overview/SKILL.md +29 -0
- package/templates/default/.claude/skills/_einja-spec-context-loader/SKILL.md +181 -0
- package/templates/default/.claude/skills/cli-package-specs/SKILL.md +294 -0
- package/templates/default/.einja-sync.json +1 -1
- package/templates/default/.github/release.yml +10 -0
- package/templates/default/.github/workflows/changeset-status.yml +60 -0
- package/templates/default/.github/workflows/deploy-stable-branches.yml +289 -59
- package/templates/default/CLAUDE.md +50 -10
- package/templates/default/README.md +20 -8
- package/templates/default/docs/plans/agile-munching-knuth.md +161 -0
- package/templates/default/docs/plans/agile-riding-nova.md +158 -0
- package/templates/default/docs/plans/agile-wibbling-dusk.md +91 -0
- package/templates/default/docs/plans/ancient-watching-otter.md +152 -0
- package/templates/default/docs/plans/bright-sauteeing-bumblebee.md +30 -0
- package/templates/default/docs/plans/composed-doodling-mountain.md +362 -0
- package/templates/default/docs/plans/dazzling-foraging-cascade.md +32 -0
- package/templates/default/docs/plans/enchanted-wiggling-ember-agent-a5befd57d0ca4c7c7.md +177 -0
- package/templates/default/docs/plans/enchanted-wiggling-ember.md +170 -0
- package/templates/default/docs/plans/federated-questing-kahan.md +47 -0
- package/templates/default/docs/plans/flickering-pondering-hearth.md +26 -0
- package/templates/default/docs/plans/fluttering-snuggling-sprout.md +172 -0
- package/templates/default/docs/plans/generic-sleeping-snowglobe-agent-a41d8da.md +179 -0
- package/templates/default/docs/plans/generic-sleeping-snowglobe.md +108 -0
- package/templates/default/docs/plans/generic-snuggling-pudding.md +57 -0
- package/templates/default/docs/plans/glistening-conjuring-cascade.md +42 -0
- package/templates/default/docs/plans/idempotent-wiggling-cherny.md +122 -0
- package/templates/default/docs/plans/linear-gathering-hejlsberg.md +596 -0
- package/templates/default/docs/plans/recursive-fluttering-mitten.md +176 -0
- package/templates/default/docs/plans/todo-create-einja-app-ux-fix.md +16 -0
- package/templates/default/docs/plans/todo-direnv-hang-fix.md +12 -0
- package/templates/default/docs/plans/todo-github-actions-release-workflow.md +34 -0
- package/templates/default/docs/plans/todo-glistening-conjuring-cascade.md +20 -0
- package/templates/default/docs/plans/todo-issue-spec-rename.md +24 -0
- package/templates/default/docs/plans/todo-skill-creator-upgrade.md +18 -0
- package/templates/default/docs/plans/todo-unified-crafting-valiant.md +23 -0
- package/templates/default/docs/plans/unified-crafting-valiant.md +60 -0
- package/templates/default/docs/plans/velvety-chasing-spark.md +28 -0
- package/templates/default/docs/plans/wondrous-strolling-crystal-agent-a0615fc.md +215 -0
- package/templates/default/docs/plans/wondrous-strolling-crystal.md +182 -0
- package/templates/default/docs/plans/zesty-roaming-steele.md +74 -0
- package/templates/default/gitignore +6 -2
- package/templates/default/package.json +6 -2
- package/templates/default/pnpm-lock.yaml +547 -0
- package/templates/default/scripts/ensure-serena.sh +28 -9
- package/templates/default/scripts/env-rotate-secrets.ts +66 -6
- package/templates/default/scripts/init-github.ts +363 -0
- package/templates/default/scripts/init.sh +11 -5
- package/templates/default/scripts/setup-dev.ts +16 -1
- package/templates/default/.claude/hooks/einja/validate-git-commit.sh +0 -239
- package/templates/default/.claude/skills/create-einja-app-release/SKILL.md +0 -186
- package/templates/default/.claude/skills/dev-cli-release/SKILL.md +0 -173
- package/templates/default/.cursor/commands/spec-create.md +0 -227
- package/templates/default/.cursor/commands/task-exec.md +0 -287
- package/templates/default/.cursor/commands/update-docs-by-task-specs.md +0 -448
- package/templates/default/packages/server-core/src/__generated__/fabbrica/index.d.ts +0 -270
- package/templates/default/packages/server-core/src/__generated__/fabbrica/index.js +0 -484
|
@@ -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` 全パス
|