design-learn-server 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -2,10 +2,6 @@
2
2
 
3
3
  A minimal single-process server entry that registers REST, WebSocket, and MCP (SSE) routes.
4
4
 
5
- ## Node version
6
-
7
- Requires Node 18 or 20 (LTS). Node 22+ is not supported because `better-sqlite3` uses native bindings.
8
-
9
5
  ## Start
10
6
 
11
7
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "design-learn-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Design-Learn server skeleton for REST/WS/MCP routes",
6
6
  "main": "src/server.js",
@@ -9,9 +9,6 @@
9
9
  "design-learn-mcp": "src/stdio.js",
10
10
  "design-learn-stdio": "src/stdio.js"
11
11
  },
12
- "engines": {
13
- "node": ">=18 <23"
14
- },
15
12
  "files": [
16
13
  "src",
17
14
  "README.md"
@@ -21,13 +18,12 @@
21
18
  },
22
19
  "scripts": {
23
20
  "start": "node src/server.js",
24
- "dev": "node src/server.js",
25
- "preinstall": "node src/check-node.js"
21
+ "dev": "node src/server.js"
26
22
  },
27
23
  "dependencies": {
28
24
  "@modelcontextprotocol/sdk": "^1.0.0",
29
- "better-sqlite3": "^11.10.0",
30
25
  "playwright": "^1.57.0",
26
+ "sql.js": "^1.13.0",
31
27
  "zod": "^3.24.1"
32
28
  }
33
29
  }
package/src/cli.js CHANGED
File without changes
package/src/mcp/index.js CHANGED
@@ -445,8 +445,8 @@ function createMcpServer({ name, version, storage, uipro }) {
445
445
  return server;
446
446
  }
447
447
 
