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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ jobs:
npm ci
npm run build

- name: Test npm runtime
working-directory: npm
run: npm test

- name: Run conformance suite (both runtimes)
run: |
python -m spec.conformance.harness.harness \
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ jobs:
- name: Install dependencies
run: npm ci

# No Jest test files exist under npm/ yet (the node runtime is covered by
# the conformance harness in ci.yml); `npm test` would fail with "No
# tests found". Typecheck instead; switch to `npm test` when tests land.
- name: Typecheck
run: npm run lint

- name: Test
run: npm test

# No token: with a trusted publisher configured on npmjs.com for this
# repo + workflow, npm publish authenticates via the job's OIDC identity
# and generates provenance automatically. prepublishOnly runs the build.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **npm CLI accepts a bare spec path** — `oa validate <spec.yaml>` and `oa run <spec.yaml>` now work without `--spec` in the npm runtime, matching the Python CLI (1.6.0). Same guardrails: `--spec` unchanged, bare path + `--spec` together is an explicit error, and a non-YAML bare argument gets a clear error naming the valid forms. First Jest tests land with this (`npm/tests/`), and both CI and the npm publish workflow now run them. (#100)

### Added (older, pre-1.4 notes)
- This changelog.
- **Agents-as-code documentation** — new section in REFERENCE.md explaining the `.agents/` pattern, bundled examples table, and scaffold/run/generate workflows.
Expand Down
4 changes: 2 additions & 2 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ npm install -g @prime-vector/open-agent-spec
Or run without installing:

```bash
npx @prime-vector/open-agent-spec run --spec agent.yaml --task summarise --input input.json
npx @prime-vector/open-agent-spec run agent.yaml --task summarise --input input.json
```

## Quick start
Expand Down Expand Up @@ -48,7 +48,7 @@ tasks:
```bash
export OPENAI_API_KEY=sk-...

oa run --spec agent.yaml --task summarise --input '{"text": "Open Agent Spec is a YAML standard for declarative AI agents."}'
oa run agent.yaml --task summarise --input '{"text": "Open Agent Spec is a YAML standard for declarative AI agents."}'
```

