@pedrocivita/tocket 2.0.0 → 2.2.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.
Files changed (39) hide show
  1. package/README.md +193 -132
  2. package/dist/commands/dashboard.js +5 -0
  3. package/dist/commands/doctor.cmd.d.ts +9 -0
  4. package/dist/commands/doctor.cmd.js +191 -0
  5. package/dist/commands/eject.cmd.d.ts +6 -0
  6. package/dist/commands/eject.cmd.js +65 -0
  7. package/dist/commands/focus.cmd.d.ts +7 -0
  8. package/dist/commands/focus.cmd.js +52 -0
  9. package/dist/commands/generate.cmd.d.ts +8 -0
  10. package/dist/commands/generate.cmd.js +27 -10
  11. package/dist/commands/init.cmd.js +15 -3
  12. package/dist/commands/lint.cmd.d.ts +12 -0
  13. package/dist/commands/lint.cmd.js +269 -0
  14. package/dist/commands/status.cmd.d.ts +2 -0
  15. package/dist/commands/status.cmd.js +85 -0
  16. package/dist/commands/sync.cmd.js +3 -2
  17. package/dist/commands/validate.cmd.d.ts +7 -0
  18. package/dist/commands/validate.cmd.js +3 -3
  19. package/dist/index.js +10 -0
  20. package/dist/tests/doctor.test.d.ts +1 -0
  21. package/dist/tests/doctor.test.js +102 -0
  22. package/dist/tests/eject.test.d.ts +1 -0
  23. package/dist/tests/eject.test.js +88 -0
  24. package/dist/tests/focus.test.d.ts +1 -0
  25. package/dist/tests/focus.test.js +130 -0
  26. package/dist/tests/generate.test.d.ts +1 -0
  27. package/dist/tests/generate.test.js +67 -0
  28. package/dist/tests/git.test.js +9 -1
  29. package/dist/tests/init.test.d.ts +1 -0
  30. package/dist/tests/init.test.js +69 -0
  31. package/dist/tests/lint.test.d.ts +1 -0
  32. package/dist/tests/lint.test.js +120 -0
  33. package/dist/tests/status.test.d.ts +1 -0
  34. package/dist/tests/status.test.js +34 -0
  35. package/dist/tests/validate.test.d.ts +1 -0
  36. package/dist/tests/validate.test.js +92 -0
  37. package/dist/utils/git.d.ts +1 -0
  38. package/dist/utils/git.js +13 -0
  39. package/package.json +1 -1
