colonynote 0.0.0 → 1.0.0-beta.10
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/LICENSE +21 -0
- package/README.md +125 -0
- package/README.zh.md +125 -0
- package/bin/colonynote.js +185 -0
- package/dist/client/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/dist/client/assets/_basePickBy-CpIFiPmc.js +1 -0
- package/dist/client/assets/_baseUniq-C06JezLB.js +1 -0
- package/dist/client/assets/arc-CS-lnk5Q.js +1 -0
- package/dist/client/assets/architectureDiagram-2XIMDMQ5-34IaPMNX.js +36 -0
- package/dist/client/assets/blockDiagram-WCTKOSBZ-B1FE-gTT.js +132 -0
- package/dist/client/assets/c4Diagram-IC4MRINW-g2IVuQJv.js +10 -0
- package/dist/client/assets/channel-DXsl4vhs.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-C9NhnJ5n.js +1 -0
- package/dist/client/assets/chunk-55IACEB6-CG_ik_rT.js +1 -0
- package/dist/client/assets/chunk-FMBD7UC4-CGeJa0Td.js +15 -0
- package/dist/client/assets/chunk-JSJVCQXG-wTqBaIzj.js +1 -0
- package/dist/client/assets/chunk-KX2RTZJC-DeSMkxPM.js +1 -0
- package/dist/client/assets/chunk-NQ4KR5QH-CsNzZMQ3.js +220 -0
- package/dist/client/assets/chunk-QZHKN3VN-DyrAfKbs.js +1 -0
- package/dist/client/assets/chunk-WL4C6EOR-DQcdvkib.js +189 -0
- package/dist/client/assets/classDiagram-VBA2DB6C-M0yhB_li.js +1 -0
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-M0yhB_li.js +1 -0
- package/dist/client/assets/clone-DY0FLUFX.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-DXvrsg79.js +1 -0
- package/dist/client/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/dist/client/assets/dagre-KLK3FWXG-I4ZL-Yjk.js +4 -0
- package/dist/client/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/client/assets/diagram-E7M64L7V-B5x_qiUv.js +24 -0
- package/dist/client/assets/diagram-IFDJBPK2-B3aH6gaM.js +43 -0
- package/dist/client/assets/diagram-P4PSJMXO-C65hl65m.js +24 -0
- package/dist/client/assets/erDiagram-INFDFZHY-CuGoj23m.js +70 -0
- package/dist/client/assets/flowDiagram-PKNHOUZH-BgI_bQaB.js +162 -0
- package/dist/client/assets/ganttDiagram-A5KZAMGK-D5fb9XZy.js +292 -0
- package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-BQoXcJsF.js +65 -0
- package/dist/client/assets/graph-CGDYVHRC.js +1 -0
- package/dist/client/assets/index-CI8fw8c4.js +709 -0
- package/dist/client/assets/index-D2TYNMWH.css +1 -0
- package/dist/client/assets/infoDiagram-LFFYTUFH-Cua6joOv.js +2 -0
- package/dist/client/assets/init-Gi6I4Gst.js +1 -0
- package/dist/client/assets/ishikawaDiagram-PHBUUO56-DuT5_xy8.js +70 -0
- package/dist/client/assets/journeyDiagram-4ABVD52K-My7-KJcV.js +139 -0
- package/dist/client/assets/kanban-definition-K7BYSVSG-ftVPxOyK.js +89 -0
- package/dist/client/assets/katex-B1X10hvy.js +261 -0
- package/dist/client/assets/layout-DhQ7R7Ms.js +1 -0
- package/dist/client/assets/linear-BIb05kGQ.js +1 -0
- package/dist/client/assets/mindmap-definition-YRQLILUH-DwD8YJaZ.js +68 -0
- package/dist/client/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/client/assets/pieDiagram-SKSYHLDU-C-17c64G.js +30 -0
- package/dist/client/assets/quadrantDiagram-337W2JSQ-kt-KJQMs.js +7 -0
- package/dist/client/assets/requirementDiagram-Z7DCOOCP-BH89qJx9.js +73 -0
- package/dist/client/assets/sankeyDiagram-WA2Y5GQK-CAokuuka.js +10 -0
- package/dist/client/assets/sequenceDiagram-2WXFIKYE-CeVpL_7D.js +145 -0
- package/dist/client/assets/stateDiagram-RAJIS63D-DdqWd2rm.js +1 -0
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-B83j0Cq2.js +1 -0
- package/dist/client/assets/timeline-definition-YZTLITO2-BnlRy3AK.js +61 -0
- package/dist/client/assets/treemap-KZPCXAKY-xwXQmLuw.js +162 -0
- package/dist/client/assets/vennDiagram-LZ73GAT5-C2MLaHWE.js +34 -0
- package/dist/client/assets/xychartDiagram-JWTSCODW-BiTzOgXT.js +7 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/index.html +48 -0
- package/dist/client/logo.png +0 -0
- package/dist/config.js +115 -0
- package/dist/server/api.js +266 -0
- package/dist/server/app.js +21 -0
- package/dist/server/ignore.js +210 -0
- package/dist/server/index.js +107 -0
- package/dist/server/watcher.js +47 -0
- package/package.json +102 -10
- package/index.js +0 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { saveUserConfig } from '../config.js';
|
|
5
|
+
function isAllowed(pathStr, config) {
|
|
6
|
+
const resolved = path.resolve(pathStr);
|
|
7
|
+
return resolved.startsWith(path.resolve(config.root));
|
|
8
|
+
}
|
|
9
|
+
function hasAllowedExtension(filename, extensions) {
|
|
10
|
+
const ext = path.extname(filename).toLowerCase();
|
|
11
|
+
return extensions.includes(ext);
|
|
12
|
+
}
|
|
13
|
+
async function walkDirectory(dir, config, matcher) {
|
|
14
|
+
const nodes = [];
|
|
15
|
+
try {
|
|
16
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const fullPath = path.join(dir, entry.name);
|
|
19
|
+
const isDir = entry.isDirectory();
|
|
20
|
+
if (!config.showHiddenFiles && entry.name.startsWith('.'))
|
|
21
|
+
continue;
|
|
22
|
+
if (matcher.isIgnored(fullPath, isDir))
|
|
23
|
+
continue;
|
|
24
|
+
if (isDir) {
|
|
25
|
+
const children = await walkDirectory(fullPath, config, matcher);
|
|
26
|
+
nodes.push({
|
|
27
|
+
name: entry.name,
|
|
28
|
+
path: fullPath.replace(config.root, '').replace(/\\/g, '/'),
|
|
29
|
+
type: 'directory',
|
|
30
|
+
children,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else if (entry.isFile()) {
|
|
34
|
+
if (hasAllowedExtension(entry.name, config.allowedExtensions)) {
|
|
35
|
+
nodes.push({
|
|
36
|
+
name: entry.name,
|
|
37
|
+
path: fullPath.replace(config.root, '').replace(/\\/g, '/'),
|
|
38
|
+
type: 'file',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
// ignore errors
|
|
46
|
+
}
|
|
47
|
+
nodes.sort((a, b) => {
|
|
48
|
+
if (a.type === 'directory' && b.type === 'file')
|
|
49
|
+
return -1;
|
|
50
|
+
if (a.type === 'file' && b.type === 'directory')
|
|
51
|
+
return 1;
|
|
52
|
+
return a.name.localeCompare(b.name);
|
|
53
|
+
});
|
|
54
|
+
return nodes;
|
|
55
|
+
}
|
|
56
|
+
export function createFileRouter(config, matcher) {
|
|
57
|
+
const router = new Hono();
|
|
58
|
+
router.get('/config', async (c) => {
|
|
59
|
+
return c.json({
|
|
60
|
+
showHiddenFiles: config.showHiddenFiles,
|
|
61
|
+
allowedExtensions: config.allowedExtensions,
|
|
62
|
+
ignore: config.ignore,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
router.patch('/config', async (c) => {
|
|
66
|
+
try {
|
|
67
|
+
const body = await c.req.json();
|
|
68
|
+
const allowedFields = ['showHiddenFiles', 'allowedExtensions', 'ignore'];
|
|
69
|
+
const updates = {};
|
|
70
|
+
for (const key of allowedFields) {
|
|
71
|
+
if (key in body) {
|
|
72
|
+
updates[key] = body[key];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
saveUserConfig(config.root, updates);
|
|
76
|
+
if (typeof updates.showHiddenFiles === 'boolean') {
|
|
77
|
+
config.showHiddenFiles = updates.showHiddenFiles;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(updates.allowedExtensions)) {
|
|
80
|
+
config.allowedExtensions = updates.allowedExtensions;
|
|
81
|
+
}
|
|
82
|
+
if (updates.ignore) {
|
|
83
|
+
if (typeof updates.ignore.enableIgnoreFiles === 'boolean') {
|
|
84
|
+
config.ignore.enableIgnoreFiles = updates.ignore.enableIgnoreFiles;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(updates.ignore.ignoreFileNames)) {
|
|
87
|
+
config.ignore.ignoreFileNames = updates.ignore.ignoreFileNames;
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(updates.ignore.patterns)) {
|
|
90
|
+
config.ignore.patterns = updates.ignore.patterns;
|
|
91
|
+
matcher.updateGlobalPatterns(updates.ignore.patterns);
|
|
92
|
+
}
|
|
93
|
+
matcher.clearCache();
|
|
94
|
+
}
|
|
95
|
+
return c.json({ success: true, config: { showHiddenFiles: config.showHiddenFiles, allowedExtensions: config.allowedExtensions, ignore: config.ignore } });
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error('Failed to update config:', e);
|
|
99
|
+
return c.json({ error: 'Failed to update config' }, 500);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
router.get('/', async (c) => {
|
|
103
|
+
try {
|
|
104
|
+
const files = await walkDirectory(config.root, config, matcher);
|
|
105
|
+
return c.json({ files });
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return c.json({ error: 'Failed to read directory' }, 500);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
router.get('/content', async (c) => {
|
|
112
|
+
const pathsParam = c.req.query('paths');
|
|
113
|
+
if (!pathsParam) {
|
|
114
|
+
return c.json({ error: 'paths parameter is required' }, 400);
|
|
115
|
+
}
|
|
116
|
+
const paths = pathsParam.split(',').filter(Boolean);
|
|
117
|
+
const results = [];
|
|
118
|
+
for (const filePath of paths) {
|
|
119
|
+
const fullPath = path.join(config.root, filePath);
|
|
120
|
+
if (!isAllowed(fullPath, config)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const stat = await fs.stat(fullPath);
|
|
125
|
+
if (stat.isFile()) {
|
|
126
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
127
|
+
results.push({
|
|
128
|
+
path: filePath,
|
|
129
|
+
name: path.basename(filePath),
|
|
130
|
+
content,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
// skip files that can't be read
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return c.json({ files: results });
|
|
139
|
+
});
|
|
140
|
+
router.get('/*', async (c) => {
|
|
141
|
+
const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
|
|
142
|
+
const fullPath = path.join(config.root, filePath);
|
|
143
|
+
if (!isAllowed(fullPath, config)) {
|
|
144
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const stat = await fs.stat(fullPath);
|
|
148
|
+
if (stat.isDirectory()) {
|
|
149
|
+
const files = await walkDirectory(fullPath, config, matcher);
|
|
150
|
+
return c.json({ files });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
// not a directory
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
158
|
+
return c.text(content);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
return c.json({ error: 'File not found' }, 404);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
router.post('/*', async (c) => {
|
|
165
|
+
const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
|
|
166
|
+
const fullPath = path.join(config.root, filePath);
|
|
167
|
+
if (!isAllowed(fullPath, config)) {
|
|
168
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
169
|
+
}
|
|
170
|
+
const contentType = c.req.header('Content-Type') || '';
|
|
171
|
+
if (contentType.includes('application/json')) {
|
|
172
|
+
const body = await c.req.json();
|
|
173
|
+
if (body.type === 'create') {
|
|
174
|
+
const parentPath = body.parentPath || '';
|
|
175
|
+
const name = body.name;
|
|
176
|
+
const targetPath = path.join(config.root, parentPath, name);
|
|
177
|
+
if (!isAllowed(targetPath, config)) {
|
|
178
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
if (body.isDirectory) {
|
|
182
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
if (!hasAllowedExtension(name, config.allowedExtensions)) {
|
|
186
|
+
return c.json({ error: 'File type not allowed' }, 400);
|
|
187
|
+
}
|
|
188
|
+
await fs.writeFile(targetPath, '', 'utf-8');
|
|
189
|
+
}
|
|
190
|
+
return c.json({ success: true, path: targetPath.replace(config.root, '') });
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
return c.json({ error: 'Failed to create' }, 500);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
199
|
+
const content = await c.req.text();
|
|
200
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
201
|
+
return c.json({ success: true });
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
return c.json({ error: 'Failed to save file' }, 500);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
router.put('/*', async (c) => {
|
|
208
|
+
const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
|
|
209
|
+
const fullPath = path.join(config.root, filePath);
|
|
210
|
+
if (!isAllowed(fullPath, config)) {
|
|
211
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const body = await c.req.json();
|
|
215
|
+
const { oldPath, newPath, isDirectory } = body;
|
|
216
|
+
if (!oldPath || !newPath) {
|
|
217
|
+
return c.json({ error: 'oldPath and newPath are required' }, 400);
|
|
218
|
+
}
|
|
219
|
+
const oldFullPath = path.join(config.root, oldPath);
|
|
220
|
+
const newFullPath = path.join(config.root, newPath);
|
|
221
|
+
if (!isAllowed(oldFullPath, config) || !isAllowed(newFullPath, config)) {
|
|
222
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
223
|
+
}
|
|
224
|
+
if (isDirectory) {
|
|
225
|
+
const stat = await fs.stat(oldFullPath);
|
|
226
|
+
if (!stat.isDirectory()) {
|
|
227
|
+
return c.json({ error: 'Source is not a directory' }, 400);
|
|
228
|
+
}
|
|
229
|
+
await fs.rename(oldFullPath, newFullPath);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const ext = path.extname(newPath).toLowerCase();
|
|
233
|
+
if (!hasAllowedExtension(newPath, config.allowedExtensions)) {
|
|
234
|
+
return c.json({ error: 'File type not allowed' }, 400);
|
|
235
|
+
}
|
|
236
|
+
await fs.rename(oldFullPath, newFullPath);
|
|
237
|
+
}
|
|
238
|
+
return c.json({ success: true, newPath });
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
console.error('Rename/Move error:', e);
|
|
242
|
+
return c.json({ error: 'Failed to rename or move' }, 500);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
router.delete('/*', async (c) => {
|
|
246
|
+
const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
|
|
247
|
+
const fullPath = path.join(config.root, filePath);
|
|
248
|
+
if (!isAllowed(fullPath, config)) {
|
|
249
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const stat = await fs.stat(fullPath);
|
|
253
|
+
if (stat.isDirectory()) {
|
|
254
|
+
await fs.rm(fullPath, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
await fs.unlink(fullPath);
|
|
258
|
+
}
|
|
259
|
+
return c.json({ success: true });
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
return c.json({ error: 'Failed to delete' }, 500);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return router;
|
|
266
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
import { createFileRouter } from './api.js';
|
|
4
|
+
import { IgnoreMatcher } from './ignore.js';
|
|
5
|
+
export function createApp(config) {
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
const ignoreConfig = {
|
|
8
|
+
enableIgnoreFiles: config.ignore.enableIgnoreFiles,
|
|
9
|
+
ignoreFileNames: config.ignore.ignoreFileNames,
|
|
10
|
+
globalPatterns: config.ignore.patterns,
|
|
11
|
+
};
|
|
12
|
+
const matcher = new IgnoreMatcher(config.root, ignoreConfig);
|
|
13
|
+
app.use('*', cors());
|
|
14
|
+
app.use('*', async (c, next) => {
|
|
15
|
+
c.set('config', config);
|
|
16
|
+
await next();
|
|
17
|
+
});
|
|
18
|
+
const fileRouter = createFileRouter(config, matcher);
|
|
19
|
+
app.route('/api/files', fileRouter);
|
|
20
|
+
return { app, matcher };
|
|
21
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { minimatch } from 'minimatch';
|
|
4
|
+
/**
|
|
5
|
+
* 默认忽略配置
|
|
6
|
+
*/
|
|
7
|
+
export const defaultIgnoreConfig = {
|
|
8
|
+
enableIgnoreFiles: true,
|
|
9
|
+
ignoreFileNames: ['.colonynoteignore', '.gitignore'],
|
|
10
|
+
globalPatterns: [],
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 忽略匹配器 - 管理所有忽略规则并提供匹配功能
|
|
14
|
+
*/
|
|
15
|
+
export class IgnoreMatcher {
|
|
16
|
+
ignoreFiles = new Map();
|
|
17
|
+
globalRules = [];
|
|
18
|
+
rootPath;
|
|
19
|
+
config;
|
|
20
|
+
constructor(rootPath, config = defaultIgnoreConfig) {
|
|
21
|
+
this.rootPath = path.resolve(rootPath);
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.globalRules = this.parsePatterns(config.globalPatterns, this.rootPath);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 解析忽略模式字符串为规则对象
|
|
27
|
+
*/
|
|
28
|
+
parsePatterns(patterns, basePath) {
|
|
29
|
+
const rules = [];
|
|
30
|
+
for (const rawPattern of patterns) {
|
|
31
|
+
// 跳过空行和注释
|
|
32
|
+
const trimmed = rawPattern.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
34
|
+
continue;
|
|
35
|
+
const isNegation = trimmed.startsWith('!');
|
|
36
|
+
const isDirectory = trimmed.endsWith('/');
|
|
37
|
+
// 处理模式字符串
|
|
38
|
+
let pattern = trimmed;
|
|
39
|
+
// 处理否定模式:移除开头的 !
|
|
40
|
+
if (isNegation) {
|
|
41
|
+
pattern = pattern.slice(1);
|
|
42
|
+
}
|
|
43
|
+
// 处理目录标记:暂时保留,匹配时再处理
|
|
44
|
+
const cleanPattern = isDirectory ? pattern.slice(0, -1) : pattern;
|
|
45
|
+
// 判断是否为绝对路径模式(以 / 开头)
|
|
46
|
+
const isAbsolute = cleanPattern.startsWith('/');
|
|
47
|
+
const rule = {
|
|
48
|
+
pattern: cleanPattern,
|
|
49
|
+
rawPattern: trimmed,
|
|
50
|
+
isNegation,
|
|
51
|
+
isDirectory,
|
|
52
|
+
basePath,
|
|
53
|
+
isAbsolute,
|
|
54
|
+
};
|
|
55
|
+
rules.push(rule);
|
|
56
|
+
}
|
|
57
|
+
return rules;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 加载指定路径的 .ignore 文件
|
|
61
|
+
*/
|
|
62
|
+
loadIgnoreFile(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
if (!fs.existsSync(filePath))
|
|
65
|
+
return false;
|
|
66
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
67
|
+
const lines = content.split(/\r?\n/);
|
|
68
|
+
const basePath = path.dirname(filePath);
|
|
69
|
+
const ignoreFile = {
|
|
70
|
+
filePath,
|
|
71
|
+
basePath,
|
|
72
|
+
rules: this.parsePatterns(lines, basePath),
|
|
73
|
+
};
|
|
74
|
+
this.ignoreFiles.set(filePath, ignoreFile);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error(`Failed to load ignore file ${filePath}:`, e);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 就近查找并加载 .ignore 文件
|
|
84
|
+
* 从 targetPath 向上查找,直到 rootPath
|
|
85
|
+
*/
|
|
86
|
+
findAndLoadIgnoreFiles(targetPath) {
|
|
87
|
+
if (!this.config.enableIgnoreFiles)
|
|
88
|
+
return;
|
|
89
|
+
let currentPath = path.resolve(targetPath);
|
|
90
|
+
// 如果是文件,从其父目录开始查找
|
|
91
|
+
try {
|
|
92
|
+
const stat = fs.statSync(currentPath);
|
|
93
|
+
if (stat.isFile()) {
|
|
94
|
+
currentPath = path.dirname(currentPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// 路径不存在,假设是目录
|
|
99
|
+
}
|
|
100
|
+
while (currentPath.startsWith(this.rootPath) && currentPath.length >= this.rootPath.length) {
|
|
101
|
+
for (const fileName of this.config.ignoreFileNames) {
|
|
102
|
+
const ignoreFilePath = path.join(currentPath, fileName);
|
|
103
|
+
// 避免重复加载
|
|
104
|
+
if (!this.ignoreFiles.has(ignoreFilePath)) {
|
|
105
|
+
this.loadIgnoreFile(ignoreFilePath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 向上一级目录
|
|
109
|
+
const parentPath = path.dirname(currentPath);
|
|
110
|
+
if (parentPath === currentPath)
|
|
111
|
+
break; // 已到达根目录
|
|
112
|
+
currentPath = parentPath;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 检查单个规则是否匹配目标路径
|
|
117
|
+
*/
|
|
118
|
+
matchRule(rule, targetPath, isDirectory) {
|
|
119
|
+
const relativePath = path.relative(rule.basePath, targetPath);
|
|
120
|
+
if (rule.isAbsolute) {
|
|
121
|
+
const patternWithoutSlash = rule.pattern.replace(/^\//, '');
|
|
122
|
+
if (relativePath === patternWithoutSlash ||
|
|
123
|
+
relativePath.startsWith(patternWithoutSlash + path.sep)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return minimatch(relativePath, patternWithoutSlash, { dot: true });
|
|
127
|
+
}
|
|
128
|
+
if (!rule.pattern.includes('/')) {
|
|
129
|
+
const basename = path.basename(targetPath);
|
|
130
|
+
if (rule.isDirectory) {
|
|
131
|
+
if (minimatch(basename, rule.pattern, { dot: true })) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const parts = relativePath.split(path.sep);
|
|
135
|
+
for (let i = 0; i < parts.length; i++) {
|
|
136
|
+
if (minimatch(parts[i], rule.pattern, { dot: true })) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
if (minimatch(basename, rule.pattern, { dot: true })) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return minimatch(relativePath, rule.pattern, { dot: true });
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 检查路径是否被忽略
|
|
152
|
+
* 返回 true 表示应该忽略,false 表示不忽略
|
|
153
|
+
*/
|
|
154
|
+
isIgnored(targetPath, isDirectory = false) {
|
|
155
|
+
const absolutePath = path.resolve(targetPath);
|
|
156
|
+
// 确保路径在 root 内
|
|
157
|
+
if (!absolutePath.startsWith(this.rootPath))
|
|
158
|
+
return false;
|
|
159
|
+
// 就近查找并加载 .ignore 文件
|
|
160
|
+
this.findAndLoadIgnoreFiles(absolutePath);
|
|
161
|
+
// 收集所有相关规则(按优先级排序)
|
|
162
|
+
// 规则优先级:就近的 .ignore 文件 > 上层的 .ignore 文件 > 全局配置
|
|
163
|
+
const allRules = [];
|
|
164
|
+
// 1. 添加全局规则(最低优先级)
|
|
165
|
+
allRules.push(...this.globalRules);
|
|
166
|
+
// 2. 添加 .ignore 文件规则(按目录层级从根到当前)
|
|
167
|
+
const sortedIgnoreFiles = Array.from(this.ignoreFiles.values())
|
|
168
|
+
.sort((a, b) => {
|
|
169
|
+
// 按 basePath 深度排序:根目录优先级低,近目录优先级高
|
|
170
|
+
const depthA = a.basePath.split(path.sep).length;
|
|
171
|
+
const depthB = b.basePath.split(path.sep).length;
|
|
172
|
+
return depthA - depthB;
|
|
173
|
+
});
|
|
174
|
+
for (const ignoreFile of sortedIgnoreFiles) {
|
|
175
|
+
// 只考虑影响当前路径的规则文件
|
|
176
|
+
if (absolutePath.startsWith(ignoreFile.basePath)) {
|
|
177
|
+
allRules.push(...ignoreFile.rules);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 3. 应用规则(后出现的规则优先级更高)
|
|
181
|
+
let shouldIgnore = false;
|
|
182
|
+
for (const rule of allRules) {
|
|
183
|
+
if (this.matchRule(rule, absolutePath, isDirectory)) {
|
|
184
|
+
// 否定规则取消忽略
|
|
185
|
+
if (rule.isNegation) {
|
|
186
|
+
shouldIgnore = false;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
shouldIgnore = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return shouldIgnore;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 清除缓存的 .ignore 文件(用于重新加载)
|
|
197
|
+
*/
|
|
198
|
+
clearCache() {
|
|
199
|
+
this.ignoreFiles.clear();
|
|
200
|
+
}
|
|
201
|
+
updateGlobalPatterns(patterns) {
|
|
202
|
+
this.globalRules = this.parsePatterns(patterns, this.rootPath);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 获取已加载的 .ignore 文件列表
|
|
206
|
+
*/
|
|
207
|
+
getLoadedIgnoreFiles() {
|
|
208
|
+
return Array.from(this.ignoreFiles.keys());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { cors } from 'hono/cors';
|
|
4
|
+
import { createFileRouter } from './api.js';
|
|
5
|
+
import { loadConfig } from '../config.js';
|
|
6
|
+
import { setupWatcher } from './watcher.js';
|
|
7
|
+
import { IgnoreMatcher } from './ignore.js';
|
|
8
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const clientDir = path.join(__dirname, '..', 'client');
|
|
14
|
+
async function main() {
|
|
15
|
+
const config = await loadConfig();
|
|
16
|
+
const matcher = new IgnoreMatcher(config.root, {
|
|
17
|
+
enableIgnoreFiles: config.ignore.enableIgnoreFiles,
|
|
18
|
+
ignoreFileNames: config.ignore.ignoreFileNames,
|
|
19
|
+
globalPatterns: config.ignore.patterns,
|
|
20
|
+
});
|
|
21
|
+
const app = new Hono();
|
|
22
|
+
app.use('*', cors());
|
|
23
|
+
const fileRouter = createFileRouter(config, matcher);
|
|
24
|
+
app.route('/api/files', fileRouter);
|
|
25
|
+
app.get('/assets/*', async (c) => {
|
|
26
|
+
const filePath = c.req.path.replace('/assets', '');
|
|
27
|
+
const fullPath = path.join(clientDir, 'assets', filePath);
|
|
28
|
+
try {
|
|
29
|
+
const content = fs.readFileSync(fullPath);
|
|
30
|
+
const ext = path.extname(filePath);
|
|
31
|
+
const contentType = ext === '.js' ? 'application/javascript' : ext === '.css' ? 'text/css' : 'text/plain';
|
|
32
|
+
return new Response(content, { headers: { 'Content-Type': contentType } });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return c.notFound();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
app.get('/logo.png', async (c) => {
|
|
39
|
+
const fullPath = path.join(clientDir, 'logo.png');
|
|
40
|
+
try {
|
|
41
|
+
const content = fs.readFileSync(fullPath);
|
|
42
|
+
return new Response(content, { headers: { 'Content-Type': 'image/png' } });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return c.notFound();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
app.get('/favicon.ico', async (c) => {
|
|
49
|
+
const fullPath = path.join(clientDir, 'favicon.ico');
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(fullPath);
|
|
52
|
+
return new Response(content, { headers: { 'Content-Type': 'image/x-icon' } });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return c.notFound();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
app.get('*', async (c) => {
|
|
59
|
+
const indexPath = path.join(clientDir, 'index.html');
|
|
60
|
+
try {
|
|
61
|
+
const content = fs.readFileSync(indexPath, 'utf-8');
|
|
62
|
+
return new Response(content, {
|
|
63
|
+
headers: { 'Content-Type': 'text/html' },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return c.notFound();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
const server = serve({
|
|
71
|
+
fetch: app.fetch,
|
|
72
|
+
port: config.port,
|
|
73
|
+
hostname: config.host,
|
|
74
|
+
});
|
|
75
|
+
const clients = new Set();
|
|
76
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
77
|
+
wss.on('connection', (ws) => {
|
|
78
|
+
clients.add(ws);
|
|
79
|
+
ws.on('close', () => clients.delete(ws));
|
|
80
|
+
});
|
|
81
|
+
server.on('upgrade', (request, socket, head) => {
|
|
82
|
+
if (request.url === '/ws') {
|
|
83
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
84
|
+
wss.emit('connection', ws, request);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
setupWatcher(config, matcher, {
|
|
89
|
+
onFileChange: (event, filePath) => {
|
|
90
|
+
const relativePath = filePath.replace(config.root, '');
|
|
91
|
+
const message = JSON.stringify({ type: 'file:change', event, path: relativePath });
|
|
92
|
+
clients.forEach((client) => {
|
|
93
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
94
|
+
client.send(message);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
console.log(`\n ColonyNote is running!\n`);
|
|
100
|
+
console.log(` Local: http://localhost:${config.port}`);
|
|
101
|
+
console.log(` Network: http://${config.host}:${config.port}`);
|
|
102
|
+
console.log(` Root: ${config.root}\n`);
|
|
103
|
+
}
|
|
104
|
+
main().catch((e) => {
|
|
105
|
+
console.error('Failed to start:', e);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
export function setupWatcher(config, matcher, callbacks) {
|
|
4
|
+
const watcher = chokidar.watch(config.root, {
|
|
5
|
+
ignored: (filePath) => {
|
|
6
|
+
if (!config.showHiddenFiles && (filePath.includes('/.') || filePath.startsWith('.')))
|
|
7
|
+
return true;
|
|
8
|
+
try {
|
|
9
|
+
const stat = fs.statSync(filePath);
|
|
10
|
+
if (matcher.isIgnored(filePath, stat.isDirectory()))
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
if (matcher.isIgnored(filePath, false))
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
|
18
|
+
if (ext && !config.allowedExtensions.includes('.' + ext) && !filePath.includes('/')) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
},
|
|
23
|
+
persistent: true,
|
|
24
|
+
ignoreInitial: true,
|
|
25
|
+
depth: 99,
|
|
26
|
+
});
|
|
27
|
+
watcher
|
|
28
|
+
.on('add', (path) => {
|
|
29
|
+
if (config.allowedExtensions.some(ext => path.endsWith(ext))) {
|
|
30
|
+
callbacks.onFileChange('add', path);
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.on('change', (path) => {
|
|
34
|
+
if (config.allowedExtensions.some(ext => path.endsWith(ext))) {
|
|
35
|
+
callbacks.onFileChange('change', path);
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.on('unlink', (path) => {
|
|
39
|
+
if (config.allowedExtensions.some(ext => path.endsWith(ext))) {
|
|
40
|
+
callbacks.onFileChange('unlink', path);
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.on('addDir', (path) => callbacks.onFileChange('addDir', path))
|
|
44
|
+
.on('unlinkDir', (path) => callbacks.onFileChange('unlinkDir', path))
|
|
45
|
+
.on('error', (error) => console.error('Watcher error:', error));
|
|
46
|
+
return watcher;
|
|
47
|
+
}
|