Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/add-untracked-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@tktco/create-devenv": minor
---

push コマンドにホワイトリスト外ファイル検知機能を追加し、モジュール定義を外部化

### ホワイトリスト外ファイル検知
- push 時にホワイトリスト(patterns)に含まれていないファイルを検出
- モジュールごとにグループ化して選択UI を表示
- 選択したファイルを modules.jsonc に自動追加(PR に含まれる)
- gitignore されているファイルは自動で除外

### モジュール定義の外部化
- モジュール定義をコードから `.devenv/modules.jsonc` に外部化
- テンプレートリポジトリの modules.jsonc から動的に読み込み
- `customPatterns` を廃止し modules.jsonc に統合

### ディレクトリベースのモジュール設計
- モジュール ID をディレクトリパスベースに変更(例: `.devcontainer`, `.github`, `.`)
- ファイルパスから即座にモジュール ID を導出可能に
- モジュール間のファイル重複を構造的に防止
53 changes: 53 additions & 0 deletions .devenv/modules.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"$schema": "./modules.schema.json",
"modules": [
{
// ルート直下のファイル(ディレクトリに属さないファイル)
"id": ".",
"name": "ルート設定",
"description": "MCP、mise などのルート設定ファイル",
"setupDescription": "プロジェクトルートの設定ファイルが適用されます",
"patterns": [
".mcp.json",
".mise.toml"
]
},
{
"id": ".devcontainer",
"name": "DevContainer",
"description": "VS Code DevContainer、Docker-in-Docker",
"setupDescription": "VS Code で DevContainer を開くと自動でセットアップされます",
// ホワイトリスト形式: テンプレートとして配布するファイルのみを明示
"patterns": [
".devcontainer/devcontainer.json",
".devcontainer/.gitignore",
".devcontainer/setup-*.sh",
".devcontainer/test-*.sh",
".devcontainer/.env.devcontainer.example"
// 除外: devcontainer.env(秘密情報)
]
},
{
"id": ".github",
"name": "GitHub",
"description": "GitHub Actions、labeler ワークフロー",
"setupDescription": "PR 作成時に自動でラベル付け、Issue リンクが行われます",
"patterns": [
".github/workflows/issue-link.yml",
".github/workflows/label.yml",
".github/labeler.yml"
// 除外: ci.yml, release.yml(リポジトリ固有のCI/CD設定)
]
},
{
"id": ".claude",
"name": "Claude",
"description": "Claude Code のプロジェクト共通設定",
"setupDescription": "Claude Code のプロジェクト設定が適用されます",
"patterns": [
".claude/settings.json"
// 除外: settings.local.json(個人設定)
]
}
]
}
1 change: 1 addition & 0 deletions packages/create-devenv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"diff": "^8.0.2",
"giget": "^2.0.0",
"ignore": "^7.0.5",
"jsonc-parser": "^3.3.1",
"pathe": "^2.0.3",
"tinyglobby": "^0.2.15",
"ts-pattern": "^5.9.0",
Expand Down
120 changes: 73 additions & 47 deletions packages/create-devenv/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ import { existsSync, mkdirSync } from "node:fs";
import { defineCommand } from "citty";
import { consola } from "consola";
import { resolve } from "pathe";
import { getModuleById, modules } from "../modules/index";
import {
defaultModules,
getModuleById,
loadModulesFile,
modulesFileExists,
} from "../modules/index";
import type {
Answers,
FileOperationResult,
OverwriteStrategy,
TemplateModule,
} from "../modules/schemas";
import { promptInit } from "../prompts/init";
import {
downloadTemplateToTemp,
fetchTemplates,
logResult,
writeFileWithStrategy,
Expand Down Expand Up @@ -59,52 +66,70 @@ export const initCommand = defineCommand({
mkdirSync(targetDir, { recursive: true });
}

// プロンプトまたは自動選択
let answers: Answers;
if (args.yes) {
answers = {
modules: modules.map((m) => m.id),
overwriteStrategy: "overwrite",
};
consola.info("すべてのモジュールを自動選択しました");
} else {
answers = await promptInit();
}

if (answers.modules.length === 0) {
consola.warn("テンプレートが選択されませんでした");
return;
// テンプレートをダウンロード
const { templateDir, cleanup } = await downloadTemplateToTemp(targetDir);

try {
// modules.jsonc からモジュールを読み込み(なければデフォルト使用)
let moduleList: TemplateModule[];
if (modulesFileExists(templateDir)) {
const { modules: loadedModules } = await loadModulesFile(templateDir);
moduleList = loadedModules;
} else {
moduleList = defaultModules;
}

// プロンプトまたは自動選択
let answers: Answers;
if (args.yes) {
answers = {
modules: moduleList.map((m) => m.id),
overwriteStrategy: "overwrite",
};
consola.info("すべてのモジュールを自動選択しました");
} else {
answers = await promptInit(moduleList);
}

if (answers.modules.length === 0) {
consola.warn("テンプレートが選択されませんでした");
return;
}

const effectiveStrategy: OverwriteStrategy = args.force
? "overwrite"
: answers.overwriteStrategy;

// テンプレート取得・適用(結果を収集)
const templateResults = await fetchTemplates({
targetDir,
modules: answers.modules,
overwriteStrategy: effectiveStrategy,
moduleList,
templateDir,
});

const allResults: FileOperationResult[] = [...templateResults];

// devcontainer.env.example を戦略に従って作成
if (answers.modules.includes("devcontainer")) {
const envResult = await createEnvExample(targetDir, effectiveStrategy);
logResult(envResult);
allResults.push(envResult);
}

// 設定ファイル生成(常に更新)
const configResult = await createDevEnvConfig(targetDir, answers.modules);
logResult(configResult);
allResults.push(configResult);

consola.box("セットアップ完了!");

// モジュール別の説明を表示
displayModuleDescriptions(answers.modules, allResults, moduleList);
} finally {
cleanup();
}

const effectiveStrategy: OverwriteStrategy = args.force
? "overwrite"
: answers.overwriteStrategy;

// テンプレート取得・適用(結果を収集)
const templateResults = await fetchTemplates({
targetDir,
modules: answers.modules,
overwriteStrategy: effectiveStrategy,
});

const allResults: FileOperationResult[] = [...templateResults];

// devcontainer.env.example を戦略に従って作成
if (answers.modules.includes("devcontainer")) {
const envResult = await createEnvExample(targetDir, effectiveStrategy);
logResult(envResult);
allResults.push(envResult);
}

// 設定ファイル生成(常に更新)
const configResult = await createDevEnvConfig(targetDir, answers.modules);
logResult(configResult);
allResults.push(configResult);

consola.box("セットアップ完了!");

// モジュール別の説明を表示
displayModuleDescriptions(answers.modules, allResults);
},
});

