@minimalcorp/tsunagi 0.1.32 → 0.1.34
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.js.nft.json +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 +13 -13
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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 +15 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -5
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +15 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +8 -8
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -3
- 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.js.nft.json +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 +15 -15
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +8 -8
- 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 +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- 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]__08c7fz~._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0h4wzcb._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0tq9wv6._.js → [root-of-the-server]__0mct.7z._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0nhzci6._.js +14 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ome304._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0vi01rl._.js +14 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0836rl_._.js → _02dcd~k._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_03hz06e._.js → _0dar4c0._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +17 -17
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0lpms0u._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0owas.l._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0amxree._.js → _0sxye4q._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_109pmo2._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +23 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0dmu62y._.js → node_modules_07-e6tb._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0z0orm9._.js → node_modules_0b6fjwn._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_046ca89._.js → node_modules_0bix9lu._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0vm.f2d._.js → node_modules_0fpqbi2._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0v86kpj._.js → node_modules_0k8xmjr._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0jcrj-2._.js → node_modules_0r9vehj._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_065xsw3._.js → node_modules_0w48uk8._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0c_-8tt._.js → node_modules_0yoat72._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0zerlct._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0xdsm99._.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/00e8eue.21.8h.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/08u0u06ip_aj0.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/0e8u7rqrqfiiy.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0gfpx.h8alews.js +11 -0
- package/.next/standalone/apps/web/.next/static/chunks/0j3485~p9pn9n.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0k9kik1oc11wc.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0kmlu-0djf-rx.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0vlse5ga2r0n7.js +24 -0
- package/.next/standalone/apps/web/.next/static/chunks/0w-rozhi-~746.js +11 -0
- package/.next/standalone/apps/web/.next/static/chunks/0x3kp2s554oy5.js +64 -0
- package/.next/standalone/apps/web/.next/static/chunks/13f6kj~4r5x0p.js +1 -0
- package/dist/server/generated/prisma/browser.d.ts +0 -5
- package/dist/server/generated/prisma/client.d.ts +0 -5
- package/dist/server/generated/prisma/commonInputTypes.d.ts +52 -52
- package/dist/server/generated/prisma/internal/class.d.ts +0 -11
- package/dist/server/generated/prisma/internal/class.js +4 -4
- package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +7 -91
- package/dist/server/generated/prisma/internal/prismaNamespace.js +2 -10
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +2 -11
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +2 -10
- package/dist/server/generated/prisma/models/EnvironmentVariable.d.ts +0 -3
- package/dist/server/generated/prisma/models/Repository.d.ts +82 -1
- package/dist/server/generated/prisma/models/Tab.d.ts +39 -1
- package/dist/server/generated/prisma/models/Task.d.ts +0 -7
- package/dist/server/generated/prisma/models.d.ts +0 -1
- package/dist/server/index.js +0 -2
- package/dist/server/lib/repositories/repository.d.ts +7 -1
- package/dist/server/lib/repositories/repository.js +27 -29
- package/dist/server/lib/repositories/task.d.ts +5 -0
- package/dist/server/lib/repositories/task.js +13 -0
- package/dist/server/lib/worktree-manager.js +14 -2
- package/dist/server/routes/hooks.js +21 -1
- package/dist/server/routes/repos.js +93 -19
- package/dist/server/routes/tasks.js +20 -0
- package/npm-shrinkwrap.json +85 -76
- package/package.json +1 -1
- package/prisma/migrations/20260915000000_drop_planner_tabs/migration.sql +3 -0
- package/prisma/migrations/20260916000000_add_repository_order/migration.sql +12 -0
- package/prisma/migrations/20260918000000_add_tab_unread/migration.sql +4 -0
- package/prisma/schema.prisma +3 -13
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__053-iaj._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__07kf5h-._.js +0 -35
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_gamc9._.js +0 -35
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_0w0y9fd._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0_.o928._.js +0 -3
- package/.next/standalone/apps/web/.next/static/chunks/0.1pdl3yo~xet.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/001oo2jah4cv9.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/05b63w8.ny19p.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/08dtfekvxgpfu.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0dpeys_k2-mrk.js +0 -32
- package/.next/standalone/apps/web/.next/static/chunks/0hvtxos.brr01.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0nh1-0lee-1pk.js +0 -32
- package/.next/standalone/apps/web/.next/static/chunks/0nhpg19g1rl_7.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0tggf01u1vugi.js +0 -3
- package/.next/standalone/apps/web/.next/static/chunks/0wxrfu5la_s94.js +0 -64
- package/.next/standalone/apps/web/.next/static/chunks/0zu48yvkl8pyj.css +0 -4
- package/.next/standalone/apps/web/.next/static/chunks/13twvr.qg3khh.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/164p.6qr5~-7x.js +0 -1
- package/dist/server/generated/prisma/models/PlannerTab.d.ts +0 -1089
- package/dist/server/generated/prisma/models/PlannerTab.js +0 -1
- package/dist/server/routes/planner.d.ts +0 -2
- package/dist/server/routes/planner.js +0 -86
- /package/.next/standalone/apps/web/.next/static/{70bPr4pgLIE7loeTg62ih → mWBhkmlLJfxVxMeoAXXc3}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{70bPr4pgLIE7loeTg62ih → mWBhkmlLJfxVxMeoAXXc3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{70bPr4pgLIE7loeTg62ih → mWBhkmlLJfxVxMeoAXXc3}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Repository } from '@minimalcorp/tsunagi-shared';
|
|
2
2
|
export declare function getRepos(): Promise<Repository[]>;
|
|
3
3
|
export declare function getRepo(owner: string, repo: string): Promise<Repository | null>;
|
|
4
|
-
export declare function createRepo(repo: Omit<Repository, 'id' | 'createdAt'>): Promise<Repository>;
|
|
4
|
+
export declare function createRepo(repo: Omit<Repository, 'id' | 'order' | 'createdAt'>): Promise<Repository>;
|
|
5
5
|
export declare function updateRepo(id: string, updates: Partial<Omit<Repository, 'id' | 'owner' | 'repo' | 'createdAt'>>): Promise<Repository | null>;
|
|
6
|
+
/**
|
|
7
|
+
* 表示順を一括更新する。
|
|
8
|
+
* 渡された id の並びをそのまま order 0..n-1 に割り当てるため、
|
|
9
|
+
* タスクの order のような玉突き処理は不要。
|
|
10
|
+
*/
|
|
11
|
+
export declare function reorderRepos(repoIds: string[]): Promise<Repository[]>;
|
|
6
12
|
export declare function deleteRepo(id: string): Promise<boolean>;
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { prisma } from '../db.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const repos = await prisma.repository.findMany({
|
|
5
|
-
orderBy: { createdAt: 'desc' },
|
|
6
|
-
});
|
|
7
|
-
return repos.map((repo) => ({
|
|
2
|
+
function mapRepo(repo) {
|
|
3
|
+
return {
|
|
8
4
|
id: repo.id,
|
|
9
5
|
owner: repo.owner,
|
|
10
6
|
repo: repo.repo,
|
|
11
7
|
cloneUrl: repo.cloneUrl,
|
|
8
|
+
order: repo.order,
|
|
12
9
|
createdAt: repo.createdAt.toISOString(),
|
|
13
|
-
}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
// リポジトリ一覧取得(表示順)
|
|
13
|
+
export async function getRepos() {
|
|
14
|
+
const repos = await prisma.repository.findMany({
|
|
15
|
+
orderBy: [{ order: 'asc' }, { createdAt: 'asc' }],
|
|
16
|
+
});
|
|
17
|
+
return repos.map(mapRepo);
|
|
14
18
|
}
|
|
15
19
|
// リポジトリ取得
|
|
16
20
|
export async function getRepo(owner, repo) {
|
|
@@ -19,15 +23,9 @@ export async function getRepo(owner, repo) {
|
|
|
19
23
|
});
|
|
20
24
|
if (!repository)
|
|
21
25
|
return null;
|
|
22
|
-
return
|
|
23
|
-
id: repository.id,
|
|
24
|
-
owner: repository.owner,
|
|
25
|
-
repo: repository.repo,
|
|
26
|
-
cloneUrl: repository.cloneUrl,
|
|
27
|
-
createdAt: repository.createdAt.toISOString(),
|
|
28
|
-
};
|
|
26
|
+
return mapRepo(repository);
|
|
29
27
|
}
|
|
30
|
-
//
|
|
28
|
+
// リポジトリ作成(表示順は末尾に追加)
|
|
31
29
|
export async function createRepo(repo) {
|
|
32
30
|
const existing = await prisma.repository.findFirst({
|
|
33
31
|
where: { owner: repo.owner, repo: repo.repo },
|
|
@@ -35,20 +33,16 @@ export async function createRepo(repo) {
|
|
|
35
33
|
if (existing) {
|
|
36
34
|
throw new Error(`Repository ${repo.owner}/${repo.repo} already exists`);
|
|
37
35
|
}
|
|
36
|
+
const last = await prisma.repository.findFirst({ orderBy: { order: 'desc' } });
|
|
38
37
|
const newRepo = await prisma.repository.create({
|
|
39
38
|
data: {
|
|
40
39
|
owner: repo.owner,
|
|
41
40
|
repo: repo.repo,
|
|
42
41
|
cloneUrl: repo.cloneUrl,
|
|
42
|
+
order: last ? last.order + 1 : 0,
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
|
-
return
|
|
46
|
-
id: newRepo.id,
|
|
47
|
-
owner: newRepo.owner,
|
|
48
|
-
repo: newRepo.repo,
|
|
49
|
-
cloneUrl: newRepo.cloneUrl,
|
|
50
|
-
createdAt: newRepo.createdAt.toISOString(),
|
|
51
|
-
};
|
|
45
|
+
return mapRepo(newRepo);
|
|
52
46
|
}
|
|
53
47
|
// リポジトリ更新
|
|
54
48
|
export async function updateRepo(id, updates) {
|
|
@@ -59,15 +53,19 @@ export async function updateRepo(id, updates) {
|
|
|
59
53
|
where: { id },
|
|
60
54
|
data: {
|
|
61
55
|
...(updates.cloneUrl && { cloneUrl: updates.cloneUrl }),
|
|
56
|
+
...(updates.order !== undefined && { order: updates.order }),
|
|
62
57
|
},
|
|
63
58
|
});
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
return mapRepo(updatedRepo);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 表示順を一括更新する。
|
|
63
|
+
* 渡された id の並びをそのまま order 0..n-1 に割り当てるため、
|
|
64
|
+
* タスクの order のような玉突き処理は不要。
|
|
65
|
+
*/
|
|
66
|
+
export async function reorderRepos(repoIds) {
|
|
67
|
+
await prisma.$transaction(repoIds.map((id, index) => prisma.repository.update({ where: { id }, data: { order: index } })));
|
|
68
|
+
return getRepos();
|
|
71
69
|
}
|
|
72
70
|
// リポジトリ削除
|
|
73
71
|
export async function deleteRepo(id) {
|
|
@@ -17,6 +17,11 @@ export declare function createTab(taskId: string, mode?: Tab['mode']): Promise<T
|
|
|
17
17
|
export declare function getTab(taskId: string, tab_id: string): Promise<Tab | null>;
|
|
18
18
|
export declare function updateTab(taskId: string, tab_id: string, updates: Partial<Omit<Tab, 'tab_id' | 'order' | 'startedAt'>>): Promise<Tab | null>;
|
|
19
19
|
export declare function deleteTab(taskId: string, tab_id: string): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* タスク配下の全タブを既読にする。
|
|
22
|
+
* 「タスク詳細を開く = 確認した」とみなすため、タブ単位ではなくタスク単位で落とす。
|
|
23
|
+
*/
|
|
24
|
+
export declare function markTaskTabsRead(taskId: string): Promise<number>;
|
|
20
25
|
/**
|
|
21
26
|
* タスクステータスを遷移させる
|
|
22
27
|
*/
|
|
@@ -156,6 +156,7 @@ export async function updateTab(taskId, tab_id, updates) {
|
|
|
156
156
|
where: { tabId: tab_id },
|
|
157
157
|
data: {
|
|
158
158
|
...(updates.status && { status: updates.status }),
|
|
159
|
+
...(updates.unread !== undefined && { unread: updates.unread }),
|
|
159
160
|
...(updates.completedAt !== undefined && {
|
|
160
161
|
completedAt: updates.completedAt ? new Date(updates.completedAt) : null,
|
|
161
162
|
}),
|
|
@@ -178,6 +179,17 @@ export async function deleteTab(taskId, tab_id) {
|
|
|
178
179
|
return false;
|
|
179
180
|
}
|
|
180
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* タスク配下の全タブを既読にする。
|
|
184
|
+
* 「タスク詳細を開く = 確認した」とみなすため、タブ単位ではなくタスク単位で落とす。
|
|
185
|
+
*/
|
|
186
|
+
export async function markTaskTabsRead(taskId) {
|
|
187
|
+
const result = await prisma.tab.updateMany({
|
|
188
|
+
where: { taskId, unread: true },
|
|
189
|
+
data: { unread: false },
|
|
190
|
+
});
|
|
191
|
+
return result.count;
|
|
192
|
+
}
|
|
181
193
|
// ============================================
|
|
182
194
|
// Claude Tool関数(タスクワークフロー用)
|
|
183
195
|
// ============================================
|
|
@@ -229,6 +241,7 @@ function mapTab(tab) {
|
|
|
229
241
|
order: tab.order,
|
|
230
242
|
status: tab.status,
|
|
231
243
|
mode: tab.mode,
|
|
244
|
+
unread: tab.unread,
|
|
232
245
|
todos: tab.todos ? JSON.parse(tab.todos) : [],
|
|
233
246
|
startedAt: tab.startedAt.toISOString(),
|
|
234
247
|
completedAt: tab.completedAt?.toISOString(),
|
|
@@ -7,6 +7,18 @@ import { promisify } from 'util';
|
|
|
7
7
|
import { normalizeBranchName } from './branch-utils.js';
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const WORKSPACES_ROOT = path.join(os.homedir(), '.tsunagi', 'workspaces');
|
|
10
|
+
/**
|
|
11
|
+
* サーバープロセスにはTTYが無いため、git/sshが認証情報を対話的に要求すると
|
|
12
|
+
* リクエストがハングしてしまう。プロンプトを禁止して即座に失敗させる。
|
|
13
|
+
*/
|
|
14
|
+
const NON_INTERACTIVE_GIT_ENV = {
|
|
15
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
16
|
+
GIT_SSH_COMMAND: 'ssh -o BatchMode=yes',
|
|
17
|
+
};
|
|
18
|
+
function nonInteractiveGit(baseDir) {
|
|
19
|
+
const git = baseDir ? simpleGit(baseDir) : simpleGit();
|
|
20
|
+
return git.env({ ...process.env, ...NON_INTERACTIVE_GIT_ENV });
|
|
21
|
+
}
|
|
10
22
|
// bare repositoryのパスを取得
|
|
11
23
|
function getBareRepoPath(owner, repo) {
|
|
12
24
|
return path.join(WORKSPACES_ROOT, owner, repo, '.bare');
|
|
@@ -51,10 +63,10 @@ export async function initBareRepository(owner, repo, cloneUrl) {
|
|
|
51
63
|
// 親ディレクトリを作成
|
|
52
64
|
await fs.mkdir(path.dirname(bareRepoPath), { recursive: true });
|
|
53
65
|
// bare repositoryとしてクローン
|
|
54
|
-
const git =
|
|
66
|
+
const git = nonInteractiveGit();
|
|
55
67
|
await git.clone(cloneUrl, bareRepoPath, ['--bare']);
|
|
56
68
|
// 標準的なfetch refspecを設定
|
|
57
|
-
const bareGit =
|
|
69
|
+
const bareGit = nonInteractiveGit(bareRepoPath);
|
|
58
70
|
await bareGit.addConfig('remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*');
|
|
59
71
|
// Remote tracking branchesを作成
|
|
60
72
|
await bareGit.fetch();
|
|
@@ -11,14 +11,34 @@ function tasksToTodos(tasks) {
|
|
|
11
11
|
status: t.status,
|
|
12
12
|
}));
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* status から未読フラグを導出する。
|
|
16
|
+
* 完了(success/error)で未読を立て、再実行・セッション終了で落とす。
|
|
17
|
+
* web 側 (apps/web/src/lib/claude-status.ts の unreadForStatus) と同じルール。
|
|
18
|
+
*/
|
|
19
|
+
function unreadForStatus(status) {
|
|
20
|
+
switch (status) {
|
|
21
|
+
case 'success':
|
|
22
|
+
case 'error':
|
|
23
|
+
return true;
|
|
24
|
+
case 'running':
|
|
25
|
+
case 'idle':
|
|
26
|
+
return false;
|
|
27
|
+
default:
|
|
28
|
+
// waiting 等は未読状態を変えない
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** DBでタブのステータス(+ todos / 未読フラグ)を直接更新する */
|
|
15
33
|
async function updateTabStatus(sessionId, status, todos) {
|
|
16
34
|
try {
|
|
35
|
+
const unread = unreadForStatus(status);
|
|
17
36
|
const result = await prisma.tab.updateMany({
|
|
18
37
|
where: { tabId: sessionId },
|
|
19
38
|
data: {
|
|
20
39
|
status,
|
|
21
40
|
...(todos !== undefined && { todos: JSON.stringify(todos) }),
|
|
41
|
+
...(unread !== undefined && { unread }),
|
|
22
42
|
},
|
|
23
43
|
});
|
|
24
44
|
return { count: result.count };
|
|
@@ -7,17 +7,39 @@ import * as worktreeManager from '../lib/worktree-manager.js';
|
|
|
7
7
|
import { getEnv } from '../lib/repositories/environment.js';
|
|
8
8
|
import { createRepo } from '../lib/repositories/repository.js';
|
|
9
9
|
const WORKSPACES_ROOT = path.join(os.homedir(), '.tsunagi', 'workspaces');
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Git URLからowner/repoを抽出する。
|
|
12
|
+
* 前後の空白・末尾のスラッシュ・www. など、コピペで紛れ込みがちな差異は吸収する。
|
|
13
|
+
* 対応形式:
|
|
14
|
+
* https://github.com/owner/repo(.git)
|
|
15
|
+
* git@github.com:owner/repo(.git)
|
|
16
|
+
* ssh://git@github.com/owner/repo(.git)
|
|
17
|
+
*/
|
|
11
18
|
function parseGitUrl(gitUrl) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const normalized = gitUrl.trim().replace(/\/+$/, '');
|
|
20
|
+
const patterns = [
|
|
21
|
+
/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
22
|
+
/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
23
|
+
/^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
24
|
+
];
|
|
25
|
+
for (const pattern of patterns) {
|
|
26
|
+
const match = normalized.match(pattern);
|
|
27
|
+
if (match)
|
|
28
|
+
return { owner: match[1], repo: match[2], url: normalized };
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** 認証が無くて失敗したときに出る git のメッセージ */
|
|
33
|
+
function isGitAuthError(error) {
|
|
34
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
35
|
+
return (message.includes('could not read username') ||
|
|
36
|
+
message.includes('could not read password') ||
|
|
37
|
+
message.includes('authentication failed') ||
|
|
38
|
+
message.includes('terminal prompts disabled') ||
|
|
39
|
+
message.includes('permission denied') ||
|
|
40
|
+
// private repository に未認証でアクセスすると GitHub は 404 を返す
|
|
41
|
+
message.includes('repository not found') ||
|
|
42
|
+
message.includes('not found'));
|
|
21
43
|
}
|
|
22
44
|
export async function reposRoutes(fastify) {
|
|
23
45
|
// GET /repos
|
|
@@ -51,6 +73,27 @@ export async function reposRoutes(fastify) {
|
|
|
51
73
|
return reply.status(500).send({ error: 'Failed to create repository' });
|
|
52
74
|
}
|
|
53
75
|
});
|
|
76
|
+
// PUT /repos/reorder - トップページの列の並び順を一括更新
|
|
77
|
+
fastify.put('/repos/reorder', async (request, reply) => {
|
|
78
|
+
try {
|
|
79
|
+
const { repoIds } = request.body;
|
|
80
|
+
if (!Array.isArray(repoIds) || repoIds.length === 0) {
|
|
81
|
+
return reply.status(400).send({ error: 'Missing required field: repoIds' });
|
|
82
|
+
}
|
|
83
|
+
const existing = await repoRepo.getRepos();
|
|
84
|
+
const existingIds = new Set(existing.map((r) => r.id));
|
|
85
|
+
const unknown = repoIds.filter((id) => !existingIds.has(id));
|
|
86
|
+
if (unknown.length > 0) {
|
|
87
|
+
return reply.status(404).send({ error: `Unknown repository ids: ${unknown.join(', ')}` });
|
|
88
|
+
}
|
|
89
|
+
const repos = await repoRepo.reorderRepos(repoIds);
|
|
90
|
+
return reply.status(200).send({ data: repos });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
fastify.log.error(error, 'PUT /repos/reorder error');
|
|
94
|
+
return reply.status(500).send({ error: 'Failed to reorder repositories' });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
54
97
|
// GET /repos/:owner/:repo
|
|
55
98
|
fastify.get('/repos/:owner/:repo', async (request, reply) => {
|
|
56
99
|
try {
|
|
@@ -149,9 +192,11 @@ export async function reposRoutes(fastify) {
|
|
|
149
192
|
}
|
|
150
193
|
const parsed = parseGitUrl(gitUrl);
|
|
151
194
|
if (!parsed) {
|
|
152
|
-
return reply.status(400).send({
|
|
195
|
+
return reply.status(400).send({
|
|
196
|
+
error: 'Invalid Git URL format. Expected https://github.com/owner/repo or git@github.com:owner/repo',
|
|
197
|
+
});
|
|
153
198
|
}
|
|
154
|
-
const { owner, repo } = parsed;
|
|
199
|
+
const { owner, repo, url: normalizedUrl } = parsed;
|
|
155
200
|
const envVars = await getEnv('global');
|
|
156
201
|
if (envVars.GITHUB_PAT) {
|
|
157
202
|
try {
|
|
@@ -162,20 +207,49 @@ export async function reposRoutes(fastify) {
|
|
|
162
207
|
}
|
|
163
208
|
}
|
|
164
209
|
const bareRepoPath = path.join(os.homedir(), '.tsunagi', 'workspaces', owner, repo, '.bare');
|
|
165
|
-
|
|
166
|
-
await worktreeManager.initBareRepository(owner, repo, gitUrl);
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
210
|
+
const cleanupBareRepo = async () => {
|
|
169
211
|
try {
|
|
170
212
|
await fs.rm(bareRepoPath, { recursive: true, force: true });
|
|
171
213
|
}
|
|
172
214
|
catch (cleanupError) {
|
|
173
215
|
fastify.log.warn(cleanupError, 'Failed to cleanup bare repository after clone failure');
|
|
174
216
|
}
|
|
175
|
-
|
|
217
|
+
};
|
|
218
|
+
const sshUrl = `git@github.com:${owner}/${repo}.git`;
|
|
219
|
+
const isHttpsUrl = normalizedUrl.startsWith('http');
|
|
220
|
+
let clonedUrl = normalizedUrl;
|
|
221
|
+
try {
|
|
222
|
+
await worktreeManager.initBareRepository(owner, repo, normalizedUrl);
|
|
176
223
|
}
|
|
177
|
-
|
|
178
|
-
|
|
224
|
+
catch (error) {
|
|
225
|
+
await cleanupBareRepo();
|
|
226
|
+
// private repository を HTTPS で clone するには認証が必要。
|
|
227
|
+
// tsunagi は SSH agent 前提で動くため、認証エラーなら SSH で自動的に試し直す。
|
|
228
|
+
if (!isHttpsUrl || !isGitAuthError(error))
|
|
229
|
+
throw error;
|
|
230
|
+
fastify.log.info({ owner, repo }, 'HTTPS clone failed with an auth error, retrying over SSH');
|
|
231
|
+
try {
|
|
232
|
+
await worktreeManager.initBareRepository(owner, repo, sshUrl);
|
|
233
|
+
clonedUrl = sshUrl;
|
|
234
|
+
}
|
|
235
|
+
catch (sshError) {
|
|
236
|
+
await cleanupBareRepo();
|
|
237
|
+
const httpsMessage = error instanceof Error ? error.message : String(error);
|
|
238
|
+
const sshMessage = sshError instanceof Error ? sshError.message : String(sshError);
|
|
239
|
+
throw new Error(`Failed to clone ${owner}/${repo}. ` +
|
|
240
|
+
`HTTPS では認証情報が無いため失敗し、SSH でも失敗しました。` +
|
|
241
|
+
`SSH鍵(ssh-agent)を設定するか、リポジトリへのアクセス権を確認してください。\n` +
|
|
242
|
+
`HTTPS: ${httpsMessage.trim()}\nSSH: ${sshMessage.trim()}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const newRepo = await createRepo({ owner, repo, cloneUrl: clonedUrl });
|
|
246
|
+
return reply.status(200).send({
|
|
247
|
+
data: {
|
|
248
|
+
repository: { ...newRepo, bareRepoPath },
|
|
249
|
+
// HTTPSで認証できずSSHで clone し直した場合、UI側で利用者に知らせる
|
|
250
|
+
fallbackToSsh: clonedUrl !== normalizedUrl,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
179
253
|
}
|
|
180
254
|
catch (error) {
|
|
181
255
|
fastify.log.error(error, 'Failed to clone repository');
|
|
@@ -288,6 +288,26 @@ export async function tasksRoutes(fastify) {
|
|
|
288
288
|
return reply.status(500).send({ error: errorMessage });
|
|
289
289
|
}
|
|
290
290
|
});
|
|
291
|
+
// POST /tasks/:id/read - タスク配下の全タブを既読にする(詳細画面を開いた = 確認した)
|
|
292
|
+
fastify.post('/tasks/:id/read', async (request, reply) => {
|
|
293
|
+
try {
|
|
294
|
+
const { id: taskId } = request.params;
|
|
295
|
+
const markedCount = await taskRepo.markTaskTabsRead(taskId);
|
|
296
|
+
const task = await taskRepo.getTask(taskId);
|
|
297
|
+
if (!task) {
|
|
298
|
+
return reply.status(404).send({ error: 'Task not found' });
|
|
299
|
+
}
|
|
300
|
+
// 既読化を他の画面(ボード等)にも反映する。変化が無いときは通知不要
|
|
301
|
+
if (markedCount > 0) {
|
|
302
|
+
io.emit('task:updated', { task });
|
|
303
|
+
}
|
|
304
|
+
return reply.status(200).send({ data: { task } });
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
fastify.log.error(error, 'POST /tasks/:id/read error');
|
|
308
|
+
return reply.status(500).send({ error: 'Failed to mark task as read' });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
291
311
|
// GET /tasks/:id/tabs
|
|
292
312
|
fastify.get('/tasks/:id/tabs', async (request, reply) => {
|
|
293
313
|
try {
|