axmap-cli 1.8.0 → 1.10.0
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/.claude/commands/ax-update.md +0 -3
- package/README.ko.md +31 -0
- package/README.md +31 -0
- package/bin/axmap.mjs +149 -88
- package/governance/vote.mjs +6 -69
- package/mcp/README.md +2 -0
- package/mcp/server.mjs +23 -8
- package/package.json +1 -1
- package/src/bridge.mjs +313 -61
- package/src/bridge.mts +208 -22
- package/src/external-console.mjs +45 -12
- package/src/external-console.mts +44 -14
- package/src/managed-session.mjs +105 -15
- package/src/managed-session.mts +84 -18
- package/src/monitor.mjs +3 -3
- package/src/monitor.mts +3 -3
- package/src/pipeline-mcp.mjs +4 -4
- package/src/pipeline-mcp.mts +4 -4
- package/src/session-dialogue.mjs +21 -5
- package/src/session-dialogue.mts +22 -5
- package/src/session-mailbox.mjs +21 -11
- package/src/session-mailbox.mts +13 -8
- package/src/session-types.mjs +3 -0
- package/src/session-types.mts +18 -6
- package/src/session-usage.mjs +16 -0
- package/src/session-usage.mts +17 -1
- package/src/terminal-bridge.mjs +29 -15
- package/src/terminal-bridge.mts +28 -16
- package/src/update.mjs +3 -20
- package/src/workspace.mjs +247 -0
- package/src/workspace.mts +220 -0
- package/tools/bridge.mjs +14 -6
- package/tools/build.mjs +1 -1
- package/tools/bus.mjs +1 -1
- package/tools/console-queue.mjs +3 -0
- package/tools/run.mjs +11 -4
- package/tools/setup.mjs +1 -1
- package/tools/windows-console.cs +24 -8
- package/tools/windows-console.ps1 +14 -5
- package/src/closure.mjs +0 -343
- package/tools/vendor.mjs +0 -382
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Generated from src/workspace.mts. Edit the .mts source and run npm run build.
|
|
2
|
+
/** 작업 폴더 → 대상 Git 저장소. MCP 서버·로컬 세션 연결·CLI가 이 한 곳에서 정한다.
|
|
3
|
+
*
|
|
4
|
+
* 사람이 AI 도구를 여는 폴더가 Git 저장소가 아닐 수 있다. 예: 한 저장소의 worktree
|
|
5
|
+
* 여러 개를 나란히 담아 둔 `robotable_hj_workspace`. 위로만 찾으면 이 폴더는
|
|
6
|
+
* "Git 저장소 아님"이 되어 연결이 끊긴다. 그래서 위로 찾기에 실패한 폴더에 한해
|
|
7
|
+
* 아래 순서로 정한다 (D30).
|
|
8
|
+
*
|
|
9
|
+
* 0. 수동 설정 — 그 폴더나 조상에 있는 `.axmap.json` 의 `{ "repo": "<경로>" }`
|
|
10
|
+
* 1. 자동 탐색 — 바로 아래 자식 폴더(깊이 1)의 Git 저장소를 공통 디렉터리로 묶어
|
|
11
|
+
* 묶음이 정확히 하나면 그 저장소
|
|
12
|
+
* 2. 모호함 — 묶음이 둘 이상이면 고르지 않고 후보를 적은 오류를 낸다
|
|
13
|
+
*
|
|
14
|
+
* 매번 디스크와 git 에게 다시 묻는다. 캐시하면 사람이 설정 파일을 쓰거나 폴더를
|
|
15
|
+
* 추가한 뒤에도 서버를 다시 켜기 전까지 옛 판단이 남는다.
|
|
16
|
+
*
|
|
17
|
+
* 읽지 못한 것과 없는 것을 구별한다. `.git` 이 있는데 git 이 읽지 못한 자식 폴더를
|
|
18
|
+
* "저장소 없음"으로 세면, 실제로는 두 저장소인데 하나로 보고 엉뚱한 장부를 잡는다.
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
// bin/axmap.mjs·mcp/server.mjs 가 CLI 를 띄울 때마다 import 하므로 Node 내장만 쓴다.
|
|
24
|
+
// 그래서 session-types 의 작은 도우미도 가져오지 않는다.
|
|
25
|
+
const errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
26
|
+
const errorCode = (e) => e instanceof Error && 'code' in e && typeof e.code === 'string' ? e.code : undefined;
|
|
27
|
+
export const WORKSPACE_CONFIG = '.axmap.json';
|
|
28
|
+
/** 오류 문구만으로 종류를 가르지 않도록 사유를 붙인다. */
|
|
29
|
+
export class WorkspaceError extends Error {
|
|
30
|
+
reason;
|
|
31
|
+
constructor(message, reason) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.reason = reason;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// 커밋 훅 안에서는 GIT_DIR 등이 설정돼 있다. 그대로 두면 자식 폴더마다 같은 저장소로
|
|
37
|
+
// 답해서 서로 다른 저장소가 하나로 묶인다. bin/axmap.mjs 의 GIT_ENV_KEYS 와 같은 목록이다.
|
|
38
|
+
const GIT_ENV_KEYS = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE',
|
|
39
|
+
'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_PREFIX'];
|
|
40
|
+
function gitEnv() {
|
|
41
|
+
// "not a git repository" 문구로 판정하므로 번역된 메시지가 나오지 않게 고정한다.
|
|
42
|
+
const env = { ...process.env, LC_ALL: 'C', LANGUAGE: 'C' };
|
|
43
|
+
for (const key of GIT_ENV_KEYS)
|
|
44
|
+
delete env[key];
|
|
45
|
+
return env;
|
|
46
|
+
}
|
|
47
|
+
function askGit(cwd, args) {
|
|
48
|
+
const r = spawnSync('git', args, { cwd, env: gitEnv(), encoding: 'utf8', windowsHide: true });
|
|
49
|
+
if (r.error)
|
|
50
|
+
return { ok: false, notRepo: false, detail: r.error.message };
|
|
51
|
+
if (r.status === 0)
|
|
52
|
+
return { ok: true, lines: (r.stdout ?? '').split(/\r?\n/).map(s => s.trim()).filter(Boolean) };
|
|
53
|
+
const detail = (r.stderr ?? '').trim() || `git 종료 코드 ${r.status}`;
|
|
54
|
+
return { ok: false, notRepo: /not a git repository/i.test(detail), detail };
|
|
55
|
+
}
|
|
56
|
+
const real = (p) => fs.realpathSync(p);
|
|
57
|
+
/** 공통 디렉터리 비교 키. bridge 의 repoKey 와 같은 규칙이다 (Windows 는 대소문자 무시). */
|
|
58
|
+
export const commonKey = (common) => process.platform === 'win32' ? common.toLowerCase() : common;
|
|
59
|
+
function statOrNull(p) {
|
|
60
|
+
try {
|
|
61
|
+
return fs.statSync(p);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (['ENOENT', 'ENOTDIR'].includes(errorCode(e) || ''))
|
|
65
|
+
return null;
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** 폴더가 속한 Git 저장소의 최상위. 저장소 밖이면 null, 판정하지 못하면 예외. */
|
|
70
|
+
export function gitTop(dir) {
|
|
71
|
+
const stat = statOrNull(dir);
|
|
72
|
+
if (!stat || !stat.isDirectory())
|
|
73
|
+
return null;
|
|
74
|
+
const r = askGit(dir, ['rev-parse', '--show-toplevel']);
|
|
75
|
+
if (r.ok) {
|
|
76
|
+
if (!r.lines[0])
|
|
77
|
+
throw new WorkspaceError(`Git 최상위 폴더를 읽지 못했습니다: ${dir}`, 'unreadable');
|
|
78
|
+
return real(r.lines[0]);
|
|
79
|
+
}
|
|
80
|
+
if (r.notRepo)
|
|
81
|
+
return null;
|
|
82
|
+
throw new WorkspaceError(`Git 저장소인지 확인하지 못했습니다: ${dir}\n ${r.detail}`, 'unreadable');
|
|
83
|
+
}
|
|
84
|
+
/** 그 폴더나 조상에 있는 `.axmap.json`. 없으면 null. 읽지 못하면 예외. */
|
|
85
|
+
export function findWorkspaceConfig(dir) {
|
|
86
|
+
for (let at = path.resolve(dir);; at = path.dirname(at)) {
|
|
87
|
+
const candidate = path.join(at, WORKSPACE_CONFIG);
|
|
88
|
+
const stat = statOrNull(candidate);
|
|
89
|
+
if (stat)
|
|
90
|
+
return candidate;
|
|
91
|
+
if (path.dirname(at) === at)
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function manualTarget(config) {
|
|
96
|
+
let data;
|
|
97
|
+
try {
|
|
98
|
+
data = JSON.parse(fs.readFileSync(config, 'utf8'));
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
throw new WorkspaceError(`작업 폴더 설정을 읽지 못했습니다: ${config}\n ${errorMessage(e)}`, 'manual');
|
|
102
|
+
}
|
|
103
|
+
const repo = data?.repo;
|
|
104
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) || typeof repo !== 'string' || !repo.trim()) {
|
|
105
|
+
throw new WorkspaceError(`작업 폴더 설정에 "repo" 경로가 없습니다: ${config}\n 예: { "repo": "폴더이름" }`, 'manual');
|
|
106
|
+
}
|
|
107
|
+
const wanted = path.resolve(path.dirname(config), repo);
|
|
108
|
+
// 설정이 틀렸을 때 자동 탐색으로 넘어가지 않는다. 사람이 고른 것을 몰래 바꾸면
|
|
109
|
+
// 다른 저장소의 장부에 선점이 남는다.
|
|
110
|
+
const top = gitTop(wanted);
|
|
111
|
+
if (!top)
|
|
112
|
+
throw new WorkspaceError(`작업 폴더 설정의 저장소가 Git 저장소가 아닙니다: ${wanted}\n (설정 파일: ${config})`, 'manual');
|
|
113
|
+
return top;
|
|
114
|
+
}
|
|
115
|
+
function scanChildren(dir) {
|
|
116
|
+
let entries;
|
|
117
|
+
try {
|
|
118
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
throw new WorkspaceError(`작업 폴더의 하위 폴더를 읽지 못했습니다: ${dir}\n ${errorMessage(e)}`, 'unreadable');
|
|
122
|
+
}
|
|
123
|
+
const groups = new Map(), unreadable = [];
|
|
124
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
125
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
126
|
+
continue;
|
|
127
|
+
const child = path.join(dir, entry.name);
|
|
128
|
+
try {
|
|
129
|
+
// worktree 폴더가 심볼릭 링크·junction 일 수 있어 링크는 따라가서 본다.
|
|
130
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && !!statOrNull(child)?.isDirectory());
|
|
131
|
+
if (!isDir)
|
|
132
|
+
continue;
|
|
133
|
+
// `.git` 이 없는 폴더는 저장소의 최상위가 아니다. git 을 띄우지 않고 넘어간다.
|
|
134
|
+
if (fs.lstatSync(path.join(child, '.git'), { throwIfNoEntry: false }) === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
unreadable.push(`${entry.name} (${errorMessage(e)})`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const r = askGit(child, ['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir']);
|
|
142
|
+
if (!r.ok || r.lines.length < 2) {
|
|
143
|
+
unreadable.push(`${entry.name} (${r.ok ? 'git 응답이 비어 있음' : r.detail})`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
let top, common;
|
|
147
|
+
try {
|
|
148
|
+
top = real(r.lines[0]);
|
|
149
|
+
common = real(r.lines[1]);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
unreadable.push(`${entry.name} (${errorMessage(e)})`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const key = commonKey(common);
|
|
156
|
+
const group = groups.get(key) ?? { key, common, target: '', members: [] };
|
|
157
|
+
group.members.push({ name: entry.name, top });
|
|
158
|
+
groups.set(key, group);
|
|
159
|
+
}
|
|
160
|
+
for (const group of groups.values()) {
|
|
161
|
+
// 메인 체크아웃(공통 디렉터리 `.git` 의 부모)을 겨눈다. 장부도 거기 있다.
|
|
162
|
+
// bare 저장소처럼 그 부모가 체크아웃이 아니면 첫 번째 worktree 를 쓴다.
|
|
163
|
+
const parent = path.dirname(group.common);
|
|
164
|
+
group.target = path.basename(group.common).toLowerCase() === '.git' && statOrNull(parent)?.isDirectory()
|
|
165
|
+
? real(parent) : group.members[0].top;
|
|
166
|
+
}
|
|
167
|
+
return { groups: [...groups.values()], unreadable };
|
|
168
|
+
}
|
|
169
|
+
/** 무엇을 왜 고르는지까지 돌려준다. 모호함은 예외가 아니라 결과다 (`workspace show` 용). */
|
|
170
|
+
export function inspectTarget(dir) {
|
|
171
|
+
const from = path.resolve(dir);
|
|
172
|
+
const top = gitTop(from);
|
|
173
|
+
if (top)
|
|
174
|
+
return { kind: 'git', from, repo: top };
|
|
175
|
+
if (!statOrNull(from)?.isDirectory())
|
|
176
|
+
return { kind: 'none', from };
|
|
177
|
+
const config = findWorkspaceConfig(from);
|
|
178
|
+
if (config)
|
|
179
|
+
return { kind: 'manual', from, repo: manualTarget(config), config };
|
|
180
|
+
const { groups, unreadable } = scanChildren(from);
|
|
181
|
+
if (unreadable.length) {
|
|
182
|
+
throw new WorkspaceError([
|
|
183
|
+
`작업 폴더 ${from} 아래에서 확인하지 못한 Git 폴더가 있어 대상 저장소를 정하지 않았습니다.`,
|
|
184
|
+
...unreadable.map(s => ` - ${s}`),
|
|
185
|
+
`저장소를 직접 고르려면 이 폴더에서 \`axmap workspace use <폴더>\` 를 실행하세요.`,
|
|
186
|
+
].join('\n'), 'unreadable');
|
|
187
|
+
}
|
|
188
|
+
if (groups.length === 1)
|
|
189
|
+
return { kind: 'auto', from, repo: groups[0].target, group: groups[0] };
|
|
190
|
+
if (groups.length > 1)
|
|
191
|
+
return { kind: 'ambiguous', from, groups };
|
|
192
|
+
return { kind: 'none', from };
|
|
193
|
+
}
|
|
194
|
+
export function ambiguityMessage(r) {
|
|
195
|
+
return [
|
|
196
|
+
`작업 폴더 ${r.from} 아래에 서로 다른 Git 저장소가 ${r.groups.length}개 있어 하나로 정하지 않았습니다.`,
|
|
197
|
+
...r.groups.map(g => ` - ${g.target} (폴더: ${g.members.map(m => m.name).join(', ')})`),
|
|
198
|
+
'고르는 방법 (둘 중 하나):',
|
|
199
|
+
' 1. 도구를 부를 때 cwd 에 작업할 저장소 폴더의 절대 경로를 넘긴다.',
|
|
200
|
+
` 2. 이 폴더에 ${WORKSPACE_CONFIG} 을 둔다: { "repo": "${r.groups[0].members[0].name}" }`,
|
|
201
|
+
` 터미널에서는: axmap workspace use ${r.groups[0].members[0].name}`,
|
|
202
|
+
].join('\n');
|
|
203
|
+
}
|
|
204
|
+
/** 대상 저장소 경로. 저장소가 없으면 null, 모호하거나 판정하지 못하면 예외. */
|
|
205
|
+
export function resolveTarget(dir) {
|
|
206
|
+
const r = inspectTarget(dir);
|
|
207
|
+
if (r.kind === 'ambiguous')
|
|
208
|
+
throw new WorkspaceError(ambiguityMessage(r), 'ambiguous');
|
|
209
|
+
return r.kind === 'none' ? null : r.repo;
|
|
210
|
+
}
|
|
211
|
+
export function describeTarget(r) {
|
|
212
|
+
switch (r.kind) {
|
|
213
|
+
case 'git': return `대상 저장소: ${r.repo}\n이유: 작업 폴더가 Git 저장소 안에 있습니다.`;
|
|
214
|
+
case 'manual': return `대상 저장소: ${r.repo}\n이유: 수동 설정 ${r.config}`;
|
|
215
|
+
case 'auto': return `대상 저장소: ${r.repo}\n이유: 자동 탐색 — 하위 폴더가 모두 같은 저장소입니다 (${r.group.members.map(m => m.name).join(', ')}).`;
|
|
216
|
+
case 'ambiguous': return ambiguityMessage(r);
|
|
217
|
+
case 'none': return `대상 저장소 없음: ${r.from}\n이유: 이 폴더와 바로 아래 폴더에서 Git 저장소를 찾지 못했습니다.`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** `axmap workspace use` — 사람이 고른 저장소를 작업 폴더의 `.axmap.json` 에 적는다. */
|
|
221
|
+
export function writeWorkspaceChoice(dir, choice) {
|
|
222
|
+
const from = path.resolve(dir);
|
|
223
|
+
if (gitTop(from))
|
|
224
|
+
throw new WorkspaceError(`이 폴더는 이미 Git 저장소 안이라 ${WORKSPACE_CONFIG} 를 쓰지 않습니다: ${from}`, 'manual');
|
|
225
|
+
const wanted = path.resolve(from, choice);
|
|
226
|
+
const repo = gitTop(wanted);
|
|
227
|
+
if (!repo)
|
|
228
|
+
throw new WorkspaceError(`Git 저장소가 아닙니다: ${wanted}`, 'manual');
|
|
229
|
+
const config = path.join(from, WORKSPACE_CONFIG);
|
|
230
|
+
let data = {};
|
|
231
|
+
if (statOrNull(config)) {
|
|
232
|
+
// 읽지 못한 설정은 덮어쓰지 않는다 (D23 과 같은 원칙).
|
|
233
|
+
try {
|
|
234
|
+
data = JSON.parse(fs.readFileSync(config, 'utf8'));
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
throw new WorkspaceError(`기존 ${config} 를 읽지 못해 바꾸지 않았습니다: ${errorMessage(e)}`, 'manual');
|
|
238
|
+
}
|
|
239
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
240
|
+
throw new WorkspaceError(`기존 ${config} 가 객체가 아니라 바꾸지 않았습니다.`, 'manual');
|
|
241
|
+
}
|
|
242
|
+
// 폴더 안의 저장소는 상대 경로로 적어 작업 폴더를 통째로 옮겨도 맞게 한다.
|
|
243
|
+
const relative = path.relative(from, wanted);
|
|
244
|
+
const value = !relative || relative.startsWith('..') || path.isAbsolute(relative) ? wanted : relative.replaceAll(path.sep, '/');
|
|
245
|
+
fs.writeFileSync(config, JSON.stringify({ ...data, repo: value }, null, 2) + '\n');
|
|
246
|
+
return { config, repo, value };
|
|
247
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/** 작업 폴더 → 대상 Git 저장소. MCP 서버·로컬 세션 연결·CLI가 이 한 곳에서 정한다.
|
|
2
|
+
*
|
|
3
|
+
* 사람이 AI 도구를 여는 폴더가 Git 저장소가 아닐 수 있다. 예: 한 저장소의 worktree
|
|
4
|
+
* 여러 개를 나란히 담아 둔 `robotable_hj_workspace`. 위로만 찾으면 이 폴더는
|
|
5
|
+
* "Git 저장소 아님"이 되어 연결이 끊긴다. 그래서 위로 찾기에 실패한 폴더에 한해
|
|
6
|
+
* 아래 순서로 정한다 (D30).
|
|
7
|
+
*
|
|
8
|
+
* 0. 수동 설정 — 그 폴더나 조상에 있는 `.axmap.json` 의 `{ "repo": "<경로>" }`
|
|
9
|
+
* 1. 자동 탐색 — 바로 아래 자식 폴더(깊이 1)의 Git 저장소를 공통 디렉터리로 묶어
|
|
10
|
+
* 묶음이 정확히 하나면 그 저장소
|
|
11
|
+
* 2. 모호함 — 묶음이 둘 이상이면 고르지 않고 후보를 적은 오류를 낸다
|
|
12
|
+
*
|
|
13
|
+
* 매번 디스크와 git 에게 다시 묻는다. 캐시하면 사람이 설정 파일을 쓰거나 폴더를
|
|
14
|
+
* 추가한 뒤에도 서버를 다시 켜기 전까지 옛 판단이 남는다.
|
|
15
|
+
*
|
|
16
|
+
* 읽지 못한 것과 없는 것을 구별한다. `.git` 이 있는데 git 이 읽지 못한 자식 폴더를
|
|
17
|
+
* "저장소 없음"으로 세면, 실제로는 두 저장소인데 하나로 보고 엉뚱한 장부를 잡는다.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { spawnSync } from 'node:child_process'
|
|
22
|
+
|
|
23
|
+
// bin/axmap.mjs·mcp/server.mjs 가 CLI 를 띄울 때마다 import 하므로 Node 내장만 쓴다.
|
|
24
|
+
// 그래서 session-types 의 작은 도우미도 가져오지 않는다.
|
|
25
|
+
const errorMessage = (e: unknown) => e instanceof Error ? e.message : String(e)
|
|
26
|
+
const errorCode = (e: unknown) => e instanceof Error && 'code' in e && typeof e.code === 'string' ? e.code : undefined
|
|
27
|
+
|
|
28
|
+
export const WORKSPACE_CONFIG = '.axmap.json'
|
|
29
|
+
|
|
30
|
+
export interface WorkspaceMember { name: string; top: string }
|
|
31
|
+
export interface RepoGroup { key: string; common: string; target: string; members: WorkspaceMember[] }
|
|
32
|
+
export type TargetResolution =
|
|
33
|
+
| { kind: 'git'; from: string; repo: string }
|
|
34
|
+
| { kind: 'manual'; from: string; repo: string; config: string }
|
|
35
|
+
| { kind: 'auto'; from: string; repo: string; group: RepoGroup }
|
|
36
|
+
| { kind: 'ambiguous'; from: string; groups: RepoGroup[] }
|
|
37
|
+
| { kind: 'none'; from: string }
|
|
38
|
+
|
|
39
|
+
/** 오류 문구만으로 종류를 가르지 않도록 사유를 붙인다. */
|
|
40
|
+
export class WorkspaceError extends Error {
|
|
41
|
+
constructor(message: string, readonly reason: 'ambiguous' | 'manual' | 'unreadable') { super(message) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 커밋 훅 안에서는 GIT_DIR 등이 설정돼 있다. 그대로 두면 자식 폴더마다 같은 저장소로
|
|
45
|
+
// 답해서 서로 다른 저장소가 하나로 묶인다. bin/axmap.mjs 의 GIT_ENV_KEYS 와 같은 목록이다.
|
|
46
|
+
const GIT_ENV_KEYS = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE',
|
|
47
|
+
'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_PREFIX']
|
|
48
|
+
function gitEnv(): NodeJS.ProcessEnv {
|
|
49
|
+
// "not a git repository" 문구로 판정하므로 번역된 메시지가 나오지 않게 고정한다.
|
|
50
|
+
const env: NodeJS.ProcessEnv = { ...process.env, LC_ALL: 'C', LANGUAGE: 'C' }
|
|
51
|
+
for (const key of GIT_ENV_KEYS) delete env[key]
|
|
52
|
+
return env
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type GitAnswer = { ok: true; lines: string[] } | { ok: false; notRepo: boolean; detail: string }
|
|
56
|
+
function askGit(cwd: string, args: string[]): GitAnswer {
|
|
57
|
+
const r = spawnSync('git', args, { cwd, env: gitEnv(), encoding: 'utf8', windowsHide: true })
|
|
58
|
+
if (r.error) return { ok: false, notRepo: false, detail: r.error.message }
|
|
59
|
+
if (r.status === 0) return { ok: true, lines: (r.stdout ?? '').split(/\r?\n/).map(s => s.trim()).filter(Boolean) }
|
|
60
|
+
const detail = (r.stderr ?? '').trim() || `git 종료 코드 ${r.status}`
|
|
61
|
+
return { ok: false, notRepo: /not a git repository/i.test(detail), detail }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const real = (p: string) => fs.realpathSync(p)
|
|
65
|
+
/** 공통 디렉터리 비교 키. bridge 의 repoKey 와 같은 규칙이다 (Windows 는 대소문자 무시). */
|
|
66
|
+
export const commonKey = (common: string) => process.platform === 'win32' ? common.toLowerCase() : common
|
|
67
|
+
|
|
68
|
+
function statOrNull(p: string): fs.Stats | null {
|
|
69
|
+
try { return fs.statSync(p) } catch (e) {
|
|
70
|
+
if (['ENOENT', 'ENOTDIR'].includes(errorCode(e) || '')) return null
|
|
71
|
+
throw e
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 폴더가 속한 Git 저장소의 최상위. 저장소 밖이면 null, 판정하지 못하면 예외. */
|
|
76
|
+
export function gitTop(dir: string): string | null {
|
|
77
|
+
const stat = statOrNull(dir)
|
|
78
|
+
if (!stat || !stat.isDirectory()) return null
|
|
79
|
+
const r = askGit(dir, ['rev-parse', '--show-toplevel'])
|
|
80
|
+
if (r.ok) {
|
|
81
|
+
if (!r.lines[0]) throw new WorkspaceError(`Git 최상위 폴더를 읽지 못했습니다: ${dir}`, 'unreadable')
|
|
82
|
+
return real(r.lines[0])
|
|
83
|
+
}
|
|
84
|
+
if (r.notRepo) return null
|
|
85
|
+
throw new WorkspaceError(`Git 저장소인지 확인하지 못했습니다: ${dir}\n ${r.detail}`, 'unreadable')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 그 폴더나 조상에 있는 `.axmap.json`. 없으면 null. 읽지 못하면 예외. */
|
|
89
|
+
export function findWorkspaceConfig(dir: string): string | null {
|
|
90
|
+
for (let at = path.resolve(dir); ; at = path.dirname(at)) {
|
|
91
|
+
const candidate = path.join(at, WORKSPACE_CONFIG)
|
|
92
|
+
const stat = statOrNull(candidate)
|
|
93
|
+
if (stat) return candidate
|
|
94
|
+
if (path.dirname(at) === at) return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function manualTarget(config: string): string {
|
|
99
|
+
let data: unknown
|
|
100
|
+
try { data = JSON.parse(fs.readFileSync(config, 'utf8')) } catch (e) {
|
|
101
|
+
throw new WorkspaceError(`작업 폴더 설정을 읽지 못했습니다: ${config}\n ${errorMessage(e)}`, 'manual')
|
|
102
|
+
}
|
|
103
|
+
const repo = (data as { repo?: unknown } | null)?.repo
|
|
104
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) || typeof repo !== 'string' || !repo.trim()) {
|
|
105
|
+
throw new WorkspaceError(`작업 폴더 설정에 "repo" 경로가 없습니다: ${config}\n 예: { "repo": "폴더이름" }`, 'manual')
|
|
106
|
+
}
|
|
107
|
+
const wanted = path.resolve(path.dirname(config), repo)
|
|
108
|
+
// 설정이 틀렸을 때 자동 탐색으로 넘어가지 않는다. 사람이 고른 것을 몰래 바꾸면
|
|
109
|
+
// 다른 저장소의 장부에 선점이 남는다.
|
|
110
|
+
const top = gitTop(wanted)
|
|
111
|
+
if (!top) throw new WorkspaceError(`작업 폴더 설정의 저장소가 Git 저장소가 아닙니다: ${wanted}\n (설정 파일: ${config})`, 'manual')
|
|
112
|
+
return top
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scanChildren(dir: string): { groups: RepoGroup[]; unreadable: string[] } {
|
|
116
|
+
let entries: fs.Dirent[]
|
|
117
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch (e) {
|
|
118
|
+
throw new WorkspaceError(`작업 폴더의 하위 폴더를 읽지 못했습니다: ${dir}\n ${errorMessage(e)}`, 'unreadable')
|
|
119
|
+
}
|
|
120
|
+
const groups = new Map<string, RepoGroup>(), unreadable: string[] = []
|
|
121
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
122
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
123
|
+
const child = path.join(dir, entry.name)
|
|
124
|
+
try {
|
|
125
|
+
// worktree 폴더가 심볼릭 링크·junction 일 수 있어 링크는 따라가서 본다.
|
|
126
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && !!statOrNull(child)?.isDirectory())
|
|
127
|
+
if (!isDir) continue
|
|
128
|
+
// `.git` 이 없는 폴더는 저장소의 최상위가 아니다. git 을 띄우지 않고 넘어간다.
|
|
129
|
+
if (fs.lstatSync(path.join(child, '.git'), { throwIfNoEntry: false }) === undefined) continue
|
|
130
|
+
} catch (e) { unreadable.push(`${entry.name} (${errorMessage(e)})`); continue }
|
|
131
|
+
const r = askGit(child, ['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir'])
|
|
132
|
+
if (!r.ok || r.lines.length < 2) { unreadable.push(`${entry.name} (${r.ok ? 'git 응답이 비어 있음' : r.detail})`); continue }
|
|
133
|
+
let top: string, common: string
|
|
134
|
+
try { top = real(r.lines[0]); common = real(r.lines[1]) } catch (e) { unreadable.push(`${entry.name} (${errorMessage(e)})`); continue }
|
|
135
|
+
const key = commonKey(common)
|
|
136
|
+
const group = groups.get(key) ?? { key, common, target: '', members: [] }
|
|
137
|
+
group.members.push({ name: entry.name, top })
|
|
138
|
+
groups.set(key, group)
|
|
139
|
+
}
|
|
140
|
+
for (const group of groups.values()) {
|
|
141
|
+
// 메인 체크아웃(공통 디렉터리 `.git` 의 부모)을 겨눈다. 장부도 거기 있다.
|
|
142
|
+
// bare 저장소처럼 그 부모가 체크아웃이 아니면 첫 번째 worktree 를 쓴다.
|
|
143
|
+
const parent = path.dirname(group.common)
|
|
144
|
+
group.target = path.basename(group.common).toLowerCase() === '.git' && statOrNull(parent)?.isDirectory()
|
|
145
|
+
? real(parent) : group.members[0].top
|
|
146
|
+
}
|
|
147
|
+
return { groups: [...groups.values()], unreadable }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 무엇을 왜 고르는지까지 돌려준다. 모호함은 예외가 아니라 결과다 (`workspace show` 용). */
|
|
151
|
+
export function inspectTarget(dir: string): TargetResolution {
|
|
152
|
+
const from = path.resolve(dir)
|
|
153
|
+
const top = gitTop(from)
|
|
154
|
+
if (top) return { kind: 'git', from, repo: top }
|
|
155
|
+
if (!statOrNull(from)?.isDirectory()) return { kind: 'none', from }
|
|
156
|
+
const config = findWorkspaceConfig(from)
|
|
157
|
+
if (config) return { kind: 'manual', from, repo: manualTarget(config), config }
|
|
158
|
+
const { groups, unreadable } = scanChildren(from)
|
|
159
|
+
if (unreadable.length) {
|
|
160
|
+
throw new WorkspaceError([
|
|
161
|
+
`작업 폴더 ${from} 아래에서 확인하지 못한 Git 폴더가 있어 대상 저장소를 정하지 않았습니다.`,
|
|
162
|
+
...unreadable.map(s => ` - ${s}`),
|
|
163
|
+
`저장소를 직접 고르려면 이 폴더에서 \`axmap workspace use <폴더>\` 를 실행하세요.`,
|
|
164
|
+
].join('\n'), 'unreadable')
|
|
165
|
+
}
|
|
166
|
+
if (groups.length === 1) return { kind: 'auto', from, repo: groups[0].target, group: groups[0] }
|
|
167
|
+
if (groups.length > 1) return { kind: 'ambiguous', from, groups }
|
|
168
|
+
return { kind: 'none', from }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function ambiguityMessage(r: Extract<TargetResolution, { kind: 'ambiguous' }>): string {
|
|
172
|
+
return [
|
|
173
|
+
`작업 폴더 ${r.from} 아래에 서로 다른 Git 저장소가 ${r.groups.length}개 있어 하나로 정하지 않았습니다.`,
|
|
174
|
+
...r.groups.map(g => ` - ${g.target} (폴더: ${g.members.map(m => m.name).join(', ')})`),
|
|
175
|
+
'고르는 방법 (둘 중 하나):',
|
|
176
|
+
' 1. 도구를 부를 때 cwd 에 작업할 저장소 폴더의 절대 경로를 넘긴다.',
|
|
177
|
+
` 2. 이 폴더에 ${WORKSPACE_CONFIG} 을 둔다: { "repo": "${r.groups[0].members[0].name}" }`,
|
|
178
|
+
` 터미널에서는: axmap workspace use ${r.groups[0].members[0].name}`,
|
|
179
|
+
].join('\n')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 대상 저장소 경로. 저장소가 없으면 null, 모호하거나 판정하지 못하면 예외. */
|
|
183
|
+
export function resolveTarget(dir: string): string | null {
|
|
184
|
+
const r = inspectTarget(dir)
|
|
185
|
+
if (r.kind === 'ambiguous') throw new WorkspaceError(ambiguityMessage(r), 'ambiguous')
|
|
186
|
+
return r.kind === 'none' ? null : r.repo
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function describeTarget(r: TargetResolution): string {
|
|
190
|
+
switch (r.kind) {
|
|
191
|
+
case 'git': return `대상 저장소: ${r.repo}\n이유: 작업 폴더가 Git 저장소 안에 있습니다.`
|
|
192
|
+
case 'manual': return `대상 저장소: ${r.repo}\n이유: 수동 설정 ${r.config}`
|
|
193
|
+
case 'auto': return `대상 저장소: ${r.repo}\n이유: 자동 탐색 — 하위 폴더가 모두 같은 저장소입니다 (${r.group.members.map(m => m.name).join(', ')}).`
|
|
194
|
+
case 'ambiguous': return ambiguityMessage(r)
|
|
195
|
+
case 'none': return `대상 저장소 없음: ${r.from}\n이유: 이 폴더와 바로 아래 폴더에서 Git 저장소를 찾지 못했습니다.`
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** `axmap workspace use` — 사람이 고른 저장소를 작업 폴더의 `.axmap.json` 에 적는다. */
|
|
200
|
+
export function writeWorkspaceChoice(dir: string, choice: string): { config: string; repo: string; value: string } {
|
|
201
|
+
const from = path.resolve(dir)
|
|
202
|
+
if (gitTop(from)) throw new WorkspaceError(`이 폴더는 이미 Git 저장소 안이라 ${WORKSPACE_CONFIG} 를 쓰지 않습니다: ${from}`, 'manual')
|
|
203
|
+
const wanted = path.resolve(from, choice)
|
|
204
|
+
const repo = gitTop(wanted)
|
|
205
|
+
if (!repo) throw new WorkspaceError(`Git 저장소가 아닙니다: ${wanted}`, 'manual')
|
|
206
|
+
const config = path.join(from, WORKSPACE_CONFIG)
|
|
207
|
+
let data: Record<string, unknown> = {}
|
|
208
|
+
if (statOrNull(config)) {
|
|
209
|
+
// 읽지 못한 설정은 덮어쓰지 않는다 (D23 과 같은 원칙).
|
|
210
|
+
try { data = JSON.parse(fs.readFileSync(config, 'utf8')) } catch (e) {
|
|
211
|
+
throw new WorkspaceError(`기존 ${config} 를 읽지 못해 바꾸지 않았습니다: ${errorMessage(e)}`, 'manual')
|
|
212
|
+
}
|
|
213
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) throw new WorkspaceError(`기존 ${config} 가 객체가 아니라 바꾸지 않았습니다.`, 'manual')
|
|
214
|
+
}
|
|
215
|
+
// 폴더 안의 저장소는 상대 경로로 적어 작업 폴더를 통째로 옮겨도 맞게 한다.
|
|
216
|
+
const relative = path.relative(from, wanted)
|
|
217
|
+
const value = !relative || relative.startsWith('..') || path.isAbsolute(relative) ? wanted : relative.replaceAll(path.sep, '/')
|
|
218
|
+
fs.writeFileSync(config, JSON.stringify({ ...data, repo: value }, null, 2) + '\n')
|
|
219
|
+
return { config, repo, value }
|
|
220
|
+
}
|
package/tools/bridge.mjs
CHANGED
|
@@ -34,12 +34,14 @@ try {
|
|
|
34
34
|
case 'connect':
|
|
35
35
|
case 'serve': {
|
|
36
36
|
const provider = args[1]
|
|
37
|
-
if (!['codex', 'agy'].includes(provider)) throw new Error('axmap bridge serve codex|agy를 사용하세요.')
|
|
37
|
+
if (!['codex', 'agy', 'claude'].includes(provider)) throw new Error('axmap bridge serve codex|agy|claude를 사용하세요.')
|
|
38
38
|
const binary = resolveBin(provider)
|
|
39
39
|
if (!binary) throw new Error(`${provider} 실행 파일을 찾지 못했습니다.`)
|
|
40
40
|
const [executable, ...prefix] = provider === 'codex' ? codexInvocation(binary) : [binary]
|
|
41
41
|
if (command === 'connect' && (!option('thread') || !option('socket'))) throw new Error('bridge connect codex --thread <기존 UUID> --socket <로컬 경로>가 필요합니다.')
|
|
42
42
|
const session = await startManagedSession({ provider, executable, prefix, model: option('model'), role: option('role'),
|
|
43
|
+
...(provider === 'claude' && option('permission-mode') ? { permissionMode: option('permission-mode') } : {}),
|
|
44
|
+
...(option('timeout-ms') ? { timeoutMs: Number(option('timeout-ms')) } : {}),
|
|
43
45
|
...(command === 'connect' ? { existingThreadId: option('thread'), socketPath: option('socket'), observeActive: args.includes('--observe') } : {}),
|
|
44
46
|
maxTokens: Number(option('max-tokens') || 100000), onEvent: output })
|
|
45
47
|
output({ event: 'connected', ...session, exited: undefined, close: undefined })
|
|
@@ -52,6 +54,8 @@ try {
|
|
|
52
54
|
const options = { ...(option('id') ? { id: option('id') } : {}), left: option('left'), right: option('right'), prompt: fs.readFileSync(0, 'utf8'),
|
|
53
55
|
maxTurns: Number(option('max-turns') || 4), maxTokens: Number(option('max-tokens') || 50000),
|
|
54
56
|
maxMessageChars: Number(option('max-message-chars') || 2000),
|
|
57
|
+
...(option('timeout-ms') ? { timeoutMs: Number(option('timeout-ms')) } : {}),
|
|
58
|
+
...(option('done-by') ? { doneBy: option('done-by') } : {}),
|
|
55
59
|
onEvent: event => console.error(JSON.stringify(event)) }
|
|
56
60
|
const r = args.includes('--background') ? await startBackgroundDialogue(options) : await runDialogue(options)
|
|
57
61
|
output(r); if (r.state !== 'completed') process.exitCode = 2
|
|
@@ -85,8 +89,8 @@ try {
|
|
|
85
89
|
case 'send': {
|
|
86
90
|
const waitMs = Number(option('wait-ms') || 0)
|
|
87
91
|
if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > 30000) throw new Error('wait-ms는 0~30000 정수여야 합니다.')
|
|
88
|
-
const r = dispatchRequest({ to: option('to'), prompt: fs.readFileSync(0, 'utf8'), cwd: process.cwd(), from: process.env.AXMAP_AGENT || 'axmap' })
|
|
89
|
-
const result = waitMs &&
|
|
92
|
+
const r = dispatchRequest({ to: option('to'), prompt: fs.readFileSync(0, 'utf8'), cwd: process.cwd(), from: process.env.AXMAP_AGENT || 'axmap', queueIfBusy: !args.includes('--no-queue') })
|
|
93
|
+
const result = waitMs && ['queued', 'submitted'].includes(r.state) ? await waitForRequest(r.id, process.cwd(), waitMs) : r
|
|
90
94
|
output(result)
|
|
91
95
|
if (result.state === 'unknown') process.exitCode = 2
|
|
92
96
|
break
|
|
@@ -95,12 +99,15 @@ try {
|
|
|
95
99
|
case undefined:
|
|
96
100
|
case '--help':
|
|
97
101
|
console.log(`axMap 로컬 세션 연결 — 관리형 Codex/agy와 제한된 자동 대화
|
|
98
|
-
axmap bridge serve codex|agy [--model 이름] [--role 역할] [--max-tokens 100000]
|
|
102
|
+
axmap bridge serve codex|agy|claude [--model 이름] [--role 역할] [--max-tokens 100000]
|
|
103
|
+
claude 는 --permission-mode default|plan|acceptEdits (기본 default: 읽기만, 고치기는 거절)
|
|
104
|
+
--timeout-ms 한 실행의 시간 한도(기본 120000). 넘기면 결과가 불명확하므로 세션을 닫고 재시도하지 않습니다.
|
|
99
105
|
명시적으로 새 관리형 CLI를 시작합니다. 기존 창에 임의로 붙지 않습니다.
|
|
100
106
|
axmap bridge connect codex --thread <기존 UUID> --socket <로컬 socket> [--role 역할] [--observe]
|
|
101
107
|
실행 중인 공식 Codex daemon의 기존 idle 대화에 연결합니다. 새 모델/대화를 만들지 않습니다.
|
|
102
108
|
--observe는 active 대화에도 관측만 연결하며 idle 전환 전에는 새 요청을 거절합니다.
|
|
103
|
-
axmap bridge chat --left <UUID> --right <UUID> [--max-turns 4] [--max-tokens 50000]
|
|
109
|
+
axmap bridge chat --left <UUID> --right <UUID> [--max-turns 4] [--max-tokens 50000] [--done-by <UUID>] [--timeout-ms 120000]
|
|
110
|
+
--done-by 를 주면 그 세션의 done:true 만 대화를 끝냅니다(다른 쪽 done 은 기록만).
|
|
104
111
|
stdin 작업을 자동 중계합니다. 기본 메시지 한도 2000자, 대기 중 모델 호출 없음.
|
|
105
112
|
axmap bridge answer --to <UUID> --request <ID> --decision accept|decline|cancel
|
|
106
113
|
Codex가 요청한 이번 실행의 승인을 사용자가 결정합니다.
|
|
@@ -111,9 +118,10 @@ axmap bridge install --experimental --executable <agy 절대 경로> 연결
|
|
|
111
118
|
axmap bridge uninstall 추가한 연결 훅 해제
|
|
112
119
|
axmap bridge sessions 이 repo에 연결된 세션
|
|
113
120
|
axmap bridge notice --to <UUID> stdin 안내를 기존 Codex 문맥에 기록 (모델 호출 없음)
|
|
114
|
-
axmap bridge attach --pid <PID> 이미 실행한 Windows agy 콘솔을 현재 repo에 연결
|
|
121
|
+
axmap bridge attach --pid <PID> 이미 실행한 Windows agy·Claude 콘솔을 현재 repo에 연결
|
|
115
122
|
axmap bridge status [--json] 로컬 연결·작업·승인 대기·종료 상태 조회 (모델 호출 없음)
|
|
116
123
|
axmap bridge send --to <세션 UUID> [--wait-ms 30000] 본문은 stdin. 전달 후 지정 시간까지 로컬에서 회신 대기
|
|
124
|
+
관리형 stdio와 Windows 일반 Claude 콘솔은 작업 중 FIFO 접수 후 실행합니다. --no-queue로 거절 모드.
|
|
117
125
|
send는 실행용 저수준 명령입니다. 일반 협업 알림에는 message를 사용하세요.
|
|
118
126
|
axmap bridge message --to <UUID> [--topic <갱신 주제>] [--key <중복 방지 키>] [--ttl-seconds 3600]
|
|
119
127
|
stdin 일반 메시지를 큐에 저장합니다. 모델 호출·새 턴 없음. 같은 발신자의 대기 중 topic만 최신으로 합칩니다.
|
package/tools/build.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/** TS owns migrated modules; checked-in JS keeps Node 20 and
|
|
2
|
+
/** TS owns migrated modules; checked-in JS keeps Node 20 source checkouts and npm installs usable without a build step. */
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
package/tools/bus.mjs
CHANGED
|
@@ -1002,7 +1002,7 @@ function publish(id) {
|
|
|
1002
1002
|
|
|
1003
1003
|
/**
|
|
1004
1004
|
* 이 파일을 지금 폴더에서 부르는 명령. **문자열로 적지 않고 계산한다.**
|
|
1005
|
-
*
|
|
1005
|
+
* 이 파일은 도구가 설치된 자리(npm 설치본·axMap 저장소)에 있고, 그 자리는 기계마다 다르다.
|
|
1006
1006
|
*/
|
|
1007
1007
|
function selfCmd() {
|
|
1008
1008
|
const self = fileURLToPath(import.meta.url)
|
package/tools/run.mjs
CHANGED
|
@@ -4,21 +4,28 @@ import path from 'node:path'
|
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
5
|
import { startTerminal, sendTerminal, installTerminalHooks, installTerminalRuntime } from '../src/terminal-bridge.mjs'
|
|
6
6
|
import { registerExternalHook } from '../src/external-console.mjs'
|
|
7
|
+
import { RPC_NOT_DELIVERED_EXIT } from '../src/session-types.mjs'
|
|
7
8
|
|
|
8
9
|
const [command, ...args] = process.argv.slice(2)
|
|
9
10
|
try {
|
|
10
11
|
if (command === 'install-runtime') {
|
|
11
12
|
console.log(JSON.stringify(installTerminalRuntime(args[0])))
|
|
12
13
|
} else if (command === 'install-hooks') {
|
|
13
|
-
console.log(JSON.stringify({ hooks: [
|
|
14
|
+
console.log(JSON.stringify({ hooks: (args[0] ? [args[0]] : ['agy', 'codex', 'claude']).map(provider => installTerminalHooks(undefined, provider)) }))
|
|
14
15
|
} else if (command === 'rpc') {
|
|
15
|
-
console.log(JSON.stringify(await sendTerminal(args[0], JSON.parse(fs.readFileSync(0, 'utf8')))))
|
|
16
|
+
try { console.log(JSON.stringify(await sendTerminal(args[0], JSON.parse(fs.readFileSync(0, 'utf8'))))) }
|
|
17
|
+
catch (e) {
|
|
18
|
+
if (e?.notDelivered !== true) throw e
|
|
19
|
+
// 연결 전에 실패해 아무것도 보내지 않았다. 호출한 쪽(bridge.terminalRPC)이
|
|
20
|
+
// 문구가 아니라 종료 코드로 "확실히 미전달"을 알아보게 한다.
|
|
21
|
+
console.error(e.message); process.exitCode = RPC_NOT_DELIVERED_EXIT
|
|
22
|
+
}
|
|
16
23
|
} else if (command === 'hook') {
|
|
17
24
|
const input = JSON.parse(fs.readFileSync(0, 'utf8'))
|
|
18
25
|
if (process.env.AXMAP_TERMINAL_SESSION && process.env.AXMAP_TERMINAL_TOKEN) {
|
|
19
26
|
await sendTerminal(process.env.AXMAP_TERMINAL_SESSION, { action: 'hook', event: args[0], input }, process.env.AXMAP_TERMINAL_TOKEN)
|
|
20
|
-
} else if (process.platform === 'win32' && input.conversationId) {
|
|
21
|
-
try { registerExternalHook(input, args[0]) }
|
|
27
|
+
} else if (process.platform === 'win32' && (input.conversationId || input.session_id)) {
|
|
28
|
+
try { registerExternalHook(input, args[0], args.includes('--provider') ? args[args.indexOf('--provider') + 1] : undefined) }
|
|
22
29
|
catch (e) { console.error(`axMap 독립 콘솔: ${e.message}`) }
|
|
23
30
|
}
|
|
24
31
|
console.log('{}')
|
package/tools/setup.mjs
CHANGED
|
@@ -257,7 +257,7 @@ if (!has('--no-register')) {
|
|
|
257
257
|
else {
|
|
258
258
|
const hookRunner = path.join(DEST, 'tools', 'run.mjs')
|
|
259
259
|
const r = spawnSync(process.execPath, [hookRunner, 'install-hooks'], { encoding: 'utf8', windowsHide: true })
|
|
260
|
-
if (r.status === 0) ok('세션 연결', 'Windows에서 직접 여는 agy와 axmap run CLI가 자동 연결됩니다')
|
|
260
|
+
if (r.status === 0) ok('세션 연결', 'Windows에서 직접 여는 Claude Code·agy와 axmap run CLI가 자동 연결됩니다')
|
|
261
261
|
else no('세션 연결', r.stderr?.trim() || '연결 훅 등록 실패')
|
|
262
262
|
}
|
|
263
263
|
}
|