@quinteroac/agents-coding-toolkit 0.1.0-preview
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/AGENTS.md +7 -0
- package/README.md +127 -0
- package/package.json +34 -0
- package/scaffold/.agents/flow/archived/tmpl_.gitkeep +0 -0
- package/scaffold/.agents/flow/tmpl_README.md +7 -0
- package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +11 -0
- package/scaffold/.agents/skills/automated-fix/tmpl_SKILL.md +67 -0
- package/scaffold/.agents/skills/create-issue/tmpl_SKILL.md +68 -0
- package/scaffold/.agents/skills/create-pr-document/tmpl_SKILL.md +125 -0
- package/scaffold/.agents/skills/create-project-context/tmpl_SKILL.md +168 -0
- package/scaffold/.agents/skills/create-test-plan/tmpl_SKILL.md +86 -0
- package/scaffold/.agents/skills/debug/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/evaluate/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/execute-test-batch/tmpl_SKILL.md +49 -0
- package/scaffold/.agents/skills/execute-test-case/tmpl_SKILL.md +47 -0
- package/scaffold/.agents/skills/implement-user-story/tmpl_SKILL.md +68 -0
- package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/refactor-prd/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/refine-pr-document/tmpl_SKILL.md +108 -0
- package/scaffold/.agents/skills/refine-project-context/tmpl_SKILL.md +157 -0
- package/scaffold/.agents/skills/refine-test-plan/tmpl_SKILL.md +76 -0
- package/scaffold/.agents/tmpl_PROJECT_CONTEXT.md +3 -0
- package/scaffold/.agents/tmpl_state.example.json +26 -0
- package/scaffold/.agents/tmpl_state_rules.md +29 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_CHANGELOG.md +18 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_TECHNICAL_DEBT.md +11 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_evaluation-report.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_product-requirement-document.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_refactor_plan.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_test-plan.md +19 -0
- package/scaffold/docs/nvst-flow/tmpl_COMMANDS.md +0 -0
- package/scaffold/docs/nvst-flow/tmpl_QUICK_USE.md +0 -0
- package/scaffold/docs/tmpl_PLACEHOLDER.md +0 -0
- package/scaffold/schemas/node-shims.d.ts +15 -0
- package/scaffold/schemas/tmpl_issues.ts +19 -0
- package/scaffold/schemas/tmpl_prd.ts +26 -0
- package/scaffold/schemas/tmpl_progress.ts +39 -0
- package/scaffold/schemas/tmpl_state.ts +81 -0
- package/scaffold/schemas/tmpl_test-plan.ts +20 -0
- package/scaffold/schemas/tmpl_validate-progress.ts +13 -0
- package/scaffold/schemas/tmpl_validate-state.ts +13 -0
- package/scaffold/tmpl_AGENTS.md +7 -0
- package/schemas/prd.ts +26 -0
- package/schemas/progress.ts +39 -0
- package/schemas/state.ts +81 -0
- package/schemas/test-plan.test.ts +53 -0
- package/schemas/test-plan.ts +20 -0
- package/schemas/validate-progress.ts +13 -0
- package/schemas/validate-state.ts +13 -0
- package/src/agent.test.ts +37 -0
- package/src/agent.ts +225 -0
- package/src/cli-path.ts +4 -0
- package/src/cli.ts +578 -0
- package/src/commands/approve-project-context.ts +37 -0
- package/src/commands/approve-requirement.ts +217 -0
- package/src/commands/approve-test-plan.test.ts +193 -0
- package/src/commands/approve-test-plan.ts +202 -0
- package/src/commands/create-issue.test.ts +484 -0
- package/src/commands/create-issue.ts +371 -0
- package/src/commands/create-project-context.ts +96 -0
- package/src/commands/create-prototype.test.ts +153 -0
- package/src/commands/create-prototype.ts +425 -0
- package/src/commands/create-test-plan.test.ts +381 -0
- package/src/commands/create-test-plan.ts +248 -0
- package/src/commands/define-requirement.ts +47 -0
- package/src/commands/destroy.ts +113 -0
- package/src/commands/execute-automated-fix.test.ts +580 -0
- package/src/commands/execute-automated-fix.ts +363 -0
- package/src/commands/execute-manual-fix.test.ts +343 -0
- package/src/commands/execute-manual-fix.ts +203 -0
- package/src/commands/execute-test-plan.test.ts +1891 -0
- package/src/commands/execute-test-plan.ts +722 -0
- package/src/commands/init.ts +85 -0
- package/src/commands/refine-project-context.ts +74 -0
- package/src/commands/refine-requirement.ts +60 -0
- package/src/commands/refine-test-plan.test.ts +200 -0
- package/src/commands/refine-test-plan.ts +93 -0
- package/src/commands/start-iteration.test.ts +144 -0
- package/src/commands/start-iteration.ts +101 -0
- package/src/commands/write-json.ts +136 -0
- package/src/install.test.ts +124 -0
- package/src/pack.test.ts +103 -0
- package/src/state.test.ts +66 -0
- package/src/state.ts +52 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdir, readdir, rename } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { State } from "../../scaffold/schemas/tmpl_state";
|
|
5
|
+
import { exists, readState, writeState, STATE_REL_PATH, FLOW_REL_DIR } from "../state";
|
|
6
|
+
|
|
7
|
+
const ARCHIVED_DIR = join(FLOW_REL_DIR, "archived");
|
|
8
|
+
|
|
9
|
+
function createInitialState(nowIso: string): State {
|
|
10
|
+
return {
|
|
11
|
+
current_iteration: "000001",
|
|
12
|
+
current_phase: "define",
|
|
13
|
+
phases: {
|
|
14
|
+
define: {
|
|
15
|
+
requirement_definition: { status: "pending", file: null },
|
|
16
|
+
prd_generation: { status: "pending", file: null },
|
|
17
|
+
},
|
|
18
|
+
prototype: {
|
|
19
|
+
project_context: { status: "pending", file: null },
|
|
20
|
+
test_plan: { status: "pending", file: null },
|
|
21
|
+
tp_generation: { status: "pending", file: null },
|
|
22
|
+
prototype_build: { status: "pending", file: null },
|
|
23
|
+
test_execution: { status: "pending", file: null },
|
|
24
|
+
prototype_approved: false,
|
|
25
|
+
},
|
|
26
|
+
refactor: {
|
|
27
|
+
evaluation_report: { status: "pending", file: null },
|
|
28
|
+
refactor_plan: { status: "pending", file: null },
|
|
29
|
+
refactor_execution: { status: "pending", file: null },
|
|
30
|
+
changelog: { status: "pending", file: null },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
last_updated: nowIso,
|
|
34
|
+
history: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nextIteration(iteration: string): string {
|
|
39
|
+
return String(Number.parseInt(iteration, 10) + 1).padStart(6, "0");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function runStartIteration(): Promise<void> {
|
|
43
|
+
const projectRoot = process.cwd();
|
|
44
|
+
const statePath = join(projectRoot, STATE_REL_PATH);
|
|
45
|
+
const flowDir = join(projectRoot, FLOW_REL_DIR);
|
|
46
|
+
const nowIso = new Date().toISOString();
|
|
47
|
+
|
|
48
|
+
await mkdir(flowDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
if (!(await exists(statePath))) {
|
|
51
|
+
await writeState(projectRoot, createInitialState(nowIso));
|
|
52
|
+
console.log("Iteration 000001 started (phase: define)");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const parsedState = await readState(projectRoot);
|
|
57
|
+
|
|
58
|
+
const currentIteration = parsedState.current_iteration;
|
|
59
|
+
const flowEntries = await readdir(flowDir, { withFileTypes: true });
|
|
60
|
+
const filePrefix = `it_${currentIteration}_`;
|
|
61
|
+
const filesToArchive = flowEntries
|
|
62
|
+
.filter((entry) => entry.isFile() && entry.name.startsWith(filePrefix))
|
|
63
|
+
.map((entry) => entry.name);
|
|
64
|
+
|
|
65
|
+
const iterationArchiveDir = join(ARCHIVED_DIR, currentIteration);
|
|
66
|
+
const iterationArchiveAbsDir = join(projectRoot, iterationArchiveDir);
|
|
67
|
+
await mkdir(iterationArchiveAbsDir, { recursive: true });
|
|
68
|
+
|
|
69
|
+
for (const fileName of filesToArchive) {
|
|
70
|
+
await rename(join(flowDir, fileName), join(iterationArchiveAbsDir, fileName));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const updatedHistory = [
|
|
74
|
+
...(parsedState.history ?? []),
|
|
75
|
+
{
|
|
76
|
+
iteration: currentIteration,
|
|
77
|
+
archived_at: nowIso,
|
|
78
|
+
archived_path: `.agents/flow/archived/${currentIteration}`,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const nextState = createInitialState(nowIso);
|
|
83
|
+
nextState.current_iteration = nextIteration(currentIteration);
|
|
84
|
+
nextState.history = updatedHistory;
|
|
85
|
+
|
|
86
|
+
// Preserve project_context when already created (immutable across iterations)
|
|
87
|
+
const prevProjectContext = parsedState.phases?.prototype?.project_context;
|
|
88
|
+
if (prevProjectContext?.status === "created" && prevProjectContext?.file) {
|
|
89
|
+
nextState.phases.prototype.project_context = {
|
|
90
|
+
status: "created",
|
|
91
|
+
file: prevProjectContext.file,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await writeState(projectRoot, nextState);
|
|
96
|
+
|
|
97
|
+
console.log(
|
|
98
|
+
`Archived ${filesToArchive.length} file(s) to .agents/flow/archived/${currentIteration}`,
|
|
99
|
+
);
|
|
100
|
+
console.log(`Iteration ${nextState.current_iteration} started (phase: define)`);
|
|
101
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import type { ZodSchema } from "zod";
|
|
4
|
+
|
|
5
|
+
import { StateSchema } from "../../scaffold/schemas/tmpl_state";
|
|
6
|
+
import { ProgressSchema } from "../../scaffold/schemas/tmpl_progress";
|
|
7
|
+
import { PrdSchema } from "../../scaffold/schemas/tmpl_prd";
|
|
8
|
+
import { TestPlanSchema } from "../../scaffold/schemas/tmpl_test-plan";
|
|
9
|
+
import { IssuesSchema } from "../../scaffold/schemas/tmpl_issues";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Schema registry — maps CLI name → Zod schema
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const SCHEMA_REGISTRY: Record<string, ZodSchema> = {
|
|
15
|
+
state: StateSchema,
|
|
16
|
+
progress: ProgressSchema,
|
|
17
|
+
prd: PrdSchema,
|
|
18
|
+
"test-plan": TestPlanSchema,
|
|
19
|
+
issues: IssuesSchema,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SUPPORTED_SCHEMAS = Object.keys(SCHEMA_REGISTRY).join(", ");
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Argument parsing helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function extractFlag(args: string[], flag: string): { value: string | null; remaining: string[] } {
|
|
28
|
+
const idx = args.indexOf(flag);
|
|
29
|
+
if (idx === -1) return { value: null, remaining: args };
|
|
30
|
+
if (idx + 1 >= args.length) {
|
|
31
|
+
throw new Error(`Missing value for ${flag}`);
|
|
32
|
+
}
|
|
33
|
+
const value = args[idx + 1];
|
|
34
|
+
const remaining = [...args.slice(0, idx), ...args.slice(idx + 2)];
|
|
35
|
+
return { value, remaining };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Read JSON payload from stdin (non-blocking, returns null if nothing piped)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
async function readStdin(): Promise<string> {
|
|
42
|
+
const chunks: Buffer[] = [];
|
|
43
|
+
const reader = Bun.stdin.stream().getReader();
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done) break;
|
|
47
|
+
chunks.push(Buffer.from(value));
|
|
48
|
+
}
|
|
49
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Main entry point
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export interface WriteJsonOptions {
|
|
56
|
+
args: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runWriteJson({ args }: WriteJsonOptions): Promise<void> {
|
|
60
|
+
// --- Parse --schema ---
|
|
61
|
+
const { value: schemaName, remaining: afterSchema } = extractFlag(args, "--schema");
|
|
62
|
+
if (!schemaName) {
|
|
63
|
+
console.error("Error: --schema <name> is required.");
|
|
64
|
+
console.error(`Supported schemas: ${SUPPORTED_SCHEMAS}`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const schema = SCHEMA_REGISTRY[schemaName];
|
|
70
|
+
if (!schema) {
|
|
71
|
+
console.error(`Error: Unknown schema "${schemaName}".`);
|
|
72
|
+
console.error(`Supported schemas: ${SUPPORTED_SCHEMAS}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Parse --out ---
|
|
78
|
+
const { value: outPath, remaining: afterOut } = extractFlag(afterSchema, "--out");
|
|
79
|
+
if (!outPath) {
|
|
80
|
+
console.error("Error: --out <path> is required.");
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Parse --data (optional) ---
|
|
86
|
+
const { value: dataArg, remaining: afterData } = extractFlag(afterOut, "--data");
|
|
87
|
+
|
|
88
|
+
// Reject unknown args
|
|
89
|
+
if (afterData.length > 0) {
|
|
90
|
+
console.error(`Error: Unknown option(s): ${afterData.join(" ")}`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Obtain JSON payload ---
|
|
96
|
+
let rawJson: string;
|
|
97
|
+
if (dataArg) {
|
|
98
|
+
rawJson = dataArg;
|
|
99
|
+
} else {
|
|
100
|
+
// Read from stdin
|
|
101
|
+
rawJson = await readStdin();
|
|
102
|
+
if (!rawJson.trim()) {
|
|
103
|
+
console.error("Error: No JSON payload provided. Use --data '<json>' or pipe via stdin.");
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Parse JSON string ---
|
|
110
|
+
let parsed: unknown;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(rawJson);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error("Error: Invalid JSON input.");
|
|
115
|
+
console.error((err as Error).message);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Validate against schema ---
|
|
121
|
+
const result = schema.safeParse(parsed);
|
|
122
|
+
if (!result.success) {
|
|
123
|
+
const formatted = result.error.format();
|
|
124
|
+
console.error(JSON.stringify({ ok: false, schema: schemaName, errors: formatted }, null, 2));
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Write file ---
|
|
130
|
+
const resolvedPath = resolve(process.cwd(), outPath);
|
|
131
|
+
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
132
|
+
const content = `${JSON.stringify(result.data, null, 2)}\n`;
|
|
133
|
+
await writeFile(resolvedPath, content, "utf-8");
|
|
134
|
+
|
|
135
|
+
console.log(`Written: ${outPath}`);
|
|
136
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const PROJECT_ROOT = join(import.meta.dir, "..");
|
|
8
|
+
const PACKAGE_VERSION = "0.1.0";
|
|
9
|
+
// Scoped packages: npm pack produces scope-package-version.tgz
|
|
10
|
+
const TARBALL_BASENAME = `quinteroac-agents-coding-toolkit-${PACKAGE_VERSION}.tgz`;
|
|
11
|
+
const TARBALL_PATH = join(PROJECT_ROOT, TARBALL_BASENAME);
|
|
12
|
+
|
|
13
|
+
const tempProjectRoots: string[] = [];
|
|
14
|
+
|
|
15
|
+
async function createTempProject(): Promise<string> {
|
|
16
|
+
const root = await mkdtemp(join(tmpdir(), "nvst-install-test-"));
|
|
17
|
+
tempProjectRoots.push(root);
|
|
18
|
+
await writeFile(
|
|
19
|
+
join(root, "package.json"),
|
|
20
|
+
JSON.stringify({ name: "test-consumer", version: "1.0.0", private: true }),
|
|
21
|
+
"utf-8",
|
|
22
|
+
);
|
|
23
|
+
return root;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function bunAdd(cwd: string, spec: string): { exitCode: number | null; stdout: string; stderr: string } {
|
|
27
|
+
const result = spawnSync("bun", ["add", spec], {
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
exitCode: result.status,
|
|
33
|
+
stdout: result.stdout ?? "",
|
|
34
|
+
stderr: result.stderr ?? "",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runNvst(cwd: string, args: string[]): { exitCode: number | null; stdout: string; stderr: string } {
|
|
39
|
+
const result = spawnSync("bunx", ["--bun", "nvst", ...args], {
|
|
40
|
+
cwd,
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
exitCode: result.status,
|
|
45
|
+
stdout: result.stdout ?? "",
|
|
46
|
+
stderr: result.stderr ?? "",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
const packResult = spawnSync("bun", ["run", "package"], {
|
|
52
|
+
cwd: PROJECT_ROOT,
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
});
|
|
55
|
+
if (packResult.status !== 0) {
|
|
56
|
+
throw new Error(`Pre-test package failed: ${packResult.stderr}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await Promise.all(
|
|
62
|
+
tempProjectRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("install package", () => {
|
|
67
|
+
test("US-002-AC03: CLI --version outputs packaged version (run from source)", () => {
|
|
68
|
+
const result = spawnSync("bun", [join(PROJECT_ROOT, "src", "cli.ts"), "--version"], {
|
|
69
|
+
cwd: PROJECT_ROOT,
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
});
|
|
72
|
+
expect(result.status).toBe(0);
|
|
73
|
+
expect(result.stdout?.trim()).toBe(PACKAGE_VERSION);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("US-002-AC01: user can install the package from the local file system or registry", async () => {
|
|
77
|
+
const tempRoot = await createTempProject();
|
|
78
|
+
|
|
79
|
+
const { exitCode, stderr } = bunAdd(tempRoot, PROJECT_ROOT);
|
|
80
|
+
expect(exitCode).toBe(0);
|
|
81
|
+
expect(stderr).not.toContain("error");
|
|
82
|
+
|
|
83
|
+
const nodeModules = join(tempRoot, "node_modules", "@quinteroac", "agents-coding-toolkit");
|
|
84
|
+
const entries = await readdir(join(tempRoot, "node_modules"));
|
|
85
|
+
expect(entries).toContain("@quinteroac");
|
|
86
|
+
|
|
87
|
+
const pkgJsonPath = join(nodeModules, "package.json");
|
|
88
|
+
const pkg = (await Bun.file(pkgJsonPath).json()) as { version?: string; bin?: Record<string, string> };
|
|
89
|
+
expect(pkg.version).toBe(PACKAGE_VERSION);
|
|
90
|
+
expect(pkg.bin).toBeDefined();
|
|
91
|
+
expect(pkg.bin?.nvst).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("US-002-AC01: user can install from packed tarball", async () => {
|
|
95
|
+
const tempRoot = await createTempProject();
|
|
96
|
+
|
|
97
|
+
const { exitCode, stderr } = bunAdd(tempRoot, TARBALL_PATH);
|
|
98
|
+
expect(exitCode).toBe(0);
|
|
99
|
+
expect(stderr).not.toContain("error");
|
|
100
|
+
|
|
101
|
+
const entries = await readdir(join(tempRoot, "node_modules"));
|
|
102
|
+
expect(entries).toContain("@quinteroac");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("US-002-AC02: after installation, the nvst command is available in the shell", async () => {
|
|
106
|
+
const tempRoot = await createTempProject();
|
|
107
|
+
bunAdd(tempRoot, PROJECT_ROOT);
|
|
108
|
+
|
|
109
|
+
const { exitCode, stdout } = runNvst(tempRoot, ["--help"]);
|
|
110
|
+
expect(exitCode).toBe(0);
|
|
111
|
+
expect(stdout).toContain("Usage: nvst <command> [options]");
|
|
112
|
+
expect(stdout).toContain("init");
|
|
113
|
+
expect(stdout).toContain("destroy");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("US-002-AC03: installed version matches the packaged version", async () => {
|
|
117
|
+
const tempRoot = await createTempProject();
|
|
118
|
+
bunAdd(tempRoot, PROJECT_ROOT);
|
|
119
|
+
|
|
120
|
+
const { exitCode, stdout } = runNvst(tempRoot, ["--version"]);
|
|
121
|
+
expect(exitCode).toBe(0);
|
|
122
|
+
expect(stdout.trim()).toBe(PACKAGE_VERSION);
|
|
123
|
+
});
|
|
124
|
+
});
|
package/src/pack.test.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { readdir, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const PROJECT_ROOT = join(import.meta.dir, "..");
|
|
7
|
+
const PACKAGE_VERSION = "0.1.0";
|
|
8
|
+
// Scoped packages: npm pack produces scope-package-version.tgz
|
|
9
|
+
const TARBALL_BASENAME = `quinteroac-agents-coding-toolkit-${PACKAGE_VERSION}.tgz`;
|
|
10
|
+
|
|
11
|
+
function runPackageScript(): { exitCode: number | null; stdout: string; stderr: string } {
|
|
12
|
+
const result = spawnSync("bun", ["run", "package"], {
|
|
13
|
+
cwd: PROJECT_ROOT,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
exitCode: result.status,
|
|
18
|
+
stdout: result.stdout ?? "",
|
|
19
|
+
stderr: result.stderr ?? "",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function listTarballContents(tarballPath: string): string[] {
|
|
24
|
+
const result = spawnSync("tar", ["-tf", tarballPath], {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
});
|
|
27
|
+
if (result.status !== 0) {
|
|
28
|
+
throw new Error(`tar -tf failed: ${result.stderr}`);
|
|
29
|
+
}
|
|
30
|
+
return (result.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractFileFromTarball(tarballPath: string, memberPath: string): string {
|
|
34
|
+
const result = spawnSync("tar", ["-xOf", tarballPath, memberPath], {
|
|
35
|
+
encoding: "utf-8",
|
|
36
|
+
});
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
throw new Error(`tar -xOf failed: ${result.stderr}`);
|
|
39
|
+
}
|
|
40
|
+
return result.stdout ?? "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
const tarballPath = join(PROJECT_ROOT, TARBALL_BASENAME);
|
|
45
|
+
try {
|
|
46
|
+
await rm(tarballPath, { force: true });
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore if file does not exist
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("package command", () => {
|
|
53
|
+
test("US-001-AC01: running package command produces a valid .tgz file", async () => {
|
|
54
|
+
const { exitCode } = runPackageScript();
|
|
55
|
+
expect(exitCode).toBe(0);
|
|
56
|
+
|
|
57
|
+
const tarballPath = join(PROJECT_ROOT, TARBALL_BASENAME);
|
|
58
|
+
const entries = await readdir(PROJECT_ROOT);
|
|
59
|
+
const hasTgz = entries.some((e) => e === TARBALL_BASENAME);
|
|
60
|
+
expect(hasTgz).toBe(true);
|
|
61
|
+
|
|
62
|
+
const stat = await Bun.file(tarballPath).exists();
|
|
63
|
+
expect(stat).toBe(true);
|
|
64
|
+
|
|
65
|
+
const contents = listTarballContents(tarballPath);
|
|
66
|
+
expect(contents.length).toBeGreaterThan(0);
|
|
67
|
+
expect(contents.some((p) => p.includes("package.json"))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("US-001-AC02: package includes all necessary dependencies and source files", async () => {
|
|
71
|
+
runPackageScript();
|
|
72
|
+
|
|
73
|
+
const tarballPath = join(PROJECT_ROOT, TARBALL_BASENAME);
|
|
74
|
+
const contents = listTarballContents(tarballPath);
|
|
75
|
+
|
|
76
|
+
const packageJsonPath = contents.find((p) => p.endsWith("package.json"));
|
|
77
|
+
expect(packageJsonPath).toBeDefined();
|
|
78
|
+
|
|
79
|
+
const baseDir = packageJsonPath!.replace(/\/package\.json$/, "");
|
|
80
|
+
const requiredPaths = [
|
|
81
|
+
`${baseDir}/package.json`,
|
|
82
|
+
`${baseDir}/src/cli.ts`,
|
|
83
|
+
`${baseDir}/src/state.ts`,
|
|
84
|
+
`${baseDir}/scaffold`,
|
|
85
|
+
`${baseDir}/schemas`,
|
|
86
|
+
];
|
|
87
|
+
for (const required of requiredPaths) {
|
|
88
|
+
const found = contents.some((p) => p === required || p.startsWith(required + "/"));
|
|
89
|
+
expect(found).toBe(true);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const pkgContent = extractFileFromTarball(tarballPath, "package/package.json");
|
|
93
|
+
const pkg = JSON.parse(pkgContent);
|
|
94
|
+
expect(pkg.dependencies).toBeDefined();
|
|
95
|
+
expect(pkg.dependencies.zod).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("US-001-AC03: build process completes without errors", () => {
|
|
99
|
+
const { exitCode, stderr } = runPackageScript();
|
|
100
|
+
expect(exitCode).toBe(0);
|
|
101
|
+
expect(stderr).not.toMatch(/\b(Error|ERR!)\b/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { ZodError } from "zod";
|
|
7
|
+
|
|
8
|
+
import { readState, STATE_REL_PATH } from "./state";
|
|
9
|
+
|
|
10
|
+
async function createTempProjectRoot(): Promise<string> {
|
|
11
|
+
return mkdtemp(join(tmpdir(), "nvst-state-test-"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function writeStateFile(projectRoot: string, content: string): Promise<void> {
|
|
15
|
+
const statePath = join(projectRoot, STATE_REL_PATH);
|
|
16
|
+
await mkdir(join(projectRoot, ".agents"), { recursive: true });
|
|
17
|
+
await writeFile(statePath, content, "utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const createdProjectRoots: string[] = [];
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Promise.all(
|
|
24
|
+
createdProjectRoots.splice(0).map((projectRoot) => rm(projectRoot, { recursive: true, force: true })),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("readState", () => {
|
|
29
|
+
test("uses StateSchema.safeParse in source", async () => {
|
|
30
|
+
const source = await readFile(join(process.cwd(), "src", "state.ts"), "utf8");
|
|
31
|
+
expect(source).toContain("StateSchema.safeParse");
|
|
32
|
+
expect(source).not.toContain("StateSchema.parse(");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("returns schema validation failure with safeParse error as cause", async () => {
|
|
36
|
+
const projectRoot = await createTempProjectRoot();
|
|
37
|
+
createdProjectRoots.push(projectRoot);
|
|
38
|
+
|
|
39
|
+
await writeStateFile(projectRoot, JSON.stringify({ current_iteration: "000001" }));
|
|
40
|
+
|
|
41
|
+
await expect(readState(projectRoot)).rejects.toMatchObject({
|
|
42
|
+
message: expect.stringContaining("failed schema validation"),
|
|
43
|
+
cause: expect.any(ZodError),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("surfaces descriptive ENOENT message when state file is missing", async () => {
|
|
48
|
+
const projectRoot = await createTempProjectRoot();
|
|
49
|
+
createdProjectRoots.push(projectRoot);
|
|
50
|
+
|
|
51
|
+
await expect(readState(projectRoot)).rejects.toThrow(
|
|
52
|
+
`State file not found at ${join(projectRoot, STATE_REL_PATH)}.`,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("surfaces descriptive malformed JSON message", async () => {
|
|
57
|
+
const projectRoot = await createTempProjectRoot();
|
|
58
|
+
createdProjectRoots.push(projectRoot);
|
|
59
|
+
|
|
60
|
+
await writeStateFile(projectRoot, "{");
|
|
61
|
+
|
|
62
|
+
await expect(readState(projectRoot)).rejects.toThrow(
|
|
63
|
+
`Malformed JSON in state file at ${join(projectRoot, STATE_REL_PATH)}.`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { StateSchema, type State } from "../scaffold/schemas/tmpl_state";
|
|
5
|
+
|
|
6
|
+
export const STATE_REL_PATH = join(".agents", "state.json");
|
|
7
|
+
export const FLOW_REL_DIR = join(".agents", "flow");
|
|
8
|
+
|
|
9
|
+
export async function exists(path: string): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
await access(path);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readState(projectRoot: string): Promise<State> {
|
|
19
|
+
const statePath = join(projectRoot, STATE_REL_PATH);
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(statePath, "utf8");
|
|
22
|
+
let json: unknown;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
json = JSON.parse(raw);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof SyntaxError) {
|
|
28
|
+
throw new Error(`Malformed JSON in state file at ${statePath}.`, { cause: error });
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parsed = StateSchema.safeParse(json);
|
|
34
|
+
if (!parsed.success) {
|
|
35
|
+
throw new Error(`State file at ${statePath} failed schema validation.`, {
|
|
36
|
+
cause: parsed.error,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return parsed.data;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
43
|
+
throw new Error(`State file not found at ${statePath}.`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writeState(projectRoot: string, state: State): Promise<void> {
|
|
50
|
+
const statePath = join(projectRoot, STATE_REL_PATH);
|
|
51
|
+
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
52
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext"],
|
|
7
|
+
"types": ["node", "bun-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "."
|
|
12
|
+
},
|
|
13
|
+
"include": ["scaffold/schemas/**/*.ts", "src/**/*.ts"],
|
|
14
|
+
"exclude": ["scaffold/schemas/node-shims.d.ts"]
|
|
15
|
+
}
|