pi-lazy-setup 3.0.0 → 3.0.1
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.
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
// skills → `npx --yes skills add <repo> --skill <name> -g --agent pi -y`
|
|
12
12
|
// clis → officecli: skill file from https://officecli.ai/SKILL.md plus
|
|
13
13
|
// binary via `curl -fsSL https://d.officecli.ai/install.sh | bash`
|
|
14
|
+
// (Windows: `irm https://d.officecli.ai/install.ps1 | iex`)
|
|
15
|
+
//
|
|
16
|
+
// Windows note: pi.exec spawns without a shell, so npm's .cmd shims (pi,
|
|
17
|
+
// npx) cannot be launched directly — every command is routed through
|
|
18
|
+
// `cmd.exe /c`, the same workaround rpiv-pi's /rpiv-setup uses.
|
|
14
19
|
|
|
15
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
21
|
import { homedir } from "node:os";
|
|
@@ -27,8 +32,18 @@ function globalSkillsDir() {
|
|
|
27
32
|
|
|
28
33
|
const INSTALL_TIMEOUT_MS = 300_000;
|
|
29
34
|
const BIN_CHECK_TIMEOUT_MS = 15_000;
|
|
35
|
+
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
30
36
|
const STDERR_SNIPPET_CHARS = 300;
|
|
31
37
|
|
|
38
|
+
// Read at call time (not module load) so tests can simulate win32.
|
|
39
|
+
function isWindows() {
|
|
40
|
+
return process.platform === "win32";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Reference to the running ExtensionAPI, captured by the factory and used by
|
|
44
|
+
// the curl fallback in downloadSkillFile.
|
|
45
|
+
let piRef = null;
|
|
46
|
+
|
|
32
47
|
/** Pi extension packages offered via `pi install <source>`. */
|
|
33
48
|
const PACKAGES = [
|
|
34
49
|
{ source: "npm:pi-footer", description: "Footer status line (token usage, model, cost)." },
|
|
@@ -58,11 +73,16 @@ const CLIS = [
|
|
|
58
73
|
bin: "officecli",
|
|
59
74
|
description: "AI-friendly CLI for Office documents (.docx/.xlsx/.pptx).",
|
|
60
75
|
skillFile: { name: "officecli", url: "https://officecli.ai/SKILL.md" },
|
|
76
|
+
// macOS/Linux — official install script
|
|
61
77
|
installCommand: "curl -fsSL https://d.officecli.ai/install.sh | bash",
|
|
78
|
+
// Windows — official PowerShell install (officecli SKILL.md documents both)
|
|
79
|
+
installCommandWindows: "irm https://d.officecli.ai/install.ps1 | iex",
|
|
62
80
|
},
|
|
63
81
|
];
|
|
64
82
|
|
|
65
83
|
export default function piSetupExtension(pi) {
|
|
84
|
+
// captured for the curl fallback in downloadSkillFile
|
|
85
|
+
piRef = pi;
|
|
66
86
|
pi.registerCommand("pi-setup", {
|
|
67
87
|
description: "Install packages, skills, and CLI tools (each confirmed individually)",
|
|
68
88
|
handler: async (_args, ctx) => {
|
|
@@ -150,28 +170,55 @@ async function installClis(pi, ctx, report) {
|
|
|
150
170
|
if (await binaryAvailable(pi, c.bin)) {
|
|
151
171
|
report.skipped.push(`cli:${c.bin}`);
|
|
152
172
|
} else {
|
|
153
|
-
const
|
|
173
|
+
const installCmd = isWindows() ? (c.installCommandWindows ?? c.installCommand) : c.installCommand;
|
|
174
|
+
const confirmed = await ctx.ui.confirm("Install CLI tool?", `${c.id} — ${c.description}\n${installCmd}`);
|
|
154
175
|
if (confirmed) {
|
|
155
176
|
ctx.ui.notify(`Installing ${c.id}…`, "info");
|
|
156
|
-
|
|
177
|
+
// macOS/Linux: run the piped install script via bash -c.
|
|
178
|
+
// Windows: powershell.exe is a real binary — spawn it DIRECTLY
|
|
179
|
+
// (runExecDirect), because routing "… | …" through cmd.exe /c
|
|
180
|
+
// would let cmd parse the pipe itself.
|
|
181
|
+
const result = isWindows()
|
|
182
|
+
? await runExecDirect(pi, "powershell", ["-NoProfile", "-Command", installCmd])
|
|
183
|
+
: await runExec(pi, "bash", ["-c", installCmd]);
|
|
157
184
|
settle(result, `cli:${c.bin}`, report);
|
|
158
185
|
}
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
188
|
}
|
|
162
189
|
|
|
163
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Run a command via pi.exec and normalize into { ok, error }.
|
|
192
|
+
*
|
|
193
|
+
* On Windows, pi.exec's shell-less spawn cannot launch npm's .cmd shims
|
|
194
|
+
* (`pi`, `npx`) — the classic ENOENT trap (see rpiv-pi's pi-installer.ts) —
|
|
195
|
+
* so commands are routed through `cmd.exe /c`. Use runExecDirect for real
|
|
196
|
+
* .exe binaries whose arguments must NOT be re-parsed by cmd.exe
|
|
197
|
+
* (e.g. powershell -Command "… | …").
|
|
198
|
+
*/
|
|
164
199
|
async function runExec(pi, command, args, timeoutMs = INSTALL_TIMEOUT_MS) {
|
|
200
|
+
const { cmd, cmdArgs } = resolveCommand(command, args);
|
|
201
|
+
return runExecDirect(pi, cmd, cmdArgs, timeoutMs);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Run an exact command (no Windows cmd.exe re-parsing) and normalize into { ok, error }. */
|
|
205
|
+
async function runExecDirect(pi, cmd, cmdArgs, timeoutMs = INSTALL_TIMEOUT_MS) {
|
|
165
206
|
try {
|
|
166
|
-
const result = await pi.exec(
|
|
207
|
+
const result = await pi.exec(cmd, cmdArgs, { timeout: timeoutMs });
|
|
167
208
|
if (result.code === 0) return { ok: true };
|
|
168
209
|
const error = (result.stderr || result.stdout || `exit ${result.code}`).trim().slice(0, STDERR_SNIPPET_CHARS);
|
|
169
|
-
return { ok: false, error };
|
|
210
|
+
return { ok: false, error: `${cmd} ${cmdArgs.join(" ")}: ${error}` };
|
|
170
211
|
} catch (err) {
|
|
171
212
|
return { ok: false, error: toMessage(err) };
|
|
172
213
|
}
|
|
173
214
|
}
|
|
174
215
|
|
|
216
|
+
/** Windows: route through cmd.exe (spawns .cmd shims, provides shell). */
|
|
217
|
+
function resolveCommand(command, args) {
|
|
218
|
+
if (!isWindows()) return { cmd: command, cmdArgs: args };
|
|
219
|
+
return { cmd: "cmd.exe", cmdArgs: ["/c", command, ...args] };
|
|
220
|
+
}
|
|
221
|
+
|
|
175
222
|
function settle(result, item, report) {
|
|
176
223
|
if (result.ok) report.installed.push(item);
|
|
177
224
|
else report.failed.push({ item, error: result.error });
|
|
@@ -196,19 +243,59 @@ async function binaryAvailable(pi, name) {
|
|
|
196
243
|
const result = await runExec(pi, name, ["--version"], BIN_CHECK_TIMEOUT_MS);
|
|
197
244
|
return result.ok;
|
|
198
245
|
}
|
|
199
|
-
|
|
200
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Download a SKILL.md into ~/.agents/skills/<name>/SKILL.md.
|
|
248
|
+
*
|
|
249
|
+
* Tries global fetch first; on failure falls back to curl so environments
|
|
250
|
+
* where fetch fails (no direct network, system proxy that fetch ignores) can
|
|
251
|
+
* still reach the URL — curl honors http_proxy/https_proxy env vars.
|
|
252
|
+
*/
|
|
201
253
|
async function downloadSkillFile(name, url) {
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
204
|
-
|
|
254
|
+
const fetchError = await tryFetchSkillFile(name, url);
|
|
255
|
+
if (fetchError === null) return;
|
|
256
|
+
// curl fallback
|
|
257
|
+
const curlArgs = ["-fsSL", "--max-time", "120", url];
|
|
258
|
+
const { cmd, cmdArgs } = resolveCommand("curl", curlArgs);
|
|
259
|
+
const result = await piExecDownload(cmd, cmdArgs);
|
|
260
|
+
if (result === null) throw new Error(`${toMessage(fetchError)} (curl fallback also failed)`);
|
|
205
261
|
const dir = join(globalSkillsDir(), name);
|
|
206
262
|
mkdirSync(dir, { recursive: true });
|
|
207
|
-
writeFileSync(join(dir, "SKILL.md"),
|
|
263
|
+
writeFileSync(join(dir, "SKILL.md"), result);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Fetch + write the SKILL.md. Returns null on success, the error otherwise. */
|
|
267
|
+
async function tryFetchSkillFile(name, url) {
|
|
268
|
+
try {
|
|
269
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
270
|
+
if (!res.ok) return new Error(`HTTP ${res.status} fetching ${url}`);
|
|
271
|
+
const text = await res.text();
|
|
272
|
+
const dir = join(globalSkillsDir(), name);
|
|
273
|
+
mkdirSync(dir, { recursive: true });
|
|
274
|
+
writeFileSync(join(dir, "SKILL.md"), text);
|
|
275
|
+
return null;
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Fetch a URL body via curl through pi.exec. Returns null on failure. */
|
|
282
|
+
async function piExecDownload(cmd, cmdArgs) {
|
|
283
|
+
if (!piRef) return null;
|
|
284
|
+
try {
|
|
285
|
+
const result = await piRef.exec(cmd, cmdArgs, { timeout: DOWNLOAD_TIMEOUT_MS });
|
|
286
|
+
if (result.code !== 0) return null;
|
|
287
|
+
return result.stdout;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
208
291
|
}
|
|
209
292
|
|
|
210
293
|
function toMessage(err) {
|
|
211
|
-
|
|
294
|
+
const msg = String(err?.message ?? err);
|
|
295
|
+
// "fetch failed" alone is useless — surface the underlying cause
|
|
296
|
+
// (DNS, TLS, connect timeout, …) when Node attached one.
|
|
297
|
+
const cause = err?.cause?.code ?? err?.cause?.message;
|
|
298
|
+
return cause ? `${msg} (${cause})` : msg;
|
|
212
299
|
}
|
|
213
300
|
|
|
214
301
|
function buildReport(report) {
|