Expand Down Expand Up @@ -167,6 +192,7 @@ async function createDevEnvConfig(
function displayModuleDescriptions(
selectedModules: string[],
fileResults: FileOperationResult[],
moduleList: TemplateModule[],
): void {
const hasChanges = fileResults.some(
(r) =>
Expand All @@ -182,7 +208,7 @@ function displayModuleDescriptions(

consola.info("追加されたモジュール:");
for (const moduleId of selectedModules) {
const mod = getModuleById(moduleId);
const mod = getModuleById(moduleId, moduleList);
if (mod?.setupDescription) {
consola.info(` ${mod.name}: ${mod.setupDescription}`);
}
Expand Down
77 changes: 74 additions & 3 deletions packages/create-devenv/src/commands/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import { defineCommand } from "citty";
import consola from "consola";
import { downloadTemplate } from "giget";
import { join, resolve } from "pathe";
import type { DevEnvConfig } from "../modules/schemas";
import {
addPatternToModulesFile,
defaultModules,
loadModulesFile,
modulesFileExists,
} from "../modules";
import type { DevEnvConfig, TemplateModule } from "../modules/schemas";
import { configSchema } from "../modules/schemas";
import {
promptAddUntrackedFiles,
promptGitHubToken,
promptPrBody,
promptPrTitle,
Expand All @@ -15,8 +22,10 @@ import {
} from "../prompts/push";
import { detectDiff, formatDiff, getPushableFiles } from "../utils/diff";
import { createPullRequest, getGitHubToken } from "../utils/github";
import { detectUntrackedFiles } from "../utils/untracked";

const TEMPLATE_SOURCE = "gh:tktcorporation/.github";
const MODULES_FILE_PATH = ".devenv/modules.jsonc";

export const pushCommand = defineCommand({
meta: {
Expand Down Expand Up @@ -96,6 +105,56 @@ export const pushCommand = defineCommand({
force: true,
});

// modules.jsonc を読み込み
let moduleList: TemplateModule[];
let modulesRawContent: string | undefined;

if (modulesFileExists(templateDir)) {
const loaded = await loadModulesFile(templateDir);
moduleList = loaded.modules;
modulesRawContent = loaded.rawContent;
} else {
moduleList = defaultModules;
}

// ホワイトリスト外ファイルの検出と追加確認
let updatedModulesContent: string | undefined;

if (!args.force && modulesRawContent) {
const untrackedByFolder = await detectUntrackedFiles({
targetDir,
moduleIds: config.modules,
config,
moduleList,
});

if (untrackedByFolder.length > 0) {
const selectedFiles =
await promptAddUntrackedFiles(untrackedByFolder);

if (selectedFiles.length > 0) {
// modules.jsonc にパターンを追加(メモリ上)
let currentContent = modulesRawContent;
for (const { moduleId, files } of selectedFiles) {
currentContent = addPatternToModulesFile(
currentContent,
moduleId,
files,
);
}
updatedModulesContent = currentContent;

const totalAdded = selectedFiles.reduce(
(sum, s) => sum + s.files.length,
0,
);
consola.info(
`${totalAdded} 件のパターンを modules.jsonc に追加します(PR に含まれます)`,
);
}
}
}

consola.start("差分を検出中...");

// 差分検出
Expand All @@ -104,12 +163,13 @@ export const pushCommand = defineCommand({
templateDir,
moduleIds: config.modules,
config,
moduleList,
});

// push 対象ファイルを取得
let pushableFiles = getPushableFiles(diff);

if (pushableFiles.length === 0) {
if (pushableFiles.length === 0 && !updatedModulesContent) {
consola.info("push するファイルがありません。");
console.log();
console.log(formatDiff(diff, false));
Expand All @@ -121,6 +181,9 @@ export const pushCommand = defineCommand({
consola.info("[ドライラン] 以下のファイルが PR として送信されます:");
console.log();
console.log(formatDiff(diff, true));
if (updatedModulesContent) {
console.log(` [+] ${MODULES_FILE_PATH} (パターン追加)`);
}
console.log();
consola.info("[ドライラン] 実際の PR は作成されませんでした。");
return;
Expand All @@ -129,7 +192,7 @@ export const pushCommand = defineCommand({
// ファイル選択(デフォルト動作)
if (args.interactive && !args.force) {
pushableFiles = await promptSelectFilesWithDiff(pushableFiles);
if (pushableFiles.length === 0) {
if (pushableFiles.length === 0 && !updatedModulesContent) {
consola.info("ファイルが選択されませんでした。キャンセルします。");
return;
}
Expand Down Expand Up @@ -160,6 +223,14 @@ export const pushCommand = defineCommand({
content: f.localContent || "",
}));

// modules.jsonc の変更があれば追加
if (updatedModulesContent) {
files.push({
path: MODULES_FILE_PATH,
content: updatedModulesContent,
});
}

consola.start("PR を作成中...");

// PR 作成
Expand Down
Loading