@minimalcorp/tsunagi 0.1.31 → 0.1.33
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]__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]__0tq9wv6._.js → [root-of-the-server]__0r5k1o3._.js} +2 -2
- 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_065xsw3._.js → node_modules_00bsm~0._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0dmu62y._.js → node_modules_03dusl-._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0z0orm9._.js → node_modules_09g056t._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0jcrj-2._.js → node_modules_09o-nzb._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0v86kpj._.js → node_modules_0_wjeog._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0vm.f2d._.js → node_modules_0az0yoa._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_046ca89._.js → node_modules_0ec.p48._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0c_-8tt._.js → node_modules_131360a._.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/02k8hsfyr1oif.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/076mcjezjnahd.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/08u0u06ip_aj0.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/0dqv~laaou7x3.js +64 -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/0k9kik1oc11wc.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0szj.38-.-4p4.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0w-rozhi-~746.js +11 -0
- package/.next/standalone/apps/web/.next/static/chunks/0z8cfvtbwii50.js +24 -0
- package/.next/standalone/apps/web/.next/static/chunks/0zdnw6m70_-dp.js +1 -0
- package/dist/cli.js +16 -1
- 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 +6 -91
- package/dist/server/generated/prisma/internal/prismaNamespace.js +1 -10
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +1 -11
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +1 -10
- package/dist/server/generated/prisma/models/Repository.d.ts +82 -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 +4 -3
- package/dist/server/lib/repositories/repository.d.ts +7 -1
- package/dist/server/lib/repositories/repository.js +27 -29
- package/dist/server/lib/worktree-manager.js +14 -2
- package/dist/server/pty-manager.js +33 -1
- package/dist/server/routes/repos.js +93 -19
- package/dist/server/routes/terminal.js +3 -3
- package/npm-shrinkwrap.json +235 -194
- 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/schema.prisma +1 -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/{66COjNvXaXarcvgB4nt9B → fUycQdovbCSAwFW2xvyuD}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{66COjNvXaXarcvgB4nt9B → fUycQdovbCSAwFW2xvyuD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{66COjNvXaXarcvgB4nt9B → fUycQdovbCSAwFW2xvyuD}/_ssgManifest.js +0 -0
|
@@ -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) {
|
|
@@ -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();
|
|
@@ -2,6 +2,26 @@ import * as pty from 'node-pty';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
const SCROLLBACK_MAX_BYTES = 256 * 1024; // 256KB
|
|
4
4
|
const SESSION_GC_INTERVAL_MS = 30 * 60 * 1000; // 30分
|
|
5
|
+
/**
|
|
6
|
+
* apps/cli が Fastify サーバー起動時に自分で注入する環境変数
|
|
7
|
+
* (TSUNAGI_SERVER_PORT, NODE_ENV, TSUNAGI_NEXT_PORT, TSUNAGI_OUTER_NODE_ENV)。
|
|
8
|
+
* サーバープロセス自身の制御用に後から足された値のため、内部ターミナルには継承しない。
|
|
9
|
+
* 外側 Terminal 由来の環境変数はこのリストと無関係に、これまで通り全て継承する
|
|
10
|
+
* (PORT はここでは扱わない。apps/cli は generic な PORT を上書きせず、専用キー
|
|
11
|
+
* TSUNAGI_SERVER_PORT で自身の待受ポートを伝えるため、ユーザーが Terminal で明示した
|
|
12
|
+
* PORT はそのまま内部ターミナルまで届く)。
|
|
13
|
+
*
|
|
14
|
+
* NODE_ENV は Fastify/Next 等が直接参照するため、PORT と違って専用キーへ逃がせず、
|
|
15
|
+
* サーバープロセス自身には強制的に 'production' が入る。apps/cli はサーバー起動前の
|
|
16
|
+
* 元の値(外側 Terminal 由来、未設定なら undefined)を TSUNAGI_OUTER_NODE_ENV に退避して
|
|
17
|
+
* 渡してくるので、ここで NODE_ENV を一旦除外した後、退避値があれば NODE_ENV として復元する。
|
|
18
|
+
*/
|
|
19
|
+
const TSUNAGI_INJECTED_ENV_KEYS = [
|
|
20
|
+
'TSUNAGI_SERVER_PORT',
|
|
21
|
+
'NODE_ENV',
|
|
22
|
+
'TSUNAGI_NEXT_PORT',
|
|
23
|
+
'TSUNAGI_OUTER_NODE_ENV',
|
|
24
|
+
];
|
|
5
25
|
class PtyManager {
|
|
6
26
|
sessions = new Map();
|
|
7
27
|
/** sessionId → 最後に input を送信した socket.id */
|
|
@@ -22,13 +42,25 @@ class PtyManager {
|
|
|
22
42
|
throw new Error(`Session already exists: ${sessionId}`);
|
|
23
43
|
}
|
|
24
44
|
const shell = process.env.SHELL || (os.platform() === 'win32' ? 'cmd.exe' : 'bash');
|
|
45
|
+
// tsunagi 自身が注入した環境変数は内部ターミナルに継承しない(TSUNAGI_INJECTED_ENV_KEYS 参照)。
|
|
46
|
+
// /settings 等で明示的に同名キーを設定した場合は env(dbEnv 経由)が後勝ちで上書きするため、
|
|
47
|
+
// カスケードの優先順位・挙動は変わらない。
|
|
48
|
+
const baseEnv = { ...process.env };
|
|
49
|
+
for (const key of TSUNAGI_INJECTED_ENV_KEYS) {
|
|
50
|
+
delete baseEnv[key];
|
|
51
|
+
}
|
|
52
|
+
// 外側 Terminal に元々 NODE_ENV があった場合は復元する。なければ未設定のままにする
|
|
53
|
+
// (tsunagi が強制した 'production' を内部ターミナルに漏らさないため)。
|
|
54
|
+
if (process.env.TSUNAGI_OUTER_NODE_ENV !== undefined) {
|
|
55
|
+
baseEnv.NODE_ENV = process.env.TSUNAGI_OUTER_NODE_ENV;
|
|
56
|
+
}
|
|
25
57
|
const ptyProcess = pty.spawn(shell, [], {
|
|
26
58
|
name: 'xterm-256color',
|
|
27
59
|
cols: 80,
|
|
28
60
|
rows: 24,
|
|
29
61
|
cwd,
|
|
30
62
|
env: {
|
|
31
|
-
...
|
|
63
|
+
...baseEnv,
|
|
32
64
|
...env,
|
|
33
65
|
TERM: 'xterm-256color',
|
|
34
66
|
COLORTERM: 'truecolor',
|
|
@@ -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');
|
|
@@ -7,9 +7,9 @@ import { getEnv } from '../lib/repositories/environment.js';
|
|
|
7
7
|
import { ensureClaudeOnboardingCompleted } from '../lib/claude-config-guard.js';
|
|
8
8
|
// サーバーはプロジェクトルートから起動されるため process.cwd() でルートを取得
|
|
9
9
|
const TSUNAGI_EDITOR_PATH = path.resolve(process.cwd(), 'scripts/monaco-editor.sh');
|
|
10
|
-
// Fastify(API) の公開ポート。index.ts
|
|
11
|
-
//
|
|
12
|
-
const SERVER_PORT = Number(process.env.
|
|
10
|
+
// Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数(TSUNAGI_SERVER_PORT)を見る。
|
|
11
|
+
// monaco-editor.sh など PTY 内のプロセスが API を叩く際の同一ホスト向けベース URL に使う。
|
|
12
|
+
const SERVER_PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
|
|
13
13
|
/**
|
|
14
14
|
* 再接続時の画面復元用 scrollback から「端末への問い合わせ(クエリ)」シーケンスを除去する。
|
|
15
15
|
*
|