specsail 0.0.0-stage → 0.1.0
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/ROADMAP.md +19 -0
- package/bin/specsail.js +4 -0
- package/package.json +51 -4
- package/scripts/check-content.mjs +62 -0
- package/scripts/check-protected-paths.sh +20 -0
- package/scripts/new-feature.sh +25 -0
- package/scripts/open-pr.sh +15 -0
- package/scripts/run-agent.sh +44 -0
- package/scripts/start-phase.sh +9 -0
- package/skills/acceptance-tests/SKILL.md +35 -0
- package/skills/implement/SKILL.md +36 -0
- package/skills/plan/SKILL.md +38 -0
- package/skills/review/SKILL.md +32 -0
- package/skills/ship/SKILL.md +27 -0
- package/skills/understand/SKILL.md +32 -0
- package/specs/_templates/blocked.md +15 -0
- package/specs/_templates/plan.md +23 -0
- package/specs/_templates/request.md +19 -0
- package/specs/_templates/review.md +18 -0
- package/specs/_templates/spec.md +20 -0
- package/specs/_templates/tasks.md +7 -0
- package/src/cli.js +297 -0
- package/templates/.github/CODEOWNERS +6 -0
- package/templates/.github/workflows/verify.yml +18 -0
- package/templates/.specsail/AGENTS.md +30 -0
- package/templates/.specsail/agents.env.example +15 -0
- package/templates/.specsail/docs/runbook.md +30 -0
- package/templates/.specsail/docs/workflow.md +47 -0
- package/templates/Makefile +19 -0
- package/templates/docs/architecture/.gitkeep +0 -0
- package/templates/docs/architecture.md +15 -0
- package/templates/docs/project.md +19 -0
- package/test/acceptance/cli.acceptance.test.js +111 -0
- package/test/cli.test.js +24 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { runCli } from "../../src/cli.js";
|
|
7
|
+
|
|
8
|
+
const packageRoot = new URL("../../", import.meta.url).pathname;
|
|
9
|
+
|
|
10
|
+
async function temporaryProject() {
|
|
11
|
+
return mkdtemp(path.join(os.tmpdir(), "specsail-test-"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function streams() {
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
return {
|
|
18
|
+
options: {
|
|
19
|
+
packageRoot,
|
|
20
|
+
stdout: (text) => { stdout += text; },
|
|
21
|
+
stderr: (text) => { stderr += text; },
|
|
22
|
+
},
|
|
23
|
+
stdout: () => stdout,
|
|
24
|
+
stderr: () => stderr,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("init preserves project-owned files, installs the crew, and is repeatable", async (context) => {
|
|
29
|
+
const cwd = await temporaryProject();
|
|
30
|
+
context.after(() => rm(cwd, { recursive: true, force: true }));
|
|
31
|
+
await mkdir(path.join(cwd, "docs"), { recursive: true });
|
|
32
|
+
await writeFile(path.join(cwd, "AGENTS.md"), "# Existing project rules\n", "utf8");
|
|
33
|
+
await writeFile(path.join(cwd, "Makefile"), "verify:\n\t@echo project checks\n", "utf8");
|
|
34
|
+
await writeFile(path.join(cwd, "docs/project.md"), "# My existing project\n", "utf8");
|
|
35
|
+
await writeFile(path.join(cwd, ".specsail.json"), JSON.stringify({ version: "old", localSetting: true }), "utf8");
|
|
36
|
+
const first = streams();
|
|
37
|
+
|
|
38
|
+
assert.equal(await runCli(["init"], { cwd, ...first.options }), 0);
|
|
39
|
+
assert.equal(await readFile(path.join(cwd, "Makefile"), "utf8"), "verify:\n\t@echo project checks\n");
|
|
40
|
+
assert.equal(await readFile(path.join(cwd, "docs/project.md"), "utf8"), "# My existing project\n");
|
|
41
|
+
assert.match(await readFile(path.join(cwd, "AGENTS.md"), "utf8"), /<!-- specsail:start -->/);
|
|
42
|
+
assert.match(await readFile(path.join(cwd, ".specsail/skills/understand/SKILL.md"), "utf8"), /Scout/);
|
|
43
|
+
assert.match(await readFile(path.join(cwd, ".gitignore"), "utf8"), /\.specsail\/agents\.env/);
|
|
44
|
+
assert.deepEqual(JSON.parse(await readFile(path.join(cwd, ".specsail.json"), "utf8")), { version: "0.1.0", localSetting: true });
|
|
45
|
+
|
|
46
|
+
const second = streams();
|
|
47
|
+
assert.equal(await runCli(["init"], { cwd, ...second.options }), 0);
|
|
48
|
+
const agents = await readFile(path.join(cwd, "AGENTS.md"), "utf8");
|
|
49
|
+
assert.equal(agents.split("<!-- specsail:start -->").length - 1, 1);
|
|
50
|
+
assert.equal(agents.split("<!-- specsail:end -->").length - 1, 1);
|
|
51
|
+
const gitignore = await readFile(path.join(cwd, ".gitignore"), "utf8");
|
|
52
|
+
assert.equal(gitignore.split("# >>> SpecSail >>>").length - 1, 1);
|
|
53
|
+
assert.equal(gitignore.split("# <<< SpecSail <<<").length - 1, 1);
|
|
54
|
+
assert.match(second.stdout(), /left \d+ existing file\(s\) unchanged/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("new scaffolds only the four feature artifacts and rejects unsafe or duplicate slugs", async (context) => {
|
|
58
|
+
const cwd = await temporaryProject();
|
|
59
|
+
context.after(() => rm(cwd, { recursive: true, force: true }));
|
|
60
|
+
const initStreams = streams();
|
|
61
|
+
assert.equal(await runCli(["init"], { cwd, ...initStreams.options }), 0);
|
|
62
|
+
|
|
63
|
+
const created = streams();
|
|
64
|
+
assert.equal(await runCli(["new", "password-reset", "new"], { cwd, ...created.options }), 0);
|
|
65
|
+
const spec = await readFile(path.join(cwd, "specs/password-reset/spec.md"), "utf8");
|
|
66
|
+
assert.match(spec, /^# Spec: password-reset/m);
|
|
67
|
+
assert.match(await readFile(path.join(cwd, "specs/password-reset/request.md"), "utf8"), /Type: NEW FEATURE/);
|
|
68
|
+
assert.deepEqual((await (await import("node:fs/promises")).readdir(path.join(cwd, "specs/password-reset"))).sort(), ["plan.md", "request.md", "spec.md", "tasks.md"]);
|
|
69
|
+
|
|
70
|
+
const invalid = streams();
|
|
71
|
+
assert.equal(await runCli(["new", "../escape", "fix"], { cwd, ...invalid.options }), 1);
|
|
72
|
+
assert.match(invalid.stderr(), /Usage: specsail new/);
|
|
73
|
+
const duplicate = streams();
|
|
74
|
+
assert.equal(await runCli(["new", "password-reset", "fix"], { cwd, ...duplicate.options }), 1);
|
|
75
|
+
assert.match(duplicate.stderr(), /refusing to overwrite/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("doctor verifies the kit and the project's verify command", async (context) => {
|
|
79
|
+
const cwd = await temporaryProject();
|
|
80
|
+
context.after(() => rm(cwd, { recursive: true, force: true }));
|
|
81
|
+
const initialized = streams();
|
|
82
|
+
assert.equal(await runCli(["init"], { cwd, ...initialized.options }), 0);
|
|
83
|
+
|
|
84
|
+
const beforeConfig = streams();
|
|
85
|
+
assert.equal(await runCli(["doctor"], { cwd, ...beforeConfig.options }), 1);
|
|
86
|
+
assert.match(beforeConfig.stderr(), /Makefile still contains the SpecSail starter template/);
|
|
87
|
+
|
|
88
|
+
await writeFile(path.join(cwd, "Makefile"), "verify:\n\t@echo configured\n", "utf8");
|
|
89
|
+
const afterConfig = streams();
|
|
90
|
+
assert.equal(await runCli(["doctor"], { cwd, ...afterConfig.options }), 0);
|
|
91
|
+
assert.match(afterConfig.stdout(), /files and verify target are present/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("init refuses to write through a symlinked SpecSail directory", async (context) => {
|
|
95
|
+
const cwd = await temporaryProject();
|
|
96
|
+
const outside = await temporaryProject();
|
|
97
|
+
context.after(async () => {
|
|
98
|
+
await rm(cwd, { recursive: true, force: true });
|
|
99
|
+
await rm(outside, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
await symlink(outside, path.join(cwd, ".specsail"), "dir");
|
|
103
|
+
} catch (failure) {
|
|
104
|
+
context.skip(`symlink creation unavailable: ${failure.message}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const result = streams();
|
|
108
|
+
assert.equal(await runCli(["init"], { cwd, ...result.options }), 1);
|
|
109
|
+
assert.match(result.stderr(), /Refusing to write through a symlink/);
|
|
110
|
+
await assert.rejects(readFile(path.join(outside, "skills/understand/SKILL.md")), { code: "ENOENT" });
|
|
111
|
+
});
|
package/test/cli.test.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { runCli } from "../src/cli.js";
|
|
4
|
+
|
|
5
|
+
const root = new URL("../", import.meta.url).pathname;
|
|
6
|
+
|
|
7
|
+
test("help and version are available without initializing a project", async () => {
|
|
8
|
+
let stdout = "";
|
|
9
|
+
let stderr = "";
|
|
10
|
+
const options = { packageRoot: root, stdout: (text) => { stdout += text; }, stderr: (text) => { stderr += text; } };
|
|
11
|
+
assert.equal(await runCli(["--help"], options), 0);
|
|
12
|
+
assert.match(stdout, /Captain is always human/);
|
|
13
|
+
assert.equal(await runCli(["--version"], options), 0);
|
|
14
|
+
assert.match(stdout, /0\.1\.0/);
|
|
15
|
+
assert.equal(stderr, "");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("unknown commands fail with a useful hint", async () => {
|
|
19
|
+
let stderr = "";
|
|
20
|
+
const status = await runCli(["sail"], { stderr: (text) => { stderr += text; } });
|
|
21
|
+
assert.equal(status, 1);
|
|
22
|
+
assert.match(stderr, /Unknown command: sail/);
|
|
23
|
+
assert.match(stderr, /specsail --help/);
|
|
24
|
+
});
|