Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/cli-node-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- '.github/workflows/cli-node-compatibility.yml'
- 'package-lock.json'
- 'packages/cli/**'
- 'packages/local-setup/**'
- 'packages/shared/**'

concurrency:
Expand Down Expand Up @@ -37,8 +38,10 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Build shared dependency
run: npm run build -w @propr/shared
- name: Build workspace dependencies
run: |
npm run build -w @propr/shared
npm run build -w @propr/local-setup

- name: Run project option regressions
run: >-
Expand Down
16 changes: 11 additions & 5 deletions .github/workflows/pr-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
- name: Build and test the CLI Agent Skill
run: |
npm run build -w @propr/shared
npm run build -w @propr/local-setup
npm run typecheck -w @propr/cli
npx tsx --experimental-test-module-mocks --test \
packages/cli/src/agentSkill.test.ts \
Expand Down Expand Up @@ -82,6 +83,7 @@ jobs:
runuser --user node -- env HOME=/home/node bash -euo pipefail <<'NON_ROOT'
test "$(node -p 'process.geteuid()')" -ne 0
npm run build -w @propr/shared
npm run build -w @propr/local-setup
npx tsx --experimental-test-module-mocks --test \
packages/cli/src/agentSkill.test.ts \
packages/cli/src/agentSkill.forceRace.test.ts \
Expand Down Expand Up @@ -111,6 +113,7 @@ jobs:
test "$(node -p process.platform)" = darwin
test "$(node -p process.arch)" = arm64
npm run build -w @propr/shared
npm run build -w @propr/local-setup
npm run typecheck -w @propr/cli
npx tsx --experimental-test-module-mocks --test \
packages/cli/src/agentSkill.test.ts \
Expand Down Expand Up @@ -281,10 +284,11 @@ jobs:
echo
echo "--- Hosted tunnel regression tests ---"
echo "Running hosted tunnel regression tests..."
# Build @propr/shared first: the tsx and UI tests below import from it,
# so a stale or missing dist in a clean checkout would fail or use old
# output. Build once, up front, before anything that depends on it.
# Build workspace dependencies first: the tsx and UI tests below import
# from them, so a stale or missing dist in a clean checkout would fail
# or use old output. Build once, up front, before their consumers.
npm run build -w @propr/shared
npm run build -w @propr/local-setup
PROPR_DEMO_MODE=true npx tsx --test \
test/orchestratorConfig.test.mjs \
packages/cli/src/commands/setup/engine.test.ts \
Expand Down Expand Up @@ -721,8 +725,10 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Build shared package
run: npm run build --workspace=@propr/shared
- name: Build workspace dependencies
run: |
npm run build --workspace=@propr/shared
npm run build --workspace=@propr/local-setup

