ithos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/ithos.d.ts +3 -0
- package/dist/bin/ithos.d.ts.map +1 -0
- package/dist/bin/ithos.js +3 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +100 -0
- package/dist/cli.test.d.ts +2 -0
- package/dist/cli.test.d.ts.map +1 -0
- package/dist/cli.test.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/ithos.d.ts +37 -0
- package/dist/ithos.d.ts.map +1 -0
- package/dist/ithos.js +278 -0
- package/dist/ithos.test.d.ts +2 -0
- package/dist/ithos.test.d.ts.map +1 -0
- package/dist/ithos.test.js +131 -0
- package/package.json +32 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ithos.d.ts","sourceRoot":"","sources":["../../src/bin/ithos.ts"],"names":[],"mappings":""}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,SAAS,IAAI,OAAO,CAoGnC"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { doctorRepository, exportMemory, initRepository, isArtifactType, recordArtifact, searchMemory, validateRepository } from "ithos-core";
|
|
3
|
+
export function createCli() {
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name("ithos")
|
|
7
|
+
.description("Local-first engineering memory")
|
|
8
|
+
.version("0.1.0");
|
|
9
|
+
program
|
|
10
|
+
.command("init")
|
|
11
|
+
.description("Initialize Ithos memory in the current repository")
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const result = await initRepository();
|
|
14
|
+
printList("Created", result.created);
|
|
15
|
+
printList("Already existed", result.existing);
|
|
16
|
+
});
|
|
17
|
+
program
|
|
18
|
+
.command("validate")
|
|
19
|
+
.description("Validate the Ithos repository structure")
|
|
20
|
+
.action(async () => {
|
|
21
|
+
const result = await validateRepository();
|
|
22
|
+
if (result.valid) {
|
|
23
|
+
console.log("Ithos repository is valid.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
result.errors.forEach((error) => console.error(error));
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command("doctor")
|
|
31
|
+
.description("Check local Ithos setup and print guidance")
|
|
32
|
+
.action(async () => {
|
|
33
|
+
const result = await doctorRepository();
|
|
34
|
+
result.messages.forEach((message) => console.log(message));
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.command("record")
|
|
41
|
+
.description("Record one engineering memory artifact")
|
|
42
|
+
.requiredOption("-t, --type <type>", "artifact type")
|
|
43
|
+
.requiredOption("--title <title>", "artifact title")
|
|
44
|
+
.option("-b, --body <body>", "artifact body")
|
|
45
|
+
.action(async (options) => {
|
|
46
|
+
if (!isArtifactType(options.type)) {
|
|
47
|
+
console.error(`Unsupported artifact type: ${options.type}`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const body = options.body ?? (await readStdin());
|
|
52
|
+
if (body.trim().length === 0) {
|
|
53
|
+
console.error("Artifact body is required through --body or stdin.");
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const file = await recordArtifact({
|
|
58
|
+
type: options.type,
|
|
59
|
+
title: options.title,
|
|
60
|
+
body
|
|
61
|
+
});
|
|
62
|
+
console.log(`Recorded ${file}`);
|
|
63
|
+
});
|
|
64
|
+
program
|
|
65
|
+
.command("search")
|
|
66
|
+
.description("Search Ithos markdown memory")
|
|
67
|
+
.argument("<query>", "keyword to search for")
|
|
68
|
+
.action(async (query) => {
|
|
69
|
+
const results = await searchMemory(query);
|
|
70
|
+
if (results.length === 0) {
|
|
71
|
+
console.log("No matches found.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
results.forEach((result) => {
|
|
75
|
+
console.log(`${result.file}:${result.line}: ${result.text}`);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
program
|
|
79
|
+
.command("export")
|
|
80
|
+
.description("Export Ithos markdown memory to stdout")
|
|
81
|
+
.action(async () => {
|
|
82
|
+
const result = await exportMemory();
|
|
83
|
+
process.stdout.write(result.markdown);
|
|
84
|
+
});
|
|
85
|
+
return program;
|
|
86
|
+
}
|
|
87
|
+
function printList(label, values) {
|
|
88
|
+
if (values.length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(`${label}:`);
|
|
92
|
+
values.forEach((value) => console.log(`- ${value}`));
|
|
93
|
+
}
|
|
94
|
+
async function readStdin() {
|
|
95
|
+
const chunks = [];
|
|
96
|
+
for await (const chunk of process.stdin) {
|
|
97
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
98
|
+
}
|
|
99
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../src/cli.test.ts"],"names":[],"mappings":""}
|
package/dist/cli.test.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createCli } from "./cli.js";
|
|
4
|
+
test("createCli returns a configured commander program", () => {
|
|
5
|
+
const program = createCli();
|
|
6
|
+
assert.equal(program.name(), "ithos");
|
|
7
|
+
const commands = program.commands.map((cmd) => cmd.name());
|
|
8
|
+
assert.ok(commands.includes("init"));
|
|
9
|
+
assert.ok(commands.includes("validate"));
|
|
10
|
+
assert.ok(commands.includes("doctor"));
|
|
11
|
+
assert.ok(commands.includes("record"));
|
|
12
|
+
assert.ok(commands.includes("search"));
|
|
13
|
+
assert.ok(commands.includes("export"));
|
|
14
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createCli } from "./cli.js";
|
package/dist/ithos.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type ArtifactType = "architecture" | "decisions" | "defects" | "features" | "gaps" | "lessons" | "patterns" | "regressions" | "releases" | "sessions";
|
|
2
|
+
export type InitResult = {
|
|
3
|
+
created: string[];
|
|
4
|
+
existing: string[];
|
|
5
|
+
};
|
|
6
|
+
export type ValidationResult = {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
};
|
|
10
|
+
export type DoctorResult = {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
messages: string[];
|
|
13
|
+
};
|
|
14
|
+
export type RecordInput = {
|
|
15
|
+
type: ArtifactType;
|
|
16
|
+
title: string;
|
|
17
|
+
body: string;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
date?: Date;
|
|
20
|
+
};
|
|
21
|
+
export type SearchResult = {
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
text: string;
|
|
25
|
+
};
|
|
26
|
+
export type ExportResult = {
|
|
27
|
+
files: string[];
|
|
28
|
+
markdown: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function isArtifactType(value: string): value is ArtifactType;
|
|
31
|
+
export declare function initRepository(cwd?: string): Promise<InitResult>;
|
|
32
|
+
export declare function validateRepository(cwd?: string): Promise<ValidationResult>;
|
|
33
|
+
export declare function doctorRepository(cwd?: string): Promise<DoctorResult>;
|
|
34
|
+
export declare function recordArtifact(input: RecordInput): Promise<string>;
|
|
35
|
+
export declare function searchMemory(query: string, cwd?: string): Promise<SearchResult[]>;
|
|
36
|
+
export declare function exportMemory(cwd?: string): Promise<ExportResult>;
|
|
37
|
+
//# sourceMappingURL=ithos.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ithos.d.ts","sourceRoot":"","sources":["../src/ithos.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,WAAW,GACX,SAAS,GACT,UAAU,GACV,MAAM,GACN,SAAS,GACT,UAAU,GACV,aAAa,GACb,UAAU,GACV,UAAU,CAAC;AAEf,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAgCF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,YAAY,CAEnE;AAED,wBAAsB,cAAc,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAoB7E;AAED,wBAAsB,kBAAkB,CACtC,GAAG,SAAgB,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAa3B;AAED,wBAAsB,gBAAgB,CACpC,GAAG,SAAgB,GAClB,OAAO,CAAC,YAAY,CAAC,CAsBvB;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCxE;AAED,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,SAAgB,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CA0BzB;AAED,wBAAsB,YAAY,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmB7E"}
|
package/dist/ithos.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { readdir, readFile, stat, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const REQUIRED_FILES = [".ithos/README.md", ".ithos/project.md"];
|
|
4
|
+
const ARTIFACT_TYPES = [
|
|
5
|
+
"architecture",
|
|
6
|
+
"decisions",
|
|
7
|
+
"defects",
|
|
8
|
+
"features",
|
|
9
|
+
"gaps",
|
|
10
|
+
"lessons",
|
|
11
|
+
"patterns",
|
|
12
|
+
"regressions",
|
|
13
|
+
"releases",
|
|
14
|
+
"sessions"
|
|
15
|
+
];
|
|
16
|
+
const DIRECTORY_LAYOUT = [
|
|
17
|
+
".ithos",
|
|
18
|
+
".ithos/sessions",
|
|
19
|
+
".ithos/decisions",
|
|
20
|
+
".ithos/architecture",
|
|
21
|
+
".ithos/features",
|
|
22
|
+
".ithos/patterns",
|
|
23
|
+
".ithos/defects",
|
|
24
|
+
".ithos/regressions",
|
|
25
|
+
".ithos/gaps",
|
|
26
|
+
".ithos/lessons",
|
|
27
|
+
".ithos/releases",
|
|
28
|
+
".ithos/attachments"
|
|
29
|
+
];
|
|
30
|
+
export function isArtifactType(value) {
|
|
31
|
+
return ARTIFACT_TYPES.includes(value);
|
|
32
|
+
}
|
|
33
|
+
export async function initRepository(cwd = process.cwd()) {
|
|
34
|
+
const created = [];
|
|
35
|
+
const existing = [];
|
|
36
|
+
for (const directory of DIRECTORY_LAYOUT) {
|
|
37
|
+
const result = await ensureDirectory(path.join(cwd, directory));
|
|
38
|
+
pushResult(result, directory, created, existing);
|
|
39
|
+
}
|
|
40
|
+
const files = new Map([
|
|
41
|
+
[".ithos/README.md", ithosReadme()],
|
|
42
|
+
[".ithos/project.md", projectTemplate()]
|
|
43
|
+
]);
|
|
44
|
+
for (const [file, content] of files) {
|
|
45
|
+
const result = await ensureFile(path.join(cwd, file), content);
|
|
46
|
+
pushResult(result, file, created, existing);
|
|
47
|
+
}
|
|
48
|
+
return { created, existing };
|
|
49
|
+
}
|
|
50
|
+
export async function validateRepository(cwd = process.cwd()) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
for (const file of REQUIRED_FILES) {
|
|
53
|
+
if (!(await exists(path.join(cwd, file)))) {
|
|
54
|
+
errors.push(`Missing required file: ${file}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
valid: errors.length === 0,
|
|
59
|
+
errors
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function doctorRepository(cwd = process.cwd()) {
|
|
63
|
+
const messages = [];
|
|
64
|
+
const validation = await validateRepository(cwd);
|
|
65
|
+
if (validation.valid) {
|
|
66
|
+
messages.push("Ithos repository structure is valid.");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
messages.push("Ithos repository structure needs attention.");
|
|
70
|
+
messages.push(...validation.errors);
|
|
71
|
+
messages.push("Run `ithos init` to create the required files.");
|
|
72
|
+
}
|
|
73
|
+
if (await exists(path.join(cwd, ".git"))) {
|
|
74
|
+
messages.push("Git repository detected.");
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
messages.push("No .git directory detected. Ithos works best inside Git.");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
ok: validation.valid,
|
|
81
|
+
messages
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function recordArtifact(input) {
|
|
85
|
+
const cwd = input.cwd ?? process.cwd();
|
|
86
|
+
const now = input.date ?? new Date();
|
|
87
|
+
const created = formatDate(now);
|
|
88
|
+
const id = createArtifactId(input.type, input.title, now);
|
|
89
|
+
const relativeFile = path.join(".ithos", input.type, `${slugify(input.title)}.md`);
|
|
90
|
+
const absoluteFile = path.join(cwd, relativeFile);
|
|
91
|
+
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
92
|
+
const markdown = [
|
|
93
|
+
"---",
|
|
94
|
+
`id: ${id}`,
|
|
95
|
+
`type: ${input.type}`,
|
|
96
|
+
"status: draft",
|
|
97
|
+
`created: ${created}`,
|
|
98
|
+
`updated: ${created}`,
|
|
99
|
+
"tags: []",
|
|
100
|
+
"---",
|
|
101
|
+
"",
|
|
102
|
+
`# ${input.title}`,
|
|
103
|
+
"",
|
|
104
|
+
input.body.trim(),
|
|
105
|
+
""
|
|
106
|
+
].join("\n");
|
|
107
|
+
await writeFile(absoluteFile, markdown, { flag: "wx" });
|
|
108
|
+
return relativeFile;
|
|
109
|
+
}
|
|
110
|
+
export async function searchMemory(query, cwd = process.cwd()) {
|
|
111
|
+
const root = path.join(cwd, ".ithos");
|
|
112
|
+
if (!(await exists(root))) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const files = await listMarkdownFiles(root);
|
|
116
|
+
const normalizedQuery = query.toLowerCase();
|
|
117
|
+
const results = [];
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
const content = await readFile(file, "utf8");
|
|
120
|
+
const lines = searchableLines(content);
|
|
121
|
+
lines.forEach((line) => {
|
|
122
|
+
if (line.text.toLowerCase().includes(normalizedQuery)) {
|
|
123
|
+
results.push({
|
|
124
|
+
file: path.relative(cwd, file),
|
|
125
|
+
line: line.number,
|
|
126
|
+
text: line.text.trim()
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
export async function exportMemory(cwd = process.cwd()) {
|
|
134
|
+
const root = path.join(cwd, ".ithos");
|
|
135
|
+
if (!(await exists(root))) {
|
|
136
|
+
return { files: [], markdown: "" };
|
|
137
|
+
}
|
|
138
|
+
const files = await listMarkdownFiles(root);
|
|
139
|
+
const sections = [];
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const relativeFile = path.relative(cwd, file);
|
|
142
|
+
const content = await readFile(file, "utf8");
|
|
143
|
+
sections.push([`<!-- ${relativeFile} -->`, content.trim(), ""].join("\n"));
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
files: files.map((file) => path.relative(cwd, file)),
|
|
147
|
+
markdown: sections.join("\n")
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function ensureDirectory(directory) {
|
|
151
|
+
if (await exists(directory)) {
|
|
152
|
+
return "existing";
|
|
153
|
+
}
|
|
154
|
+
await mkdir(directory, { recursive: true });
|
|
155
|
+
return "created";
|
|
156
|
+
}
|
|
157
|
+
async function ensureFile(file, content) {
|
|
158
|
+
if (await exists(file)) {
|
|
159
|
+
return "existing";
|
|
160
|
+
}
|
|
161
|
+
await writeFile(file, content, { flag: "wx" });
|
|
162
|
+
return "created";
|
|
163
|
+
}
|
|
164
|
+
function pushResult(result, file, created, existing) {
|
|
165
|
+
if (result === "created") {
|
|
166
|
+
created.push(file);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
existing.push(file);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function exists(file) {
|
|
173
|
+
try {
|
|
174
|
+
await stat(file);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function listMarkdownFiles(directory) {
|
|
185
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
186
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
187
|
+
const absolutePath = path.join(directory, entry.name);
|
|
188
|
+
if (entry.isDirectory()) {
|
|
189
|
+
return listMarkdownFiles(absolutePath);
|
|
190
|
+
}
|
|
191
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
192
|
+
return [absolutePath];
|
|
193
|
+
}
|
|
194
|
+
return [];
|
|
195
|
+
}));
|
|
196
|
+
return files.flat().sort();
|
|
197
|
+
}
|
|
198
|
+
function isNodeError(error) {
|
|
199
|
+
return error instanceof Error && "code" in error;
|
|
200
|
+
}
|
|
201
|
+
function formatDate(date) {
|
|
202
|
+
const year = date.getFullYear();
|
|
203
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
204
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
205
|
+
return `${year}-${month}-${day}`;
|
|
206
|
+
}
|
|
207
|
+
function createArtifactId(type, title, date) {
|
|
208
|
+
const prefix = type.slice(0, 3).toUpperCase();
|
|
209
|
+
const timestamp = [
|
|
210
|
+
date.getFullYear(),
|
|
211
|
+
String(date.getMonth() + 1).padStart(2, "0"),
|
|
212
|
+
String(date.getDate()).padStart(2, "0"),
|
|
213
|
+
String(date.getHours()).padStart(2, "0"),
|
|
214
|
+
String(date.getMinutes()).padStart(2, "0"),
|
|
215
|
+
String(date.getSeconds()).padStart(2, "0")
|
|
216
|
+
].join("");
|
|
217
|
+
return `${prefix}-${timestamp}-${slugify(title)}`;
|
|
218
|
+
}
|
|
219
|
+
function searchableLines(content) {
|
|
220
|
+
const lines = content.split("\n");
|
|
221
|
+
if (!content.startsWith("---\n")) {
|
|
222
|
+
return lines.map((text, index) => ({ number: index + 1, text }));
|
|
223
|
+
}
|
|
224
|
+
const end = content.indexOf("\n---\n", 4);
|
|
225
|
+
if (end === -1) {
|
|
226
|
+
return lines.map((text, index) => ({ number: index + 1, text }));
|
|
227
|
+
}
|
|
228
|
+
const frontmatterLineCount = content
|
|
229
|
+
.slice(0, end + "\n---\n".length)
|
|
230
|
+
.split("\n").length;
|
|
231
|
+
return lines
|
|
232
|
+
.map((text, index) => ({ number: index + 1, text }))
|
|
233
|
+
.filter((line) => line.number > frontmatterLineCount);
|
|
234
|
+
}
|
|
235
|
+
function slugify(value) {
|
|
236
|
+
const slug = value
|
|
237
|
+
.trim()
|
|
238
|
+
.toLowerCase()
|
|
239
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
240
|
+
.replace(/^-+|-+$/g, "");
|
|
241
|
+
return slug || "untitled";
|
|
242
|
+
}
|
|
243
|
+
function ithosReadme() {
|
|
244
|
+
return `# Ithos
|
|
245
|
+
|
|
246
|
+
This directory preserves the engineering memory for this repository.
|
|
247
|
+
|
|
248
|
+
Ithos records why the software changed: decisions, patterns, lessons, defects,
|
|
249
|
+
regressions, gaps, sessions and releases.
|
|
250
|
+
|
|
251
|
+
Capture signal, not exhaust. Markdown is the source of truth.
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
function projectTemplate() {
|
|
255
|
+
return `# Project
|
|
256
|
+
|
|
257
|
+
## Summary
|
|
258
|
+
|
|
259
|
+
Describe what this project is and why it exists.
|
|
260
|
+
|
|
261
|
+
## Goals
|
|
262
|
+
|
|
263
|
+
- Preserve engineering memory locally.
|
|
264
|
+
|
|
265
|
+
## Technology Stack
|
|
266
|
+
|
|
267
|
+
- TypeScript
|
|
268
|
+
|
|
269
|
+
## Architecture
|
|
270
|
+
|
|
271
|
+
Describe the high-level architecture as it evolves.
|
|
272
|
+
|
|
273
|
+
## Conventions
|
|
274
|
+
|
|
275
|
+
- Keep markdown human-readable.
|
|
276
|
+
- Prefer concise, factual engineering memory.
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ithos.test.d.ts","sourceRoot":"","sources":["../src/ithos.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { exportMemory, initRepository, recordArtifact, searchMemory, validateRepository } from "./ithos.js";
|
|
7
|
+
test("initRepository creates the required Ithos files", async () => {
|
|
8
|
+
const cwd = await temporaryDirectory();
|
|
9
|
+
try {
|
|
10
|
+
const result = await initRepository(cwd);
|
|
11
|
+
assert.ok(result.created.includes(".ithos/README.md"));
|
|
12
|
+
assert.ok(result.created.includes(".ithos/project.md"));
|
|
13
|
+
const validation = await validateRepository(cwd);
|
|
14
|
+
assert.equal(validation.valid, true);
|
|
15
|
+
assert.deepEqual(validation.errors, []);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
await removeDirectory(cwd);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
test("validateRepository reports missing required files", async () => {
|
|
22
|
+
const cwd = await temporaryDirectory();
|
|
23
|
+
try {
|
|
24
|
+
const validation = await validateRepository(cwd);
|
|
25
|
+
assert.equal(validation.valid, false);
|
|
26
|
+
assert.deepEqual(validation.errors, [
|
|
27
|
+
"Missing required file: .ithos/README.md",
|
|
28
|
+
"Missing required file: .ithos/project.md"
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await removeDirectory(cwd);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
test("recordArtifact creates a markdown artifact without overwriting", async () => {
|
|
36
|
+
const cwd = await temporaryDirectory();
|
|
37
|
+
try {
|
|
38
|
+
await initRepository(cwd);
|
|
39
|
+
const file = await recordArtifact({
|
|
40
|
+
cwd,
|
|
41
|
+
type: "decisions",
|
|
42
|
+
title: "Use Markdown",
|
|
43
|
+
body: "Markdown stays readable in Git.",
|
|
44
|
+
date: new Date("2026-07-02T12:00:00.000Z")
|
|
45
|
+
});
|
|
46
|
+
assert.equal(file, ".ithos/decisions/use-markdown.md");
|
|
47
|
+
const content = await readFile(path.join(cwd, file), "utf8");
|
|
48
|
+
assert.match(content, /type: decisions/);
|
|
49
|
+
assert.match(content, /# Use Markdown/);
|
|
50
|
+
assert.match(content, /Markdown stays readable in Git\./);
|
|
51
|
+
await assert.rejects(() => recordArtifact({
|
|
52
|
+
cwd,
|
|
53
|
+
type: "decisions",
|
|
54
|
+
title: "Use Markdown",
|
|
55
|
+
body: "This should not overwrite the original.",
|
|
56
|
+
date: new Date("2026-07-02T12:01:00.000Z")
|
|
57
|
+
}), { code: "EEXIST" });
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await removeDirectory(cwd);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
test("recordArtifact uses local dates in frontmatter and ids", async () => {
|
|
64
|
+
const cwd = await temporaryDirectory();
|
|
65
|
+
try {
|
|
66
|
+
await initRepository(cwd);
|
|
67
|
+
const file = await recordArtifact({
|
|
68
|
+
cwd,
|
|
69
|
+
type: "sessions",
|
|
70
|
+
title: "Local Date",
|
|
71
|
+
body: "Dates should reflect the local developer calendar.",
|
|
72
|
+
date: new Date(2026, 6, 3, 0, 5, 0)
|
|
73
|
+
});
|
|
74
|
+
const content = await readFile(path.join(cwd, file), "utf8");
|
|
75
|
+
assert.match(content, /id: SES-20260703000500-local-date/);
|
|
76
|
+
assert.match(content, /created: 2026-07-03/);
|
|
77
|
+
assert.match(content, /updated: 2026-07-03/);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await removeDirectory(cwd);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
test("searchMemory returns matching markdown lines", async () => {
|
|
84
|
+
const cwd = await temporaryDirectory();
|
|
85
|
+
try {
|
|
86
|
+
await initRepository(cwd);
|
|
87
|
+
await recordArtifact({
|
|
88
|
+
cwd,
|
|
89
|
+
type: "lessons",
|
|
90
|
+
title: "Capture Signal",
|
|
91
|
+
body: "Capture signal, not exhaust.",
|
|
92
|
+
date: new Date("2026-07-02T12:00:00.000Z")
|
|
93
|
+
});
|
|
94
|
+
const results = await searchMemory("not exhaust", cwd);
|
|
95
|
+
assert.deepEqual(results, [
|
|
96
|
+
{
|
|
97
|
+
file: ".ithos/README.md",
|
|
98
|
+
line: 8,
|
|
99
|
+
text: "Capture signal, not exhaust. Markdown is the source of truth."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
file: ".ithos/lessons/capture-signal.md",
|
|
103
|
+
line: 12,
|
|
104
|
+
text: "Capture signal, not exhaust."
|
|
105
|
+
}
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await removeDirectory(cwd);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
test("exportMemory concatenates markdown files", async () => {
|
|
113
|
+
const cwd = await temporaryDirectory();
|
|
114
|
+
try {
|
|
115
|
+
await initRepository(cwd);
|
|
116
|
+
const result = await exportMemory(cwd);
|
|
117
|
+
assert.ok(result.files.includes(".ithos/README.md"));
|
|
118
|
+
assert.ok(result.files.includes(".ithos/project.md"));
|
|
119
|
+
assert.match(result.markdown, /<!-- \.ithos\/README\.md -->/);
|
|
120
|
+
assert.match(result.markdown, /<!-- \.ithos\/project\.md -->/);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await removeDirectory(cwd);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
async function temporaryDirectory() {
|
|
127
|
+
return mkdtemp(path.join(os.tmpdir(), "ithos-test-"));
|
|
128
|
+
}
|
|
129
|
+
async function removeDirectory(directory) {
|
|
130
|
+
await rm(directory, { recursive: true, force: true });
|
|
131
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ithos",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"ithos": "./dist/bin/ithos.js"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"test": "npm run build && node --test dist/*.test.js",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"ithos-core": "*",
|
|
30
|
+
"commander": "^14.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|