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.
@@ -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 };
@@ -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();