@xiaofandegeng/rmemo 0.0.3
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/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +28 -0
- package/DEVELOPMENT_PLAN.md +77 -0
- package/LICENSE +22 -0
- package/README.md +150 -0
- package/README.zh-CN.md +156 -0
- package/bin/rmemo.js +63 -0
- package/package.json +47 -0
- package/src/cmd/check.js +30 -0
- package/src/cmd/context.js +15 -0
- package/src/cmd/done.js +55 -0
- package/src/cmd/hook.js +108 -0
- package/src/cmd/init.js +86 -0
- package/src/cmd/log.js +12 -0
- package/src/cmd/print.js +15 -0
- package/src/cmd/scan.js +25 -0
- package/src/cmd/start.js +41 -0
- package/src/cmd/status.js +147 -0
- package/src/cmd/todo.js +85 -0
- package/src/core/check.js +396 -0
- package/src/core/context.js +114 -0
- package/src/core/journal.js +31 -0
- package/src/core/scan.js +282 -0
- package/src/core/todos.js +143 -0
- package/src/lib/args.js +81 -0
- package/src/lib/git.js +35 -0
- package/src/lib/io.js +43 -0
- package/src/lib/paths.js +38 -0
- package/src/lib/stdin.js +12 -0
- package/src/lib/time.js +13 -0
- package/src/lib/walk.js +44 -0
- package/test/smoke.test.js +395 -0
package/src/cmd/hook.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
6
|
+
import { ensureDir, fileExists, readText, writeText } from "../lib/io.js";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
const HOOK_MARKER = "rmemo pre-commit hook";
|
|
11
|
+
|
|
12
|
+
async function gitTopLevel(root) {
|
|
13
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd: root });
|
|
14
|
+
return stdout.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function nowStamp() {
|
|
18
|
+
const d = new Date();
|
|
19
|
+
const y = d.getFullYear();
|
|
20
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
21
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
22
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
23
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
24
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
25
|
+
return `${y}${m}${day}_${hh}${mm}${ss}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderPreCommit({ rmemoBinAbs }) {
|
|
29
|
+
// Prefer calling the exact rmemo entrypoint that installed the hook.
|
|
30
|
+
// This keeps behavior consistent even before rmemo is published to npm.
|
|
31
|
+
const q = (s) => `"${String(s).replace(/"/g, '\\"')}"`;
|
|
32
|
+
|
|
33
|
+
return `#!/usr/bin/env bash
|
|
34
|
+
# ${HOOK_MARKER}
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
|
|
37
|
+
repo_root="$(git rev-parse --show-toplevel)"
|
|
38
|
+
|
|
39
|
+
${q(rmemoBinAbs)} --root "$repo_root" --staged check
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function installPreCommitHook({ repoRoot, rmemoBinAbs, force }) {
|
|
44
|
+
const hooksDir = path.join(repoRoot, ".git", "hooks");
|
|
45
|
+
await ensureDir(hooksDir);
|
|
46
|
+
|
|
47
|
+
const hookPath = path.join(hooksDir, "pre-commit");
|
|
48
|
+
const content = renderPreCommit({ rmemoBinAbs });
|
|
49
|
+
|
|
50
|
+
if (await fileExists(hookPath)) {
|
|
51
|
+
const existing = await readText(hookPath, 256_000);
|
|
52
|
+
const ours = existing.includes(HOOK_MARKER);
|
|
53
|
+
if (!force && !ours) {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`Refusing to overwrite existing pre-commit hook: ${hookPath}\n` +
|
|
56
|
+
`Re-run with --force to overwrite (a backup will be created).\n`
|
|
57
|
+
);
|
|
58
|
+
process.exitCode = 2;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (force && !ours) {
|
|
62
|
+
const bak = `${hookPath}.bak.${nowStamp()}`;
|
|
63
|
+
await fs.copyFile(hookPath, bak);
|
|
64
|
+
process.stdout.write(`Backed up existing hook to: ${bak}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await writeText(hookPath, content);
|
|
69
|
+
await fs.chmod(hookPath, 0o755);
|
|
70
|
+
process.stdout.write(`Installed pre-commit hook: ${hookPath}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function cmdHook({ rest, flags }) {
|
|
74
|
+
const sub = rest[0];
|
|
75
|
+
if (!sub || sub === "help") {
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
[
|
|
78
|
+
"Usage:",
|
|
79
|
+
" rmemo hook install [--force]",
|
|
80
|
+
"",
|
|
81
|
+
"Notes:",
|
|
82
|
+
"- The hook runs `rmemo check` before commit.",
|
|
83
|
+
"- Run `rmemo init` in the target repo to create `.repo-memory/rules.json`."
|
|
84
|
+
].join("\n") + "\n"
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (sub !== "install") {
|
|
90
|
+
throw new Error(`Unknown subcommand: hook ${sub}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const root = resolveRoot(flags);
|
|
94
|
+
const force = !!flags.force;
|
|
95
|
+
|
|
96
|
+
let repoRoot;
|
|
97
|
+
try {
|
|
98
|
+
repoRoot = await gitTopLevel(root);
|
|
99
|
+
} catch {
|
|
100
|
+
process.stderr.write(`Not a git repo (or git not available) under: ${root}\n`);
|
|
101
|
+
process.exitCode = 2;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Absolute path to the rmemo entrypoint used to install this hook.
|
|
106
|
+
const rmemoBinAbs = path.resolve(process.argv[1]);
|
|
107
|
+
await installPreCommitHook({ repoRoot, rmemoBinAbs, force });
|
|
108
|
+
}
|
package/src/cmd/init.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDir, fileExists, writeJson, writeText } from "../lib/io.js";
|
|
4
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
5
|
+
import { journalDir, manifestPath, indexPath, rulesJsonPath, rulesPath, todosPath } from "../lib/paths.js";
|
|
6
|
+
import { scanRepo } from "../core/scan.js";
|
|
7
|
+
import { generateContext } from "../core/context.js";
|
|
8
|
+
import { contextPath, memDir } from "../lib/paths.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_RULES = `# Rules
|
|
11
|
+
|
|
12
|
+
This file is intentionally short and strict.
|
|
13
|
+
|
|
14
|
+
## Project Conventions
|
|
15
|
+
- (Add your conventions here)
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
- (Add module boundaries here)
|
|
19
|
+
|
|
20
|
+
## AI Constraints
|
|
21
|
+
- Do not invent files or APIs. Ask if unsure.
|
|
22
|
+
- Follow existing patterns in this repo.
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const DEFAULT_TODOS = `# Todos
|
|
26
|
+
|
|
27
|
+
## Next
|
|
28
|
+
- (Write the next concrete step)
|
|
29
|
+
|
|
30
|
+
## Blockers
|
|
31
|
+
- (If any)
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const DEFAULT_RULES_JSON = {
|
|
35
|
+
schema: 1,
|
|
36
|
+
// These are repo-relative patterns.
|
|
37
|
+
// Patterns support glob like "src/**" or regex like "re:^src/.*\\.vue$".
|
|
38
|
+
requiredPaths: [],
|
|
39
|
+
forbiddenPaths: [
|
|
40
|
+
// Example: forbid committing secrets
|
|
41
|
+
".env",
|
|
42
|
+
".env.*"
|
|
43
|
+
],
|
|
44
|
+
// Content scans are optional and disabled by default.
|
|
45
|
+
// Use this to prevent committing secrets (keys/tokens) by matching patterns in file contents.
|
|
46
|
+
// Example:
|
|
47
|
+
// forbiddenContent: [
|
|
48
|
+
// { include: ["**/*"], exclude: ["**/*.png"], match: "BEGIN PRIVATE KEY", message: "Do not commit private keys." }
|
|
49
|
+
// ]
|
|
50
|
+
forbiddenContent: [],
|
|
51
|
+
namingRules: [
|
|
52
|
+
// Example:
|
|
53
|
+
// {
|
|
54
|
+
// "include": ["src/pages/**"],
|
|
55
|
+
// "exclude": ["src/pages/**/__tests__/**"],
|
|
56
|
+
// "target": "basename",
|
|
57
|
+
// "match": "^[a-z0-9-]+\\.vue$",
|
|
58
|
+
// "message": "Vue page filenames should be kebab-case."
|
|
59
|
+
// }
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export async function cmdInit({ flags }) {
|
|
64
|
+
const root = resolveRoot(flags);
|
|
65
|
+
|
|
66
|
+
await ensureDir(memDir(root));
|
|
67
|
+
await ensureDir(journalDir(root));
|
|
68
|
+
|
|
69
|
+
if (!(await fileExists(rulesPath(root)))) await writeText(rulesPath(root), DEFAULT_RULES);
|
|
70
|
+
if (!(await fileExists(rulesJsonPath(root)))) await writeJson(rulesJsonPath(root), DEFAULT_RULES_JSON);
|
|
71
|
+
if (!(await fileExists(todosPath(root)))) await writeText(todosPath(root), DEFAULT_TODOS);
|
|
72
|
+
|
|
73
|
+
const preferGit = flags["no-git"] ? false : true;
|
|
74
|
+
const maxFiles = Number(flags["max-files"] || 4000);
|
|
75
|
+
const { manifest, index } = await scanRepo(root, { maxFiles, preferGit });
|
|
76
|
+
|
|
77
|
+
await writeJson(manifestPath(root), manifest);
|
|
78
|
+
await writeJson(indexPath(root), index);
|
|
79
|
+
|
|
80
|
+
const snipLines = Number(flags["snip-lines"] || 120);
|
|
81
|
+
const recentDays = Number(flags["recent-days"] || 7);
|
|
82
|
+
const ctx = await generateContext(root, { snipLines, recentDays });
|
|
83
|
+
await fs.writeFile(contextPath(root), ctx, "utf8");
|
|
84
|
+
|
|
85
|
+
process.stdout.write(`Initialized: ${path.relative(process.cwd(), memDir(root))}\n`);
|
|
86
|
+
}
|
package/src/cmd/log.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
3
|
+
import { appendJournalEntry } from "../core/journal.js";
|
|
4
|
+
|
|
5
|
+
export async function cmdLog({ rest, flags }) {
|
|
6
|
+
const root = resolveRoot(flags);
|
|
7
|
+
const text = rest.join(" ").trim();
|
|
8
|
+
if (!text) throw new Error("Missing log text. Usage: rmemo log <text>");
|
|
9
|
+
|
|
10
|
+
const p = await appendJournalEntry(root, { kind: "Log", text });
|
|
11
|
+
process.stdout.write(`Logged: ${path.relative(process.cwd(), p)}\n`);
|
|
12
|
+
}
|
package/src/cmd/print.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
3
|
+
import { contextPath } from "../lib/paths.js";
|
|
4
|
+
import { ensureContextFile } from "../core/context.js";
|
|
5
|
+
|
|
6
|
+
export async function cmdPrint({ flags }) {
|
|
7
|
+
const root = resolveRoot(flags);
|
|
8
|
+
await ensureContextFile(root, {
|
|
9
|
+
snipLines: Number(flags["snip-lines"] || 120),
|
|
10
|
+
recentDays: Number(flags["recent-days"] || 7)
|
|
11
|
+
});
|
|
12
|
+
const s = await fs.readFile(contextPath(root), "utf8");
|
|
13
|
+
process.stdout.write(s);
|
|
14
|
+
}
|
|
15
|
+
|
package/src/cmd/scan.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeJson } from "../lib/io.js";
|
|
4
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
5
|
+
import { indexPath, manifestPath, contextPath } from "../lib/paths.js";
|
|
6
|
+
import { scanRepo } from "../core/scan.js";
|
|
7
|
+
import { generateContext } from "../core/context.js";
|
|
8
|
+
|
|
9
|
+
export async function cmdScan({ flags }) {
|
|
10
|
+
const root = resolveRoot(flags);
|
|
11
|
+
const preferGit = flags["no-git"] ? false : true;
|
|
12
|
+
const maxFiles = Number(flags["max-files"] || 4000);
|
|
13
|
+
const { manifest, index } = await scanRepo(root, { maxFiles, preferGit });
|
|
14
|
+
|
|
15
|
+
await writeJson(manifestPath(root), manifest);
|
|
16
|
+
await writeJson(indexPath(root), index);
|
|
17
|
+
|
|
18
|
+
const snipLines = Number(flags["snip-lines"] || 120);
|
|
19
|
+
const recentDays = Number(flags["recent-days"] || 7);
|
|
20
|
+
const ctx = await generateContext(root, { snipLines, recentDays });
|
|
21
|
+
await fs.writeFile(contextPath(root), ctx, "utf8");
|
|
22
|
+
|
|
23
|
+
process.stdout.write(`Scanned: ${path.relative(process.cwd(), root)}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
package/src/cmd/start.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
4
|
+
import { manifestPath, indexPath, contextPath } from "../lib/paths.js";
|
|
5
|
+
import { writeJson } from "../lib/io.js";
|
|
6
|
+
import { scanRepo } from "../core/scan.js";
|
|
7
|
+
import { generateContext } from "../core/context.js";
|
|
8
|
+
import { cmdStatus } from "./status.js";
|
|
9
|
+
|
|
10
|
+
export async function cmdStart({ flags }) {
|
|
11
|
+
const root = resolveRoot(flags);
|
|
12
|
+
const preferGit = flags["no-git"] ? false : true;
|
|
13
|
+
const maxFiles = Number(flags["max-files"] || 4000);
|
|
14
|
+
|
|
15
|
+
// 1) Scan and persist
|
|
16
|
+
const { manifest, index } = await scanRepo(root, { maxFiles, preferGit });
|
|
17
|
+
await writeJson(manifestPath(root), manifest);
|
|
18
|
+
await writeJson(indexPath(root), index);
|
|
19
|
+
|
|
20
|
+
// 2) Generate context pack
|
|
21
|
+
const snipLines = Number(flags["snip-lines"] || 120);
|
|
22
|
+
const recentDays = Number(flags["recent-days"] || 7);
|
|
23
|
+
const ctx = await generateContext(root, { snipLines, recentDays });
|
|
24
|
+
await fs.writeFile(contextPath(root), ctx, "utf8");
|
|
25
|
+
|
|
26
|
+
// 3) Print status (md) to stdout (paste-ready)
|
|
27
|
+
process.stdout.write("\n");
|
|
28
|
+
await cmdStatus({ flags: { ...flags, format: "md", mode: flags.mode || "brief" } });
|
|
29
|
+
|
|
30
|
+
const ctxRel = path.relative(process.cwd(), contextPath(root));
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
[
|
|
33
|
+
"",
|
|
34
|
+
"## Paste To AI (Start Of Day)",
|
|
35
|
+
"",
|
|
36
|
+
`1. Paste file: ${ctxRel}`,
|
|
37
|
+
"2. If needed, also paste the Status above",
|
|
38
|
+
""
|
|
39
|
+
].join("\n")
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileExists, readText } from "../lib/io.js";
|
|
4
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
5
|
+
import { journalDir, manifestPath, rulesPath, todosPath } from "../lib/paths.js";
|
|
6
|
+
import { parseTodos } from "../core/todos.js";
|
|
7
|
+
|
|
8
|
+
function clampLines(s, maxLines) {
|
|
9
|
+
const lines = s.split("\n");
|
|
10
|
+
if (lines.length <= maxLines) return s.trimEnd();
|
|
11
|
+
return lines.slice(0, maxLines).join("\n").trimEnd() + "\n[...truncated]";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function readMaybe(p, maxBytes = 512_000) {
|
|
15
|
+
try {
|
|
16
|
+
if (!(await fileExists(p))) return null;
|
|
17
|
+
return await readText(p, maxBytes);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function listRecentJournalFiles(root, recentDays) {
|
|
24
|
+
const dir = journalDir(root);
|
|
25
|
+
try {
|
|
26
|
+
const ents = await fs.readdir(dir, { withFileTypes: true });
|
|
27
|
+
const files = ents
|
|
28
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
29
|
+
.map((e) => e.name)
|
|
30
|
+
.sort()
|
|
31
|
+
.reverse();
|
|
32
|
+
return files.slice(0, Math.max(0, recentDays));
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function cmdStatus({ flags }) {
|
|
39
|
+
const root = resolveRoot(flags);
|
|
40
|
+
const format = String(flags.format || "md").toLowerCase();
|
|
41
|
+
const mode = String(flags.mode || "full").toLowerCase();
|
|
42
|
+
const snipLines = Number(flags["snip-lines"] || 120);
|
|
43
|
+
const recentDays = Number(flags["recent-days"] || 7);
|
|
44
|
+
|
|
45
|
+
const rules = await readMaybe(rulesPath(root), 512_000);
|
|
46
|
+
const todosMd = await readMaybe(todosPath(root), 512_000);
|
|
47
|
+
const manifestText = await readMaybe(manifestPath(root), 2_000_000);
|
|
48
|
+
|
|
49
|
+
if (!rules && !todosMd && !manifestText) {
|
|
50
|
+
process.stderr.write(`No .repo-memory found under: ${root}\nRun: rmemo --root <repo> init\n`);
|
|
51
|
+
process.exitCode = 2;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let manifest = null;
|
|
56
|
+
if (manifestText) {
|
|
57
|
+
try {
|
|
58
|
+
manifest = JSON.parse(manifestText);
|
|
59
|
+
} catch {
|
|
60
|
+
manifest = { parseError: true };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const todos = todosMd ? parseTodos(todosMd) : null;
|
|
65
|
+
const journalFiles = await listRecentJournalFiles(root, recentDays);
|
|
66
|
+
const journal = [];
|
|
67
|
+
for (const fn of journalFiles) {
|
|
68
|
+
const jp = path.join(journalDir(root), fn);
|
|
69
|
+
const s = await readMaybe(jp, 512_000);
|
|
70
|
+
if (!s) continue;
|
|
71
|
+
journal.push({ file: fn, text: s.trimEnd() });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (format === "json") {
|
|
75
|
+
const payload = {
|
|
76
|
+
schema: 1,
|
|
77
|
+
generatedAt: new Date().toISOString(),
|
|
78
|
+
root,
|
|
79
|
+
mode,
|
|
80
|
+
title: manifest?.title || null,
|
|
81
|
+
manifest,
|
|
82
|
+
rules: rules ? clampLines(rules, snipLines) : null,
|
|
83
|
+
todos,
|
|
84
|
+
recentJournal: journal.map((j) => ({ file: j.file, text: clampLines(j.text, snipLines) }))
|
|
85
|
+
};
|
|
86
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (format !== "md") {
|
|
91
|
+
throw new Error(`Unsupported --format: ${format} (use md or json)`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parts = [];
|
|
95
|
+
parts.push(`# Status\n`);
|
|
96
|
+
if (manifest?.title) parts.push(`Repo: ${manifest.title}\n`);
|
|
97
|
+
parts.push(`Root: ${root}\n`);
|
|
98
|
+
parts.push(`Generated: ${new Date().toISOString()}\n`);
|
|
99
|
+
|
|
100
|
+
if (todos) {
|
|
101
|
+
parts.push(`## Next\n`);
|
|
102
|
+
if (todos.next.length) parts.push(todos.next.map((x) => `- ${x}`).join("\n") + "\n");
|
|
103
|
+
else parts.push(`- (empty)\n`);
|
|
104
|
+
|
|
105
|
+
parts.push(`## Blockers\n`);
|
|
106
|
+
if (todos.blockers.length) parts.push(todos.blockers.map((x) => `- ${x}`).join("\n") + "\n");
|
|
107
|
+
else parts.push(`- (none)\n`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (manifest) {
|
|
111
|
+
parts.push(`## Structure Hints\n`);
|
|
112
|
+
if (Array.isArray(manifest.repoHints) && manifest.repoHints.length) parts.push(`- repoHints: ${manifest.repoHints.join(", ")}\n`);
|
|
113
|
+
if (Array.isArray(manifest.lockfiles) && manifest.lockfiles.length) parts.push(`- lockfiles: ${manifest.lockfiles.join(", ")}\n`);
|
|
114
|
+
if (manifest.packageJson?.frameworks?.length) parts.push(`- frameworks: ${manifest.packageJson.frameworks.join(", ")}\n`);
|
|
115
|
+
if (manifest.packageJson?.packageManager) parts.push(`- packageManager: ${manifest.packageJson.packageManager}\n`);
|
|
116
|
+
if (Array.isArray(manifest.topDirs) && manifest.topDirs.length) {
|
|
117
|
+
const dirs = manifest.topDirs.slice(0, 10).map((d) => `${d.name}(${d.fileCount})`).join(", ");
|
|
118
|
+
parts.push(`- topDirs: ${dirs}\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (mode !== "brief") {
|
|
123
|
+
if (rules) {
|
|
124
|
+
parts.push(`## Rules (Excerpt)\n`);
|
|
125
|
+
parts.push(clampLines(rules, Math.min(snipLines, 80)) + "\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (journal.length) {
|
|
129
|
+
parts.push(`## Recent Journal\n`);
|
|
130
|
+
for (const j of journal) {
|
|
131
|
+
parts.push(`### ${j.file}\n`);
|
|
132
|
+
parts.push(clampLines(j.text, Math.min(snipLines, 120)) + "\n");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
parts.push(`## Paste To AI\n`);
|
|
138
|
+
parts.push(
|
|
139
|
+
[
|
|
140
|
+
"1. Run: `rmemo context`",
|
|
141
|
+
"2. Paste: `.repo-memory/context.md`",
|
|
142
|
+
"3. Optionally paste this `status` output for a quick 'where we are now'."
|
|
143
|
+
].join("\n") + "\n"
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
process.stdout.write(parts.join("\n").trimEnd() + "\n");
|
|
147
|
+
}
|
package/src/cmd/todo.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { resolveRoot } from "../lib/paths.js";
|
|
4
|
+
import {
|
|
5
|
+
ensureTodosFile,
|
|
6
|
+
parseTodos,
|
|
7
|
+
addTodoNext,
|
|
8
|
+
addTodoBlocker,
|
|
9
|
+
removeTodoNextByIndex,
|
|
10
|
+
removeTodoBlockerByIndex
|
|
11
|
+
} from "../core/todos.js";
|
|
12
|
+
import { todosPath } from "../lib/paths.js";
|
|
13
|
+
|
|
14
|
+
export async function cmdTodo({ rest, flags }) {
|
|
15
|
+
const root = resolveRoot(flags);
|
|
16
|
+
const sub = rest[0];
|
|
17
|
+
|
|
18
|
+
if (!sub || sub === "help") {
|
|
19
|
+
process.stdout.write(
|
|
20
|
+
[
|
|
21
|
+
"Usage:",
|
|
22
|
+
" rmemo todo add <text>",
|
|
23
|
+
" rmemo todo block <text>",
|
|
24
|
+
" rmemo todo done <n>",
|
|
25
|
+
" rmemo todo unblock <n>",
|
|
26
|
+
" rmemo todo ls",
|
|
27
|
+
""
|
|
28
|
+
].join("\n") + "\n"
|
|
29
|
+
);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (sub === "add") {
|
|
34
|
+
const text = rest.slice(1).join(" ").trim();
|
|
35
|
+
if (!text) throw new Error("Missing text. Usage: rmemo todo add <text>");
|
|
36
|
+
const p = await addTodoNext(root, text);
|
|
37
|
+
process.stdout.write(`Updated todos: ${path.relative(process.cwd(), p)}\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (sub === "block") {
|
|
42
|
+
const text = rest.slice(1).join(" ").trim();
|
|
43
|
+
if (!text) throw new Error("Missing text. Usage: rmemo todo block <text>");
|
|
44
|
+
const p = await addTodoBlocker(root, text);
|
|
45
|
+
process.stdout.write(`Updated todos: ${path.relative(process.cwd(), p)}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (sub === "ls") {
|
|
50
|
+
await ensureTodosFile(root);
|
|
51
|
+
const md = await fs.readFile(todosPath(root), "utf8");
|
|
52
|
+
const t = parseTodos(md);
|
|
53
|
+
const numbered = (arr) => arr.map((x, i) => `${i + 1}. ${x}`);
|
|
54
|
+
const out = [
|
|
55
|
+
"# Todos",
|
|
56
|
+
"",
|
|
57
|
+
"## Next",
|
|
58
|
+
...(t.next.length ? numbered(t.next) : ["- (empty)"]),
|
|
59
|
+
"",
|
|
60
|
+
"## Blockers",
|
|
61
|
+
...(t.blockers.length ? numbered(t.blockers) : ["- (none)"]),
|
|
62
|
+
""
|
|
63
|
+
].join("\n");
|
|
64
|
+
process.stdout.write(out);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sub === "done") {
|
|
69
|
+
const n = rest[1];
|
|
70
|
+
if (!n) throw new Error("Missing index. Usage: rmemo todo done <n>");
|
|
71
|
+
const p = await removeTodoNextByIndex(root, n);
|
|
72
|
+
process.stdout.write(`Updated todos: ${path.relative(process.cwd(), p)}\n`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sub === "unblock") {
|
|
77
|
+
const n = rest[1];
|
|
78
|
+
if (!n) throw new Error("Missing index. Usage: rmemo todo unblock <n>");
|
|
79
|
+
const p = await removeTodoBlockerByIndex(root, n);
|
|
80
|
+
process.stdout.write(`Updated todos: ${path.relative(process.cwd(), p)}\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error(`Unknown subcommand: todo ${sub}`);
|
|
85
|
+
}
|