@pedrocivita/tocket 2.1.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.
- package/README.md +193 -141
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/doctor.cmd.d.ts +9 -0
- package/dist/commands/doctor.cmd.js +191 -0
- package/dist/commands/generate.cmd.d.ts +8 -0
- package/dist/commands/generate.cmd.js +27 -10
- package/dist/commands/init.cmd.js +15 -3
- package/dist/commands/lint.cmd.d.ts +12 -0
- package/dist/commands/lint.cmd.js +269 -0
- package/dist/commands/sync.cmd.js +3 -2
- package/dist/commands/validate.cmd.d.ts +7 -0
- package/dist/commands/validate.cmd.js +3 -3
- package/dist/index.js +4 -0
- package/dist/tests/doctor.test.d.ts +1 -0
- package/dist/tests/doctor.test.js +102 -0
- package/dist/tests/generate.test.d.ts +1 -0
- package/dist/tests/generate.test.js +67 -0
- package/dist/tests/git.test.js +9 -1
- package/dist/tests/init.test.d.ts +1 -0
- package/dist/tests/init.test.js +69 -0
- package/dist/tests/lint.test.d.ts +1 -0
- package/dist/tests/lint.test.js +120 -0
- package/dist/tests/validate.test.d.ts +1 -0
- package/dist/tests/validate.test.js +92 -0
- package/dist/utils/git.d.ts +1 -0
- package/dist/utils/git.js +13 -0
- package/package.json +1 -1
|
@@ -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,92 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { checkFile, checkStale, checkAgentFile } from "../commands/validate.cmd.js";
|
|
7
|
+
describe("checkFile", () => {
|
|
8
|
+
const tempDir = mkdtempSync(join(tmpdir(), "tocket-validate-test-"));
|
|
9
|
+
after(() => {
|
|
10
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
it("returns pass icon when required file exists", () => {
|
|
13
|
+
writeFileSync(join(tempDir, "exists.md"), "content", "utf-8");
|
|
14
|
+
const result = checkFile(tempDir, "exists.md", true);
|
|
15
|
+
assert.ok(result.message.includes("found"));
|
|
16
|
+
});
|
|
17
|
+
it("returns fail icon when required file is missing", () => {
|
|
18
|
+
const result = checkFile(tempDir, "missing.md", true);
|
|
19
|
+
assert.ok(result.message.includes("missing (required)"));
|
|
20
|
+
});
|
|
21
|
+
it("returns warn icon when optional file is missing", () => {
|
|
22
|
+
const result = checkFile(tempDir, "optional.md", false);
|
|
23
|
+
assert.ok(result.message.includes("missing (optional)"));
|
|
24
|
+
});
|
|
25
|
+
it("returns pass icon when optional file exists", () => {
|
|
26
|
+
writeFileSync(join(tempDir, "opt-exists.md"), "content", "utf-8");
|
|
27
|
+
const result = checkFile(tempDir, "opt-exists.md", false);
|
|
28
|
+
assert.ok(result.message.includes("found"));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("checkStale", () => {
|
|
32
|
+
const tempDir = mkdtempSync(join(tmpdir(), "tocket-stale-test-"));
|
|
33
|
+
after(() => {
|
|
34
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
it("returns null when file does not exist", () => {
|
|
37
|
+
const result = checkStale(tempDir, "nonexistent.md");
|
|
38
|
+
assert.equal(result, null);
|
|
39
|
+
});
|
|
40
|
+
it("returns null when file is fresh (< 7 days)", () => {
|
|
41
|
+
writeFileSync(join(tempDir, "fresh.md"), "content", "utf-8");
|
|
42
|
+
const result = checkStale(tempDir, "fresh.md");
|
|
43
|
+
assert.equal(result, null);
|
|
44
|
+
});
|
|
45
|
+
it("returns warn result when file is stale (> 7 days)", () => {
|
|
46
|
+
const filePath = join(tempDir, "stale.md");
|
|
47
|
+
writeFileSync(filePath, "content", "utf-8");
|
|
48
|
+
// Set mtime to 10 days ago
|
|
49
|
+
const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000);
|
|
50
|
+
utimesSync(filePath, tenDaysAgo, tenDaysAgo);
|
|
51
|
+
const result = checkStale(tempDir, "stale.md");
|
|
52
|
+
assert.ok(result !== null);
|
|
53
|
+
assert.ok(result.message.includes("days ago"));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("checkAgentFile", () => {
|
|
57
|
+
const tempDir = mkdtempSync(join(tmpdir(), "tocket-agent-test-"));
|
|
58
|
+
after(() => {
|
|
59
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
it("returns pass when CLAUDE.md exists", () => {
|
|
62
|
+
const dir = join(tempDir, "with-claude");
|
|
63
|
+
mkdirSync(dir);
|
|
64
|
+
writeFileSync(join(dir, "CLAUDE.md"), "# Claude", "utf-8");
|
|
65
|
+
const result = checkAgentFile(dir);
|
|
66
|
+
assert.ok(result.message.includes("Agent config found"));
|
|
67
|
+
assert.ok(result.message.includes("CLAUDE.md"));
|
|
68
|
+
});
|
|
69
|
+
it("returns pass when .cursorrules exists", () => {
|
|
70
|
+
const dir = join(tempDir, "with-cursor");
|
|
71
|
+
mkdirSync(dir);
|
|
72
|
+
writeFileSync(join(dir, ".cursorrules"), "rules", "utf-8");
|
|
73
|
+
const result = checkAgentFile(dir);
|
|
74
|
+
assert.ok(result.message.includes("Agent config found"));
|
|
75
|
+
assert.ok(result.message.includes(".cursorrules"));
|
|
76
|
+
});
|
|
77
|
+
it("returns warn when no agent files exist", () => {
|
|
78
|
+
const dir = join(tempDir, "empty");
|
|
79
|
+
mkdirSync(dir);
|
|
80
|
+
const result = checkAgentFile(dir);
|
|
81
|
+
assert.ok(result.message.includes("No agent config file found"));
|
|
82
|
+
});
|
|
83
|
+
it("lists multiple agent files when present", () => {
|
|
84
|
+
const dir = join(tempDir, "multi");
|
|
85
|
+
mkdirSync(dir);
|
|
86
|
+
writeFileSync(join(dir, "CLAUDE.md"), "# Claude", "utf-8");
|
|
87
|
+
writeFileSync(join(dir, "GEMINI.md"), "# Gemini", "utf-8");
|
|
88
|
+
const result = checkAgentFile(dir);
|
|
89
|
+
assert.ok(result.message.includes("CLAUDE.md"));
|
|
90
|
+
assert.ok(result.message.includes("GEMINI.md"));
|
|
91
|
+
});
|
|
92
|
+
});
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export declare function getStagedFiles(cwd?: string): string[];
|
|
|
3
3
|
export declare function getModifiedFiles(cwd?: string): string[];
|
|
4
4
|
export declare function getRecentCommits(n?: number, cwd?: string): string[];
|
|
5
5
|
export declare function getRecentCommitsRaw(n?: number, cwd?: string): string;
|
|
6
|
+
export declare function getLastCommitMessage(cwd?: string): string;
|
|
6
7
|
export declare function getCurrentBranch(cwd?: string): string;
|
package/dist/utils/git.js
CHANGED
|
@@ -64,6 +64,19 @@ export function getRecentCommitsRaw(n = 5, cwd = process.cwd()) {
|
|
|
64
64
|
return "_No commits found or not a git repository._";
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
export function getLastCommitMessage(cwd = process.cwd()) {
|
|
68
|
+
if (!isGitRepo(cwd))
|
|
69
|
+
return "";
|
|
70
|
+
try {
|
|
71
|
+
return execSync("git log -1 --pretty=%B", {
|
|
72
|
+
cwd,
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
}).trim();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
67
80
|
export function getCurrentBranch(cwd = process.cwd()) {
|
|
68
81
|
if (!isGitRepo(cwd))
|
|
69
82
|
return "";
|