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
25 changes: 25 additions & 0 deletions .changeset/cyan-falcons-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@tktco/create-devenv": minor
---

双方向同期機能とホワイトリスト形式を追加

### 新機能
- `push` コマンド: ローカル変更を GitHub PR として自動送信
- `diff` コマンド: ローカルとテンプレートの差分をプレビュー

### 破壊的変更
- モジュール定義を `files` + `excludeFiles` 形式から `patterns` (glob) 形式に移行
- テンプレート対象ファイルをホワイトリスト形式で明示的に指定するように変更

### 使用例
```bash
# 差分を確認
npx @tktco/create-devenv diff

# ローカル変更を PR として送信
npx @tktco/create-devenv push --message "feat: DevContainer設定を更新"

# ドライラン
npx @tktco/create-devenv push --dry-run
```
32 changes: 5 additions & 27 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,46 +1,24 @@
{
"image": "mcr.microsoft.com/devcontainers/base:bookworm",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
// "ghcr.io/devcontainers/features/docker-in-docker:2": {
// "version": "latest",
// "enableNonRootDocker": "true",
// "moby": "true"
// },
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers-extra/features/mise:1": {}
},
"ports": [
8000,
8030
],
"runArgs": ["--env-file", ".devcontainer/devcontainer.env"],
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-docker",
"GitHub.copilot",
"donjayamanne.githistory",
"eamodio.gitlens",
"mhutchie.git-graph",
"steoates.autoimport",
"Atishay-Jain.All-Autocomplete",
"usernamehw.errorlens",
"dbaeumer.vscode-eslint",
"mgmcdermott.vscode-language-babel",
"pflannery.vscode-versionlens",
"editorconfig.editorconfig",
"WakaTime.vscode-wakatime",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-python.python",
"kawamataryo.copy-python-dotted-path",
"zxh404.vscode-proto3",
"Vue.volar",
"ms-python.flake8",
"ms-python.mypy-type-checker",
"ms-python.isort",
"ms-python.autopep8",
"nemesv.copy-file-name"
]
}
Expand Down
2 changes: 2 additions & 0 deletions packages/create-devenv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
},
"dependencies": {
"@inquirer/prompts": "^8.0.2",
"@octokit/rest": "^21.0.0",
"citty": "^0.1.6",
"consola": "^3.4.2",
"defu": "^6.1.4",
"giget": "^2.0.0",
"pathe": "^2.0.3",
"tinyglobby": "^0.2.0",
"ts-pattern": "^5.9.0",
"zod": "^4.1.13"
},
Expand Down
101 changes: 101 additions & 0 deletions packages/create-devenv/src/commands/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { existsSync } from "node:fs";
import { readFile, rm } from "node:fs/promises";
import { defineCommand } from "citty";
import consola from "consola";
import { downloadTemplate } from "giget";
import { join, resolve } from "pathe";
import type { DevEnvConfig } from "../modules/schemas";
import { configSchema } from "../modules/schemas";
import { detectDiff, formatDiff, hasDiff } from "../utils/diff";

const TEMPLATE_SOURCE = "gh:tktcorporation/.github";

export const diffCommand = defineCommand({
meta: {
name: "diff",
description: "ローカルとテンプレートの差分を表示",
},
args: {
dir: {
type: "positional",
description: "プロジェクトディレクトリ",
default: ".",
},
verbose: {
type: "boolean",
alias: "v",
description: "詳細な差分を表示",
default: false,
},
},
async run({ args }) {
const targetDir = resolve(args.dir);
const configPath = join(targetDir, ".devenv.json");

// .devenv.json の存在確認
if (!existsSync(configPath)) {
consola.error(
".devenv.json が見つかりません。先に init コマンドを実行してください。",
);
process.exit(1);
}

// 設定読み込み
const configContent = await readFile(configPath, "utf-8");
const configData = JSON.parse(configContent);
const parseResult = configSchema.safeParse(configData);

if (!parseResult.success) {
consola.error(
".devenv.json の形式が不正です:",
parseResult.error.message,
);
process.exit(1);
}

const config: DevEnvConfig = parseResult.data;

if (config.modules.length === 0) {
consola.warn("インストール済みのモジュールがありません。");
return;
}

consola.start("テンプレートをダウンロード中...");

// テンプレートを一時ディレクトリにダウンロード
const tempDir = join(targetDir, ".devenv-temp");

try {
const { dir: templateDir } = await downloadTemplate(TEMPLATE_SOURCE, {
dir: tempDir,
force: true,
});

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

// 差分検出
const diff = await detectDiff({
targetDir,
templateDir,
moduleIds: config.modules,
config,
});

// 結果表示
console.log();
console.log(formatDiff(diff, args.verbose));
console.log();

if (hasDiff(diff)) {
consola.info(
'ローカルの変更をテンプレートに反映するには "push" コマンドを使用してください。',
);
}
} finally {
// 一時ディレクトリを削除
if (existsSync(tempDir)) {
await rm(tempDir, { recursive: true, force: true });
}
}
},
});
174 changes: 174 additions & 0 deletions packages/create-devenv/src/commands/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { existsSync } from "node:fs";
import { readFile, rm } from "node:fs/promises";
import { defineCommand } from "citty";
import consola from "consola";
import { downloadTemplate } from "giget";
import { join, resolve } from "pathe";
import type { DevEnvConfig } from "../modules/schemas";
import { configSchema } from "../modules/schemas";
import {
promptGitHubToken,
promptPrBody,
promptPrTitle,
promptPushConfirm,
} from "../prompts/push";
import { detectDiff, formatDiff, getPushableFiles } from "../utils/diff";
import { createPullRequest, getGitHubToken } from "../utils/github";

