pleumrouter 0.5.41 → 0.5.42
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/package.json +1 -1
- package/src/install.mjs +58 -7
- package/src/menu.mjs +3 -4
- package/src/recipes.mjs +1 -1
package/package.json
CHANGED
package/src/install.mjs
CHANGED
|
@@ -2,22 +2,25 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { resolveLaunchCommand } from "./launch.mjs";
|
|
3
3
|
import { t } from "./i18n.mjs";
|
|
4
4
|
|
|
5
|
-
// 설치 명령 실행기(#40·#10) — RECIPES.install 문자열을 셸 없이
|
|
5
|
+
// 설치 명령 실행기(#40·#10) — RECIPES.install 문자열을 셸 없이 실행한다.
|
|
6
6
|
//
|
|
7
|
-
// 지원
|
|
8
|
-
// - 공백 구분 argv
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// 지원 형태:
|
|
8
|
+
// - 공백 구분 argv("npm i -g x")와 "&&" 순차 단계
|
|
9
|
+
// - 고정된 curl|sh 한 줄. curl과 sh/bash를 각각 spawn하고 stdout만 잇는다.
|
|
10
|
+
// - https .sh 주소 하나(antigravity)는 curl -fsSL | bash 로 같은 경로.
|
|
11
|
+
// 그 외 파이프·리다이렉트·치환은 {manual: true}. 문서 HTML은 실행하지 않는다.
|
|
12
12
|
// Windows .cmd/.bat spawn(EINVAL)은 resolveLaunchCommand 가 node 진입점으로
|
|
13
13
|
// 우회해 예방한다(#10).
|
|
14
14
|
|
|
15
15
|
const SHELL_METACHARS = /[|<>;$`' "\\]/;
|
|
16
|
+
const CURL_PIPE = /^curl\s+(-[fsSL]+)\s+(https:\/\/\S+)\s+\|\s+(sh|bash)$/;
|
|
16
17
|
|
|
17
18
|
export function parseInstallCommand(install) {
|
|
18
19
|
const raw = String(install ?? "").trim();
|
|
19
20
|
if (!raw) return { manual: true, reason: "empty" };
|
|
20
|
-
|
|
21
|
+
const piped = raw.match(CURL_PIPE);
|
|
22
|
+
if (piped) return { pipe: { curl: ["curl", piped[1], piped[2]], shell: piped[3] } };
|
|
23
|
+
if (/^https:\/\/\S+\.sh$/.test(raw)) return { pipe: { curl: ["curl", "-fsSL", raw], shell: "bash" } };
|
|
21
24
|
if (/^https?:\/\//.test(raw) && !/\s/.test(raw)) return { manual: true, reason: "url" };
|
|
22
25
|
const steps = raw.split("&&").map((part) => part.trim().split(/\s+/).filter(Boolean));
|
|
23
26
|
if (steps.some((args) => args.length === 0)) return { manual: true, reason: "empty-step" };
|
|
@@ -31,12 +34,60 @@ export function parseInstallCommand(install) {
|
|
|
31
34
|
* 설치 명령 실행. {code, manual} 반환 — manual이면 호출자가 수동 안내를 출력한다.
|
|
32
35
|
* spawnImpl/resolve는 테스트 주입용.
|
|
33
36
|
*/
|
|
37
|
+
function runCurlPipe(pipe, spawnImpl, resolve) {
|
|
38
|
+
let curlCmd;
|
|
39
|
+
let shellCmd;
|
|
40
|
+
try {
|
|
41
|
+
curlCmd = resolve(pipe.curl[0], pipe.curl.slice(1));
|
|
42
|
+
shellCmd = resolve(pipe.shell, []);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`✗ ${e.message}`);
|
|
45
|
+
return Promise.resolve(1);
|
|
46
|
+
}
|
|
47
|
+
return new Promise((res) => {
|
|
48
|
+
const curlChild = spawnImpl(curlCmd.command, curlCmd.args, { stdio: ["ignore", "pipe", "inherit"] });
|
|
49
|
+
const shellChild = spawnImpl(shellCmd.command, shellCmd.args, { stdio: ["pipe", "inherit", "inherit"] });
|
|
50
|
+
if (!curlChild.stdout?.pipe || !shellChild.stdin) {
|
|
51
|
+
res(1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
curlChild.stdout.pipe(shellChild.stdin);
|
|
55
|
+
let curlCode = null;
|
|
56
|
+
let shellCode = null;
|
|
57
|
+
const finish = () => {
|
|
58
|
+
if (curlCode == null || shellCode == null) return;
|
|
59
|
+
res(curlCode === 0 ? shellCode : curlCode);
|
|
60
|
+
};
|
|
61
|
+
curlChild.on("error", () => {
|
|
62
|
+
curlCode = 1;
|
|
63
|
+
try { shellChild.stdin?.end?.(); } catch { /* pipe가 이미 닫았을 수 있다 */ }
|
|
64
|
+
shellChild.kill?.();
|
|
65
|
+
finish();
|
|
66
|
+
});
|
|
67
|
+
shellChild.on("error", () => {
|
|
68
|
+
shellCode = 1;
|
|
69
|
+
finish();
|
|
70
|
+
});
|
|
71
|
+
curlChild.on("exit", (n) => {
|
|
72
|
+
curlCode = n ?? 1;
|
|
73
|
+
finish();
|
|
74
|
+
});
|
|
75
|
+
shellChild.on("exit", (n) => {
|
|
76
|
+
shellCode = n ?? 1;
|
|
77
|
+
finish();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
34
82
|
export async function runInstallCommand(install, {
|
|
35
83
|
spawnImpl = spawn,
|
|
36
84
|
resolve = resolveLaunchCommand,
|
|
37
85
|
} = {}) {
|
|
38
86
|
const parsed = parseInstallCommand(install);
|
|
39
87
|
if (parsed.manual) return { manual: true, reason: parsed.reason, code: null };
|
|
88
|
+
if (parsed.pipe) {
|
|
89
|
+
return { manual: false, code: await runCurlPipe(parsed.pipe, spawnImpl, resolve) };
|
|
90
|
+
}
|
|
40
91
|
|
|
41
92
|
for (const args of parsed.steps) {
|
|
42
93
|
const [bin, ...rest] = args;
|
package/src/menu.mjs
CHANGED
|
@@ -51,8 +51,8 @@ const AGENT_DESC = {
|
|
|
51
51
|
interpreter: "Local code execution & pair programmer",
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
54
|
+
// 설치 가능한 명령은 엔터로 실행한다. npm -g, curl|sh, https .sh 가 여기 들어온다.
|
|
55
|
+
// 문서 주소와 그 외 셸 문법은 수동 안내로 남긴다.
|
|
56
56
|
const autoInstallable = (r) => !parseInstallCommand(r.install).manual;
|
|
57
57
|
|
|
58
58
|
// 최상위 메뉴 항목(순수 함수 — 테스트 대상). 로그인은 첫 에이전트 실행 시 시작한다.
|
|
@@ -223,8 +223,7 @@ async function ensureInstalled(r) {
|
|
|
223
223
|
return false;
|
|
224
224
|
}
|
|
225
225
|
console.log(style.dim(`$ ${r.install}`));
|
|
226
|
-
// 공용 실행기: "&&" 순차
|
|
227
|
-
// 셸 문법(파이프 등)은 절대 임의 실행하지 않고 수동 안내로 명시한다.
|
|
226
|
+
// 공용 실행기: "&&" 순차 단계와 고정된 curl|sh. Windows .cmd는 resolveLaunchCommand 우회.
|
|
228
227
|
const result = await runInstallCommand(r.install);
|
|
229
228
|
if (result.manual) {
|
|
230
229
|
printManualInstall(r.install);
|
package/src/recipes.mjs
CHANGED
|
@@ -200,7 +200,7 @@ export const RECIPES = {
|
|
|
200
200
|
label: "Goose",
|
|
201
201
|
bin: "goose",
|
|
202
202
|
type: "env",
|
|
203
|
-
install: "https://
|
|
203
|
+
install: "curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash",
|
|
204
204
|
note: "Goose sets its model in its own config — use the openai/<id> format in `goose configure`.",
|
|
205
205
|
attribution: {
|
|
206
206
|
title: "Goose",
|