openclew 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +299 -0
- package/bin/openclew.js +53 -0
- package/hooks/generate-index.py +157 -0
- package/lib/checkout.js +288 -0
- package/lib/config.js +34 -0
- package/lib/detect.js +74 -0
- package/lib/index-gen.js +40 -0
- package/lib/init.js +273 -0
- package/lib/inject.js +42 -0
- package/lib/new-doc.js +46 -0
- package/lib/new-log.js +48 -0
- package/lib/templates.js +368 -0
- package/package.json +27 -2
- package/templates/living.md +45 -0
- package/templates/log.md +36 -0
package/lib/checkout.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew checkout — end-of-session summary + log creation.
|
|
3
|
+
*
|
|
4
|
+
* 1. Collect git activity (today's commits, uncommitted changes)
|
|
5
|
+
* 2. Display summary table
|
|
6
|
+
* 3. Create a session log pre-filled with the activity
|
|
7
|
+
* 4. Regenerate the index
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { execSync } = require("child_process");
|
|
13
|
+
const { logContent, slugifyLog, today } = require("./templates");
|
|
14
|
+
const { readConfig } = require("./config");
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = process.cwd();
|
|
17
|
+
const DOC_DIR = path.join(PROJECT_ROOT, "doc");
|
|
18
|
+
const LOG_DIR = path.join(DOC_DIR, "log");
|
|
19
|
+
|
|
20
|
+
function run(cmd) {
|
|
21
|
+
try {
|
|
22
|
+
return execSync(cmd, { cwd: PROJECT_ROOT, encoding: "utf-8" }).trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function collectGitActivity() {
|
|
29
|
+
const date = today();
|
|
30
|
+
|
|
31
|
+
// Today's commits
|
|
32
|
+
const commitLog = run(
|
|
33
|
+
`git log --since="${date} 00:00" --format="%h %s" --no-merges`
|
|
34
|
+
);
|
|
35
|
+
const commits = commitLog
|
|
36
|
+
? commitLog.split("\n").filter((l) => l.trim())
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
// Uncommitted changes
|
|
40
|
+
const status = run("git status --porcelain");
|
|
41
|
+
const uncommitted = status ? status.split("\n").filter((l) => l.trim()) : [];
|
|
42
|
+
|
|
43
|
+
// Files changed today (committed)
|
|
44
|
+
const changedFiles = run(
|
|
45
|
+
`git diff --name-only HEAD~${Math.max(commits.length, 1)}..HEAD 2>/dev/null`
|
|
46
|
+
);
|
|
47
|
+
const files = changedFiles
|
|
48
|
+
? changedFiles.split("\n").filter((l) => l.trim())
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
// Today's logs already created
|
|
52
|
+
const existingLogs = fs.existsSync(LOG_DIR)
|
|
53
|
+
? fs.readdirSync(LOG_DIR).filter((f) => f.startsWith(date))
|
|
54
|
+
: [];
|
|
55
|
+
|
|
56
|
+
// Living docs
|
|
57
|
+
const livingDocs = fs.existsSync(DOC_DIR)
|
|
58
|
+
? fs.readdirSync(DOC_DIR).filter((f) => f.startsWith("_") && f !== "_INDEX.md" && f.endsWith(".md"))
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
return { date, commits, uncommitted, files, existingLogs, livingDocs };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractActions(commits) {
|
|
65
|
+
// Group commits by type (feat, fix, refactor, docs, etc.)
|
|
66
|
+
return commits.map((c) => {
|
|
67
|
+
const match = c.match(/^([a-f0-9]+)\s+(\w+)(?:\(([^)]*)\))?:\s*(.+)$/);
|
|
68
|
+
if (match) {
|
|
69
|
+
return {
|
|
70
|
+
hash: match[1],
|
|
71
|
+
type: match[2],
|
|
72
|
+
scope: match[3] || "",
|
|
73
|
+
desc: match[4],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Non-conventional commit
|
|
77
|
+
const parts = c.match(/^([a-f0-9]+)\s+(.+)$/);
|
|
78
|
+
return {
|
|
79
|
+
hash: parts ? parts[1] : "",
|
|
80
|
+
type: "other",
|
|
81
|
+
scope: "",
|
|
82
|
+
desc: parts ? parts[2] : c,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function typeLabel(type) {
|
|
88
|
+
const labels = {
|
|
89
|
+
feat: "Feature",
|
|
90
|
+
fix: "Fix",
|
|
91
|
+
refactor: "Refactor",
|
|
92
|
+
docs: "Doc",
|
|
93
|
+
test: "Test",
|
|
94
|
+
build: "Build",
|
|
95
|
+
chore: "Chore",
|
|
96
|
+
};
|
|
97
|
+
return labels[type] || type.charAt(0).toUpperCase() + type.slice(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function displaySummary(activity) {
|
|
101
|
+
const { date, commits, uncommitted, existingLogs, livingDocs } = activity;
|
|
102
|
+
const actions = extractActions(commits);
|
|
103
|
+
|
|
104
|
+
console.log(`\nopenclew checkout — ${date}\n`);
|
|
105
|
+
|
|
106
|
+
if (actions.length === 0 && uncommitted.length === 0) {
|
|
107
|
+
console.log(" Nothing to report — no commits or changes today.");
|
|
108
|
+
console.log("");
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Summary table
|
|
113
|
+
if (actions.length > 0) {
|
|
114
|
+
console.log(" Commits today:");
|
|
115
|
+
console.log(
|
|
116
|
+
" ┌─────┬──────────────────────────────────────────────┬─────┐"
|
|
117
|
+
);
|
|
118
|
+
console.log(
|
|
119
|
+
" │ Sta │ Action │ Com │"
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
" ├─────┼──────────────────────────────────────────────┼─────┤"
|
|
123
|
+
);
|
|
124
|
+
for (const a of actions) {
|
|
125
|
+
const label = `${typeLabel(a.type)} : ${a.desc}`;
|
|
126
|
+
const truncated = label.length > 44 ? label.slice(0, 43) + "…" : label;
|
|
127
|
+
const padded = truncated.padEnd(44);
|
|
128
|
+
console.log(` │ ✅ │ ${padded} │ 🟢 │`);
|
|
129
|
+
}
|
|
130
|
+
console.log(
|
|
131
|
+
" └─────┴──────────────────────────────────────────────┴─────┘"
|
|
132
|
+
);
|
|
133
|
+
console.log("");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (uncommitted.length > 0) {
|
|
137
|
+
console.log(` Uncommitted changes: ${uncommitted.length} file(s)`);
|
|
138
|
+
for (const line of uncommitted.slice(0, 10)) {
|
|
139
|
+
console.log(` ${line}`);
|
|
140
|
+
}
|
|
141
|
+
if (uncommitted.length > 10) {
|
|
142
|
+
console.log(` ... and ${uncommitted.length - 10} more`);
|
|
143
|
+
}
|
|
144
|
+
console.log("");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Documentation status
|
|
148
|
+
if (existingLogs.length > 0) {
|
|
149
|
+
console.log(` 📗 Today's logs: ${existingLogs.join(", ")}`);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(" 📕 No log created today");
|
|
152
|
+
}
|
|
153
|
+
console.log("");
|
|
154
|
+
|
|
155
|
+
// Living docs reminder
|
|
156
|
+
if (livingDocs.length > 0) {
|
|
157
|
+
console.log(" 📚 Living docs — check if any need updating:");
|
|
158
|
+
for (const doc of livingDocs) {
|
|
159
|
+
console.log(` ${doc}`);
|
|
160
|
+
}
|
|
161
|
+
console.log("");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return actions;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function generateSessionLog(activity, actions) {
|
|
168
|
+
const { date } = activity;
|
|
169
|
+
|
|
170
|
+
// Build a descriptive title from actions
|
|
171
|
+
let sessionTitle;
|
|
172
|
+
if (actions.length === 1) {
|
|
173
|
+
sessionTitle = actions[0].desc;
|
|
174
|
+
} else if (actions.length > 1) {
|
|
175
|
+
const types = [...new Set(actions.map((a) => typeLabel(a.type)))];
|
|
176
|
+
sessionTitle = types.join(" + ") + " session";
|
|
177
|
+
} else {
|
|
178
|
+
sessionTitle = "Work session";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build pre-filled log content
|
|
182
|
+
const keywords = [
|
|
183
|
+
...new Set(actions.map((a) => a.scope).filter(Boolean)),
|
|
184
|
+
];
|
|
185
|
+
const keywordsStr =
|
|
186
|
+
keywords.length > 0 ? `[${keywords.join(", ")}]` : "[]";
|
|
187
|
+
|
|
188
|
+
const commitList = actions
|
|
189
|
+
.map((a) => `- ${typeLabel(a.type)}: ${a.desc} (${a.hash})`)
|
|
190
|
+
.join("\n");
|
|
191
|
+
|
|
192
|
+
const content = `<!-- L1_START -->
|
|
193
|
+
# L1 - Metadata
|
|
194
|
+
date: ${date}
|
|
195
|
+
type: ${actions.length === 1 ? actions[0].type === "fix" ? "Bug" : "Feature" : "Feature"}
|
|
196
|
+
subject: ${sessionTitle}
|
|
197
|
+
short_story: ${actions.map((a) => a.desc).join(". ")}.
|
|
198
|
+
status: Done
|
|
199
|
+
category:
|
|
200
|
+
keywords: ${keywordsStr}
|
|
201
|
+
<!-- L1_END -->
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
<!-- L2_START -->
|
|
206
|
+
# L2 - Summary
|
|
207
|
+
|
|
208
|
+
## Objective
|
|
209
|
+
<!-- Why this work was undertaken -->
|
|
210
|
+
|
|
211
|
+
## What was done
|
|
212
|
+
${commitList}
|
|
213
|
+
|
|
214
|
+
## Result
|
|
215
|
+
<!-- Outcome — what works now that didn't before -->
|
|
216
|
+
<!-- L2_END -->
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
<!-- L3_START -->
|
|
221
|
+
# L3 - Details
|
|
222
|
+
|
|
223
|
+
<!-- Technical details, code changes, debugging steps... -->
|
|
224
|
+
<!-- L3_END -->
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
const slug = slugifyLog(sessionTitle);
|
|
228
|
+
const filename = `${date}_${slug}.md`;
|
|
229
|
+
const filepath = path.join(LOG_DIR, filename);
|
|
230
|
+
|
|
231
|
+
if (fs.existsSync(filepath)) {
|
|
232
|
+
console.log(` Log already exists: doc/log/${filename}`);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
237
|
+
console.log(" No doc/log/ directory. Run 'openclew init' first.");
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fs.writeFileSync(filepath, content, "utf-8");
|
|
242
|
+
console.log(` 📝 Created doc/log/${filename}`);
|
|
243
|
+
console.log(" Pre-filled with today's commits. Edit to add context.");
|
|
244
|
+
return filename;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function regenerateIndex() {
|
|
248
|
+
const indexScript = path.join(DOC_DIR, "generate-index.py");
|
|
249
|
+
if (!fs.existsSync(indexScript)) return;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
|
|
253
|
+
console.log(" 📋 Regenerated doc/_INDEX.md");
|
|
254
|
+
} catch {
|
|
255
|
+
// Silent — index will be regenerated on next commit anyway
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function main() {
|
|
260
|
+
if (!fs.existsSync(DOC_DIR)) {
|
|
261
|
+
console.error("No doc/ directory found. Run 'openclew init' first.");
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!readConfig(PROJECT_ROOT)) {
|
|
266
|
+
console.warn("Warning: no .openclew.json found. Run 'openclew init' first.\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const activity = collectGitActivity();
|
|
270
|
+
const actions = displaySummary(activity);
|
|
271
|
+
|
|
272
|
+
if (!actions || actions.length === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Create session log
|
|
277
|
+
console.log("─── Log ───");
|
|
278
|
+
const created = generateSessionLog(activity, actions);
|
|
279
|
+
|
|
280
|
+
// Regenerate index
|
|
281
|
+
if (created) {
|
|
282
|
+
regenerateIndex();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log("");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
main();
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read/write .openclew.json config at project root.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE = ".openclew.json";
|
|
9
|
+
|
|
10
|
+
function configPath(projectRoot) {
|
|
11
|
+
return path.join(projectRoot || process.cwd(), CONFIG_FILE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readConfig(projectRoot) {
|
|
15
|
+
const p = configPath(projectRoot);
|
|
16
|
+
if (!fs.existsSync(p)) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeConfig(config, projectRoot) {
|
|
25
|
+
const p = configPath(projectRoot);
|
|
26
|
+
fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getEntryPoint(projectRoot) {
|
|
30
|
+
const config = readConfig(projectRoot);
|
|
31
|
+
return config && config.entryPoint ? config.entryPoint : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { readConfig, writeConfig, getEntryPoint, CONFIG_FILE };
|
package/lib/detect.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect existing AI instruction files in the project root.
|
|
3
|
+
* Returns an array of { tool, file, fullPath, isDir } objects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const INSTRUCTION_FILES = [
|
|
10
|
+
{ tool: "Claude Code", file: "CLAUDE.md" },
|
|
11
|
+
{ tool: "Cursor", file: ".cursorrules" },
|
|
12
|
+
{ tool: "Cursor", file: ".cursor/rules" },
|
|
13
|
+
{ tool: "GitHub Copilot", file: ".github/copilot-instructions.md" },
|
|
14
|
+
{ tool: "Windsurf", file: ".windsurfrules" },
|
|
15
|
+
{ tool: "Windsurf", file: ".windsurf/rules" },
|
|
16
|
+
{ tool: "Cline", file: ".clinerules" },
|
|
17
|
+
{ tool: "Codex / Gemini", file: "AGENTS.md" },
|
|
18
|
+
{ tool: "Antigravity", file: ".antigravity/rules.md" },
|
|
19
|
+
{ tool: "Gemini CLI", file: ".gemini/GEMINI.md" },
|
|
20
|
+
{ tool: "Aider", file: "CONVENTIONS.md" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find AGENTS.md case-insensitively in projectRoot.
|
|
25
|
+
* Returns the actual filename (e.g. "agents.md", "Agents.md") or null.
|
|
26
|
+
*/
|
|
27
|
+
function findAgentsMdCaseInsensitive(projectRoot) {
|
|
28
|
+
try {
|
|
29
|
+
const entries = fs.readdirSync(projectRoot);
|
|
30
|
+
const match = entries.find(
|
|
31
|
+
(e) => e.toLowerCase() === "agents.md" && fs.statSync(path.join(projectRoot, e)).isFile()
|
|
32
|
+
);
|
|
33
|
+
return match || null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function detectInstructionFiles(projectRoot) {
|
|
40
|
+
const found = [];
|
|
41
|
+
const seenLower = new Set();
|
|
42
|
+
|
|
43
|
+
for (const entry of INSTRUCTION_FILES) {
|
|
44
|
+
// Skip AGENTS.md in the static list — handled by case-insensitive scan
|
|
45
|
+
if (entry.file.toLowerCase() === "agents.md") continue;
|
|
46
|
+
|
|
47
|
+
const fullPath = path.join(projectRoot, entry.file);
|
|
48
|
+
if (fs.existsSync(fullPath)) {
|
|
49
|
+
const stat = fs.statSync(fullPath);
|
|
50
|
+
found.push({
|
|
51
|
+
tool: entry.tool,
|
|
52
|
+
file: entry.file,
|
|
53
|
+
fullPath,
|
|
54
|
+
isDir: stat.isDirectory(),
|
|
55
|
+
});
|
|
56
|
+
seenLower.add(entry.file.toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Case-insensitive AGENTS.md detection
|
|
61
|
+
const agentsFile = findAgentsMdCaseInsensitive(projectRoot);
|
|
62
|
+
if (agentsFile && !seenLower.has(agentsFile.toLowerCase())) {
|
|
63
|
+
found.push({
|
|
64
|
+
tool: "Codex / Gemini",
|
|
65
|
+
file: agentsFile,
|
|
66
|
+
fullPath: path.join(projectRoot, agentsFile),
|
|
67
|
+
isDir: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { detectInstructionFiles, findAgentsMdCaseInsensitive, INSTRUCTION_FILES };
|
package/lib/index-gen.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew index — regenerate doc/_INDEX.md
|
|
3
|
+
*
|
|
4
|
+
* Wraps hooks/generate-index.py. Falls back to a JS implementation
|
|
5
|
+
* if Python is not available.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
|
|
12
|
+
const docDir = path.join(process.cwd(), "doc");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(docDir)) {
|
|
15
|
+
console.error("No doc/ directory found. Run 'openclew init' first.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Try local generate-index.py first
|
|
20
|
+
const localScript = path.join(docDir, "generate-index.py");
|
|
21
|
+
const packageScript = path.join(__dirname, "..", "hooks", "generate-index.py");
|
|
22
|
+
const script = fs.existsSync(localScript) ? localScript : packageScript;
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(script)) {
|
|
25
|
+
try {
|
|
26
|
+
const output = execSync(`python3 "${script}" "${docDir}"`, {
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
});
|
|
29
|
+
console.log(output.trim());
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} catch {
|
|
32
|
+
console.error(
|
|
33
|
+
"python3 not available. Install Python 3.8+ or regenerate manually."
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.error("generate-index.py not found. Run 'openclew init' first.");
|
|
40
|
+
process.exit(1);
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew init — set up openclew in the current project.
|
|
3
|
+
*
|
|
4
|
+
* 1. Create doc/ and doc/log/
|
|
5
|
+
* 2. Detect entry point (AGENTS.md case-insensitive by default)
|
|
6
|
+
* 3. Inject openclew block into entry point
|
|
7
|
+
* 4. Install pre-commit hook for index generation
|
|
8
|
+
* 5. Create guide + example docs
|
|
9
|
+
* 6. Generate initial _INDEX.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const readline = require("readline");
|
|
15
|
+
const { detectInstructionFiles, findAgentsMdCaseInsensitive } = require("./detect");
|
|
16
|
+
const { inject, isAlreadyInjected } = require("./inject");
|
|
17
|
+
const { writeConfig } = require("./config");
|
|
18
|
+
const { guideContent, exampleLivingDocContent, exampleLogContent, today } = require("./templates");
|
|
19
|
+
|
|
20
|
+
const PROJECT_ROOT = process.cwd();
|
|
21
|
+
const DOC_DIR = path.join(PROJECT_ROOT, "doc");
|
|
22
|
+
const LOG_DIR = path.join(DOC_DIR, "log");
|
|
23
|
+
const GIT_DIR = path.join(PROJECT_ROOT, ".git");
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const noHook = args.includes("--no-hook");
|
|
27
|
+
const noInject = args.includes("--no-inject");
|
|
28
|
+
|
|
29
|
+
function ask(question) {
|
|
30
|
+
const rl = readline.createInterface({
|
|
31
|
+
input: process.stdin,
|
|
32
|
+
output: process.stdout,
|
|
33
|
+
});
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
rl.question(question, (answer) => {
|
|
36
|
+
rl.close();
|
|
37
|
+
resolve(answer.trim());
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createDirs() {
|
|
43
|
+
if (!fs.existsSync(DOC_DIR)) {
|
|
44
|
+
fs.mkdirSync(DOC_DIR, { recursive: true });
|
|
45
|
+
console.log(" Created doc/");
|
|
46
|
+
} else {
|
|
47
|
+
console.log(" doc/ already exists");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
51
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
52
|
+
console.log(" Created doc/log/");
|
|
53
|
+
} else {
|
|
54
|
+
console.log(" doc/log/ already exists");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the entry point file.
|
|
60
|
+
*
|
|
61
|
+
* Priority:
|
|
62
|
+
* 1. AGENTS.md (case-insensitive) — default, universal
|
|
63
|
+
* 2. Other detected instruction files — user picks one
|
|
64
|
+
* 3. Create AGENTS.md — if nothing exists
|
|
65
|
+
*/
|
|
66
|
+
async function resolveEntryPoint() {
|
|
67
|
+
if (noInject) {
|
|
68
|
+
console.log(" Skipping entry point setup (--no-inject)");
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 1. Check for AGENTS.md (case-insensitive)
|
|
73
|
+
const agentsFile = findAgentsMdCaseInsensitive(PROJECT_ROOT);
|
|
74
|
+
if (agentsFile) {
|
|
75
|
+
if (!process.stdin.isTTY) {
|
|
76
|
+
// Non-interactive: accept AGENTS.md by default
|
|
77
|
+
console.log(` Using ${agentsFile} (non-interactive)`);
|
|
78
|
+
return { file: agentsFile, fullPath: path.join(PROJECT_ROOT, agentsFile), created: false };
|
|
79
|
+
}
|
|
80
|
+
const answer = await ask(` Found ${agentsFile} — use as entry point? [Y/n] `);
|
|
81
|
+
if (answer === "" || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
82
|
+
return { file: agentsFile, fullPath: path.join(PROJECT_ROOT, agentsFile), created: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Detect other instruction files
|
|
87
|
+
const others = detectInstructionFiles(PROJECT_ROOT).filter(
|
|
88
|
+
(f) => !f.isDir && f.file.toLowerCase() !== "agents.md"
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (others.length > 0) {
|
|
92
|
+
console.log(" Detected instruction files:");
|
|
93
|
+
others.forEach((f, i) => console.log(` ${i + 1}. ${f.file} (${f.tool})`));
|
|
94
|
+
console.log(` ${others.length + 1}. Create new AGENTS.md`);
|
|
95
|
+
|
|
96
|
+
if (!process.stdin.isTTY) {
|
|
97
|
+
// Non-interactive: default to first detected file
|
|
98
|
+
console.log(` Using ${others[0].file} (non-interactive)`);
|
|
99
|
+
return { file: others[0].file, fullPath: others[0].fullPath, created: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const choice = await ask(` Choose entry point [1-${others.length + 1}]: `);
|
|
103
|
+
const idx = parseInt(choice, 10) - 1;
|
|
104
|
+
|
|
105
|
+
if (idx >= 0 && idx < others.length) {
|
|
106
|
+
return { file: others[idx].file, fullPath: others[idx].fullPath, created: false };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3. Create AGENTS.md
|
|
111
|
+
const agentsPath = path.join(PROJECT_ROOT, "AGENTS.md");
|
|
112
|
+
fs.writeFileSync(agentsPath, `# ${path.basename(PROJECT_ROOT)}\n\nProject instructions for AI agents.\n`, "utf-8");
|
|
113
|
+
console.log(" Created AGENTS.md");
|
|
114
|
+
return { file: "AGENTS.md", fullPath: agentsPath, created: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function installPreCommitHook() {
|
|
118
|
+
if (!fs.existsSync(GIT_DIR)) {
|
|
119
|
+
console.log(" No .git/ found — skipping hook installation");
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hooksDir = path.join(GIT_DIR, "hooks");
|
|
124
|
+
if (!fs.existsSync(hooksDir)) {
|
|
125
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const preCommitPath = path.join(hooksDir, "pre-commit");
|
|
129
|
+
const indexScript = `if [ -f doc/generate-index.py ]; then
|
|
130
|
+
python3 doc/generate-index.py doc 2>/dev/null || echo "openclew: index generation failed"
|
|
131
|
+
git add doc/_INDEX.md 2>/dev/null
|
|
132
|
+
fi`;
|
|
133
|
+
|
|
134
|
+
const MARKER = "# openclew-index";
|
|
135
|
+
|
|
136
|
+
if (fs.existsSync(preCommitPath)) {
|
|
137
|
+
const existing = fs.readFileSync(preCommitPath, "utf-8");
|
|
138
|
+
if (existing.includes(MARKER)) {
|
|
139
|
+
console.log(" Pre-commit hook already contains openclew index generation");
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
fs.appendFileSync(preCommitPath, `\n\n${MARKER}\n${indexScript}\n`, "utf-8");
|
|
143
|
+
console.log(" Appended openclew index generation to existing pre-commit hook");
|
|
144
|
+
} else {
|
|
145
|
+
fs.writeFileSync(preCommitPath, `#!/bin/sh\n\n${MARKER}\n${indexScript}\n`, "utf-8");
|
|
146
|
+
fs.chmodSync(preCommitPath, "755");
|
|
147
|
+
console.log(" Created pre-commit hook for index generation");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function copyGenerateIndex() {
|
|
154
|
+
const src = path.join(__dirname, "..", "hooks", "generate-index.py");
|
|
155
|
+
const dst = path.join(DOC_DIR, "generate-index.py");
|
|
156
|
+
|
|
157
|
+
if (fs.existsSync(dst)) {
|
|
158
|
+
console.log(" doc/generate-index.py already exists");
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (fs.existsSync(src)) {
|
|
163
|
+
fs.copyFileSync(src, dst);
|
|
164
|
+
console.log(" Copied generate-index.py to doc/");
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(" generate-index.py not found in package — skipping");
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createDocs() {
|
|
173
|
+
// Guide — always created
|
|
174
|
+
const guidePath = path.join(DOC_DIR, "_USING_OPENCLEW.md");
|
|
175
|
+
if (!fs.existsSync(guidePath)) {
|
|
176
|
+
fs.writeFileSync(guidePath, guideContent(), "utf-8");
|
|
177
|
+
console.log(" Created doc/_USING_OPENCLEW.md (guide)");
|
|
178
|
+
} else {
|
|
179
|
+
console.log(" doc/_USING_OPENCLEW.md already exists");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Example living doc
|
|
183
|
+
const examplePath = path.join(DOC_DIR, "_ARCHITECTURE.md");
|
|
184
|
+
if (!fs.existsSync(examplePath)) {
|
|
185
|
+
fs.writeFileSync(examplePath, exampleLivingDocContent(), "utf-8");
|
|
186
|
+
console.log(" Created doc/_ARCHITECTURE.md (example living doc)");
|
|
187
|
+
} else {
|
|
188
|
+
console.log(" doc/_ARCHITECTURE.md already exists");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Example log
|
|
192
|
+
const logPath = path.join(LOG_DIR, `${today()}_setup-openclew.md`);
|
|
193
|
+
if (!fs.existsSync(logPath)) {
|
|
194
|
+
fs.writeFileSync(logPath, exampleLogContent(), "utf-8");
|
|
195
|
+
console.log(` Created doc/log/${today()}_setup-openclew.md (example log)`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` doc/log/${today()}_setup-openclew.md already exists`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runIndexGenerator() {
|
|
202
|
+
const indexScript = path.join(DOC_DIR, "generate-index.py");
|
|
203
|
+
if (!fs.existsSync(indexScript)) return;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const { execSync } = require("child_process");
|
|
207
|
+
execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
|
|
208
|
+
console.log(" Generated doc/_INDEX.md");
|
|
209
|
+
} catch {
|
|
210
|
+
console.log(" Could not generate index (python3 not available)");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function main() {
|
|
215
|
+
console.log("\nopenclew init\n");
|
|
216
|
+
|
|
217
|
+
// Step 1: Create directories
|
|
218
|
+
console.log("1. Project structure");
|
|
219
|
+
createDirs();
|
|
220
|
+
|
|
221
|
+
// Step 2: Copy index generator
|
|
222
|
+
console.log("\n2. Index generator");
|
|
223
|
+
copyGenerateIndex();
|
|
224
|
+
|
|
225
|
+
// Step 3: Entry point
|
|
226
|
+
console.log("\n3. Entry point");
|
|
227
|
+
const entryPoint = await resolveEntryPoint();
|
|
228
|
+
|
|
229
|
+
if (entryPoint) {
|
|
230
|
+
if (isAlreadyInjected(entryPoint.fullPath)) {
|
|
231
|
+
console.log(` ${entryPoint.file} already has openclew block`);
|
|
232
|
+
} else {
|
|
233
|
+
inject(entryPoint.fullPath);
|
|
234
|
+
console.log(` Injected openclew block into ${entryPoint.file}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
writeConfig({ entryPoint: entryPoint.file }, PROJECT_ROOT);
|
|
238
|
+
console.log(` Saved entry point → .openclew.json`);
|
|
239
|
+
} else {
|
|
240
|
+
// --no-inject: still create config to mark init was done
|
|
241
|
+
writeConfig({ entryPoint: null }, PROJECT_ROOT);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Step 4: Pre-commit hook
|
|
245
|
+
console.log("\n4. Pre-commit hook");
|
|
246
|
+
if (noHook) {
|
|
247
|
+
console.log(" Skipping (--no-hook)");
|
|
248
|
+
} else {
|
|
249
|
+
installPreCommitHook();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Step 5: Docs
|
|
253
|
+
console.log("\n5. Docs");
|
|
254
|
+
createDocs();
|
|
255
|
+
|
|
256
|
+
// Step 6: Generate index
|
|
257
|
+
console.log("\n6. Index");
|
|
258
|
+
runIndexGenerator();
|
|
259
|
+
|
|
260
|
+
// Done
|
|
261
|
+
console.log("\n─── Ready ───\n");
|
|
262
|
+
if (entryPoint) {
|
|
263
|
+
console.log(` Entry point: ${entryPoint.file}`);
|
|
264
|
+
}
|
|
265
|
+
console.log(" Guide: doc/_USING_OPENCLEW.md");
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log(" Start a session with your agent now.");
|
|
268
|
+
console.log(' Ask it: "Read doc/_USING_OPENCLEW.md and document our architecture."');
|
|
269
|
+
console.log(" That's it — openclew works from here.");
|
|
270
|
+
console.log("");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
main();
|