adde-acp 0.1.3
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/LICENSE +21 -0
- package/README.ko.md +88 -0
- package/README.md +88 -0
- package/dist/backend/acp/client.d.ts +149 -0
- package/dist/backend/acp/client.js +538 -0
- package/dist/backend/acp/client.js.map +1 -0
- package/dist/backend/acp/index.d.ts +8 -0
- package/dist/backend/acp/index.js +7 -0
- package/dist/backend/acp/index.js.map +1 -0
- package/dist/backend/acp/lifecycle.d.ts +15 -0
- package/dist/backend/acp/lifecycle.js +56 -0
- package/dist/backend/acp/lifecycle.js.map +1 -0
- package/dist/backend/acp/perm-diff.d.ts +37 -0
- package/dist/backend/acp/perm-diff.js +58 -0
- package/dist/backend/acp/perm-diff.js.map +1 -0
- package/dist/backend/acp/spawn.d.ts +20 -0
- package/dist/backend/acp/spawn.js +70 -0
- package/dist/backend/acp/spawn.js.map +1 -0
- package/dist/cli/adde.d.ts +2 -0
- package/dist/cli/adde.js +11 -0
- package/dist/cli/adde.js.map +1 -0
- package/dist/cli/alias.d.ts +45 -0
- package/dist/cli/alias.js +94 -0
- package/dist/cli/alias.js.map +1 -0
- package/dist/cli/completion.d.ts +4 -0
- package/dist/cli/completion.js +209 -0
- package/dist/cli/completion.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +114 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/lane.d.ts +20 -0
- package/dist/cli/lane.js +350 -0
- package/dist/cli/lane.js.map +1 -0
- package/dist/cli/ops.d.ts +5 -0
- package/dist/cli/ops.js +230 -0
- package/dist/cli/ops.js.map +1 -0
- package/dist/cli/prompt.d.ts +15 -0
- package/dist/cli/prompt.js +41 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.js +216 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/spec.d.ts +48 -0
- package/dist/cli/spec.js +98 -0
- package/dist/cli/spec.js.map +1 -0
- package/dist/core/diagnostics.d.ts +73 -0
- package/dist/core/diagnostics.js +333 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +9 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/injector.d.ts +27 -0
- package/dist/core/injector.js +297 -0
- package/dist/core/injector.js.map +1 -0
- package/dist/core/lane-config.d.ts +80 -0
- package/dist/core/lane-config.js +303 -0
- package/dist/core/lane-config.js.map +1 -0
- package/dist/core/launchd.d.ts +81 -0
- package/dist/core/launchd.js +216 -0
- package/dist/core/launchd.js.map +1 -0
- package/dist/core/messages.d.ts +31 -0
- package/dist/core/messages.js +71 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/queue.d.ts +74 -0
- package/dist/core/queue.js +227 -0
- package/dist/core/queue.js.map +1 -0
- package/dist/core/runtime-state.d.ts +52 -0
- package/dist/core/runtime-state.js +90 -0
- package/dist/core/runtime-state.js.map +1 -0
- package/dist/core/session-ledger.d.ts +25 -0
- package/dist/core/session-ledger.js +89 -0
- package/dist/core/session-ledger.js.map +1 -0
- package/dist/core/supervisor.d.ts +41 -0
- package/dist/core/supervisor.js +315 -0
- package/dist/core/supervisor.js.map +1 -0
- package/dist/core/transcript.d.ts +22 -0
- package/dist/core/transcript.js +93 -0
- package/dist/core/transcript.js.map +1 -0
- package/dist/core/update-check.d.ts +25 -0
- package/dist/core/update-check.js +142 -0
- package/dist/core/update-check.js.map +1 -0
- package/dist/core/version.d.ts +7 -0
- package/dist/core/version.js +32 -0
- package/dist/core/version.js.map +1 -0
- package/dist/gate/gate.d.ts +41 -0
- package/dist/gate/gate.js +28 -0
- package/dist/gate/gate.js.map +1 -0
- package/dist/gate/index.d.ts +6 -0
- package/dist/gate/index.js +6 -0
- package/dist/gate/index.js.map +1 -0
- package/dist/shared/conf.d.ts +54 -0
- package/dist/shared/conf.js +85 -0
- package/dist/shared/conf.js.map +1 -0
- package/dist/shared/deny-match.d.ts +19 -0
- package/dist/shared/deny-match.js +122 -0
- package/dist/shared/deny-match.js.map +1 -0
- package/dist/shared/envelope.d.ts +37 -0
- package/dist/shared/envelope.js +91 -0
- package/dist/shared/envelope.js.map +1 -0
- package/dist/shared/errors.d.ts +8 -0
- package/dist/shared/errors.js +23 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/fs-atomic.d.ts +17 -0
- package/dist/shared/fs-atomic.js +31 -0
- package/dist/shared/fs-atomic.js.map +1 -0
- package/dist/shared/i18n.d.ts +23 -0
- package/dist/shared/i18n.js +53 -0
- package/dist/shared/i18n.js.map +1 -0
- package/dist/shared/locales/en.d.ts +393 -0
- package/dist/shared/locales/en.js +447 -0
- package/dist/shared/locales/en.js.map +1 -0
- package/dist/shared/locales/ko.d.ts +389 -0
- package/dist/shared/locales/ko.js +443 -0
- package/dist/shared/locales/ko.js.map +1 -0
- package/dist/shared/mask.d.ts +6 -0
- package/dist/shared/mask.js +28 -0
- package/dist/shared/mask.js.map +1 -0
- package/dist/shared/notify.d.ts +15 -0
- package/dist/shared/notify.js +20 -0
- package/dist/shared/notify.js.map +1 -0
- package/dist/shared/paths.d.ts +42 -0
- package/dist/shared/paths.js +83 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/src-adapters/index.d.ts +8 -0
- package/dist/src-adapters/index.js +6 -0
- package/dist/src-adapters/index.js.map +1 -0
- package/dist/src-adapters/markdown.d.ts +80 -0
- package/dist/src-adapters/markdown.js +794 -0
- package/dist/src-adapters/markdown.js.map +1 -0
- package/dist/src-adapters/source.d.ts +33 -0
- package/dist/src-adapters/source.js +3 -0
- package/dist/src-adapters/source.js.map +1 -0
- package/dist/src-adapters/telegram.d.ts +48 -0
- package/dist/src-adapters/telegram.js +412 -0
- package/dist/src-adapters/telegram.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Promise 를 시한부로 감싼다 — 초과 시 onTimeout() 으로 reject. settle 시 타이머 정리(누수 방지). */
|
|
2
|
+
export function withTimeout(p, ms, onTimeout) {
|
|
3
|
+
let timer;
|
|
4
|
+
const timeout = new Promise((_, reject) => {
|
|
5
|
+
timer = setTimeout(() => reject(onTimeout()), ms);
|
|
6
|
+
});
|
|
7
|
+
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 실패 경로용 즉시 강제 종료 — 핸드셰이크 실패 등에서 child 누수 방지.
|
|
11
|
+
* 종료 판정은 exitCode===null(생존)로만 한다 — child.killed 는 "신호를 보냈다"는 뜻이지
|
|
12
|
+
* "죽었다"가 아니므로 가드에 쓰면 SIGKILL 이 누락된다.
|
|
13
|
+
*/
|
|
14
|
+
export function killChild(child) {
|
|
15
|
+
// kill() 은 신호 전송 불가 상태(이미 종료·EPERM)에서 throw 할 수 있다 — 흡수한다.
|
|
16
|
+
// 생존 판정은 호출부가 exitCode 로 하므로 전송 실패가 곧 누수는 아니다.
|
|
17
|
+
if (child.exitCode === null) {
|
|
18
|
+
try {
|
|
19
|
+
child.kill("SIGKILL");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* 신호 전송 실패 — noop(이미 종료 등) */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** graceful 종료: SIGTERM → graceMs 유예 → 미종료 시 SIGKILL. child exit 시 조기 정리. */
|
|
27
|
+
export async function closeChild(child, graceMs) {
|
|
28
|
+
if (child.exitCode !== null)
|
|
29
|
+
return;
|
|
30
|
+
await new Promise((resolve) => {
|
|
31
|
+
let done = false;
|
|
32
|
+
const finish = () => {
|
|
33
|
+
if (done)
|
|
34
|
+
return;
|
|
35
|
+
done = true;
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolve();
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
// SIGTERM 무응답 → 강제 종료. child.killed(신호 전송 여부) 가 아니라 생존 여부로 판정.
|
|
41
|
+
killChild(child);
|
|
42
|
+
finish();
|
|
43
|
+
}, graceMs);
|
|
44
|
+
child.once("exit", finish);
|
|
45
|
+
try {
|
|
46
|
+
child.kill("SIGTERM");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// SIGTERM 전송 실패(이미 종료·EPERM 등) — executor throw 는 Promise 를 reject 시켜 close 호출부의
|
|
50
|
+
// 셧다운을 중단시키므로 흡수한다. 즉시 강제 종료 시도 후 종결(무한 대기 방지).
|
|
51
|
+
killChild(child);
|
|
52
|
+
finish();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../../src/backend/acp/lifecycle.ts"],"names":[],"mappings":"AAMA,+EAA+E;AAC/E,MAAM,UAAU,WAAW,CAAI,CAAa,EAAE,EAAU,EAAE,SAAsB;IAC9E,IAAI,KAAoC,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QAC/C,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAmB;IAC3C,2DAA2D;IAC3D,+CAA+C;IAC/C,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,8BAA8B;QAChC,CAAC;IACH,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAAmB,EAAE,OAAe;IACnE,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO;IACpC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,IAAI,IAAI,GAAG,KAAK,CAAC;QACjB,MAAM,MAAM,GAAG,GAAS,EAAE;YACxB,IAAI,IAAI;gBAAE,OAAO;YACjB,IAAI,GAAG,IAAI,CAAC;YACZ,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,+DAA+D;YAC/D,SAAS,CAAC,KAAK,CAAC,CAAC;YACjB,MAAM,EAAE,CAAC;QACX,CAAC,EAAE,OAAO,CAAC,CAAC;QACZ,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC3B,IAAI,CAAC;YACH,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,iFAAiF;YACjF,gDAAgD;YAChD,SAAS,CAAC,KAAK,CAAC,CAAC;YACjB,MAAM,EAAE,CAAC;QACX,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { t } from "../../shared/i18n.js";
|
|
2
|
+
/** 로케일 고정 t (레인별 채널 로케일). 미지정 시 전역 로케일. */
|
|
3
|
+
type Tl = typeof t;
|
|
4
|
+
export interface AddePolicy {
|
|
5
|
+
perm_tier: string;
|
|
6
|
+
allowlist?: string[];
|
|
7
|
+
/** perm_tier=autopass 에서 채널 승인으로 폴백할 도구명 목록. */
|
|
8
|
+
denylist?: string[];
|
|
9
|
+
/** 방어심화 하드-거부 목록 — 매칭 도구는 티어 무관하게 즉시 거부(채널 프롬프트 없음). */
|
|
10
|
+
hard_deny?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface EngineEffective {
|
|
13
|
+
permissionMode?: string | undefined;
|
|
14
|
+
bypassPermissions?: boolean | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface PermDiffResult {
|
|
17
|
+
diff: boolean;
|
|
18
|
+
warn?: {
|
|
19
|
+
level: "WARN";
|
|
20
|
+
message: string;
|
|
21
|
+
adde: AddePolicy;
|
|
22
|
+
engine: EngineEffective | null;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* ADDE 정책과 엔진 실효 설정을 비교한다.
|
|
28
|
+
* 동일 입력에서 단일 분기 블록으로 "차이 여부 + WARN 발화" 결정(research §E).
|
|
29
|
+
*
|
|
30
|
+
* engineEffective 가 null 이면 조회 실패로 간주 → 보수적 "확인불가=차이" WARN.
|
|
31
|
+
*/
|
|
32
|
+
export declare function comparePerm(addePolicy: AddePolicy, engineEffective: EngineEffective | null, tl?: Tl): PermDiffResult;
|
|
33
|
+
/**
|
|
34
|
+
* WARN 메시지 포맷 — 마스킹 적용.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatWarn(adde: AddePolicy, engine: EngineEffective | null, reason: string, tl?: Tl): string;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 설정 차이 비교·WARN.
|
|
3
|
+
* ADDE 정책 ↔ 엔진 실효 설정 비교.
|
|
4
|
+
* 안전망: 조회 실패 시 "확인불가=차이"로 보수적 WARN 발화.
|
|
5
|
+
*/
|
|
6
|
+
import { maskSecrets } from "../../shared/mask.js";
|
|
7
|
+
import { t } from "../../shared/i18n.js";
|
|
8
|
+
/**
|
|
9
|
+
* ADDE 정책과 엔진 실효 설정을 비교한다.
|
|
10
|
+
* 동일 입력에서 단일 분기 블록으로 "차이 여부 + WARN 발화" 결정(research §E).
|
|
11
|
+
*
|
|
12
|
+
* engineEffective 가 null 이면 조회 실패로 간주 → 보수적 "확인불가=차이" WARN.
|
|
13
|
+
*/
|
|
14
|
+
export function comparePerm(addePolicy, engineEffective, tl = t) {
|
|
15
|
+
if (engineEffective === null) {
|
|
16
|
+
const warn = {
|
|
17
|
+
level: "WARN",
|
|
18
|
+
message: formatWarn(addePolicy, null, tl("permDiff.queryFailedMsg"), tl),
|
|
19
|
+
adde: addePolicy,
|
|
20
|
+
engine: null,
|
|
21
|
+
reason: "조회실패",
|
|
22
|
+
};
|
|
23
|
+
return { diff: true, warn };
|
|
24
|
+
}
|
|
25
|
+
const engineIsBypass = engineEffective.bypassPermissions === true ||
|
|
26
|
+
engineEffective.permissionMode === "bypassPermissions";
|
|
27
|
+
if (addePolicy.perm_tier === "acp" && engineIsBypass) {
|
|
28
|
+
const warn = {
|
|
29
|
+
level: "WARN",
|
|
30
|
+
message: formatWarn(addePolicy, engineEffective, tl("permDiff.looseEngine"), tl),
|
|
31
|
+
adde: addePolicy,
|
|
32
|
+
engine: engineEffective,
|
|
33
|
+
reason: "정책차이",
|
|
34
|
+
};
|
|
35
|
+
return { diff: true, warn };
|
|
36
|
+
}
|
|
37
|
+
if (addePolicy.perm_tier === "autopass" && engineIsBypass) {
|
|
38
|
+
// 엔진 bypass 는 권한 요청 자체를 발화하지 않는다 — autopass 의 denylist 폴백·자동허용
|
|
39
|
+
// 기록이 전부 무력화되므로 acp 와 별도 사유로 표기한다.
|
|
40
|
+
const warn = {
|
|
41
|
+
level: "WARN",
|
|
42
|
+
message: formatWarn(addePolicy, engineEffective, tl("permDiff.bypassMsg"), tl),
|
|
43
|
+
adde: addePolicy,
|
|
44
|
+
engine: engineEffective,
|
|
45
|
+
reason: "정책차이",
|
|
46
|
+
};
|
|
47
|
+
return { diff: true, warn };
|
|
48
|
+
}
|
|
49
|
+
return { diff: false };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* WARN 메시지 포맷 — 마스킹 적용.
|
|
53
|
+
*/
|
|
54
|
+
export function formatWarn(adde, engine, reason, tl = t) {
|
|
55
|
+
const engineStr = engine ? JSON.stringify(engine) : tl("permDiff.engineUnknown");
|
|
56
|
+
return maskSecrets(tl("permDiff.warnLine", { reason, tier: adde.perm_tier, engine: engineStr }));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=perm-diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perm-diff.js","sourceRoot":"","sources":["../../../src/backend/acp/perm-diff.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,CAAC,EAAE,MAAM,sBAAsB,CAAC;AA8BzC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,UAAsB,EACtB,eAAuC,EACvC,KAAS,CAAC;IAEV,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG;YACX,KAAK,EAAE,MAAe;YACtB,OAAO,EAAE,UAAU,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC,yBAAyB,CAAC,EAAE,EAAE,CAAC;YACxE,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,MAAM;SACf,CAAC;QACF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9B,CAAC;IAED,MAAM,cAAc,GAClB,eAAe,CAAC,iBAAiB,KAAK,IAAI;QAC1C,eAAe,CAAC,cAAc,KAAK,mBAAmB,CAAC;IAEzD,IAAI,UAAU,CAAC,SAAS,KAAK,KAAK,IAAI,cAAc,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG;YACX,KAAK,EAAE,MAAe;YACtB,OAAO,EAAE,UAAU,CAAC,UAAU,EAAE,eAAe,EAAE,EAAE,CAAC,sBAAsB,CAAC,EAAE,EAAE,CAAC;YAChF,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,eAAe;YACvB,MAAM,EAAE,MAAM;SACf,CAAC;QACF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,UAAU,CAAC,SAAS,KAAK,UAAU,IAAI,cAAc,EAAE,CAAC;QAC1D,+DAA+D;QAC/D,mCAAmC;QACnC,MAAM,IAAI,GAAG;YACX,KAAK,EAAE,MAAe;YACtB,OAAO,EAAE,UAAU,CAAC,UAAU,EAAE,eAAe,EAAE,EAAE,CAAC,oBAAoB,CAAC,EAAE,EAAE,CAAC;YAC9E,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,eAAe;YACvB,MAAM,EAAE,MAAM;SACf,CAAC;QACF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CACxB,IAAgB,EAChB,MAA8B,EAC9B,MAAc,EACd,KAAS,CAAC;IAEV,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,wBAAwB,CAAC,CAAC;IACjF,OAAO,WAAW,CAAC,EAAE,CAAC,mBAAmB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;AACnG,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* process.env 복사본에서 중첩 유발 키를 제거한 clean env 반환.
|
|
4
|
+
* 테스트 주입 가능하도록 분리 export.
|
|
5
|
+
*/
|
|
6
|
+
export declare function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
7
|
+
/** spawnEngine 옵션. */
|
|
8
|
+
export interface SpawnEngineOptions {
|
|
9
|
+
/**
|
|
10
|
+
* 지정되면 엔진 stderr 를 이 경로로 append 캡처한다(디렉터리 자동 생성).
|
|
11
|
+
* 미지정 시 기존 동작(부모 stderr inherit)을 유지한다 — 테스트/레거시 호환.
|
|
12
|
+
*/
|
|
13
|
+
stderrPath?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* ACP 엔진 바이너리를 clean env 로 spawn 한다.
|
|
17
|
+
* stdio는 pipe 모드: stdin/stdout 을 ACP JSON-RPC 채널로 사용.
|
|
18
|
+
* opts.stderrPath 지정 시 stderr 를 파일로 append 캡처(미지정 시 inherit).
|
|
19
|
+
*/
|
|
20
|
+
export declare function spawnEngine(bin: string, args: string[], opts?: SpawnEngineOptions): ChildProcess;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 엔진 서브프로세스 spawn — clean env 보장.
|
|
3
|
+
* CLAUDECODE·CLAUDE_CODE_ENTRYPOINT 삭제 후 spawn.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { createWriteStream, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { maskSecrets } from "../../shared/mask.js";
|
|
9
|
+
/** CLAUDECODE 중첩 유발 환경변수 목록. */
|
|
10
|
+
const NESTED_GUARD_KEYS = ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"];
|
|
11
|
+
/**
|
|
12
|
+
* process.env 복사본에서 중첩 유발 키를 제거한 clean env 반환.
|
|
13
|
+
* 테스트 주입 가능하도록 분리 export.
|
|
14
|
+
*/
|
|
15
|
+
export function cleanEnv(env) {
|
|
16
|
+
const result = { ...env };
|
|
17
|
+
for (const key of NESTED_GUARD_KEYS) {
|
|
18
|
+
delete result[key];
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* ACP 엔진 바이너리를 clean env 로 spawn 한다.
|
|
24
|
+
* stdio는 pipe 모드: stdin/stdout 을 ACP JSON-RPC 채널로 사용.
|
|
25
|
+
* opts.stderrPath 지정 시 stderr 를 파일로 append 캡처(미지정 시 inherit).
|
|
26
|
+
*/
|
|
27
|
+
export function spawnEngine(bin, args, opts = {}) {
|
|
28
|
+
const captureStderr = opts.stderrPath !== undefined;
|
|
29
|
+
const child = spawn(bin, args, {
|
|
30
|
+
// stderr: 캡처 시 pipe(소비 필수 — 미소비 시 backpressure 로 child 가 막힘), 아니면 inherit.
|
|
31
|
+
stdio: ["pipe", "pipe", captureStderr ? "pipe" : "inherit"],
|
|
32
|
+
env: cleanEnv(process.env),
|
|
33
|
+
});
|
|
34
|
+
if (captureStderr && child.stderr) {
|
|
35
|
+
const path = opts.stderrPath;
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
const ws = createWriteStream(path, { flags: "a" });
|
|
38
|
+
// 로그 스트림 오류는 흡수(진단 로그 — 엔진 동작 비차단, unhandled 'error' 방지).
|
|
39
|
+
ws.on("error", () => { });
|
|
40
|
+
// engine.log 는 마스킹되지 않는 side channel — 라인 단위로 maskSecrets 적용 후 기록
|
|
41
|
+
// (transcript 만 마스킹하면 엔진 stderr 로 토큰·민감경로가 평문 유출될 수 있음).
|
|
42
|
+
// pipe 대신 data 핸들러로 소비 — pipe 한 stream 미소비 시 backpressure 로 child 가 막힌다.
|
|
43
|
+
let buf = "";
|
|
44
|
+
// 개행 없는 초장문 방어 — 상한 초과 시 마스킹해 flush(메모리 무한 증가 방지).
|
|
45
|
+
const MAX_BUF = 1 << 20; // 1MB
|
|
46
|
+
child.stderr.setEncoding("utf8");
|
|
47
|
+
child.stderr.on("data", (chunk) => {
|
|
48
|
+
buf += chunk;
|
|
49
|
+
let nl;
|
|
50
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
51
|
+
const line = buf.slice(0, nl);
|
|
52
|
+
buf = buf.slice(nl + 1);
|
|
53
|
+
ws.write(maskSecrets(line) + "\n");
|
|
54
|
+
}
|
|
55
|
+
if (buf.length > MAX_BUF) {
|
|
56
|
+
ws.write(maskSecrets(buf));
|
|
57
|
+
buf = "";
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// stderr EOF('end') 에서 잔여 부분 라인 flush 후 스트림 정리 — 'exit' 는 stderr 버퍼 배출을
|
|
61
|
+
// 보장하지 않아 write-after-end·꼬리 라인 마스킹 누락을 유발할 수 있다(EOF 는 데이터 완결 보장).
|
|
62
|
+
child.stderr.on("end", () => {
|
|
63
|
+
if (buf.length > 0)
|
|
64
|
+
ws.write(maskSecrets(buf));
|
|
65
|
+
ws.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return child;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=spawn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn.js","sourceRoot":"","sources":["../../../src/backend/acp/spawn.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/adde.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from "./run.js";
|
|
3
|
+
void Promise.resolve(run(process.argv.slice(2)))
|
|
4
|
+
.then((code) => process.exit(code))
|
|
5
|
+
.catch((err) => {
|
|
6
|
+
// 최후 방어선 — dispatch 에서 예기치 못한 예외가 전파되면 스택트레이스 대신 한 줄로 알리고
|
|
7
|
+
// 비정상 종료한다(unhandled rejection 으로 인한 프로세스 강제 종료 방지).
|
|
8
|
+
process.stderr.write(`adde: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
//# sourceMappingURL=adde.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adde.js","sourceRoot":"","sources":["../../src/cli/adde.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KAC7C,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;KAClC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACtB,0DAA0D;IAC1D,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** 설치 후보로 추천하는 짧은 별칭(순서 = 표시 순서). */
|
|
2
|
+
export declare const RECOMMENDED_ALIASES: readonly ["ad", "add"];
|
|
3
|
+
/** 별칭 설치에 필요한 주입 의존성(테스트 가능하게 분리). */
|
|
4
|
+
export interface AliasDeps {
|
|
5
|
+
/** 별칭 심링크를 놓을 디렉터리(= adde 실행 파일이 있는 디렉터리). */
|
|
6
|
+
binDir: string;
|
|
7
|
+
/** 별칭이 가리킬 대상(adde 실행 파일 경로). */
|
|
8
|
+
addeTarget: string;
|
|
9
|
+
/** PATH 에 해당 이름의 실행 명령이 이미 존재하는지. */
|
|
10
|
+
commandExists: (name: string) => Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export type AliasSkipReason = "exists" | "occupied" | "error";
|
|
13
|
+
export interface AliasSetupResult {
|
|
14
|
+
/** 새로 만든 별칭. */
|
|
15
|
+
created: string[];
|
|
16
|
+
/** 이미 adde 를 가리키고 있어 그대로 둔 별칭. */
|
|
17
|
+
alreadyLinked: string[];
|
|
18
|
+
/**
|
|
19
|
+
* 건너뛴 별칭 — exists(PATH 에 동명 명령 존재) / occupied(자리 점유) /
|
|
20
|
+
* error(심링크 생성 실패: EACCES 루트소유 bin·EEXIST 비심링크 파일 등). detail 은 실패 사유.
|
|
21
|
+
*/
|
|
22
|
+
skipped: {
|
|
23
|
+
name: string;
|
|
24
|
+
reason: AliasSkipReason;
|
|
25
|
+
detail?: string;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* PATH 를 스캔해 실행 가능한 파일 경로를 찾는다(첫 히트). 없으면 null.
|
|
30
|
+
* `command -v` 셸 빌트인 대신 직접 스캔 — 자식 셸 스폰 없이 결정적.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findExecutableInPath(name: string, env?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
33
|
+
/**
|
|
34
|
+
* 별칭들을 설치한다. 각 이름에 대해:
|
|
35
|
+
* - 이미 adde 를 가리키는 우리 심링크면 alreadyLinked(멱등).
|
|
36
|
+
* - PATH 에 동명 명령이 있으면 skipped(exists) — 사용자 요구대로 실패 출력.
|
|
37
|
+
* - 자리에 우리 것이 아닌 무언가가 있으면 skipped(occupied).
|
|
38
|
+
* - 그 외엔 심링크 생성 → created.
|
|
39
|
+
*/
|
|
40
|
+
export declare function setupAliases(names: readonly string[], deps: AliasDeps): Promise<AliasSetupResult>;
|
|
41
|
+
/**
|
|
42
|
+
* 실 환경 의존성 해석 — PATH 에서 `adde` 실행 파일을 찾아 그 디렉터리에 별칭을 놓는다.
|
|
43
|
+
* 전역 설치가 아니면(개발 tsx 등) adde 가 PATH 에 없어 null — 호출부가 안내 후 스킵.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveAliasDeps(): Promise<AliasDeps | null>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 짧은 별칭(`ad`·`add`) 설치 — 전역 설치된 `adde` 실행 파일 옆에 심볼릭 링크를 만든다.
|
|
3
|
+
* npm 은 `npm i -g` 도중 대화형 프롬프트를 띄울 수 없으므로 별칭을 bin 에 굽지 않고,
|
|
4
|
+
* 온보딩(`adde init`)·`adde alias` 에서 사용자가 옵트인으로 설치한다.
|
|
5
|
+
* PATH 에 동명 명령이 이미 있으면(우리 것이 아닌) 그 별칭은 실패로 건너뛴다(사용자 요구).
|
|
6
|
+
*/
|
|
7
|
+
import { symlink, readlink, stat } from "node:fs/promises";
|
|
8
|
+
import { join, dirname, resolve } from "node:path";
|
|
9
|
+
/** 설치 후보로 추천하는 짧은 별칭(순서 = 표시 순서). */
|
|
10
|
+
export const RECOMMENDED_ALIASES = ["ad", "add"];
|
|
11
|
+
/** 심링크면 대상 경로, 아니면(부재·일반 파일) null. */
|
|
12
|
+
async function readlinkSafe(p) {
|
|
13
|
+
try {
|
|
14
|
+
return await readlink(p);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* PATH 를 스캔해 실행 가능한 파일 경로를 찾는다(첫 히트). 없으면 null.
|
|
22
|
+
* `command -v` 셸 빌트인 대신 직접 스캔 — 자식 셸 스폰 없이 결정적.
|
|
23
|
+
*/
|
|
24
|
+
export async function findExecutableInPath(name, env = process.env) {
|
|
25
|
+
const dirs = (env["PATH"] ?? "").split(":").filter((d) => d.length > 0);
|
|
26
|
+
for (const dir of dirs) {
|
|
27
|
+
const candidate = join(dir, name);
|
|
28
|
+
try {
|
|
29
|
+
const st = await stat(candidate);
|
|
30
|
+
if (st.isFile() && (st.mode & 0o111) !== 0)
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// 이 디렉터리엔 없음 — 계속.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 별칭들을 설치한다. 각 이름에 대해:
|
|
41
|
+
* - 이미 adde 를 가리키는 우리 심링크면 alreadyLinked(멱등).
|
|
42
|
+
* - PATH 에 동명 명령이 있으면 skipped(exists) — 사용자 요구대로 실패 출력.
|
|
43
|
+
* - 자리에 우리 것이 아닌 무언가가 있으면 skipped(occupied).
|
|
44
|
+
* - 그 외엔 심링크 생성 → created.
|
|
45
|
+
*/
|
|
46
|
+
export async function setupAliases(names, deps) {
|
|
47
|
+
const result = { created: [], alreadyLinked: [], skipped: [] };
|
|
48
|
+
const target = resolve(deps.addeTarget);
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
const linkPath = join(deps.binDir, name);
|
|
51
|
+
const existing = await readlinkSafe(linkPath);
|
|
52
|
+
if (existing !== null && resolve(dirname(linkPath), existing) === target) {
|
|
53
|
+
result.alreadyLinked.push(name);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (await deps.commandExists(name)) {
|
|
57
|
+
result.skipped.push({ name, reason: "exists" });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (existing !== null) {
|
|
61
|
+
result.skipped.push({ name, reason: "occupied" });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await symlink(target, linkPath);
|
|
66
|
+
result.created.push(name);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// 심링크 실패(EACCES 루트소유 bin·EEXIST 비심링크 파일 등)는 이 별칭만 건너뛴다 —
|
|
70
|
+
// 옵트인 편의 단계가 alias/init 전체 흐름을 중단시키지 않도록 흡수해 사유로 보고한다.
|
|
71
|
+
result.skipped.push({
|
|
72
|
+
name,
|
|
73
|
+
reason: "error",
|
|
74
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 실 환경 의존성 해석 — PATH 에서 `adde` 실행 파일을 찾아 그 디렉터리에 별칭을 놓는다.
|
|
82
|
+
* 전역 설치가 아니면(개발 tsx 등) adde 가 PATH 에 없어 null — 호출부가 안내 후 스킵.
|
|
83
|
+
*/
|
|
84
|
+
export async function resolveAliasDeps() {
|
|
85
|
+
const addePath = await findExecutableInPath("adde");
|
|
86
|
+
if (!addePath)
|
|
87
|
+
return null;
|
|
88
|
+
return {
|
|
89
|
+
binDir: dirname(addePath),
|
|
90
|
+
addeTarget: addePath,
|
|
91
|
+
commandExists: (n) => findExecutableInPath(n).then((p) => p !== null),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=alias.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alias.js","sourceRoot":"","sources":["../../src/cli/alias.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 셸 자동완성 스크립트 생성 — `adde completion <bash|zsh>`.
|
|
3
|
+
* COMMAND_SPECS(SSOT)에서 명령·플래그·위치인자 종류를 파생하므로, 스펙에 추가하면
|
|
4
|
+
* 자동완성이 함께 갱신된다. bash·zsh(맥 기본) 지원. adde + 짧은 별칭(ad·add) 등록.
|
|
5
|
+
*
|
|
6
|
+
* 동적 완성(proj/lane 이름)은 node 스폰 없이 셸에서 설정 base(`${ADDE_HOME:-~/.config/adde}`)를
|
|
7
|
+
* 직접 스캔한다(즉응성). enum 플래그 값·디렉터리 플래그·zsh 명령 설명도 스펙에서 생성.
|
|
8
|
+
*/
|
|
9
|
+
import { visibleCommands, LANE_ADD_FLAGS, LANE_SUBS, FLAG_VALUES, DIR_FLAGS, GLOBAL_FLAGS, } from "./spec.js";
|
|
10
|
+
import { RECOMMENDED_ALIASES } from "./alias.js";
|
|
11
|
+
export const SUPPORTED_SHELLS = ["bash", "zsh"];
|
|
12
|
+
/** adde + 짧은 별칭을 완성 대상으로 등록(공백 구분, 중복 제거). */
|
|
13
|
+
const COMPLETION_TARGETS = [...new Set(["adde", ...RECOMMENDED_ALIASES])].join(" ");
|
|
14
|
+
/** 최상위 명령 중 자동완성에서 특수 처리하는(제네릭 case 제외) 이름. */
|
|
15
|
+
const SPECIAL = new Set(["lane", "completion", "alias"]);
|
|
16
|
+
function commandNames() {
|
|
17
|
+
return visibleCommands()
|
|
18
|
+
.map((c) => c.name)
|
|
19
|
+
.join(" ");
|
|
20
|
+
}
|
|
21
|
+
/** 제네릭(위치인자 proj/lane) 명령 목록 — lane/completion/alias 제외. */
|
|
22
|
+
function genericCommands() {
|
|
23
|
+
return visibleCommands().filter((c) => !SPECIAL.has(c.name));
|
|
24
|
+
}
|
|
25
|
+
// ── bash ────────────────────────────────────────────────────────────────
|
|
26
|
+
/** 값 플래그(enum·디렉터리) 뒤 완성 case (bash). */
|
|
27
|
+
function bashFlagValueCases() {
|
|
28
|
+
const lines = Object.entries(FLAG_VALUES).map(([flag, vals]) => ` ${flag}) COMPREPLY=( $(compgen -W "${vals.join(" ")}" -- "$cur") ); return;;`);
|
|
29
|
+
lines.push(` ${DIR_FLAGS.join("|")}) COMPREPLY=( $(compgen -d -- "$cur") ); return;;`);
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
/** 제네릭 명령별 위치인자·플래그 완성 case (bash). */
|
|
33
|
+
function bashCommandCases() {
|
|
34
|
+
return genericCommands()
|
|
35
|
+
.map((c) => {
|
|
36
|
+
const flags = c.flags.join(" ");
|
|
37
|
+
const withFlags = (base) => (flags ? `${base} ${flags}` : base);
|
|
38
|
+
const pos = c.positional ?? [];
|
|
39
|
+
if (pos[0] === "proj" && pos[1] === "lane") {
|
|
40
|
+
const tail = flags
|
|
41
|
+
? `\n COMPREPLY=( $(compgen -W "${flags}" -- "$cur") ); return;;`
|
|
42
|
+
: `\n ;;`;
|
|
43
|
+
return ` ${c.name})
|
|
44
|
+
if [ "$cword" -eq 2 ]; then COMPREPLY=( $(compgen -W "$(_adde_projects)" -- "$cur") ); return; fi
|
|
45
|
+
if [ "$cword" -eq 3 ]; then COMPREPLY=( $(compgen -W "${withFlags('$(_adde_lanes "${COMP_WORDS[2]}")')}" -- "$cur") ); return; fi${tail}`;
|
|
46
|
+
}
|
|
47
|
+
if (pos[0] === "proj") {
|
|
48
|
+
const tail = flags
|
|
49
|
+
? `\n COMPREPLY=( $(compgen -W "${flags}" -- "$cur") ); return;;`
|
|
50
|
+
: `\n ;;`;
|
|
51
|
+
return ` ${c.name})
|
|
52
|
+
if [ "$cword" -eq 2 ]; then COMPREPLY=( $(compgen -W "${withFlags("$(_adde_projects)")}" -- "$cur") ); return; fi${tail}`;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
})
|
|
56
|
+
.filter((s) => s !== null)
|
|
57
|
+
.join("\n");
|
|
58
|
+
}
|
|
59
|
+
function bashScript() {
|
|
60
|
+
const commands = commandNames();
|
|
61
|
+
const globals = GLOBAL_FLAGS.join(" ");
|
|
62
|
+
const laneSubs = LANE_SUBS.join(" ");
|
|
63
|
+
const laneAddFlags = LANE_ADD_FLAGS.join(" ");
|
|
64
|
+
const aliasSuggest = RECOMMENDED_ALIASES.join(" ");
|
|
65
|
+
return `# adde bash completion — generated by \`adde completion bash\`
|
|
66
|
+
# 설치: adde completion bash > /usr/local/etc/bash_completion.d/adde (또는 ~/.bashrc 에서 source)
|
|
67
|
+
# proj/lane 동적 완성은 \${ADDE_HOME:-~/.config/adde} 를 스캔한다.
|
|
68
|
+
_adde_projects() {
|
|
69
|
+
local base="\${ADDE_HOME:-$HOME/.config/adde}" d
|
|
70
|
+
[ -d "$base" ] || return 0
|
|
71
|
+
for d in "$base"/*/; do [ -d "\${d}lanes.d" ] && basename "$d"; done 2>/dev/null
|
|
72
|
+
}
|
|
73
|
+
_adde_lanes() {
|
|
74
|
+
local base="\${ADDE_HOME:-$HOME/.config/adde}" f
|
|
75
|
+
[ -n "$1" ] || return 0
|
|
76
|
+
for f in "$base/$1/lanes.d"/*.conf; do [ -e "$f" ] && basename "$f" .conf; done 2>/dev/null
|
|
77
|
+
}
|
|
78
|
+
_adde() {
|
|
79
|
+
local cur prev cword
|
|
80
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
81
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
82
|
+
cword=\${COMP_CWORD}
|
|
83
|
+
case "$prev" in
|
|
84
|
+
${bashFlagValueCases()}
|
|
85
|
+
esac
|
|
86
|
+
local commands="${commands}"
|
|
87
|
+
local cmd="\${COMP_WORDS[1]}"
|
|
88
|
+
if [ "$cword" -eq 1 ]; then
|
|
89
|
+
COMPREPLY=( $(compgen -W "\${commands} ${globals}" -- "$cur") )
|
|
90
|
+
return
|
|
91
|
+
fi
|
|
92
|
+
case "$cmd" in
|
|
93
|
+
lane)
|
|
94
|
+
if [ "$cword" -eq 2 ]; then COMPREPLY=( $(compgen -W "${laneSubs}" -- "$cur") ); return; fi
|
|
95
|
+
local sub="\${COMP_WORDS[2]}"
|
|
96
|
+
case "$sub" in
|
|
97
|
+
add)
|
|
98
|
+
if [ "$cword" -eq 3 ]; then COMPREPLY=( $(compgen -W "$(_adde_projects)" -- "$cur") ); return; fi
|
|
99
|
+
if [ "$cword" -ge 5 ] || [ "\${cur:0:1}" = "-" ]; then COMPREPLY=( $(compgen -W "${laneAddFlags}" -- "$cur") ); return; fi
|
|
100
|
+
;;
|
|
101
|
+
ls)
|
|
102
|
+
if [ "$cword" -eq 3 ]; then COMPREPLY=( $(compgen -W "$(_adde_projects)" -- "$cur") ); return; fi ;;
|
|
103
|
+
show|rm)
|
|
104
|
+
if [ "$cword" -eq 3 ]; then COMPREPLY=( $(compgen -W "$(_adde_projects)" -- "$cur") ); return; fi
|
|
105
|
+
if [ "$cword" -eq 4 ]; then COMPREPLY=( $(compgen -W "$(_adde_lanes "\${COMP_WORDS[3]}")" -- "$cur") ); return; fi ;;
|
|
106
|
+
esac
|
|
107
|
+
return;;
|
|
108
|
+
completion) COMPREPLY=( $(compgen -W "${SUPPORTED_SHELLS.join(" ")}" -- "$cur") ); return;;
|
|
109
|
+
alias) COMPREPLY=( $(compgen -W "${aliasSuggest}" -- "$cur") ); return;;
|
|
110
|
+
${bashCommandCases()}
|
|
111
|
+
esac
|
|
112
|
+
}
|
|
113
|
+
complete -F _adde ${COMPLETION_TARGETS}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
// ── zsh ─────────────────────────────────────────────────────────────────
|
|
117
|
+
/** 값 플래그(enum·디렉터리) 뒤 완성 case (zsh). */
|
|
118
|
+
function zshFlagValueCases() {
|
|
119
|
+
const lines = Object.entries(FLAG_VALUES).map(([flag, vals]) => ` ${flag}) compadd ${vals.join(" ")}; return;;`);
|
|
120
|
+
lines.push(` ${DIR_FLAGS.join("|")}) _files -/; return;;`);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
/** 제네릭 명령별 위치인자·플래그 완성 case (zsh). */
|
|
124
|
+
function zshCommandCases() {
|
|
125
|
+
return genericCommands()
|
|
126
|
+
.map((c) => {
|
|
127
|
+
const flagsVals = c.flags.length > 0 ? `; _values 'option' ${c.flags.join(" ")}` : "";
|
|
128
|
+
const pos = c.positional ?? [];
|
|
129
|
+
if (pos[0] === "proj" && pos[1] === "lane") {
|
|
130
|
+
return ` ${c.name})
|
|
131
|
+
if (( CURRENT == 3 )); then _adde_projects; return; fi
|
|
132
|
+
if (( CURRENT == 4 )); then _adde_lanes "\${words[3]}"${flagsVals}; return; fi${flagsVals ? `\n _values 'option' ${c.flags.join(" ")} ;;` : "\n ;;"}`;
|
|
133
|
+
}
|
|
134
|
+
if (pos[0] === "proj") {
|
|
135
|
+
return ` ${c.name})
|
|
136
|
+
if (( CURRENT == 3 )); then _adde_projects${flagsVals}; return; fi${flagsVals ? `\n _values 'option' ${c.flags.join(" ")} ;;` : "\n ;;"}`;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
})
|
|
140
|
+
.filter((s) => s !== null)
|
|
141
|
+
.join("\n");
|
|
142
|
+
}
|
|
143
|
+
function zshScript() {
|
|
144
|
+
const commandDescribe = visibleCommands()
|
|
145
|
+
.map((c) => `'${c.name}:${c.desc ?? c.name}'`)
|
|
146
|
+
.join(" ");
|
|
147
|
+
const laneSubs = LANE_SUBS.join(" ");
|
|
148
|
+
const laneAddFlags = LANE_ADD_FLAGS.join(" ");
|
|
149
|
+
const aliasSuggest = RECOMMENDED_ALIASES.join(" ");
|
|
150
|
+
const targets = ["adde", ...RECOMMENDED_ALIASES].join(" ");
|
|
151
|
+
return `#compdef ${targets}
|
|
152
|
+
# adde zsh completion — generated by \`adde completion zsh\`
|
|
153
|
+
# 설치: adde completion zsh > "\${fpath[1]}/_adde" (또는 compinit 후 이 스크립트를 source)
|
|
154
|
+
# proj/lane 동적 완성은 \${ADDE_HOME:-~/.config/adde} 를 스캔한다.
|
|
155
|
+
_adde_projects() {
|
|
156
|
+
local base="\${ADDE_HOME:-$HOME/.config/adde}" d
|
|
157
|
+
local -a p
|
|
158
|
+
for d in "$base"/*/lanes.d(/N); do p+=("\${\${d:h}:t}"); done
|
|
159
|
+
compadd -- $p
|
|
160
|
+
}
|
|
161
|
+
_adde_lanes() {
|
|
162
|
+
local base="\${ADDE_HOME:-$HOME/.config/adde}" f
|
|
163
|
+
local -a l
|
|
164
|
+
[ -n "$1" ] || return 0
|
|
165
|
+
for f in "$base/$1/lanes.d"/*.conf(N); do l+=("\${\${f:t}:r}"); done
|
|
166
|
+
compadd -- $l
|
|
167
|
+
}
|
|
168
|
+
_adde() {
|
|
169
|
+
local -a commands
|
|
170
|
+
commands=(${commandDescribe})
|
|
171
|
+
case "\${words[CURRENT-1]}" in
|
|
172
|
+
${zshFlagValueCases()}
|
|
173
|
+
esac
|
|
174
|
+
if (( CURRENT == 2 )); then
|
|
175
|
+
_describe 'adde command' commands
|
|
176
|
+
return
|
|
177
|
+
fi
|
|
178
|
+
case "\${words[2]}" in
|
|
179
|
+
lane)
|
|
180
|
+
if (( CURRENT == 3 )); then _values 'lane subcommand' ${laneSubs}; return; fi
|
|
181
|
+
case "\${words[3]}" in
|
|
182
|
+
add)
|
|
183
|
+
if (( CURRENT == 4 )); then _adde_projects; return; fi
|
|
184
|
+
if (( CURRENT == 5 )) && [[ "\${words[5]}" != -* ]]; then return; fi
|
|
185
|
+
_values 'option' ${laneAddFlags} ;;
|
|
186
|
+
ls)
|
|
187
|
+
if (( CURRENT == 4 )); then _adde_projects; return; fi ;;
|
|
188
|
+
show|rm)
|
|
189
|
+
if (( CURRENT == 4 )); then _adde_projects; return; fi
|
|
190
|
+
if (( CURRENT == 5 )); then _adde_lanes "\${words[4]}"; return; fi ;;
|
|
191
|
+
esac
|
|
192
|
+
;;
|
|
193
|
+
completion) _values 'shell' ${SUPPORTED_SHELLS.join(" ")} ;;
|
|
194
|
+
alias) compadd ${aliasSuggest} ;;
|
|
195
|
+
${zshCommandCases()}
|
|
196
|
+
esac
|
|
197
|
+
}
|
|
198
|
+
compdef _adde ${targets}
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
/** 지정 셸의 자동완성 스크립트 텍스트. 미지원 셸은 null. */
|
|
202
|
+
export function completionScript(shell) {
|
|
203
|
+
if (shell === "bash")
|
|
204
|
+
return bashScript();
|
|
205
|
+
if (shell === "zsh")
|
|
206
|
+
return zshScript();
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=completion.js.map
|