colonynote 1.0.0-beta.10 → 1.0.0-beta.13

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/bin/colonynote.js +40 -78
  2. package/dist/client/assets/_baseUniq-9Pn_qMNx.js +1 -0
  3. package/dist/client/assets/{arc-CS-lnk5Q.js → arc-CwgOuPiQ.js} +1 -1
  4. package/dist/client/assets/architectureDiagram-Q4EWVU46-DH8zBsFD.js +36 -0
  5. package/dist/client/assets/{blockDiagram-WCTKOSBZ-B1FE-gTT.js → blockDiagram-DXYQGD6D-Rinexrq-.js} +6 -6
  6. package/dist/client/assets/c4Diagram-AHTNJAMY-DUJE4tY8.js +10 -0
  7. package/dist/client/assets/channel-BJqbnDhj.js +1 -0
  8. package/dist/client/assets/{chunk-4BX2VUAB-C9NhnJ5n.js → chunk-4BX2VUAB-CVubEDEM.js} +1 -1
  9. package/dist/client/assets/chunk-4TB4RGXK-GtceFh5R.js +206 -0
  10. package/dist/client/assets/{chunk-55IACEB6-CG_ik_rT.js → chunk-55IACEB6-BvQuQjO3.js} +1 -1
  11. package/dist/client/assets/{chunk-KX2RTZJC-DeSMkxPM.js → chunk-EDXVE4YY-DwTEXUMb.js} +1 -1
  12. package/dist/client/assets/{chunk-FMBD7UC4-CGeJa0Td.js → chunk-FMBD7UC4-CP_VCEId.js} +1 -1
  13. package/dist/client/assets/chunk-OYMX7WX6-DekTZ61_.js +231 -0
  14. package/dist/client/assets/{chunk-QZHKN3VN-DyrAfKbs.js → chunk-QZHKN3VN-DaQazFvi.js} +1 -1
  15. package/dist/client/assets/{chunk-JSJVCQXG-wTqBaIzj.js → chunk-YZCP3GAM-DkNfaXLf.js} +1 -1
  16. package/dist/client/assets/classDiagram-6PBFFD2Q-DvENVsHG.js +1 -0
  17. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DvENVsHG.js +1 -0
  18. package/dist/client/assets/clone-Ge_jPAPG.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-S5V4N54A-DXvrsg79.js → cose-bilkent-S5V4N54A-Db3Okii7.js} +1 -1
  20. package/dist/client/assets/{cytoscape.esm-BQaXIfA_.js → cytoscape.esm-DxGcaOPV.js} +4 -4
  21. package/dist/client/assets/dagre-KV5264BT-CWRMd3SH.js +4 -0
  22. package/dist/client/assets/diagram-5BDNPKRD-8TiBcRv9.js +10 -0
  23. package/dist/client/assets/diagram-G4DWMVQ6-Djb_aFKY.js +24 -0
  24. package/dist/client/assets/diagram-MMDJMWI5-DhCslint.js +43 -0
  25. package/dist/client/assets/diagram-TYMM5635-DhePsl7z.js +24 -0
  26. package/dist/client/assets/erDiagram-SMLLAGMA-COaIktX_.js +85 -0
  27. package/dist/client/assets/flowDiagram-DWJPFMVM-BiDAMluS.js +162 -0
  28. package/dist/client/assets/{ganttDiagram-A5KZAMGK-D5fb9XZy.js → ganttDiagram-T4ZO3ILL-Dl4AW2On.js} +4 -4
  29. package/dist/client/assets/gitGraphDiagram-UUTBAWPF-Dohcub9p.js +106 -0
  30. package/dist/client/assets/graph-C0KifbR6.js +1 -0
  31. package/dist/client/assets/index-Dl1nQm2g.js +778 -0
  32. package/dist/client/assets/index-G934YbUs.css +1 -0
  33. package/dist/client/assets/infoDiagram-42DDH7IO-BZjLv6IH.js +2 -0
  34. package/dist/client/assets/{ishikawaDiagram-PHBUUO56-DuT5_xy8.js → ishikawaDiagram-UXIWVN3A-rnfc72_m.js} +2 -2
  35. package/dist/client/assets/journeyDiagram-VCZTEJTY-DcW9mUFI.js +139 -0
  36. package/dist/client/assets/{kanban-definition-K7BYSVSG-ftVPxOyK.js → kanban-definition-6JOO6SKY-BgEOJRmr.js} +8 -8
  37. package/dist/client/assets/katex-DkKDou_j.js +257 -0
  38. package/dist/client/assets/layout-BQJpJX3E.js +1 -0
  39. package/dist/client/assets/{linear-BIb05kGQ.js → linear-4R4_f67K.js} +1 -1
  40. package/dist/client/assets/min-B78pX87t.js +1 -0
  41. package/dist/client/assets/mindmap-definition-QFDTVHPH-C34BXoiK.js +96 -0
  42. package/dist/client/assets/pieDiagram-DEJITSTG-ijiNKQ8e.js +30 -0
  43. package/dist/client/assets/{quadrantDiagram-337W2JSQ-kt-KJQMs.js → quadrantDiagram-34T5L4WZ-CZtSib2Y.js} +1 -1
  44. package/dist/client/assets/requirementDiagram-MS252O5E-BN-J-8AJ.js +84 -0
  45. package/dist/client/assets/{sankeyDiagram-WA2Y5GQK-CAokuuka.js → sankeyDiagram-XADWPNL6-CWP-qBhv.js} +1 -1
  46. package/dist/client/assets/sequenceDiagram-FGHM5R23-BnJnhdS6.js +157 -0
  47. package/dist/client/assets/stateDiagram-FHFEXIEX-DN4fDIGN.js +1 -0
  48. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-D6Qg_ItE.js +1 -0
  49. package/dist/client/assets/timeline-definition-GMOUNBTQ-Ds6_LT-T.js +120 -0
  50. package/dist/client/assets/{vennDiagram-LZ73GAT5-C2MLaHWE.js → vennDiagram-DHZGUBPP-BrbuTJAI.js} +5 -5
  51. package/dist/client/assets/wardley-RL74JXVD-BsQbbPRZ.js +162 -0
  52. package/dist/client/assets/wardleyDiagram-NUSXRM2D-BH6aFYai.js +20 -0
  53. package/dist/client/assets/xychartDiagram-5P7HB3ND-BwK9W_yy.js +7 -0
  54. package/dist/client/index.html +2 -2
  55. package/dist/config.js +79 -77
  56. package/dist/server/api.js +353 -26
  57. package/dist/server/app.js +1 -1
  58. package/dist/server/index.js +19 -10
  59. package/dist/server/watcher.js +22 -7
  60. package/package.json +4 -1
  61. package/dist/client/assets/_basePickBy-CpIFiPmc.js +0 -1
  62. package/dist/client/assets/_baseUniq-C06JezLB.js +0 -1
  63. package/dist/client/assets/architectureDiagram-2XIMDMQ5-34IaPMNX.js +0 -36
  64. package/dist/client/assets/c4Diagram-IC4MRINW-g2IVuQJv.js +0 -10
  65. package/dist/client/assets/channel-DXsl4vhs.js +0 -1
  66. package/dist/client/assets/chunk-NQ4KR5QH-CsNzZMQ3.js +0 -220
  67. package/dist/client/assets/chunk-WL4C6EOR-DQcdvkib.js +0 -189
  68. package/dist/client/assets/classDiagram-VBA2DB6C-M0yhB_li.js +0 -1
  69. package/dist/client/assets/classDiagram-v2-RAHNMMFH-M0yhB_li.js +0 -1
  70. package/dist/client/assets/clone-DY0FLUFX.js +0 -1
  71. package/dist/client/assets/dagre-KLK3FWXG-I4ZL-Yjk.js +0 -4
  72. package/dist/client/assets/diagram-E7M64L7V-B5x_qiUv.js +0 -24
  73. package/dist/client/assets/diagram-IFDJBPK2-B3aH6gaM.js +0 -43
  74. package/dist/client/assets/diagram-P4PSJMXO-C65hl65m.js +0 -24
  75. package/dist/client/assets/erDiagram-INFDFZHY-CuGoj23m.js +0 -70
  76. package/dist/client/assets/flowDiagram-PKNHOUZH-BgI_bQaB.js +0 -162
  77. package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-BQoXcJsF.js +0 -65
  78. package/dist/client/assets/graph-CGDYVHRC.js +0 -1
  79. package/dist/client/assets/index-CI8fw8c4.js +0 -709
  80. package/dist/client/assets/index-D2TYNMWH.css +0 -1
  81. package/dist/client/assets/infoDiagram-LFFYTUFH-Cua6joOv.js +0 -2
  82. package/dist/client/assets/journeyDiagram-4ABVD52K-My7-KJcV.js +0 -139
  83. package/dist/client/assets/katex-B1X10hvy.js +0 -261
  84. package/dist/client/assets/layout-DhQ7R7Ms.js +0 -1
  85. package/dist/client/assets/mindmap-definition-YRQLILUH-DwD8YJaZ.js +0 -68
  86. package/dist/client/assets/pieDiagram-SKSYHLDU-C-17c64G.js +0 -30
  87. package/dist/client/assets/requirementDiagram-Z7DCOOCP-BH89qJx9.js +0 -73
  88. package/dist/client/assets/sequenceDiagram-2WXFIKYE-CeVpL_7D.js +0 -145
  89. package/dist/client/assets/stateDiagram-RAJIS63D-DdqWd2rm.js +0 -1
  90. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-B83j0Cq2.js +0 -1
  91. package/dist/client/assets/timeline-definition-YZTLITO2-BnlRy3AK.js +0 -61
  92. package/dist/client/assets/treemap-KZPCXAKY-xwXQmLuw.js +0 -162
  93. package/dist/client/assets/xychartDiagram-JWTSCODW-BiTzOgXT.js +0 -7
