@pzy560117/codex-harness 0.1.0

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/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Codex Harness CLI
2
+
3
+ Thin CLI for downloading a Codex Harness package source and invoking the PowerShell runtime/install scripts.
4
+
5
+ ## Commands
6
+
7
+ ```powershell
8
+ harness init
9
+ harness doctor
10
+ harness verify
11
+ harness run
12
+ ```
13
+
14
+ ## Release Assets
15
+
16
+ GitHub Release assets are built from the repository package source with:
17
+
18
+ ```powershell
19
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\release\build-release.ps1
20
+ ```
package/bin/harness.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../lib/main.js";
3
+
4
+ runCli(process.argv).catch((error) => {
5
+ console.error(error?.message || String(error));
6
+ process.exit(error?.exitCode || 1);
7
+ });
@@ -0,0 +1,12 @@
1
+ import path from "node:path";
2
+
3
+ import { detectProjectRoot } from "../project/detect-project-root.js";
4
+ import { assertInitialized } from "../project/assert-initialized.js";
5
+ import { invokePowerShellScript } from "../powershell/invoke-script.js";
6
+
7
+ export async function doctorCommand(options) {
8
+ const projectRoot = detectProjectRoot(options.projectRoot);
9
+ assertInitialized(projectRoot);
10
+ const scriptPath = path.join(projectRoot, "tools", "harness", "doctor.ps1");
11
+ await invokePowerShellScript(scriptPath, ["-ProjectRoot", projectRoot], { cwd: projectRoot });
12
+ }
@@ -0,0 +1,99 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+
5
+ import { detectProjectRoot } from "../project/detect-project-root.js";
6
+ import { invokePowerShellScript } from "../powershell/invoke-script.js";
7
+ import {
8
+ getDownloadedManifestPath,
9
+ getDownloadedShaPath,
10
+ getDownloadedZipPath,
11
+ getPackageInstallRoot,
12
+ getReleaseAssetNames
13
+ } from "../release/cache-layout.js";
14
+ import { downloadToFile, fileExists } from "../release/download-release.js";
15
+ import { buildReleaseAssetUrl } from "../release/release-config.js";
16
+ import { getReleasePlatformEntry } from "../release/release-manifest.js";
17
+ import { resolvePackageSourceRoot } from "../release/resolve-package-source.js";
18
+ import { resolveVersion } from "../release/resolve-version.js";
19
+ import { unpackZip } from "../release/unpack-zip.js";
20
+ import { verifyFileSha256 } from "../release/verify-sha256.js";
21
+
22
+ function getLocalPackageRoot() {
23
+ return path.resolve(import.meta.dirname, "..", "..", "..");
24
+ }
25
+
26
+ export async function initCommand(options) {
27
+ const localPackageRoot = getLocalPackageRoot();
28
+ const projectRoot = detectProjectRoot(options.projectRoot);
29
+ const userProfile = process.env.USERPROFILE;
30
+ if (!userProfile) {
31
+ throw new Error("USERPROFILE is not set.");
32
+ }
33
+
34
+ const packageRoot = resolvePackageSourceRoot({
35
+ packageRoot: localPackageRoot,
36
+ cachedRoot: "",
37
+ fsLike: { existsSync: fileExists }
38
+ });
39
+
40
+ let effectivePackageRoot = packageRoot;
41
+ if (effectivePackageRoot === "") {
42
+ const version = await resolveVersion(options.version);
43
+ const manifestPath = getDownloadedManifestPath(userProfile, version);
44
+ const fallbackAssets = getReleaseAssetNames(version);
45
+
46
+ if (!fileExists(manifestPath)) {
47
+ await downloadToFile(buildReleaseAssetUrl({ version, assetName: fallbackAssets.manifest }), manifestPath);
48
+ }
49
+
50
+ const releaseManifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
51
+ const releaseAssets = getReleasePlatformEntry(releaseManifest);
52
+ if (releaseAssets.version !== version) {
53
+ throw new Error(`Release manifest version mismatch: expected ${version}, got ${releaseAssets.version}`);
54
+ }
55
+
56
+ const packageInstallRoot = getPackageInstallRoot(userProfile, releaseAssets.version);
57
+ const zipPath = getDownloadedZipPath(userProfile, releaseAssets.version);
58
+ const shaPath = getDownloadedShaPath(userProfile, releaseAssets.version);
59
+
60
+ if (!fileExists(zipPath)) {
61
+ await downloadToFile(buildReleaseAssetUrl({ version, assetName: releaseAssets.zip }), zipPath);
62
+ }
63
+
64
+ if (!fileExists(shaPath)) {
65
+ await downloadToFile(buildReleaseAssetUrl({ version, assetName: releaseAssets.sha256File }), shaPath);
66
+ }
67
+
68
+ await verifyFileSha256(zipPath, shaPath, releaseAssets.sha256);
69
+
70
+ if (!fileExists(path.join(packageInstallRoot, "tools", "install", "install-agent.ps1"))) {
71
+ await unpackZip(zipPath, packageInstallRoot);
72
+ }
73
+
74
+ effectivePackageRoot = packageInstallRoot;
75
+ }
76
+
77
+ const scriptPath = path.join(effectivePackageRoot, "tools", "install", "install-agent.ps1");
78
+ const args = ["-ProjectRoot", projectRoot];
79
+
80
+ if (options.vendor) {
81
+ args.push("-Scope", "vendor");
82
+ } else {
83
+ args.push("-Scope", "project");
84
+ }
85
+
86
+ if (options.force) {
87
+ args.push("-Force");
88
+ }
89
+
90
+ if (options.initGit) {
91
+ args.push("-InitGitIfNeeded");
92
+ }
93
+
94
+ if (options.plan) {
95
+ args.push("-Plan");
96
+ }
97
+
98
+ await invokePowerShellScript(scriptPath, args, { cwd: effectivePackageRoot });
99
+ }
@@ -0,0 +1,22 @@
1
+ import path from "node:path";
2
+
3
+ import { detectProjectRoot } from "../project/detect-project-root.js";
4
+ import { assertInitialized } from "../project/assert-initialized.js";
5
+ import { invokePowerShellScript } from "../powershell/invoke-script.js";
6
+
7
+ export async function runCommand(options) {
8
+ const projectRoot = detectProjectRoot(options.projectRoot);
9
+ assertInitialized(projectRoot);
10
+ const scriptPath = path.join(projectRoot, "tools", "harness", "codex-loop.ps1");
11
+ const args = ["-ProjectRoot", projectRoot];
12
+
13
+ if (options.taskFile) {
14
+ args.push("-TaskFile", options.taskFile);
15
+ }
16
+
17
+ if (options.runUntilDone) {
18
+ args.push("-RunUntilDone");
19
+ }
20
+
21
+ await invokePowerShellScript(scriptPath, args, { cwd: projectRoot });
22
+ }
@@ -0,0 +1,12 @@
1
+ import path from "node:path";
2
+
3
+ import { detectProjectRoot } from "../project/detect-project-root.js";
4
+ import { assertInitialized } from "../project/assert-initialized.js";
5
+ import { invokePowerShellScript } from "../powershell/invoke-script.js";
6
+
7
+ export async function verifyCommand(options) {
8
+ const projectRoot = detectProjectRoot(options.projectRoot);
9
+ assertInitialized(projectRoot);
10
+ const scriptPath = path.join(projectRoot, "tools", "harness", "verify.ps1");
11
+ await invokePowerShellScript(scriptPath, ["-ProjectRoot", projectRoot], { cwd: projectRoot });
12
+ }
package/lib/main.js ADDED
@@ -0,0 +1,62 @@
1
+ import { initCommand } from "./commands/init.js";
2
+ import { doctorCommand } from "./commands/doctor.js";
3
+ import { verifyCommand } from "./commands/verify.js";
4
+ import { runCommand } from "./commands/run.js";
5
+
6
+ function normalizeOptionName(name) {
7
+ return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
8
+ }
9
+
10
+ export function parseCliArgs(argv) {
11
+ const args = [...argv].slice(2);
12
+ const command = args.shift() || "help";
13
+ const options = {};
14
+
15
+ while (args.length > 0) {
16
+ const token = args.shift();
17
+ if (!token.startsWith("--")) {
18
+ continue;
19
+ }
20
+
21
+ const rawName = token.slice(2);
22
+ const optionName = normalizeOptionName(rawName);
23
+ if (args.length === 0 || args[0].startsWith("--")) {
24
+ options[optionName] = true;
25
+ continue;
26
+ }
27
+
28
+ options[optionName] = args.shift();
29
+ }
30
+
31
+ return { command, options };
32
+ }
33
+
34
+ export async function runCli(argv) {
35
+ const { command, options } = parseCliArgs(argv);
36
+
37
+ switch (command) {
38
+ case "init":
39
+ return initCommand(options);
40
+ case "doctor":
41
+ return doctorCommand(options);
42
+ case "verify":
43
+ return verifyCommand(options);
44
+ case "run":
45
+ return runCommand(options);
46
+ case "help":
47
+ default:
48
+ printHelp();
49
+ return;
50
+ }
51
+ }
52
+
53
+ function printHelp() {
54
+ console.log(`Codex Harness CLI
55
+
56
+ Usage:
57
+ harness init [--version <version>] [--project-root <path>] [--vendor] [--force] [--init-git] [--plan]
58
+ harness doctor [--project-root <path>]
59
+ harness verify [--project-root <path>]
60
+ harness run [--project-root <path>] [--task-file <path>] [--run-until-done]
61
+ `);
62
+ }
@@ -0,0 +1,3 @@
1
+ export function findPowerShell() {
2
+ return "powershell";
3
+ }
@@ -0,0 +1,34 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import { findPowerShell } from "./find-powershell.js";
4
+
5
+ export function invokePowerShellScript(scriptPath, args = [], options = {}) {
6
+ const powershell = findPowerShell();
7
+ const commandArgs = [
8
+ "-NoProfile",
9
+ "-ExecutionPolicy",
10
+ "Bypass",
11
+ "-File",
12
+ scriptPath,
13
+ ...args
14
+ ];
15
+
16
+ return new Promise((resolve, reject) => {
17
+ const child = spawn(powershell, commandArgs, {
18
+ cwd: options.cwd,
19
+ stdio: "inherit"
20
+ });
21
+
22
+ child.on("error", reject);
23
+ child.on("exit", (code) => {
24
+ if (code === 0) {
25
+ resolve();
26
+ return;
27
+ }
28
+
29
+ const error = new Error(`PowerShell script failed with exit code ${code}: ${scriptPath}`);
30
+ error.exitCode = code || 1;
31
+ reject(error);
32
+ });
33
+ });
34
+ }
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function assertInitialized(projectRoot) {
5
+ const requiredPaths = [
6
+ "tools/harness/doctor.ps1",
7
+ "tools/harness/verify.ps1",
8
+ "tools/harness/codex-loop.ps1",
9
+ "task.json"
10
+ ];
11
+
12
+ for (const relativePath of requiredPaths) {
13
+ const fullPath = path.join(projectRoot, relativePath);
14
+ if (!fs.existsSync(fullPath)) {
15
+ const error = new Error(`Project is not initialized. Missing: ${relativePath}. Run 'harness init'.`);
16
+ error.exitCode = 2;
17
+ throw error;
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ import process from "node:process";
2
+
3
+ export function detectProjectRoot(projectRoot) {
4
+ return projectRoot || process.cwd();
5
+ }
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import { RELEASE_MANIFEST_FILE } from "./release-manifest.js";
3
+
4
+ export function getPackageCacheRoot(userProfile) {
5
+ return path.win32.join(userProfile, ".codex", "packages", "codex-harness");
6
+ }
7
+
8
+ export function getPackageInstallRoot(userProfile, version) {
9
+ return path.win32.join(getPackageCacheRoot(userProfile), version);
10
+ }
11
+
12
+ export function getUserPackageLockPath(userProfile) {
13
+ return path.win32.join(getPackageCacheRoot(userProfile), "package.lock.json");
14
+ }
15
+
16
+ export function getReleaseAssetNames(version) {
17
+ return {
18
+ zip: `codex-harness-${version}-win.zip`,
19
+ sha256: `codex-harness-${version}-win.sha256`,
20
+ manifest: RELEASE_MANIFEST_FILE
21
+ };
22
+ }
23
+
24
+ export function getDownloadedZipPath(userProfile, version) {
25
+ return path.win32.join(getPackageCacheRoot(userProfile), getReleaseAssetNames(version).zip);
26
+ }
27
+
28
+ export function getDownloadedShaPath(userProfile, version) {
29
+ return path.win32.join(getPackageCacheRoot(userProfile), getReleaseAssetNames(version).sha256);
30
+ }
31
+
32
+ export function getDownloadedManifestPath(userProfile, version) {
33
+ return path.win32.join(getPackageCacheRoot(userProfile), `release-manifest-${version}.json`);
34
+ }
@@ -0,0 +1,25 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+
5
+ export async function downloadToFile(url, destinationPath) {
6
+ const response = await fetch(url, {
7
+ headers: {
8
+ "User-Agent": "codex-harness-cli"
9
+ },
10
+ redirect: "follow"
11
+ });
12
+
13
+ if (!response.ok) {
14
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
15
+ }
16
+
17
+ await mkdir(path.dirname(destinationPath), { recursive: true });
18
+ const buffer = Buffer.from(await response.arrayBuffer());
19
+ await writeFile(destinationPath, buffer);
20
+ return destinationPath;
21
+ }
22
+
23
+ export function fileExists(pathLike) {
24
+ return fs.existsSync(pathLike);
25
+ }
@@ -0,0 +1,13 @@
1
+ export const RELEASE_SOURCE_ENTRIES = [
2
+ "AGENTS.md",
3
+ "README.md",
4
+ "PACKAGE.md",
5
+ "install-manifest.json",
6
+ "install-manifest.schema.json",
7
+ "tools/install",
8
+ "docs/codex-harness-engineering/templates"
9
+ ];
10
+
11
+ export function shouldIncludeReleaseSource(relativePath) {
12
+ return RELEASE_SOURCE_ENTRIES.includes(relativePath);
13
+ }
@@ -0,0 +1,6 @@
1
+ export const DEFAULT_RELEASE_OWNER = "pzy560117";
2
+ export const DEFAULT_RELEASE_REPO = "codex-herness-agent";
3
+
4
+ export function buildReleaseAssetUrl({ owner = DEFAULT_RELEASE_OWNER, repo = DEFAULT_RELEASE_REPO, version, assetName }) {
5
+ return `https://github.com/${owner}/${repo}/releases/download/v${version}/${assetName}`;
6
+ }
@@ -0,0 +1,76 @@
1
+ export const RELEASE_MANIFEST_FILE = "release-manifest.json";
2
+ export const DEFAULT_RELEASE_PLATFORM = "win32-x64";
3
+
4
+ export function createReleaseManifest({
5
+ packageName = "codex-harness",
6
+ version,
7
+ zipFile,
8
+ sha256File,
9
+ sha256,
10
+ size,
11
+ releaseTag = `v${version}`,
12
+ generatedAt,
13
+ owner = "",
14
+ repo = "",
15
+ sourceCommit = ""
16
+ }) {
17
+ if (!version) {
18
+ throw new Error("release manifest version is required.");
19
+ }
20
+
21
+ if (!zipFile || !sha256File || !sha256) {
22
+ throw new Error("release manifest assets are incomplete.");
23
+ }
24
+
25
+ return {
26
+ package: packageName,
27
+ version,
28
+ releaseTag,
29
+ generatedAt: generatedAt ?? new Date().toISOString(),
30
+ source: {
31
+ owner,
32
+ repo,
33
+ commit: sourceCommit
34
+ },
35
+ platforms: {
36
+ [DEFAULT_RELEASE_PLATFORM]: {
37
+ zip: zipFile,
38
+ sha256File,
39
+ sha256,
40
+ size
41
+ }
42
+ }
43
+ };
44
+ }
45
+
46
+ export function getReleasePlatformEntry(manifest, platform = DEFAULT_RELEASE_PLATFORM) {
47
+ if (!manifest || typeof manifest !== "object") {
48
+ throw new Error("release manifest is missing.");
49
+ }
50
+
51
+ if (String(manifest.package || "").trim() !== "codex-harness") {
52
+ throw new Error("release manifest package must be codex-harness.");
53
+ }
54
+
55
+ const version = String(manifest.version || "").trim();
56
+ if (!version) {
57
+ throw new Error("release manifest version is missing.");
58
+ }
59
+
60
+ const entry = manifest.platforms?.[platform];
61
+ if (!entry) {
62
+ throw new Error(`release manifest does not include platform ${platform}.`);
63
+ }
64
+
65
+ if (!entry.zip || !entry.sha256File || !entry.sha256) {
66
+ throw new Error(`release manifest platform ${platform} is incomplete.`);
67
+ }
68
+
69
+ return {
70
+ version,
71
+ zip: entry.zip,
72
+ sha256File: entry.sha256File,
73
+ sha256: entry.sha256,
74
+ size: entry.size ?? null
75
+ };
76
+ }
@@ -0,0 +1,16 @@
1
+ import path from "node:path";
2
+
3
+ export function resolvePackageSourceRoot({ packageRoot, cachedRoot, fsLike }) {
4
+ const existsSync = fsLike?.existsSync ?? (() => false);
5
+ const localScript = path.win32.join(packageRoot, "tools", "install", "install-agent.ps1");
6
+ if (existsSync(localScript)) {
7
+ return packageRoot;
8
+ }
9
+
10
+ const cachedScript = path.win32.join(cachedRoot, "tools", "install", "install-agent.ps1");
11
+ if (existsSync(cachedScript)) {
12
+ return cachedRoot;
13
+ }
14
+
15
+ return "";
16
+ }
@@ -0,0 +1,26 @@
1
+ import { DEFAULT_RELEASE_OWNER, DEFAULT_RELEASE_REPO } from "./release-config.js";
2
+
3
+ export async function resolveVersion(requestedVersion) {
4
+ if (requestedVersion && requestedVersion !== "latest") {
5
+ return requestedVersion;
6
+ }
7
+
8
+ const apiUrl = `https://api.github.com/repos/${DEFAULT_RELEASE_OWNER}/${DEFAULT_RELEASE_REPO}/releases/latest`;
9
+ const response = await fetch(apiUrl, {
10
+ headers: {
11
+ "User-Agent": "codex-harness-cli"
12
+ }
13
+ });
14
+
15
+ if (!response.ok) {
16
+ throw new Error(`Failed to resolve latest release version: ${response.status} ${response.statusText}`);
17
+ }
18
+
19
+ const json = await response.json();
20
+ const tag = String(json.tag_name || "").trim();
21
+ if (!tag) {
22
+ throw new Error("Latest release did not include tag_name.");
23
+ }
24
+
25
+ return tag.startsWith("v") ? tag.slice(1) : tag;
26
+ }
@@ -0,0 +1,25 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { invokePowerShellScript } from "../powershell/invoke-script.js";
5
+
6
+ export async function unpackZip(zipPath, destinationRoot) {
7
+ if (!fs.existsSync(destinationRoot)) {
8
+ fs.mkdirSync(destinationRoot, { recursive: true });
9
+ }
10
+
11
+ const scriptPath = path.join(destinationRoot, "__expand-archive.ps1");
12
+ fs.writeFileSync(
13
+ scriptPath,
14
+ `Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationRoot.replace(/'/g, "''")}' -Force`,
15
+ "utf8"
16
+ );
17
+
18
+ try {
19
+ await invokePowerShellScript(scriptPath, [], { cwd: destinationRoot });
20
+ } finally {
21
+ if (fs.existsSync(scriptPath)) {
22
+ fs.unlinkSync(scriptPath);
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,16 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+
4
+ export async function verifyFileSha256(filePath, shaFilePath, expectedSha256 = "") {
5
+ const fileBuffer = await fs.readFile(filePath);
6
+ const expected = (await fs.readFile(shaFilePath, "utf8")).trim().split(/\s+/)[0];
7
+ const actual = crypto.createHash("sha256").update(fileBuffer).digest("hex");
8
+
9
+ if (expected.toLowerCase() !== actual.toLowerCase()) {
10
+ throw new Error(`SHA256 mismatch for ${filePath}`);
11
+ }
12
+
13
+ if (expectedSha256 && expectedSha256.toLowerCase() !== actual.toLowerCase()) {
14
+ throw new Error(`Release manifest SHA256 mismatch for ${filePath}`);
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@pzy560117/codex-harness",
3
+ "version": "0.1.0",
4
+ "description": "Codex Harness installer and project runtime CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "harness": "./bin/harness.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test",
19
+ "pack:check": "npm pack --dry-run"
20
+ }
21
+ }