@asc-agent/runtime 0.3.1 → 0.4.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/README.md +1 -1
- package/dist/adapters/claude-code/install.d.ts +7 -0
- package/dist/adapters/claude-code/install.js +114 -38
- package/dist/adapters/claude-code/probe.js +6 -1
- package/dist/adapters/claude-code/session-start.d.ts +20 -0
- package/dist/adapters/claude-code/session-start.js +111 -0
- package/dist/adapters/jam/adapter.d.ts +46 -9
- package/dist/adapters/jam/adapter.js +88 -22
- package/dist/adapters/jam/mcp-client.js +5 -1
- package/dist/adapters/jam/setup.d.ts +62 -0
- package/dist/adapters/jam/setup.js +85 -0
- package/dist/adapters/service/launchd.d.ts +12 -0
- package/dist/adapters/service/launchd.js +80 -0
- package/dist/adapters/service/schtasks.d.ts +16 -0
- package/dist/adapters/service/schtasks.js +67 -0
- package/dist/adapters/service/systemd-user.d.ts +15 -0
- package/dist/adapters/service/systemd-user.js +95 -0
- package/dist/cli/asc.js +886 -147
- package/dist/composition/registry.js +11 -3
- package/dist/composition/runtime.d.ts +33 -0
- package/dist/composition/runtime.js +74 -0
- package/dist/core/attach/setup-plan.d.ts +73 -1
- package/dist/core/attach/setup-plan.js +56 -3
- package/dist/core/distribution/external-command.d.ts +22 -0
- package/dist/core/distribution/external-command.js +79 -0
- package/dist/core/distribution/persistent-runtime.d.ts +73 -0
- package/dist/core/distribution/persistent-runtime.js +49 -0
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/operator/progress.js +3 -1
- package/dist/core/runtime/background.d.ts +104 -0
- package/dist/core/runtime/background.js +225 -0
- package/dist/core/runtime/front.d.ts +48 -0
- package/dist/core/runtime/front.js +34 -0
- package/dist/core/runtime/session.js +6 -1
- package/dist/core/runtime/workspaces.d.ts +39 -0
- package/dist/core/runtime/workspaces.js +64 -0
- package/dist/core/workspace/resolve.d.ts +36 -0
- package/dist/core/workspace/resolve.js +124 -3
- package/dist/ports/adapter.d.ts +12 -0
- package/dist/schemas/profile.d.ts +6 -6
- package/package.json +1 -1
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 1. explicit root 사람이 말한 것이 이긴다
|
|
11
11
|
// 2. workspace index 이 기계에 등록된 locator (경로가 바뀌어도 따라온다)
|
|
12
|
-
// 3.
|
|
13
|
-
// 4.
|
|
12
|
+
// 3. linked worktree 같은 Git repository의 다른 checkout이 등록돼 있다 (C-11 §1.3)
|
|
13
|
+
// 4. project-adopted 저장소 안의 `.asc/` — 팀이 채택한 경우 (legacy 개인 사용 포함)
|
|
14
|
+
// 5. UNRESOLVED 모르면 모른다고 한다
|
|
15
|
+
//
|
|
16
|
+
// **3번은 index가 빗나갔을 때만 돈다.** 등록된 locator로 풀리는 정상 경로는 예전 그대로
|
|
17
|
+
// 파일 접근 하나이며, Git을 부르지 않는다 — guard hook이 매 Bash 호출마다 지나는 길이다.
|
|
14
18
|
//
|
|
15
19
|
// **3번의 탐색에는 경계가 있다.** 예전에는 파일시스템 루트까지 올라갔는데, 사용자 홈에
|
|
16
20
|
// `~/.asc` 가 생기는 순간 홈 아래 아무 저장소나 그 뿌리로 오인 매칭된다. 그래서 홈을
|
|
@@ -47,11 +51,112 @@ export async function resolveWorkspace(input) {
|
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
}
|
|
54
|
+
// index가 빗나갔다. 여기서 처음으로 Git에게 **구조**를 묻는다 — remote가 같다는 이유로
|
|
55
|
+
// 이어붙이지 않는다 (C-11 불변식 ③). 같은 linked repository라는 것은 로컬에서 증명된다.
|
|
56
|
+
if (input.index && input.worktrees) {
|
|
57
|
+
const linked = await resolveLinkedWorktree(input.cwd, input.index, input.worktrees, exists);
|
|
58
|
+
if (linked)
|
|
59
|
+
return linked;
|
|
60
|
+
}
|
|
50
61
|
const local = await findProjectLocal(input.cwd, input.stopAt, exists);
|
|
51
62
|
if (local)
|
|
52
63
|
return { kind: 'PROJECT_LOCAL', root: join(local, ASC_DIR), projectRoot: local };
|
|
53
64
|
return { kind: 'UNRESOLVED', detail: '이 경로에 붙은 ASC workspace가 없다' };
|
|
54
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* 이 checkout이 **이미 등록된 checkout과 같은 Git repository**인가.
|
|
68
|
+
*
|
|
69
|
+
* `git worktree list` 는 그 저장소의 checkout만 든다. 그래서 remote가 같은 **독립 clone**은
|
|
70
|
+
* 서로의 경로를 담지 않고, 여기서 절대 이어붙지 않는다 — 그것이 이 판정이 alias 대조가
|
|
71
|
+
* 아니라 worktree 목록을 쓰는 이유다 (C-11 불변식 ③: alias 일치는 recover candidate일 뿐).
|
|
72
|
+
*
|
|
73
|
+
* 후보 workspace가 둘 이상이면 **고르지 않는다.** 고르면 사람이 그 선택을 보지 못한다.
|
|
74
|
+
*/
|
|
75
|
+
async function resolveLinkedWorktree(cwd, index, probe, exists) {
|
|
76
|
+
const paths = await probe(cwd);
|
|
77
|
+
if (!paths || paths.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
const normalized = paths.map(normalizeLocator);
|
|
80
|
+
const here = currentWorktree(normalized, normalizeLocator(cwd));
|
|
81
|
+
if (!here)
|
|
82
|
+
return null;
|
|
83
|
+
const hits = new Map();
|
|
84
|
+
for (const path of normalized) {
|
|
85
|
+
if (path === here)
|
|
86
|
+
continue;
|
|
87
|
+
const found = lookupLocator(index, path);
|
|
88
|
+
if (found)
|
|
89
|
+
hits.set(found.workspaceId, { root: found.root, via: found.locator });
|
|
90
|
+
}
|
|
91
|
+
if (hits.size === 0)
|
|
92
|
+
return null;
|
|
93
|
+
if (hits.size > 1) {
|
|
94
|
+
return {
|
|
95
|
+
kind: 'UNRESOLVED',
|
|
96
|
+
detail: `이 저장소의 다른 checkout들이 서로 다른 workspace에 등록돼 있다 (${[...hits.keys()].join(', ')}) — 고르지 않는다`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const [[workspaceId, hit]] = [...hits.entries()];
|
|
100
|
+
// 등록은 돼 있는데 뿌리가 없으면 넘어가지 않는다 — REGISTERED와 같은 태도다.
|
|
101
|
+
if (!(await exists(hit.root))) {
|
|
102
|
+
return {
|
|
103
|
+
kind: 'UNRESOLVED',
|
|
104
|
+
detail: `${workspaceId} 가 등록돼 있으나 runtime(${hit.root})이 없다 — 옮겼거나 지워졌다`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
kind: 'LINKED_WORKTREE',
|
|
109
|
+
root: hit.root,
|
|
110
|
+
workspaceId,
|
|
111
|
+
locator: here,
|
|
112
|
+
via: hit.via,
|
|
113
|
+
// 목록의 첫 항목이 main worktree다 (git worktree list 의 계약).
|
|
114
|
+
kindOfLocator: here === normalized[0] ? 'checkout' : 'worktree',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 목록 중 지금 서 있는 checkout. cwd는 그 아래 하위 디렉터리일 수 있으므로 **가장 긴
|
|
119
|
+
* 접두어**를 고른다. 경계는 구분자에서만 인정한다 — `/a/repo` 가 `/a/repo-2` 를 삼키면 안 된다.
|
|
120
|
+
*/
|
|
121
|
+
function currentWorktree(paths, cwd) {
|
|
122
|
+
let best = null;
|
|
123
|
+
for (const path of paths) {
|
|
124
|
+
if (cwd !== path && !cwd.startsWith(`${path}/`))
|
|
125
|
+
continue;
|
|
126
|
+
if (best === null || path.length > best.length)
|
|
127
|
+
best = path;
|
|
128
|
+
}
|
|
129
|
+
return best;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 기본 통로 — `git worktree list --porcelain`. 첫 `worktree` 항목이 main checkout이다.
|
|
133
|
+
*
|
|
134
|
+
* git이 없거나 저장소가 아니면 `null`을 돌려준다. 빈 배열이 아니다 — "checkout이 하나도
|
|
135
|
+
* 없다"와 "물어보지 못했다"는 다른 사실이고, 후자를 전자로 적으면 조용한 오판이 된다.
|
|
136
|
+
*/
|
|
137
|
+
export const gitWorktrees = async (cwd) => {
|
|
138
|
+
const { execFile } = await import('node:child_process');
|
|
139
|
+
const { promisify } = await import('node:util');
|
|
140
|
+
try {
|
|
141
|
+
const { stdout } = await promisify(execFile)('git', ['worktree', 'list', '--porcelain'], {
|
|
142
|
+
cwd,
|
|
143
|
+
// 목록은 짧다. 오래 걸리면 그것 자체가 "물어보지 못했다"이다 — 명령을 세우지 않는다.
|
|
144
|
+
timeout: 5_000,
|
|
145
|
+
});
|
|
146
|
+
return parseWorktreeList(stdout);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/** porcelain 출력에서 checkout 경로만. 순서를 보존한다 — 첫 줄이 main worktree다. */
|
|
153
|
+
export function parseWorktreeList(stdout) {
|
|
154
|
+
return stdout
|
|
155
|
+
.split(/\r?\n/)
|
|
156
|
+
.filter((line) => line.startsWith('worktree '))
|
|
157
|
+
.map((line) => line.slice('worktree '.length).trim())
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
}
|
|
55
160
|
/**
|
|
56
161
|
* 저장소 안의 `.asc/` 를 위로 올라가며 찾되 **경계를 지킨다**.
|
|
57
162
|
*
|
|
@@ -66,7 +171,13 @@ async function findProjectLocal(start, stopAt, exists) {
|
|
|
66
171
|
// 정지선 자체는 보지 않는다 — 홈의 `.asc` 는 user runtime이지 프로젝트 상태가 아니다
|
|
67
172
|
if (boundary && normalized === boundary)
|
|
68
173
|
return null;
|
|
69
|
-
|
|
174
|
+
const candidate = join(dir, ASC_DIR);
|
|
175
|
+
// 정지선이 홈이어도 **다른 홈**의 `.asc` 는 걸러지지 않는다. Windows는 temp
|
|
176
|
+
// 디렉터리가 사용자 프로필 아래라, temp의 프로젝트에서 위로 걷다 실사용자
|
|
177
|
+
// `~/.asc` 를 프로젝트 상태로 오인했다 (실 프로젝트 실측 — setup status가
|
|
178
|
+
// UNATTACHED 대신 BROKEN을 답한 원인). user runtime은 내용으로 알아본다:
|
|
179
|
+
// workspaces/·profiles/·runtime.json 은 홈에만 생긴다.
|
|
180
|
+
if ((await exists(candidate)) && !(await looksLikeUserRuntime(candidate, exists)))
|
|
70
181
|
return dir;
|
|
71
182
|
const parent = dirname(dir);
|
|
72
183
|
if (parent === dir)
|
|
@@ -74,6 +185,14 @@ async function findProjectLocal(start, stopAt, exists) {
|
|
|
74
185
|
dir = parent;
|
|
75
186
|
}
|
|
76
187
|
}
|
|
188
|
+
/** `~/.asc` 꼴인가 — 프로젝트 부착이 아니라 이 기계의 user runtime 홈인가. */
|
|
189
|
+
async function looksLikeUserRuntime(ascDir, exists) {
|
|
190
|
+
for (const marker of ['workspaces', 'profiles', 'runtime.json']) {
|
|
191
|
+
if (await exists(join(ascDir, marker)))
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
77
196
|
/** 사람이 읽는 한 줄. 왜 그 뿌리인지가 함께 와야 사람이 틀린 결합을 알아챈다. */
|
|
78
197
|
export function resolutionLine(resolution) {
|
|
79
198
|
switch (resolution.kind) {
|
|
@@ -81,6 +200,8 @@ export function resolutionLine(resolution) {
|
|
|
81
200
|
return `runtime: ${resolution.root} (given with --root)`;
|
|
82
201
|
case 'REGISTERED':
|
|
83
202
|
return `runtime: ${resolution.root} (workspace ${resolution.workspaceId} · ${resolution.locator})`;
|
|
203
|
+
case 'LINKED_WORKTREE':
|
|
204
|
+
return `runtime: ${resolution.root} (workspace ${resolution.workspaceId} · same git repository as ${resolution.via})`;
|
|
84
205
|
case 'PROJECT_LOCAL':
|
|
85
206
|
return `runtime: ${resolution.root} (.asc inside the repository — team-adopted, or personal state not yet migrated)`;
|
|
86
207
|
case 'UNRESOLVED':
|
package/dist/ports/adapter.d.ts
CHANGED
|
@@ -5,6 +5,18 @@ export type DiscoveryContext = {
|
|
|
5
5
|
projectRoot: string;
|
|
6
6
|
/** 환경변수. 자격 **존재 여부** 판단에만 쓰고 값을 실어 나르지 않는다. */
|
|
7
7
|
env?: NodeJS.ProcessEnv;
|
|
8
|
+
/**
|
|
9
|
+
* Profile 이 이 adapter 에 대해 선언한 자원들 (C-09 §3.1).
|
|
10
|
+
*
|
|
11
|
+
* **추측의 반대다.** adapter 가 지역 흔적에서 아무것도 못 찾아도, 사람이 Profile 에
|
|
12
|
+
* 적어 둔 것은 이미 내려진 결정이다 — 그것을 못 본 척하면 "선언은 있는데 아무 일도
|
|
13
|
+
* 일어나지 않는" 상태가 된다. adapter 는 이 값을 후보로 올릴 수 있고, 실제로 되는지는
|
|
14
|
+
* 여전히 probe 가 정한다.
|
|
15
|
+
*/
|
|
16
|
+
declared?: readonly {
|
|
17
|
+
adapterId: string;
|
|
18
|
+
resource: string;
|
|
19
|
+
}[];
|
|
8
20
|
};
|
|
9
21
|
export type ProbeResult = {
|
|
10
22
|
state: ProbeState;
|
|
@@ -212,20 +212,20 @@ export declare const ProjectProfile: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, un
|
|
|
212
212
|
resource: z.ZodString;
|
|
213
213
|
}, "strip", z.ZodTypeAny, {
|
|
214
214
|
role: string;
|
|
215
|
-
resource: string;
|
|
216
215
|
adapter: string;
|
|
216
|
+
resource: string;
|
|
217
217
|
}, {
|
|
218
218
|
role: string;
|
|
219
|
-
resource: string;
|
|
220
219
|
adapter: string;
|
|
220
|
+
resource: string;
|
|
221
221
|
}>, "many">>, {
|
|
222
222
|
role: string;
|
|
223
|
-
resource: string;
|
|
224
223
|
adapter: string;
|
|
224
|
+
resource: string;
|
|
225
225
|
}[] | undefined, {
|
|
226
226
|
role: string;
|
|
227
|
-
resource: string;
|
|
228
227
|
adapter: string;
|
|
228
|
+
resource: string;
|
|
229
229
|
}[] | undefined>;
|
|
230
230
|
/** 프로젝트 용어를 Core 개념에 붙인다. 사람이 읽는 문서에만 쓰인다. */
|
|
231
231
|
terminology: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
@@ -280,8 +280,8 @@ export declare const ProjectProfile: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, un
|
|
|
280
280
|
}> | undefined;
|
|
281
281
|
bindings?: {
|
|
282
282
|
role: string;
|
|
283
|
-
resource: string;
|
|
284
283
|
adapter: string;
|
|
284
|
+
resource: string;
|
|
285
285
|
}[] | undefined;
|
|
286
286
|
}, {
|
|
287
287
|
id: string;
|
|
@@ -318,8 +318,8 @@ export declare const ProjectProfile: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, un
|
|
|
318
318
|
}> | undefined;
|
|
319
319
|
bindings?: {
|
|
320
320
|
role: string;
|
|
321
|
-
resource: string;
|
|
322
321
|
adapter: string;
|
|
322
|
+
resource: string;
|
|
323
323
|
}[] | undefined;
|
|
324
324
|
policy?: {
|
|
325
325
|
hardDeny?: string[] | undefined;
|