@@ -1,16 +1,56 @@
1
1
  import { Hono } from 'hono';
2
2
  import fs from 'fs/promises';
3
+ import { existsSync } from 'fs';
3
4
  import path from 'path';
4
- import { saveUserConfig } from '../config.js';
5
+ import os from 'os';
6
+ import { saveConfig, DEFAULT_SENSITIVE_PATHS } from '../config.js';
7
+ import { minimatch } from 'minimatch';
5
8
  function isAllowed(pathStr, config) {
6
9
  const resolved = path.resolve(pathStr);
7
- return resolved.startsWith(path.resolve(config.root));
10
+ return config.roots.some(root => resolved.startsWith(path.resolve(root.path)));
11
+ }
12
+ function validateRoot(rootPath, config) {
13
+ const resolved = path.resolve(rootPath);
14
+ const root = config.roots.find(r => path.resolve(r.path) === resolved);
15
+ return root ? path.resolve(root.path) : null;
16
+ }
17
+ function checkSensitivePath(inputPath) {
18
+ const basename = path.basename(inputPath);
19
+ for (const pattern of DEFAULT_SENSITIVE_PATHS) {
20
+ if (minimatch(basename, pattern, { nocase: true, dot: true }))
21
+ return true;
22
+ }
23
+ return false;
24
+ }
25
+ function checkNestedPath(newPath, existingRoots) {
26
+ const resolved = path.resolve(newPath);
27
+ for (const root of existingRoots) {
28
+ const existing = path.resolve(root.path);
29
+ if (resolved === existing)
30
+ return { isNested: true, conflictWith: root.path, reason: 'duplicate' };
31
+ if (resolved.startsWith(existing + path.sep))
32
+ return { isNested: true, conflictWith: root.path, reason: 'child' };
33
+ if (existing.startsWith(resolved + path.sep))
34
+ return { isNested: true, conflictWith: root.path, reason: 'parent' };
35
+ }
36
+ return { isNested: false };
37
+ }
38
+ function findRootForPath(filePath, config) {
39
+ for (const root of config.roots) {
40
+ const rootPath = path.resolve(root.path);
41
+ const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
42
+ const fullPath = path.join(rootPath, relativePath);
43
+ if ((fullPath.startsWith(rootPath + path.sep) || fullPath === rootPath) && existsSync(fullPath)) {
44
+ return rootPath;
45
+ }
46
+ }
47
+ return null;
8
48
  }
