@claudelaw/taichu 0.6.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.
Files changed (93) hide show
  1. package/.dockerignore +13 -0
  2. package/Dockerfile +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +208 -0
  5. package/docker-compose.yml +42 -0
  6. package/docs/ROADMAP.md +101 -0
  7. package/docs/api/README.md +102 -0
  8. package/docs/architecture/001-zero-dependency-core.md +61 -0
  9. package/docs/architecture/002-structured-content-model.md +70 -0
  10. package/docs/architecture/003-hook-based-extension.md +82 -0
  11. package/docs/architecture/004-api-first-architecture.md +122 -0
  12. package/docs/architecture/README.md +24 -0
  13. package/docs/logo.svg +40 -0
  14. package/docs/research/ai-era-cms-user-research.md +247 -0
  15. package/docs/zh/README.md +81 -0
  16. package/docs/zh/guides/deploy.md +75 -0
  17. package/docs/zh/guides/mcp.md +84 -0
  18. package/docs/zh/guides/promotion.md +51 -0
  19. package/marketplace.json +78 -0
  20. package/package.json +60 -0
  21. package/packages/core/src/auth.js +158 -0
  22. package/packages/core/src/content-type.js +244 -0
  23. package/packages/core/src/core.test.js +406 -0
  24. package/packages/core/src/errors.js +60 -0
  25. package/packages/core/src/hooks.js +104 -0
  26. package/packages/core/src/index.js +16 -0
  27. package/packages/core/src/server.test.js +149 -0
  28. package/packages/core/src/sm-crypto.js +31 -0
  29. package/packages/core/src/sqlite-store.js +354 -0
  30. package/packages/core/src/store.js +174 -0
  31. package/packages/core/src/tokenizer.js +89 -0
  32. package/packages/core/src/vector-index.js +131 -0
  33. package/packages/llm-providers/src/index.js +181 -0
  34. package/packages/mcp/src/index.js +355 -0
  35. package/packages/server/public/admin/assets/index-DApxOVTx.js +191 -0
  36. package/packages/server/public/admin/assets/index-DtMvdQm9.css +1 -0
  37. package/packages/server/public/admin/index.html +28 -0
  38. package/packages/server/public/aurora/style.css +1173 -0
  39. package/packages/server/public/favicon.svg +46 -0
  40. package/packages/server/public/theme/index.html +288 -0
  41. package/packages/server/public/theme/style.css +133 -0
  42. package/packages/server/public/theme-minimal/index.html +223 -0
  43. package/packages/server/public/theme-minimal/style.css +109 -0
  44. package/packages/server/public/ws-test.html +106 -0
  45. package/packages/server/src/activitypub.js +228 -0
  46. package/packages/server/src/audit.js +104 -0
  47. package/packages/server/src/auth-provider.js +76 -0
  48. package/packages/server/src/body-parser.js +52 -0
  49. package/packages/server/src/bootstrap.js +272 -0
  50. package/packages/server/src/collab.js +154 -0
  51. package/packages/server/src/config.js +136 -0
  52. package/packages/server/src/context.js +86 -0
  53. package/packages/server/src/email.js +317 -0
  54. package/packages/server/src/index.js +195 -0
  55. package/packages/server/src/logger.js +78 -0
  56. package/packages/server/src/media-store.js +213 -0
  57. package/packages/server/src/middleware/auth.js +203 -0
  58. package/packages/server/src/middleware/cors.js +15 -0
  59. package/packages/server/src/middleware/error-handler.js +49 -0
  60. package/packages/server/src/middleware/rate-limit.js +118 -0
  61. package/packages/server/src/multipart.js +150 -0
  62. package/packages/server/src/notify.js +126 -0
  63. package/packages/server/src/pipeline.js +206 -0
  64. package/packages/server/src/plugin-installer.js +139 -0
  65. package/packages/server/src/plugin-manager.js +165 -0
  66. package/packages/server/src/relationships.js +217 -0
  67. package/packages/server/src/revisions.js +114 -0
  68. package/packages/server/src/router.js +194 -0
  69. package/packages/server/src/routes/activitypub.js +140 -0
  70. package/packages/server/src/routes/api.js +363 -0
  71. package/packages/server/src/routes/audit.js +222 -0
  72. package/packages/server/src/routes/auth.js +205 -0
  73. package/packages/server/src/routes/collab.js +90 -0
  74. package/packages/server/src/routes/export.js +77 -0
  75. package/packages/server/src/routes/graphql.js +344 -0
  76. package/packages/server/src/routes/media.js +169 -0
  77. package/packages/server/src/routes/plugin-marketplace.js +171 -0
  78. package/packages/server/src/routes/relationships.js +133 -0
  79. package/packages/server/src/routes/rss.js +92 -0
  80. package/packages/server/src/routes/sso.js +211 -0
  81. package/packages/server/src/routes/theme.js +119 -0
  82. package/packages/server/src/routes/webhook.js +94 -0
  83. package/packages/server/src/routes/wechat.js +115 -0
  84. package/packages/server/src/routes/workflow.js +157 -0
  85. package/packages/server/src/scheduler.js +96 -0
  86. package/packages/server/src/search.js +100 -0
  87. package/packages/server/src/server.test.js +295 -0
  88. package/packages/server/src/sso-analytics.js +78 -0
  89. package/packages/server/src/static.js +70 -0
  90. package/packages/server/src/theme-engine.js +119 -0
  91. package/packages/server/src/webhook.js +192 -0
  92. package/packages/server/src/websocket.js +308 -0
  93. package/scripts/cli.js +90 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Webhook System — 内容变更事件推送
