@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.
- package/README.md +38 -7
- package/dist/commands/config.cmd.d.ts +2 -0
- package/dist/commands/config.cmd.js +102 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +68 -0
- package/dist/commands/eject.cmd.d.ts +6 -0
- package/dist/commands/eject.cmd.js +65 -0
- package/dist/commands/focus.cmd.d.ts +7 -0
- package/dist/commands/focus.cmd.js +52 -0
- package/dist/commands/generate.cmd.js +81 -29
- package/dist/commands/init.cmd.js +31 -10
- package/dist/commands/status.cmd.d.ts +2 -0
- package/dist/commands/status.cmd.js +85 -0
- package/dist/commands/sync.cmd.js +9 -13
- package/dist/commands/validate.cmd.js +7 -6
- package/dist/index.js +21 -2
- package/dist/tests/config.test.d.ts +1 -0
- package/dist/tests/config.test.js +68 -0
- package/dist/tests/eject.test.d.ts +1 -0
- package/dist/tests/eject.test.js +88 -0
- package/dist/tests/focus.test.d.ts +1 -0
- package/dist/tests/focus.test.js +130 -0
- package/dist/tests/git.test.d.ts +1 -0
- package/dist/tests/git.test.js +61 -0
- package/dist/tests/status.test.d.ts +1 -0
- package/dist/tests/status.test.js +34 -0
- package/dist/tests/theme.test.d.ts +1 -0
- package/dist/tests/theme.test.js +58 -0
- package/dist/utils/config.d.ts +16 -0
- package/dist/utils/config.js +33 -0
- package/dist/utils/git.d.ts +6 -0
- package/dist/utils/git.js +79 -0
- package/dist/utils/theme.d.ts +13 -0
- package/dist/utils/theme.js +38 -0
- package/package.json +2 -1
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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("
|
|
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 =
|
|
24
|
+
const commits = getRecentCommitsRaw();
|
|
30
25
|
const date = new Date().toISOString().split("T")[0];
|
|
31
|
-
const
|
|
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
|
|
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
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
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("
|
|
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("
|
|
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(
|
|
18
|
+
.version(pkg.version);
|
|
12
19
|
registerInitCommand(program);
|
|
13
20
|
registerGenerateCommand(program);
|
|
14
21
|
registerSyncCommand(program);
|
|
15
22
|
registerValidateCommand(program);
|
|
16
|
-
program
|
|
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
|
+
});
|