@pedrocivita/tocket 1.1.0 → 2.0.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 +29 -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 +65 -0
- package/dist/commands/generate.cmd.js +81 -29
- package/dist/commands/init.cmd.js +173 -12
- package/dist/commands/sync.cmd.js +9 -13
- package/dist/commands/validate.cmd.js +7 -6
- package/dist/index.js +15 -2
- package/dist/templates/memory-bank.d.ts +9 -1
- package/dist/templates/memory-bank.js +47 -6
- package/dist/tests/config.test.d.ts +1 -0
- package/dist/tests/config.test.js +68 -0
- package/dist/tests/git.test.d.ts +1 -0
- package/dist/tests/git.test.js +61 -0
- package/dist/tests/memory-bank.test.js +69 -2
- 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
package/README.md
CHANGED
|
@@ -32,10 +32,13 @@ The CLI just automates the scaffolding.
|
|
|
32
32
|
## Quick Start
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
+
# Interactive dashboard — guided entry point
|
|
36
|
+
npx @pedrocivita/tocket
|
|
37
|
+
|
|
35
38
|
# Scaffold a new workspace (creates .context/, TOCKET.md, CLAUDE.md, GEMINI.md)
|
|
36
39
|
npx @pedrocivita/tocket init
|
|
37
40
|
|
|
38
|
-
# Generate a payload XML
|
|
41
|
+
# Generate a payload XML with smart git integration
|
|
39
42
|
npx @pedrocivita/tocket generate
|
|
40
43
|
|
|
41
44
|
# Sync session progress into Memory Bank
|
|
@@ -44,12 +47,31 @@ npx @pedrocivita/tocket sync
|
|
|
44
47
|
|
|
45
48
|
## Commands
|
|
46
49
|
|
|
47
|
-
| Command | What it does
|
|
48
|
-
| ----------------- |
|
|
49
|
-
| `tocket
|
|
50
|
-
| `tocket
|
|
51
|
-
| `tocket
|
|
52
|
-
| `tocket
|
|
50
|
+
| Command | What it does |
|
|
51
|
+
| ----------------- | -------------------------------------------------------------------- |
|
|
52
|
+
| `tocket` | Interactive dashboard with guided menu |
|
|
53
|
+
| `tocket init` | Scaffolds `.context/`, `TOCKET.md`, `CLAUDE.md`, `GEMINI.md` |
|
|
54
|
+
| `tocket generate` | Smart payload builder — auto-fills scope from git, multi-task support |
|
|
55
|
+
| `tocket sync` | Appends session summary + git log to `.context/progress.md` |
|
|
56
|
+
| `tocket validate` | Checks if the current directory has a valid Tocket Memory Bank |
|
|
57
|
+
| `tocket config` | Manage global settings (`~/.tocketrc.json`) |
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
Set global defaults so you don't repeat yourself:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Interactive setup
|
|
65
|
+
tocket config
|
|
66
|
+
|
|
67
|
+
# Or use flags (CI-friendly)
|
|
68
|
+
tocket config --author "Your Name" --priority medium --skills "core,lsp"
|
|
69
|
+
|
|
70
|
+
# View current config
|
|
71
|
+
tocket config --show
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Config is stored at `~/.tocketrc.json` and pre-fills author, priority, and skills in all commands.
|
|
53
75
|
|
|
54
76
|
## How Triangulation works
|
|
55
77
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
2
|
+
import { getConfig, saveConfig, resetConfig, getConfigPath, } from "../utils/config.js";
|
|
3
|
+
import { success, heading, dim } from "../utils/theme.js";
|
|
4
|
+
export function registerConfigCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("config")
|
|
7
|
+
.description("Manage global Tocket configuration (~/.tocketrc.json)")
|
|
8
|
+
.option("--author <name>", "Set default author name")
|
|
9
|
+
.option("--agent <name>", "Set default agent name")
|
|
10
|
+
.option("--priority <level>", "Set default priority (high|medium|low)")
|
|
11
|
+
.option("--skills <list>", "Set default skills (comma-separated)")
|
|
12
|
+
.option("--show", "Display current configuration")
|
|
13
|
+
.option("--path", "Show config file path")
|
|
14
|
+
.option("--reset", "Reset configuration to defaults")
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
// --path: print and exit
|
|
17
|
+
if (options.path) {
|
|
18
|
+
console.log(getConfigPath());
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// --show: display current config
|
|
22
|
+
if (options.show) {
|
|
23
|
+
const config = await getConfig();
|
|
24
|
+
console.log(heading("\n Current Configuration\n"));
|
|
25
|
+
console.log(JSON.stringify(config, null, 2)
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((l) => " " + l)
|
|
28
|
+
.join("\n"));
|
|
29
|
+
console.log(dim(`\n Path: ${getConfigPath()}\n`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// --reset: clear config
|
|
33
|
+
if (options.reset) {
|
|
34
|
+
const ok = await confirm({
|
|
35
|
+
message: "Reset all configuration to defaults?",
|
|
36
|
+
default: false,
|
|
37
|
+
});
|
|
38
|
+
if (ok) {
|
|
39
|
+
await resetConfig();
|
|
40
|
+
console.log(success("Configuration reset."));
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Non-interactive: handle individual flags
|
|
45
|
+
const hasFlags = options.author || options.agent || options.priority || options.skills;
|
|
46
|
+
if (hasFlags) {
|
|
47
|
+
const config = await getConfig();
|
|
48
|
+
if (options.author)
|
|
49
|
+
config.author = options.author;
|
|
50
|
+
if (options.agent)
|
|
51
|
+
config.defaultAgent = options.agent;
|
|
52
|
+
if (options.priority) {
|
|
53
|
+
if (!config.defaults)
|
|
54
|
+
config.defaults = {};
|
|
55
|
+
config.defaults.priority = options.priority;
|
|
56
|
+
}
|
|
57
|
+
if (options.skills) {
|
|
58
|
+
if (!config.defaults)
|
|
59
|
+
config.defaults = {};
|
|
60
|
+
config.defaults.skills = options.skills;
|
|
61
|
+
}
|
|
62
|
+
await saveConfig(config);
|
|
63
|
+
console.log(success("Configuration updated."));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Interactive TUI mode
|
|
67
|
+
const current = await getConfig();
|
|
68
|
+
console.log(heading("\n Tocket Configuration\n"));
|
|
69
|
+
const author = await input({
|
|
70
|
+
message: "Author name:",
|
|
71
|
+
default: current.author || undefined,
|
|
72
|
+
});
|
|
73
|
+
const agent = await input({
|
|
74
|
+
message: "Default agent name:",
|
|
75
|
+
default: current.defaultAgent || undefined,
|
|
76
|
+
});
|
|
77
|
+
const priority = await select({
|
|
78
|
+
message: "Default priority:",
|
|
79
|
+
choices: [
|
|
80
|
+
{ value: "high", name: "high" },
|
|
81
|
+
{ value: "medium", name: "medium" },
|
|
82
|
+
{ value: "low", name: "low" },
|
|
83
|
+
],
|
|
84
|
+
default: current.defaults?.priority ?? "medium",
|
|
85
|
+
});
|
|
86
|
+
const skills = await input({
|
|
87
|
+
message: "Default skills (comma-separated):",
|
|
88
|
+
default: current.defaults?.skills || undefined,
|
|
89
|
+
});
|
|
90
|
+
await saveConfig({
|
|
91
|
+
...current,
|
|
92
|
+
author: author || undefined,
|
|
93
|
+
defaultAgent: agent || undefined,
|
|
94
|
+
defaults: {
|
|
95
|
+
priority: priority,
|
|
96
|
+
skills: skills || undefined,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
console.log("\n" + success("Configuration saved."));
|
|
100
|
+
console.log(dim(` Path: ${getConfigPath()}\n`));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { select } from "@inquirer/prompts";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { banner, heading, success, dim, warn } from "../utils/theme.js";
|
|
6
|
+
import { getConfig } from "../utils/config.js";
|
|
7
|
+
function extractFocus(content) {
|
|
8
|
+
const match = content.match(/## Current Focus\s*\n+(.+)/);
|
|
9
|
+
if (!match?.[1])
|
|
10
|
+
return "";
|
|
11
|
+
const line = match[1].trim();
|
|
12
|
+
if (line.startsWith("_") || line.includes("No active tasks"))
|
|
13
|
+
return "";
|
|
14
|
+
return line.length > 80 ? line.substring(0, 77) + "..." : line;
|
|
15
|
+
}
|
|
16
|
+
export async function showDashboard(program) {
|
|
17
|
+
const config = await getConfig();
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const hasWorkspace = existsSync(join(cwd, ".context"));
|
|
20
|
+
if (!config.theme?.disableBanner) {
|
|
21
|
+
console.log(banner());
|
|
22
|
+
}
|
|
23
|
+
console.log(heading(" Dashboard\n"));
|
|
24
|
+
if (hasWorkspace) {
|
|
25
|
+
console.log(" " + success("Workspace detected"));
|
|
26
|
+
try {
|
|
27
|
+
const ctx = await readFile(join(cwd, ".context", "activeContext.md"), "utf-8");
|
|
28
|
+
const focus = extractFocus(ctx);
|
|
29
|
+
if (focus) {
|
|
30
|
+
console.log(" " + dim(`Focus: ${focus}`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// activeContext.md missing or unreadable — ignore
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(" " + warn("No workspace in this directory"));
|
|
39
|
+
console.log(" " + dim("Run init to scaffold a Tocket workspace here.\n"));
|
|
40
|
+
}
|
|
41
|
+
console.log();
|
|
42
|
+
const choices = hasWorkspace
|
|
43
|
+
? [
|
|
44
|
+
{ value: "generate", name: "Generate payload" },
|
|
45
|
+
{ value: "sync", name: "Sync progress" },
|
|
46
|
+
{ value: "validate", name: "Validate workspace" },
|
|
47
|
+
{ value: "config", name: "Configure settings" },
|
|
48
|
+
{ value: "exit", name: "Exit" },
|
|
49
|
+
]
|
|
50
|
+
: [
|
|
51
|
+
{ value: "init", name: "Initialize workspace" },
|
|
52
|
+
{ value: "config", name: "Configure settings" },
|
|
53
|
+
{ value: "exit", name: "Exit" },
|
|
54
|
+
];
|
|
55
|
+
const action = await select({
|
|
56
|
+
message: "What would you like to do?",
|
|
57
|
+
choices,
|
|
58
|
+
});
|
|
59
|
+
if (action === "exit") {
|
|
60
|
+
console.log(dim("\n Goodbye!\n"));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log();
|
|
64
|
+
await program.parseAsync(["node", "tocket", action]);
|
|
65
|
+
}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import { input, select } from "@inquirer/prompts";
|
|
1
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
2
2
|
import clipboard from "clipboardy";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { success, heading, dim, banner } from "../utils/theme.js";
|
|
4
|
+
import { getConfig } from "../utils/config.js";
|
|
5
|
+
import { getStagedFiles, getModifiedFiles } from "../utils/git.js";
|
|
6
|
+
function buildPayloadXml(tasks) {
|
|
7
|
+
const first = tasks[0];
|
|
8
|
+
const skillsAttr = first.skills.trim()
|
|
9
|
+
? `\n <skills>${first.skills.trim()}</skills>`
|
|
6
10
|
: "";
|
|
11
|
+
const taskBlocks = tasks
|
|
12
|
+
.map((t, i) => ` <task id="${i + 1}" type="create | edit | delete">
|
|
13
|
+
<target><!-- file/path --></target>
|
|
14
|
+
<action>${t.intent}</action>
|
|
15
|
+
<spec><!-- Detailed specification --></spec>
|
|
16
|
+
<done><!-- Definition of done --></done>
|
|
17
|
+
</task>`)
|
|
18
|
+
.join("\n");
|
|
7
19
|
return `<payload version="2.0">
|
|
8
20
|
<meta>
|
|
9
|
-
<intent>${
|
|
10
|
-
<scope>${
|
|
11
|
-
<priority>${
|
|
21
|
+
<intent>${first.intent}</intent>
|
|
22
|
+
<scope>${first.scope}</scope>${skillsAttr}
|
|
23
|
+
<priority>${first.priority}</priority>
|
|
12
24
|
</meta>
|
|
13
25
|
|
|
14
26
|
<context>
|
|
@@ -16,12 +28,7 @@ function buildPayloadXml(opts) {
|
|
|
16
28
|
</context>
|
|
17
29
|
|
|
18
30
|
<tasks>
|
|
19
|
-
|
|
20
|
-
<target><!-- file/path --></target>
|
|
21
|
-
<action><!-- What to do --></action>
|
|
22
|
-
<spec><!-- Detailed specification --></spec>
|
|
23
|
-
<done><!-- Definition of done --></done>
|
|
24
|
-
</task>
|
|
31
|
+
${taskBlocks}
|
|
25
32
|
</tasks>
|
|
26
33
|
|
|
27
34
|
<validate>
|
|
@@ -29,26 +36,71 @@ function buildPayloadXml(opts) {
|
|
|
29
36
|
</validate>
|
|
30
37
|
</payload>`;
|
|
31
38
|
}
|
|
39
|
+
function suggestScope() {
|
|
40
|
+
const staged = getStagedFiles();
|
|
41
|
+
const modified = getModifiedFiles();
|
|
42
|
+
const all = [...new Set([...staged, ...modified])];
|
|
43
|
+
return all.join(", ");
|
|
44
|
+
}
|
|
32
45
|
export function registerGenerateCommand(program) {
|
|
33
46
|
program
|
|
34
47
|
.command("generate")
|
|
35
48
|
.description("Build payload XMLs interactively for Architect-Executor handoff")
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
.option("--no-preview", "Skip payload preview before copying")
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
const config = await getConfig();
|
|
52
|
+
if (!config.theme?.disableBanner) {
|
|
53
|
+
console.log(banner());
|
|
54
|
+
}
|
|
55
|
+
const tasks = [];
|
|
56
|
+
const suggestedScope = suggestScope();
|
|
57
|
+
const defaultPriority = config.defaults?.priority ?? "medium";
|
|
58
|
+
const defaultSkills = config.defaults?.skills ?? "";
|
|
59
|
+
let addMore = true;
|
|
60
|
+
while (addMore) {
|
|
61
|
+
const taskNum = tasks.length + 1;
|
|
62
|
+
if (taskNum > 1) {
|
|
63
|
+
console.log(heading(`\n Task ${taskNum}\n`));
|
|
64
|
+
}
|
|
65
|
+
const intent = await input({ message: "Intent (goal in one line):" });
|
|
66
|
+
const scope = await input({
|
|
67
|
+
message: "Scope (files/folders affected):",
|
|
68
|
+
default: taskNum === 1 && suggestedScope ? suggestedScope : undefined,
|
|
69
|
+
});
|
|
70
|
+
const priority = await select({
|
|
71
|
+
message: "Priority:",
|
|
72
|
+
choices: [
|
|
73
|
+
{ value: "high", name: "high" },
|
|
74
|
+
{ value: "medium", name: "medium" },
|
|
75
|
+
{ value: "low", name: "low" },
|
|
76
|
+
],
|
|
77
|
+
default: defaultPriority,
|
|
78
|
+
});
|
|
79
|
+
const skills = await input({
|
|
80
|
+
message: "Skills/plugins (comma-separated, optional):",
|
|
81
|
+
default: taskNum === 1 && defaultSkills ? defaultSkills : undefined,
|
|
82
|
+
});
|
|
83
|
+
tasks.push({ intent, scope, priority, skills });
|
|
84
|
+
addMore = await confirm({
|
|
85
|
+
message: "Add another task?",
|
|
86
|
+
default: false,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const xml = buildPayloadXml(tasks);
|
|
90
|
+
// Preview
|
|
91
|
+
if (options.preview !== false) {
|
|
92
|
+
console.log(dim("\n--- Payload Preview ---"));
|
|
93
|
+
const lines = xml.split("\n");
|
|
94
|
+
const preview = lines.length > 20
|
|
95
|
+
? lines.slice(0, 20).join("\n") + dim("\n ... (" + (lines.length - 20) + " more lines)")
|
|
96
|
+
: lines.join("\n");
|
|
97
|
+
console.log(dim(preview));
|
|
98
|
+
console.log(dim("--- End Preview ---\n"));
|
|
99
|
+
}
|
|
51
100
|
clipboard.writeSync(xml);
|
|
52
|
-
console.log(
|
|
101
|
+
console.log(success(`Payload XML (v2.0) copied to clipboard!`) +
|
|
102
|
+
dim(` ${tasks.length} task(s).`) +
|
|
103
|
+
"\n" +
|
|
104
|
+
dim(" Paste it into your Architect to continue.\n"));
|
|
53
105
|
});
|
|
54
106
|
}
|
|
@@ -1,31 +1,192 @@
|
|
|
1
|
-
import { input } from "@inquirer/prompts";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { input, confirm } from "@inquirer/prompts";
|
|
2
|
+
import { mkdir, readFile, writeFile, access } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { banner, heading, info, success, dim } from "../utils/theme.js";
|
|
5
|
+
import { getConfig } from "../utils/config.js";
|
|
6
|
+
import { claudeMd, geminiMd, tocketMd, activeContextMd, systemPatternsMd, productContextMd, techContextMd, progressMd, cursorrulesMd, } from "../templates/memory-bank.js";
|
|
7
|
+
async function fileExists(path) {
|
|
8
|
+
try {
|
|
9
|
+
await access(path);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function detectFramework(deps, devDeps) {
|
|
17
|
+
const all = [...deps, ...devDeps];
|
|
18
|
+
if (all.includes("next"))
|
|
19
|
+
return "Next.js";
|
|
20
|
+
if (all.includes("nuxt"))
|
|
21
|
+
return "Nuxt";
|
|
22
|
+
if (all.includes("@sveltejs/kit"))
|
|
23
|
+
return "SvelteKit";
|
|
24
|
+
if (all.includes("remix") || all.includes("@remix-run/react"))
|
|
25
|
+
return "Remix";
|
|
26
|
+
if (all.includes("astro"))
|
|
27
|
+
return "Astro";
|
|
28
|
+
if (all.includes("react"))
|
|
29
|
+
return "React";
|
|
30
|
+
if (all.includes("vue"))
|
|
31
|
+
return "Vue";
|
|
32
|
+
if (all.includes("svelte"))
|
|
33
|
+
return "Svelte";
|
|
34
|
+
if (all.includes("angular") || all.includes("@angular/core"))
|
|
35
|
+
return "Angular";
|
|
36
|
+
if (all.includes("express"))
|
|
37
|
+
return "Express";
|
|
38
|
+
if (all.includes("fastify"))
|
|
39
|
+
return "Fastify";
|
|
40
|
+
if (all.includes("hono"))
|
|
41
|
+
return "Hono";
|
|
42
|
+
if (all.includes("commander") || all.includes("yargs"))
|
|
43
|
+
return "CLI (Node.js)";
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
function detectBuild(devDeps) {
|
|
47
|
+
if (devDeps.includes("vite"))
|
|
48
|
+
return "Vite";
|
|
49
|
+
if (devDeps.includes("webpack"))
|
|
50
|
+
return "Webpack";
|
|
51
|
+
if (devDeps.includes("esbuild"))
|
|
52
|
+
return "esbuild";
|
|
53
|
+
if (devDeps.includes("rollup"))
|
|
54
|
+
return "Rollup";
|
|
55
|
+
if (devDeps.includes("turbopack") || devDeps.includes("turbo"))
|
|
56
|
+
return "Turbopack";
|
|
57
|
+
if (devDeps.includes("typescript"))
|
|
58
|
+
return "tsc";
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
function pickExtras(deps, devDeps) {
|
|
62
|
+
const notable = [
|
|
63
|
+
"tailwindcss", "prisma", "@prisma/client",
|
|
64
|
+
"drizzle-orm", "mongoose", "sequelize",
|
|
65
|
+
"trpc", "@trpc/server", "graphql",
|
|
66
|
+
"zod", "joi", "yup",
|
|
67
|
+
"jest", "vitest", "mocha",
|
|
68
|
+
"eslint", "prettier", "biome",
|
|
69
|
+
"docker-compose", "firebase", "supabase",
|
|
70
|
+
"stripe", "clerk", "@clerk/nextjs",
|
|
71
|
+
"socket.io", "redis", "bullmq",
|
|
72
|
+
];
|
|
73
|
+
const all = [...deps, ...devDeps];
|
|
74
|
+
return notable.filter((n) => all.includes(n));
|
|
75
|
+
}
|
|
76
|
+
async function detectStack(cwd) {
|
|
77
|
+
const empty = {
|
|
78
|
+
language: "",
|
|
79
|
+
runtime: "",
|
|
80
|
+
build: "",
|
|
81
|
+
framework: "",
|
|
82
|
+
extras: [],
|
|
83
|
+
};
|
|
84
|
+
const pkgPath = join(cwd, "package.json");
|
|
85
|
+
if (!(await fileExists(pkgPath))) {
|
|
86
|
+
return { stack: empty, detectedName: "", detectedDescription: "" };
|
|
87
|
+
}
|
|
88
|
+
let pkg;
|
|
89
|
+
try {
|
|
90
|
+
const raw = await readFile(pkgPath, "utf-8");
|
|
91
|
+
pkg = JSON.parse(raw);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { stack: empty, detectedName: "", detectedDescription: "" };
|
|
95
|
+
}
|
|
96
|
+
const deps = Object.keys(pkg.dependencies ?? {});
|
|
97
|
+
const devDeps = Object.keys(pkg.devDependencies ?? {});
|
|
98
|
+
const hasTsConfig = await fileExists(join(cwd, "tsconfig.json"));
|
|
99
|
+
const hasTs = hasTsConfig || devDeps.includes("typescript");
|
|
100
|
+
const stack = {
|
|
101
|
+
language: hasTs ? "TypeScript" : "JavaScript",
|
|
102
|
+
runtime: "Node.js",
|
|
103
|
+
build: detectBuild(devDeps),
|
|
104
|
+
framework: detectFramework(deps, devDeps),
|
|
105
|
+
extras: pickExtras(deps, devDeps),
|
|
106
|
+
};
|
|
107
|
+
const rawName = pkg.name ?? "";
|
|
108
|
+
const detectedName = rawName.startsWith("@")
|
|
109
|
+
? rawName.split("/").pop() ?? rawName
|
|
110
|
+
: rawName;
|
|
111
|
+
return {
|
|
112
|
+
stack,
|
|
113
|
+
detectedName,
|
|
114
|
+
detectedDescription: pkg.description ?? "",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
5
117
|
export function registerInitCommand(program) {
|
|
6
118
|
program
|
|
7
119
|
.command("init")
|
|
8
120
|
.description("Scaffold an agentic workspace with Memory Bank and triangulation config")
|
|
9
|
-
.
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
121
|
+
.option("-f, --force", "Overwrite existing files without prompting")
|
|
122
|
+
.action(async (options) => {
|
|
123
|
+
const force = options.force ?? false;
|
|
124
|
+
const cwd = process.cwd();
|
|
125
|
+
const globalConfig = await getConfig();
|
|
126
|
+
if (!globalConfig.theme?.disableBanner) {
|
|
127
|
+
console.log(banner());
|
|
128
|
+
}
|
|
129
|
+
const { stack, detectedName, detectedDescription } = await detectStack(cwd);
|
|
130
|
+
const hasDetection = Boolean(stack.language);
|
|
131
|
+
if (hasDetection) {
|
|
132
|
+
console.log(heading(" Auto-detected stack:\n"));
|
|
133
|
+
if (stack.language)
|
|
134
|
+
console.log(info(`Language: ${stack.language}`));
|
|
135
|
+
if (stack.runtime)
|
|
136
|
+
console.log(info(`Runtime: ${stack.runtime}`));
|
|
137
|
+
if (stack.build)
|
|
138
|
+
console.log(info(`Build: ${stack.build}`));
|
|
139
|
+
if (stack.framework)
|
|
140
|
+
console.log(info(`Framework: ${stack.framework}`));
|
|
141
|
+
if (stack.extras.length)
|
|
142
|
+
console.log(info(`Extras: ${stack.extras.join(", ")}`));
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
const projectName = await input({
|
|
146
|
+
message: "Project Name:",
|
|
147
|
+
default: detectedName || undefined,
|
|
148
|
+
});
|
|
149
|
+
const description = await input({
|
|
150
|
+
message: "Short Description:",
|
|
151
|
+
default: detectedDescription || undefined,
|
|
152
|
+
});
|
|
153
|
+
const contextDir = join(cwd, ".context");
|
|
13
154
|
await mkdir(contextDir, { recursive: true });
|
|
14
155
|
const files = [
|
|
15
156
|
["TOCKET.md", tocketMd(projectName)],
|
|
16
157
|
["CLAUDE.md", claudeMd(projectName, description)],
|
|
17
158
|
["GEMINI.md", geminiMd(projectName, description)],
|
|
159
|
+
[".cursorrules", cursorrulesMd(projectName, description)],
|
|
18
160
|
[join(".context", "activeContext.md"), activeContextMd(projectName)],
|
|
19
161
|
[join(".context", "systemPatterns.md"), systemPatternsMd(projectName)],
|
|
20
|
-
[
|
|
21
|
-
|
|
162
|
+
[
|
|
163
|
+
join(".context", "productContext.md"),
|
|
164
|
+
productContextMd(projectName, description),
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
join(".context", "techContext.md"),
|
|
168
|
+
techContextMd(projectName, hasDetection ? stack : undefined),
|
|
169
|
+
],
|
|
22
170
|
[join(".context", "progress.md"), progressMd(projectName)],
|
|
23
171
|
];
|
|
24
172
|
for (const [filePath, content] of files) {
|
|
25
|
-
const fullPath = join(
|
|
173
|
+
const fullPath = join(cwd, filePath);
|
|
174
|
+
const exists = await fileExists(fullPath);
|
|
175
|
+
if (exists && !force) {
|
|
176
|
+
const overwrite = await confirm({
|
|
177
|
+
message: `${filePath} already exists. Overwrite?`,
|
|
178
|
+
default: false,
|
|
179
|
+
});
|
|
180
|
+
if (!overwrite) {
|
|
181
|
+
console.log(" " + dim(`skipped ${filePath}`));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
26
185
|
await writeFile(fullPath, content, "utf-8");
|
|
27
|
-
console.log(
|
|
186
|
+
console.log(" " + success(`${exists ? "updated" : "created"} ${filePath}`));
|
|
28
187
|
}
|
|
29
|
-
console.log(
|
|
188
|
+
console.log("\n" + success(`Workspace initialized for ${projectName}!`) +
|
|
189
|
+
(hasDetection ? dim(" Stack pre-populated from package.json.") : "") +
|
|
190
|
+
"\n" + dim(" Next: run tocket generate to create your first payload.\n"));
|
|
30
191
|
});
|
|
31
192
|
}
|
|
@@ -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,29 @@
|
|
|
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
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
7
11
|
const program = new Command();
|
|
8
12
|
program
|
|
9
13
|
.name("tocket")
|
|
10
14
|
.description("The Context Engineering Framework for Multi-Agent Workspaces")
|
|
11
|
-
.version(
|
|
15
|
+
.version(pkg.version);
|
|
12
16
|
registerInitCommand(program);
|
|
13
17
|
registerGenerateCommand(program);
|
|
14
18
|
registerSyncCommand(program);
|
|
15
19
|
registerValidateCommand(program);
|
|
16
|
-
program
|
|
20
|
+
registerConfigCommand(program);
|
|
21
|
+
// No-args: show interactive dashboard (TTY) or help (non-TTY)
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
if (args.length === 0 && process.stdin.isTTY) {
|
|
24
|
+
const { showDashboard } = await import("./commands/dashboard.js");
|
|
25
|
+
await showDashboard(program);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
program.parse();
|
|
29
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
export interface StackInfo {
|
|
2
|
+
language: string;
|
|
3
|
+
runtime: string;
|
|
4
|
+
build: string;
|
|
5
|
+
framework: string;
|
|
6
|
+
extras: string[];
|
|
7
|
+
}
|
|
1
8
|
export declare const claudeMd: (projectName: string, description: string) => string;
|
|
2
9
|
export declare const geminiMd: (projectName: string, description: string) => string;
|
|
3
10
|
export declare const activeContextMd: (projectName: string) => string;
|
|
4
11
|
export declare const systemPatternsMd: (projectName: string) => string;
|
|
5
12
|
export declare const productContextMd: (projectName: string, description: string) => string;
|
|
6
|
-
export declare const techContextMd: (projectName: string) => string;
|
|
13
|
+
export declare const techContextMd: (projectName: string, stack?: StackInfo) => string;
|
|
7
14
|
export declare const progressMd: (projectName: string) => string;
|
|
15
|
+
export declare const cursorrulesMd: (projectName: string, description: string) => string;
|
|
8
16
|
export declare const tocketMd: (projectName: string) => string;
|
|
@@ -163,7 +163,15 @@ _Who is this for?_
|
|
|
163
163
|
|
|
164
164
|
- _List the guiding principles for this project_
|
|
165
165
|
`;
|
|
166
|
-
export const techContextMd = (projectName) =>
|
|
166
|
+
export const techContextMd = (projectName, stack) => {
|
|
167
|
+
const lang = stack?.language || "";
|
|
168
|
+
const rt = stack?.runtime || "";
|
|
169
|
+
const bld = stack?.build || "";
|
|
170
|
+
const fw = stack?.framework || "";
|
|
171
|
+
const extras = stack?.extras?.length
|
|
172
|
+
? `\n### Notable Dependencies\n\n${stack.extras.map((d) => `- \`${d}\``).join("\n")}\n`
|
|
173
|
+
: "";
|
|
174
|
+
return `# Tech Context - ${projectName}
|
|
167
175
|
|
|
168
176
|
<!-- Stack, build tools, and critical rules. Updated by the Architect. -->
|
|
169
177
|
|
|
@@ -171,11 +179,11 @@ export const techContextMd = (projectName) => `# Tech Context - ${projectName}
|
|
|
171
179
|
|
|
172
180
|
| Layer | Technology | Notes |
|
|
173
181
|
|-------|-----------|-------|
|
|
174
|
-
| Language | | |
|
|
175
|
-
| Runtime | | |
|
|
176
|
-
| Build | | |
|
|
177
|
-
| Framework | | |
|
|
178
|
-
|
|
182
|
+
| Language | ${lang} | ${lang ? "Auto-detected by Tocket" : ""} |
|
|
183
|
+
| Runtime | ${rt} | ${rt ? "Auto-detected by Tocket" : ""} |
|
|
184
|
+
| Build | ${bld} | ${bld ? "Auto-detected by Tocket" : ""} |
|
|
185
|
+
| Framework | ${fw} | ${fw ? "Auto-detected by Tocket" : ""} |
|
|
186
|
+
${extras}
|
|
179
187
|
## Critical Rules
|
|
180
188
|
|
|
181
189
|
_Document any rules that agents must follow (e.g., import conventions, naming patterns)._
|
|
@@ -188,6 +196,7 @@ ${projectName}/
|
|
|
188
196
|
.context/ # Memory Bank
|
|
189
197
|
\`\`\`
|
|
190
198
|
`;
|
|
199
|
+
};
|
|
191
200
|
export const progressMd = (projectName) => `# Progress Log - ${projectName}
|
|
192
201
|
|
|
193
202
|
<!-- Appended by tocket sync and manual updates -->
|
|
@@ -203,6 +212,38 @@ export const progressMd = (projectName) => `# Progress Log - ${projectName}
|
|
|
203
212
|
|
|
204
213
|
- [ ] _Define your first milestone here_
|
|
205
214
|
`;
|
|
215
|
+
export const cursorrulesMd = (projectName, description) => `# .cursorrules - ${projectName}
|
|
216
|
+
|
|
217
|
+
# Generated by Tocket CLI — compatible with Cursor IDE
|
|
218
|
+
|
|
219
|
+
## Role
|
|
220
|
+
|
|
221
|
+
You are the **Executor** for **${projectName}**.
|
|
222
|
+
${description ? `\n> ${description}\n` : ""}
|
|
223
|
+
Your job is to **implement**, not to plan. Read the Memory Bank before every session, follow the Architect's decisions, and write code.
|
|
224
|
+
|
|
225
|
+
## Rules
|
|
226
|
+
|
|
227
|
+
1. **Always read \`.context/\` first** — Start every session by reading activeContext.md and systemPatterns.md.
|
|
228
|
+
2. **Follow the Architect's plan** — Implementation decisions come from mission briefs. Do not redesign.
|
|
229
|
+
3. **Ask before deviating** — If the plan is unclear or blocked, ask the user. Do not improvise architecture.
|
|
230
|
+
4. **Write code in English** — Variables, functions, comments, commits — all in \`en-US\`.
|
|
231
|
+
5. **Update Memory Bank on completion** — After finishing a task, update activeContext.md with what changed.
|
|
232
|
+
|
|
233
|
+
## Memory Bank
|
|
234
|
+
|
|
235
|
+
| File | Purpose |
|
|
236
|
+
|------|---------|
|
|
237
|
+
| \`.context/activeContext.md\` | Current focus, recent changes, open decisions |
|
|
238
|
+
| \`.context/systemPatterns.md\` | Architecture patterns, tech stack, conventions |
|
|
239
|
+
| \`.context/techContext.md\` | Stack, build tools, critical rules |
|
|
240
|
+
|
|
241
|
+
## Workflow
|
|
242
|
+
|
|
243
|
+
\`\`\`
|
|
244
|
+
1. Read .context/ → 2. Receive task → 3. Implement → 4. Update .context/
|
|
245
|
+
\`\`\`
|
|
246
|
+
`;
|
|
206
247
|
export const tocketMd = (projectName) => `# Tocket Protocol Specification
|
|
207
248
|
|
|
208
249
|
> How any AI agent should operate in **${projectName}**.
|
|
@@ -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,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
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { claudeMd, geminiMd, tocketMd, activeContextMd, systemPatternsMd, productContextMd, techContextMd, progressMd, } from "../templates/memory-bank.js";
|
|
3
|
+
import { claudeMd, geminiMd, tocketMd, activeContextMd, systemPatternsMd, productContextMd, techContextMd, progressMd, cursorrulesMd, } from "../templates/memory-bank.js";
|
|
4
4
|
describe("claudeMd", () => {
|
|
5
5
|
const output = claudeMd("TestProject", "A test project");
|
|
6
6
|
it("includes the project name in the title", () => {
|
|
@@ -106,17 +106,63 @@ describe("productContextMd", () => {
|
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
describe("techContextMd", () => {
|
|
109
|
-
const output = techContextMd("TestProject");
|
|
110
109
|
it("includes the project name", () => {
|
|
110
|
+
const output = techContextMd("TestProject");
|
|
111
111
|
assert.ok(output.includes("# Tech Context - TestProject"));
|
|
112
112
|
});
|
|
113
113
|
it("has a stack table", () => {
|
|
114
|
+
const output = techContextMd("TestProject");
|
|
114
115
|
assert.ok(output.includes("| Language |"));
|
|
115
116
|
assert.ok(output.includes("| Runtime |"));
|
|
116
117
|
});
|
|
117
118
|
it("includes project structure with project name", () => {
|
|
119
|
+
const output = techContextMd("TestProject");
|
|
118
120
|
assert.ok(output.includes("TestProject/"));
|
|
119
121
|
});
|
|
122
|
+
it("renders empty rows when no stack provided", () => {
|
|
123
|
+
const output = techContextMd("TestProject");
|
|
124
|
+
// Should have empty cells (no "Auto-detected")
|
|
125
|
+
assert.ok(!output.includes("Auto-detected"));
|
|
126
|
+
});
|
|
127
|
+
it("renders detected stack when StackInfo is provided", () => {
|
|
128
|
+
const stack = {
|
|
129
|
+
language: "TypeScript",
|
|
130
|
+
runtime: "Node.js",
|
|
131
|
+
build: "Vite",
|
|
132
|
+
framework: "React",
|
|
133
|
+
extras: ["tailwindcss", "prisma"],
|
|
134
|
+
};
|
|
135
|
+
const output = techContextMd("TestProject", stack);
|
|
136
|
+
assert.ok(output.includes("TypeScript"));
|
|
137
|
+
assert.ok(output.includes("Node.js"));
|
|
138
|
+
assert.ok(output.includes("Vite"));
|
|
139
|
+
assert.ok(output.includes("React"));
|
|
140
|
+
assert.ok(output.includes("Auto-detected by Tocket"));
|
|
141
|
+
});
|
|
142
|
+
it("renders notable dependencies when extras are present", () => {
|
|
143
|
+
const stack = {
|
|
144
|
+
language: "TypeScript",
|
|
145
|
+
runtime: "Node.js",
|
|
146
|
+
build: "tsc",
|
|
147
|
+
framework: "",
|
|
148
|
+
extras: ["tailwindcss", "zod"],
|
|
149
|
+
};
|
|
150
|
+
const output = techContextMd("TestProject", stack);
|
|
151
|
+
assert.ok(output.includes("### Notable Dependencies"));
|
|
152
|
+
assert.ok(output.includes("`tailwindcss`"));
|
|
153
|
+
assert.ok(output.includes("`zod`"));
|
|
154
|
+
});
|
|
155
|
+
it("omits notable dependencies section when extras are empty", () => {
|
|
156
|
+
const stack = {
|
|
157
|
+
language: "TypeScript",
|
|
158
|
+
runtime: "Node.js",
|
|
159
|
+
build: "tsc",
|
|
160
|
+
framework: "",
|
|
161
|
+
extras: [],
|
|
162
|
+
};
|
|
163
|
+
const output = techContextMd("TestProject", stack);
|
|
164
|
+
assert.ok(!output.includes("### Notable Dependencies"));
|
|
165
|
+
});
|
|
120
166
|
});
|
|
121
167
|
describe("progressMd", () => {
|
|
122
168
|
const output = progressMd("TestProject");
|
|
@@ -130,3 +176,24 @@ describe("progressMd", () => {
|
|
|
130
176
|
assert.ok(output.includes("## Next Up"));
|
|
131
177
|
});
|
|
132
178
|
});
|
|
179
|
+
describe("cursorrulesMd", () => {
|
|
180
|
+
const output = cursorrulesMd("TestProject", "A cool CLI tool");
|
|
181
|
+
it("includes the project name", () => {
|
|
182
|
+
assert.ok(output.includes("# .cursorrules - TestProject"));
|
|
183
|
+
});
|
|
184
|
+
it("includes the description as a blockquote", () => {
|
|
185
|
+
assert.ok(output.includes("> A cool CLI tool"));
|
|
186
|
+
});
|
|
187
|
+
it("defines the Executor role", () => {
|
|
188
|
+
assert.ok(output.includes("**Executor**"));
|
|
189
|
+
});
|
|
190
|
+
it("references Memory Bank files", () => {
|
|
191
|
+
assert.ok(output.includes(".context/activeContext.md"));
|
|
192
|
+
assert.ok(output.includes(".context/systemPatterns.md"));
|
|
193
|
+
});
|
|
194
|
+
it("handles empty description gracefully", () => {
|
|
195
|
+
const noDesc = cursorrulesMd("Foo", "");
|
|
196
|
+
assert.ok(noDesc.includes("# .cursorrules - Foo"));
|
|
197
|
+
assert.ok(!noDesc.includes("> \n"));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TocketConfig {
|
|
2
|
+
author?: string;
|
|
3
|
+
defaultAgent?: string;
|
|
4
|
+
defaults?: {
|
|
5
|
+
priority?: "high" | "medium" | "low";
|
|
6
|
+
skills?: string;
|
|
7
|
+
};
|
|
8
|
+
theme?: {
|
|
9
|
+
disableBanner?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function getConfigPath(): string;
|
|
13
|
+
export declare function getConfig(configPath?: string): Promise<TocketConfig>;
|
|
14
|
+
export declare function saveConfig(config: TocketConfig, configPath?: string): Promise<void>;
|
|
15
|
+
export declare function updateConfig(partial: Partial<TocketConfig>, configPath?: string): Promise<void>;
|
|
16
|
+
export declare function resetConfig(configPath?: string): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
export function getConfigPath() {
|
|
5
|
+
return join(homedir(), ".tocketrc.json");
|
|
6
|
+
}
|
|
7
|
+
export async function getConfig(configPath) {
|
|
8
|
+
const path = configPath ?? getConfigPath();
|
|
9
|
+
try {
|
|
10
|
+
const content = await readFile(path, "utf-8");
|
|
11
|
+
return JSON.parse(content);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function saveConfig(config, configPath) {
|
|
18
|
+
const path = configPath ?? getConfigPath();
|
|
19
|
+
await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
20
|
+
}
|
|
21
|
+
export async function updateConfig(partial, configPath) {
|
|
22
|
+
const current = await getConfig(configPath);
|
|
23
|
+
const merged = {
|
|
24
|
+
...current,
|
|
25
|
+
...partial,
|
|
26
|
+
defaults: { ...current.defaults, ...partial.defaults },
|
|
27
|
+
theme: { ...current.theme, ...partial.theme },
|
|
28
|
+
};
|
|
29
|
+
await saveConfig(merged, configPath);
|
|
30
|
+
}
|
|
31
|
+
export async function resetConfig(configPath) {
|
|
32
|
+
await saveConfig({}, configPath);
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function isGitRepo(cwd?: string): boolean;
|
|
2
|
+
export declare function getStagedFiles(cwd?: string): string[];
|
|
3
|
+
export declare function getModifiedFiles(cwd?: string): string[];
|
|
4
|
+
export declare function getRecentCommits(n?: number, cwd?: string): string[];
|
|
5
|
+
export declare function getRecentCommitsRaw(n?: number, cwd?: string): string;
|
|
6
|
+
export declare function getCurrentBranch(cwd?: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export function isGitRepo(cwd = process.cwd()) {
|
|
5
|
+
return existsSync(join(cwd, ".git"));
|
|
6
|
+
}
|
|
7
|
+
export function getStagedFiles(cwd = process.cwd()) {
|
|
8
|
+
if (!isGitRepo(cwd))
|
|
9
|
+
return [];
|
|
10
|
+
try {
|
|
11
|
+
const output = execSync("git diff --name-only --cached", {
|
|
12
|
+
cwd,
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
}).trim();
|
|
15
|
+
return output ? output.split("\n") : [];
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function getModifiedFiles(cwd = process.cwd()) {
|
|
22
|
+
if (!isGitRepo(cwd))
|
|
23
|
+
return [];
|
|
24
|
+
try {
|
|
25
|
+
const output = execSync("git status --porcelain", {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
}).trim();
|
|
29
|
+
if (!output)
|
|
30
|
+
return [];
|
|
31
|
+
return output
|
|
32
|
+
.split("\n")
|
|
33
|
+
.filter((line) => line.length > 3)
|
|
34
|
+
.map((line) => line.substring(3).trim());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function getRecentCommits(n = 5, cwd = process.cwd()) {
|
|
41
|
+
if (!isGitRepo(cwd))
|
|
42
|
+
return [];
|
|
43
|
+
try {
|
|
44
|
+
const output = execSync(`git log --oneline -${n}`, {
|
|
45
|
+
cwd,
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
}).trim();
|
|
48
|
+
return output ? output.split("\n") : [];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function getRecentCommitsRaw(n = 5, cwd = process.cwd()) {
|
|
55
|
+
if (!isGitRepo(cwd))
|
|
56
|
+
return "_No commits found or not a git repository._";
|
|
57
|
+
try {
|
|
58
|
+
return execSync(`git log --oneline -${n}`, {
|
|
59
|
+
cwd,
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
}).trim();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return "_No commits found or not a git repository._";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function getCurrentBranch(cwd = process.cwd()) {
|
|
68
|
+
if (!isGitRepo(cwd))
|
|
69
|
+
return "";
|
|
70
|
+
try {
|
|
71
|
+
return execSync("git branch --show-current", {
|
|
72
|
+
cwd,
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
}).trim();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const purple: import("chalk").ChalkInstance;
|
|
2
|
+
export declare const green: import("chalk").ChalkInstance;
|
|
3
|
+
export declare const red: import("chalk").ChalkInstance;
|
|
4
|
+
export declare const yellow: import("chalk").ChalkInstance;
|
|
5
|
+
export declare const dimmed: import("chalk").ChalkInstance;
|
|
6
|
+
export declare const bold: import("chalk").ChalkInstance;
|
|
7
|
+
export declare function banner(): string;
|
|
8
|
+
export declare function success(msg: string): string;
|
|
9
|
+
export declare function error(msg: string): string;
|
|
10
|
+
export declare function warn(msg: string): string;
|
|
11
|
+
export declare function info(msg: string): string;
|
|
12
|
+
export declare function heading(msg: string): string;
|
|
13
|
+
export declare function dim(msg: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
// ── Brand colors ──────────────────────────────────────────────────────
|
|
3
|
+
export const purple = chalk.hex("#7C3AED");
|
|
4
|
+
export const green = chalk.green;
|
|
5
|
+
export const red = chalk.red;
|
|
6
|
+
export const yellow = chalk.yellow;
|
|
7
|
+
export const dimmed = chalk.dim;
|
|
8
|
+
export const bold = chalk.bold;
|
|
9
|
+
// ── ASCII banner ──────────────────────────────────────────────────────
|
|
10
|
+
export function banner() {
|
|
11
|
+
const art = `
|
|
12
|
+
████████╗ ██████╗ ██████╗██╗ ██╗███████╗████████╗
|
|
13
|
+
╚══██╔══╝██╔═══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝
|
|
14
|
+
██║ ██║ ██║██║ █████╔╝ █████╗ ██║
|
|
15
|
+
██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║
|
|
16
|
+
██║ ╚██████╔╝╚██████╗██║ ██╗███████╗ ██║
|
|
17
|
+
╚═╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝`;
|
|
18
|
+
return purple(art) + "\n" + dimmed(" Context Engineering Framework\n");
|
|
19
|
+
}
|
|
20
|
+
// ── Semantic helpers ──────────────────────────────────────────────────
|
|
21
|
+
export function success(msg) {
|
|
22
|
+
return `${green("\u2713")} ${msg}`;
|
|
23
|
+
}
|
|
24
|
+
export function error(msg) {
|
|
25
|
+
return `${red("\u2717")} ${msg}`;
|
|
26
|
+
}
|
|
27
|
+
export function warn(msg) {
|
|
28
|
+
return `${yellow("\u26A0")} ${msg}`;
|
|
29
|
+
}
|
|
30
|
+
export function info(msg) {
|
|
31
|
+
return `${purple("\u203A")} ${msg}`;
|
|
32
|
+
}
|
|
33
|
+
export function heading(msg) {
|
|
34
|
+
return bold(purple(msg));
|
|
35
|
+
}
|
|
36
|
+
export function dim(msg) {
|
|
37
|
+
return dimmed(msg);
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pedrocivita/tocket",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "The Context Engineering Framework for Multi-Agent Workspaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@inquirer/prompts": "^8.3.0",
|
|
40
|
+
"chalk": "^5.6.2",
|
|
40
41
|
"clipboardy": "^5.3.0",
|
|
41
42
|
"commander": "^14.0.3"
|
|
42
43
|
},
|