- name: Parse init JSON output
run: npx tsx --test packages/cli/src/commands/initCommands.test.ts
Expand Down
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"test": "node --test",
"test:prepare": "npm run build --workspace=packages/shared && npm run build --workspace=packages/core && npm run build --workspace=packages/cli",
"test:prepare": "npm run build --workspace=packages/shared && npm run build --workspace=packages/core && npm run build --workspace=packages/local-setup && npm run build --workspace=packages/cli",
"test:server": "node scripts/run-test-suite.mjs",
"test:full:prepared": "npm run test:server",
"test:full": "npm run test:prepare && npm run test:full:prepared",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json"
},
"dependencies": {
"@propr/local-setup": "^0.8.15",
"@propr/shared": "^0.8.15",
"commander": "^13.1.0",
"dotenv": "^16.5.0",
Expand Down
54 changes: 34 additions & 20 deletions packages/cli/scripts/build-publish.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// Build a standalone, publishable npm package for the CLI.
//
// The in-repo package is the scoped workspace package `@propr/cli`, which depends
// on the workspace package `@propr/shared`. Neither scoped package is published to
// npm, so we ship the CLI under the unscoped public name `propr-cli` with
// `@propr/shared` *vendored* into `dist/vendor/shared/` (it is dependency-free) and
// the two `@propr/shared` imports rewritten to a relative path. The result has no
// scoped dependencies and installs cleanly from the public registry.
// on the workspace packages `@propr/shared` and `@propr/local-setup`. These scoped
// packages are not published to npm, so we ship the CLI under the unscoped public
// name `propr-cli` with both packages vendored into `dist/vendor/` and their imports
// rewritten to relative paths. The result has no scoped dependencies and installs
// cleanly from the public registry.
//
// Usage:
// node scripts/build-publish.mjs # build the staging package + npm pack --dry-run
Expand Down Expand Up @@ -35,6 +35,7 @@ const here = dirname(fileURLToPath(import.meta.url));
const cliDir = resolve(here, "..");
const repoRoot = resolve(cliDir, "..", "..");
const sharedDir = join(repoRoot, "packages", "shared");
const localSetupDir = join(repoRoot, "packages", "local-setup");
const stageDir = join(repoRoot, "dist-publish", "propr-cli");
const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2024.12.2";

Expand Down Expand Up @@ -75,6 +76,7 @@ const buildLauncherManifest = (version) => {

// 1. Build the workspace packages we depend on.
run("npm", ["run", "build", "-w", "@propr/shared"]);
run("npm", ["run", "build", "-w", "@propr/local-setup"]);
run("npm", ["run", "build", "-w", "@propr/cli"]);

// 2. Stage the CLI dist + README.
Expand Down Expand Up @@ -103,12 +105,18 @@ for (const auditedFile of ["directory-operations.c", "README.md"]) {
if (!existsSync(bundled)) throw new Error(`Audited native helper file is missing: ${bundled}`);
}

// 3. Vendor shared's compiled JS (dependency-free) into dist/vendor/shared.
const vendorDir = join(stageDir, "dist", "vendor", "shared");
mkdirSync(vendorDir, { recursive: true });
for (const file of readdirSync(join(sharedDir, "dist"))) {
if (file.endsWith(".js")) {
cpSync(join(sharedDir, "dist", file), join(vendorDir, file));
// 3. Vendor the compiled workspace packages into dist/vendor.
const vendorRoot = join(stageDir, "dist", "vendor");
const vendorPackages = [
{ source: sharedDir, destination: join(vendorRoot, "shared") },
{ source: localSetupDir, destination: join(vendorRoot, "local-setup") },
];
for (const { source, destination } of vendorPackages) {
mkdirSync(destination, { recursive: true });
for (const file of readdirSync(join(source, "dist"))) {
if (file.endsWith(".js")) {
cpSync(join(source, "dist", file), join(destination, file));
}
}
}

Expand All @@ -122,23 +130,29 @@ const stripMaps = (dir) => {
};
stripMaps(join(stageDir, "dist"));

// 5. Rewrite the `@propr/shared` import specifier to the vendored relative path.
const rewriteSharedImports = (dir) => {
// 5. Rewrite private workspace imports to their vendored relative paths.
const vendoredImports = new Map([
["@propr/shared", join(vendorRoot, "shared", "index.js")],
["@propr/local-setup", join(vendorRoot, "local-setup", "index.js")],
]);
const rewriteVendoredImports = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
rewriteSharedImports(full);
rewriteVendoredImports(full);
} else if (entry.name.endsWith(".js")) {
const src = readFileSync(full, "utf8");
if (src.includes('"@propr/shared"')) {
let sharedPath = relative(dirname(full), join(vendorDir, "index.js")).split(sep).join("/");
if (!sharedPath.startsWith(".")) sharedPath = `./${sharedPath}`;
writeFileSync(full, src.replaceAll('"@propr/shared"', `"${sharedPath}"`));
let src = readFileSync(full, "utf8");
for (const [specifier, target] of vendoredImports) {
if (!src.includes(`"${specifier}"`)) continue;
let vendorPath = relative(dirname(full), target).split(sep).join("/");
if (!vendorPath.startsWith(".")) vendorPath = `./${vendorPath}`;
src = src.replaceAll(`"${specifier}"`, `"${vendorPath}"`);
}
writeFileSync(full, src);
}
}
};
rewriteSharedImports(join(stageDir, "dist"));
rewriteVendoredImports(join(stageDir, "dist"));

// 6. Write the unscoped package.json (no scoped deps, no build scripts).
const cliPkg = JSON.parse(readFileSync(join(cliDir, "package.json"), "utf8"));
Expand Down
65 changes: 65 additions & 0 deletions packages/cli/src/commands/setup/agentHostActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import type { AgentSetupActions } from "@propr/local-setup";
import type { ConfigManager } from "../../config/index.js";
import { localhostServiceUrl } from "../../utils/dockerPort.js";

/** Bind the portable agent setup engine to the CLI API and Docker launcher. */
export function createDefaultAgentSetupActions(configManager?: ConfigManager): AgentSetupActions {
const localApiClient = async (rootDir: string): Promise<import("../../api/client.js").ApiClient> => {
const { getHostConfig } = await import("../../orchestrator/index.js");
const { cfg } = await getHostConfig({ configManager, root: rootDir });
const { createApiClient } = await import("../../api/client.js");
return createApiClient({ baseUrl: localhostServiceUrl(cfg.apiPort) });
};

return {
async listAgents(rootDir) {
const { listAgents } = await import("../../api/agents.js");
return (await listAgents(await localApiClient(rootDir))).agents;
},
async addAgent(rootDir, options) {
const { addAgent } = await import("../../api/agents.js");
await addAgent(options, await localApiClient(rootDir));
},
async loginableAgents() {
const { loginableAgents } = await import("../agentValidation.js");
return loginableAgents();
},
async loginAgent(rootDir, type) {
const { getHostConfig } = await import("../../orchestrator/index.js");
const { planAgentLogin } = await import("../agentValidation.js");
const { orch, cfg } = await getHostConfig({ configManager, root: rootDir });
const temporaryRoot = mkdtempSync(join(tmpdir(), "propr-setup-login-"));
const workspaceDir = join(temporaryRoot, "workspace");
mkdirSync(workspaceDir, { recursive: true, mode: 0o700 });
try {
const { plan, error } = planAgentLogin(type, cfg, workspaceDir, orch.validateDockerBindPath);
if (error || !plan) return { available: false, success: false, detail: error };
if (!orch.docker(["images", "-q", plan.image], { capture: true }).stdout.trim()) {
return { available: true, success: false, detail: `image ${plan.image} not present locally — run \`propr images pull\`` };
}
mkdirSync(plan.hostDir, { recursive: true, mode: 0o700 });
const result = spawnSync("docker", plan.dockerArgs, { stdio: "inherit" });
return result.status === 0
? { available: true, success: true, detail: `${type} login finished — credentials written to ${plan.hostDir}` }
: { available: true, success: false, detail: `${type} login exited with code ${result.status ?? "?"}` };
} finally {
rmSync(temporaryRoot, { recursive: true, force: true });
}
},
async validateAgents(rootDir, types) {
const { getHostConfig } = await import("../../orchestrator/index.js");
const { validateAgents } = await import("../agentValidation.js");
const { orch, cfg } = await getHostConfig({ configManager, root: rootDir });
const rows = await validateAgents(orch, cfg, { agents: types, skipHost: true });
return rows.map((row) => ({
type: row.type,
status: row.image.status === "ok" ? "ok" as const : row.image.status === "fail" ? "failed" as const : "skipped" as const,
detail: row.image.detail,
}));
},
};
}
Loading
Loading