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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "antigravity-superpowers",
|
"name": "antigravity-superpowers",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "CLI to initialize the Antigravity Superpowers .agent profile",
|
"description": "CLI to initialize the Antigravity Superpowers .agent profile",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test tests",
|
"test": "node --test 'tests/**/*.test.mjs'",
|
||||||
"smoke:pack": "node scripts/check-pack.mjs",
|
"smoke:pack": "node scripts/check-pack.mjs",
|
||||||
"prepublishOnly": "npm test && npm run smoke:pack"
|
"prepublishOnly": "npm test && npm run smoke:pack"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const required = [
|
|||||||
"bin/antigravity-superpowers.js",
|
"bin/antigravity-superpowers.js",
|
||||||
"src/cli.js",
|
"src/cli.js",
|
||||||
"src/commands/init.js",
|
"src/commands/init.js",
|
||||||
|
"src/commands/validate.js",
|
||||||
|
"src/commands/list.js",
|
||||||
"templates/.agent/AGENTS.md",
|
"templates/.agent/AGENTS.md",
|
||||||
"templates/.agent/INSTALL.md",
|
"templates/.agent/INSTALL.md",
|
||||||
"templates/.agent/task.md",
|
"templates/.agent/task.md",
|
||||||
@@ -31,14 +33,23 @@ const required = [
|
|||||||
"templates/.agent/agents/code-reviewer.md",
|
"templates/.agent/agents/code-reviewer.md",
|
||||||
"templates/.agent/tests/run-tests.sh",
|
"templates/.agent/tests/run-tests.sh",
|
||||||
"templates/.agent/tests/check-antigravity-profile.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/SKILL.md",
|
||||||
"templates/.agent/skills/single-flow-task-execution/implementer-prompt.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/spec-reviewer-prompt.md",
|
||||||
"templates/.agent/skills/single-flow-task-execution/code-quality-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));
|
const missing = required.filter((path) => !packagedPaths.has(path));
|
||||||
|
|||||||
44
src/cli.js
44
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 { 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() {
|
function helpText() {
|
||||||
return [
|
return [
|
||||||
"antigravity-superpowers",
|
"antigravity-superpowers",
|
||||||
"",
|
"",
|
||||||
"Usage:",
|
"Usage:",
|
||||||
" antigravity-superpowers init [target-directory] [--force]",
|
" antigravity-superpowers <command> [options]",
|
||||||
"",
|
"",
|
||||||
"Commands:",
|
"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:",
|
"Options:",
|
||||||
" -f, --force Overwrite existing .agent directory",
|
" -f, --force Overwrite existing .agent directory",
|
||||||
" -h, --help Show help",
|
" -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");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +39,12 @@ export async function runCli(args, io = process) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command === "-v" || command === "--version") {
|
||||||
|
const version = await getVersion();
|
||||||
|
io.stdout.write(`${version}\n`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (command === "init") {
|
if (command === "init") {
|
||||||
return initCommand(rest, {
|
return initCommand(rest, {
|
||||||
cwd: io.cwd?.() ?? process.cwd(),
|
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`);
|
io.stderr.write(`Unknown command: ${command}\n\n${helpText()}\n`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { constants as fsConstants } from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { join, resolve } from "node:path";
|
import { join, relative, resolve } from "node:path";
|
||||||
|
|
||||||
function getTemplateDir() {
|
function getTemplateDir() {
|
||||||
return fileURLToPath(new URL("../../templates/.agent", import.meta.url));
|
return fileURLToPath(new URL("../../templates/.agent", import.meta.url));
|
||||||
@@ -20,6 +20,8 @@ function parseInitArgs(args) {
|
|||||||
const parsed = {
|
const parsed = {
|
||||||
target: ".",
|
target: ".",
|
||||||
force: false,
|
force: false,
|
||||||
|
backup: false,
|
||||||
|
dryRun: false,
|
||||||
};
|
};
|
||||||
let targetSet = false;
|
let targetSet = false;
|
||||||
|
|
||||||
@@ -29,6 +31,16 @@ function parseInitArgs(args) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg === "--backup" || arg === "-b") {
|
||||||
|
parsed.backup = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--dry-run" || arg === "-n") {
|
||||||
|
parsed.dryRun = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg.startsWith("-")) {
|
if (arg.startsWith("-")) {
|
||||||
throw new Error(`Unknown option for init: ${arg}`);
|
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 }) {
|
export async function initCommand(args, { cwd, stdout, stderr }) {
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
@@ -82,6 +108,20 @@ export async function initCommand(args, { cwd, stdout, stderr }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agentExists = await exists(agentDir);
|
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) {
|
if (agentExists && !parsed.force) {
|
||||||
stderr.write(
|
stderr.write(
|
||||||
`.agent already exists at ${agentDir}. Re-run with --force to replace it.\n`,
|
`.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) {
|
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 });
|
await cp(templateDir, agentDir, { recursive: true });
|
||||||
|
|
||||||
stdout.write(`Initialized Antigravity Superpowers profile at ${agentDir}\n`);
|
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(
|
stdout.write(
|
||||||
"Note: docs/plans/task.md is created at runtime by skills when task tracking starts.\n",
|
"Note: docs/plans/task.md is created at runtime by skills when task tracking starts.\n",
|
||||||
);
|
);
|
||||||
|
|||||||
79
src/commands/list.js
Normal file
79
src/commands/list.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
209
src/commands/validate.js
Normal file
209
src/commands/validate.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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."
|
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores requirements and design before implementation."
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
name: execute-plan
|
||||||
description: Execute plan in single-flow mode
|
description: Execute plan in single-flow mode
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
name: write-plan
|
||||||
description: Create detailed implementation plan with bite-sized tasks
|
description: Create detailed implementation plan with bite-sized tasks
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
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 { constants as fsConstants } from "node:fs";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
@@ -77,3 +77,96 @@ test("init replaces .agent with --force", async () => {
|
|||||||
await rm(projectDir, { recursive: true, force: true });
|
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