const TEMPLATE_SOURCE = "gh:tktcorporation/.github";

export const pushCommand = defineCommand({
meta: {
name: "push",
description: "ローカル変更をテンプレートリポジトリに PR として送信",
},
args: {
dir: {
type: "positional",
description: "プロジェクトディレクトリ",
default: ".",
},
dryRun: {
type: "boolean",
alias: "n",
description: "実際の PR を作成せず、プレビューのみ表示",
default: false,
},
message: {
type: "string",
alias: "m",
description: "PR のタイトル",
},
force: {
type: "boolean",
alias: "f",
description: "確認プロンプトをスキップ",
default: false,
},
},
async run({ args }) {
const targetDir = resolve(args.dir);
const configPath = join(targetDir, ".devenv.json");

// .devenv.json の存在確認
if (!existsSync(configPath)) {
consola.error(
".devenv.json が見つかりません。先に init コマンドを実行してください。",
);
process.exit(1);
}

// 設定読み込み
const configContent = await readFile(configPath, "utf-8");
const configData = JSON.parse(configContent);
const parseResult = configSchema.safeParse(configData);

if (!parseResult.success) {
consola.error(
".devenv.json の形式が不正です:",
parseResult.error.message,
);
process.exit(1);
}

const config: DevEnvConfig = parseResult.data;

if (config.modules.length === 0) {
consola.warn("インストール済みのモジュールがありません。");
return;
}

consola.start("テンプレートをダウンロード中...");

// テンプレートを一時ディレクトリにダウンロード
const tempDir = join(targetDir, ".devenv-temp");

try {
const { dir: templateDir } = await downloadTemplate(TEMPLATE_SOURCE, {
dir: tempDir,
force: true,
});

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

// 差分検出
const diff = await detectDiff({
targetDir,
templateDir,
moduleIds: config.modules,
config,
});

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

if (pushableFiles.length === 0) {
consola.info("push するファイルがありません。");
console.log();
console.log(formatDiff(diff, false));
return;
}

// ドライランモード
if (args.dryRun) {
consola.info("[ドライラン] 以下のファイルが PR として送信されます:");
console.log();
console.log(formatDiff(diff, true));
console.log();
consola.info("[ドライラン] 実際の PR は作成されませんでした。");
return;
}

// 確認プロンプト
if (!args.force) {
const confirmed = await promptPushConfirm(diff);
if (!confirmed) {
consola.info("キャンセルしました。");
return;
}
}

// GitHub トークン取得
let token = getGitHubToken();
if (!token) {
token = await promptGitHubToken();
}

// PR タイトル取得
const title = args.message || (await promptPrTitle());

// PR 本文取得
const body = await promptPrBody();

// ファイル内容を準備
const files = pushableFiles.map((f) => ({
path: f.path,
content: f.localContent || "",
}));

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

// PR 作成
const result = await createPullRequest(token, {
owner: config.source.owner,
repo: config.source.repo,
files,
title,
body,
baseBranch: config.source.ref || "main",
});

console.log();
consola.success(`PR を作成しました!`);
console.log();
console.log(` URL: ${result.url}`);
console.log(` Branch: ${result.branch}`);
console.log();
} finally {
// 一時ディレクトリを削除
if (existsSync(tempDir)) {
await rm(tempDir, { recursive: true, force: true });
}
}
},
});
Loading