feat: package Pi extensions as Bun workspaces
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const inventoryPath = path.join(root, "config", "imported-packages.json");
|
||||
const inventory = JSON.parse(readFileSync(inventoryPath, "utf8"));
|
||||
const [command, requestedName, ...rest] = process.argv.slice(2);
|
||||
|
||||
function usage(message) {
|
||||
if (message) console.error(message);
|
||||
console.error(
|
||||
"Usage:\n" +
|
||||
" bun scripts/import-package-snapshots.mjs check [name|all]\n" +
|
||||
" bun scripts/import-package-snapshots.mjs stage <name|all> --output <empty-directory>",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (!command || !["check", "stage"].includes(command)) usage();
|
||||
const selection = requestedName ?? "all";
|
||||
const packages =
|
||||
selection === "all"
|
||||
? inventory.packages
|
||||
: inventory.packages.filter((entry) => entry.name === selection);
|
||||
if (packages.length === 0) usage(`Unknown imported package: ${selection}`);
|
||||
|
||||
function remoteCommit(entry) {
|
||||
const output = execFileSync(
|
||||
"git",
|
||||
["ls-remote", entry.source, `refs/heads/${entry.branch}`],
|
||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] },
|
||||
).trim();
|
||||
const [commit] = output.split(/\s+/u);
|
||||
if (!/^[0-9a-f]{40}$/u.test(commit ?? "")) {
|
||||
throw new Error(`No ${entry.branch} branch returned for ${entry.name}`);
|
||||
}
|
||||
return commit;
|
||||
}
|
||||
|
||||
if (command === "check") {
|
||||
let stale = false;
|
||||
for (const entry of packages) {
|
||||
const current = remoteCommit(entry);
|
||||
const status = current === entry.commit ? "current" : "update available";
|
||||
console.log(`${entry.name}: ${status} (${entry.commit.slice(0, 7)} -> ${current.slice(0, 7)})`);
|
||||
stale ||= current !== entry.commit;
|
||||
}
|
||||
process.exitCode = stale ? 1 : 0;
|
||||
} else {
|
||||
const outputIndex = rest.indexOf("--output");
|
||||
const outputArg = outputIndex >= 0 ? rest[outputIndex + 1] : undefined;
|
||||
if (!outputArg) usage("stage requires --output <empty-directory>");
|
||||
const outputRoot = path.resolve(root, outputArg);
|
||||
if (existsSync(outputRoot)) usage(`Output already exists: ${outputRoot}`);
|
||||
mkdirSync(outputRoot, { recursive: true });
|
||||
|
||||
for (const entry of packages) {
|
||||
const destination = path.join(outputRoot, entry.name);
|
||||
execFileSync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--quiet",
|
||||
"--depth",
|
||||
"1",
|
||||
"--single-branch",
|
||||
"--branch",
|
||||
entry.branch,
|
||||
entry.source,
|
||||
destination,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const commit = execFileSync("git", ["-C", destination, "rev-parse", "HEAD"], {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
rmSync(path.join(destination, ".git"), { recursive: true, force: true });
|
||||
rmSync(path.join(destination, "package-lock.json"), { force: true });
|
||||
console.log(`${entry.name}: staged ${commit} at ${destination}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const extensionsRoot = path.join(root, "extensions");
|
||||
const registry = "https://git.jb.codes/api/packages/jb/npm/";
|
||||
const expectedWorkspaces = [
|
||||
"copy-all",
|
||||
"dictate",
|
||||
"effort-controls",
|
||||
"file-search",
|
||||
"interactive-subagents",
|
||||
"lm-studio",
|
||||
"native-hooks",
|
||||
"prompt-snippets",
|
||||
"shared",
|
||||
"subagents",
|
||||
"todo-herdr",
|
||||
"update",
|
||||
"workflow",
|
||||
];
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function workspaceManifest(directory) {
|
||||
return readJson(path.join(extensionsRoot, directory, "package.json"));
|
||||
}
|
||||
|
||||
function dependencyText(manifest) {
|
||||
return JSON.stringify({
|
||||
dependencies: manifest.dependencies,
|
||||
optionalDependencies: manifest.optionalDependencies,
|
||||
peerDependencies: manifest.peerDependencies,
|
||||
});
|
||||
}
|
||||
|
||||
test("workspace inventory keeps reference workflows unpublished", () => {
|
||||
const rootManifest = readJson(path.join(root, "package.json"));
|
||||
assert.equal(rootManifest.private, true);
|
||||
assert.deepEqual(rootManifest.workspaces.packages, [
|
||||
"extensions/*",
|
||||
"!extensions/_workflows",
|
||||
]);
|
||||
assert.equal(
|
||||
readdirSync(path.join(extensionsRoot, "_workflows")).includes("package.json"),
|
||||
false,
|
||||
);
|
||||
|
||||
const actual = readdirSync(extensionsRoot)
|
||||
.filter((directory) => {
|
||||
try {
|
||||
workspaceManifest(directory);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.sort();
|
||||
assert.deepEqual(actual, expectedWorkspaces);
|
||||
assert.equal(workspaceManifest("workflow").private, true);
|
||||
});
|
||||
|
||||
test("imported package snapshots retain explicit main-branch provenance", () => {
|
||||
const inventory = readJson(path.join(root, "config", "imported-packages.json"));
|
||||
assert.equal(inventory.version, 1);
|
||||
assert.deepEqual(
|
||||
inventory.packages.map(({ name, branch, target }) => ({ name, branch, target })),
|
||||
[
|
||||
{
|
||||
name: "pi-interactive-subagents",
|
||||
branch: "main",
|
||||
target: "extensions/interactive-subagents",
|
||||
},
|
||||
{
|
||||
name: "pi-native-hooks",
|
||||
branch: "main",
|
||||
target: "extensions/native-hooks",
|
||||
},
|
||||
{
|
||||
name: "pi-todo-herdr",
|
||||
branch: "main",
|
||||
target: "extensions/todo-herdr",
|
||||
},
|
||||
],
|
||||
);
|
||||
for (const entry of inventory.packages) {
|
||||
assert.match(entry.commit, /^[0-9a-f]{40}$/);
|
||||
assert.match(entry.source, /^ssh:\/\/git@git\.jb\.codes:7722\/jb\//);
|
||||
assert.match(
|
||||
readJson(path.join(root, entry.target, "package.json")).name,
|
||||
/^@_jb\/pi-/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("publishable workspaces have Gitea-safe package contracts", () => {
|
||||
for (const directory of expectedWorkspaces) {
|
||||
const packageRoot = path.join(extensionsRoot, directory);
|
||||
const manifest = workspaceManifest(directory);
|
||||
if (manifest.private) continue;
|
||||
|
||||
assert.match(manifest.name, /^@_jb\/pi-[a-z0-9-]+$/);
|
||||
assert.equal(manifest.publishConfig?.registry, registry);
|
||||
assert.equal(manifest.engines?.node, ">=22.19.0");
|
||||
assert.equal(readFileSync(path.join(packageRoot, "LICENSE"), "utf8").length > 0, true);
|
||||
assert.equal(readFileSync(path.join(packageRoot, "README.md"), "utf8").length > 0, true);
|
||||
|
||||
for (const entry of manifest.pi?.extensions ?? []) {
|
||||
assert.equal(readFileSync(path.resolve(packageRoot, entry), "utf8").length > 0, true);
|
||||
}
|
||||
for (const entry of manifest.pi?.skills ?? []) {
|
||||
assert.equal(readFileSync(path.resolve(packageRoot, entry, "SKILL.md"), "utf8").length > 0, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Bun packs concrete dependency versions and excludes tests", () => {
|
||||
const outputRoot = mkdtempSync(path.join(tmpdir(), "harness-tools-pack-"));
|
||||
try {
|
||||
for (const directory of expectedWorkspaces) {
|
||||
const manifest = workspaceManifest(directory);
|
||||
if (manifest.private) continue;
|
||||
const packageRoot = path.join(extensionsRoot, directory);
|
||||
const before = new Set(readdirSync(outputRoot));
|
||||
execFileSync(
|
||||
"bun",
|
||||
["pm", "pack", "--destination", outputRoot, "--ignore-scripts", "--quiet"],
|
||||
{ cwd: packageRoot, stdio: "pipe" },
|
||||
);
|
||||
const created = readdirSync(outputRoot).filter((entry) => !before.has(entry));
|
||||
assert.equal(created.length, 1, `${manifest.name} should create one tarball`);
|
||||
const tarball = path.join(outputRoot, created[0]);
|
||||
const packedManifest = JSON.parse(
|
||||
execFileSync("tar", ["-xOf", tarball, "package/package.json"], {
|
||||
encoding: "utf8",
|
||||
}),
|
||||
);
|
||||
assert.equal(dependencyText(packedManifest).includes("catalog:"), false);
|
||||
assert.equal(dependencyText(packedManifest).includes("workspace:"), false);
|
||||
const files = execFileSync("tar", ["-tzf", tarball], { encoding: "utf8" });
|
||||
assert.match(files, /^package\/LICENSE$/m);
|
||||
assert.match(files, /^package\/README\.md$/m);
|
||||
assert.doesNotMatch(files, /\.(?:test|spec)\.ts$/m);
|
||||
}
|
||||
} finally {
|
||||
rmSync(outputRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -246,7 +246,7 @@ test("normal exposure and package evidence retain the Slice 0 security boundary"
|
||||
await readFile(path.join(repositoryRoot, "config", "pi-links.json"), "utf8"),
|
||||
);
|
||||
assert.equal(links.extensions.globalSymlinks.update, undefined);
|
||||
assert.equal(links.extensions.globalSymlinks["pi-workflow"], undefined);
|
||||
assert.equal(links.extensions.globalSymlinks["workflow"], undefined);
|
||||
|
||||
const lock = JSON.parse(
|
||||
await readFile(
|
||||
|
||||
Reference in New Issue
Block a user