@geminilight/mindos 0.6.31 → 0.6.33
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/app/app/api/ask/route.ts +69 -29
- package/app/app/api/graph/route.ts +5 -76
- package/app/app/trash/page.tsx +1 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +22 -8
- package/app/components/ExportModal.tsx +2 -2
- package/app/components/FileTree.tsx +26 -5
- package/app/components/HomeContent.tsx +4 -0
- package/app/components/SystemPulse.tsx +318 -0
- package/app/components/TrashPageClient.tsx +9 -9
- package/app/components/agents/AgentsSkillsSection.tsx +173 -102
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +11 -21
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +15 -2
- package/app/components/renderers/workflow-yaml/selectors.tsx +3 -3
- package/app/components/ui/Toaster.tsx +11 -2
- package/app/lib/actions.ts +20 -9
- package/app/lib/agent/context.ts +22 -11
- package/app/lib/agent/loop-detection.ts +52 -0
- package/app/lib/agent/retry.ts +19 -0
- package/app/lib/core/backlinks.ts +33 -9
- package/app/lib/core/index.ts +4 -1
- package/app/lib/core/link-index.ts +224 -0
- package/app/lib/core/search-index.ts +310 -14
- package/app/lib/core/search.ts +180 -29
- package/app/lib/fs.ts +67 -10
- package/app/lib/hooks/usePinnedFiles.ts +7 -2
- package/app/lib/i18n/modules/knowledge.ts +62 -0
- package/app/lib/toast.ts +7 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +2 -0
- package/package.json +1 -1
- package/scripts/parse-syncinclude.sh +92 -0
- package/scripts/write-build-stamp.js +40 -0
package/app/lib/fs.ts
CHANGED
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
gitLog as coreGitLog,
|
|
25
25
|
gitShowFile as coreGitShowFile,
|
|
26
26
|
invalidateSearchIndex,
|
|
27
|
+
updateSearchIndexFile,
|
|
28
|
+
addSearchIndexFile,
|
|
29
|
+
removeSearchIndexFile,
|
|
30
|
+
LinkIndex,
|
|
27
31
|
summarizeTopLevelSpaces,
|
|
28
32
|
appendContentChange as coreAppendContentChange,
|
|
29
33
|
listContentChanges as coreListContentChanges,
|
|
@@ -96,12 +100,62 @@ function isCacheValid(): boolean {
|
|
|
96
100
|
return _cache !== null && (Date.now() - _cache.timestamp) < CACHE_TTL_MS;
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/** Module-level link index singleton. Lazily built on first graph/backlink access. */
|
|
104
|
+
const _linkIndex = new LinkIndex();
|
|
105
|
+
|
|
106
|
+
/** Get the link index, ensuring it's built for the current mindRoot. */
|
|
107
|
+
export function getLinkIndex(): LinkIndex {
|
|
108
|
+
const root = getMindRoot();
|
|
109
|
+
if (!_linkIndex.isBuiltFor(root)) {
|
|
110
|
+
_linkIndex.rebuild(root);
|
|
111
|
+
}
|
|
112
|
+
return _linkIndex;
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
/** Invalidate cache — call after any write/create/delete/rename operation */
|
|
100
116
|
export function invalidateCache(): void {
|
|
101
117
|
_cache = null;
|
|
102
118
|
_searchIndex = null;
|
|
103
119
|
_treeVersion++;
|
|
104
120
|
invalidateSearchIndex();
|
|
121
|
+
_linkIndex.invalidate();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Invalidate cache after a single file was modified (content write, line edit, append).
|
|
126
|
+
* Tree cache is cleared (file list/mtime changed), but search index is updated
|
|
127
|
+
* incrementally for just this file — O(tokens) instead of O(all-files).
|
|
128
|
+
*/
|
|
129
|
+
function invalidateCacheForFile(filePath: string): void {
|
|
130
|
+
_cache = null;
|
|
131
|
+
_searchIndex = null;
|
|
132
|
+
_treeVersion++;
|
|
133
|
+
updateSearchIndexFile(getMindRoot(), filePath);
|
|
134
|
+
if (_linkIndex.isBuilt()) _linkIndex.updateFile(getMindRoot(), filePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Invalidate cache after a new file was created.
|
|
139
|
+
* Tree cache is cleared, search index gets incremental addFile.
|
|
140
|
+
*/
|
|
141
|
+
function invalidateCacheForNewFile(filePath: string): void {
|
|
142
|
+
_cache = null;
|
|
143
|
+
_searchIndex = null;
|
|
144
|
+
_treeVersion++;
|
|
145
|
+
addSearchIndexFile(getMindRoot(), filePath);
|
|
146
|
+
if (_linkIndex.isBuilt()) _linkIndex.updateFile(getMindRoot(), filePath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Invalidate cache after a file was deleted.
|
|
151
|
+
* Tree cache is cleared, search index gets incremental removeFile.
|
|
152
|
+
*/
|
|
153
|
+
function invalidateCacheForDeletedFile(filePath: string): void {
|
|
154
|
+
_cache = null;
|
|
155
|
+
_searchIndex = null;
|
|
156
|
+
_treeVersion++;
|
|
157
|
+
removeSearchIndexFile(filePath);
|
|
158
|
+
if (_linkIndex.isBuilt()) _linkIndex.removeFile(filePath);
|
|
105
159
|
}
|
|
106
160
|
|
|
107
161
|
function ensureCache(): FileTreeCache {
|
|
@@ -336,18 +390,18 @@ export function getFileContent(filePath: string): string {
|
|
|
336
390
|
/** Atomically writes content to a file given a relative path from MIND_ROOT. */
|
|
337
391
|
export function saveFileContent(filePath: string, content: string): void {
|
|
338
392
|
coreWriteFile(getMindRoot(), filePath, content);
|
|
339
|
-
|
|
393
|
+
invalidateCacheForFile(filePath);
|
|
340
394
|
}
|
|
341
395
|
|
|
342
396
|
/** Creates a new file at the given relative path. Creates parent dirs as needed. */
|
|
343
397
|
export function createFile(filePath: string, initialContent = ''): void {
|
|
344
398
|
coreCreateFile(getMindRoot(), filePath, initialContent);
|
|
345
|
-
|
|
399
|
+
invalidateCacheForNewFile(filePath);
|
|
346
400
|
}
|
|
347
401
|
|
|
348
402
|
export function deleteFile(filePath: string): void {
|
|
349
403
|
coreDeleteFile(getMindRoot(), filePath);
|
|
350
|
-
|
|
404
|
+
invalidateCacheForDeletedFile(filePath);
|
|
351
405
|
}
|
|
352
406
|
|
|
353
407
|
/** Renames a file. newName must be a plain filename (no path separators). */
|
|
@@ -384,12 +438,12 @@ export function readLines(filePath: string): string[] {
|
|
|
384
438
|
|
|
385
439
|
export function insertLines(filePath: string, afterIndex: number, lines: string[]): void {
|
|
386
440
|
coreInsertLines(getMindRoot(), filePath, afterIndex, lines);
|
|
387
|
-
|
|
441
|
+
invalidateCacheForFile(filePath);
|
|
388
442
|
}
|
|
389
443
|
|
|
390
444
|
export function updateLines(filePath: string, startIndex: number, endIndex: number, newLines: string[]): void {
|
|
391
445
|
coreUpdateLines(getMindRoot(), filePath, startIndex, endIndex, newLines);
|
|
392
|
-
|
|
446
|
+
invalidateCacheForFile(filePath);
|
|
393
447
|
}
|
|
394
448
|
|
|
395
449
|
export function deleteLines(filePath: string, startIndex: number, endIndex: number): void {
|
|
@@ -405,17 +459,17 @@ export function deleteLines(filePath: string, startIndex: number, endIndex: numb
|
|
|
405
459
|
|
|
406
460
|
export function appendToFile(filePath: string, content: string): void {
|
|
407
461
|
coreAppendToFile(getMindRoot(), filePath, content);
|
|
408
|
-
|
|
462
|
+
invalidateCacheForFile(filePath);
|
|
409
463
|
}
|
|
410
464
|
|
|
411
465
|
export function insertAfterHeading(filePath: string, heading: string, content: string): void {
|
|
412
466
|
coreInsertAfterHeading(getMindRoot(), filePath, heading, content);
|
|
413
|
-
|
|
467
|
+
invalidateCacheForFile(filePath);
|
|
414
468
|
}
|
|
415
469
|
|
|
416
470
|
export function updateSection(filePath: string, heading: string, newContent: string): void {
|
|
417
471
|
coreUpdateSection(getMindRoot(), filePath, heading, newContent);
|
|
418
|
-
|
|
472
|
+
invalidateCacheForFile(filePath);
|
|
419
473
|
}
|
|
420
474
|
|
|
421
475
|
/** App-level search result (extends core SearchResult with Fuse.js match details) */
|
|
@@ -649,7 +703,10 @@ export function purgeExpiredTrash() {
|
|
|
649
703
|
}
|
|
650
704
|
|
|
651
705
|
export function findBacklinks(targetPath: string): BacklinkEntry[] {
|
|
652
|
-
const
|
|
653
|
-
|
|
706
|
+
const mindRoot = getMindRoot();
|
|
707
|
+
// Use LinkIndex for O(1) source lookup, then only scan matching files
|
|
708
|
+
const linkIndex = getLinkIndex();
|
|
709
|
+
const linkingSources = linkIndex.getBacklinks(targetPath);
|
|
710
|
+
return coreFindBacklinks(mindRoot, targetPath, linkingSources);
|
|
654
711
|
}
|
|
655
712
|
|
|
@@ -15,8 +15,13 @@ function getSnapshot(): string[] {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function getClientSnapshot(): string[] {
|
|
19
|
+
return cachedPins;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SERVER_SNAPSHOT: string[] = [];
|
|
18
23
|
function getServerSnapshot(): string[] {
|
|
19
|
-
return
|
|
24
|
+
return SERVER_SNAPSHOT;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
// Lazy init — avoid calling getSnapshot() at module load time during SSR
|
|
@@ -62,7 +67,7 @@ function writePins(pins: string[]): void {
|
|
|
62
67
|
|
|
63
68
|
export function usePinnedFiles() {
|
|
64
69
|
ensureInit();
|
|
65
|
-
const pins = useSyncExternalStore(subscribe,
|
|
70
|
+
const pins = useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot);
|
|
66
71
|
|
|
67
72
|
const isPinned = useCallback((path: string) => pins.includes(path), [pins]);
|
|
68
73
|
|
|
@@ -70,6 +70,23 @@ export const knowledgeEn = {
|
|
|
70
70
|
cleanupExamplesButton: 'Clean up',
|
|
71
71
|
cleanupExamplesDone: 'Example files removed',
|
|
72
72
|
},
|
|
73
|
+
pulse: {
|
|
74
|
+
title: 'Knowledge Pulse',
|
|
75
|
+
manage: 'Manage',
|
|
76
|
+
connectTitle: 'Connect your AI tools',
|
|
77
|
+
connectDesc: 'Let Cursor, Claude Code, and ChatGPT read your notes.',
|
|
78
|
+
connectAction: 'Connect Agent',
|
|
79
|
+
active: 'Connected',
|
|
80
|
+
detected: 'Detected',
|
|
81
|
+
notFound: 'Not found',
|
|
82
|
+
running: 'Online',
|
|
83
|
+
offline: 'Offline',
|
|
84
|
+
summary: (agents: number, skills: number) => `${agents} Agent${agents !== 1 ? 's' : ''} · ${skills} Skills`,
|
|
85
|
+
skillCount: (enabled: number, total: number) => `${enabled}/${total} skills`,
|
|
86
|
+
showMore: (n: number) => `${n} more agent${n !== 1 ? 's' : ''}`,
|
|
87
|
+
showLess: 'Show less',
|
|
88
|
+
otherDetected: 'Detected',
|
|
89
|
+
},
|
|
73
90
|
fileTree: {
|
|
74
91
|
newFileTitle: 'New file in this directory',
|
|
75
92
|
rename: 'Rename',
|
|
@@ -118,6 +135,9 @@ export const knowledgeEn = {
|
|
|
118
135
|
expiresIn: (days: number) => `Expires in ${days} day${days !== 1 ? 's' : ''}`,
|
|
119
136
|
restored: 'Restored successfully',
|
|
120
137
|
deleted: 'Permanently deleted',
|
|
138
|
+
movedToTrash: 'Deleted',
|
|
139
|
+
undo: 'Undo',
|
|
140
|
+
undoFailed: 'Undo failed',
|
|
121
141
|
emptied: (count: number) => `Emptied ${count} item${count !== 1 ? 's' : ''} from Trash`,
|
|
122
142
|
restoreConflict: 'A file with this name already exists at the original location.',
|
|
123
143
|
overwrite: 'Overwrite',
|
|
@@ -126,6 +146,11 @@ export const knowledgeEn = {
|
|
|
126
146
|
undoDelete: 'Undo',
|
|
127
147
|
viewInTrash: 'View in Trash',
|
|
128
148
|
fileDeleted: (name: string) => `"${name}" moved to Trash`,
|
|
149
|
+
cancel: 'Cancel',
|
|
150
|
+
justNow: 'just now',
|
|
151
|
+
minutesAgo: (m: number) => `${m}m ago`,
|
|
152
|
+
hoursAgo: (h: number) => `${h}h ago`,
|
|
153
|
+
daysAgo: (d: number) => `${d}d ago`,
|
|
129
154
|
},
|
|
130
155
|
export: {
|
|
131
156
|
title: 'Export',
|
|
@@ -144,6 +169,7 @@ export const knowledgeEn = {
|
|
|
144
169
|
exporting: 'Exporting...',
|
|
145
170
|
exportButton: 'Export',
|
|
146
171
|
cancel: 'Cancel',
|
|
172
|
+
close: 'Done',
|
|
147
173
|
done: 'Export Complete',
|
|
148
174
|
downloadAgain: 'Download Again',
|
|
149
175
|
fileCount: (n: number) => `${n} file${n !== 1 ? 's' : ''}`,
|
|
@@ -263,6 +289,11 @@ export const knowledgeEn = {
|
|
|
263
289
|
view: {
|
|
264
290
|
saveDirectory: 'Directory',
|
|
265
291
|
saveFileName: 'File name',
|
|
292
|
+
delete: 'Delete',
|
|
293
|
+
deleteConfirm: (name: string) => `Delete "${name}"? It will be moved to Trash.`,
|
|
294
|
+
cancel: 'Cancel',
|
|
295
|
+
rename: 'Rename',
|
|
296
|
+
dismiss: 'Dismiss',
|
|
266
297
|
},
|
|
267
298
|
findInPage: {
|
|
268
299
|
placeholder: 'Find in document…',
|
|
@@ -341,6 +372,23 @@ export const knowledgeZh = {
|
|
|
341
372
|
cleanupExamplesButton: '一键清理',
|
|
342
373
|
cleanupExamplesDone: '示例文件已清理',
|
|
343
374
|
},
|
|
375
|
+
pulse: {
|
|
376
|
+
title: '知识脉搏',
|
|
377
|
+
manage: '管理',
|
|
378
|
+
connectTitle: '连接你的 AI 工具',
|
|
379
|
+
connectDesc: '让 Cursor、Claude Code 等 AI 读到你的笔记。',
|
|
380
|
+
connectAction: '连接 Agent',
|
|
381
|
+
active: '已连接',
|
|
382
|
+
detected: '已检测',
|
|
383
|
+
notFound: '未发现',
|
|
384
|
+
running: '在线',
|
|
385
|
+
offline: '离线',
|
|
386
|
+
summary: (agents: number, skills: number) => `${agents} 个 Agent · ${skills} 个技能`,
|
|
387
|
+
skillCount: (enabled: number, total: number) => `${enabled}/${total} 个技能`,
|
|
388
|
+
showMore: (n: number) => `还有 ${n} 个`,
|
|
389
|
+
showLess: '收起',
|
|
390
|
+
otherDetected: '已检测到',
|
|
391
|
+
},
|
|
344
392
|
fileTree: {
|
|
345
393
|
newFileTitle: '在此目录新建文件',
|
|
346
394
|
rename: '重命名',
|
|
@@ -389,6 +437,9 @@ export const knowledgeZh = {
|
|
|
389
437
|
expiresIn: (days: number) => `${days} 天后过期`,
|
|
390
438
|
restored: '已恢复',
|
|
391
439
|
deleted: '已彻底删除',
|
|
440
|
+
movedToTrash: '已删除',
|
|
441
|
+
undo: '撤销',
|
|
442
|
+
undoFailed: '撤销失败',
|
|
392
443
|
emptied: (count: number) => `已清空 ${count} 个项目`,
|
|
393
444
|
restoreConflict: '原位置已有同名文件。',
|
|
394
445
|
overwrite: '覆盖',
|
|
@@ -397,6 +448,11 @@ export const knowledgeZh = {
|
|
|
397
448
|
undoDelete: '撤销',
|
|
398
449
|
viewInTrash: '查看回收站',
|
|
399
450
|
fileDeleted: (name: string) => `「${name}」已移至回收站`,
|
|
451
|
+
cancel: '取消',
|
|
452
|
+
justNow: '刚刚',
|
|
453
|
+
minutesAgo: (m: number) => `${m} 分钟前`,
|
|
454
|
+
hoursAgo: (h: number) => `${h} 小时前`,
|
|
455
|
+
daysAgo: (d: number) => `${d} 天前`,
|
|
400
456
|
},
|
|
401
457
|
export: {
|
|
402
458
|
title: '导出',
|
|
@@ -415,6 +471,7 @@ export const knowledgeZh = {
|
|
|
415
471
|
exporting: '导出中...',
|
|
416
472
|
exportButton: '导出',
|
|
417
473
|
cancel: '取消',
|
|
474
|
+
close: '完成',
|
|
418
475
|
done: '导出完成',
|
|
419
476
|
downloadAgain: '再次下载',
|
|
420
477
|
fileCount: (n: number) => `${n} 个文件`,
|
|
@@ -534,6 +591,11 @@ export const knowledgeZh = {
|
|
|
534
591
|
view: {
|
|
535
592
|
saveDirectory: '目录',
|
|
536
593
|
saveFileName: '文件名',
|
|
594
|
+
delete: '删除',
|
|
595
|
+
deleteConfirm: (name: string) => `确定删除「${name}」?文件将移至回收站。`,
|
|
596
|
+
cancel: '取消',
|
|
597
|
+
rename: '重命名',
|
|
598
|
+
dismiss: '关闭',
|
|
537
599
|
},
|
|
538
600
|
findInPage: {
|
|
539
601
|
placeholder: '在文档中查找…',
|
package/app/lib/toast.ts
CHANGED
|
@@ -15,9 +15,10 @@ export interface Toast {
|
|
|
15
15
|
message: string;
|
|
16
16
|
type: 'success' | 'error' | 'info';
|
|
17
17
|
duration: number;
|
|
18
|
+
action?: { label: string; onClick: () => void };
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
type ToastInput = { message: string; type?: Toast['type']; duration?: number };
|
|
21
|
+
type ToastInput = { message: string; type?: Toast['type']; duration?: number; action?: Toast['action'] };
|
|
21
22
|
|
|
22
23
|
const DEFAULT_DURATION = 2000;
|
|
23
24
|
const MAX_TOASTS = 3;
|
|
@@ -51,6 +52,7 @@ function addToast(input: ToastInput) {
|
|
|
51
52
|
message: input.message,
|
|
52
53
|
type: input.type ?? 'info',
|
|
53
54
|
duration: input.duration ?? DEFAULT_DURATION,
|
|
55
|
+
action: input.action,
|
|
54
56
|
};
|
|
55
57
|
toasts = [...toasts, t].slice(-MAX_TOASTS);
|
|
56
58
|
emit();
|
|
@@ -68,6 +70,10 @@ function toast(message: string, opts?: { type?: Toast['type']; duration?: number
|
|
|
68
70
|
toast.success = (message: string, duration?: number) =>
|
|
69
71
|
addToast({ message, type: 'success', duration });
|
|
70
72
|
|
|
73
|
+
/** Show a toast with an undo action (5 second default) */
|
|
74
|
+
toast.undo = (message: string, onUndo: () => void, opts?: { duration?: number; label?: string }) =>
|
|
75
|
+
addToast({ message, type: 'info', duration: opts?.duration ?? 5000, action: { label: opts?.label ?? 'Undo', onClick: onUndo } });
|
|
76
|
+
|
|
71
77
|
/** Show an error toast */
|
|
72
78
|
toast.error = (message: string, duration?: number) =>
|
|
73
79
|
addToast({ message, type: 'error', duration });
|
package/app/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/app/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"dev": "npx tsx scripts/generate-explore.ts && next dev -p ${MINDOS_WEB_PORT:-3456}",
|
|
8
8
|
"prebuild": "npx tsx scripts/generate-explore.ts && node ../scripts/gen-renderer-index.js",
|
|
9
9
|
"build": "next build --webpack",
|
|
10
|
+
"postbuild": "node ../scripts/write-build-stamp.js",
|
|
10
11
|
"start": "next start -p ${MINDOS_WEB_PORT:-3456}",
|
|
11
12
|
"lint": "eslint",
|
|
12
13
|
"test": "vitest run",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"react": "19.2.3",
|
|
55
56
|
"react-dom": "19.2.3",
|
|
56
57
|
"react-markdown": "^10.1.0",
|
|
58
|
+
"react-virtuoso": "^4.18.4",
|
|
57
59
|
"rehype-highlight": "^7.0.2",
|
|
58
60
|
"rehype-raw": "^7.0.0",
|
|
59
61
|
"rehype-slug": "^6.0.0",
|
package/package.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# parse-syncinclude.sh — Parse .syncinclude and export shell variables.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# eval "$(scripts/parse-syncinclude.sh)"
|
|
7
|
+
# # Now you have: SYNC_DIRS, ROOT_FILES, WIKI_EXCLUDES, GLOBAL_EXCLUDES, BLOCKED_PATTERNS
|
|
8
|
+
#
|
|
9
|
+
# Or source it:
|
|
10
|
+
# source <(scripts/parse-syncinclude.sh)
|
|
11
|
+
#
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
16
|
+
SYNCFILE="$REPO_ROOT/.syncinclude"
|
|
17
|
+
|
|
18
|
+
if [ ! -f "$SYNCFILE" ]; then
|
|
19
|
+
echo "echo '::error::.syncinclude not found at $SYNCFILE'" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# ─── Parser ───────────────────────────────────────────────────────────────────
|
|
24
|
+
# Simple YAML-subset parser: reads "section:" headers and " - value" items.
|
|
25
|
+
# No external dependencies (no yq/python needed).
|
|
26
|
+
|
|
27
|
+
current_section=""
|
|
28
|
+
DIRS=()
|
|
29
|
+
FILES=()
|
|
30
|
+
WIKI_EX=()
|
|
31
|
+
GLOBAL_EX=()
|
|
32
|
+
BLOCKED=()
|
|
33
|
+
|
|
34
|
+
while IFS= read -r line; do
|
|
35
|
+
# Skip comments and blank lines
|
|
36
|
+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
|
37
|
+
[[ -z "${line// /}" ]] && continue
|
|
38
|
+
|
|
39
|
+
# Section header (no leading whitespace, ends with colon)
|
|
40
|
+
if [[ "$line" =~ ^([a-z_]+): ]]; then
|
|
41
|
+
current_section="${BASH_REMATCH[1]}"
|
|
42
|
+
continue
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# List item (leading whitespace + dash)
|
|
46
|
+
if [[ "$line" =~ ^[[:space:]]+-[[:space:]]+(.*) ]]; then
|
|
47
|
+
value="${BASH_REMATCH[1]}"
|
|
48
|
+
# Strip surrounding quotes
|
|
49
|
+
value="${value#\"}"
|
|
50
|
+
value="${value%\"}"
|
|
51
|
+
value="${value#\'}"
|
|
52
|
+
value="${value%\'}"
|
|
53
|
+
|
|
54
|
+
case "$current_section" in
|
|
55
|
+
directories) DIRS+=("$value") ;;
|
|
56
|
+
root_files) FILES+=("$value") ;;
|
|
57
|
+
wiki_exclude) WIKI_EX+=("$value") ;;
|
|
58
|
+
global_exclude) GLOBAL_EX+=("$value") ;;
|
|
59
|
+
blocked_patterns) BLOCKED+=("$value") ;;
|
|
60
|
+
esac
|
|
61
|
+
fi
|
|
62
|
+
done < "$SYNCFILE"
|
|
63
|
+
|
|
64
|
+
# ─── Output ───────────────────────────────────────────────────────────────────
|
|
65
|
+
# Print shell variable assignments that can be eval'd by the caller.
|
|
66
|
+
|
|
67
|
+
# Space-separated lists (for simple iteration)
|
|
68
|
+
echo "SYNC_DIRS='${DIRS[*]:-}'"
|
|
69
|
+
echo "ROOT_FILES='${FILES[*]:-}'"
|
|
70
|
+
|
|
71
|
+
# Build rsync --exclude flags
|
|
72
|
+
wiki_excludes=""
|
|
73
|
+
for ex in "${WIKI_EX[@]:-}"; do
|
|
74
|
+
[ -n "$ex" ] && wiki_excludes="$wiki_excludes --exclude='$ex'"
|
|
75
|
+
done
|
|
76
|
+
echo "WIKI_EXCLUDES='$wiki_excludes'"
|
|
77
|
+
|
|
78
|
+
global_excludes=""
|
|
79
|
+
for ex in "${GLOBAL_EX[@]:-}"; do
|
|
80
|
+
[ -n "$ex" ] && global_excludes="$global_excludes --exclude='$ex'"
|
|
81
|
+
done
|
|
82
|
+
echo "GLOBAL_EXCLUDES='$global_excludes'"
|
|
83
|
+
|
|
84
|
+
# Newline-separated for safe iteration (patterns may contain spaces/globs)
|
|
85
|
+
blocked_str=""
|
|
86
|
+
for pat in "${BLOCKED[@]:-}"; do
|
|
87
|
+
[ -n "$pat" ] && blocked_str="$blocked_str$pat"$'\n'
|
|
88
|
+
done
|
|
89
|
+
# Use heredoc-style to safely pass patterns with special chars
|
|
90
|
+
cat <<BLOCKED_EOF
|
|
91
|
+
BLOCKED_PATTERNS='$(echo -n "$blocked_str")'
|
|
92
|
+
BLOCKED_EOF
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Write .mindos-build-version stamp after `next build`.
|
|
4
|
+
*
|
|
5
|
+
* This ensures Desktop's isNextBuildCurrent() recognizes builds
|
|
6
|
+
* done via `npm run build` (app/package.json) without going through
|
|
7
|
+
* the CLI's `mindos build` command.
|
|
8
|
+
*
|
|
9
|
+
* The stamp file is read by:
|
|
10
|
+
* - desktop/src/mindos-runtime-layout.ts (isNextBuildCurrent)
|
|
11
|
+
* - bin/lib/build.js (needsBuild / writeBuildStamp)
|
|
12
|
+
*
|
|
13
|
+
* Safe to run multiple times (idempotent).
|
|
14
|
+
*/
|
|
15
|
+
const { readFileSync, writeFileSync, existsSync } = require('fs');
|
|
16
|
+
const { resolve } = require('path');
|
|
17
|
+
|
|
18
|
+
const STAMP_NAME = '.mindos-build-version';
|
|
19
|
+
const appDir = resolve(__dirname, '..', 'app');
|
|
20
|
+
const nextDir = resolve(appDir, '.next');
|
|
21
|
+
const stampPath = resolve(nextDir, STAMP_NAME);
|
|
22
|
+
const pkgPath = resolve(__dirname, '..', 'package.json');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (!existsSync(nextDir)) {
|
|
26
|
+
// next build didn't produce output — nothing to stamp
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
30
|
+
const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
|
|
31
|
+
if (!version) {
|
|
32
|
+
console.warn('[write-build-stamp] No version in package.json, skipping');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
writeFileSync(stampPath, version, 'utf-8');
|
|
36
|
+
console.log(`[write-build-stamp] ${version} → ${stampPath}`);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Non-fatal — build succeeded, stamp is a bonus
|
|
39
|
+
console.warn('[write-build-stamp] Failed:', err.message);
|
|
40
|
+
}
|