9
49
  function hasAllowedExtension(filename, extensions) {
10
50
  const ext = path.extname(filename).toLowerCase();
11
51
  return extensions.includes(ext);
12
52
  }
13
- async function walkDirectory(dir, config, matcher) {
53
+ async function walkDirectory(dir, rootPath, config, matcher) {
14
54
  const nodes = [];
15
55
  try {
16
56
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -22,11 +62,12 @@ async function walkDirectory(dir, config, matcher) {
22
62
  if (matcher.isIgnored(fullPath, isDir))
23
63
  continue;
24
64
  if (isDir) {
25
- const children = await walkDirectory(fullPath, config, matcher);
65
+ const children = await walkDirectory(fullPath, rootPath, config, matcher);
26
66
  nodes.push({
27
67
  name: entry.name,
28
- path: fullPath.replace(config.root, '').replace(/\\/g, '/'),
68
+ path: fullPath.replace(rootPath, '').replace(/\\/g, '/') || '/',
29
69
  type: 'directory',
70
+ rootPath,
30
71
  children,
31
72
  });
32
73
  }
@@ -34,15 +75,19 @@ async function walkDirectory(dir, config, matcher) {
34
75
  if (hasAllowedExtension(entry.name, config.allowedExtensions)) {
35
76
  nodes.push({
36
77
  name: entry.name,
37
- path: fullPath.replace(config.root, '').replace(/\\/g, '/'),
78
+ path: fullPath.replace(rootPath, '').replace(/\\/g, '/') || '/',
38
79
  type: 'file',
80
+ rootPath,
39
81
  });
40
82
  }
41
83
  }
42
84
  }
43
85
  }
44
86
  catch (e) {
45
- // ignore errors
87
+ // Re-throw error if this is the root directory, otherwise ignore
88
+ if (dir === rootPath) {
89
+ throw e;
90
+ }
46
91
  }
47
92
  nodes.sort((a, b) => {
48
93
  if (a.type === 'directory' && b.type === 'file')
@@ -72,7 +117,7 @@ export function createFileRouter(config, matcher) {
72
117
  updates[key] = body[key];
73
118
  }
74
119
  }
75
- saveUserConfig(config.root, updates);
120
+ saveConfig(config);
76
121
  if (typeof updates.showHiddenFiles === 'boolean') {
77
122
  config.showHiddenFiles = updates.showHiddenFiles;
78
123
  }
@@ -99,10 +144,140 @@ export function createFileRouter(config, matcher) {
99
144
  return c.json({ error: 'Failed to update config' }, 500);
100
145
  }
101
146
  });
147
+ // Root management routes
148
+ router.get('/roots', async (c) => {
149
+ return c.json({ roots: config.roots });
150
+ });
151
+ router.post('/roots', async (c) => {
152
+ try {
153
+ const body = await c.req.json();
154
+ const newPath = body.path;
155
+ if (!newPath)
156
+ return c.json({ error: 'Path is required' }, 400);
157
+ try {
158
+ const stat = await fs.stat(newPath);
159
+ if (!stat.isDirectory())
160
+ return c.json({ error: 'Path must be a directory' }, 400);
161
+ }
162
+ catch {
163
+ return c.json({ error: 'Path does not exist' }, 400);
164
+ }
165
+ if (checkSensitivePath(newPath))
166
+ return c.json({ error: 'Sensitive path not allowed' }, 400);
167
+ const nested = checkNestedPath(newPath, config.roots);
168
+ if (nested.isNested)
169
+ return c.json({ error: 'Nested path not allowed', conflictWith: nested.conflictWith, reason: nested.reason }, 400);
170
+ const newRoot = { path: path.resolve(newPath), exclude: body.exclude };
171
+ config.roots.push(newRoot);
172
+ saveConfig(config);
173
+ return c.json({ success: true, root: newRoot });
174
+ }
175
+ catch (e) {
176
+ return c.json({ error: 'Failed to add root' }, 500);
177
+ }
178
+ });
179
+ router.delete('/roots', async (c) => {
180
+ const pathParam = c.req.query('path');
181
+ if (!pathParam)
182
+ return c.json({ error: 'path parameter required' }, 400);
183
+ const idx = config.roots.findIndex(r => path.resolve(r.path) === path.resolve(pathParam));
184
+ if (idx === -1)
185
+ return c.json({ error: 'Root not found' }, 404);
186
+ config.roots.splice(idx, 1);
187
+ saveConfig(config);
188
+ return c.json({ success: true });
189
+ });
190
+ router.patch('/roots', async (c) => {
191
+ try {
192
+ const body = await c.req.json();
193
+ const { path: rootPath, exclude } = body;
194
+ if (!rootPath)
195
+ return c.json({ error: 'Path is required' }, 400);
196
+ const root = config.roots.find(r => path.resolve(r.path) === path.resolve(rootPath));
197
+ if (!root)
198
+ return c.json({ error: 'Root not found' }, 404);
199
+ if (exclude !== undefined)
200
+ root.exclude = exclude;
201
+ saveConfig(config);
202
+ return c.json({ success: true, root });
203
+ }
204
+ catch (e) {
205
+ return c.json({ error: 'Failed to update root' }, 500);
206
+ }
207
+ });
208
+ router.get('/roots/search', async (c) => {
209
+ const query = c.req.query('q') || '';
210
+ if (!query.trim())
211
+ return c.json({ matches: [] });
212
+ const matches = [];
213
+ let searchRoot;
214
+ let searchTerm;
215
+ if (query.startsWith('/')) {
216
+ const normalizedQuery = path.normalize(query);
217
+ const lastSlashIndex = normalizedQuery.lastIndexOf('/');
218
+ if (lastSlashIndex <= 0) {
219
+ searchRoot = '/';
220
+ searchTerm = normalizedQuery.slice(1);
221
+ }
222
+ else {
223
+ const dirPart = normalizedQuery.slice(0, lastSlashIndex) || '/';
224
+ const basePart = normalizedQuery.slice(lastSlashIndex + 1);
225
+ if (existsSync(dirPart) && (await fs.stat(dirPart)).isDirectory()) {
226
+ searchRoot = dirPart;
227
+ searchTerm = basePart;
228
+ }
229
+ else {
230
+ searchRoot = path.dirname(dirPart);
231
+ searchTerm = basePart;
232
+ }
233
+ }
234
+ }
235
+ else {
236
+ searchRoot = os.homedir();
237
+ searchTerm = query;
238
+ }
239
+ const pattern = `*${searchTerm}*`;
240
+ async function traverse(dir, depth) {
241
+ if (depth > 3 || matches.length >= 20)
242
+ return;
243
+ try {
244
+ const entries = await fs.readdir(dir, { withFileTypes: true });
245
+ for (const entry of entries) {
246
+ if (matches.length >= 20)
247
+ break;
248
+ if (entry.name.startsWith('.'))
249
+ continue;
250
+ const fullPath = path.join(dir, entry.name);
251
+ if (entry.isDirectory() && !checkSensitivePath(fullPath)) {
252
+ if (minimatch(entry.name, pattern, { nocase: true }))
253
+ matches.push(fullPath);
254
+ await traverse(fullPath, depth + 1);
255
+ }
256
+ }
257
+ }
258
+ catch { }
259
+ }
260
+ await traverse(searchRoot, 0);
261
+ return c.json({ matches });
262
+ });
102
263
  router.get('/', async (c) => {
103
264
  try {
104
- const files = await walkDirectory(config.root, config, matcher);
105
- return c.json({ files });
265
+ const groups = await Promise.all(config.roots.map(async (root) => {
266
+ try {
267
+ return {
268
+ root,
269
+ files: await walkDirectory(root.path, root.path, config, matcher)
270
+ };
271
+ }
272
+ catch (e) {
273
+ return {
274
+ root,
275
+ files: [],
276
+ error: e instanceof Error ? e.message : 'Failed to read directory'
277
+ };
278
+ }
279
+ }));
280
+ return c.json({ groups });
106
281
  }
107
282
  catch (e) {
108
283
  return c.json({ error: 'Failed to read directory' }, 500);
@@ -116,7 +291,21 @@ export function createFileRouter(config, matcher) {
116
291
  const paths = pathsParam.split(',').filter(Boolean);
117
292
  const results = [];
118
293
  for (const filePath of paths) {
119
- const fullPath = path.join(config.root, filePath);
294
+ const rootParam = c.req.query('root');
295
+ let rootPath;
296
+ if (rootParam) {
297
+ rootPath = path.resolve(rootParam);
298
+ if (!config.roots.some(r => path.resolve(r.path) === rootPath)) {
299
+ return c.json({ error: 'Invalid root' }, 400);
300
+ }
301
+ }
302
+ else {
303
+ rootPath = findRootForPath(filePath, config);
304
+ }
305
+ if (!rootPath)
306
+ return c.json({ error: 'Access denied' }, 403);
307
+ const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
308
+ const fullPath = path.join(rootPath, relativePath);
120
309
  if (!isAllowed(fullPath, config)) {
121
310
  continue;
122
311
  }
@@ -139,14 +328,53 @@ export function createFileRouter(config, matcher) {
139
328
  });
140
329
  router.get('/*', async (c) => {
141
330
  const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
142
- const fullPath = path.join(config.root, filePath);
331
+ // Handle root path - return grouped file tree
332
+ if (filePath === '/' || filePath === '') {
333
+ try {
334
+ const groups = await Promise.all(config.roots.map(async (root) => {
335
+ try {
336
+ return {
337
+ root,
338
+ files: await walkDirectory(root.path, root.path, config, matcher)
339
+ };
340
+ }
341
+ catch (e) {
342
+ return {
343
+ root,
344
+ files: [],
345
+ error: e instanceof Error ? e.message : 'Failed to read directory'
346
+ };
347
+ }
348
+ }));
349
+ return c.json({ groups });
350
+ }
351
+ catch (e) {
352
+ return c.json({ error: 'Failed to read directory' }, 500);
353
+ }
354
+ }
355
+ // Handle root parameter from query string
356
+ const rootParam = c.req.query('root');
357
+ let rootPath;
358
+ if (rootParam) {
359
+ rootPath = path.resolve(rootParam);
360
+ if (!config.roots.some(r => path.resolve(r.path) === rootPath)) {
361
+ return c.json({ error: 'Invalid root' }, 400);
362
+ }
363
+ }
364
+ else {
365
+ rootPath = findRootForPath(filePath, config);
366
+ }
367
+ if (!rootPath)
368
+ return c.json({ error: 'Access denied' }, 403);
369
+ const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
370
+ const fullPath = path.join(rootPath, relativePath);
143
371
  if (!isAllowed(fullPath, config)) {
144
372
  return c.json({ error: 'Access denied' }, 403);
145
373
  }
146
374
  try {
147
375
  const stat = await fs.stat(fullPath);
148
376
  if (stat.isDirectory()) {
149
- const files = await walkDirectory(fullPath, config, matcher);
377
+ const files = await walkDirectory(fullPath, rootPath, config, matcher);
150
378
  return c.json({ files });
151
379
  }
152
380
  }
@@ -161,9 +389,62 @@ export function createFileRouter(config, matcher) {
161
389
  return c.json({ error: 'File not found' }, 404);
162
390
  }
163
391
  });
392
+ router.post('/copy', async (c) => {
393
+ try {
394
+ const body = await c.req.json();
395
+ let { sourcePath, targetPath, sourceRoot, targetRoot } = body;
396
+ if (!sourcePath || !targetPath) {
397
+ return c.json({ error: 'sourcePath and targetPath required' }, 400);
398
+ }
399
+ if (!sourceRoot || !targetRoot) {
400
+ return c.json({ error: 'sourceRoot and targetRoot required' }, 400);
401
+ }
402
+ const validatedSourceRoot = validateRoot(sourceRoot, config);
403
+ const validatedTargetRoot = validateRoot(targetRoot, config);
404
+ if (!validatedSourceRoot)
405
+ return c.json({ error: 'Invalid source root' }, 400);
406
+ if (!validatedTargetRoot)
407
+ return c.json({ error: 'Invalid target root' }, 400);
408
+ const srcFullPath = path.join(validatedSourceRoot, sourcePath);
409
+ let tgtFullPath = path.join(validatedTargetRoot, targetPath);
410
+ const stat = await fs.stat(srcFullPath);
411
+ const targetExists = await fs.access(tgtFullPath).then(() => true).catch(() => false);
412
+ if (targetExists) {
413
+ const ext = path.extname(targetPath);
414
+ const base = path.basename(targetPath, ext);
415
+ const dir = path.dirname(targetPath);
416
+ targetPath = `${dir}/${base} (copy)${ext}`;
417
+ tgtFullPath = path.join(validatedTargetRoot, targetPath);
418
+ }
419
+ if (stat.isDirectory()) {
420
+ await fs.cp(srcFullPath, tgtFullPath, { recursive: true });
421
+ }
422
+ else {
423
+ await fs.copyFile(srcFullPath, tgtFullPath);
424
+ }
425
+ return c.json({ success: true, newPath: targetPath });
426
+ }
427
+ catch (e) {
428
+ console.error('Copy error:', e);
429
+ return c.json({ error: 'Failed to copy' }, 500);
430
+ }
431
+ });
164
432
  router.post('/*', async (c) => {
165
433
  const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
166
- const fullPath = path.join(config.root, filePath);
434
+ const rootParam = c.req.query('root');
435
+ let rootPath;
436
+ if (rootParam) {
437
+ rootPath = validateRoot(rootParam, config);
438
+ if (!rootPath)
439
+ return c.json({ error: 'Invalid root' }, 400);
440
+ }
441
+ else {
442
+ rootPath = findRootForPath(filePath, config);
443
+ if (!rootPath)
444
+ return c.json({ error: 'Access denied' }, 403);
445
+ }
446
+ const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
447
+ const fullPath = path.join(rootPath, relativePath);
167
448
  if (!isAllowed(fullPath, config)) {
168
449
  return c.json({ error: 'Access denied' }, 403);
169
450
  }
@@ -173,7 +454,19 @@ export function createFileRouter(config, matcher) {
173
454
  if (body.type === 'create') {
174
455
  const parentPath = body.parentPath || '';
175
456
  const name = body.name;
176
- const targetPath = path.join(config.root, parentPath, name);
457
+ let parentRootPath;
458
+ if (body.root) {
459
+ parentRootPath = validateRoot(body.root, config);
460
+ if (!parentRootPath) {
461
+ return c.json({ error: 'Invalid root' }, 400);
462
+ }
463
+ }
464
+ else {
465
+ parentRootPath = findRootForPath(parentPath, config);
466
+ if (!parentRootPath)
467
+ return c.json({ error: 'Access denied' }, 403);
468
+ }
469
+ const targetPath = path.join(parentRootPath, parentPath, name);
177
470
  if (!isAllowed(targetPath, config)) {
178
471
  return c.json({ error: 'Access denied' }, 403);
179
472
  }
@@ -187,7 +480,7 @@ export function createFileRouter(config, matcher) {
187
480
  }
188
481
  await fs.writeFile(targetPath, '', 'utf-8');
189
482
  }
190
- return c.json({ success: true, path: targetPath.replace(config.root, '') });
483
+ return c.json({ success: true, path: targetPath.replace(parentRootPath, '') });
191
484
  }
192
485
  catch (e) {
193
486
  return c.json({ error: 'Failed to create' }, 500);
@@ -205,19 +498,23 @@ export function createFileRouter(config, matcher) {
205
498
  }
206
499
  });
207
500
  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
501
  try {
214
502
  const body = await c.req.json();
215
- const { oldPath, newPath, isDirectory } = body;
503
+ const { oldPath, newPath, isDirectory, sourceRoot, targetRoot } = body;
216
504
  if (!oldPath || !newPath) {
217
505
  return c.json({ error: 'oldPath and newPath are required' }, 400);
218
506
  }
219
- const oldFullPath = path.join(config.root, oldPath);
220
- const newFullPath = path.join(config.root, newPath);
507
+ if (!sourceRoot || !targetRoot) {
508
+ return c.json({ error: 'sourceRoot and targetRoot are required' }, 400);
509
+ }
510
+ const validatedSourceRoot = validateRoot(sourceRoot, config);
511
+ const validatedTargetRoot = validateRoot(targetRoot, config);
512
+ if (!validatedSourceRoot)
513
+ return c.json({ error: 'Invalid source root' }, 400);
514
+ if (!validatedTargetRoot)
515
+ return c.json({ error: 'Invalid target root' }, 400);
516
+ const oldFullPath = path.join(validatedSourceRoot, oldPath);
517
+ const newFullPath = path.join(validatedTargetRoot, newPath);
221
518
  if (!isAllowed(oldFullPath, config) || !isAllowed(newFullPath, config)) {
222
519
  return c.json({ error: 'Access denied' }, 403);
223
520
  }
@@ -226,15 +523,32 @@ export function createFileRouter(config, matcher) {
226
523
  if (!stat.isDirectory()) {
227
524
  return c.json({ error: 'Source is not a directory' }, 400);
228
525
  }
229
- await fs.rename(oldFullPath, newFullPath);
230
526
  }
231
527
  else {
232
528
  const ext = path.extname(newPath).toLowerCase();
233
529
  if (!hasAllowedExtension(newPath, config.allowedExtensions)) {
234
530
  return c.json({ error: 'File type not allowed' }, 400);
235
531
  }
532
+ }
533
+ try {
236
534
  await fs.rename(oldFullPath, newFullPath);
237
535
  }
536
+ catch (err) {
537
+ if (err instanceof Error && 'code' in err && err.code === 'EXDEV') {
538
+ // Cross-filesystem move - copy then delete
539
+ if (isDirectory) {
540
+ await fs.cp(oldFullPath, newFullPath, { recursive: true });
541
+ await fs.rm(oldFullPath, { recursive: true });
542
+ }
543
+ else {
544
+ await fs.copyFile(oldFullPath, newFullPath);
545
+ await fs.unlink(oldFullPath);
546
+ }
547
+ }
548
+ else {
549
+ throw err;
550
+ }
551
+ }
238
552
  return c.json({ success: true, newPath });
239
553
  }
240
554
  catch (e) {
@@ -244,7 +558,20 @@ export function createFileRouter(config, matcher) {
244
558
  });
245
559
  router.delete('/*', async (c) => {
246
560
  const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
247
- const fullPath = path.join(config.root, filePath);
561
+ const rootParam = c.req.query('root');
562
+ let rootPath;
563
+ if (rootParam) {
564
+ rootPath = validateRoot(rootParam, config);
565
+ if (!rootPath)
566
+ return c.json({ error: 'Invalid root' }, 400);
567
+ }
568
+ else {
569
+ rootPath = findRootForPath(filePath, config);
570
+ if (!rootPath)
571
+ return c.json({ error: 'Access denied' }, 403);
572
+ }
573
+ const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
574
+ const fullPath = path.join(rootPath, relativePath);
248
575
  if (!isAllowed(fullPath, config)) {
249
576
  return c.json({ error: 'Access denied' }, 403);
250
577
  }
@@ -9,7 +9,7 @@ export function createApp(config) {
9
9
  ignoreFileNames: config.ignore.ignoreFileNames,
10
10
  globalPatterns: config.ignore.patterns,
11
11
  };
12
- const matcher = new IgnoreMatcher(config.root, ignoreConfig);
12
+ const matcher = new IgnoreMatcher(config.roots[0]?.path || process.cwd(), ignoreConfig);
13
13
  app.use('*', cors());
14
14
  app.use('*', async (c, next) => {
15
15
  c.set('config', config);
@@ -2,7 +2,7 @@ import { serve } from '@hono/node-server';
2
2
  import { Hono } from 'hono';
3
3
  import { cors } from 'hono/cors';
4
4
  import { createFileRouter } from './api.js';
5
- import { loadConfig } from '../config.js';
5
+ import { loadConfig, DEFAULT_PORT, DEFAULT_HOST } from '../config.js';
6
6
  import { setupWatcher } from './watcher.js';
7
7
  import { IgnoreMatcher } from './ignore.js';
8
8
  import { WebSocketServer, WebSocket } from 'ws';
@@ -11,9 +11,17 @@ import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
  const clientDir = path.join(__dirname, '..', 'client');
14
+ function findRootForPath(filePath, config) {
15
+ for (const root of config.roots) {
16
+ if (filePath.startsWith(root.path)) {
17
+ return root.path;
18
+ }
19
+ }
20
+ return config.roots[0]?.path || '';
21
+ }
14
22
  async function main() {
15
23
  const config = await loadConfig();
16
- const matcher = new IgnoreMatcher(config.root, {
24
+ const matcher = new IgnoreMatcher(config.roots[0]?.path || process.cwd(), {
17
25
  enableIgnoreFiles: config.ignore.enableIgnoreFiles,
18
26
  ignoreFileNames: config.ignore.ignoreFileNames,
19
27
  globalPatterns: config.ignore.patterns,
@@ -69,8 +77,8 @@ async function main() {
69
77
  });
70
78
  const server = serve({
71
79
  fetch: app.fetch,
72
- port: config.port,
73
- hostname: config.host,
80
+ port: DEFAULT_PORT,
81
+ hostname: DEFAULT_HOST,
74
82
  });
75
83
  const clients = new Set();
76
84
  const wss = new WebSocketServer({ noServer: true });
@@ -86,9 +94,10 @@ async function main() {
86
94
  }
87
95
  });
88
96
  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 });
97
+ onFileChange: (rootPath, event, filePath) => {
98
+ const actualRootPath = findRootForPath(filePath, config);
99
+ const relativePath = filePath.replace(actualRootPath, '');
100
+ const message = JSON.stringify({ type: 'file:change', event, path: relativePath, rootPath: actualRootPath });
92
101
  clients.forEach((client) => {
93
102
  if (client.readyState === WebSocket.OPEN) {
94
103
  client.send(message);
@@ -97,9 +106,9 @@ async function main() {
97
106
  },
98
107
  });
99
108
  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`);
109
+ console.log(` Local: http://localhost:${DEFAULT_PORT}`);
110
+ console.log(` Network: http://${DEFAULT_HOST}:${DEFAULT_PORT}`);
111
+ console.log(` Roots: ${config.roots.map(r => r.path).join(', ')}\n`);
103
112
  }
104
113
  main().catch((e) => {
105
114
  console.error('Failed to start:', e);
@@ -1,7 +1,8 @@
1
1
  import chokidar from 'chokidar';
2
2
  import fs from 'fs';
3
3
  export function setupWatcher(config, matcher, callbacks) {
4
- const watcher = chokidar.watch(config.root, {
4
+ const rootPaths = config.roots.map(r => r.path);
5
+ const watcher = chokidar.watch(rootPaths, {
5
6
  ignored: (filePath) => {
6
7
  if (!config.showHiddenFiles && (filePath.includes('/.') || filePath.startsWith('.')))
7
8
  return true;
@@ -22,26 +23,40 @@ export function setupWatcher(config, matcher, callbacks) {
22
23
  },
23
24
  persistent: true,
24
25
  ignoreInitial: true,
25
- depth: 99,
26
+ depth: 3,
26
27
  });
27
28
  watcher
28
29
  .on('add', (path) => {
29
30
  if (config.allowedExtensions.some(ext => path.endsWith(ext))) {
30
- callbacks.onFileChange('add', path);
31
+ const matchingRoot = config.roots.find(r => path.startsWith(r.path));
32
+ const rootPath = matchingRoot?.path || config.roots[0]?.path || '';
33
+ callbacks.onFileChange(rootPath, 'add', path);
31
34
  }
32
35
  })
33
36
  .on('change', (path) => {
34
37
  if (config.allowedExtensions.some(ext => path.endsWith(ext))) {
35
- callbacks.onFileChange('change', path);
38
+ const matchingRoot = config.roots.find(r => path.startsWith(r.path));
39
+ const rootPath = matchingRoot?.path || config.roots[0]?.path || '';
40
+ callbacks.onFileChange(rootPath, 'change', path);
36
41
  }
37
42
  })
38
43
  .on('unlink', (path) => {
39
44
  if (config.allowedExtensions.some(ext => path.endsWith(ext))) {
40
- callbacks.onFileChange('unlink', path);
45
+ const matchingRoot = config.roots.find(r => path.startsWith(r.path));
46
+ const rootPath = matchingRoot?.path || config.roots[0]?.path || '';
47
+ callbacks.onFileChange(rootPath, 'unlink', path);
41
48
  }
42
49
  })
43
- .on('addDir', (path) => callbacks.onFileChange('addDir', path))
44
- .on('unlinkDir', (path) => callbacks.onFileChange('unlinkDir', path))
50
+ .on('addDir', (path) => {
51
+ const matchingRoot = config.roots.find(r => path.startsWith(r.path));
52
+ const rootPath = matchingRoot?.path || config.roots[0]?.path || '';
53
+ callbacks.onFileChange(rootPath, 'addDir', path);
54
+ })
55
+ .on('unlinkDir', (path) => {
56
+ const matchingRoot = config.roots.find(r => path.startsWith(r.path));
57
+ const rootPath = matchingRoot?.path || config.roots[0]?.path || '';
58
+ callbacks.onFileChange(rootPath, 'unlinkDir', path);
59
+ })
45
60
  .on('error', (error) => console.error('Watcher error:', error));
46
61
  return watcher;
47
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "colonynote",
3
- "version": "1.0.0-beta.10",
3
+ "version": "1.0.0-beta.13",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -65,6 +65,9 @@
65
65
  "chokidar": "^4.0.3",
66
66
  "class-variance-authority": "^0.7.1",
67
67
  "clsx": "^2.1.1",
68
+ "cmdk": "^1.1.1",
69
+ "colonynote": "1.0.0-beta.10",
70
+ "commander": "^14.0.3",
68
71
  "flexsearch": "^0.8.212",
69
72
  "hono": "^4.12.9",
70
73
  "katex": "^0.16.40",
@@ -1 +0,0 @@
1
- import{e as x,c as O,g as m,k as P,h as p,j as w,l as c,m as A,n as I,t as N,o as _}from"./_baseUniq-C06JezLB.js";import{aU as g,aC as $,aV as E,aW as F,aX as M,aY as l,aZ as B,a_ as T,a$ as y,b0 as S}from"./index-CI8fw8c4.js";var C=/\s/;function G(n){for(var r=n.length;r--&&C.test(n.charAt(r)););return r}var H=/^\s+/;function L(n){return n&&n.slice(0,G(n)+1).replace(H,"")}var o=NaN,R=/^[-+]0x[0-9a-f]+$/i,W=/^0b[01]+$/i,X=/^0o[0-7]+$/i,Y=parseInt;function q(n){if(typeof n=="number")return n;if(x(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=L(n);var t=W.test(n);return t||X.test(n)?Y(n.slice(2),t?2:8):R.test(n)?o:+n}var v=1/0,z=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=q(n),n===v||n===-v){var r=n<0?-1:1;return r*z}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?O(n):[]}var b=Object.prototype,Z=b.hasOwnProperty,dn=$(function(n,r){n=Object(n);var t=-1,i=r.length,a=i>2?r[2]:void 0;for(a&&E(r[0],r[1],a)&&(i=1);++t<i;)for(var f=r[t],e=F(f),s=-1,d=e.length;++s<d;){var u=e[s],h=n[u];(h===void 0||M(h,b[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,i){var a=Object(r);if(!l(r)){var f=m(t);r=P(r),t=function(s){return f(a[s],s,a)}}var e=n(r,t,i);return e>-1?a[f?r[e]:e]:void 0}}var J=Math.max;function Q(n,r,t){var i=n==null?0:n.length;if(!i)return-1;var a=t==null?0:U(t);return a<0&&(a=J(i+a,0)),p(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,i=l(n)?Array(n.length):[];return w(n,function(a,f,e){i[++t]=r(a,f,e)}),i}function gn(n,r){var t=B(n)?c:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&A(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var i=-1,a=n.length;++i<a;){var f=n[i],e=r(f);if(e!=null&&(s===void 0?e===e&&!x(e):t(e,s)))var s=e,d=f}return d}function on(n){return n&&n.length?tn(n,T,rn):void 0}function an(n,r,t,i){if(!g(n))return n;r=I(r,n);for(var a=-1,f=r.length,e=f-1,s=n;s!=null&&++a<f;){var d=N(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=e){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:y(r[a+1])?[]:{})}S(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var i=-1,a=r.length,f={};++i<a;){var e=r[i],s=_(n,e);t(s,e)&&an(f,I(e,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};