448
- function createMcpHandler(options = {}) {
449
- const storage = options.storage || createStorage({ dataDir: options.dataDir });
448
+ async function createMcpHandler(options = {}) {
449
+ const storage = options.storage || (await createStorage({ dataDir: options.dataDir }));
450
450
  const ownsStorage = !options.storage;
451
451
  const serverName = options.serverName || 'design-learn';
452
452
  const serverVersion = options.serverVersion || '0.1.0';
package/src/server.js CHANGED
@@ -22,69 +22,64 @@ const { readJson, writeJson } = require('./storage/fileStore');
22
22
  const defaultDataDir = path.join(os.homedir(), '.design-learn', 'data');
23
23
  const dataDir = process.env.DESIGN_LEARN_DATA_DIR || process.env.DATA_DIR || defaultDataDir;
24
24
 
25
- const storage = createStorage({ dataDir });
26
- const uipro = createUipro({ dataDir });
27
- const extractionPipeline = createExtractionPipeline({ storage });
28
- const previewPipeline = createPreviewPipeline({ storage });
25
+ let storage;
26
+ let uipro;
27
+ let extractionPipeline;
28
+ let previewPipeline;
29
+ let mcpHandler;
29
30
 
30
31
  // import job -> designId,用于将 pipeline 状态回写到 design metadata(便于 UI 与 MCP 立刻可见)
31
32
  const importJobDesignMap = new Map();
32
33
 
33
- extractionPipeline.onProgress((event) => {
34
- const designId = importJobDesignMap.get(event.job.id);
35
- if (!designId) {
36
- return;
37
- }
38
-
39
- const now = new Date().toISOString();
40
- void (async () => {
41
- const design = await storage.getDesign(designId);
42
- const meta = design?.metadata || {};
43
- const aiRequested = !!meta.aiRequested;
44
- const aiCompleted = meta.processingMessage === 'ai_completed' || meta.aiCompleted;
45
-
46
- const metaPatch = {
47
- processingStatus: event.event === 'failed' ? 'failed' : event.event === 'completed' ? 'completed' : 'processing',
48
- processingJobId: event.job.id,
49
- processingUpdatedAt: now,
50
- };
34
+ function registerPipelineHandlers() {
35
+ extractionPipeline.onProgress((event) => {
36
+ const designId = importJobDesignMap.get(event.job.id);
37
+ if (!designId) {
38
+ return;
39
+ }
51
40
 
52
- if (event.event === 'failed') {
53
- metaPatch.processingError = event.job.error?.message || 'unknown_error';
54
- metaPatch.processingMessage = 'failed';
55
- metaPatch.processingProgress = 100;
56
- } else if (event.event === 'completed') {
57
- metaPatch.processingError = null;
58
- metaPatch.lastImportAt = now;
59
- metaPatch.lastImportVersionId = event.job.result?.versionId || null;
60
- if (aiRequested && !aiCompleted) {
61
- metaPatch.processingStatus = 'analyzing';
62
- metaPatch.processingMessage = 'ai_pending';
63
- metaPatch.processingProgress = 80;
64
- } else {
65
- metaPatch.processingMessage = 'completed';
41
+ const now = new Date().toISOString();
42
+ void (async () => {
43
+ const design = await storage.getDesign(designId);
44
+ const meta = design?.metadata || {};
45
+ const aiRequested = !!meta.aiRequested;
46
+ const aiCompleted = meta.processingMessage === 'ai_completed' || meta.aiCompleted;
47
+
48
+ const metaPatch = {
49
+ processingStatus: event.event === 'failed' ? 'failed' : event.event === 'completed' ? 'completed' : 'processing',
50
+ processingJobId: event.job.id,
51
+ processingUpdatedAt: now,
52
+ };
53
+
54
+ if (event.event === 'failed') {
55
+ metaPatch.processingError = event.job.error?.message || 'unknown_error';
56
+ metaPatch.processingMessage = 'failed';
66
57
  metaPatch.processingProgress = 100;
58
+ } else if (event.event === 'completed') {
59
+ metaPatch.processingError = null;
60
+ metaPatch.lastImportAt = now;
61
+ metaPatch.lastImportVersionId = event.job.result?.versionId || null;
62
+ if (aiRequested && !aiCompleted) {
63
+ metaPatch.processingStatus = 'analyzing';
64
+ metaPatch.processingMessage = 'ai_pending';
65
+ metaPatch.processingProgress = 80;
66
+ } else {
67
+ metaPatch.processingMessage = 'completed';
68
+ metaPatch.processingProgress = 100;
69
+ }
70
+ } else {
71
+ metaPatch.processingMessage = event.job.message || 'processing';
72
+ metaPatch.processingProgress = event.job.progress || 0;
67
73
  }
68
- } else {
69
- metaPatch.processingMessage = event.job.message || 'processing';
70
- metaPatch.processingProgress = event.job.progress || 0;
71
- }
72
74
 
73
- await storage.updateDesign(designId, { metadata: metaPatch });
74
- })().catch(() => undefined);
75
+ await storage.updateDesign(designId, { metadata: metaPatch });
76
+ })().catch(() => undefined);
75
77
 
76
- if (event.event === 'failed' || event.event === 'completed') {
77
- importJobDesignMap.delete(event.job.id);
78
- }
79
- });
80
- const mcpHandler = createMcpHandler({
81
- storage,
82
- dataDir: process.env.DESIGN_LEARN_DATA_DIR,
83
- serverName: process.env.MCP_SERVER_NAME,
84
- serverVersion: process.env.MCP_SERVER_VERSION,
85
- authToken: process.env.MCP_AUTH_TOKEN,
86
- uipro,
87
- });
78
+ if (event.event === 'failed' || event.event === 'completed') {
79
+ importJobDesignMap.delete(event.job.id);
80
+ }
81
+ });
82
+ }
88
83
 
89
84
  const routes = [
90
85
  {
@@ -1367,17 +1362,40 @@ server.on('clientError', (err, socket) => {
1367
1362
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
1368
1363
  });
1369
1364
 
1370
- server.listen(DEFAULT_PORT, () => {
1371
- console.log(`[design-learn-server] listening on http://localhost:${DEFAULT_PORT}`);
1372
- console.log(`[design-learn-server] data dir: ${storage.dataDir}`);
1365
+ async function start() {
1366
+ storage = await createStorage({ dataDir });
1367
+ uipro = createUipro({ dataDir });
1368
+ extractionPipeline = createExtractionPipeline({ storage });
1369
+ previewPipeline = createPreviewPipeline({ storage });
1370
+ registerPipelineHandlers();
1371
+ mcpHandler = await createMcpHandler({
1372
+ storage,
1373
+ dataDir: process.env.DESIGN_LEARN_DATA_DIR,
1374
+ serverName: process.env.MCP_SERVER_NAME,
1375
+ serverVersion: process.env.MCP_SERVER_VERSION,
1376
+ authToken: process.env.MCP_AUTH_TOKEN,
1377
+ uipro,
1378
+ });
1379
+
1380
+ server.listen(DEFAULT_PORT, () => {
1381
+ console.log(`[design-learn-server] listening on http://localhost:${DEFAULT_PORT}`);
1382
+ console.log(`[design-learn-server] data dir: ${storage.dataDir}`);
1383
+ });
1384
+ }
1385
+
1386
+ start().catch((error) => {
1387
+ console.error('[design-learn-server] startup failed', error);
1388
+ process.exit(1);
1373
1389
  });
1374
1390
 
1375
1391
  function shutdown(signal) {
1376
1392
  console.log(`[design-learn-server] received ${signal}, shutting down`);
1377
- mcpHandler.close().catch((error) => console.error('[mcp] close error', error));
1378
- extractionPipeline.close();
1379
- previewPipeline.close();
1380
- storage.close();
1393
+ if (mcpHandler) {
1394
+ mcpHandler.close().catch((error) => console.error('[mcp] close error', error));
1395
+ }
1396
+ extractionPipeline?.close();
1397
+ previewPipeline?.close();
1398
+ storage?.close();
1381
1399
  server.close(() => process.exit(0));
1382
1400
  }
1383
1401
 
package/src/stdio.js CHANGED
@@ -55,11 +55,12 @@ async function ensurePlaywright() {
55
55
  const os = require('os');
56
56
  const defaultDataDir = path.join(os.homedir(), '.design-learn', 'data');
57
57
  const dataDir = process.env.DESIGN_LEARN_DATA_DIR || process.env.DATA_DIR || defaultDataDir;
58
- const storage = createStorage({ dataDir });
58
+ const storagePromise = createStorage({ dataDir });
59
59
  const uipro = createUipro({ dataDir });
60
60
 
61
- function matchDesigns(query) {
61
+ async function matchDesigns(query) {
62
62
  const needle = query.toLowerCase();
63
+ const storage = await storagePromise;
63
64
  const designs = storage.listDesigns();
64
65
  return designs.filter((design) => {
65
66
  const tags = Array.isArray(design.metadata?.tags) ? design.metadata.tags.join(' ') : '';
@@ -91,6 +92,7 @@ server.tool(
91
92
  'List stored design resources',
92
93
  { limit: z.number().min(1).max(100).optional() },
93
94
  async ({ limit }) => {
95
+ const storage = await storagePromise;
94
96
  const designs = storage.listDesigns();
95
97
  const data = typeof limit === 'number' ? designs.slice(0, limit) : designs;
96
98
  return {
@@ -108,7 +110,7 @@ server.tool(
108
110
  limit: z.number().min(1).max(100).optional(),
109
111
  },
110
112
  async ({ query, limit }) => {
111
- const matches = matchDesigns(query);
113
+ const matches = await matchDesigns(query);
112
114
  const data = typeof limit === 'number' ? matches.slice(0, limit) : matches;
113
115
  return {
114
116
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
@@ -135,14 +137,12 @@ server.tool(
135
137
  },
136
138
  async ({ query, sources, domain, stack, limit, designLimit }) => {
137
139
  const effectiveSources = Array.isArray(sources) && sources.length > 0 ? sources : ['designs', 'uipro'];
138
- const designs =
139
- effectiveSources.includes('designs')
140
- ? (() => {
141
- const matches = matchDesigns(query);
142
- const max = typeof designLimit === 'number' ? designLimit : typeof limit === 'number' ? limit : undefined;
143
- return typeof max === 'number' ? matches.slice(0, max) : matches;
144
- })()
145
- : [];
140
+ let designs = [];
141
+ if (effectiveSources.includes('designs')) {
142
+ const matches = await matchDesigns(query);
143
+ const max = typeof designLimit === 'number' ? designLimit : typeof limit === 'number' ? limit : undefined;
144
+ designs = typeof max === 'number' ? matches.slice(0, max) : matches;
145
+ }
146
146
 
147
147
  const resolvedDomain = domain === 'auto' ? undefined : domain;
148
148
  const maxUipro = typeof limit === 'number' ? limit : 10;
@@ -201,6 +201,7 @@ server.tool(
201
201
  'Get styleguide markdown by design ID (latest version).',
202
202
  { designId: z.string() },
203
203
  async ({ designId }) => {
204
+ const storage = await storagePromise;
204
205
  const versions = storage.listVersions(designId);
205
206
  if (!versions || versions.length === 0) {
206
207
  return {
@@ -19,9 +19,9 @@ const {
19
19
  const { ensureDir, writeJson, readJson, writeText, readText, removePath } = require('./fileStore');
20
20
  const { openDatabase } = require('./sqliteStore');
21
21
 
22
- function createStorage(options = {}) {
22
+ async function createStorage(options = {}) {
23
23
  const dataDir = resolveDataDir(options.dataDir);
24
- const db = openDatabase(getDatabasePath(dataDir));
24
+ const db = await openDatabase(getDatabasePath(dataDir));
25
25
 
26
26
  return {
27
27
  dataDir,
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const Database = require('better-sqlite3');
3
+ const initSqlJs = require('sql.js');
4
4
 
5
5
  const SCHEMA_VERSION = 2;
6
6
 
@@ -8,9 +8,118 @@ function ensureDir(dirPath) {
8
8
  fs.mkdirSync(dirPath, { recursive: true });
9
9
  }
10
10
 
11
- function openDatabase(dbPath) {
11
+ function readDatabaseFile(dbPath) {
12
+ try {
13
+ return fs.readFileSync(dbPath);
14
+ } catch (error) {
15
+ if (error.code === 'ENOENT') {
16
+ return null;
17
+ }
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ function wrapDatabase(rawDb, dbPath) {
23
+ let closed = false;
24
+
25
+ function persist() {
26
+ if (closed) {
27
+ return;
28
+ }
29
+ const data = rawDb.export();
30
+ const buffer = Buffer.from(data);
31
+ fs.writeFileSync(dbPath, buffer);
32
+ }
33
+
34
+ function prepare(sql) {
35
+ const stmt = rawDb.prepare(sql);
36
+ const normalizeParams = (params) => (params.length === 1 && Array.isArray(params[0]) ? params[0] : params);
37
+ const finalize = () => {
38
+ try {
39
+ stmt.free();
40
+ } catch {
41
+ return;
42
+ }
43
+ };
44
+
45
+ return {
46
+ run: (...params) => {
47
+ const bound = normalizeParams(params);
48
+ stmt.run(bound);
49
+ const changes = typeof rawDb.getRowsModified === 'function' ? rawDb.getRowsModified() : 0;
50
+ finalize();
51
+ persist();
52
+ return { changes };
53
+ },
54
+ get: (...params) => {
55
+ const bound = normalizeParams(params);
56
+ stmt.bind(bound);
57
+ if (!stmt.step()) {
58
+ finalize();
59
+ return undefined;
60
+ }
61
+ const row = stmt.getAsObject();
62
+ finalize();
63
+ return row;
64
+ },
65
+ all: (...params) => {
66
+ const bound = normalizeParams(params);
67
+ const rows = [];
68
+ stmt.bind(bound);
69
+ while (stmt.step()) {
70
+ rows.push(stmt.getAsObject());
71
+ }
72
+ finalize();
73
+ return rows;
74
+ },
75
+ };
76
+ }
77
+
78
+ function exec(sql) {
79
+ rawDb.exec(sql);
80
+ persist();
81
+ }
82
+
83
+ function pragma(statement, options = {}) {
84
+ const trimmed = statement.trim();
85
+ if (trimmed.includes('=')) {
86
+ rawDb.exec(`PRAGMA ${statement}`);
87
+ persist();
88
+ return undefined;
89
+ }
90
+ const result = rawDb.exec(`PRAGMA ${statement}`);
91
+ if (options.simple) {
92
+ return result?.[0]?.values?.[0]?.[0] ?? 0;
93
+ }
94
+ return result;
95
+ }
96
+
97
+ function close() {
98
+ if (closed) {
99
+ return;
100
+ }
101
+ persist();
102
+ rawDb.close();
103
+ closed = true;
104
+ }
105
+
106
+ return {
107
+ prepare,
108
+ exec,
109
+ pragma,
110
+ close,
111
+ };
112
+ }
113
+
114
+ async function openDatabase(dbPath) {
12
115
  ensureDir(path.dirname(dbPath));
13
- const db = new Database(dbPath);
116
+ const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm');
117
+ const SQL = await initSqlJs({
118
+ locateFile: () => wasmPath,
119
+ });
120
+ const fileBuffer = readDatabaseFile(dbPath);
121
+ const rawDb = fileBuffer ? new SQL.Database(fileBuffer) : new SQL.Database();
122
+ const db = wrapDatabase(rawDb, dbPath);
14
123
  db.pragma('journal_mode = WAL');
15
124
  migrate(db);
16
125
  return db;
package/src/check-node.js DELETED
@@ -1,14 +0,0 @@
1
- "use strict";
2
-
3
- const major = Number(process.versions.node.split(".")[0]);
4
- const isSupported = major >= 18 && major < 23;
5
-
6
- if (!isSupported) {
7
- console.error("=== Node 版本不支持 ===");
8
- console.error(
9
- `[design-learn] 仅支持 Node 18/20,当前版本 ${process.versions.node}。`
10
- );
11
- console.error("[design-learn] 请切换到 Node 18/20 后重试。");
12
- process.exit(1);
13
- }
14
-