3
+ *
4
+ * 注册外部 URL,当内容变更时 POST 事件数据。
5
+ *
6
+ * 功能:
7
+ * - Webhook 注册/列表/删除 API
8
+ * - 事件过滤(按内容类型、操作类型)
9
+ * - HMAC-SHA256 签名验证
10
+ * - 指数退避重试(最多 3 次)
11
+ * - 投递日志
12
+ */
13
+
14
+ import { createHmac, randomBytes } from 'node:crypto';
15
+ import { createLogger } from './logger.js';
16
+
17
+ const log = createLogger('webhook');
18
+
19
+ const MAX_RETRIES = parseInt(process.env.TAICHU_WEBHOOK_RETRIES) || 3;
20
+ const RETRY_BASE_MS = parseInt(process.env.TAICHU_WEBHOOK_RETRY_BASE_MS) || 1000; // 1s, 2s, 4s
21
+
22
+ class WebhookManager {
23
+ constructor(store) {
24
+ this.store = store;
25
+ this.deliveryLog = [];
26
+ }
27
+
28
+ /**
29
+ * Register a new webhook.
30
+ * @param {{ url: string, events?: string[], types?: string[], secret?: string, label?: string }} opts
31
+ * @returns {Promise<object>}
32
+ */
33
+ async register({ url, events = ['*'], types = ['*'], secret, label }) {
34
+ const id = randomBytes(12).toString('hex');
35
+ const webhook = {
36
+ id, url, events, types,
37
+ secret: secret || randomBytes(16).toString('hex'),
38
+ label: label || url,
39
+ active: true,
40
+ createdAt: new Date().toISOString(),
41
+ stats: { delivered: 0, failed: 0, lastDelivery: null }
42
+ };
43
+
44
+ const doc = await this.store.create({
45
+ type: 'webhook',
46
+ data: webhook,
47
+ status: 'active'
48
+ });
49
+
50
+ log.info(`Webhook registered: ${label || url} (${id})`);
51
+ return { id: doc.id, ...webhook };
52
+ }
53
+
54
+ /**
55
+ * List all registered webhooks.
56
+ */
57
+ async list() {
58
+ const docs = await this.store.list({ type: 'webhook' });
59
+ return docs.map(d => ({ id: d.id, ...d.data }));
60
+ }
61
+
62
+ /**
63
+ * Delete a webhook.
64
+ */
65
+ async remove(id) {
66
+ await this.store.delete(id);
67
+ }
68
+
69
+ /**
70
+ * Fire event to all matching webhooks (called by hook system).
71
+ * @param {string} event — "create" | "update" | "delete" | "publish"
72
+ * @param {object} payload — { id, type, data, status, ... }
73
+ */
74
+ async fire(event, payload) {
75
+ const hooks = await this.list();
76
+ const active = hooks.filter(h => h.active);
77
+ if (active.length === 0) return;
78
+
79
+ for (const wh of active) {
80
+ if (!this._matches(wh, event, payload)) continue;
81
+ await this._deliver(wh, event, payload);
82
+ }
83
+ }
84
+
85
+ /** Check if webhook matches event + payload type */
86
+ _matches(wh, event, payload) {
87
+ const eventMatch = wh.events.includes('*') || wh.events.includes(event);
88
+ const typeMatch = wh.types.includes('*') || wh.types.includes(payload.type);
89
+ return eventMatch && typeMatch;
90
+ }
91
+
92
+ /** Deliver event with retry */
93
+ async _deliver(wh, event, payload) {
94
+ const deliveryId = randomBytes(6).toString('hex');
95
+ const body = JSON.stringify({
96
+ id: deliveryId,
97
+ event,
98
+ type: payload.type,
99
+ timestamp: new Date().toISOString(),
100
+ data: payload
101
+ });
102
+
103
+ const signature = createHmac('sha256', wh.secret)
104
+ .update(body)
105
+ .digest('hex');
106
+
107
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
108
+ try {
109
+ const controller = new AbortController();
110
+ const timeout = setTimeout(() => controller.abort(), 10000);
111
+
112
+ const res = await fetch(wh.url, {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/json',
116
+ 'X-Taichu-Webhook-Event': event,
117
+ 'X-Taichu-Webhook-Signature': `sha256=${signature}`,
118
+ 'X-Taichu-Webhook-Id': deliveryId
119
+ },
120
+ body,
121
+ signal: controller.signal
122
+ });
123
+ clearTimeout(timeout);
124
+
125
+ if (res.ok) {
126
+ this._logDelivery(wh.id, deliveryId, true, attempt);
127
+ // Update stats
128
+ const doc = await this.store.get(wh.id);
129
+ if (doc) {
130
+ doc.data.stats.delivered++;
131
+ doc.data.stats.lastDelivery = new Date().toISOString();
132
+ await this.store.update(wh.id, { data: doc.data });
133
+ }
134
+ return;
135
+ }
136
+
137
+ // Non-2xx response — retry if server error
138
+ if (res.status >= 500) throw new Error(`Server error: ${res.status}`);
139
+ // 4xx — no retry
140
+ this._logDelivery(wh.id, deliveryId, false, attempt, `HTTP ${res.status}`);
141
+ break;
142
+
143
+ } catch (err) {
144
+ if (attempt < MAX_RETRIES) {
145
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt - 1);
146
+ log.warn(`Webhook retry ${attempt}/${MAX_RETRIES} for ${wh.url}: ${err.message}`);
147
+ await new Promise(r => setTimeout(r, delay));
148
+ } else {
149
+ this._logDelivery(wh.id, deliveryId, false, attempt, err.message);
150
+ const doc = await this.store.get(wh.id);
151
+ if (doc) {
152
+ doc.data.stats.failed++;
153
+ await this.store.update(wh.id, { data: doc.data });
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ _logDelivery(webhookId, deliveryId, success, attempt, error) {
161
+ const entry = {
162
+ webhookId, deliveryId, success, attempt,
163
+ timestamp: new Date().toISOString(),
164
+ error: error || null
165
+ };
166
+ this.deliveryLog.push(entry);
167
+ if (this.deliveryLog.length > 1000) this.deliveryLog.shift();
168
+
169
+ if (!success) log.error(`Webhook delivery failed: ${webhookId} — ${error}`);
170
+ }
171
+
172
+ /** Get delivery log */
173
+ getLog(limit = 50) {
174
+ return this.deliveryLog.slice(-limit).reverse();
175
+ }
176
+
177
+ /** Get stats */
178
+ getStats() {
179
+ const recent = this.deliveryLog.slice(-100);
180
+ const success = recent.filter(e => e.success).length;
181
+ return { recent: recent.length, success, failed: recent.length - success };
182
+ }
183
+ }
184
+
185
+ // ── Singleton ──────────────────────────────────────────────
186
+
187
+ let _wm = null;
188
+
189
+ export function getWebhookManager(store) {
190
+ if (!_wm && store) _wm = new WebhookManager(store);
191
+ return _wm;
192
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * WebSocket Server — 实时内容变更推送
3
+ *
4
+ * 零依赖,基于 Node.js 原生 WebSocket (RFC 6455)。
5
+ *
6
+ * 功能:
7
+ * - 频道订阅(按内容类型:article, page, * 等)
8
+ * - 事件广播(create/update/delete/publish)
9
+ * - 连接心跳(30s ping)
10
+ * - 客户端数量追踪
11
+ *
12
+ * 协议:
13
+ * Client → Server: { type: "subscribe", channel: "article" }
14
+ * Server → Client: { type: "content_change", event: "update", channel: "article", doc: {...} }
15
+ */
16
+
17
+ import { createHash, randomBytes } from 'node:crypto';
18
+ import { EventEmitter } from 'node:events';
19
+
20
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
21
+ const HEARTBEAT_INTERVAL = 30000;
22
+
23
+ class WebSocketServer extends EventEmitter {
24
+ constructor() {
25
+ super();
26
+ /** @type {Map<string, WebSocket>} */
27
+ this.clients = new Map();
28
+ /** @type {Map<string, Set<string>>} channel → clientIds */
29
+ this.subscriptions = new Map();
30
+ this.heartbeatTimer = null;
31
+ this.stats = { connected: 0, messagesReceived: 0, messagesSent: 0 };
32
+ }
33
+
34
+ /**
35
+ * Attach WebSocket upgrade handler to an existing HTTP server.
36
+ * @param {import('node:http').Server} httpServer
37
+ */
38
+ attach(httpServer) {
39
+ httpServer.on('upgrade', (req, socket, head) => {
40
+ if (req.headers['upgrade']?.toLowerCase() !== 'websocket') return;
41
+
42
+ const key = req.headers['sec-websocket-key'];
43
+ if (!key) {
44
+ socket.destroy();
45
+ return;
46
+ }
47
+
48
+ // Accept the WebSocket connection
49
+ const acceptKey = createHash('sha1')
50
+ .update(key + WS_GUID)
51
+ .digest('base64');
52
+
53
+ socket.write(
54
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
55
+ 'Upgrade: websocket\r\n' +
56
+ 'Connection: Upgrade\r\n' +
57
+ `Sec-WebSocket-Accept: ${acceptKey}\r\n\r\n`
58
+ );
59
+
60
+ const ws = new WebSocket(socket);
61
+ const clientId = randomBytes(8).toString('hex');
62
+ this.clients.set(clientId, ws);
63
+ this.stats.connected++;
64
+
65
+ this.emit('connect', clientId);
66
+
67
+ ws.on('message', (data) => {
68
+ this.stats.messagesReceived++;
69
+ this._handleMessage(clientId, data);
70
+ });
71
+
72
+ ws.on('close', () => {
73
+ this.clients.delete(clientId);
74
+ this.stats.connected = this.clients.size;
75
+ // Remove from all subscriptions
76
+ for (const [, subs] of this.subscriptions) {
77
+ subs.delete(clientId);
78
+ }
79
+ this.emit('disconnect', clientId);
80
+ });
81
+
82
+ ws.on('error', () => {
83
+ ws.close();
84
+ });
85
+ });
86
+
87
+ // Start heartbeat
88
+ this.heartbeatTimer = setInterval(() => {
89
+ for (const [, ws] of this.clients) {
90
+ ws.ping();
91
+ }
92
+ }, HEARTBEAT_INTERVAL);
93
+ }
94
+
95
+ /**
96
+ * Broadcast a content change event to subscribers of a channel.
97
+ * @param {string} channel — content type name or "*" for all
98
+ * @param {string} event — "create" | "update" | "delete" | "publish" | "archive"
99
+ * @param {object} payload
100
+ */
101
+ broadcast(channel, event, payload) {
102
+ const channels = [channel, '*'];
103
+ const sentTo = new Set();
104
+
105
+ for (const ch of channels) {
106
+ const subs = this.subscriptions.get(ch);
107
+ if (!subs) continue;
108
+ for (const clientId of subs) {
109
+ if (sentTo.has(clientId)) continue;
110
+ const ws = this.clients.get(clientId);
111
+ if (!ws) continue;
112
+ ws.send(JSON.stringify({
113
+ type: 'content_change',
114
+ channel,
115
+ event,
116
+ doc: payload,
117
+ ts: new Date().toISOString()
118
+ }));
119
+ sentTo.add(clientId);
120
+ this.stats.messagesSent++;
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get current server stats.
127
+ */
128
+ getStats() {
129
+ const channels = {};
130
+ for (const [ch, subs] of this.subscriptions) {
131
+ channels[ch] = subs.size;
132
+ }
133
+ return { ...this.stats, channels };
134
+ }
135
+
136
+ /** Close all connections */
137
+ close() {
138
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
139
+ for (const [, ws] of this.clients) {
140
+ ws.close(1001, 'Server shutting down');
141
+ }
142
+ }
143
+
144
+ _handleMessage(clientId, raw) {
145
+ try {
146
+ if (typeof raw !== 'string') return;
147
+ const msg = JSON.parse(raw);
148
+
149
+ switch (msg.type) {
150
+ case 'subscribe': {
151
+ const channel = msg.channel || '*';
152
+ if (!this.subscriptions.has(channel)) {
153
+ this.subscriptions.set(channel, new Set());
154
+ }
155
+ this.subscriptions.get(channel).add(clientId);
156
+ const ws = this.clients.get(clientId);
157
+ if (ws) ws.send(JSON.stringify({ type: 'subscribed', channel }));
158
+ break;
159
+ }
160
+ case 'unsubscribe': {
161
+ const channel = msg.channel || '*';
162
+ const subs = this.subscriptions.get(channel);
163
+ if (subs) subs.delete(clientId);
164
+ break;
165
+ }
166
+ }
167
+ } catch { /* ignore malformed */ }
168
+ }
169
+ }
170
+
171
+ // ── WebSocket Frame Protocol ───────────────────────────────
172
+
173
+ class WebSocket {
174
+ constructor(socket) {
175
+ this.socket = socket;
176
+ this._buffer = Buffer.alloc(0);
177
+ this._closed = false;
178
+
179
+ socket.on('data', (chunk) => {
180
+ this._buffer = Buffer.concat([this._buffer, chunk]);
181
+ this._parseFrames();
182
+ });
183
+
184
+ socket.on('close', () => {
185
+ this._closed = true;
186
+ this.emit('close');
187
+ });
188
+
189
+ socket.on('error', () => {
190
+ this._closed = true;
191
+ this.emit('error');
192
+ });
193
+ }
194
+
195
+ _listeners = {};
196
+
197
+ on(ev, fn) { (this._listeners[ev] ||= []).push(fn); }
198
+ emit(ev, ...args) { (this._listeners[ev] || []).forEach(f => f(...args)); }
199
+
200
+ send(data) {
201
+ if (this._closed) return;
202
+ try {
203
+ const payload = Buffer.from(data);
204
+ const frame = this._encodeFrame(payload, 0x1); // text frame
205
+ this.socket.write(frame);
206
+ } catch {}
207
+ }
208
+
209
+ ping() {
210
+ if (this._closed) return;
211
+ try {
212
+ this.socket.write(this._encodeFrame(Buffer.alloc(0), 0x9));
213
+ } catch {}
214
+ }
215
+
216
+ close(code = 1000, reason = '') {
217
+ if (this._closed) return;
218
+ try {
219
+ const payload = Buffer.alloc(2 + reason.length);
220
+ payload.writeUInt16BE(code, 0);
221
+ payload.write(reason, 2);
222
+ this.socket.write(this._encodeFrame(payload, 0x8));
223
+ this.socket.end();
224
+ } catch {}
225
+ this._closed = true;
226
+ }
227
+
228
+ _parseFrames() {
229
+ while (this._buffer.length >= 2) {
230
+ const opcode = this._buffer[0] & 0x0f;
231
+ const masked = (this._buffer[1] & 0x80) !== 0;
232
+ let payloadLen = this._buffer[1] & 0x7f;
233
+ let offset = 2;
234
+
235
+ if (payloadLen === 126) {
236
+ if (this._buffer.length < 4) return;
237
+ payloadLen = this._buffer.readUInt16BE(2);
238
+ offset = 4;
239
+ } else if (payloadLen === 127) {
240
+ if (this._buffer.length < 10) return;
241
+ payloadLen = Number(this._buffer.readBigUInt64BE(2));
242
+ offset = 10;
243
+ }
244
+
245
+ const maskLen = masked ? 4 : 0;
246
+ if (this._buffer.length < offset + maskLen + payloadLen) return;
247
+
248
+ const payload = this._buffer.subarray(offset + maskLen, offset + maskLen + payloadLen);
249
+ if (masked) {
250
+ const mask = this._buffer.subarray(offset, offset + 4);
251
+ for (let i = 0; i < payload.length; i++) {
252
+ payload[i] ^= mask[i % 4];
253
+ }
254
+ }
255
+
256
+ this._buffer = this._buffer.subarray(offset + maskLen + payloadLen);
257
+
258
+ switch (opcode) {
259
+ case 0x1: // text
260
+ this.emit('message', payload.toString('utf-8'));
261
+ break;
262
+ case 0x8: // close
263
+ this.close();
264
+ break;
265
+ case 0x9: // ping → pong
266
+ if (!this._closed) this.socket.write(this._encodeFrame(payload, 0xA));
267
+ break;
268
+ }
269
+ }
270
+ }
271
+
272
+ _encodeFrame(payload, opcode) {
273
+ const len = payload.length;
274
+ let header;
275
+
276
+ if (len < 126) {
277
+ header = Buffer.alloc(2);
278
+ header[0] = 0x80 | opcode;
279
+ header[1] = len;
280
+ } else if (len < 65536) {
281
+ header = Buffer.alloc(4);
282
+ header[0] = 0x80 | opcode;
283
+ header[1] = 126;
284
+ header.writeUInt16BE(len, 2);
285
+ } else {
286
+ header = Buffer.alloc(10);
287
+ header[0] = 0x80 | opcode;
288
+ header[1] = 127;
289
+ header.writeBigUInt64BE(BigInt(len), 2);
290
+ }
291
+
292
+ return Buffer.concat([header, payload]);
293
+ }
294
+ }
295
+
296
+ // ── Singleton ──────────────────────────────────────────────
297
+
298
+ let _wss = null;
299
+
300
+ export function getWSS() {
301
+ if (!_wss) _wss = new WebSocketServer();
302
+ return _wss;
303
+ }
304
+
305
+ export function createWSS() {
306
+ _wss = new WebSocketServer();
307
+ return _wss;
308
+ }
package/scripts/cli.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Taichu CLI — Quick Start
4
+ *
5
+ * npx taichu init → Create a new Taichu project
6
+ * npx taichu dev → Start development server
7
+ * npx taichu migrate --from=wordpress --file=dump.xml
8
+ */
9
+
10
+ import { spawn } from 'node:child_process';
11
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ const cmd = process.argv[2];
15
+
16
+ switch (cmd) {
17
+ case 'init': {
18
+ const dir = process.argv[3] || 'taichu-project';
19
+ if (existsSync(dir)) {
20
+ console.log(`Directory "${dir}" already exists.`);
21
+ process.exit(1);
22
+ }
23
+ mkdirSync(dir, { recursive: true });
24
+ mkdirSync(join(dir, '.taichu', 'data'), { recursive: true });
25
+ mkdirSync(join(dir, 'plugins'), { recursive: true });
26
+
27
+ writeFileSync(join(dir, '.env'), `# Taichu Configuration
28
+ TAICHU_PORT=3120
29
+ TAICHU_STORAGE=sqlite
30
+ TAICHU_DATA_DIR=./.taichu/data
31
+ TAICHU_JWT_SECRET=change-me-to-a-random-string
32
+ TAICHU_LOG_LEVEL=info
33
+ `);
34
+
35
+ writeFileSync(join(dir, 'taichu.config.json'), JSON.stringify({
36
+ siteName: 'My Taichu Site',
37
+ language: 'zh-CN',
38
+ timezone: 'Asia/Shanghai'
39
+ }, null, 2));
40
+
41
+ console.log(`
42
+ ⚡ Taichu CMS initialized in ${dir}/
43
+
44
+ To start:
45
+ cd ${dir}
46
+ npx taichu dev
47
+
48
+ Or with Docker:
49
+ docker run -p 3120:3120 -v $(pwd)/.taichu:/app/.taichu caludelaw/taichu
50
+ `);
51
+ process.exit(0);
52
+ break;
53
+ }
54
+
55
+ case 'dev': {
56
+ // Find taichu server module
57
+ const serverPath = join(import.meta.dirname || process.cwd(), '..', '..', 'packages', 'server', 'src', 'index.js');
58
+ if (!existsSync(serverPath)) {
59
+ console.error('Taichu server not found. Run from a Taichu project root.');
60
+ process.exit(1);
61
+ }
62
+ const child = spawn('node', [serverPath], { stdio: 'inherit', env: { ...process.env, TAICHU_PORT: process.env.TAICHU_PORT || '3120' } });
63
+ child.on('exit', (code) => process.exit(code));
64
+ break;
65
+ }
66
+
67
+ case 'migrate':
68
+ await import('./migrate.js');
69
+ break;
70
+
71
+ case 'plugin':
72
+ await import('./plugin-cli.js');
73
+ break;
74
+
75
+ default:
76
+ console.log(`
77
+ ⚡ Taichu CMS CLI
78
+
79
+ Commands:
80
+ npx taichu init [dir] Create a new Taichu project
81
+ npx taichu dev Start development server
82
+ npx taichu migrate Import content (WP/Markdown)
83
+ npx taichu plugin Browse & install plugins
84
+
85
+ Environment:
86
+ TAICHU_PORT=3120 Server port
87
+ TAICHU_STORAGE=sqlite Storage engine (memory/sqlite)
88
+ TAICHU_AGENT_KEY API key for MCP Agent
89
+ `);
90
+ }