@pedrocivita/tocket 2.0.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 +9 -0
- package/dist/commands/dashboard.js +3 -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/status.cmd.d.ts +2 -0
- package/dist/commands/status.cmd.js +85 -0
- package/dist/index.js +6 -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/status.test.d.ts +1 -0
- package/dist/tests/status.test.js +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,12 @@ npx @pedrocivita/tocket generate
|
|
|
43
43
|
|
|
44
44
|
# Sync session progress into Memory Bank
|
|
45
45
|
npx @pedrocivita/tocket sync
|
|
46
|
+
|
|
47
|
+
# Update the current focus
|
|
48
|
+
npx @pedrocivita/tocket focus "Refactoring payment module"
|
|
49
|
+
|
|
50
|
+
# Remove all Tocket files (with confirmation)
|
|
51
|
+
npx @pedrocivita/tocket eject
|
|
46
52
|
```
|
|
47
53
|
|
|
48
54
|
## Commands
|
|
@@ -54,7 +60,10 @@ npx @pedrocivita/tocket sync
|
|
|
54
60
|
| `tocket generate` | Smart payload builder — auto-fills scope from git, multi-task support |
|
|
55
61
|
| `tocket sync` | Appends session summary + git log to `.context/progress.md` |
|
|
56
62
|
| `tocket validate` | Checks if the current directory has a valid Tocket Memory Bank |
|
|
63
|
+
| `tocket focus` | Update the Current Focus in `.context/activeContext.md` |
|
|
64
|
+
| `tocket status` | Quick overview of workspace, focus, branch, and agents |
|
|
57
65
|
| `tocket config` | Manage global settings (`~/.tocketrc.json`) |
|
|
66
|
+
| `tocket eject` | Remove all Tocket files from the workspace |
|
|
58
67
|
|
|
59
68
|
## Configuration
|
|
60
69
|
|
|
@@ -44,7 +44,10 @@ export async function showDashboard(program) {
|
|
|
44
44
|
{ value: "generate", name: "Generate payload" },
|
|
45
45
|
{ value: "sync", name: "Sync progress" },
|
|
46
46
|
{ value: "validate", name: "Validate workspace" },
|
|
47
|
+
{ value: "focus", name: "Update focus" },
|
|
48
|
+
{ value: "status", name: "Workspace status" },
|
|
47
49
|
{ value: "config", name: "Configure settings" },
|
|
50
|
+
{ value: "eject", name: "Eject workspace" },
|
|
48
51
|
{ value: "exit", name: "Exit" },
|
|
49
52
|
]
|
|
50
53
|
: [
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/** Files created by `tocket init` that eject should remove. */
|
|
3
|
+
export declare const EJECT_FILES: readonly ["TOCKET.md", "CLAUDE.md", "GEMINI.md", ".cursorrules"];
|
|
4
|
+
/** Directories created by `tocket init` that eject should remove. */
|
|
5
|
+
export declare const EJECT_DIRS: readonly [".context"];
|
|
6
|
+
export declare function registerEjectCommand(program: Command): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { success, warn, info, dim } from "../utils/theme.js";
|
|
6
|
+
/** Files created by `tocket init` that eject should remove. */
|
|
7
|
+
export const EJECT_FILES = [
|
|
8
|
+
"TOCKET.md",
|
|
9
|
+
"CLAUDE.md",
|
|
10
|
+
"GEMINI.md",
|
|
11
|
+
".cursorrules",
|
|
12
|
+
];
|
|
13
|
+
/** Directories created by `tocket init` that eject should remove. */
|
|
14
|
+
export const EJECT_DIRS = [".context"];
|
|
15
|
+
export function registerEjectCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command("eject")
|
|
18
|
+
.description("Remove all Tocket files from the current workspace")
|
|
19
|
+
.option("-f, --force", "Skip confirmation prompt")
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const contextDir = join(cwd, ".context");
|
|
23
|
+
if (!existsSync(contextDir)) {
|
|
24
|
+
console.log(warn("No Tocket workspace found in this directory."));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!options.force) {
|
|
28
|
+
const ok = await confirm({
|
|
29
|
+
message: "This will permanently remove .context/, CLAUDE.md, GEMINI.md, TOCKET.md, and .cursorrules. Continue?",
|
|
30
|
+
default: false,
|
|
31
|
+
});
|
|
32
|
+
if (!ok) {
|
|
33
|
+
console.log(dim("\n Cancelled.\n"));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let removedCount = 0;
|
|
38
|
+
for (const dir of EJECT_DIRS) {
|
|
39
|
+
const fullPath = join(cwd, dir);
|
|
40
|
+
if (existsSync(fullPath)) {
|
|
41
|
+
await rm(fullPath, { recursive: true, force: true });
|
|
42
|
+
console.log(info(`Removed ${dir}/`));
|
|
43
|
+
removedCount++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const file of EJECT_FILES) {
|
|
47
|
+
const fullPath = join(cwd, file);
|
|
48
|
+
if (existsSync(fullPath)) {
|
|
49
|
+
await rm(fullPath, { force: true });
|
|
50
|
+
console.log(info(`Removed ${file}`));
|
|
51
|
+
removedCount++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (removedCount === 0) {
|
|
55
|
+
console.log(warn("Nothing to remove."));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log("\n" +
|
|
59
|
+
success("Tocket workspace ejected.") +
|
|
60
|
+
" " +
|
|
61
|
+
dim("Global config (~/.tocketrc.json) was not touched.") +
|
|
62
|
+
"\n");
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Replaces the content under ## Current Focus with the new message.
|
|
4
|
+
* Exported for testing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function replaceFocusSection(content: string, newFocus: string): string;
|
|
7
|
+
export declare function registerFocusCommand(program: Command): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { input } from "@inquirer/prompts";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { success, error as themeError, info } from "../utils/theme.js";
|
|
6
|
+
/**
|
|
7
|
+
* Replaces the content under ## Current Focus with the new message.
|
|
8
|
+
* Exported for testing.
|
|
9
|
+
*/
|
|
10
|
+
export function replaceFocusSection(content, newFocus) {
|
|
11
|
+
const regex = /(## Current Focus[ \t]*\n)[\s\S]*?(?=\n## |\n*$)/;
|
|
12
|
+
if (!regex.test(content)) {
|
|
13
|
+
return content.trimEnd() + "\n\n## Current Focus\n\n" + newFocus + "\n";
|
|
14
|
+
}
|
|
15
|
+
return content.replace(regex, `$1\n${newFocus}\n`);
|
|
16
|
+
}
|
|
17
|
+
export function registerFocusCommand(program) {
|
|
18
|
+
program
|
|
19
|
+
.command("focus")
|
|
20
|
+
.description("Update the Current Focus in activeContext.md")
|
|
21
|
+
.argument("[message...]", "Focus message (prompted if omitted)")
|
|
22
|
+
.action(async (messageParts) => {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const contextDir = join(cwd, ".context");
|
|
25
|
+
if (!existsSync(contextDir)) {
|
|
26
|
+
console.error(themeError("No .context/ directory found. Run 'tocket init' first."));
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const filePath = join(contextDir, "activeContext.md");
|
|
31
|
+
if (!existsSync(filePath)) {
|
|
32
|
+
console.error(themeError("activeContext.md not found. Run 'tocket init' first."));
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let message = messageParts.join(" ").trim();
|
|
37
|
+
if (!message) {
|
|
38
|
+
message = await input({ message: "What is the current focus?" });
|
|
39
|
+
message = message.trim();
|
|
40
|
+
}
|
|
41
|
+
if (!message) {
|
|
42
|
+
console.error(themeError("Focus message cannot be empty."));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const content = await readFile(filePath, "utf-8");
|
|
47
|
+
const updated = replaceFocusSection(content, message);
|
|
48
|
+
await writeFile(filePath, updated, "utf-8");
|
|
49
|
+
console.log(info("Focus updated in .context/activeContext.md"));
|
|
50
|
+
console.log(" " + success(message));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import { registerGenerateCommand } from "./commands/generate.cmd.js";
|
|
|
7
7
|
import { registerSyncCommand } from "./commands/sync.cmd.js";
|
|
8
8
|
import { registerValidateCommand } from "./commands/validate.cmd.js";
|
|
9
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";
|
|
10
13
|
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
11
14
|
const program = new Command();
|
|
12
15
|
program
|
|
@@ -18,6 +21,9 @@ registerGenerateCommand(program);
|
|
|
18
21
|
registerSyncCommand(program);
|
|
19
22
|
registerValidateCommand(program);
|
|
20
23
|
registerConfigCommand(program);
|
|
24
|
+
registerEjectCommand(program);
|
|
25
|
+
registerFocusCommand(program);
|
|
26
|
+
registerStatusCommand(program);
|
|
21
27
|
// No-args: show interactive dashboard (TTY) or help (non-TTY)
|
|
22
28
|
const args = process.argv.slice(2);
|
|
23
29
|
if (args.length === 0 && process.stdin.isTTY) {
|
|
@@ -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,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
|
+
});
|