Upgrade CLI to v0.2.0: add validate, list commands, --version, --dry-run, --backup flags
- Fix npm test glob pattern for Node >= 21 compatibility - Add --version/-v flag to display package version - Add validate command: run 78 profile checks (files, skills, frontmatter, legacy patterns, AGENTS mapping) - Add list command: display all 13 bundled skills with descriptions - Add --dry-run/-n flag for init: preview files without copying - Add --backup/-b flag for init --force: backup existing .agent before overwrite - Add name field to workflow frontmatter (brainstorm, execute-plan, write-plan) - Expand smoke check to cover all 13 skills and new command files - Increase test coverage from 3 to 20 tests (cli, init, validate) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
61
tests/cli.test.mjs
Normal file
61
tests/cli.test.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
import { resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const cliPath = resolve(process.cwd(), "bin/antigravity-superpowers.js");
|
||||
|
||||
function runCli(args) {
|
||||
return spawnSync(process.execPath, [cliPath, ...args], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
test("--help shows usage information", () => {
|
||||
const result = runCli(["--help"]);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /Usage:/);
|
||||
assert.match(result.stdout, /Commands:/);
|
||||
assert.match(result.stdout, /init/);
|
||||
assert.match(result.stdout, /validate/);
|
||||
assert.match(result.stdout, /list/);
|
||||
});
|
||||
|
||||
test("-h shows usage information", () => {
|
||||
const result = runCli(["-h"]);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /Usage:/);
|
||||
});
|
||||
|
||||
test("no arguments shows help", () => {
|
||||
const result = runCli([]);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /Usage:/);
|
||||
});
|
||||
|
||||
test("--version shows version number", () => {
|
||||
const result = runCli(["--version"]);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout.trim(), /^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
test("-v shows version number", () => {
|
||||
const result = runCli(["-v"]);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout.trim(), /^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
test("unknown command returns exit code 1", () => {
|
||||
const result = runCli(["foobar"]);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /Unknown command: foobar/);
|
||||
});
|
||||
|
||||
test("list shows available skills", () => {
|
||||
const result = runCli(["list"]);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /13 skills available/);
|
||||
assert.match(result.stdout, /brainstorming/);
|
||||
assert.match(result.stdout, /test-driven-development/);
|
||||
assert.match(result.stdout, /single-flow-task-execution/);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, mkdir, rm, access } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, readdir, rm, access, writeFile } from "node:fs/promises";
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
@@ -77,3 +77,96 @@ test("init replaces .agent with --force", async () => {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("init with -f short flag works", async () => {
|
||||
const projectDir = await createTempProject("agsp-short-f-");
|
||||
|
||||
try {
|
||||
await mkdir(join(projectDir, ".agent"), { recursive: true });
|
||||
|
||||
const result = runCli(["init", "-f"], projectDir);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /Initialized/);
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("init with target directory argument", async () => {
|
||||
const parentDir = await createTempProject("agsp-parent-");
|
||||
const targetDir = join(parentDir, "myproject");
|
||||
await mkdir(targetDir);
|
||||
|
||||
try {
|
||||
const result = runCli(["init", targetDir], parentDir);
|
||||
assert.equal(result.status, 0);
|
||||
|
||||
const hasAgent = await pathExists(join(targetDir, ".agent", "AGENTS.md"));
|
||||
assert.equal(hasAgent, true);
|
||||
} finally {
|
||||
await rm(parentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("init fails when target directory does not exist", async () => {
|
||||
const result = runCli(["init", "/tmp/nonexistent-dir-agsp-12345"]);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /does not exist/i);
|
||||
});
|
||||
|
||||
test("init with unknown option fails", async () => {
|
||||
const projectDir = await createTempProject("agsp-unknown-");
|
||||
|
||||
try {
|
||||
const result = runCli(["init", "--bogus"], projectDir);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /Unknown option/);
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("init --dry-run previews files without copying", async () => {
|
||||
const projectDir = await createTempProject("agsp-dryrun-");
|
||||
|
||||
try {
|
||||
const result = runCli(["init", "--dry-run"], projectDir);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /Dry run/);
|
||||
assert.match(result.stdout, /AGENTS\.md/);
|
||||
|
||||
const hasAgent = await pathExists(join(projectDir, ".agent"));
|
||||
assert.equal(hasAgent, false, ".agent should not be created during dry run");
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("init --force --backup creates backup before replacing", async () => {
|
||||
const projectDir = await createTempProject("agsp-backup-");
|
||||
|
||||
try {
|
||||
// Create initial .agent with a custom file
|
||||
await mkdir(join(projectDir, ".agent"), { recursive: true });
|
||||
await writeFile(join(projectDir, ".agent", "custom.txt"), "my custom config");
|
||||
|
||||
const result = runCli(["init", "--force", "--backup"], projectDir);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /Backed up/);
|
||||
|
||||
// New .agent should exist
|
||||
const hasAgent = await pathExists(join(projectDir, ".agent", "AGENTS.md"));
|
||||
assert.equal(hasAgent, true);
|
||||
|
||||
// Backup dir should exist
|
||||
const entries = await readdir(projectDir);
|
||||
const backupDirs = entries.filter((e) => e.startsWith(".agent-backup-"));
|
||||
assert.equal(backupDirs.length, 1, "Should have exactly one backup directory");
|
||||
|
||||
// Backup should contain the custom file
|
||||
const hasCustom = await pathExists(join(projectDir, backupDirs[0], "custom.txt"));
|
||||
assert.equal(hasCustom, true, "Backup should preserve custom files");
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
73
tests/validate.test.mjs
Normal file
73
tests/validate.test.mjs
Normal file
@@ -0,0 +1,73 @@
|
||||
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const cliPath = resolve(process.cwd(), "bin/antigravity-superpowers.js");
|
||||
|
||||
function runCli(args, cwd) {
|
||||
return spawnSync(process.execPath, [cliPath, ...args], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
async function createTempProject(prefix) {
|
||||
return mkdtemp(join(tmpdir(), prefix));
|
||||
}
|
||||
|
||||
test("validate passes on freshly initialized project", async () => {
|
||||
const projectDir = await createTempProject("agsp-val-pass-");
|
||||
|
||||
try {
|
||||
runCli(["init"], projectDir);
|
||||
const result = runCli(["validate"], projectDir);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /PASSED/);
|
||||
assert.match(result.stdout, /Failed: 0/);
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("validate fails when .agent is missing", async () => {
|
||||
const projectDir = await createTempProject("agsp-val-missing-");
|
||||
|
||||
try {
|
||||
const result = runCli(["validate"], projectDir);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /No \.agent directory/);
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("validate fails when skills are missing", async () => {
|
||||
const projectDir = await createTempProject("agsp-val-incomplete-");
|
||||
|
||||
try {
|
||||
// Create partial .agent
|
||||
await mkdir(join(projectDir, ".agent"), { recursive: true });
|
||||
const result = runCli(["validate"], projectDir);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stdout, /FAIL/);
|
||||
assert.match(result.stdout, /FAILED/);
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("validate accepts target directory argument", async () => {
|
||||
const projectDir = await createTempProject("agsp-val-target-");
|
||||
|
||||
try {
|
||||
runCli(["init"], projectDir);
|
||||
const result = runCli(["validate", projectDir]);
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /PASSED/);
|
||||
} finally {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user