**3. Output**
Expand Down
8 changes: 8 additions & 0 deletions npm/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('jest').Config} */
export default {
testEnvironment: "node",
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: { "^(\\.{1,2}/.*)\\.js$": "$1" },
transform: { "^.+\\.ts$": ["ts-jest", { useESM: true }] },
testMatch: ["**/tests/**/*.test.ts"],
};
4 changes: 2 additions & 2 deletions npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"homepage": "https://openagentspec.dev",
"repository": {
"type": "git",
"url": "https://github.com/prime-vector/open-agent-spec"
"url": "git+https://github.com/prime-vector/open-agent-spec.git"
},
"license": "MIT",
"publishConfig": {
Expand All @@ -31,7 +31,7 @@
}
},
"bin": {
"oa": "./bin/oa.js"
"oa": "bin/oa.js"
},
"files": [
"dist",
Expand Down
53 changes: 46 additions & 7 deletions npm/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,42 @@ function parseInput(raw: string): RunInput {
return { text: contents };
}

// Reconcile the bare positional spec path with --spec (#94/#100 parity with
// the Python CLI): either form alone is fine, both is an explicit error, and a
// bare argument must look like a YAML path so mistakes get a clear message
// instead of a confusing load failure.
export function reconcileSpecArgs(
cmd: string,
specArg: string | undefined,
specOpt: string | undefined,
): { spec: string } | { error: string } {
const forms = `Valid forms: 'oa ${cmd} <spec.yaml>' or 'oa ${cmd} --spec <path>'.`;
if (specArg !== undefined && specOpt !== undefined) {
return { error: "Pass the spec path either as a bare argument or with --spec, not both." };
}
if (specArg !== undefined && !/\.ya?ml$/i.test(specArg)) {
return { error: `'${specArg}' does not look like a spec YAML path. ${forms}` };
}
const spec = specArg ?? specOpt;
if (spec === undefined) {
return { error: `Missing spec path. ${forms}` };
}
return { spec };
}

function resolveSpecOrExit(
cmd: string,
specArg: string | undefined,
specOpt: string | undefined,
): string {
const result = reconcileSpecArgs(cmd, specArg, specOpt);
if ("error" in result) {
console.error(`Error: ${result.error}`);
process.exit(1);
}
return result.spec;
}

export function createCli(): Command {
const program = new Command();

Expand All @@ -74,9 +110,10 @@ export function createCli(): Command {
program
.command("validate")
.description("Validate a spec file against the Open Agent Spec schema (no model calls).")
.requiredOption("--spec <path>", "Path to the spec YAML file")
.action((opts: { spec: string }) => {
const specPath = resolve(opts.spec);
.argument("[spec]", "Path to the spec YAML file (shorthand for --spec)")
.option("--spec <path>", "Path to the spec YAML file")
.action((specArg: string | undefined, opts: { spec?: string }) => {
const specPath = resolve(resolveSpecOrExit("validate", specArg, opts.spec));
try {
loadSpecFromFile(specPath);
} catch (err) {
Expand All @@ -93,21 +130,23 @@ export function createCli(): Command {
program
.command("run")
.description("Run a task from an OA spec file.")
.requiredOption("--spec <path>", "Path to the spec YAML file")
.argument("[spec]", "Path to the spec YAML file (shorthand for --spec)")
.option("--spec <path>", "Path to the spec YAML file")
.option("--task <name>", "Task name to run (defaults to the only task if there is one)")
.option("--input <json-or-file>", "Input as a JSON string, a .json file path, or a plain text file path")
.option("--quiet", "Output only JSON (no decorative logging)")
.action(async (opts: {
spec: string;
.action(async (specArg: string | undefined, opts: {
spec?: string;
task?: string;
input?: string;
quiet?: boolean;
}) => {
const specPath = resolve(resolveSpecOrExit("run", specArg, opts.spec));
const input: RunInput = opts.input ? parseInput(opts.input) : {};

try {
const result = await runTask({
specPath: resolve(opts.spec),
specPath,
taskName: opts.task,
input,
});
Expand Down
43 changes: 43 additions & 0 deletions npm/tests/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, test } from "@jest/globals";
import { reconcileSpecArgs } from "../src/cli.js";

describe("reconcileSpecArgs (#100 — bare spec path parity with the Python CLI)", () => {
test("bare .yaml path is accepted", () => {
expect(reconcileSpecArgs("validate", "agent.yaml", undefined)).toEqual({ spec: "agent.yaml" });
});

test("bare .yml path is accepted, case-insensitively", () => {
expect(reconcileSpecArgs("run", "Agent.YML", undefined)).toEqual({ spec: "Agent.YML" });
});

test("--spec alone still works", () => {
expect(reconcileSpecArgs("validate", undefined, "agent.yaml")).toEqual({ spec: "agent.yaml" });
});

test("--spec is not suffix-gated (any path allowed, as before)", () => {
expect(reconcileSpecArgs("run", undefined, "specs/agent.config")).toEqual({
spec: "specs/agent.config",
});
});

test("bare path and --spec together is an explicit error", () => {
const result = reconcileSpecArgs("validate", "a.yaml", "b.yaml");
expect(result).toHaveProperty("error");
expect((result as { error: string }).error).toContain("not both");
});

test("non-YAML bare argument errors and names the valid forms", () => {
const result = reconcileSpecArgs("run", "notes.txt", undefined);
expect(result).toHaveProperty("error");
const error = (result as { error: string }).error;
expect(error).toContain("notes.txt");
expect(error).toContain("oa run <spec.yaml>");
expect(error).toContain("--spec");
});

test("neither form errors and names the valid forms", () => {
const result = reconcileSpecArgs("validate", undefined, undefined);
expect(result).toHaveProperty("error");
expect((result as { error: string }).error).toContain("oa validate <spec.yaml>");
});
});
Loading