create-takt-sdd 0.4.0 → 0.6.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/dist/i18n.js CHANGED
@@ -2,8 +2,10 @@ const en = {
2
2
  downloading: "Downloading takt-sdd...",
3
3
  downloadingVersion: (tag) => `Downloading takt-sdd ${tag}...`,
4
4
  installing: "Installing pieces and facets to .takt/...",
5
+ updating: "Updating takt-sdd...",
5
6
  existsError: (cmd) => `.takt/ already exists. To overwrite, run:\n ${cmd} --force`,
6
7
  complete: "Installation complete!",
8
+ updateComplete: "Update complete!",
7
9
  dryRunHeader: "[dry-run] The following files would be installed:",
8
10
  dryRunItem: (path) => ` ${path}`,
9
11
  dryRunSkipped: "[dry-run] No files were written.",
@@ -13,6 +15,7 @@ const en = {
13
15
  scriptsSkipped: (keys) => `Skipped existing scripts: ${keys.join(", ")}`,
14
16
  scriptsCreated: "Created package.json with npm scripts and devDependencies",
15
17
  depsAdded: (keys) => `Added devDependencies: ${keys.join(", ")}`,
18
+ depsUpdated: (keys) => `Updated devDependencies: ${keys.join(", ")}`,
16
19
  installingSkills: "Installing takt skills to .agent/skills/...",
17
20
  skillInstalled: (name) => `Installed skill: ${name}`,
18
21
  skillSymlinked: (name, target) => `Symlinked ${target}/${name} -> .agent/skills/${name}`,
@@ -20,12 +23,16 @@ const en = {
20
23
  taktRefsInstalled: "Installed takt references (builtins, docs)",
21
24
  taktRefsSkipped: "Takt references already exist, skipping",
22
25
  taktRefsError: "Warning: Failed to download takt references. Skills may not find style guides.",
26
+ fileAdded: (path) => `Added: ${path}`,
27
+ fileUpdated: (path) => `Updated: ${path}`,
28
+ fileSkippedCustomized: (path) => `Skipped (customized): ${path}`,
29
+ manifestCreated: "Created install manifest",
23
30
  helpText: `Usage: npx create-takt-sdd [options]
24
31
 
25
32
  Options:
26
33
  --tag <version> Version to install ("latest", "0.2.0", default: installer version)
27
34
  --lang <en|ja> Message language (default: en)
28
- --force Overwrite existing .takt/ directory
35
+ --force Overwrite existing .takt/ directory (ignored if manifest exists)
29
36
  --dry-run Preview without writing files
30
37
  --without-skills Skip installing takt skills to .agent/skills/
31
38
  --refs-path <path> Path for takt references (default: references/takt)
@@ -49,8 +56,10 @@ const ja = {
49
56
  downloading: "takt-sdd をダウンロード中...",
50
57
  downloadingVersion: (tag) => `takt-sdd ${tag} をダウンロード中...`,
51
58
  installing: ".takt/ にピースとファセットをインストール中...",
59
+ updating: "takt-sdd をアップデート中...",
52
60
  existsError: (cmd) => `.takt/ が既に存在します。上書きするには以下を実行してください:\n ${cmd} --force`,
53
61
  complete: "インストール完了!",
62
+ updateComplete: "アップデート完了!",
54
63
  dryRunHeader: "[dry-run] 以下のファイルがインストールされます:",
55
64
  dryRunItem: (path) => ` ${path}`,
56
65
  dryRunSkipped: "[dry-run] ファイルは書き込まれませんでした。",
@@ -60,6 +69,7 @@ const ja = {
60
69
  scriptsSkipped: (keys) => `既存のスクリプトをスキップしました: ${keys.join(", ")}`,
61
70
  scriptsCreated: "npm scripts と devDependencies 付きの package.json を作成しました",
62
71
  depsAdded: (keys) => `devDependencies を追加しました: ${keys.join(", ")}`,
72
+ depsUpdated: (keys) => `devDependencies を更新しました: ${keys.join(", ")}`,
63
73
  installingSkills: ".agent/skills/ に takt スキルをインストール中...",
64
74
  skillInstalled: (name) => `スキルをインストールしました: ${name}`,
65
75
  skillSymlinked: (name, target) => `シンボリックリンク作成: ${target}/${name} -> .agent/skills/${name}`,
@@ -67,12 +77,16 @@ const ja = {
67
77
  taktRefsInstalled: "takt リファレンスをインストールしました(builtins, docs)",
68
78
  taktRefsSkipped: "takt リファレンスは既に存在するためスキップしました",
69
79
  taktRefsError: "警告: takt リファレンスのダウンロードに失敗しました。スキルがスタイルガイドを参照できない可能性があります。",
80
+ fileAdded: (path) => `追加: ${path}`,
81
+ fileUpdated: (path) => `更新: ${path}`,
82
+ fileSkippedCustomized: (path) => `スキップ(カスタマイズ済み): ${path}`,
83
+ manifestCreated: "インストールマニフェストを作成しました",
70
84
  helpText: `使い方: npx create-takt-sdd [オプション]
71
85
 
72
86
  オプション:
73
87
  --tag <version> インストールするバージョン ("latest", "0.2.0", デフォルト: インストーラのバージョン)
74
88
  --lang <en|ja> メッセージ言語 (デフォルト: en)
75
- --force 既存の .takt/ を上書き
89
+ --force 既存の .takt/ を上書き(マニフェストがある場合は無視)
76
90
  --dry-run プレビューのみ(ファイル書き込みなし)
77
91
  --without-skills takt スキルのインストールをスキップ
78
92
  --refs-path <path> takt リファレンスのパス(デフォルト: references/takt)
package/dist/install.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, statSync, writeFileSync } from "node:fs";
3
4
  import https from "node:https";
4
5
  import { createWriteStream } from "node:fs";
@@ -144,9 +145,68 @@ function collectFiles(dir, base) {
144
145
  }
145
146
  return results;
146
147
  }
148
+ const MANIFEST_FILE = ".manifest.json";
149
+ function computeFileHash(filePath) {
150
+ const content = readFileSync(filePath);
151
+ return createHash("sha256").update(content).digest("hex");
152
+ }
153
+ function loadManifest(manifestPath) {
154
+ if (!existsSync(manifestPath))
155
+ return null;
156
+ try {
157
+ return JSON.parse(readFileSync(manifestPath, "utf-8"));
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ function syncDirectory(srcDir, destDir, srcBase, destBase, manifest, msg, cwd) {
164
+ const files = {};
165
+ if (!existsSync(srcDir))
166
+ return { files };
167
+ const srcFiles = collectFiles(srcDir, srcBase);
168
+ for (const relFile of srcFiles) {
169
+ const srcPath = join(srcBase, relFile);
170
+ const destPath = join(destBase, relFile);
171
+ const manifestKey = relative(cwd, destPath).split("\\").join("/");
172
+ const srcHash = computeFileHash(srcPath);
173
+ files[manifestKey] = srcHash;
174
+ if (!existsSync(destPath)) {
175
+ // New file — copy
176
+ mkdirSync(dirname(destPath), { recursive: true });
177
+ cpSync(srcPath, destPath);
178
+ info(msg.fileAdded(manifestKey));
179
+ }
180
+ else if (manifest !== null) {
181
+ const recordedHash = manifest.files[manifestKey];
182
+ if (recordedHash === undefined) {
183
+ // File exists but not in manifest (pre-manifest environment)
184
+ warn(msg.fileSkippedCustomized(manifestKey));
185
+ }
186
+ else {
187
+ const currentHash = computeFileHash(destPath);
188
+ if (currentHash === recordedHash) {
189
+ // User has not modified — safe to overwrite
190
+ cpSync(srcPath, destPath);
191
+ info(msg.fileUpdated(manifestKey));
192
+ }
193
+ else {
194
+ // User has customized — skip
195
+ warn(msg.fileSkippedCustomized(manifestKey));
196
+ }
197
+ }
198
+ }
199
+ else {
200
+ // No manifest, fresh install or force — overwrite
201
+ cpSync(srcPath, destPath);
202
+ }
203
+ }
204
+ return { files };
205
+ }
147
206
  export async function install(options) {
148
207
  const msg = getMessages(options.lang);
149
208
  const targetPath = join(options.cwd, TARGET_DIR);
209
+ const manifestPath = join(targetPath, MANIFEST_FILE);
150
210
  // tar の存在チェック
151
211
  try {
152
212
  execSync("which tar", { stdio: "ignore" });
@@ -154,8 +214,11 @@ export async function install(options) {
154
214
  catch {
155
215
  errorExit(msg.tarNotFound);
156
216
  }
157
- // 既存ディレクトリチェック
158
- if (existsSync(join(targetPath, "pieces")) && !options.force) {
217
+ // マニフェスト読み込み&モード判定
218
+ const manifest = loadManifest(manifestPath);
219
+ const isUpdate = manifest !== null;
220
+ const piecesExist = existsSync(join(targetPath, "pieces"));
221
+ if (!isUpdate && piecesExist && !options.force) {
159
222
  errorExit(msg.existsError("npx create-takt-sdd"));
160
223
  }
161
224
  // ダウンロード
@@ -208,17 +271,19 @@ export async function install(options) {
208
271
  info(msg.dryRunSkipped);
209
272
  return;
210
273
  }
211
- // インストール
212
- info(msg.installing);
274
+ // インストール / アップデート
275
+ info(isUpdate ? msg.updating : msg.installing);
213
276
  mkdirSync(targetPath, { recursive: true });
277
+ const allFiles = {};
214
278
  for (const dir of FACET_DIRS) {
215
279
  const srcDir = join(extractedTakt, options.lang, dir);
216
280
  if (existsSync(srcDir)) {
217
281
  const destDir = join(targetPath, dir);
218
- if (existsSync(destDir)) {
282
+ if (!isUpdate && existsSync(destDir)) {
219
283
  rmSync(destDir, { recursive: true });
220
284
  }
221
- cpSync(srcDir, destDir, { recursive: true });
285
+ const result = syncDirectory(srcDir, destDir, join(extractedTakt, options.lang), targetPath, isUpdate ? manifest : null, msg, options.cwd);
286
+ Object.assign(allFiles, result.files);
222
287
  }
223
288
  }
224
289
  // .gitignore は takt が初回実行時に自動配置するため、インストーラでは生成しない
@@ -232,20 +297,20 @@ export async function install(options) {
232
297
  const skillSrc = join(extractedSkillsDir, skill);
233
298
  if (!existsSync(skillSrc))
234
299
  continue;
235
- const skillDest = join(agentSkillsDir, skill);
236
- if (existsSync(skillDest)) {
237
- rmSync(skillDest, { recursive: true });
238
- }
239
- cpSync(skillSrc, skillDest, { recursive: true });
300
+ // Prepare a temp copy with language selection applied
301
+ const skillTmp = join(tmpDir, "skill-prep", skill);
302
+ if (existsSync(skillTmp))
303
+ rmSync(skillTmp, { recursive: true });
304
+ cpSync(skillSrc, skillTmp, { recursive: true });
240
305
  // Select SKILL.md based on language
241
- const skillLangMd = join(skillDest, `SKILL.${options.lang}.md`);
242
- const skillMdPath = join(skillDest, "SKILL.md");
306
+ const skillLangMd = join(skillTmp, `SKILL.${options.lang}.md`);
307
+ const skillMdPath = join(skillTmp, "SKILL.md");
243
308
  if (existsSync(skillLangMd)) {
244
309
  cpSync(skillLangMd, skillMdPath);
245
310
  }
246
- // Remove language-specific SKILL files
311
+ // Remove language-specific SKILL files from temp
247
312
  for (const l of ["ja", "en"]) {
248
- const langFile = join(skillDest, `SKILL.${l}.md`);
313
+ const langFile = join(skillTmp, `SKILL.${l}.md`);
249
314
  if (existsSync(langFile)) {
250
315
  rmSync(langFile);
251
316
  }
@@ -258,6 +323,12 @@ export async function install(options) {
258
323
  writeFileSync(skillMdPath, updated, "utf-8");
259
324
  }
260
325
  }
326
+ const skillDest = join(agentSkillsDir, skill);
327
+ if (!isUpdate && existsSync(skillDest)) {
328
+ rmSync(skillDest, { recursive: true });
329
+ }
330
+ const result = syncDirectory(skillTmp, skillDest, skillTmp, skillDest, isUpdate ? manifest : null, msg, options.cwd);
331
+ Object.assign(allFiles, result.files);
261
332
  info(msg.skillInstalled(skill));
262
333
  }
263
334
  // .claude/skills/ と .codex/skills/ にシンボリックリンクを作成
@@ -279,7 +350,10 @@ export async function install(options) {
279
350
  // takt リファレンスのダウンロード(スキルが参照するbuiltins等)
280
351
  if (!options.withoutSkills) {
281
352
  const refsDir = join(options.cwd, options.refsPath);
282
- if (!existsSync(join(refsDir, "builtins"))) {
353
+ const needsRefDownload = isUpdate
354
+ ? manifest.taktRefHash !== TAKT_REF_HASH
355
+ : !existsSync(join(refsDir, "builtins"));
356
+ if (needsRefDownload) {
283
357
  info(msg.downloadingTaktRefs(options.refsPath));
284
358
  const taktTmpDir = mkdtempSync(join(tmpdir(), "takt-refs-"));
285
359
  try {
@@ -292,16 +366,45 @@ export async function install(options) {
292
366
  if (taktExtracted) {
293
367
  const taktRoot = join(taktTmpDir, taktExtracted);
294
368
  mkdirSync(refsDir, { recursive: true });
295
- // builtins/ をコピー
369
+ // builtins/ を syncDirectory でコピー
296
370
  const builtinsSrc = join(taktRoot, "builtins");
297
371
  if (existsSync(builtinsSrc)) {
298
- cpSync(builtinsSrc, join(refsDir, "builtins"), { recursive: true });
372
+ const builtinsDest = join(refsDir, "builtins");
373
+ const result = syncDirectory(builtinsSrc, builtinsDest, builtinsSrc, builtinsDest, isUpdate ? manifest : null, msg, options.cwd);
374
+ Object.assign(allFiles, result.files);
299
375
  }
300
376
  // docs/faceted-prompting.ja.md をコピー
301
377
  const fpSrc = join(taktRoot, "docs", "faceted-prompting.ja.md");
302
378
  if (existsSync(fpSrc)) {
303
- mkdirSync(join(refsDir, "docs"), { recursive: true });
304
- cpSync(fpSrc, join(refsDir, "docs", "faceted-prompting.ja.md"));
379
+ const docsDir = join(refsDir, "docs");
380
+ mkdirSync(docsDir, { recursive: true });
381
+ const fpDest = join(docsDir, "faceted-prompting.ja.md");
382
+ const fpKey = relative(options.cwd, fpDest).split("\\").join("/");
383
+ const fpHash = computeFileHash(fpSrc);
384
+ if (!existsSync(fpDest)) {
385
+ cpSync(fpSrc, fpDest);
386
+ info(msg.fileAdded(fpKey));
387
+ }
388
+ else if (isUpdate) {
389
+ const recordedHash = manifest.files[fpKey];
390
+ if (recordedHash === undefined) {
391
+ warn(msg.fileSkippedCustomized(fpKey));
392
+ }
393
+ else {
394
+ const currentHash = computeFileHash(fpDest);
395
+ if (currentHash === recordedHash) {
396
+ cpSync(fpSrc, fpDest);
397
+ info(msg.fileUpdated(fpKey));
398
+ }
399
+ else {
400
+ warn(msg.fileSkippedCustomized(fpKey));
401
+ }
402
+ }
403
+ }
404
+ else {
405
+ cpSync(fpSrc, fpDest);
406
+ }
407
+ allFiles[fpKey] = fpHash;
305
408
  }
306
409
  info(msg.taktRefsInstalled);
307
410
  }
@@ -318,6 +421,14 @@ export async function install(options) {
318
421
  }
319
422
  else {
320
423
  info(msg.taktRefsSkipped);
424
+ // Preserve existing file hashes from manifest for refs
425
+ if (isUpdate) {
426
+ for (const [key, hash] of Object.entries(manifest.files)) {
427
+ if (key.startsWith(options.refsPath)) {
428
+ allFiles[key] = hash;
429
+ }
430
+ }
431
+ }
321
432
  }
322
433
  }
323
434
  // アーカイブの package.json から devDependencies を取得
@@ -349,11 +460,16 @@ export async function install(options) {
349
460
  pkg.scripts = scripts;
350
461
  const devDeps = pkg.devDependencies ?? {};
351
462
  const depsAdded = [];
463
+ const depsUpdatedKeys = [];
352
464
  for (const [key, value] of Object.entries(sddDevDependencies)) {
353
465
  if (devDeps[key] === undefined) {
354
466
  devDeps[key] = value;
355
467
  depsAdded.push(key);
356
468
  }
469
+ else if (devDeps[key] !== value) {
470
+ devDeps[key] = value;
471
+ depsUpdatedKeys.push(key);
472
+ }
357
473
  }
358
474
  pkg.devDependencies = devDeps;
359
475
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
@@ -366,6 +482,9 @@ export async function install(options) {
366
482
  if (depsAdded.length > 0) {
367
483
  info(msg.depsAdded(depsAdded));
368
484
  }
485
+ if (depsUpdatedKeys.length > 0) {
486
+ info(msg.depsUpdated(depsUpdatedKeys));
487
+ }
369
488
  }
370
489
  else {
371
490
  const pkg = {
@@ -376,7 +495,17 @@ export async function install(options) {
376
495
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
377
496
  info(msg.scriptsCreated);
378
497
  }
379
- info(msg.complete);
498
+ // マニフェスト書き込み
499
+ const newManifest = {
500
+ version: version,
501
+ installedAt: new Date().toISOString(),
502
+ lang: options.lang,
503
+ taktRefHash: TAKT_REF_HASH,
504
+ files: allFiles,
505
+ };
506
+ writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2) + "\n", "utf-8");
507
+ info(msg.manifestCreated);
508
+ info(isUpdate ? msg.updateComplete : msg.complete);
380
509
  console.log(msg.usageExamples);
381
510
  console.log("");
382
511
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-takt-sdd",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Installer for takt-sdd: Spec-Driven Development workflow for takt",
5
5
  "type": "module",
6
6
  "bin": {