Files
openclaw/src/infra/install-flow.ts
samzong d832ad214c [Feat] Add upload archive install RPC (#74430)
* feat(skills): add upload archive install RPC

- src/agents/skills-archive-install.ts:83 [BOT-SCOPE]: `withExtractedArchiveRoot()` still returns unstructured extract failures, so exact transient-vs-terminal classification should be moved into the shared install-flow layer in a follow-up rather than expanding this PR.

Signed-off-by: samzong <samzong.lu@gmail.com>

* fix(skills): address archive upload review findings

Signed-off-by: samzong <samzong.lu@gmail.com>

* fix(skills): regen protocol bindings and classify transient archive errors

* feat: gate uploaded skill installs by config

* test: add docker skill install proof

* docs: clarify uploaded skill archive gate

* chore: refresh config docs baseline

* style: format docker e2e plan test

* fix: use fs-safe path checks for skill archives

* fix: classify skill publish failures as unavailable

* test: update skill clawhub path mock

* fix: pass mutable archive root markers

* fix: use current json dir mode option

* test: satisfy skill upload lint

* test: refresh core support expectations

---------

Signed-off-by: samzong <samzong.lu@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-09 20:44:18 -04:00

66 lines
2.0 KiB
TypeScript

import type { Stats } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { resolveUserPath } from "../utils.js";
import { type ArchiveLogger, extractArchive, resolvePackedRootDir } from "./archive.js";
import { pathExists } from "./fs-safe.js";
import { withTempDir } from "./install-source-utils.js";
type ExistingInstallPathResult =
| {
ok: true;
resolvedPath: string;
stat: Stats;
}
| {
ok: false;
error: string;
};
export async function resolveExistingInstallPath(
inputPath: string,
): Promise<ExistingInstallPathResult> {
const resolvedPath = resolveUserPath(inputPath);
if (!(await pathExists(resolvedPath))) {
return { ok: false, error: `path not found: ${resolvedPath}` };
}
const stat = await fs.stat(resolvedPath);
return { ok: true, resolvedPath, stat };
}
export async function withExtractedArchiveRoot<TResult extends { ok: boolean }>(params: {
archivePath: string;
tempDirPrefix: string;
timeoutMs: number;
logger?: ArchiveLogger;
rootMarkers?: readonly string[];
onExtracted: (rootDir: string) => Promise<TResult>;
}): Promise<TResult | { ok: false; error: string }> {
return await withTempDir(params.tempDirPrefix, async (tmpDir) => {
const extractDir = path.join(tmpDir, "extract");
await fs.mkdir(extractDir, { recursive: true });
params.logger?.info?.(`Extracting ${params.archivePath}`);
try {
await extractArchive({
archivePath: params.archivePath,
destDir: extractDir,
timeoutMs: params.timeoutMs,
logger: params.logger,
});
} catch (err) {
return { ok: false, error: `failed to extract archive: ${String(err)}` };
}
let rootDir = "";
try {
rootDir = await resolvePackedRootDir(extractDir, {
rootMarkers: params.rootMarkers ? [...params.rootMarkers] : undefined,
});
} catch (err) {
return { ok: false, error: String(err) };
}
return await params.onExtracted(rootDir);
});
}