@pedrocivita/tocket 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -132
- package/dist/commands/dashboard.js +5 -0
- package/dist/commands/doctor.cmd.d.ts +9 -0
- package/dist/commands/doctor.cmd.js +191 -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.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/status.cmd.d.ts +2 -0
- package/dist/commands/status.cmd.js +85 -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 +10 -0
- package/dist/tests/doctor.test.d.ts +1 -0
- package/dist/tests/doctor.test.js +102 -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/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/status.test.d.ts +1 -0
- package/dist/tests/status.test.js +34 -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,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
|
+
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
export interface TaskInput {
|
|
3
|
+
intent: string;
|
|
4
|
+
scope: string;
|
|
5
|
+
priority: string;
|
|
6
|
+
skills: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildPayloadXml(tasks: TaskInput[]): string;
|
|
9
|
+
export declare function suggestScope(): string;
|
|
2
10
|
export declare function registerGenerateCommand(program: Command): void;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { input, select, confirm } from "@inquirer/prompts";
|
|
2
|
-
import
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
3
|
import { success, heading, dim, banner } from "../utils/theme.js";
|
|
4
4
|
import { getConfig } from "../utils/config.js";
|
|
5
|
-
import { getStagedFiles, getModifiedFiles } from "../utils/git.js";
|
|
6
|
-
function buildPayloadXml(tasks) {
|
|
5
|
+
import { getStagedFiles, getModifiedFiles, getLastCommitMessage } from "../utils/git.js";
|
|
6
|
+
export function buildPayloadXml(tasks) {
|
|
7
7
|
const first = tasks[0];
|
|
8
8
|
const skillsAttr = first.skills.trim()
|
|
9
9
|
? `\n <skills>${first.skills.trim()}</skills>`
|
|
@@ -36,7 +36,7 @@ ${taskBlocks}
|
|
|
36
36
|
</validate>
|
|
37
37
|
</payload>`;
|
|
38
38
|
}
|
|
39
|
-
function suggestScope() {
|
|
39
|
+
export function suggestScope() {
|
|
40
40
|
const staged = getStagedFiles();
|
|
41
41
|
const modified = getModifiedFiles();
|
|
42
42
|
const all = [...new Set([...staged, ...modified])];
|
|
@@ -47,6 +47,7 @@ export function registerGenerateCommand(program) {
|
|
|
47
47
|
.command("generate")
|
|
48
48
|
.description("Build payload XMLs interactively for Architect-Executor handoff")
|
|
49
49
|
.option("--no-preview", "Skip payload preview before copying")
|
|
50
|
+
.option("--to <target>", "Output target: clipboard (default), stdout, or file path")
|
|
50
51
|
.action(async (options) => {
|
|
51
52
|
const config = await getConfig();
|
|
52
53
|
if (!config.theme?.disableBanner) {
|
|
@@ -62,7 +63,11 @@ export function registerGenerateCommand(program) {
|
|
|
62
63
|
if (taskNum > 1) {
|
|
63
64
|
console.log(heading(`\n Task ${taskNum}\n`));
|
|
64
65
|
}
|
|
65
|
-
const
|
|
66
|
+
const lastCommit = getLastCommitMessage();
|
|
67
|
+
const intent = await input({
|
|
68
|
+
message: "Intent (goal in one line):",
|
|
69
|
+
default: lastCommit || undefined,
|
|
70
|
+
});
|
|
66
71
|
const scope = await input({
|
|
67
72
|
message: "Scope (files/folders affected):",
|
|
68
73
|
default: taskNum === 1 && suggestedScope ? suggestedScope : undefined,
|
|
@@ -97,10 +102,22 @@ export function registerGenerateCommand(program) {
|
|
|
97
102
|
console.log(dim(preview));
|
|
98
103
|
console.log(dim("--- End Preview ---\n"));
|
|
99
104
|
}
|
|
100
|
-
clipboard
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
const target = options.to ?? "clipboard";
|
|
106
|
+
if (target === "stdout") {
|
|
107
|
+
console.log(xml);
|
|
108
|
+
}
|
|
109
|
+
else if (target === "clipboard") {
|
|
110
|
+
const { default: clipboard } = await import("clipboardy");
|
|
111
|
+
clipboard.writeSync(xml);
|
|
112
|
+
console.log(success(`Payload XML (v2.0) copied to clipboard!`) +
|
|
113
|
+
dim(` ${tasks.length} task(s).`) +
|
|
114
|
+
"\n" +
|
|
115
|
+
dim(" Paste it into your Architect to continue.\n"));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
writeFileSync(target, xml, "utf-8");
|
|
119
|
+
console.log(success(`Payload XML (v2.0) written to ${target}!`) +
|
|
120
|
+
dim(` ${tasks.length} task(s).\n`));
|
|
121
|
+
}
|
|
105
122
|
});
|
|
106
123
|
}
|
|
@@ -119,8 +119,12 @@ export function registerInitCommand(program) {
|
|
|
119
119
|
.command("init")
|
|
120
120
|
.description("Scaffold an agentic workspace with Memory Bank and triangulation config")
|
|
121
121
|
.option("-f, --force", "Overwrite existing files without prompting")
|
|
122
|
+
.option("--minimal", "Scaffold only essential files (.context/ + TOCKET.md)")
|
|
123
|
+
.option("--name <name>", "Project name (skip prompt)")
|
|
124
|
+
.option("--description <desc>", "Project description (skip prompt)")
|
|
122
125
|
.action(async (options) => {
|
|
123
126
|
const force = options.force ?? false;
|
|
127
|
+
const minimal = options.minimal ?? false;
|
|
124
128
|
const cwd = process.cwd();
|
|
125
129
|
const globalConfig = await getConfig();
|
|
126
130
|
if (!globalConfig.theme?.disableBanner) {
|
|
@@ -142,11 +146,11 @@ export function registerInitCommand(program) {
|
|
|
142
146
|
console.log(info(`Extras: ${stack.extras.join(", ")}`));
|
|
143
147
|
console.log();
|
|
144
148
|
}
|
|
145
|
-
const projectName = await input({
|
|
149
|
+
const projectName = options.name ?? await input({
|
|
146
150
|
message: "Project Name:",
|
|
147
151
|
default: detectedName || undefined,
|
|
148
152
|
});
|
|
149
|
-
const description = await input({
|
|
153
|
+
const description = options.description ?? await input({
|
|
150
154
|
message: "Short Description:",
|
|
151
155
|
default: detectedDescription || undefined,
|
|
152
156
|
});
|
|
@@ -169,7 +173,15 @@ export function registerInitCommand(program) {
|
|
|
169
173
|
],
|
|
170
174
|
[join(".context", "progress.md"), progressMd(projectName)],
|
|
171
175
|
];
|
|
172
|
-
|
|
176
|
+
const minimalPaths = new Set([
|
|
177
|
+
"TOCKET.md",
|
|
178
|
+
join(".context", "activeContext.md"),
|
|
179
|
+
join(".context", "systemPatterns.md"),
|
|
180
|
+
]);
|
|
181
|
+
const filesToWrite = minimal
|
|
182
|
+
? files.filter(([path]) => minimalPaths.has(path))
|
|
183
|
+
: files;
|
|
184
|
+
for (const [filePath, content] of filesToWrite) {
|
|
173
185
|
const fullPath = join(cwd, filePath);
|
|
174
186
|
const exists = await fileExists(fullPath);
|
|
175
187
|
if (exists && !force) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
export interface LintResult {
|
|
3
|
+
severity: "pass" | "warn" | "info" | "fail";
|
|
4
|
+
message: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
suggestion?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function lintActiveContext(content: string): LintResult[];
|
|
9
|
+
export declare function lintSystemPatterns(content: string): LintResult[];
|
|
10
|
+
export declare function lintProtocolSpec(content: string): LintResult[];
|
|
11
|
+
export declare function lintAgentConfig(name: string, content: string): LintResult[];
|
|
12
|
+
export declare function registerLintCommand(program: Command): void;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { success as themePass, warn as themeWarn, error as themeFail, heading, dim, info as themeInfo } from "../utils/theme.js";
|
|
5
|
+
import { isGitRepo } from "../utils/git.js";
|
|
6
|
+
export function lintActiveContext(content) {
|
|
7
|
+
const results = [];
|
|
8
|
+
// Check required sections exist
|
|
9
|
+
const requiredSections = ["## Current Focus", "## Recent Changes", "## Open Decisions"];
|
|
10
|
+
for (const section of requiredSections) {
|
|
11
|
+
if (content.includes(section)) {
|
|
12
|
+
results.push({ severity: "pass", message: `Has ${section} section` });
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
results.push({
|
|
16
|
+
severity: "fail",
|
|
17
|
+
message: `Missing ${section} section`,
|
|
18
|
+
suggestion: `Add a "${section}" heading to activeContext.md`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Check Current Focus is populated
|
|
23
|
+
const focusMatch = content.match(/## Current Focus\s*\n+([\s\S]*?)(?=\n##|\n*$)/);
|
|
24
|
+
const focusBody = focusMatch?.[1]?.trim() ?? "";
|
|
25
|
+
if (!focusBody || focusBody.startsWith("_") || focusBody.includes("No active tasks")) {
|
|
26
|
+
results.push({
|
|
27
|
+
severity: "warn",
|
|
28
|
+
message: "Current Focus is empty or placeholder",
|
|
29
|
+
suggestion: "Run tocket focus \"your current task\" to set it",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
results.push({ severity: "pass", message: "Current Focus is populated" });
|
|
34
|
+
}
|
|
35
|
+
// Check Open Decisions section
|
|
36
|
+
const decisionsMatch = content.match(/## Open Decisions\s*\n+([\s\S]*?)(?=\n##|\n*$)/);
|
|
37
|
+
const decisionsBody = decisionsMatch?.[1]?.trim() ?? "";
|
|
38
|
+
if (!decisionsBody || decisionsBody.startsWith("_")) {
|
|
39
|
+
results.push({
|
|
40
|
+
severity: "info",
|
|
41
|
+
message: "Open Decisions section is empty",
|
|
42
|
+
suggestion: "List any unresolved decisions or write \"None\" if all resolved",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
export function lintSystemPatterns(content) {
|
|
48
|
+
const results = [];
|
|
49
|
+
const hasConvention = content.includes("- ") || content.includes("| ");
|
|
50
|
+
if (hasConvention) {
|
|
51
|
+
results.push({ severity: "pass", message: "Has documented conventions" });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
results.push({
|
|
55
|
+
severity: "warn",
|
|
56
|
+
message: "No conventions documented",
|
|
57
|
+
suggestion: "Add bullet points or a table with your project's coding conventions",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Check for key decisions table
|
|
61
|
+
if (content.includes("Key Decisions") || content.includes("key decisions")) {
|
|
62
|
+
const hasDecisionRows = content.match(/\|[^|]+\|[^|]+\|[^|]+\|/g);
|
|
63
|
+
const hasDataRows = hasDecisionRows && hasDecisionRows.length > 2; // header + separator + at least 1 row
|
|
64
|
+
if (hasDataRows) {
|
|
65
|
+
results.push({ severity: "pass", message: "Has key decisions recorded" });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
results.push({
|
|
69
|
+
severity: "info",
|
|
70
|
+
message: "Key Decisions table is empty",
|
|
71
|
+
suggestion: "Record architectural decisions with rationale and date",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
export function lintProtocolSpec(content) {
|
|
78
|
+
const results = [];
|
|
79
|
+
if (content.includes("payload") || content.includes("Payload")) {
|
|
80
|
+
results.push({ severity: "pass", message: "References payload format" });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
results.push({
|
|
84
|
+
severity: "warn",
|
|
85
|
+
message: "Does not reference payload format",
|
|
86
|
+
suggestion: "TOCKET.md should describe the payload XML handoff format",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (content.includes("Memory Bank") || content.includes(".context/")) {
|
|
90
|
+
results.push({ severity: "pass", message: "References Memory Bank" });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
results.push({
|
|
94
|
+
severity: "warn",
|
|
95
|
+
message: "Does not reference Memory Bank",
|
|
96
|
+
suggestion: "TOCKET.md should describe the .context/ directory",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
export function lintAgentConfig(name, content) {
|
|
102
|
+
const results = [];
|
|
103
|
+
if (content.includes(".context/") || content.includes(".context\\")) {
|
|
104
|
+
results.push({ severity: "pass", message: `References .context/` });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
results.push({
|
|
108
|
+
severity: "warn",
|
|
109
|
+
message: `Does not reference .context/`,
|
|
110
|
+
suggestion: `Add instructions to read .context/ before acting`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
function severityIcon(severity) {
|
|
116
|
+
switch (severity) {
|
|
117
|
+
case "pass": return themePass("");
|
|
118
|
+
case "warn": return themeWarn("");
|
|
119
|
+
case "info": return themeInfo("");
|
|
120
|
+
case "fail": return themeFail("");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function registerLintCommand(program) {
|
|
124
|
+
program
|
|
125
|
+
.command("lint")
|
|
126
|
+
.description("Audit .context/ content quality and suggest improvements")
|
|
127
|
+
.action(() => {
|
|
128
|
+
const cwd = process.cwd();
|
|
129
|
+
let passCount = 0;
|
|
130
|
+
let warnCount = 0;
|
|
131
|
+
let infoCount = 0;
|
|
132
|
+
let failCount = 0;
|
|
133
|
+
console.log(heading("\nTocket Lint\n"));
|
|
134
|
+
// Check TOCKET.md exists
|
|
135
|
+
const tocketPath = join(cwd, "TOCKET.md");
|
|
136
|
+
if (!existsSync(tocketPath)) {
|
|
137
|
+
console.log(themeFail(" TOCKET.md not found. Run tocket init first."));
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Lint activeContext.md
|
|
142
|
+
const acPath = join(cwd, ".context", "activeContext.md");
|
|
143
|
+
if (existsSync(acPath)) {
|
|
144
|
+
console.log(dim(" .context/activeContext.md"));
|
|
145
|
+
const content = readFileSync(acPath, "utf-8");
|
|
146
|
+
const results = lintActiveContext(content);
|
|
147
|
+
for (const r of results) {
|
|
148
|
+
printResult(r);
|
|
149
|
+
countResult(r.severity);
|
|
150
|
+
}
|
|
151
|
+
// Staleness check
|
|
152
|
+
const stats = statSync(acPath);
|
|
153
|
+
const daysSince = Math.floor((Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24));
|
|
154
|
+
if (daysSince > 7) {
|
|
155
|
+
const r = {
|
|
156
|
+
severity: "warn",
|
|
157
|
+
message: `Last modified ${daysSince} days ago`,
|
|
158
|
+
suggestion: "Update activeContext.md with current state",
|
|
159
|
+
};
|
|
160
|
+
printResult(r);
|
|
161
|
+
countResult(r.severity);
|
|
162
|
+
}
|
|
163
|
+
console.log();
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.log(dim(" .context/activeContext.md"));
|
|
167
|
+
console.log(` ${severityIcon("fail")} File missing (required)`);
|
|
168
|
+
failCount++;
|
|
169
|
+
console.log();
|
|
170
|
+
}
|
|
171
|
+
// Lint systemPatterns.md
|
|
172
|
+
const spPath = join(cwd, ".context", "systemPatterns.md");
|
|
173
|
+
if (existsSync(spPath)) {
|
|
174
|
+
console.log(dim(" .context/systemPatterns.md"));
|
|
175
|
+
const content = readFileSync(spPath, "utf-8");
|
|
176
|
+
const results = lintSystemPatterns(content);
|
|
177
|
+
for (const r of results) {
|
|
178
|
+
printResult(r);
|
|
179
|
+
countResult(r.severity);
|
|
180
|
+
}
|
|
181
|
+
console.log();
|
|
182
|
+
}
|
|
183
|
+
// Lint TOCKET.md
|
|
184
|
+
console.log(dim(" TOCKET.md"));
|
|
185
|
+
const tocketContent = readFileSync(tocketPath, "utf-8");
|
|
186
|
+
const tocketResults = lintProtocolSpec(tocketContent);
|
|
187
|
+
for (const r of tocketResults) {
|
|
188
|
+
printResult(r);
|
|
189
|
+
countResult(r.severity);
|
|
190
|
+
}
|
|
191
|
+
console.log();
|
|
192
|
+
// Lint agent configs
|
|
193
|
+
const agentFiles = ["CLAUDE.md", "GEMINI.md"];
|
|
194
|
+
for (const af of agentFiles) {
|
|
195
|
+
const afPath = join(cwd, af);
|
|
196
|
+
if (existsSync(afPath)) {
|
|
197
|
+
console.log(dim(` ${af}`));
|
|
198
|
+
const content = readFileSync(afPath, "utf-8");
|
|
199
|
+
const results = lintAgentConfig(af, content);
|
|
200
|
+
for (const r of results) {
|
|
201
|
+
printResult(r);
|
|
202
|
+
countResult(r.severity);
|
|
203
|
+
}
|
|
204
|
+
console.log();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Uncommitted context check
|
|
208
|
+
if (isGitRepo(cwd)) {
|
|
209
|
+
try {
|
|
210
|
+
const output = execSync("git status --porcelain .context/", {
|
|
211
|
+
cwd,
|
|
212
|
+
encoding: "utf-8",
|
|
213
|
+
}).trim();
|
|
214
|
+
if (output) {
|
|
215
|
+
const count = output.split("\n").length;
|
|
216
|
+
console.log(dim(" git"));
|
|
217
|
+
const r = {
|
|
218
|
+
severity: "info",
|
|
219
|
+
message: `${count} uncommitted change(s) in .context/`,
|
|
220
|
+
suggestion: "Commit .context/ changes to preserve shared memory",
|
|
221
|
+
};
|
|
222
|
+
printResult(r);
|
|
223
|
+
countResult(r.severity);
|
|
224
|
+
console.log();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// git error — skip
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Summary
|
|
232
|
+
const total = passCount + warnCount + infoCount + failCount;
|
|
233
|
+
console.log(dim(` ${passCount} passed, ${warnCount} warnings, ${infoCount} info, ${failCount} failures (${total} checks)`));
|
|
234
|
+
console.log();
|
|
235
|
+
if (failCount > 0) {
|
|
236
|
+
console.log(themeFail("Context has issues that need attention."));
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
}
|
|
239
|
+
else if (warnCount > 0) {
|
|
240
|
+
console.log(themeWarn("Context is functional but could be improved."));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
console.log(themePass("Context is in great shape."));
|
|
244
|
+
}
|
|
245
|
+
function printResult(r) {
|
|
246
|
+
const icon = severityIcon(r.severity);
|
|
247
|
+
console.log(` ${icon} ${r.message}`);
|
|
248
|
+
if (r.suggestion) {
|
|
249
|
+
console.log(` ${dim(r.suggestion)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function countResult(severity) {
|
|
253
|
+
switch (severity) {
|
|
254
|
+
case "pass":
|
|
255
|
+
passCount++;
|
|
256
|
+
break;
|
|
257
|
+
case "warn":
|
|
258
|
+
warnCount++;
|
|
259
|
+
break;
|
|
260
|
+
case "info":
|
|
261
|
+
infoCount++;
|
|
262
|
+
break;
|
|
263
|
+
case "fail":
|
|
264
|
+
failCount++;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -9,7 +9,8 @@ export function registerSyncCommand(program) {
|
|
|
9
9
|
program
|
|
10
10
|
.command("sync")
|
|
11
11
|
.description("Update Memory Bank from git history and session artifacts")
|
|
12
|
-
.
|
|
12
|
+
.option("--summary <text>", "Session summary (skip prompt)")
|
|
13
|
+
.action(async (options) => {
|
|
13
14
|
const progressPath = join(process.cwd(), ".context", "progress.md");
|
|
14
15
|
const contextDir = join(process.cwd(), ".context");
|
|
15
16
|
if (!existsSync(contextDir)) {
|
|
@@ -18,7 +19,7 @@ export function registerSyncCommand(program) {
|
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
const config = await getConfig();
|
|
21
|
-
const summary = await input({
|
|
22
|
+
const summary = options.summary ?? await input({
|
|
22
23
|
message: "What did you accomplish in this session?",
|
|
23
24
|
});
|
|
24
25
|
const commits = getRecentCommitsRaw();
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
export interface CheckResult {
|
|
3
|
+
icon: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function checkFile(basePath: string, relativePath: string, required: boolean): CheckResult;
|
|
7
|
+
export declare function checkStale(basePath: string, relativePath: string): CheckResult | null;
|
|
8
|
+
export declare function checkAgentFile(basePath: string): CheckResult;
|
|
2
9
|
export declare function registerValidateCommand(program: Command): void;
|
|
@@ -4,7 +4,7 @@ import { success as themePass, warn as themeWarn, error as themeFail, heading }
|
|
|
4
4
|
const PASS = themePass("");
|
|
5
5
|
const WARN = themeWarn("");
|
|
6
6
|
const FAIL = themeFail("");
|
|
7
|
-
function checkFile(basePath, relativePath, required) {
|
|
7
|
+
export function checkFile(basePath, relativePath, required) {
|
|
8
8
|
const fullPath = join(basePath, relativePath);
|
|
9
9
|
if (existsSync(fullPath)) {
|
|
10
10
|
return { icon: PASS, message: `${relativePath} found` };
|
|
@@ -14,7 +14,7 @@ function checkFile(basePath, relativePath, required) {
|
|
|
14
14
|
}
|
|
15
15
|
return { icon: WARN, message: `${relativePath} missing (optional)` };
|
|
16
16
|
}
|
|
17
|
-
function checkStale(basePath, relativePath) {
|
|
17
|
+
export function checkStale(basePath, relativePath) {
|
|
18
18
|
const fullPath = join(basePath, relativePath);
|
|
19
19
|
if (!existsSync(fullPath))
|
|
20
20
|
return null;
|
|
@@ -28,7 +28,7 @@ function checkStale(basePath, relativePath) {
|
|
|
28
28
|
}
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
|
-
function checkAgentFile(basePath) {
|
|
31
|
+
export function checkAgentFile(basePath) {
|
|
32
32
|
const agentFiles = [
|
|
33
33
|
"CLAUDE.md",
|
|
34
34
|
"GEMINI.md",
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,11 @@ 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";
|
|
13
|
+
import { registerDoctorCommand } from "./commands/doctor.cmd.js";
|
|
14
|
+
import { registerLintCommand } from "./commands/lint.cmd.js";
|
|
10
15
|
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
11
16
|
const program = new Command();
|
|
12
17
|
program
|
|
@@ -18,6 +23,11 @@ registerGenerateCommand(program);
|
|
|
18
23
|
registerSyncCommand(program);
|
|
19
24
|
registerValidateCommand(program);
|
|
20
25
|
registerConfigCommand(program);
|
|
26
|
+
registerEjectCommand(program);
|
|
27
|
+
registerFocusCommand(program);
|
|
28
|
+
registerStatusCommand(program);
|
|
29
|
+
registerDoctorCommand(program);
|
|
30
|
+
registerLintCommand(program);
|
|
21
31
|
// No-args: show interactive dashboard (TTY) or help (non-TTY)
|
|
22
32
|
const args = process.argv.slice(2);
|
|
23
33
|
if (args.length === 0 && process.stdin.isTTY) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|