@hasna/microservices 0.0.9 → 0.0.11
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/bin/index.js +236 -36
- package/bin/mcp.js +153 -4
- package/dist/index.js +120 -3
- package/microservices/microservice-analytics/package.json +27 -0
- package/microservices/microservice-analytics/src/cli/index.ts +373 -0
- package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
- package/microservices/microservice-analytics/src/db/database.ts +93 -0
- package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
- package/microservices/microservice-analytics/src/index.ts +37 -0
- package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
- package/microservices/microservice-assets/package.json +27 -0
- package/microservices/microservice-assets/src/cli/index.ts +375 -0
- package/microservices/microservice-assets/src/db/assets.ts +370 -0
- package/microservices/microservice-assets/src/db/database.ts +93 -0
- package/microservices/microservice-assets/src/db/migrations.ts +51 -0
- package/microservices/microservice-assets/src/index.ts +32 -0
- package/microservices/microservice-assets/src/mcp/index.ts +346 -0
- package/microservices/microservice-compliance/package.json +27 -0
- package/microservices/microservice-compliance/src/cli/index.ts +467 -0
- package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
- package/microservices/microservice-compliance/src/db/database.ts +93 -0
- package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
- package/microservices/microservice-compliance/src/index.ts +46 -0
- package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
- package/microservices/microservice-habits/package.json +27 -0
- package/microservices/microservice-habits/src/cli/index.ts +315 -0
- package/microservices/microservice-habits/src/db/database.ts +93 -0
- package/microservices/microservice-habits/src/db/habits.ts +451 -0
- package/microservices/microservice-habits/src/db/migrations.ts +46 -0
- package/microservices/microservice-habits/src/index.ts +31 -0
- package/microservices/microservice-habits/src/mcp/index.ts +313 -0
- package/microservices/microservice-health/package.json +27 -0
- package/microservices/microservice-health/src/cli/index.ts +484 -0
- package/microservices/microservice-health/src/db/database.ts +93 -0
- package/microservices/microservice-health/src/db/health.ts +708 -0
- package/microservices/microservice-health/src/db/migrations.ts +70 -0
- package/microservices/microservice-health/src/index.ts +63 -0
- package/microservices/microservice-health/src/mcp/index.ts +437 -0
- package/microservices/microservice-leads/package.json +27 -0
- package/microservices/microservice-leads/src/cli/index.ts +596 -0
- package/microservices/microservice-leads/src/db/database.ts +93 -0
- package/microservices/microservice-leads/src/db/leads.ts +520 -0
- package/microservices/microservice-leads/src/db/lists.ts +151 -0
- package/microservices/microservice-leads/src/db/migrations.ts +93 -0
- package/microservices/microservice-leads/src/index.ts +65 -0
- package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
- package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
- package/microservices/microservice-leads/src/mcp/index.ts +533 -0
- package/microservices/microservice-notifications/package.json +27 -0
- package/microservices/microservice-notifications/src/cli/index.ts +349 -0
- package/microservices/microservice-notifications/src/db/database.ts +93 -0
- package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
- package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
- package/microservices/microservice-notifications/src/index.ts +41 -0
- package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
- package/microservices/microservice-products/package.json +27 -0
- package/microservices/microservice-products/src/cli/index.ts +416 -0
- package/microservices/microservice-products/src/db/categories.ts +154 -0
- package/microservices/microservice-products/src/db/database.ts +93 -0
- package/microservices/microservice-products/src/db/migrations.ts +58 -0
- package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
- package/microservices/microservice-products/src/db/products.ts +452 -0
- package/microservices/microservice-products/src/index.ts +53 -0
- package/microservices/microservice-products/src/mcp/index.ts +453 -0
- package/microservices/microservice-projects/package.json +27 -0
- package/microservices/microservice-projects/src/cli/index.ts +480 -0
- package/microservices/microservice-projects/src/db/database.ts +93 -0
- package/microservices/microservice-projects/src/db/migrations.ts +65 -0
- package/microservices/microservice-projects/src/db/projects.ts +715 -0
- package/microservices/microservice-projects/src/index.ts +57 -0
- package/microservices/microservice-projects/src/mcp/index.ts +501 -0
- package/microservices/microservice-proposals/package.json +27 -0
- package/microservices/microservice-proposals/src/cli/index.ts +400 -0
- package/microservices/microservice-proposals/src/db/database.ts +93 -0
- package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
- package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
- package/microservices/microservice-proposals/src/index.ts +37 -0
- package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
- package/microservices/microservice-reading/package.json +27 -0
- package/microservices/microservice-reading/src/cli/index.ts +464 -0
- package/microservices/microservice-reading/src/db/database.ts +93 -0
- package/microservices/microservice-reading/src/db/migrations.ts +59 -0
- package/microservices/microservice-reading/src/db/reading.ts +524 -0
- package/microservices/microservice-reading/src/index.ts +51 -0
- package/microservices/microservice-reading/src/mcp/index.ts +368 -0
- package/microservices/microservice-travel/package.json +27 -0
- package/microservices/microservice-travel/src/cli/index.ts +505 -0
- package/microservices/microservice-travel/src/db/database.ts +93 -0
- package/microservices/microservice-travel/src/db/migrations.ts +77 -0
- package/microservices/microservice-travel/src/db/travel.ts +802 -0
- package/microservices/microservice-travel/src/index.ts +60 -0
- package/microservices/microservice-travel/src/mcp/index.ts +495 -0
- package/microservices/microservice-wiki/package.json +27 -0
- package/microservices/microservice-wiki/src/cli/index.ts +345 -0
- package/microservices/microservice-wiki/src/db/database.ts +93 -0
- package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
- package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
- package/microservices/microservice-wiki/src/index.ts +32 -0
- package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
- package/package.json +1 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import {
|
|
5
|
+
createPage,
|
|
6
|
+
getPage,
|
|
7
|
+
getPageBySlug,
|
|
8
|
+
listPages,
|
|
9
|
+
updatePage,
|
|
10
|
+
deletePage,
|
|
11
|
+
searchPages,
|
|
12
|
+
getPageTree,
|
|
13
|
+
getRecentlyUpdated,
|
|
14
|
+
getByCategory,
|
|
15
|
+
getByTag,
|
|
16
|
+
getPageHistory,
|
|
17
|
+
revertToVersion,
|
|
18
|
+
type PageTreeNode,
|
|
19
|
+
} from "../db/wiki.js";
|
|
20
|
+
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name("microservice-wiki")
|
|
25
|
+
.description("Wiki microservice")
|
|
26
|
+
.version("0.0.1");
|
|
27
|
+
|
|
28
|
+
// --- Pages ---
|
|
29
|
+
|
|
30
|
+
const pageCmd = program
|
|
31
|
+
.command("page")
|
|
32
|
+
.description("Page management");
|
|
33
|
+
|
|
34
|
+
pageCmd
|
|
35
|
+
.command("create")
|
|
36
|
+
.description("Create a new page")
|
|
37
|
+
.requiredOption("--title <title>", "Page title")
|
|
38
|
+
.option("--slug <slug>", "URL slug (auto-generated from title if omitted)")
|
|
39
|
+
.option("--content <content>", "Page content")
|
|
40
|
+
.option("--format <format>", "Content format (markdown or html)", "markdown")
|
|
41
|
+
.option("--category <category>", "Category")
|
|
42
|
+
.option("--parent <id>", "Parent page ID")
|
|
43
|
+
.option("--author <author>", "Author name")
|
|
44
|
+
.option("--status <status>", "Status (draft, published, archived)", "published")
|
|
45
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
46
|
+
.option("--json", "Output as JSON", false)
|
|
47
|
+
.action((opts) => {
|
|
48
|
+
const page = createPage({
|
|
49
|
+
title: opts.title,
|
|
50
|
+
slug: opts.slug,
|
|
51
|
+
content: opts.content,
|
|
52
|
+
format: opts.format,
|
|
53
|
+
category: opts.category,
|
|
54
|
+
parent_id: opts.parent,
|
|
55
|
+
author: opts.author,
|
|
56
|
+
status: opts.status,
|
|
57
|
+
tags: opts.tags ? opts.tags.split(",").map((t: string) => t.trim()) : undefined,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (opts.json) {
|
|
61
|
+
console.log(JSON.stringify(page, null, 2));
|
|
62
|
+
} else {
|
|
63
|
+
console.log(`Created page: ${page.title} (${page.id})`);
|
|
64
|
+
console.log(` Slug: ${page.slug}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
pageCmd
|
|
69
|
+
.command("get")
|
|
70
|
+
.description("Get a page by ID or slug")
|
|
71
|
+
.argument("<id-or-slug>", "Page ID or slug")
|
|
72
|
+
.option("--json", "Output as JSON", false)
|
|
73
|
+
.action((idOrSlug, opts) => {
|
|
74
|
+
let page = getPage(idOrSlug);
|
|
75
|
+
if (!page) page = getPageBySlug(idOrSlug);
|
|
76
|
+
if (!page) {
|
|
77
|
+
console.error(`Page '${idOrSlug}' not found.`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(JSON.stringify(page, null, 2));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(`${page.title} (v${page.version})`);
|
|
85
|
+
console.log(` Slug: ${page.slug}`);
|
|
86
|
+
console.log(` Status: ${page.status}`);
|
|
87
|
+
console.log(` Format: ${page.format}`);
|
|
88
|
+
if (page.category) console.log(` Category: ${page.category}`);
|
|
89
|
+
if (page.author) console.log(` Author: ${page.author}`);
|
|
90
|
+
if (page.tags.length) console.log(` Tags: ${page.tags.join(", ")}`);
|
|
91
|
+
if (page.content) console.log(`\n${page.content}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pageCmd
|
|
96
|
+
.command("list")
|
|
97
|
+
.description("List pages")
|
|
98
|
+
.option("--search <query>", "Search by title or content")
|
|
99
|
+
.option("--category <category>", "Filter by category")
|
|
100
|
+
.option("--status <status>", "Filter by status")
|
|
101
|
+
.option("--tag <tag>", "Filter by tag")
|
|
102
|
+
.option("--limit <n>", "Limit results")
|
|
103
|
+
.option("--json", "Output as JSON", false)
|
|
104
|
+
.action((opts) => {
|
|
105
|
+
const pages = listPages({
|
|
106
|
+
search: opts.search,
|
|
107
|
+
category: opts.category,
|
|
108
|
+
status: opts.status,
|
|
109
|
+
tag: opts.tag,
|
|
110
|
+
limit: opts.limit ? parseInt(opts.limit) : undefined,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (opts.json) {
|
|
114
|
+
console.log(JSON.stringify(pages, null, 2));
|
|
115
|
+
} else {
|
|
116
|
+
if (pages.length === 0) {
|
|
117
|
+
console.log("No pages found.");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const p of pages) {
|
|
121
|
+
const status = p.status !== "published" ? ` [${p.status}]` : "";
|
|
122
|
+
const tags = p.tags.length ? ` (${p.tags.join(", ")})` : "";
|
|
123
|
+
console.log(` ${p.title}${status}${tags} — /${p.slug}`);
|
|
124
|
+
}
|
|
125
|
+
console.log(`\n${pages.length} page(s)`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
pageCmd
|
|
130
|
+
.command("update")
|
|
131
|
+
.description("Update a page")
|
|
132
|
+
.argument("<id>", "Page ID")
|
|
133
|
+
.option("--title <title>", "Title")
|
|
134
|
+
.option("--slug <slug>", "Slug")
|
|
135
|
+
.option("--content <content>", "Content")
|
|
136
|
+
.option("--format <format>", "Format")
|
|
137
|
+
.option("--category <category>", "Category")
|
|
138
|
+
.option("--parent <id>", "Parent page ID")
|
|
139
|
+
.option("--author <author>", "Author")
|
|
140
|
+
.option("--status <status>", "Status")
|
|
141
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
142
|
+
.option("--json", "Output as JSON", false)
|
|
143
|
+
.action((id, opts) => {
|
|
144
|
+
const input: Record<string, unknown> = {};
|
|
145
|
+
if (opts.title !== undefined) input.title = opts.title;
|
|
146
|
+
if (opts.slug !== undefined) input.slug = opts.slug;
|
|
147
|
+
if (opts.content !== undefined) input.content = opts.content;
|
|
148
|
+
if (opts.format !== undefined) input.format = opts.format;
|
|
149
|
+
if (opts.category !== undefined) input.category = opts.category;
|
|
150
|
+
if (opts.parent !== undefined) input.parent_id = opts.parent;
|
|
151
|
+
if (opts.author !== undefined) input.author = opts.author;
|
|
152
|
+
if (opts.status !== undefined) input.status = opts.status;
|
|
153
|
+
if (opts.tags !== undefined) input.tags = opts.tags.split(",").map((t: string) => t.trim());
|
|
154
|
+
|
|
155
|
+
const page = updatePage(id, input);
|
|
156
|
+
if (!page) {
|
|
157
|
+
console.error(`Page '${id}' not found.`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (opts.json) {
|
|
162
|
+
console.log(JSON.stringify(page, null, 2));
|
|
163
|
+
} else {
|
|
164
|
+
console.log(`Updated: ${page.title} (v${page.version})`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
pageCmd
|
|
169
|
+
.command("delete")
|
|
170
|
+
.description("Delete a page")
|
|
171
|
+
.argument("<id>", "Page ID")
|
|
172
|
+
.action((id) => {
|
|
173
|
+
const deleted = deletePage(id);
|
|
174
|
+
if (deleted) {
|
|
175
|
+
console.log(`Deleted page ${id}`);
|
|
176
|
+
} else {
|
|
177
|
+
console.error(`Page '${id}' not found.`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
pageCmd
|
|
183
|
+
.command("search")
|
|
184
|
+
.description("Search pages")
|
|
185
|
+
.argument("<query>", "Search term")
|
|
186
|
+
.option("--json", "Output as JSON", false)
|
|
187
|
+
.action((query, opts) => {
|
|
188
|
+
const results = searchPages(query);
|
|
189
|
+
|
|
190
|
+
if (opts.json) {
|
|
191
|
+
console.log(JSON.stringify(results, null, 2));
|
|
192
|
+
} else {
|
|
193
|
+
if (results.length === 0) {
|
|
194
|
+
console.log(`No pages matching "${query}".`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const p of results) {
|
|
198
|
+
console.log(` ${p.title} — /${p.slug}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
pageCmd
|
|
204
|
+
.command("tree")
|
|
205
|
+
.description("Show page tree (hierarchical)")
|
|
206
|
+
.option("--json", "Output as JSON", false)
|
|
207
|
+
.action((opts) => {
|
|
208
|
+
const tree = getPageTree();
|
|
209
|
+
|
|
210
|
+
if (opts.json) {
|
|
211
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
212
|
+
} else {
|
|
213
|
+
if (tree.length === 0) {
|
|
214
|
+
console.log("No pages found.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
function printTree(nodes: PageTreeNode[], indent: number = 0) {
|
|
218
|
+
for (const node of nodes) {
|
|
219
|
+
const prefix = " ".repeat(indent);
|
|
220
|
+
console.log(`${prefix}${node.title} — /${node.slug}`);
|
|
221
|
+
if (node.children.length > 0) {
|
|
222
|
+
printTree(node.children, indent + 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
printTree(tree);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
pageCmd
|
|
231
|
+
.command("history")
|
|
232
|
+
.description("Show version history for a page")
|
|
233
|
+
.argument("<id>", "Page ID")
|
|
234
|
+
.option("--json", "Output as JSON", false)
|
|
235
|
+
.action((id, opts) => {
|
|
236
|
+
const history = getPageHistory(id);
|
|
237
|
+
|
|
238
|
+
if (opts.json) {
|
|
239
|
+
console.log(JSON.stringify(history, null, 2));
|
|
240
|
+
} else {
|
|
241
|
+
if (history.length === 0) {
|
|
242
|
+
console.log("No version history.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const v of history) {
|
|
246
|
+
console.log(` v${v.version} — ${v.title || "(no title)"} by ${v.author || "unknown"} at ${v.changed_at}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
pageCmd
|
|
252
|
+
.command("revert")
|
|
253
|
+
.description("Revert a page to a previous version")
|
|
254
|
+
.argument("<id>", "Page ID")
|
|
255
|
+
.requiredOption("--version <n>", "Version number to revert to")
|
|
256
|
+
.option("--json", "Output as JSON", false)
|
|
257
|
+
.action((id, opts) => {
|
|
258
|
+
const page = revertToVersion(id, parseInt(opts.version));
|
|
259
|
+
if (!page) {
|
|
260
|
+
console.error(`Page '${id}' or version ${opts.version} not found.`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (opts.json) {
|
|
265
|
+
console.log(JSON.stringify(page, null, 2));
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`Reverted to v${opts.version}: ${page.title} (now v${page.version})`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
pageCmd
|
|
272
|
+
.command("recent")
|
|
273
|
+
.description("Show recently updated pages")
|
|
274
|
+
.option("--limit <n>", "Number of results", "10")
|
|
275
|
+
.option("--json", "Output as JSON", false)
|
|
276
|
+
.action((opts) => {
|
|
277
|
+
const pages = getRecentlyUpdated(parseInt(opts.limit));
|
|
278
|
+
|
|
279
|
+
if (opts.json) {
|
|
280
|
+
console.log(JSON.stringify(pages, null, 2));
|
|
281
|
+
} else {
|
|
282
|
+
if (pages.length === 0) {
|
|
283
|
+
console.log("No pages found.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
for (const p of pages) {
|
|
287
|
+
console.log(` ${p.title} — updated ${p.updated_at}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// --- Categories & Tags (top-level for convenience) ---
|
|
293
|
+
|
|
294
|
+
program
|
|
295
|
+
.command("categories")
|
|
296
|
+
.description("List all categories")
|
|
297
|
+
.option("--json", "Output as JSON", false)
|
|
298
|
+
.action((opts) => {
|
|
299
|
+
const pages = listPages();
|
|
300
|
+
const categories = [...new Set(pages.map((p) => p.category).filter(Boolean))] as string[];
|
|
301
|
+
categories.sort();
|
|
302
|
+
|
|
303
|
+
if (opts.json) {
|
|
304
|
+
console.log(JSON.stringify(categories));
|
|
305
|
+
} else {
|
|
306
|
+
if (categories.length === 0) {
|
|
307
|
+
console.log("No categories found.");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
for (const cat of categories) {
|
|
311
|
+
const count = pages.filter((p) => p.category === cat).length;
|
|
312
|
+
console.log(` ${cat} (${count})`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
program
|
|
318
|
+
.command("tags")
|
|
319
|
+
.description("List all tags")
|
|
320
|
+
.option("--json", "Output as JSON", false)
|
|
321
|
+
.action((opts) => {
|
|
322
|
+
const pages = listPages();
|
|
323
|
+
const tagCounts = new Map<string, number>();
|
|
324
|
+
for (const p of pages) {
|
|
325
|
+
for (const tag of p.tags) {
|
|
326
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const tags = [...tagCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
331
|
+
|
|
332
|
+
if (opts.json) {
|
|
333
|
+
console.log(JSON.stringify(Object.fromEntries(tags)));
|
|
334
|
+
} else {
|
|
335
|
+
if (tags.length === 0) {
|
|
336
|
+
console.log("No tags found.");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
for (const [tag, count] of tags) {
|
|
340
|
+
console.log(` ${tag} (${count})`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection for microservice-wiki
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { MIGRATIONS } from "./migrations.js";
|
|
9
|
+
|
|
10
|
+
let _db: Database | null = null;
|
|
11
|
+
|
|
12
|
+
function getDbPath(): string {
|
|
13
|
+
// Environment variable override
|
|
14
|
+
if (process.env["MICROSERVICES_DIR"]) {
|
|
15
|
+
return join(process.env["MICROSERVICES_DIR"], "microservice-wiki", "data.db");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for .microservices in current or parent directories
|
|
19
|
+
let dir = resolve(process.cwd());
|
|
20
|
+
while (true) {
|
|
21
|
+
const candidate = join(dir, ".microservices", "microservice-wiki", "data.db");
|
|
22
|
+
const msDir = join(dir, ".microservices");
|
|
23
|
+
if (existsSync(msDir)) return candidate;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Global fallback
|
|
30
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
31
|
+
return join(home, ".microservices", "microservice-wiki", "data.db");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDir(filePath: string): void {
|
|
35
|
+
const dir = dirname(resolve(filePath));
|
|
36
|
+
if (!existsSync(dir)) {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getDatabase(): Database {
|
|
42
|
+
if (_db) return _db;
|
|
43
|
+
|
|
44
|
+
const dbPath = getDbPath();
|
|
45
|
+
ensureDir(dbPath);
|
|
46
|
+
|
|
47
|
+
_db = new Database(dbPath);
|
|
48
|
+
_db.exec("PRAGMA journal_mode = WAL");
|
|
49
|
+
_db.exec("PRAGMA foreign_keys = ON");
|
|
50
|
+
|
|
51
|
+
// Create migrations table
|
|
52
|
+
_db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
54
|
+
id INTEGER PRIMARY KEY,
|
|
55
|
+
name TEXT NOT NULL,
|
|
56
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
// Apply pending migrations
|
|
61
|
+
const applied = _db
|
|
62
|
+
.query("SELECT id FROM _migrations ORDER BY id")
|
|
63
|
+
.all() as { id: number }[];
|
|
64
|
+
const appliedIds = new Set(applied.map((r) => r.id));
|
|
65
|
+
|
|
66
|
+
for (const migration of MIGRATIONS) {
|
|
67
|
+
if (appliedIds.has(migration.id)) continue;
|
|
68
|
+
|
|
69
|
+
_db.exec("BEGIN");
|
|
70
|
+
try {
|
|
71
|
+
_db.exec(migration.sql);
|
|
72
|
+
_db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(
|
|
73
|
+
migration.id,
|
|
74
|
+
migration.name
|
|
75
|
+
);
|
|
76
|
+
_db.exec("COMMIT");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
_db.exec("ROLLBACK");
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return _db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function closeDatabase(): void {
|
|
89
|
+
if (_db) {
|
|
90
|
+
_db.close();
|
|
91
|
+
_db = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface MigrationEntry {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
sql: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const MIGRATIONS: MigrationEntry[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
name: "initial_schema",
|
|
11
|
+
sql: `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS pages (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
title TEXT NOT NULL,
|
|
15
|
+
slug TEXT UNIQUE,
|
|
16
|
+
content TEXT,
|
|
17
|
+
format TEXT NOT NULL DEFAULT 'markdown' CHECK(format IN ('markdown', 'html')),
|
|
18
|
+
category TEXT,
|
|
19
|
+
parent_id TEXT,
|
|
20
|
+
author TEXT,
|
|
21
|
+
status TEXT NOT NULL DEFAULT 'published' CHECK(status IN ('draft', 'published', 'archived')),
|
|
22
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
23
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
24
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS page_versions (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
page_id TEXT NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
|
32
|
+
version INTEGER NOT NULL,
|
|
33
|
+
title TEXT,
|
|
34
|
+
content TEXT,
|
|
35
|
+
author TEXT,
|
|
36
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS page_links (
|
|
40
|
+
source_id TEXT NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
|
41
|
+
target_id TEXT NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
|
42
|
+
PRIMARY KEY (source_id, target_id)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_pages_category ON pages(category);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_pages_parent ON pages(parent_id);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_pages_status ON pages(status);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_pages_updated ON pages(updated_at);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_page_versions_page ON page_versions(page_id);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_page_links_source ON page_links(source_id);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_page_links_target ON page_links(target_id);
|
|
53
|
+
`,
|
|
54
|
+
},
|
|
55
|
+
];
|