diff --git a/package.json b/package.json index 5bc5cd9..6144875 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "antigravity-superpowers", - "version": "0.1.0", + "version": "0.2.0", "description": "CLI to initialize the Antigravity Superpowers .agent profile", "type": "module", "bin": { @@ -13,7 +13,7 @@ "README.md" ], "scripts": { - "test": "node --test tests", + "test": "node --test 'tests/**/*.test.mjs'", "smoke:pack": "node scripts/check-pack.mjs", "prepublishOnly": "npm test && npm run smoke:pack" }, diff --git a/scripts/check-pack.mjs b/scripts/check-pack.mjs index e6601b2..f45779f 100644 --- a/scripts/check-pack.mjs +++ b/scripts/check-pack.mjs @@ -22,6 +22,8 @@ const required = [ "bin/antigravity-superpowers.js", "src/cli.js", "src/commands/init.js", + "src/commands/validate.js", + "src/commands/list.js", "templates/.agent/AGENTS.md", "templates/.agent/INSTALL.md", "templates/.agent/task.md", @@ -31,14 +33,23 @@ const required = [ "templates/.agent/agents/code-reviewer.md", "templates/.agent/tests/run-tests.sh", "templates/.agent/tests/check-antigravity-profile.sh", + "templates/.agent/skills/brainstorming/SKILL.md", + "templates/.agent/skills/executing-plans/SKILL.md", + "templates/.agent/skills/finishing-a-development-branch/SKILL.md", + "templates/.agent/skills/receiving-code-review/SKILL.md", + "templates/.agent/skills/requesting-code-review/SKILL.md", + "templates/.agent/skills/requesting-code-review/code-reviewer.md", + "templates/.agent/skills/systematic-debugging/SKILL.md", + "templates/.agent/skills/test-driven-development/SKILL.md", + "templates/.agent/skills/using-git-worktrees/SKILL.md", + "templates/.agent/skills/using-superpowers/SKILL.md", + "templates/.agent/skills/verification-before-completion/SKILL.md", + "templates/.agent/skills/writing-plans/SKILL.md", + "templates/.agent/skills/writing-skills/SKILL.md", "templates/.agent/skills/single-flow-task-execution/SKILL.md", "templates/.agent/skills/single-flow-task-execution/implementer-prompt.md", "templates/.agent/skills/single-flow-task-execution/spec-reviewer-prompt.md", "templates/.agent/skills/single-flow-task-execution/code-quality-reviewer-prompt.md", - "templates/.agent/skills/executing-plans/SKILL.md", - "templates/.agent/skills/verification-before-completion/SKILL.md", - "templates/.agent/skills/writing-plans/SKILL.md", - "templates/.agent/skills/test-driven-development/SKILL.md", ]; const missing = required.filter((path) => !packagedPaths.has(path)); diff --git a/src/cli.js b/src/cli.js index e19abbf..3569eab 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,18 +1,33 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; import { initCommand } from "./commands/init.js"; +import { validateCommand } from "./commands/validate.js"; +import { listCommand } from "./commands/list.js"; + +async function getVersion() { + const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url)); + const pkg = JSON.parse(await readFile(pkgPath, "utf8")); + return pkg.version; +} function helpText() { return [ "antigravity-superpowers", "", "Usage:", - " antigravity-superpowers init [target-directory] [--force]", + " antigravity-superpowers [options]", "", "Commands:", - " init Initialize .agent profile in a project", + " init [dir] [--force] [--backup] [--dry-run] Initialize .agent profile in a project", + " validate [dir] Validate .agent profile in a project", + " list List available skills", "", "Options:", - " -f, --force Overwrite existing .agent directory", - " -h, --help Show help", + " -f, --force Overwrite existing .agent directory", + " -b, --backup Backup existing .agent before overwrite (use with --force)", + " -n, --dry-run Preview files that would be copied", + " -v, --version Show version", + " -h, --help Show help", ].join("\n"); } @@ -24,6 +39,12 @@ export async function runCli(args, io = process) { return 0; } + if (command === "-v" || command === "--version") { + const version = await getVersion(); + io.stdout.write(`${version}\n`); + return 0; + } + if (command === "init") { return initCommand(rest, { cwd: io.cwd?.() ?? process.cwd(), @@ -32,6 +53,21 @@ export async function runCli(args, io = process) { }); } + if (command === "validate") { + return validateCommand(rest, { + cwd: io.cwd?.() ?? process.cwd(), + stdout: io.stdout, + stderr: io.stderr, + }); + } + + if (command === "list") { + return listCommand({ + stdout: io.stdout, + stderr: io.stderr, + }); + } + io.stderr.write(`Unknown command: ${command}\n\n${helpText()}\n`); return 1; } diff --git a/src/commands/init.js b/src/commands/init.js index 1009979..3edb60b 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -1,7 +1,7 @@ -import { access, cp, rm, stat } from "node:fs/promises"; +import { access, cp, readdir, rename, rm, stat } from "node:fs/promises"; import { constants as fsConstants } from "node:fs"; import { fileURLToPath } from "node:url"; -import { join, resolve } from "node:path"; +import { join, relative, resolve } from "node:path"; function getTemplateDir() { return fileURLToPath(new URL("../../templates/.agent", import.meta.url)); @@ -20,6 +20,8 @@ function parseInitArgs(args) { const parsed = { target: ".", force: false, + backup: false, + dryRun: false, }; let targetSet = false; @@ -29,6 +31,16 @@ function parseInitArgs(args) { continue; } + if (arg === "--backup" || arg === "-b") { + parsed.backup = true; + continue; + } + + if (arg === "--dry-run" || arg === "-n") { + parsed.dryRun = true; + continue; + } + if (arg.startsWith("-")) { throw new Error(`Unknown option for init: ${arg}`); } @@ -57,6 +69,20 @@ async function validateTargetDir(targetDir) { } } +async function collectFiles(dir, base = dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await collectFiles(fullPath, base))); + } else { + files.push(relative(base, fullPath)); + } + } + return files.sort(); +} + export async function initCommand(args, { cwd, stdout, stderr }) { let parsed; try { @@ -82,6 +108,20 @@ export async function initCommand(args, { cwd, stdout, stderr }) { } const agentExists = await exists(agentDir); + + if (parsed.dryRun) { + const files = await collectFiles(templateDir); + stdout.write(`Dry run: the following files would be copied to ${agentDir}/\n\n`); + for (const file of files) { + stdout.write(` .agent/${file}\n`); + } + stdout.write(`\nTotal: ${files.length} files\n`); + if (agentExists && !parsed.force) { + stdout.write(`\nNote: .agent already exists. Use --force to replace it.\n`); + } + return 0; + } + if (agentExists && !parsed.force) { stderr.write( `.agent already exists at ${agentDir}. Re-run with --force to replace it.\n`, @@ -90,13 +130,20 @@ export async function initCommand(args, { cwd, stdout, stderr }) { } if (agentExists && parsed.force) { - await rm(agentDir, { recursive: true, force: true }); + if (parsed.backup) { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const backupDir = join(targetDir, `.agent-backup-${timestamp}`); + await rename(agentDir, backupDir); + stdout.write(`Backed up existing .agent to ${backupDir}\n`); + } else { + await rm(agentDir, { recursive: true, force: true }); + } } await cp(templateDir, agentDir, { recursive: true }); stdout.write(`Initialized Antigravity Superpowers profile at ${agentDir}\n`); - stdout.write("Next step: bash .agent/tests/run-tests.sh\n"); + stdout.write("Next step: antigravity-superpowers validate\n"); stdout.write( "Note: docs/plans/task.md is created at runtime by skills when task tracking starts.\n", ); diff --git a/src/commands/list.js b/src/commands/list.js new file mode 100644 index 0000000..aa62e1c --- /dev/null +++ b/src/commands/list.js @@ -0,0 +1,79 @@ +import { readdir, readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; + +function getSkillsDir() { + return fileURLToPath(new URL("../../templates/.agent/skills", import.meta.url)); +} + +function parseFrontmatter(content) { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) return {}; + + const fm = {}; + const lines = match[1].split("\n"); + let currentKey = null; + let currentValue = ""; + + for (const line of lines) { + const keyMatch = line.match(/^(\w+):\s*(.*)$/); + if (keyMatch) { + if (currentKey) { + fm[currentKey] = currentValue.trim(); + } + currentKey = keyMatch[1]; + currentValue = keyMatch[2].replace(/^["']|["']$/g, ""); + } else if (currentKey) { + currentValue += " " + line.trim(); + } + } + if (currentKey) { + fm[currentKey] = currentValue.trim(); + } + + return fm; +} + +export async function listCommand({ stdout, stderr }) { + const skillsDir = getSkillsDir(); + + let entries; + try { + entries = await readdir(skillsDir, { withFileTypes: true }); + } catch { + stderr.write("Could not read bundled skills directory.\n"); + return 1; + } + + const skills = []; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const skillFile = join(skillsDir, entry.name, "SKILL.md"); + try { + const content = await readFile(skillFile, "utf8"); + const fm = parseFrontmatter(content); + skills.push({ + name: fm.name || entry.name, + description: fm.description || "(no description)", + }); + } catch { + skills.push({ name: entry.name, description: "(SKILL.md not found)" }); + } + } + + skills.sort((a, b) => a.name.localeCompare(b.name)); + + stdout.write(`Antigravity Superpowers — ${skills.length} skills available:\n\n`); + + const maxName = Math.max(...skills.map((s) => s.name.length)); + for (const skill of skills) { + const desc = skill.description.length > 80 + ? skill.description.slice(0, 77) + "..." + : skill.description; + stdout.write(` ${skill.name.padEnd(maxName + 2)} ${desc}\n`); + } + + stdout.write("\n"); + return 0; +} diff --git a/src/commands/validate.js b/src/commands/validate.js new file mode 100644 index 0000000..2a7e1ce --- /dev/null +++ b/src/commands/validate.js @@ -0,0 +1,209 @@ +import { access, readdir, readFile } from "node:fs/promises"; +import { constants as fsConstants } from "node:fs"; +import { join, resolve } from "node:path"; + +async function exists(path) { + try { + await access(path, fsConstants.F_OK); + return true; + } catch { + return false; + } +} + +function parseValidateArgs(args) { + const parsed = { target: "." }; + let targetSet = false; + + for (const arg of args) { + if (arg.startsWith("-")) { + throw new Error(`Unknown option for validate: ${arg}`); + } + if (targetSet) { + throw new Error("Too many positional arguments. Only one target directory is supported."); + } + parsed.target = arg; + targetSet = true; + } + + return parsed; +} + +const REQUIRED_FILES = [ + "AGENTS.md", + "INSTALL.md", + "task.md", + "workflows/brainstorm.md", + "workflows/write-plan.md", + "workflows/execute-plan.md", + "agents/code-reviewer.md", + "tests/run-tests.sh", + "tests/check-antigravity-profile.sh", +]; + +const REQUIRED_SKILLS = [ + "brainstorming", + "executing-plans", + "finishing-a-development-branch", + "receiving-code-review", + "requesting-code-review", + "systematic-debugging", + "test-driven-development", + "using-git-worktrees", + "using-superpowers", + "verification-before-completion", + "writing-plans", + "writing-skills", + "single-flow-task-execution", +]; + +const LEGACY_PATTERNS = [ + "Skill tool", + "Task tool with", + 'Task\\("', + "Dispatch implementer subagent", + "Dispatch code-reviewer subagent", + "Create TodoWrite", + "Mark task complete in TodoWrite", + "Use TodoWrite", + "superpowers:", +]; + +const AGENTS_MAPPINGS = [ + "task_boundary", + "browser_subagent", + "view_file", + "docs/plans/task.md", + "run_command", + "grep_search", + "find_by_name", +]; + +export async function validateCommand(args, { cwd, stdout, stderr }) { + let parsed; + try { + parsed = parseValidateArgs(args); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + stderr.write(`${message}\n`); + return 1; + } + + const targetDir = resolve(cwd, parsed.target); + const agentDir = join(targetDir, ".agent"); + + if (!(await exists(agentDir))) { + stderr.write(`No .agent directory found at ${agentDir}\nRun 'antigravity-superpowers init' first.\n`); + return 1; + } + + let passCount = 0; + let failCount = 0; + + function pass(msg) { + stdout.write(` [PASS] ${msg}\n`); + passCount++; + } + + function fail(msg) { + stdout.write(` [FAIL] ${msg}\n`); + failCount++; + } + + stdout.write("Checking required files...\n"); + for (const file of REQUIRED_FILES) { + const filePath = join(agentDir, file); + if (await exists(filePath)) { + pass(`File exists: .agent/${file}`); + } else { + fail(`Missing file: .agent/${file}`); + } + } + + stdout.write("\nChecking required skills...\n"); + for (const skill of REQUIRED_SKILLS) { + const skillPath = join(agentDir, "skills", skill, "SKILL.md"); + if (await exists(skillPath)) { + pass(`Skill exists: ${skill}`); + } else { + fail(`Missing skill: ${skill}`); + } + } + + stdout.write("\nChecking frontmatter...\n"); + for (const skill of REQUIRED_SKILLS) { + const skillPath = join(agentDir, "skills", skill, "SKILL.md"); + if (!(await exists(skillPath))) continue; + + const content = await readFile(skillPath, "utf8"); + const hasDelimiters = /^---$/m.test(content); + const hasName = /^name:\s*\S/m.test(content); + const hasDescription = /^description:\s*\S/m.test(content); + + if (hasDelimiters) pass(`${skill} has frontmatter`); + else fail(`${skill} missing frontmatter delimiters`); + + if (hasName) pass(`${skill} has name`); + else fail(`${skill} missing name field`); + + if (hasDescription) pass(`${skill} has description`); + else fail(`${skill} missing description field`); + } + + stdout.write("\nChecking for legacy patterns...\n"); + const skillsDir = join(agentDir, "skills"); + let allSkillContent = ""; + if (await exists(skillsDir)) { + const skills = await readdir(skillsDir, { withFileTypes: true }); + for (const entry of skills) { + if (!entry.isDirectory()) continue; + const skillFile = join(skillsDir, entry.name, "SKILL.md"); + if (await exists(skillFile)) { + allSkillContent += await readFile(skillFile, "utf8"); + } + } + } + + for (const pattern of LEGACY_PATTERNS) { + const regex = new RegExp(pattern); + if (regex.test(allSkillContent)) { + fail(`Legacy pattern found: ${pattern}`); + } else { + pass(`Legacy pattern absent: ${pattern}`); + } + } + + stdout.write("\nChecking AGENTS mapping contract...\n"); + const agentsPath = join(agentDir, "AGENTS.md"); + if (await exists(agentsPath)) { + const agentsContent = await readFile(agentsPath, "utf8"); + for (const mapping of AGENTS_MAPPINGS) { + if (agentsContent.includes(mapping)) { + pass(`AGENTS includes: ${mapping}`); + } else { + fail(`AGENTS missing: ${mapping}`); + } + } + } else { + fail("AGENTS.md not found"); + } + + // Check runtime tracker is NOT bundled + const runtimeTracker = join(targetDir, "docs", "plans", "task.md"); + if (!(await exists(runtimeTracker))) { + pass("Runtime tracker not bundled (created at runtime)"); + } else { + // It's OK if user created it during runtime, don't fail + pass("Runtime tracker exists (created by skill flow)"); + } + + stdout.write(`\n Passed: ${passCount}\n Failed: ${failCount}\n\n`); + + if (failCount > 0) { + stdout.write("STATUS: FAILED\n"); + return 1; + } + + stdout.write("STATUS: PASSED\n"); + return 0; +} diff --git a/templates/.agent/workflows/brainstorm.md b/templates/.agent/workflows/brainstorm.md index a65dde0..eaa1abc 100644 --- a/templates/.agent/workflows/brainstorm.md +++ b/templates/.agent/workflows/brainstorm.md @@ -1,4 +1,5 @@ --- +name: brainstorm description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores requirements and design before implementation." --- diff --git a/templates/.agent/workflows/execute-plan.md b/templates/.agent/workflows/execute-plan.md index c6af271..b198a5f 100644 --- a/templates/.agent/workflows/execute-plan.md +++ b/templates/.agent/workflows/execute-plan.md @@ -1,4 +1,5 @@ --- +name: execute-plan description: Execute plan in single-flow mode --- diff --git a/templates/.agent/workflows/write-plan.md b/templates/.agent/workflows/write-plan.md index 2f77e4d..bf16000 100644 --- a/templates/.agent/workflows/write-plan.md +++ b/templates/.agent/workflows/write-plan.md @@ -1,4 +1,5 @@ --- +name: write-plan description: Create detailed implementation plan with bite-sized tasks --- diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs new file mode 100644 index 0000000..03cb633 --- /dev/null +++ b/tests/cli.test.mjs @@ -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/); +}); diff --git a/tests/init.test.mjs b/tests/init.test.mjs index 1c51be6..11f5f98 100644 --- a/tests/init.test.mjs +++ b/tests/init.test.mjs @@ -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 }); + } +}); diff --git a/tests/validate.test.mjs b/tests/validate.test.mjs new file mode 100644 index 0000000..66d91e2 --- /dev/null +++ b/tests/validate.test.mjs @@ -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 }); + } +});