@pedrocivita/tocket 1.2.0 → 2.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.
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { success, warn, info, dim, heading } from "../utils/theme.js";
4
+ import { getCurrentBranch, isGitRepo } from "../utils/git.js";
5
+ function extractFocus(content) {
6
+ const match = content.match(/## Current Focus\s*\n+(.+)/);
7
+ if (!match?.[1])
8
+ return "";
9
+ const line = match[1].trim();
10
+ if (line.startsWith("_") || line.includes("No active tasks"))
11
+ return "";
12
+ return line.length > 80 ? line.substring(0, 77) + "..." : line;
13
+ }
14
+ function daysSince(filePath) {
15
+ const stats = statSync(filePath);
16
+ return Math.floor((Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24));
17
+ }
18
+ function countContextFiles(contextDir) {
19
+ const expected = [
20
+ "activeContext.md",
21
+ "systemPatterns.md",
22
+ "productContext.md",
23
+ "techContext.md",
24
+ "progress.md",
25
+ ];
26
+ return expected.filter((f) => existsSync(join(contextDir, f))).length;
27
+ }
28
+ export function registerStatusCommand(program) {
29
+ program
30
+ .command("status")
31
+ .description("Show a quick overview of the current workspace")
32
+ .action(() => {
33
+ const cwd = process.cwd();
34
+ const contextDir = join(cwd, ".context");
35
+ const hasWorkspace = existsSync(contextDir);
36
+ console.log(heading("\n Tocket Status\n"));
37
+ // Workspace
38
+ if (!hasWorkspace) {
39
+ console.log(" " + warn("No Tocket workspace in this directory."));
40
+ console.log(" " + dim("Run 'tocket init' to get started.\n"));
41
+ return;
42
+ }
43
+ const fileCount = countContextFiles(contextDir);
44
+ console.log(" " + success(`Workspace: ${fileCount}/5 context files`));
45
+ // Focus
46
+ const activeContextPath = join(contextDir, "activeContext.md");
47
+ if (existsSync(activeContextPath)) {
48
+ const content = readFileSync(activeContextPath, "utf-8");
49
+ const focus = extractFocus(content);
50
+ if (focus) {
51
+ console.log(" " + info(`Focus: ${focus}`));
52
+ }
53
+ else {
54
+ console.log(" " + dim(" Focus: (none set)"));
55
+ }
56
+ const days = daysSince(activeContextPath);
57
+ if (days > 7) {
58
+ console.log(" " + warn(`Context last updated ${days} days ago`));
59
+ }
60
+ }
61
+ // Git
62
+ if (isGitRepo(cwd)) {
63
+ const branch = getCurrentBranch(cwd);
64
+ if (branch) {
65
+ console.log(" " + info(`Branch: ${branch}`));
66
+ }
67
+ }
68
+ // Agent configs
69
+ const agents = ["CLAUDE.md", "GEMINI.md", ".cursorrules"].filter((f) => existsSync(join(cwd, f)));
70
+ if (agents.length > 0) {
71
+ console.log(" " + info(`Agents: ${agents.join(", ")}`));
72
+ }
73
+ // Last sync
74
+ const progressPath = join(contextDir, "progress.md");
75
+ if (existsSync(progressPath)) {
76
+ const progressContent = readFileSync(progressPath, "utf-8");
77
+ const sessionMatch = progressContent.match(/## Session: (\d{4}-\d{2}-\d{2})/g);
78
+ if (sessionMatch && sessionMatch.length > 0) {
79
+ const lastSession = sessionMatch[sessionMatch.length - 1].replace("## Session: ", "");
80
+ console.log(" " + dim(` Last sync: ${lastSession}`));
81
+ }
82
+ }
83
+ console.log();
84
+ });
85
+ }
@@ -1,16 +1,10 @@
1
1
  import { input } from "@inquirer/prompts";
2
2
  import { existsSync } from "node:fs";
3
3
  import { appendFile, writeFile } from "node:fs/promises";
4
- import { execSync } from "node:child_process";
5
4
  import { join } from "node:path";
6
- function getRecentCommits() {
7
- try {
8
- return execSync("git log --oneline -5", { encoding: "utf-8" }).trim();
9
- }
10
- catch {
11
- return "_No commits found or not a git repository._";
12
- }
13
- }
5
+ import { success, error as themeError } from "../utils/theme.js";
6
+ import { getRecentCommitsRaw } from "../utils/git.js";
7
+ import { getConfig } from "../utils/config.js";
14
8
  export function registerSyncCommand(program) {
15
9
  program
16
10
  .command("sync")
@@ -19,16 +13,18 @@ export function registerSyncCommand(program) {
19
13
  const progressPath = join(process.cwd(), ".context", "progress.md");
20
14
  const contextDir = join(process.cwd(), ".context");
21
15
  if (!existsSync(contextDir)) {
22
- console.error("\x1b[31mMemory Bank not found. Run 'tocket init' first.\x1b[0m");
16
+ console.error(themeError("Memory Bank not found. Run 'tocket init' first."));
23
17
  process.exitCode = 1;
24
18
  return;
25
19
  }
20
+ const config = await getConfig();
26
21
  const summary = await input({
27
22
  message: "What did you accomplish in this session?",
28
23
  });
29
- const commits = getRecentCommits();
24
+ const commits = getRecentCommitsRaw();
30
25
  const date = new Date().toISOString().split("T")[0];
31
- const block = `## Session: ${date}
26
+ const authorTag = config.author ? ` (${config.author})` : "";
27
+ const block = `## Session: ${date}${authorTag}
32
28
 
33
29
  **Summary**: ${summary}
34
30
 
@@ -46,6 +42,6 @@ ${commits}
46
42
  else {
47
43
  await appendFile(progressPath, block, "utf-8");
48
44
  }
49
- console.log("\n\x1b[32m✓\x1b[0m Memory Bank synchronized successfully!");
45
+ console.log("\n" + success("Memory Bank synchronized!"));
50
46
  });
51
47
  }
@@ -1,8 +1,9 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- const PASS = "\x1b[32m\u2713\x1b[0m";
4
- const WARN = "\x1b[33m\u26A0\x1b[0m";
5
- const FAIL = "\x1b[31m\u2717\x1b[0m";
3
+ import { success as themePass, warn as themeWarn, error as themeFail, heading } from "../utils/theme.js";
4
+ const PASS = themePass("");
5
+ const WARN = themeWarn("");
6
+ const FAIL = themeFail("");
6
7
  function checkFile(basePath, relativePath, required) {
7
8
  const fullPath = join(basePath, relativePath);
8
9
  if (existsSync(fullPath)) {
@@ -55,7 +56,7 @@ export function registerValidateCommand(program) {
55
56
  const cwd = process.cwd();
56
57
  const results = [];
57
58
  let hasFailure = false;
58
- console.log("\nValidating Tocket workspace...\n");
59
+ console.log(heading("\nValidating Tocket workspace...\n"));
59
60
  // Required files
60
61
  results.push(checkFile(cwd, join(".context"), true));
61
62
  results.push(checkFile(cwd, join(".context", "activeContext.md"), true));
@@ -80,11 +81,11 @@ export function registerValidateCommand(program) {
80
81
  }
81
82
  console.log("");
82
83
  if (hasFailure) {
83
- console.log("\x1b[31mWorkspace has issues.\x1b[0m Run \x1b[1mtocket init\x1b[0m to scaffold missing files.");
84
+ console.log(themeFail("Workspace has issues.") + " Run tocket init to scaffold missing files.");
84
85
  process.exitCode = 1;
85
86
  }
86
87
  else {
87
- console.log("\x1b[32mWorkspace is healthy.\x1b[0m");
88
+ console.log(themePass("Workspace is healthy."));
88
89
  }
89
90
  });
90
91
  }
package/dist/index.js CHANGED
@@ -1,16 +1,35 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import { Command } from "commander";
3
5
  import { registerInitCommand } from "./commands/init.cmd.js";
4
6
  import { registerGenerateCommand } from "./commands/generate.cmd.js";
5
7
  import { registerSyncCommand } from "./commands/sync.cmd.js";
6
8
  import { registerValidateCommand } from "./commands/validate.cmd.js";
9
+ import { registerConfigCommand } from "./commands/config.cmd.js";
10
+ import { registerEjectCommand } from "./commands/eject.cmd.js";
11
+ import { registerFocusCommand } from "./commands/focus.cmd.js";
12
+ import { registerStatusCommand } from "./commands/status.cmd.js";
13
+ const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
7
14
  const program = new Command();
8
15
  program
9
16
  .name("tocket")
10
17
  .description("The Context Engineering Framework for Multi-Agent Workspaces")
11
- .version("1.2.0");
18
+ .version(pkg.version);
12
19
  registerInitCommand(program);
13
20
  registerGenerateCommand(program);
14
21
  registerSyncCommand(program);
15
22
  registerValidateCommand(program);
16
- program.parse();
23
+ registerConfigCommand(program);
24
+ registerEjectCommand(program);
25
+ registerFocusCommand(program);
26
+ registerStatusCommand(program);
27
+ // No-args: show interactive dashboard (TTY) or help (non-TTY)
28
+ const args = process.argv.slice(2);
29
+ if (args.length === 0 && process.stdin.isTTY) {
30
+ const { showDashboard } = await import("./commands/dashboard.js");
31
+ await showDashboard(program);
32
+ }
33
+ else {
34
+ program.parse();
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { getConfig, saveConfig, updateConfig, resetConfig, } from "../utils/config.js";
7
+ describe("config - read and write", () => {
8
+ let tempDir;
9
+ let configPath;
10
+ before(() => {
11
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-config-test-"));
12
+ configPath = join(tempDir, ".tocketrc.json");
13
+ });
14
+ after(() => {
15
+ rmSync(tempDir, { recursive: true, force: true });
16
+ });
17
+ it("getConfig returns empty object when file does not exist", async () => {
18
+ const config = await getConfig(configPath);
19
+ assert.deepEqual(config, {});
20
+ });
21
+ it("saveConfig writes a valid config", async () => {
22
+ const data = { author: "Test User" };
23
+ await saveConfig(data, configPath);
24
+ const config = await getConfig(configPath);
25
+ assert.equal(config.author, "Test User");
26
+ });
27
+ it("saveConfig preserves all fields", async () => {
28
+ const data = {
29
+ author: "Alice",
30
+ defaultAgent: "Claude",
31
+ defaults: { priority: "high", skills: "core,lsp" },
32
+ theme: { disableBanner: true },
33
+ };
34
+ await saveConfig(data, configPath);
35
+ const config = await getConfig(configPath);
36
+ assert.equal(config.author, "Alice");
37
+ assert.equal(config.defaultAgent, "Claude");
38
+ assert.equal(config.defaults?.priority, "high");
39
+ assert.equal(config.defaults?.skills, "core,lsp");
40
+ assert.equal(config.theme?.disableBanner, true);
41
+ });
42
+ it("updateConfig merges without overwriting", async () => {
43
+ await saveConfig({ author: "Bob", defaultAgent: "Gemini" }, configPath);
44
+ await updateConfig({ author: "Charlie" }, configPath);
45
+ const config = await getConfig(configPath);
46
+ assert.equal(config.author, "Charlie");
47
+ assert.equal(config.defaultAgent, "Gemini");
48
+ });
49
+ it("updateConfig merges nested defaults", async () => {
50
+ await saveConfig({ defaults: { priority: "low", skills: "a,b" } }, configPath);
51
+ await updateConfig({ defaults: { priority: "high" } }, configPath);
52
+ const config = await getConfig(configPath);
53
+ assert.equal(config.defaults?.priority, "high");
54
+ assert.equal(config.defaults?.skills, "a,b");
55
+ });
56
+ it("resetConfig clears all data", async () => {
57
+ await saveConfig({ author: "Remove Me" }, configPath);
58
+ await resetConfig(configPath);
59
+ const config = await getConfig(configPath);
60
+ assert.deepEqual(config, {});
61
+ });
62
+ });
63
+ describe("config - error handling", () => {
64
+ it("getConfig returns empty object for non-existent path", async () => {
65
+ const config = await getConfig("/nonexistent/path/.tocketrc.json");
66
+ assert.deepEqual(config, {});
67
+ });
68
+ });
@@ -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,61 @@
1
+ import { describe, it, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { isGitRepo, getStagedFiles, getModifiedFiles, getRecentCommits, getRecentCommitsRaw, getCurrentBranch, } from "../utils/git.js";
7
+ // Use the Tocket repo itself for "in git" tests
8
+ const tocketRoot = join(import.meta.dirname, "..", "..");
9
+ describe("git - in a git repository", () => {
10
+ it("isGitRepo returns true", () => {
11
+ assert.equal(isGitRepo(tocketRoot), true);
12
+ });
13
+ it("getRecentCommits returns an array with items", () => {
14
+ const commits = getRecentCommits(3, tocketRoot);
15
+ assert.ok(Array.isArray(commits));
16
+ assert.ok(commits.length > 0);
17
+ });
18
+ it("getRecentCommitsRaw returns a non-empty string", () => {
19
+ const raw = getRecentCommitsRaw(3, tocketRoot);
20
+ assert.ok(raw.length > 0);
21
+ assert.ok(!raw.includes("No commits found"));
22
+ });
23
+ it("getCurrentBranch returns a non-empty string", () => {
24
+ const branch = getCurrentBranch(tocketRoot);
25
+ assert.ok(typeof branch === "string");
26
+ assert.ok(branch.length > 0);
27
+ });
28
+ it("getStagedFiles returns an array", () => {
29
+ const files = getStagedFiles(tocketRoot);
30
+ assert.ok(Array.isArray(files));
31
+ });
32
+ it("getModifiedFiles returns an array", () => {
33
+ const files = getModifiedFiles(tocketRoot);
34
+ assert.ok(Array.isArray(files));
35
+ });
36
+ });
37
+ describe("git - in a non-git directory", () => {
38
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-git-test-"));
39
+ after(() => {
40
+ rmSync(tempDir, { recursive: true, force: true });
41
+ });
42
+ it("isGitRepo returns false", () => {
43
+ assert.equal(isGitRepo(tempDir), false);
44
+ });
45
+ it("getStagedFiles returns empty array", () => {
46
+ assert.deepEqual(getStagedFiles(tempDir), []);
47
+ });
48
+ it("getModifiedFiles returns empty array", () => {
49
+ assert.deepEqual(getModifiedFiles(tempDir), []);
50
+ });
51
+ it("getRecentCommits returns empty array", () => {
52
+ assert.deepEqual(getRecentCommits(5, tempDir), []);
53
+ });
54
+ it("getRecentCommitsRaw returns fallback message", () => {
55
+ const raw = getRecentCommitsRaw(5, tempDir);
56
+ assert.ok(raw.includes("No commits found"));
57
+ });
58
+ it("getCurrentBranch returns empty string", () => {
59
+ assert.equal(getCurrentBranch(tempDir), "");
60
+ });
61
+ });
@@ -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 {};
@@ -0,0 +1,58 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { success, error, warn, info, heading, dim, banner } from "../utils/theme.js";
4
+ describe("theme - success", () => {
5
+ it("returns a string containing the message", () => {
6
+ const result = success("done");
7
+ assert.ok(result.includes("done"));
8
+ });
9
+ it("contains a checkmark character", () => {
10
+ const result = success("ok");
11
+ assert.ok(result.includes("\u2713"));
12
+ });
13
+ });
14
+ describe("theme - error", () => {
15
+ it("returns a string containing the message", () => {
16
+ const result = error("fail");
17
+ assert.ok(result.includes("fail"));
18
+ });
19
+ it("contains an X character", () => {
20
+ const result = error("bad");
21
+ assert.ok(result.includes("\u2717"));
22
+ });
23
+ });
24
+ describe("theme - warn", () => {
25
+ it("returns a string containing the message", () => {
26
+ const result = warn("caution");
27
+ assert.ok(result.includes("caution"));
28
+ });
29
+ });
30
+ describe("theme - info", () => {
31
+ it("returns a string containing the message", () => {
32
+ const result = info("notice");
33
+ assert.ok(result.includes("notice"));
34
+ });
35
+ });
36
+ describe("theme - heading", () => {
37
+ it("returns a non-empty string", () => {
38
+ const result = heading("Title");
39
+ assert.ok(result.length > 0);
40
+ assert.ok(result.includes("Title"));
41
+ });
42
+ });
43
+ describe("theme - dim", () => {
44
+ it("returns a string containing the message", () => {
45
+ const result = dim("faded");
46
+ assert.ok(result.includes("faded"));
47
+ });
48
+ });
49
+ describe("theme - banner", () => {
50
+ it("returns a long string", () => {
51
+ const result = banner();
52
+ assert.ok(result.length > 100);
53
+ });
54
+ it("contains framework tagline", () => {
55
+ const result = banner();
56
+ assert.ok(result.includes("Context Engineering Framework"));
57
+ });
58
+ });