@kne/fastify-app-manager 0.1.9 → 0.1.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.
@@ -4,7 +4,7 @@ const fs = require('fs-extra');
4
4
  const AdmZip = require('adm-zip');
5
5
  const { createSequelizeFromEnv, SQL_MIGRATIONS_TABLE } = require('../utils/migrate');
6
6
  const { resolveAppDbEnv, hasAppDbConfig } = require('../utils/env');
7
- const { resolveDbScope, normalizeOwnedTables, isSystemTable, resolveTablePrefix } = require('../utils/dbIdentity');
7
+ const { resolveDbScope, normalizeOwnedTables, isSystemTable, resolveTablePrefix, filterOwnedTables } = require('../utils/dbIdentity');
8
8
  const { assertReadOnlyQuerySql } = require('../utils/sqlQueryGuard');
9
9
 
10
10
  module.exports = fp(async (fastify, options) => {
@@ -83,15 +83,7 @@ module.exports = fp(async (fastify, options) => {
83
83
  return { table, columns, primaryKey };
84
84
  };
85
85
 
86
- const getOwnedTables = (app, allTables) => {
87
- const scope = resolveDbScope(app.env || {}, app.options || {});
88
- const existing = new Set(allTables);
89
- if (scope === 'dedicated') {
90
- return allTables.filter(t => !isSystemTable(t));
91
- }
92
- const owned = normalizeOwnedTables(app.options?.ownedTables);
93
- return owned.filter(t => existing.has(t));
94
- };
86
+ const getOwnedTables = (app, allTables) => filterOwnedTables({ appEnv: app.env || {}, ownedTables: app.options?.ownedTables, allTables });
95
87
 
96
88
  const assertTableOwned = (app, allTables, table) => {
97
89
  const owned = getOwnedTables(app, allTables);
@@ -1,12 +1,10 @@
1
1
  const fp = require('fastify-plugin');
2
- const path = require('node:path');
3
- const fs = require('fs-extra');
4
- const { listSqlFiles } = require('../utils/validatePackage');
5
2
  const { isValidSqlFileName, readExecutedMigrations, recordMigration, removeMigrationRecord } = require('../utils/migrate');
6
3
 
7
4
  module.exports = fp(async (fastify, options) => {
8
5
  const { models } = fastify[options.name];
9
6
  const getDbops = () => fastify[options.name].services.dbops;
7
+ const getAppService = () => fastify[options.name].services.app;
10
8
 
11
9
  const httpError = (statusCode, message) => {
12
10
  if (fastify.httpErrors) {
@@ -27,8 +25,7 @@ module.exports = fp(async (fastify, options) => {
27
25
  if (!ver || ver.appName !== name) {
28
26
  httpError(404, 'version not found');
29
27
  }
30
- const sqlDir = path.join(ver.artifactPath, 'server', ver.migrationPath || options.sqlPath);
31
- return { app, ver, sqlDir };
28
+ return { app, ver };
32
29
  };
33
30
 
34
31
  const assertFileName = file => {
@@ -37,25 +34,20 @@ module.exports = fp(async (fastify, options) => {
37
34
  }
38
35
  };
39
36
 
40
- const assertFileExists = async (sqlDir, file) => {
37
+ const getFiles = ver => getAppService().getMigrationFiles(ver);
38
+
39
+ const findFile = async (ver, file) => {
41
40
  assertFileName(file);
42
- const filePath = path.join(sqlDir, file);
43
- if (!(await fs.pathExists(filePath))) {
41
+ const item = (await getFiles(ver)).find(f => f.name === file);
42
+ if (!item) {
44
43
  httpError(404, `sql file ${file} not found`);
45
44
  }
46
- return filePath;
47
- };
48
-
49
- const syncHasMigration = async (ver, sqlDir) => {
50
- const hasMigration = (await listSqlFiles(sqlDir)).length > 0;
51
- if (ver.hasMigration !== hasMigration) {
52
- await ver.update({ hasMigration });
53
- }
45
+ return item;
54
46
  };
55
47
 
56
48
  const list = async ({ name, versionId }) => {
57
- const { app, ver, sqlDir } = await resolveVersion({ name, versionId });
58
- const files = await listSqlFiles(sqlDir);
49
+ const { app, ver } = await resolveVersion({ name, versionId });
50
+ const files = await getFiles(ver);
59
51
  let executedMap = new Map();
60
52
  let dbError = null;
61
53
  try {
@@ -63,18 +55,13 @@ module.exports = fp(async (fastify, options) => {
63
55
  } catch (e) {
64
56
  dbError = e.message;
65
57
  }
66
- const pageData = await Promise.all(
67
- files.map(async file => {
68
- const stat = await fs.stat(path.join(sqlDir, file));
69
- return {
70
- name: file,
71
- size: stat.size,
72
- updatedAt: stat.mtime,
73
- executed: dbError ? null : executedMap.has(file),
74
- executedAt: executedMap.get(file) || null
75
- };
76
- })
77
- );
58
+ const pageData = files.map(item => ({
59
+ name: item.name,
60
+ size: Buffer.byteLength(item.content || '', 'utf8'),
61
+ updatedAt: item.updatedAt || null,
62
+ executed: dbError ? null : executedMap.has(item.name),
63
+ executedAt: executedMap.get(item.name) || null
64
+ }));
78
65
  return {
79
66
  versionId: String(ver.id),
80
67
  version: ver.version,
@@ -87,32 +74,33 @@ module.exports = fp(async (fastify, options) => {
87
74
  };
88
75
 
89
76
  const content = async ({ name, versionId, file }) => {
90
- const { sqlDir } = await resolveVersion({ name, versionId });
91
- const filePath = await assertFileExists(sqlDir, file);
92
- return { name: file, content: await fs.readFile(filePath, 'utf8') };
77
+ const { ver } = await resolveVersion({ name, versionId });
78
+ const item = await findFile(ver, file);
79
+ return { name: file, content: item.content || '' };
93
80
  };
94
81
 
95
82
  const save = async ({ name, versionId, file, content: sql }) => {
96
83
  assertFileName(file);
97
- const { ver, sqlDir } = await resolveVersion({ name, versionId });
98
- await fs.ensureDir(sqlDir);
99
- await fs.writeFile(path.join(sqlDir, file), sql || '', 'utf8');
100
- await syncHasMigration(ver, sqlDir);
84
+ const { ver } = await resolveVersion({ name, versionId });
85
+ const files = (await getFiles(ver)).filter(f => f.name !== file);
86
+ files.push({ name: file, content: sql || '', updatedAt: new Date().toISOString() });
87
+ await getAppService().saveMigrationFiles(ver, files);
101
88
  return { name: file };
102
89
  };
103
90
 
104
91
  const remove = async ({ name, versionId, file }) => {
105
- const { ver, sqlDir } = await resolveVersion({ name, versionId });
106
- const filePath = await assertFileExists(sqlDir, file);
107
- await fs.remove(filePath);
108
- await syncHasMigration(ver, sqlDir);
92
+ const { ver } = await resolveVersion({ name, versionId });
93
+ await findFile(ver, file);
94
+ await getAppService().saveMigrationFiles(
95
+ ver,
96
+ (await getFiles(ver)).filter(f => f.name !== file)
97
+ );
109
98
  return { name: file };
110
99
  };
111
100
 
112
101
  const execute = async ({ name, versionId, file }) => {
113
- const { app, sqlDir } = await resolveVersion({ name, versionId });
114
- const filePath = await assertFileExists(sqlDir, file);
115
- const sql = await fs.readFile(filePath, 'utf8');
102
+ const { app, ver } = await resolveVersion({ name, versionId });
103
+ const { content: sql } = await findFile(ver, file);
116
104
  const dbops = getDbops();
117
105
  let beforeTables = [];
118
106
  let afterTables = [];
@@ -131,8 +119,8 @@ module.exports = fp(async (fastify, options) => {
131
119
  };
132
120
 
133
121
  const mark = async ({ name, versionId, file }) => {
134
- const { app, sqlDir } = await resolveVersion({ name, versionId });
135
- await assertFileExists(sqlDir, file);
122
+ const { app, ver } = await resolveVersion({ name, versionId });
123
+ await findFile(ver, file);
136
124
  await getDbops().withSequelize(app, sequelize => recordMigration(sequelize, file));
137
125
  return { name: file, executed: true };
138
126
  };
@@ -1,5 +1,5 @@
1
1
  const path = require('node:path');
2
- const { hasAppDbConfig } = require('./env');
2
+ const { isAppDbEnabled } = require('./env');
3
3
 
4
4
  const SYSTEM_TABLES = new Set(['_fs_sql_migrations']);
5
5
 
@@ -69,12 +69,8 @@ const assertDefaultAppDbSeparated = (defaultAppDb, hostSequelize) => {
69
69
  }
70
70
  };
71
71
 
72
- const resolveDbScope = (appEnv = {}, options = {}) => {
73
- if (options.dbScope === 'dedicated' || options.dbScope === 'shared') {
74
- return options.dbScope;
75
- }
76
- return hasAppDbConfig(appEnv) ? 'dedicated' : 'shared';
77
- };
72
+ // 与 startProcess 注入的连接一致:只看 env(独立库配置 + DB_DEDICATED 开关)。options.dbScope 只是认领表时写入的缓存,env 变化后会过期
73
+ const resolveDbScope = (appEnv = {}) => (isAppDbEnabled(appEnv) ? 'dedicated' : 'shared');
78
74
 
79
75
  const normalizeOwnedTables = tables => [...new Set((Array.isArray(tables) ? tables : []).map(t => String(t || '').trim()).filter(Boolean))];
80
76
 
@@ -104,8 +100,21 @@ const resolveTablePrefix = (appName, appEnv = {}) => {
104
100
  return buildTablePrefix(appName);
105
101
  };
106
102
 
103
+ /**
104
+ * 共享库:只认登记过的表;独立库:库内业务表全归本应用,但显式配了 DB_TABLE_PREFIX 说明该库与其它系统共用,只认本前缀的表
105
+ */
106
+ const filterOwnedTables = ({ appEnv = {}, ownedTables, allTables = [] }) => {
107
+ if (resolveDbScope(appEnv) === 'dedicated') {
108
+ const prefix = String(appEnv.DB_TABLE_PREFIX || '').trim();
109
+ return allTables.filter(t => !isSystemTable(t) && (!prefix || String(t).startsWith(prefix)));
110
+ }
111
+ const existing = new Set(allTables);
112
+ return normalizeOwnedTables(ownedTables).filter(t => existing.has(t));
113
+ };
114
+
107
115
  module.exports = {
108
116
  SYSTEM_TABLES,
117
+ filterOwnedTables,
109
118
  fingerprintDbConfig,
110
119
  fingerprintFromEnv,
111
120
  assertDefaultAppDbSeparated,
@@ -0,0 +1,37 @@
1
+ const { EventEmitter } = require('node:events');
2
+
3
+ /** 部署阶段,按顺序推进:准备部署包 → SQL 迁移 → 启动进程 → 等待就绪 */
4
+ const DEPLOY_STEPS = ['package', 'migrate', 'start', 'ready'];
5
+
6
+ /**
7
+ * 每个应用只保留最近一次部署的进度事件,供 SSE 订阅与断线重连回放。
8
+ * 事件:{ progressId, step, status: 'running' | 'done' | 'skipped' | 'error', detail?, message?, at }
9
+ */
10
+ const createDeployProgress = () => {
11
+ const emitter = new EventEmitter();
12
+ emitter.setMaxListeners(0);
13
+ const latest = new Map();
14
+
15
+ const begin = (name, progressId) => {
16
+ const state = { progressId, events: [] };
17
+ latest.set(name, state);
18
+ const report = (step, status, { detail = null, message = null } = {}) => {
19
+ const event = { progressId, step, status, detail, message, at: new Date().toISOString() };
20
+ state.events.push(event);
21
+ emitter.emit(`deploy:${name}`, event);
22
+ return event;
23
+ };
24
+ return { progressId, report };
25
+ };
26
+
27
+ const snapshot = name => latest.get(name) || null;
28
+
29
+ const clear = name => latest.delete(name);
30
+
31
+ return { emitter, begin, snapshot, clear };
32
+ };
33
+
34
+ module.exports = {
35
+ DEPLOY_STEPS,
36
+ createDeployProgress
37
+ };
package/libs/utils/env.js CHANGED
@@ -23,6 +23,22 @@ const hasAppDbConfig = (appEnv = {}) => {
23
23
  return !!appEnv.DB_DATABASE;
24
24
  };
25
25
 
26
+ const DB_DEDICATED_KEY = 'DB_DEDICATED';
27
+ const DB_CONNECTION_KEYS = ['DB_DIALECT', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'DB_STORAGE'];
28
+
29
+ // 配好独立库后默认启用;DB_DEDICATED=false 时保留配置但回落共享库
30
+ const isAppDbEnabled = (appEnv = {}) =>
31
+ hasAppDbConfig(appEnv) &&
32
+ String(appEnv[DB_DEDICATED_KEY] ?? '')
33
+ .trim()
34
+ .toLowerCase() !== 'false';
35
+
36
+ const omitDbConnection = (appEnv = {}) => {
37
+ const next = Object.assign({}, appEnv);
38
+ DB_CONNECTION_KEYS.forEach(key => delete next[key]);
39
+ return next;
40
+ };
41
+
26
42
  const buildDefaultAppDbEnv = (defaultAppDb = {}) => {
27
43
  const dialect = defaultAppDb.dialect || 'sqlite';
28
44
  const env = { DB_DIALECT: dialect };
@@ -59,10 +75,11 @@ const buildDefaultAppDbEnv = (defaultAppDb = {}) => {
59
75
  */
60
76
  const resolveAppDbEnv = ({ passthroughEnvKeys = [], appEnv = {}, defaultAppDb = null, port = null, includePort = false, appName = null, injectTablePrefix = true } = {}) => {
61
77
  const base = Object.assign({}, pickHostEnv(passthroughEnvKeys));
62
- if (defaultAppDb && !hasAppDbConfig(appEnv)) {
78
+ const useAppDb = isAppDbEnabled(appEnv || {});
79
+ if (defaultAppDb && !useAppDb) {
63
80
  Object.assign(base, buildDefaultAppDbEnv(defaultAppDb));
64
81
  }
65
- Object.assign(base, appEnv || {});
82
+ Object.assign(base, useAppDb ? appEnv || {} : omitDbConnection(appEnv || {}));
66
83
  if (includePort && port != null) {
67
84
  base.PORT = String(port);
68
85
  }
@@ -105,17 +122,17 @@ const isSecretKey = (key, pattern, secretKeys = []) => {
105
122
  return isPatternSecretKey(key, pattern);
106
123
  };
107
124
 
108
- // 由系统写入维护:不在接口中返回,也不接受外部新增 / 修改 / 删除
125
+ // 由系统写入维护:不在接口中返回,也不接受外部新增 / 修改 / 删除;宿主可通过 systemEnvKeys 选项追加
109
126
  const SYSTEM_ENV_KEYS = ['DB_TABLE_PREFIX'];
110
127
 
111
- const isSystemEnvKey = key => SYSTEM_ENV_KEYS.includes(key);
128
+ const isSystemEnvKey = (key, systemEnvKeys = []) => SYSTEM_ENV_KEYS.includes(key) || (systemEnvKeys || []).includes(key);
112
129
 
113
- const omitSystemEnv = (appEnv = {}) => Object.fromEntries(Object.entries(appEnv || {}).filter(([key]) => !isSystemEnvKey(key)));
130
+ const omitSystemEnv = (appEnv = {}, systemEnvKeys = []) => Object.fromEntries(Object.entries(appEnv || {}).filter(([key]) => !isSystemEnvKey(key, systemEnvKeys)));
114
131
 
115
- const maskEnvForResponse = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [] } = {}) => {
132
+ const maskEnvForResponse = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [], systemEnvKeys = [] } = {}) => {
116
133
  const masked = {};
117
134
  const explicit = normalizeSecretKeys(secretEnvKeys);
118
- for (const [key, value] of Object.entries(omitSystemEnv(appEnv))) {
135
+ for (const [key, value] of Object.entries(omitSystemEnv(appEnv, systemEnvKeys))) {
119
136
  if (isSecretKey(key, secretEnvKeyPattern, explicit)) {
120
137
  masked[key] = value == null || value === '' ? null : SECRET_MASK;
121
138
  } else {
@@ -131,11 +148,11 @@ const maskEnvForResponse = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys =
131
148
  * SECRET_MASK => keep existing (when key is secret by pattern or explicit list)
132
149
  * other => set
133
150
  */
134
- const applyEnvPatch = (current = {}, patch = {}, { secretEnvKeyPattern, secretEnvKeys = [] } = {}) => {
151
+ const applyEnvPatch = (current = {}, patch = {}, { secretEnvKeyPattern, secretEnvKeys = [], systemEnvKeys = [] } = {}) => {
135
152
  const next = Object.assign({}, current);
136
153
  const explicit = normalizeSecretKeys(secretEnvKeys);
137
154
  for (const [key, value] of Object.entries(patch || {})) {
138
- if (isSystemEnvKey(key)) {
155
+ if (isSystemEnvKey(key, systemEnvKeys)) {
139
156
  continue;
140
157
  }
141
158
  if (value === null) {
@@ -153,10 +170,10 @@ const applyEnvPatch = (current = {}, patch = {}, { secretEnvKeyPattern, secretEn
153
170
  /**
154
171
  * 响应中的密钥键列表 = 显式列表 ∪ 当前 env 中匹配 pattern 的键
155
172
  */
156
- const collectSecretKeys = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [] } = {}) => {
173
+ const collectSecretKeys = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [], systemEnvKeys = [] } = {}) => {
157
174
  const explicit = normalizeSecretKeys(secretEnvKeys);
158
175
  const fromPattern = Object.keys(appEnv || {}).filter(key => isPatternSecretKey(key, secretEnvKeyPattern));
159
- return normalizeSecretKeys([...explicit, ...fromPattern]).filter(key => !isSystemEnvKey(key));
176
+ return normalizeSecretKeys([...explicit, ...fromPattern]).filter(key => !isSystemEnvKey(key, systemEnvKeys));
160
177
  };
161
178
 
162
179
  const PM2_CONFIG_KEYS = ['exec_mode', 'instances', 'autorestart', 'max_memory_restart', 'max_restarts', 'min_uptime', 'kill_timeout', 'merge_logs'];
@@ -176,6 +193,8 @@ module.exports = {
176
193
  normalizeSecretKeys,
177
194
  pickHostEnv,
178
195
  hasAppDbConfig,
196
+ DB_DEDICATED_KEY,
197
+ isAppDbEnabled,
179
198
  buildDefaultAppDbEnv,
180
199
  resolveAppDbEnv,
181
200
  mergeEnv,
@@ -27,9 +27,11 @@ const rewriteSetCookiePath = (setCookie, mountPrefix) => {
27
27
  });
28
28
  };
29
29
 
30
- const TEXTUAL_TYPES = /text\/|javascript|json|xml|svg/i;
30
+ const TEXTUAL_TYPES = /text\/|json|xml|svg/i;
31
+ // 前端脚本已按注入的 runtimePublicUrl / runtimeApiUrl 自行拼前缀(如 axios baseURL + '/api/...'),再改写会得到 /app/x/app/x/api
32
+ const SCRIPT_TYPES = /javascript|ecmascript/i;
31
33
 
32
- const shouldRewriteBody = contentType => TEXTUAL_TYPES.test(contentType || '');
34
+ const shouldRewriteBody = contentType => TEXTUAL_TYPES.test(contentType || '') && !SCRIPT_TYPES.test(contentType || '');
33
35
 
34
36
  const rewriteBody = (body, mountPrefix, prefixes = ['/static', '/api', '/account']) => {
35
37
  if (!body || !mountPrefix) {
@@ -47,9 +49,32 @@ const rewriteBody = (body, mountPrefix, prefixes = ['/static', '/api', '/account
47
49
  return text;
48
50
  };
49
51
 
52
+ // 路径模式响应体经网关改写,ETag 带上改写规则版本,与子应用原始文件及旧规则下的缓存区分;改写规则变化时递增
53
+ const ETAG_SUFFIX = '-gw2';
54
+
55
+ const tagEtag = etag => (typeof etag === 'string' && etag.endsWith('"') ? `${etag.slice(0, -1)}${ETAG_SUFFIX}"` : etag);
56
+
57
+ // 只有本网关签发的 ETag 才还原后交给子应用协商缓存;其它(旧规则缓存)去掉条件头,强制返回完整内容
58
+ const restoreConditionalHeaders = headers => {
59
+ const next = { ...headers };
60
+ const tags = String(next['if-none-match'] || '')
61
+ .split(',')
62
+ .map(tag => tag.trim())
63
+ .filter(Boolean);
64
+ if (tags.length && tags.every(tag => tag.endsWith(`${ETAG_SUFFIX}"`))) {
65
+ next['if-none-match'] = tags.map(tag => `${tag.slice(0, -(ETAG_SUFFIX.length + 1))}"`).join(', ');
66
+ } else {
67
+ delete next['if-none-match'];
68
+ delete next['if-modified-since'];
69
+ }
70
+ return next;
71
+ };
72
+
50
73
  module.exports = {
51
74
  rewriteLocation,
52
75
  rewriteSetCookiePath,
53
76
  shouldRewriteBody,
54
- rewriteBody
77
+ rewriteBody,
78
+ tagEtag,
79
+ restoreConditionalHeaders
55
80
  };
@@ -33,8 +33,10 @@ const runNpmInstall = (cwd, timeoutMs) =>
33
33
  });
34
34
  });
35
35
 
36
- const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, maxZipEntries, npmInstallTimeoutMs, sqlPath }) => {
36
+ /** install=false 时只解压校验,不装依赖(用于不常驻磁盘的版本上传) */
37
+ const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, maxZipEntries, npmInstallTimeoutMs, sqlPath, install = true, onProgress }) => {
37
38
  await fs.emptyDir(artifactPath);
39
+ onProgress && onProgress('extract');
38
40
  const extractDir = path.join(artifactPath, '_extract');
39
41
  await extractZipSafe(zipBuffer, extractDir, { maxZipSize, maxZipEntries });
40
42
  const packageRoot = await resolvePackageRoot(extractDir);
@@ -60,7 +62,10 @@ const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, max
60
62
  });
61
63
 
62
64
  const serverDir = path.join(artifactPath, 'server');
63
- await runNpmInstall(serverDir, npmInstallTimeoutMs);
65
+ if (install) {
66
+ onProgress && onProgress('install');
67
+ await runNpmInstall(serverDir, npmInstallTimeoutMs);
68
+ }
64
69
 
65
70
  const sqlDir = path.join(serverDir, sqlPath || 'sql');
66
71
  const sqlFiles = await listSqlFiles(sqlDir);
@@ -0,0 +1,33 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const { listSqlFiles } = require('./validatePackage');
4
+
5
+ /** 读取目录内的 .sql 为 [{ name, content, updatedAt }] */
6
+ const readSqlDir = async sqlDir => {
7
+ const files = [];
8
+ for (const name of await listSqlFiles(sqlDir)) {
9
+ const filePath = path.join(sqlDir, name);
10
+ const [content, stat] = await Promise.all([fs.readFile(filePath, 'utf8'), fs.stat(filePath)]);
11
+ files.push({ name, content, updatedAt: stat.mtime.toISOString() });
12
+ }
13
+ return files;
14
+ };
15
+
16
+ /** 以 files 为准覆盖目录内的 .sql:删掉多余的,写入全部 */
17
+ const writeSqlDir = async (sqlDir, files) => {
18
+ await fs.ensureDir(sqlDir);
19
+ const keep = new Set(files.map(item => item.name));
20
+ for (const name of await listSqlFiles(sqlDir)) {
21
+ if (!keep.has(name)) {
22
+ await fs.remove(path.join(sqlDir, name));
23
+ }
24
+ }
25
+ for (const item of files) {
26
+ await fs.writeFile(path.join(sqlDir, item.name), item.content || '', 'utf8');
27
+ }
28
+ };
29
+
30
+ module.exports = {
31
+ readSqlDir,
32
+ writeSqlDir
33
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kne/fastify-app-manager",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Fastify 单机应用市场插件:版本上传、PM2 托管、路径/域名反代与日志 SSE",
5
5
  "main": "index.js",
6
6
  "scripts": {