@minimalcorp/tsunagi 0.1.44 → 0.1.46
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/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0ze019p._.js → [root-of-the-server]__00qvk7s._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d7excn._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__13yhig0._.js → [root-of-the-server]__0n83hx6._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w3_4.k._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +13 -13
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0lgu9gx._.js → node_modules_049fpp9._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0f3mifg._.js → node_modules_04g5ove._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0z02r3w._.js → node_modules_076bd-f._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_07f58ia._.js → node_modules_0akt-n8._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0p2g6on._.js → node_modules_0b48qku._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0szd5t1._.js → node_modules_0bnsxo_._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ih0wxv._.js → node_modules_0bvpcr6._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0bq-u.l._.js → node_modules_0dmu3re._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0~pcd4h._.js → node_modules_0gq_a6h._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0nwi8c2._.js → node_modules_0p8a~x3._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06uw9~g._.js → node_modules_0pksdi6._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_00z7dqq._.js → node_modules_0qvxljj._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0vtmkdp._.js → node_modules_0soq_if._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_07ve26r._.js → node_modules_0v6uk6x._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_00~3~7_._.js → node_modules_0ydrd8p._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10mbxmu._.js → node_modules_0zir34w._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{0wvv75.1gvloi.js → 0.ud795.723zr.js} +13 -13
- package/.next/standalone/apps/web/.next/static/chunks/{0.e10jueei8qv.js → 04fnr6blquen6.js} +3 -3
- package/.next/standalone/apps/web/.next/static/chunks/04h-q2fe2awuu.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0u_07wgcigiqu.js → 0pfyw6txeoy_p.js} +7 -7
- package/.next/standalone/apps/web/.next/static/chunks/{10usg-2-r6y3f.js → 0z43cogzp17lz.js} +3 -3
- package/dist/prisma/migrations/20260929000000_drop_task_worktree_status/migration.sql +31 -0
- package/dist/prisma/schema.prisma +0 -1
- package/dist/server/generated/prisma/internal/class.js +4 -4
- package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +0 -1
- package/dist/server/generated/prisma/internal/prismaNamespace.js +0 -1
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +0 -1
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +0 -1
- package/dist/server/generated/prisma/models/Task.d.ts +1 -40
- package/dist/server/lib/branch-utils.d.ts +1 -0
- package/dist/server/lib/branch-utils.js +30 -0
- package/dist/server/lib/repositories/task.js +0 -3
- package/dist/server/lib/services/task-service.d.ts +10 -2
- package/dist/server/lib/services/task-service.js +79 -48
- package/dist/server/lib/worktree-manager.d.ts +2 -0
- package/dist/server/lib/worktree-manager.js +22 -0
- package/dist/server/routes/mcp.js +7 -2
- package/dist/server/routes/repos.js +20 -15
- package/dist/server/routes/tasks.js +22 -23
- package/dist/server/routes/terminal.js +6 -1
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/09osn8dd42~wz.css +0 -4
- /package/.next/standalone/apps/web/.next/static/{b89JVAeQs5UYaMHr5WMOT → eJVDCrc-0k3bUASWyCnDU}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{b89JVAeQs5UYaMHr5WMOT → eJVDCrc-0k3bUASWyCnDU}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{b89JVAeQs5UYaMHr5WMOT → eJVDCrc-0k3bUASWyCnDU}/_ssgManifest.js +0 -0
|
@@ -22,8 +22,8 @@ export interface CreateTaskParams {
|
|
|
22
22
|
status?: Task['status'];
|
|
23
23
|
}
|
|
24
24
|
export declare class TaskServiceError extends Error {
|
|
25
|
-
code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR';
|
|
26
|
-
constructor(message: string, code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR');
|
|
25
|
+
code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'INVALID_BRANCH' | 'WORKTREE_CREATION_FAILED' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR';
|
|
26
|
+
constructor(message: string, code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'INVALID_BRANCH' | 'WORKTREE_CREATION_FAILED' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR');
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* id / session_id / cwd からタスクを解決する
|
|
@@ -42,11 +42,19 @@ export declare function listTasks(filter?: {
|
|
|
42
42
|
* タスクを取得(identifier で解決)
|
|
43
43
|
*/
|
|
44
44
|
export declare function getTask(identifier: TaskIdentifier): Promise<Task>;
|
|
45
|
+
/**
|
|
46
|
+
* 新規タスクのブランチ名を検証する(worktree 作成に失敗する値を事前に弾く)
|
|
47
|
+
*/
|
|
48
|
+
export declare function validateNewTaskBranch(owner: string, repo: string, branch: string): Promise<void>;
|
|
45
49
|
/**
|
|
46
50
|
* タスクを作成
|
|
51
|
+
*
|
|
52
|
+
* worktree 作成に成功した場合のみタスクを登録する(worktree の無いタスクを作らない)。
|
|
53
|
+
* fetch 失敗時はローカルの参照で worktree を作成し、warnings で通知する。
|
|
47
54
|
*/
|
|
48
55
|
export declare function createTask(params: CreateTaskParams, options?: ServiceOptions): Promise<{
|
|
49
56
|
task: Task;
|
|
57
|
+
warnings: string[];
|
|
50
58
|
}>;
|
|
51
59
|
/**
|
|
52
60
|
* タスクを更新(identifier で解決)
|
|
@@ -5,7 +5,7 @@ import * as os from 'os';
|
|
|
5
5
|
import * as taskRepo from '../repositories/task.js';
|
|
6
6
|
import * as repoRepo from '../repositories/repository.js';
|
|
7
7
|
import * as worktreeManager from '../worktree-manager.js';
|
|
8
|
-
import { normalizeBranchName } from '../branch-utils.js';
|
|
8
|
+
import { normalizeBranchName, validateBranchName } from '../branch-utils.js';
|
|
9
9
|
import { prisma } from '../db.js';
|
|
10
10
|
export class TaskServiceError extends Error {
|
|
11
11
|
code;
|
|
@@ -90,73 +90,104 @@ export async function getTask(identifier) {
|
|
|
90
90
|
}
|
|
91
91
|
return task;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* 新規タスクのブランチ名を検証する(worktree 作成に失敗する値を事前に弾く)
|
|
95
|
+
*/
|
|
96
|
+
export async function validateNewTaskBranch(owner, repo, branch) {
|
|
97
|
+
const invalidReason = validateBranchName(branch);
|
|
98
|
+
if (invalidReason || !(await worktreeManager.isValidBranchRef(branch))) {
|
|
99
|
+
throw new TaskServiceError(`Invalid branch name "${branch}": ${invalidReason ?? 'rejected by git check-ref-format'}`, 'INVALID_BRANCH');
|
|
100
|
+
}
|
|
101
|
+
const existingTasks = await taskRepo.getTasks({ owner, repo, includeDeleted: false });
|
|
102
|
+
const duplicateTask = existingTasks.find((task) => task.branch === branch);
|
|
103
|
+
if (duplicateTask) {
|
|
104
|
+
throw new TaskServiceError(`Branch "${branch}" already exists. Task "${duplicateTask.title}" (ID: ${duplicateTask.id}) is already using this branch.`, 'BRANCH_DUPLICATE');
|
|
105
|
+
}
|
|
106
|
+
// "/" は "-" に正規化されるため、別名のブランチでも worktree ディレクトリが衝突しうる
|
|
107
|
+
if (await worktreeManager.worktreeExists(owner, repo, branch)) {
|
|
108
|
+
throw new TaskServiceError(`Worktree directory "${normalizeBranchName(branch)}" already exists. Use a different branch name.`, 'BRANCH_DUPLICATE');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
93
111
|
/**
|
|
94
112
|
* タスクを作成
|
|
113
|
+
*
|
|
114
|
+
* worktree 作成に成功した場合のみタスクを登録する(worktree の無いタスクを作らない)。
|
|
115
|
+
* fetch 失敗時はローカルの参照で worktree を作成し、warnings で通知する。
|
|
95
116
|
*/
|
|
96
117
|
export async function createTask(params, options) {
|
|
97
118
|
const { title, description = '', owner, repo, baseBranch: baseBranchInput, effort, order, status = 'backlog', } = params;
|
|
119
|
+
const warnings = [];
|
|
98
120
|
// リポジトリ存在チェック
|
|
99
121
|
const repository = await repoRepo.getRepo(owner, repo);
|
|
100
122
|
if (!repository) {
|
|
101
123
|
throw new TaskServiceError('Repository not found. Please clone the repository first.', 'REPO_NOT_FOUND');
|
|
102
124
|
}
|
|
103
|
-
// baseBranch解決
|
|
104
|
-
const baseBranch = baseBranchInput || (await worktreeManager.getDefaultBranch(owner, repo));
|
|
105
125
|
// branch名の解決(未指定の場合はtitleから自動生成)
|
|
106
126
|
const branch = params.branch || generateBranchName(title);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const duplicateTask = existingTasks.find((task) => task.owner === owner && task.repo === repo && task.branch === branch);
|
|
110
|
-
if (duplicateTask) {
|
|
111
|
-
throw new TaskServiceError(`Branch "${branch}" already exists. Task "${duplicateTask.title}" (ID: ${duplicateTask.id}) is already using this branch.`, 'BRANCH_DUPLICATE');
|
|
112
|
-
}
|
|
113
|
-
// 1. order解決 + bumpOrder + DB登録をアトミックに実行
|
|
114
|
-
const newTask = await prisma.$transaction(async (tx) => {
|
|
115
|
-
let resolvedOrder;
|
|
116
|
-
if (order !== undefined) {
|
|
117
|
-
resolvedOrder = order;
|
|
118
|
-
await taskRepo.bumpOrder(resolvedOrder, undefined, tx);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
const result = await tx.task.aggregate({
|
|
122
|
-
where: { owner, repo, deletedAt: null },
|
|
123
|
-
_max: { order: true },
|
|
124
|
-
});
|
|
125
|
-
resolvedOrder = (result._max.order ?? -1) + 1;
|
|
126
|
-
}
|
|
127
|
-
return await taskRepo.createTask({
|
|
128
|
-
title,
|
|
129
|
-
description,
|
|
130
|
-
status,
|
|
131
|
-
owner,
|
|
132
|
-
repo,
|
|
133
|
-
branch,
|
|
134
|
-
baseBranch,
|
|
135
|
-
repoId: repository.id,
|
|
136
|
-
worktreeStatus: 'pending',
|
|
137
|
-
effort,
|
|
138
|
-
order: resolvedOrder,
|
|
139
|
-
}, tx);
|
|
140
|
-
});
|
|
141
|
-
// 3. worktree作成
|
|
127
|
+
await validateNewTaskBranch(owner, repo, branch);
|
|
128
|
+
// 1. fetch(失敗してもローカルの参照で続行する)
|
|
142
129
|
try {
|
|
143
130
|
await worktreeManager.fetchRemote(owner, repo);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.warn('Failed to fetch remote:', error);
|
|
134
|
+
warnings.push('Failed to fetch from remote. The worktree was created from local refs; update it once the network is available.');
|
|
135
|
+
}
|
|
136
|
+
// baseBranch解決
|
|
137
|
+
const baseBranch = baseBranchInput || (await worktreeManager.getDefaultBranch(owner, repo));
|
|
138
|
+
// 2. worktree作成(失敗したらタスクは作らない)
|
|
139
|
+
try {
|
|
144
140
|
await worktreeManager.createWorktree(owner, repo, branch, baseBranch);
|
|
145
|
-
await taskRepo.updateTask(newTask.id, { worktreeStatus: 'created' });
|
|
146
141
|
}
|
|
147
142
|
catch (error) {
|
|
148
143
|
console.error('Failed to create worktree:', error);
|
|
149
|
-
|
|
144
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
145
|
+
throw new TaskServiceError(`Failed to create worktree: ${reason}`, 'WORKTREE_CREATION_FAILED');
|
|
150
146
|
}
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
// 3. order解決 + bumpOrder + DB登録をアトミックに実行(失敗したら worktree を削除)
|
|
148
|
+
let newTask;
|
|
149
|
+
try {
|
|
150
|
+
newTask = await prisma.$transaction(async (tx) => {
|
|
151
|
+
let resolvedOrder;
|
|
152
|
+
if (order !== undefined) {
|
|
153
|
+
resolvedOrder = order;
|
|
154
|
+
await taskRepo.bumpOrder(resolvedOrder, undefined, tx);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const result = await tx.task.aggregate({
|
|
158
|
+
where: { owner, repo, deletedAt: null },
|
|
159
|
+
_max: { order: true },
|
|
160
|
+
});
|
|
161
|
+
resolvedOrder = (result._max.order ?? -1) + 1;
|
|
162
|
+
}
|
|
163
|
+
return await taskRepo.createTask({
|
|
164
|
+
title,
|
|
165
|
+
description,
|
|
166
|
+
status,
|
|
167
|
+
owner,
|
|
168
|
+
repo,
|
|
169
|
+
branch,
|
|
170
|
+
baseBranch,
|
|
171
|
+
repoId: repository.id,
|
|
172
|
+
effort,
|
|
173
|
+
order: resolvedOrder,
|
|
174
|
+
}, tx);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// force=false: 既存のローカルブランチを流用した場合に削除しないよう、ブランチは残す
|
|
179
|
+
try {
|
|
180
|
+
await worktreeManager.removeWorktree(owner, repo, branch);
|
|
181
|
+
}
|
|
182
|
+
catch (cleanupError) {
|
|
183
|
+
console.error('Failed to remove worktree after task creation failure:', cleanupError);
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
156
186
|
}
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
187
|
+
// 初期Tabは作らない。タブはユーザーがタスク詳細で起動先(Claude / Ollama 等)を選んで作る
|
|
188
|
+
// 4. Socket.IO通知
|
|
189
|
+
options?.io?.emit('task:created', { task: newTask, warnings });
|
|
190
|
+
return { task: newTask, warnings };
|
|
160
191
|
}
|
|
161
192
|
/**
|
|
162
193
|
* タスクを更新(identifier で解決)
|
|
@@ -12,6 +12,8 @@ export declare function getRemoteBranches(owner: string, repo: string): Promise<
|
|
|
12
12
|
export declare function isEmptyRepo(owner: string, repo: string): Promise<boolean>;
|
|
13
13
|
export declare function isUnbornWorktree(worktreePath: string): Promise<boolean>;
|
|
14
14
|
export declare function getDefaultBranch(owner: string, repo: string): Promise<string>;
|
|
15
|
+
export declare function isValidBranchRef(branch: string): Promise<boolean>;
|
|
16
|
+
export declare function worktreeExists(owner: string, repo: string, branch: string): Promise<boolean>;
|
|
15
17
|
export declare function createWorktree(owner: string, repo: string, branch: string, baseBranch?: string): Promise<{
|
|
16
18
|
worktreePath: string;
|
|
17
19
|
}>;
|
|
@@ -169,6 +169,28 @@ export async function getDefaultBranch(owner, repo) {
|
|
|
169
169
|
return branches[0] || 'main';
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
// git 自身のルールでブランチ名として有効か判定(最終判定用)
|
|
173
|
+
// --branch はカレントのリポジトリ探索を伴い cwd 次第で失敗するため、refs/heads/ 形式で判定する
|
|
174
|
+
// ("-" 始まり・"@"・"HEAD" は validateBranchName 側で弾く)
|
|
175
|
+
export async function isValidBranchRef(branch) {
|
|
176
|
+
try {
|
|
177
|
+
await simpleGit().raw(['check-ref-format', `refs/heads/${branch}`]);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// worktreeディレクトリが存在するか確認
|
|
185
|
+
export async function worktreeExists(owner, repo, branch) {
|
|
186
|
+
try {
|
|
187
|
+
await fs.access(getWorktreePath(owner, repo, branch));
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
172
194
|
// worktreeを作成
|
|
173
195
|
export async function createWorktree(owner, repo, branch, baseBranch) {
|
|
174
196
|
const bareRepoPath = await ensureBareRepository(owner, repo);
|
|
@@ -93,7 +93,7 @@ function createMcpServer(io) {
|
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
name: 'tsunagi_create_task',
|
|
96
|
-
description: 'タスクを作成する。worktree作成・初期タブ作成・Socket.IO通知を含むフル作成フロー。branchを省略するとtitleから自動生成される。',
|
|
96
|
+
description: 'タスクを作成する。worktree作成・初期タブ作成・Socket.IO通知を含むフル作成フロー。branchを省略するとtitleから自動生成される。worktree作成に失敗した場合(gitで不正なブランチ名等)はタスクは作成されない。',
|
|
97
97
|
inputSchema: {
|
|
98
98
|
type: 'object',
|
|
99
99
|
required: ['owner', 'repo', 'title'],
|
|
@@ -231,7 +231,12 @@ function createMcpServer(io) {
|
|
|
231
231
|
branch,
|
|
232
232
|
baseBranch,
|
|
233
233
|
}, { io });
|
|
234
|
-
return {
|
|
234
|
+
return {
|
|
235
|
+
content: [
|
|
236
|
+
{ type: 'text', text: JSON.stringify(result.task, null, 2) },
|
|
237
|
+
...result.warnings.map((warning) => ({ type: 'text', text: `Warning: ${warning}` })),
|
|
238
|
+
],
|
|
239
|
+
};
|
|
235
240
|
}
|
|
236
241
|
case 'tsunagi_update_task': {
|
|
237
242
|
const { id, session_id, cwd, title, description, status, effort, order, baseBranch, pullRequestUrl, } = (args ?? {});
|
|
@@ -215,39 +215,44 @@ export async function reposRoutes(fastify) {
|
|
|
215
215
|
fastify.log.warn(cleanupError, 'Failed to cleanup bare repository after clone failure');
|
|
216
216
|
}
|
|
217
217
|
};
|
|
218
|
-
const sshUrl = `git@github.com:${owner}/${repo}.git`;
|
|
219
218
|
const isHttpsUrl = normalizedUrl.startsWith('http');
|
|
219
|
+
const protocol = isHttpsUrl ? 'HTTPS' : 'SSH';
|
|
220
|
+
const altProtocol = isHttpsUrl ? 'SSH' : 'HTTPS';
|
|
221
|
+
// 認証エラー時にもう一方の方式で試し直すためのURL
|
|
222
|
+
const altUrl = isHttpsUrl
|
|
223
|
+
? `git@github.com:${owner}/${repo}.git`
|
|
224
|
+
: `https://github.com/${owner}/${repo}.git`;
|
|
220
225
|
let clonedUrl = normalizedUrl;
|
|
221
226
|
try {
|
|
222
227
|
await worktreeManager.initBareRepository(owner, repo, normalizedUrl);
|
|
223
228
|
}
|
|
224
229
|
catch (error) {
|
|
225
230
|
await cleanupBareRepo();
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
if (!
|
|
231
|
+
// 認証手段は環境により異なる(SSH鍵のみ / HTTPS + credential helper のみ 等)。
|
|
232
|
+
// 認証エラーならもう一方の方式で自動的に試し直す。
|
|
233
|
+
if (!isGitAuthError(error))
|
|
229
234
|
throw error;
|
|
230
|
-
fastify.log.info({ owner, repo },
|
|
235
|
+
fastify.log.info({ owner, repo }, `${protocol} clone failed with an auth error, retrying over ${altProtocol}`);
|
|
231
236
|
try {
|
|
232
|
-
await worktreeManager.initBareRepository(owner, repo,
|
|
233
|
-
clonedUrl =
|
|
237
|
+
await worktreeManager.initBareRepository(owner, repo, altUrl);
|
|
238
|
+
clonedUrl = altUrl;
|
|
234
239
|
}
|
|
235
|
-
catch (
|
|
240
|
+
catch (altError) {
|
|
236
241
|
await cleanupBareRepo();
|
|
237
|
-
const
|
|
238
|
-
const
|
|
242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
243
|
+
const altMessage = altError instanceof Error ? altError.message : String(altError);
|
|
239
244
|
throw new Error(`Failed to clone ${owner}/${repo}. ` +
|
|
240
|
-
`HTTPS
|
|
241
|
-
`SSH鍵(ssh-agent)を設定するか、リポジトリへのアクセス権を確認してください。\n` +
|
|
242
|
-
|
|
245
|
+
`HTTPS・SSH のどちらでも認証に失敗しました。` +
|
|
246
|
+
`SSH鍵(ssh-agent)または git の credential helper を設定するか、リポジトリへのアクセス権を確認してください。\n` +
|
|
247
|
+
`${protocol}: ${message.trim()}\n${altProtocol}: ${altMessage.trim()}`);
|
|
243
248
|
}
|
|
244
249
|
}
|
|
245
250
|
const newRepo = await createRepo({ owner, repo, cloneUrl: clonedUrl });
|
|
246
251
|
return reply.status(200).send({
|
|
247
252
|
data: {
|
|
248
253
|
repository: { ...newRepo, bareRepoPath },
|
|
249
|
-
//
|
|
250
|
-
|
|
254
|
+
// 認証できず別方式で clone し直した場合、UI側で利用者に知らせる
|
|
255
|
+
fallbackProtocol: clonedUrl !== normalizedUrl ? altProtocol.toLowerCase() : null,
|
|
251
256
|
},
|
|
252
257
|
});
|
|
253
258
|
}
|
|
@@ -2,7 +2,13 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
2
2
|
import * as taskRepo from '../lib/repositories/task.js';
|
|
3
3
|
import * as worktreeManager from '../lib/worktree-manager.js';
|
|
4
4
|
import { ptyManager } from '../pty-manager.js';
|
|
5
|
-
import { listTasks, createTask, getTask, updateTask, deleteTask, TaskServiceError, } from '../lib/services/task-service.js';
|
|
5
|
+
import { listTasks, createTask, getTask, updateTask, deleteTask, validateNewTaskBranch, TaskServiceError, } from '../lib/services/task-service.js';
|
|
6
|
+
const CREATE_ERROR_STATUS = {
|
|
7
|
+
REPO_NOT_FOUND: 404,
|
|
8
|
+
BRANCH_DUPLICATE: 409,
|
|
9
|
+
INVALID_BRANCH: 400,
|
|
10
|
+
WORKTREE_CREATION_FAILED: 422,
|
|
11
|
+
};
|
|
6
12
|
export async function tasksRoutes(fastify) {
|
|
7
13
|
const io = fastify.io;
|
|
8
14
|
// POST /tasks/validate
|
|
@@ -17,22 +23,16 @@ export async function tasksRoutes(fastify) {
|
|
|
17
23
|
],
|
|
18
24
|
});
|
|
19
25
|
}
|
|
20
|
-
|
|
21
|
-
const duplicateTask = existingTasks.find((task) => task.owner === owner && task.repo === repo && task.branch === branch);
|
|
22
|
-
if (duplicateTask) {
|
|
23
|
-
return reply.status(409).send({
|
|
24
|
-
valid: false,
|
|
25
|
-
errors: [
|
|
26
|
-
{
|
|
27
|
-
field: 'branch',
|
|
28
|
-
message: `Branch "${branch}" already exists. Task "${duplicateTask.title}" (ID: ${duplicateTask.id}) is already using this branch.`,
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
});
|
|
32
|
-
}
|
|
26
|
+
await validateNewTaskBranch(owner, repo, branch);
|
|
33
27
|
return reply.status(200).send({ valid: true });
|
|
34
28
|
}
|
|
35
29
|
catch (error) {
|
|
30
|
+
if (error instanceof TaskServiceError) {
|
|
31
|
+
return reply.status(error.code === 'BRANCH_DUPLICATE' ? 409 : 400).send({
|
|
32
|
+
valid: false,
|
|
33
|
+
errors: [{ field: 'branch', message: error.message }],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
36
|
fastify.log.error(error, 'POST /tasks/validate error');
|
|
37
37
|
return reply.status(500).send({
|
|
38
38
|
valid: false,
|
|
@@ -128,11 +128,11 @@ export async function tasksRoutes(fastify) {
|
|
|
128
128
|
}
|
|
129
129
|
try {
|
|
130
130
|
const result = await createTask({ title, description, owner, repo, branch, baseBranch, effort, order }, { io });
|
|
131
|
-
return reply.status(201).send({ data: { task: result.task } });
|
|
131
|
+
return reply.status(201).send({ data: { task: result.task, warnings: result.warnings } });
|
|
132
132
|
}
|
|
133
133
|
catch (error) {
|
|
134
134
|
if (error instanceof TaskServiceError) {
|
|
135
|
-
const statusCode = error.code
|
|
135
|
+
const statusCode = CREATE_ERROR_STATUS[error.code] ?? 500;
|
|
136
136
|
return reply.status(statusCode).send({
|
|
137
137
|
errors: [{ field: 'global', message: error.message }],
|
|
138
138
|
});
|
|
@@ -146,7 +146,8 @@ export async function tasksRoutes(fastify) {
|
|
|
146
146
|
try {
|
|
147
147
|
const { id } = request.params;
|
|
148
148
|
const task = await getTask({ id });
|
|
149
|
-
|
|
149
|
+
const worktreeExists = await worktreeManager.worktreeExists(task.owner, task.repo, task.branch);
|
|
150
|
+
return reply.status(200).send({ data: { task, worktreeExists } });
|
|
150
151
|
}
|
|
151
152
|
catch (error) {
|
|
152
153
|
if (error instanceof TaskServiceError && error.code === 'TASK_NOT_FOUND') {
|
|
@@ -243,10 +244,8 @@ export async function tasksRoutes(fastify) {
|
|
|
243
244
|
if (!task) {
|
|
244
245
|
return reply.status(404).send({ error: 'Task not found' });
|
|
245
246
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
needsRebase = await worktreeManager.checkRebaseNeeded(task.owner, task.repo, task.branch, task.baseBranch);
|
|
249
|
-
}
|
|
247
|
+
// worktreeが存在しない場合は checkRebaseNeeded が false を返す
|
|
248
|
+
const needsRebase = await worktreeManager.checkRebaseNeeded(task.owner, task.repo, task.branch, task.baseBranch);
|
|
250
249
|
return reply.status(200).send({ data: { needsRebase } });
|
|
251
250
|
}
|
|
252
251
|
catch (error) {
|
|
@@ -263,8 +262,8 @@ export async function tasksRoutes(fastify) {
|
|
|
263
262
|
if (!task) {
|
|
264
263
|
return reply.status(404).send({ error: 'Task not found' });
|
|
265
264
|
}
|
|
266
|
-
if (task.
|
|
267
|
-
return reply.status(400).send({ error: 'Worktree
|
|
265
|
+
if (!(await worktreeManager.worktreeExists(task.owner, task.repo, task.branch))) {
|
|
266
|
+
return reply.status(400).send({ error: 'Worktree not found' });
|
|
268
267
|
}
|
|
269
268
|
const isClaudeRunning = task.tabs.some((tab) => tab.status === 'running');
|
|
270
269
|
if (isClaudeRunning) {
|
|
@@ -227,11 +227,16 @@ export async function terminalRoutes(fastify) {
|
|
|
227
227
|
let workingDir = tab
|
|
228
228
|
? getWorktreePath(tab.task.owner, tab.task.repo, tab.task.branch)
|
|
229
229
|
: (cwd ?? defaultDir);
|
|
230
|
-
// cwd
|
|
230
|
+
// cwd が存在するか確認。Task のタブは worktree 必須(無ければ起動しない)、
|
|
231
|
+
// それ以外はデフォルトにフォールバック
|
|
231
232
|
try {
|
|
232
233
|
await fs.access(workingDir);
|
|
233
234
|
}
|
|
234
235
|
catch {
|
|
236
|
+
if (tab) {
|
|
237
|
+
fastify.log.warn({ sessionId, workingDir }, 'worktree does not exist');
|
|
238
|
+
return reply.status(409).send({ error: `Worktree not found at ${workingDir}` });
|
|
239
|
+
}
|
|
235
240
|
fastify.log.warn({ sessionId, workingDir }, 'cwd does not exist, falling back to default');
|
|
236
241
|
workingDir = defaultDir;
|
|
237
242
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minimalcorp/tsunagi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@minimalcorp/tsunagi",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.46",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE",
|
|
11
11
|
"os": [
|
|
12
12
|
"darwin",
|
|
@@ -1500,9 +1500,9 @@
|
|
|
1500
1500
|
}
|
|
1501
1501
|
},
|
|
1502
1502
|
"node_modules/bundle-name": {
|
|
1503
|
-
"version": "4.1.
|
|
1504
|
-
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.
|
|
1505
|
-
"integrity": "sha512-
|
|
1503
|
+
"version": "4.1.1",
|
|
1504
|
+
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz",
|
|
1505
|
+
"integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==",
|
|
1506
1506
|
"license": "MIT",
|
|
1507
1507
|
"dependencies": {
|
|
1508
1508
|
"run-applescript": "^7.0.0"
|
package/package.json
CHANGED