@@ -0,0 +1,102 @@
1
+ import { describe, it, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { checkContentHealth, checkGitTracking, checkStaleness } from "../commands/doctor.cmd.js";
7
+ describe("checkContentHealth", () => {
8
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-doctor-content-"));
9
+ after(() => {
10
+ rmSync(tempDir, { recursive: true, force: true });
11
+ });
12
+ it("detects active focus in activeContext.md", () => {
13
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
14
+ writeFileSync(join(tempDir, ".context", "activeContext.md"), "# Active Context\n\n## Current Focus\n\nWorking on feature X.\n\n## Recent Changes\n", "utf-8");
15
+ const results = checkContentHealth(tempDir);
16
+ const focusResult = results.find((r) => r.message.includes("active focus"));
17
+ assert.ok(focusResult);
18
+ assert.ok(focusResult.message.includes("active focus"));
19
+ });
20
+ it("warns when Current Focus is placeholder", () => {
21
+ const dir = join(tempDir, "placeholder");
22
+ mkdirSync(join(dir, ".context"), { recursive: true });
23
+ writeFileSync(join(dir, ".context", "activeContext.md"), "# Active Context\n\n## Current Focus\n\n_Describe what you're working on._\n", "utf-8");
24
+ const results = checkContentHealth(dir);
25
+ const focusResult = results.find((r) => r.message.includes("no meaningful"));
26
+ assert.ok(focusResult);
27
+ });
28
+ it("detects conventions in systemPatterns.md", () => {
29
+ writeFileSync(join(tempDir, ".context", "systemPatterns.md"), "# Patterns\n\n## Conventions\n\n- Code in English\n- ESM only\n", "utf-8");
30
+ const results = checkContentHealth(tempDir);
31
+ const patternResult = results.find((r) => r.message.includes("documented conventions"));
32
+ assert.ok(patternResult);
33
+ });
34
+ it("warns when systemPatterns.md has no conventions", () => {
35
+ const dir = join(tempDir, "empty-patterns");
36
+ mkdirSync(join(dir, ".context"), { recursive: true });
37
+ writeFileSync(join(dir, ".context", "systemPatterns.md"), "# Patterns\n\nNothing here yet.\n", "utf-8");
38
+ const results = checkContentHealth(dir);
39
+ const patternResult = results.find((r) => r.message.includes("no conventions"));
40
+ assert.ok(patternResult);
41
+ });
42
+ it("validates TOCKET.md contains protocol keywords", () => {
43
+ writeFileSync(join(tempDir, "TOCKET.md"), "# Tocket Protocol\n\nThe payload version is 2.0.\n", "utf-8");
44
+ const results = checkContentHealth(tempDir);
45
+ const tocketResult = results.find((r) => r.message.includes("protocol keywords"));
46
+ assert.ok(tocketResult);
47
+ });
48
+ it("checks agent config references .context/", () => {
49
+ writeFileSync(join(tempDir, "CLAUDE.md"), "# Claude\n\nRead `.context/` before acting.\n", "utf-8");
50
+ const results = checkContentHealth(tempDir);
51
+ const agentResult = results.find((r) => r.message.includes("CLAUDE.md references"));
52
+ assert.ok(agentResult);
53
+ });
54
+ it("warns when agent config does not reference .context/", () => {
55
+ const dir = join(tempDir, "no-ref");
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(join(dir, "CLAUDE.md"), "# Claude\n\nJust some instructions.\n", "utf-8");
58
+ const results = checkContentHealth(dir);
59
+ const agentResult = results.find((r) => r.message.includes("does not reference"));
60
+ assert.ok(agentResult);
61
+ });
62
+ });
63
+ describe("checkGitTracking", () => {
64
+ // Use the Tocket repo itself for git tests
65
+ const tocketRoot = join(import.meta.dirname, "..", "..");
66
+ it("returns results in a git repository", () => {
67
+ const results = checkGitTracking(tocketRoot);
68
+ assert.ok(results.length > 0);
69
+ });
70
+ it("detects .context/ is not gitignored", () => {
71
+ const results = checkGitTracking(tocketRoot);
72
+ const ignoreResult = results.find((r) => r.message.includes("gitignore"));
73
+ assert.ok(ignoreResult);
74
+ assert.ok(ignoreResult.message.includes("not gitignored"));
75
+ });
76
+ it("returns warn for non-git directory", () => {
77
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-doctor-git-"));
78
+ try {
79
+ const results = checkGitTracking(tempDir);
80
+ assert.ok(results.some((r) => r.message.includes("Not a git repository")));
81
+ }
82
+ finally {
83
+ rmSync(tempDir, { recursive: true, force: true });
84
+ }
85
+ });
86
+ });
87
+ describe("checkStaleness", () => {
88
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-doctor-stale-"));
89
+ after(() => {
90
+ rmSync(tempDir, { recursive: true, force: true });
91
+ });
92
+ it("returns null when .context/activeContext.md does not exist", () => {
93
+ const result = checkStaleness(tempDir);
94
+ assert.equal(result, null);
95
+ });
96
+ it("returns null when file is fresh", () => {
97
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
98
+ writeFileSync(join(tempDir, ".context", "activeContext.md"), "fresh", "utf-8");
99
+ const result = checkStaleness(tempDir);
100
+ assert.equal(result, null);
101
+ });
102
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, } from "node:fs";
4
+ import { rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { EJECT_FILES, EJECT_DIRS } from "../commands/eject.cmd.js";
8
+ describe("eject - constants", () => {
9
+ it("EJECT_FILES contains all expected files", () => {
10
+ assert.ok(EJECT_FILES.includes("TOCKET.md"));
11
+ assert.ok(EJECT_FILES.includes("CLAUDE.md"));
12
+ assert.ok(EJECT_FILES.includes("GEMINI.md"));
13
+ assert.ok(EJECT_FILES.includes(".cursorrules"));
14
+ });
15
+ it("EJECT_DIRS contains .context", () => {
16
+ assert.ok(EJECT_DIRS.includes(".context"));
17
+ });
18
+ });
19
+ describe("eject - removal logic", () => {
20
+ let tempDir;
21
+ before(() => {
22
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-eject-test-"));
23
+ });
24
+ after(() => {
25
+ rmSync(tempDir, { recursive: true, force: true });
26
+ });
27
+ it("removes .context directory recursively", async () => {
28
+ const contextDir = join(tempDir, ".context");
29
+ mkdirSync(contextDir, { recursive: true });
30
+ writeFileSync(join(contextDir, "activeContext.md"), "test", "utf-8");
31
+ writeFileSync(join(contextDir, "systemPatterns.md"), "test", "utf-8");
32
+ assert.ok(existsSync(contextDir));
33
+ await rm(contextDir, { recursive: true, force: true });
34
+ assert.ok(!existsSync(contextDir));
35
+ });
36
+ it("removes individual eject files", async () => {
37
+ for (const file of EJECT_FILES) {
38
+ const fullPath = join(tempDir, file);
39
+ writeFileSync(fullPath, "test content", "utf-8");
40
+ assert.ok(existsSync(fullPath));
41
+ await rm(fullPath, { force: true });
42
+ assert.ok(!existsSync(fullPath));
43
+ }
44
+ });
45
+ it("does not error when files are already missing", async () => {
46
+ for (const file of EJECT_FILES) {
47
+ const fullPath = join(tempDir, file);
48
+ assert.ok(!existsSync(fullPath));
49
+ await rm(fullPath, { force: true });
50
+ }
51
+ });
52
+ it("does not error when .context is already missing", async () => {
53
+ const contextDir = join(tempDir, ".context");
54
+ assert.ok(!existsSync(contextDir));
55
+ await rm(contextDir, { recursive: true, force: true });
56
+ });
57
+ });
58
+ describe("eject - partial workspace", () => {
59
+ let tempDir;
60
+ before(() => {
61
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-eject-partial-"));
62
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
63
+ writeFileSync(join(tempDir, ".context", "activeContext.md"), "data", "utf-8");
64
+ writeFileSync(join(tempDir, "TOCKET.md"), "data", "utf-8");
65
+ });
66
+ after(() => {
67
+ rmSync(tempDir, { recursive: true, force: true });
68
+ });
69
+ it("removes existing files and skips missing ones", async () => {
70
+ assert.ok(existsSync(join(tempDir, ".context")));
71
+ assert.ok(existsSync(join(tempDir, "TOCKET.md")));
72
+ assert.ok(!existsSync(join(tempDir, "CLAUDE.md")));
73
+ for (const dir of EJECT_DIRS) {
74
+ const fullPath = join(tempDir, dir);
75
+ if (existsSync(fullPath)) {
76
+ await rm(fullPath, { recursive: true, force: true });
77
+ }
78
+ }
79
+ for (const file of EJECT_FILES) {
80
+ const fullPath = join(tempDir, file);
81
+ if (existsSync(fullPath)) {
82
+ await rm(fullPath, { force: true });
83
+ }
84
+ }
85
+ assert.ok(!existsSync(join(tempDir, ".context")));
86
+ assert.ok(!existsSync(join(tempDir, "TOCKET.md")));
87
+ });
88
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,130 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { replaceFocusSection } from "../commands/focus.cmd.js";
7
+ describe("replaceFocusSection", () => {
8
+ it("replaces single-line focus content", () => {
9
+ const input = `# Active Context
10
+
11
+ ## Current Focus
12
+
13
+ Old focus text here.
14
+
15
+ ## Recent Changes
16
+
17
+ Some changes.
18
+ `;
19
+ const result = replaceFocusSection(input, "New focus message");
20
+ assert.ok(result.includes("New focus message"));
21
+ assert.ok(!result.includes("Old focus text here"));
22
+ assert.ok(result.includes("## Recent Changes"));
23
+ });
24
+ it("replaces multi-line focus content", () => {
25
+ const input = `# Active Context
26
+
27
+ ## Current Focus
28
+
29
+ **v2.0.0 — UX Overhaul.** Added purple theme, dashboard,
30
+ config command, smart generate, and 29 new tests.
31
+ All decisions resolved.
32
+
33
+ ## Recent Changes
34
+
35
+ | Date | Change |
36
+ `;
37
+ const result = replaceFocusSection(input, "Working on v2.1.0 features");
38
+ assert.ok(result.includes("Working on v2.1.0 features"));
39
+ assert.ok(!result.includes("v2.0.0"));
40
+ assert.ok(result.includes("## Recent Changes"));
41
+ });
42
+ it("preserves sections before and after Current Focus", () => {
43
+ const input = `# Active Context
44
+
45
+ <!-- comment -->
46
+
47
+ ## Current Focus
48
+
49
+ Old stuff.
50
+
51
+ ## Recent Changes
52
+
53
+ Data here.
54
+
55
+ ## Open Decisions
56
+
57
+ Decisions here.
58
+ `;
59
+ const result = replaceFocusSection(input, "Brand new focus");
60
+ assert.ok(result.includes("# Active Context"));
61
+ assert.ok(result.includes("<!-- comment -->"));
62
+ assert.ok(result.includes("## Recent Changes"));
63
+ assert.ok(result.includes("Data here."));
64
+ assert.ok(result.includes("## Open Decisions"));
65
+ assert.ok(result.includes("Decisions here."));
66
+ });
67
+ it("appends section when ## Current Focus is missing", () => {
68
+ const input = `# Active Context
69
+
70
+ ## Recent Changes
71
+
72
+ Some data.
73
+ `;
74
+ const result = replaceFocusSection(input, "New focus");
75
+ assert.ok(result.includes("## Current Focus"));
76
+ assert.ok(result.includes("New focus"));
77
+ assert.ok(result.includes("## Recent Changes"));
78
+ });
79
+ it("handles Current Focus at end of file (no following section)", () => {
80
+ const input = `# Active Context
81
+
82
+ ## Current Focus
83
+
84
+ Last section, nothing after this.
85
+ `;
86
+ const result = replaceFocusSection(input, "Updated final section");
87
+ assert.ok(result.includes("Updated final section"));
88
+ assert.ok(!result.includes("Last section"));
89
+ });
90
+ it("handles empty content between heading and next section", () => {
91
+ const input = `## Current Focus
92
+
93
+ ## Recent Changes
94
+ `;
95
+ const result = replaceFocusSection(input, "Fill the gap");
96
+ assert.ok(result.includes("Fill the gap"));
97
+ assert.ok(result.includes("## Recent Changes"));
98
+ });
99
+ });
100
+ describe("focus - file integration", () => {
101
+ let tempDir;
102
+ before(() => {
103
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-focus-test-"));
104
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
105
+ });
106
+ after(() => {
107
+ rmSync(tempDir, { recursive: true, force: true });
108
+ });
109
+ it("correctly round-trips through file read/write", () => {
110
+ const filePath = join(tempDir, ".context", "activeContext.md");
111
+ const original = `# Active Context - Test
112
+
113
+ ## Current Focus
114
+
115
+ Old focus.
116
+
117
+ ## Recent Changes
118
+
119
+ Nothing yet.
120
+ `;
121
+ writeFileSync(filePath, original, "utf-8");
122
+ const content = readFileSync(filePath, "utf-8");
123
+ const updated = replaceFocusSection(content, "Round-tripped focus");
124
+ writeFileSync(filePath, updated, "utf-8");
125
+ const final = readFileSync(filePath, "utf-8");
126
+ assert.ok(final.includes("Round-tripped focus"));
127
+ assert.ok(!final.includes("Old focus"));
128
+ assert.ok(final.includes("## Recent Changes"));
129
+ });
130
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { buildPayloadXml } from "../commands/generate.cmd.js";
4
+ describe("buildPayloadXml", () => {
5
+ const baseTask = {
6
+ intent: "Add input validation",
7
+ scope: "src/commands/sync.cmd.ts",
8
+ priority: "medium",
9
+ skills: "",
10
+ };
11
+ it("produces valid XML with version 2.0", () => {
12
+ const xml = buildPayloadXml([baseTask]);
13
+ assert.ok(xml.includes('<payload version="2.0">'));
14
+ assert.ok(xml.includes("</payload>"));
15
+ });
16
+ it("includes intent, scope, and priority in meta", () => {
17
+ const xml = buildPayloadXml([baseTask]);
18
+ assert.ok(xml.includes("<intent>Add input validation</intent>"));
19
+ assert.ok(xml.includes("<scope>src/commands/sync.cmd.ts</scope>"));
20
+ assert.ok(xml.includes("<priority>medium</priority>"));
21
+ });
22
+ it("omits skills tag when skills is empty", () => {
23
+ const xml = buildPayloadXml([baseTask]);
24
+ assert.ok(!xml.includes("<skills>"));
25
+ });
26
+ it("includes skills tag when skills is provided", () => {
27
+ const task = { ...baseTask, skills: "core,lsp" };
28
+ const xml = buildPayloadXml([task]);
29
+ assert.ok(xml.includes("<skills>core,lsp</skills>"));
30
+ });
31
+ it("generates correct task ids for multiple tasks", () => {
32
+ const tasks = [
33
+ { ...baseTask, intent: "Task one" },
34
+ { ...baseTask, intent: "Task two" },
35
+ { ...baseTask, intent: "Task three" },
36
+ ];
37
+ const xml = buildPayloadXml(tasks);
38
+ assert.ok(xml.includes('task id="1"'));
39
+ assert.ok(xml.includes('task id="2"'));
40
+ assert.ok(xml.includes('task id="3"'));
41
+ });
42
+ it("includes action text from each task intent", () => {
43
+ const tasks = [
44
+ { ...baseTask, intent: "First action" },
45
+ { ...baseTask, intent: "Second action" },
46
+ ];
47
+ const xml = buildPayloadXml(tasks);
48
+ assert.ok(xml.includes("<action>First action</action>"));
49
+ assert.ok(xml.includes("<action>Second action</action>"));
50
+ });
51
+ it("includes validate section", () => {
52
+ const xml = buildPayloadXml([baseTask]);
53
+ assert.ok(xml.includes("<validate>"));
54
+ assert.ok(xml.includes("<check>"));
55
+ });
56
+ it("uses first task meta for the payload-level meta", () => {
57
+ const tasks = [
58
+ { intent: "Main goal", scope: "src/", priority: "high", skills: "api" },
59
+ { intent: "Secondary", scope: "test/", priority: "low", skills: "" },
60
+ ];
61
+ const xml = buildPayloadXml(tasks);
62
+ // Meta should come from first task
63
+ assert.ok(xml.includes("<intent>Main goal</intent>"));
64
+ assert.ok(xml.includes("<scope>src/</scope>"));
65
+ assert.ok(xml.includes("<priority>high</priority>"));
66
+ });
67
+ });
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { mkdtempSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { isGitRepo, getStagedFiles, getModifiedFiles, getRecentCommits, getRecentCommitsRaw, getCurrentBranch, } from "../utils/git.js";
6
+ import { isGitRepo, getStagedFiles, getModifiedFiles, getRecentCommits, getRecentCommitsRaw, getCurrentBranch, getLastCommitMessage, } from "../utils/git.js";
7
7
  // Use the Tocket repo itself for "in git" tests
8
8
  const tocketRoot = join(import.meta.dirname, "..", "..");
9
9
  describe("git - in a git repository", () => {
@@ -33,6 +33,11 @@ describe("git - in a git repository", () => {
33
33
  const files = getModifiedFiles(tocketRoot);
34
34
  assert.ok(Array.isArray(files));
35
35
  });
36
+ it("getLastCommitMessage returns a non-empty string", () => {
37
+ const msg = getLastCommitMessage(tocketRoot);
38
+ assert.ok(typeof msg === "string");
39
+ assert.ok(msg.length > 0);
40
+ });
36
41
  });
37
42
  describe("git - in a non-git directory", () => {
38
43
  const tempDir = mkdtempSync(join(tmpdir(), "tocket-git-test-"));
@@ -58,4 +63,7 @@ describe("git - in a non-git directory", () => {
58
63
  it("getCurrentBranch returns empty string", () => {
59
64
  assert.equal(getCurrentBranch(tempDir), "");
60
65
  });
66
+ it("getLastCommitMessage returns empty string", () => {
67
+ assert.equal(getLastCommitMessage(tempDir), "");
68
+ });
61
69
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { describe, it, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, existsSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { execSync } from "node:child_process";
7
+ // Path to the built CLI
8
+ const cliPath = join(import.meta.dirname, "..", "index.js");
9
+ describe("init --minimal", () => {
10
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-init-minimal-"));
11
+ after(() => {
12
+ rmSync(tempDir, { recursive: true, force: true });
13
+ });
14
+ it("creates only essential files with --minimal --name --description --force", () => {
15
+ execSync(`node "${cliPath}" init --minimal --name testproject --description "A test" --force`, { cwd: tempDir, encoding: "utf-8" });
16
+ // Essential files should exist
17
+ assert.ok(existsSync(join(tempDir, ".context", "activeContext.md")));
18
+ assert.ok(existsSync(join(tempDir, ".context", "systemPatterns.md")));
19
+ assert.ok(existsSync(join(tempDir, "TOCKET.md")));
20
+ // Non-essential files should NOT exist
21
+ assert.ok(!existsSync(join(tempDir, "CLAUDE.md")));
22
+ assert.ok(!existsSync(join(tempDir, "GEMINI.md")));
23
+ assert.ok(!existsSync(join(tempDir, ".cursorrules")));
24
+ assert.ok(!existsSync(join(tempDir, ".context", "productContext.md")));
25
+ assert.ok(!existsSync(join(tempDir, ".context", "techContext.md")));
26
+ assert.ok(!existsSync(join(tempDir, ".context", "progress.md")));
27
+ });
28
+ });
29
+ describe("init --name --description (non-interactive)", () => {
30
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-init-flags-"));
31
+ after(() => {
32
+ rmSync(tempDir, { recursive: true, force: true });
33
+ });
34
+ it("creates full workspace without interactive prompts", () => {
35
+ execSync(`node "${cliPath}" init --name flagproject --description "Flag desc" --force`, { cwd: tempDir, encoding: "utf-8" });
36
+ // All 9 files should exist (not minimal)
37
+ assert.ok(existsSync(join(tempDir, ".context", "activeContext.md")));
38
+ assert.ok(existsSync(join(tempDir, ".context", "systemPatterns.md")));
39
+ assert.ok(existsSync(join(tempDir, ".context", "productContext.md")));
40
+ assert.ok(existsSync(join(tempDir, ".context", "techContext.md")));
41
+ assert.ok(existsSync(join(tempDir, ".context", "progress.md")));
42
+ assert.ok(existsSync(join(tempDir, "TOCKET.md")));
43
+ assert.ok(existsSync(join(tempDir, "CLAUDE.md")));
44
+ assert.ok(existsSync(join(tempDir, "GEMINI.md")));
45
+ assert.ok(existsSync(join(tempDir, ".cursorrules")));
46
+ });
47
+ });
48
+ describe("init --minimal file count", () => {
49
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-init-count-"));
50
+ after(() => {
51
+ rmSync(tempDir, { recursive: true, force: true });
52
+ });
53
+ it("minimal creates exactly 3 files + 1 directory", () => {
54
+ execSync(`node "${cliPath}" init --minimal --name counttest --description "test" --force`, { cwd: tempDir, encoding: "utf-8" });
55
+ // Count files in .context/
56
+ const contextDir = join(tempDir, ".context");
57
+ assert.ok(existsSync(contextDir));
58
+ // Exactly 2 files in .context/
59
+ const contextFiles = ["activeContext.md", "systemPatterns.md"];
60
+ for (const f of contextFiles) {
61
+ assert.ok(existsSync(join(contextDir, f)), `${f} should exist`);
62
+ }
63
+ // These should NOT be in .context/ with minimal
64
+ const skippedContextFiles = ["productContext.md", "techContext.md", "progress.md"];
65
+ for (const f of skippedContextFiles) {
66
+ assert.ok(!existsSync(join(contextDir, f)), `${f} should not exist in minimal mode`);
67
+ }
68
+ });
69
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { lintActiveContext, lintSystemPatterns, lintProtocolSpec, lintAgentConfig, } from "../commands/lint.cmd.js";
4
+ describe("lintActiveContext", () => {
5
+ const wellFormed = `# Active Context
6
+
7
+ ## Current Focus
8
+
9
+ Working on feature X.
10
+
11
+ ## Recent Changes
12
+
13
+ | Date | Change | Agent |
14
+ |------|--------|-------|
15
+ | 2026-02-25 | Added X | Claude |
16
+
17
+ ## Open Decisions
18
+
19
+ - Should we use Y or Z?
20
+ `;
21
+ it("passes all checks on well-formed content", () => {
22
+ const results = lintActiveContext(wellFormed);
23
+ const fails = results.filter((r) => r.severity === "fail");
24
+ assert.equal(fails.length, 0);
25
+ });
26
+ it("detects all three required sections", () => {
27
+ const results = lintActiveContext(wellFormed);
28
+ const sectionPasses = results.filter((r) => r.severity === "pass" && r.message.includes("Has ##"));
29
+ assert.equal(sectionPasses.length, 3);
30
+ });
31
+ it("detects populated focus", () => {
32
+ const results = lintActiveContext(wellFormed);
33
+ const focusPass = results.find((r) => r.message.includes("Focus is populated"));
34
+ assert.ok(focusPass);
35
+ });
36
+ it("warns on placeholder focus", () => {
37
+ const placeholder = wellFormed.replace("Working on feature X.", "_Describe what you're working on._");
38
+ const results = lintActiveContext(placeholder);
39
+ const focusWarn = results.find((r) => r.severity === "warn" && r.message.includes("Focus"));
40
+ assert.ok(focusWarn);
41
+ assert.ok(focusWarn.suggestion);
42
+ });
43
+ it("fails when Current Focus section is missing", () => {
44
+ const noFocus = "# Active Context\n\n## Recent Changes\n\n## Open Decisions\n";
45
+ const results = lintActiveContext(noFocus);
46
+ const fail = results.find((r) => r.severity === "fail" && r.message.includes("Current Focus"));
47
+ assert.ok(fail);
48
+ });
49
+ it("fails when Recent Changes section is missing", () => {
50
+ const noRecent = "# Active Context\n\n## Current Focus\n\nDoing X.\n\n## Open Decisions\n";
51
+ const results = lintActiveContext(noRecent);
52
+ const fail = results.find((r) => r.severity === "fail" && r.message.includes("Recent Changes"));
53
+ assert.ok(fail);
54
+ });
55
+ it("reports info when Open Decisions is empty", () => {
56
+ const emptyDecisions = wellFormed.replace("- Should we use Y or Z?", "_List anything unresolved._");
57
+ const results = lintActiveContext(emptyDecisions);
58
+ const info = results.find((r) => r.severity === "info" && r.message.includes("Open Decisions"));
59
+ assert.ok(info);
60
+ });
61
+ });
62
+ describe("lintSystemPatterns", () => {
63
+ it("passes when conventions are documented with bullet points", () => {
64
+ const content = "# Patterns\n\n- Code in English\n- ESM only\n";
65
+ const results = lintSystemPatterns(content);
66
+ const pass = results.find((r) => r.message.includes("documented conventions"));
67
+ assert.ok(pass);
68
+ assert.equal(pass.severity, "pass");
69
+ });
70
+ it("passes when conventions are documented with table rows", () => {
71
+ const content = "# Patterns\n\n| Convention | Detail |\n|---|---|\n| Language | English |\n";
72
+ const results = lintSystemPatterns(content);
73
+ const pass = results.find((r) => r.message.includes("documented conventions"));
74
+ assert.ok(pass);
75
+ });
76
+ it("warns when no conventions exist", () => {
77
+ const content = "# Patterns\n\nNothing here yet.\n";
78
+ const results = lintSystemPatterns(content);
79
+ const warn = results.find((r) => r.severity === "warn");
80
+ assert.ok(warn);
81
+ assert.ok(warn.suggestion);
82
+ });
83
+ });
84
+ describe("lintProtocolSpec", () => {
85
+ it("passes when payload and Memory Bank are referenced", () => {
86
+ const content = "# Protocol\n\nThe payload format...\n\nMemory Bank in .context/\n";
87
+ const results = lintProtocolSpec(content);
88
+ const passes = results.filter((r) => r.severity === "pass");
89
+ assert.equal(passes.length, 2);
90
+ });
91
+ it("warns when payload is not referenced", () => {
92
+ const content = "# Protocol\n\nMemory Bank in .context/\n";
93
+ const results = lintProtocolSpec(content);
94
+ const warn = results.find((r) => r.message.includes("payload"));
95
+ assert.ok(warn);
96
+ assert.equal(warn.severity, "warn");
97
+ });
98
+ it("warns when Memory Bank is not referenced", () => {
99
+ const content = "# Protocol\n\nPayload format is XML.\n";
100
+ const results = lintProtocolSpec(content);
101
+ const warn = results.find((r) => r.message.includes("Memory Bank"));
102
+ assert.ok(warn);
103
+ assert.equal(warn.severity, "warn");
104
+ });
105
+ });
106
+ describe("lintAgentConfig", () => {
107
+ it("passes when .context/ is referenced", () => {
108
+ const content = "# Claude\n\nRead .context/ before acting.\n";
109
+ const results = lintAgentConfig("CLAUDE.md", content);
110
+ assert.equal(results.length, 1);
111
+ assert.equal(results[0].severity, "pass");
112
+ });
113
+ it("warns when .context/ is not referenced", () => {
114
+ const content = "# Claude\n\nJust some instructions.\n";
115
+ const results = lintAgentConfig("CLAUDE.md", content);
116
+ assert.equal(results.length, 1);
117
+ assert.equal(results[0].severity, "warn");
118
+ assert.ok(results[0].suggestion);
119
+ });
120
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ describe("status - workspace detection", () => {
7
+ let tempDir;
8
+ before(() => {
9
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-status-test-"));
10
+ });
11
+ after(() => {
12
+ rmSync(tempDir, { recursive: true, force: true });
13
+ });
14
+ it("detects absence of .context directory", () => {
15
+ const contextDir = join(tempDir, ".context");
16
+ assert.ok(!existsSync(contextDir));
17
+ });
18
+ it("detects presence of .context directory with files", () => {
19
+ const contextDir = join(tempDir, ".context");
20
+ mkdirSync(contextDir, { recursive: true });
21
+ writeFileSync(join(contextDir, "activeContext.md"), "# Active Context\n\n## Current Focus\n\nTesting status command.\n", "utf-8");
22
+ writeFileSync(join(contextDir, "systemPatterns.md"), "test", "utf-8");
23
+ writeFileSync(join(contextDir, "progress.md"), "# Progress\n\n## Session: 2026-02-25\n\n**Summary**: test\n", "utf-8");
24
+ assert.ok(existsSync(contextDir));
25
+ assert.ok(existsSync(join(contextDir, "activeContext.md")));
26
+ });
27
+ it("counts context files correctly", () => {
28
+ const contextDir = join(tempDir, ".context");
29
+ const expected = ["activeContext.md", "systemPatterns.md", "productContext.md", "techContext.md", "progress.md"];
30
+ const count = expected.filter((f) => existsSync(join(contextDir, f))).length;
31
+ // We created 3 files above
32
+ assert.equal(count, 3);
33
+ });
34
+ });
@@ -0,0 +1 @@
1
+ export {};