synthos 0.9.0 → 0.10.1
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/default-pages/neon_asteroids/page.html +2 -21
- package/default-pages/oregon_trail/page.html +3 -19
- package/default-pages/solar_explorer/page.html +1 -1
- package/default-pages/western_cities_1850/page.html +2 -2
- package/dist/agents/a2a/a2aProvider.d.ts +3 -0
- package/dist/agents/discovery.d.ts +30 -0
- package/dist/agents/openclaw/openclawProvider.d.ts +3 -0
- package/dist/agents/types.d.ts +64 -0
- package/dist/connectors/index.d.ts +3 -0
- package/dist/connectors/types.d.ts +84 -0
- package/dist/customizer/Customizer.d.ts +5 -0
- package/dist/customizer/Customizer.d.ts.map +1 -1
- package/dist/customizer/Customizer.js +10 -0
- package/dist/customizer/Customizer.js.map +1 -1
- package/dist/customizer/index.d.ts +4 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +2 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +71 -37
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +12 -0
- package/dist/models/chainOfThought.d.ts +12 -0
- package/dist/models/fireworksai.d.ts +30 -0
- package/dist/models/logCompletePrompt.d.ts +3 -0
- package/dist/models/providers.d.ts +8 -0
- package/dist/models/utils.d.ts +6 -0
- package/dist/pages.d.ts +12 -11
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +94 -66
- package/dist/pages.js.map +1 -1
- package/dist/scripts.d.ts +15 -0
- package/dist/service/createCompletePrompt.d.ts +6 -0
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +2 -2
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/debugLog.d.ts +11 -0
- package/dist/service/generateImage.d.ts +32 -0
- package/dist/service/index.d.ts +8 -0
- package/dist/service/modelInstructions.d.ts +7 -0
- package/dist/service/requiresSettings.d.ts +4 -0
- package/dist/service/requiresSettings.d.ts.map +1 -1
- package/dist/service/requiresSettings.js +3 -3
- package/dist/service/requiresSettings.js.map +1 -1
- package/dist/service/server.d.ts +5 -0
- package/dist/service/useAgentRoutes.js +12 -12
- package/dist/service/useAgentRoutes.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts +5 -0
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +74 -60
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts +4 -0
- package/dist/service/useConnectorRoutes.js +11 -11
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts +4 -0
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +13 -10
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useFileRoutes.d.ts.map +1 -1
- package/dist/service/useFileRoutes.js +13 -13
- package/dist/service/useFileRoutes.js.map +1 -1
- package/dist/service/usePageRoutes.d.ts +6 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +54 -38
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +13 -10
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/service/useSharedFileRoutes.d.ts.map +1 -1
- package/dist/service/useSharedFileRoutes.js +13 -13
- package/dist/service/useSharedFileRoutes.js.map +1 -1
- package/dist/settings.d.ts +4 -3
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +11 -10
- package/dist/settings.js.map +1 -1
- package/dist/storage/FsStorageProvider.d.ts +25 -0
- package/dist/storage/FsStorageProvider.d.ts.map +1 -0
- package/dist/storage/FsStorageProvider.js +103 -0
- package/dist/storage/FsStorageProvider.js.map +1 -0
- package/dist/storage/StorageProvider.d.ts +31 -0
- package/dist/storage/StorageProvider.d.ts.map +1 -0
- package/dist/storage/StorageProvider.js +3 -0
- package/dist/storage/StorageProvider.js.map +1 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/synthos-cli.d.ts +2 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +42 -18
- package/dist/themes.js.map +1 -1
- package/package.json +1 -1
- package/src/builders/anthropic.ts +5 -5
- package/src/customizer/Customizer.ts +12 -0
- package/src/index.ts +2 -1
- package/src/init.ts +78 -42
- package/src/models/providers.ts +2 -2
- package/src/pages.ts +98 -67
- package/src/service/createCompletePrompt.ts +3 -2
- package/src/service/requiresSettings.ts +4 -3
- package/src/service/useAgentRoutes.ts +12 -12
- package/src/service/useApiRoutes.ts +76 -61
- package/src/service/useConnectorRoutes.ts +11 -11
- package/src/service/useDataRoutes.ts +13 -10
- package/src/service/useFileRoutes.ts +14 -13
- package/src/service/usePageRoutes.ts +42 -40
- package/src/service/useSharedDataRoutes.ts +13 -10
- package/src/service/useSharedFileRoutes.ts +14 -13
- package/src/settings.ts +12 -10
- package/src/storage/FsStorageProvider.ts +87 -0
- package/src/storage/StorageProvider.ts +34 -0
- package/src/storage/index.ts +2 -0
- package/src/themes.ts +44 -18
- package/tests/builders.spec.ts +2 -2
- package/tests/pages.spec.ts +54 -84
- package/tests/providers.spec.ts +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import { createReadStream } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { StorageProvider } from './StorageProvider';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default StorageProvider backed by the local filesystem via `fs/promises`.
|
|
8
|
+
* Mirrors the existing logic in `files.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export class FsStorageProvider implements StorageProvider {
|
|
11
|
+
// --- Text file operations ---
|
|
12
|
+
|
|
13
|
+
async checkIfExists(filePath: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(filePath);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async loadFile(filePath: string): Promise<string> {
|
|
23
|
+
return await fs.readFile(filePath, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async saveFile(filePath: string, content: string): Promise<void> {
|
|
27
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async deleteFile(filePath: string): Promise<void> {
|
|
31
|
+
await fs.unlink(filePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Binary file operations ---
|
|
35
|
+
|
|
36
|
+
async saveBuffer(filePath: string, data: Buffer): Promise<void> {
|
|
37
|
+
await fs.writeFile(filePath, data);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async loadBuffer(filePath: string): Promise<Buffer> {
|
|
41
|
+
return await fs.readFile(filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async stat(filePath: string): Promise<{ size: number; isFile: boolean }> {
|
|
45
|
+
const s = await fs.stat(filePath);
|
|
46
|
+
return { size: s.size, isFile: s.isFile() };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
createReadStream(filePath: string): NodeJS.ReadableStream {
|
|
50
|
+
return createReadStream(filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Directory operations ---
|
|
54
|
+
|
|
55
|
+
async listFiles(dirPath: string): Promise<string[]> {
|
|
56
|
+
return (await fs.readdir(dirPath)).filter(file => !file.startsWith('.') && file.includes('.'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async listFolders(dirPath: string): Promise<string[]> {
|
|
60
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
61
|
+
return entries
|
|
62
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
63
|
+
.map(entry => entry.name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async ensureFolderExists(dirPath: string): Promise<void> {
|
|
67
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async deleteFolder(dirPath: string): Promise<void> {
|
|
71
|
+
await fs.rm(dirPath, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async copyFolderRecursive(srcPath: string, destPath: string): Promise<void> {
|
|
75
|
+
await this.ensureFolderExists(destPath);
|
|
76
|
+
const entries = await fs.readdir(srcPath, { withFileTypes: true });
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const src = path.join(srcPath, entry.name);
|
|
79
|
+
const dest = path.join(destPath, entry.name);
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
await this.copyFolderRecursive(src, dest);
|
|
82
|
+
} else {
|
|
83
|
+
await fs.copyFile(src, dest);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstraction over user-mutable storage (pages, settings, themes, data, uploads).
|
|
3
|
+
*
|
|
4
|
+
* All paths are absolute. Callers build paths with `path.join(config.pagesFolder, ...)`
|
|
5
|
+
* exactly as they do today — the provider simply executes the I/O against
|
|
6
|
+
* whatever backend it wraps (local filesystem, blob storage, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Package/read-only content (required-pages, default-themes, static-files,
|
|
9
|
+
* service-connectors) stays on local filesystem and is NOT routed through
|
|
10
|
+
* this interface.
|
|
11
|
+
*/
|
|
12
|
+
export interface StorageProvider {
|
|
13
|
+
// --- Text file operations ---
|
|
14
|
+
|
|
15
|
+
checkIfExists(filePath: string): Promise<boolean>;
|
|
16
|
+
loadFile(filePath: string): Promise<string>;
|
|
17
|
+
saveFile(filePath: string, content: string): Promise<void>;
|
|
18
|
+
deleteFile(filePath: string): Promise<void>;
|
|
19
|
+
|
|
20
|
+
// --- Binary file operations (uploads / images) ---
|
|
21
|
+
|
|
22
|
+
saveBuffer(filePath: string, data: Buffer): Promise<void>;
|
|
23
|
+
loadBuffer(filePath: string): Promise<Buffer>;
|
|
24
|
+
stat(filePath: string): Promise<{ size: number; isFile: boolean }>;
|
|
25
|
+
createReadStream(filePath: string): NodeJS.ReadableStream;
|
|
26
|
+
|
|
27
|
+
// --- Directory operations ---
|
|
28
|
+
|
|
29
|
+
listFiles(dirPath: string): Promise<string[]>;
|
|
30
|
+
listFolders(dirPath: string): Promise<string[]>;
|
|
31
|
+
ensureFolderExists(dirPath: string): Promise<void>;
|
|
32
|
+
deleteFolder(dirPath: string): Promise<void>;
|
|
33
|
+
copyFolderRecursive(srcPath: string, destPath: string): Promise<void>;
|
|
34
|
+
}
|
package/src/themes.ts
CHANGED
|
@@ -28,10 +28,29 @@ export function parseThemeFilename(filename: string): { name: string; version: n
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Find the CSS file for a theme by name in a folder.
|
|
31
|
+
* Find the CSS file for a theme by name in a user folder (via storageProvider).
|
|
32
32
|
* Prefers the highest-versioned file (e.g. name.v2.css over name.css).
|
|
33
33
|
*/
|
|
34
|
-
async function
|
|
34
|
+
async function findUserThemeCssFile(config: SynthOSConfig, folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
|
|
35
|
+
const sp = config.storageProvider;
|
|
36
|
+
if (!await sp.checkIfExists(folder)) return undefined;
|
|
37
|
+
const files = await sp.listFiles(folder);
|
|
38
|
+
let best: { path: string; version: number } | undefined;
|
|
39
|
+
for (const f of files) {
|
|
40
|
+
const parsed = parseThemeFilename(f);
|
|
41
|
+
if (parsed && parsed.name === name) {
|
|
42
|
+
if (!best || parsed.version > best.version) {
|
|
43
|
+
best = { path: path.join(folder, f), version: parsed.version };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return best;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find the CSS file for a theme by name in a local-fs folder (package defaults).
|
|
52
|
+
*/
|
|
53
|
+
async function findDefaultThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
|
|
35
54
|
if (!await checkIfExists(folder)) return undefined;
|
|
36
55
|
const files = await listFiles(folder);
|
|
37
56
|
let best: { path: string; version: number } | undefined;
|
|
@@ -47,23 +66,26 @@ async function findThemeCssFile(folder: string, name: string): Promise<{ path: s
|
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
export async function loadThemeVersion(name: string, config: SynthOSConfig): Promise<number> {
|
|
50
|
-
const local = await
|
|
69
|
+
const local = await findUserThemeCssFile(config, userThemesFolder(config), name);
|
|
51
70
|
if (local) return local.version;
|
|
52
71
|
for (const folder of config.defaultThemesFolders) {
|
|
53
|
-
const def = await
|
|
72
|
+
const def = await findDefaultThemeCssFile(folder, name);
|
|
54
73
|
if (def) return def.version;
|
|
55
74
|
}
|
|
56
75
|
return 1;
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
export async function loadThemeInfo(name: string, config: SynthOSConfig): Promise<ThemeInfo | undefined> {
|
|
60
|
-
|
|
79
|
+
const sp = config.storageProvider;
|
|
80
|
+
|
|
81
|
+
// Check user's local themes first (user storage)
|
|
61
82
|
const localPath = path.join(userThemesFolder(config), `${name}.json`);
|
|
62
|
-
if (await checkIfExists(localPath)) {
|
|
63
|
-
const raw = await loadFile(localPath);
|
|
83
|
+
if (await sp.checkIfExists(localPath)) {
|
|
84
|
+
const raw = await sp.loadFile(localPath);
|
|
64
85
|
return raw ? JSON.parse(raw) : undefined;
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
// Fall back to package defaults (local fs)
|
|
67
89
|
const defaultPath = await findFileInFolders(config.defaultThemesFolders, `${name}.json`);
|
|
68
90
|
if (defaultPath) {
|
|
69
91
|
const raw = await loadFile(defaultPath);
|
|
@@ -74,15 +96,17 @@ export async function loadThemeInfo(name: string, config: SynthOSConfig): Promis
|
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
export async function loadTheme(name: string, config: SynthOSConfig): Promise<string | undefined> {
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
const sp = config.storageProvider;
|
|
100
|
+
|
|
101
|
+
// Check user's local themes first (user storage)
|
|
102
|
+
const local = await findUserThemeCssFile(config, userThemesFolder(config), name);
|
|
79
103
|
if (local) {
|
|
80
|
-
return await loadFile(local.path);
|
|
104
|
+
return await sp.loadFile(local.path);
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
// Search all default theme folders
|
|
107
|
+
// Search all default theme folders (local fs)
|
|
84
108
|
for (const folder of config.defaultThemesFolders) {
|
|
85
|
-
const def = await
|
|
109
|
+
const def = await findDefaultThemeCssFile(folder, name);
|
|
86
110
|
if (def) {
|
|
87
111
|
return await loadFile(def.path);
|
|
88
112
|
}
|
|
@@ -92,19 +116,20 @@ export async function loadTheme(name: string, config: SynthOSConfig): Promise<st
|
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
export async function listThemes(config: SynthOSConfig): Promise<string[]> {
|
|
119
|
+
const sp = config.storageProvider;
|
|
95
120
|
const names = new Set<string>();
|
|
96
121
|
|
|
97
|
-
// Collect from user's local themes folder
|
|
122
|
+
// Collect from user's local themes folder (user storage)
|
|
98
123
|
const localFolder = userThemesFolder(config);
|
|
99
|
-
if (await checkIfExists(localFolder)) {
|
|
100
|
-
const files = await listFiles(localFolder);
|
|
124
|
+
if (await sp.checkIfExists(localFolder)) {
|
|
125
|
+
const files = await sp.listFiles(localFolder);
|
|
101
126
|
for (const f of files) {
|
|
102
127
|
const parsed = parseThemeFilename(f);
|
|
103
128
|
if (parsed) names.add(parsed.name);
|
|
104
129
|
}
|
|
105
130
|
}
|
|
106
131
|
|
|
107
|
-
// Collect from all default theme folders
|
|
132
|
+
// Collect from all default theme folders (local fs)
|
|
108
133
|
const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
|
|
109
134
|
for (const f of defaultFiles) {
|
|
110
135
|
const parsed = parseThemeFilename(f);
|
|
@@ -118,11 +143,12 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
|
|
|
118
143
|
* Compare local theme versions against defaults and return themes that need upgrading.
|
|
119
144
|
*/
|
|
120
145
|
export async function getOutdatedThemes(config: SynthOSConfig): Promise<string[]> {
|
|
146
|
+
const sp = config.storageProvider;
|
|
121
147
|
const localFolder = userThemesFolder(config);
|
|
122
|
-
if (!await checkIfExists(localFolder)) return [];
|
|
148
|
+
if (!await sp.checkIfExists(localFolder)) return [];
|
|
123
149
|
|
|
124
150
|
const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
|
|
125
|
-
const localFiles = await listFiles(localFolder);
|
|
151
|
+
const localFiles = await sp.listFiles(localFolder);
|
|
126
152
|
|
|
127
153
|
// Build maps: theme name → highest version
|
|
128
154
|
const defaultVersions = new Map<string, number>();
|
package/tests/builders.spec.ts
CHANGED
|
@@ -46,7 +46,7 @@ describe('Anthropic Builder — Haiku Classifier Routing', () => {
|
|
|
46
46
|
|
|
47
47
|
const builder = createAnthropicBuilder(tracker.fn, undefined, 'SynthOS', {
|
|
48
48
|
apiKey: 'test-key',
|
|
49
|
-
model: 'claude-sonnet-4-
|
|
49
|
+
model: 'claude-sonnet-4-6-20250514',
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
const result = await builder.run(currentPage, additionalSections, 'change heading', false);
|
|
@@ -113,7 +113,7 @@ describe('Anthropic Builder — Haiku Classifier Routing', () => {
|
|
|
113
113
|
// Non-Opus model should just work without classification
|
|
114
114
|
const builder = createBuilder('Anthropic', tracker.fn, undefined, 'SynthOS', {
|
|
115
115
|
apiKey: 'key',
|
|
116
|
-
model: 'claude-sonnet-4-
|
|
116
|
+
model: 'claude-sonnet-4-6-20250514',
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
const result = await builder.run(currentPage, additionalSections, 'test', false);
|
package/tests/pages.spec.ts
CHANGED
|
@@ -9,12 +9,31 @@ import {
|
|
|
9
9
|
loadPageMetadata,
|
|
10
10
|
savePageState,
|
|
11
11
|
loadPageState,
|
|
12
|
-
updatePageState,
|
|
13
12
|
listPages,
|
|
14
13
|
deletePage,
|
|
15
14
|
copyPage,
|
|
16
15
|
PAGE_VERSION,
|
|
17
16
|
} from '../src/pages';
|
|
17
|
+
import { SynthOSConfig } from '../src/init';
|
|
18
|
+
import { FsStorageProvider } from '../src/storage';
|
|
19
|
+
|
|
20
|
+
/** Create a minimal SynthOSConfig for testing. */
|
|
21
|
+
function makeConfig(pagesFolder: string): SynthOSConfig {
|
|
22
|
+
return {
|
|
23
|
+
localFolder: '.synthos',
|
|
24
|
+
pagesFolder,
|
|
25
|
+
requiredPagesFolders: [],
|
|
26
|
+
defaultPagesFolders: [],
|
|
27
|
+
defaultScriptsFolders: [],
|
|
28
|
+
defaultThemesFolders: [],
|
|
29
|
+
staticFilesFolders: [],
|
|
30
|
+
serviceConnectorsFolders: [],
|
|
31
|
+
requiredPages: [],
|
|
32
|
+
storageProvider: new FsStorageProvider(),
|
|
33
|
+
debug: false,
|
|
34
|
+
debugPageUpdates: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
18
37
|
|
|
19
38
|
// ---------------------------------------------------------------------------
|
|
20
39
|
// normalizePageName
|
|
@@ -123,15 +142,16 @@ describe('parseMetadata', () => {
|
|
|
123
142
|
|
|
124
143
|
describe('page I/O', () => {
|
|
125
144
|
let tmpDir: string;
|
|
126
|
-
let
|
|
145
|
+
let config: SynthOSConfig;
|
|
127
146
|
let fallbackFolder: string;
|
|
128
147
|
|
|
129
148
|
beforeEach(async () => {
|
|
130
149
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synthos-test-'));
|
|
131
|
-
pagesFolder = path.join(tmpDir, 'synthos');
|
|
150
|
+
const pagesFolder = path.join(tmpDir, 'synthos');
|
|
132
151
|
fallbackFolder = path.join(tmpDir, 'fallback');
|
|
133
152
|
await fs.mkdir(pagesFolder, { recursive: true });
|
|
134
153
|
await fs.mkdir(fallbackFolder, { recursive: true });
|
|
154
|
+
config = makeConfig(pagesFolder);
|
|
135
155
|
});
|
|
136
156
|
|
|
137
157
|
afterEach(async () => {
|
|
@@ -152,8 +172,8 @@ describe('page I/O', () => {
|
|
|
152
172
|
pageVersion: 2,
|
|
153
173
|
mode: 'locked' as const,
|
|
154
174
|
};
|
|
155
|
-
await savePageMetadata(
|
|
156
|
-
const loaded = await loadPageMetadata(
|
|
175
|
+
await savePageMetadata(config, 'mypage', meta);
|
|
176
|
+
const loaded = await loadPageMetadata(config, 'mypage');
|
|
157
177
|
assert.ok(loaded);
|
|
158
178
|
assert.strictEqual(loaded.title, 'Test Page');
|
|
159
179
|
assert.deepStrictEqual(loaded.categories, ['Tools', 'Dev']);
|
|
@@ -163,17 +183,19 @@ describe('page I/O', () => {
|
|
|
163
183
|
});
|
|
164
184
|
|
|
165
185
|
it('returns undefined when no metadata file exists', async () => {
|
|
166
|
-
const loaded = await loadPageMetadata(
|
|
186
|
+
const loaded = await loadPageMetadata(config, 'nonexistent');
|
|
167
187
|
assert.strictEqual(loaded, undefined);
|
|
168
188
|
});
|
|
169
189
|
|
|
170
190
|
it('falls back to fallbackFolder', async () => {
|
|
171
|
-
// Write a fallback JSON file
|
|
191
|
+
// Write a fallback JSON file in folder-based structure
|
|
192
|
+
const fallbackPageDir = path.join(fallbackFolder, 'system-page');
|
|
193
|
+
await fs.mkdir(fallbackPageDir, { recursive: true });
|
|
172
194
|
await fs.writeFile(
|
|
173
|
-
path.join(
|
|
195
|
+
path.join(fallbackPageDir, 'page.json'),
|
|
174
196
|
JSON.stringify({ title: 'System', categories: ['System'], pinned: true, pageVersion: 2, mode: 'locked' }),
|
|
175
197
|
);
|
|
176
|
-
const loaded = await loadPageMetadata(
|
|
198
|
+
const loaded = await loadPageMetadata(config, 'system-page', [fallbackFolder]);
|
|
177
199
|
assert.ok(loaded);
|
|
178
200
|
assert.strictEqual(loaded.title, 'System');
|
|
179
201
|
});
|
|
@@ -184,89 +206,46 @@ describe('page I/O', () => {
|
|
|
184
206
|
describe('savePageState / loadPageState', () => {
|
|
185
207
|
it('roundtrips page HTML and creates metadata', async () => {
|
|
186
208
|
const html = '<html><body>Hello</body></html>';
|
|
187
|
-
await savePageState(
|
|
188
|
-
const loaded = await loadPageState(
|
|
209
|
+
await savePageState(config, 'testpage', html, 'Test Title', ['Cat']);
|
|
210
|
+
const loaded = await loadPageState(config, 'testpage');
|
|
189
211
|
assert.strictEqual(loaded, html);
|
|
190
212
|
|
|
191
213
|
// Metadata should have been created
|
|
192
|
-
const meta = await loadPageMetadata(
|
|
214
|
+
const meta = await loadPageMetadata(config, 'testpage');
|
|
193
215
|
assert.ok(meta);
|
|
194
216
|
assert.strictEqual(meta.pageVersion, PAGE_VERSION);
|
|
195
217
|
});
|
|
196
218
|
|
|
197
219
|
it('returns undefined for non-existent page', async () => {
|
|
198
|
-
const loaded = await loadPageState(
|
|
220
|
+
const loaded = await loadPageState(config, 'nope');
|
|
199
221
|
assert.strictEqual(loaded, undefined);
|
|
200
222
|
});
|
|
201
|
-
|
|
202
|
-
it('with reset=true re-reads from disk', async () => {
|
|
203
|
-
const html1 = '<html>v1</html>';
|
|
204
|
-
const html2 = '<html>v2</html>';
|
|
205
|
-
await savePageState(pagesFolder, 'resetpage', html1);
|
|
206
|
-
// First load
|
|
207
|
-
const first = await loadPageState(pagesFolder, 'resetpage', false);
|
|
208
|
-
assert.strictEqual(first, html1);
|
|
209
|
-
|
|
210
|
-
// Overwrite the file on disk directly
|
|
211
|
-
const pageHtmlPath = path.join(pagesFolder, 'pages', 'resetpage', 'page.html');
|
|
212
|
-
await fs.writeFile(pageHtmlPath, html2);
|
|
213
|
-
|
|
214
|
-
// Without reset, should return cached version
|
|
215
|
-
const cached = await loadPageState(pagesFolder, 'resetpage', false);
|
|
216
|
-
assert.strictEqual(cached, html1);
|
|
217
|
-
|
|
218
|
-
// With reset, should re-read
|
|
219
|
-
const refreshed = await loadPageState(pagesFolder, 'resetpage', true);
|
|
220
|
-
assert.strictEqual(refreshed, html2);
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// -- updatePageState -----------------------------------------------------
|
|
225
|
-
|
|
226
|
-
describe('updatePageState', () => {
|
|
227
|
-
it('updates in-memory cache', async () => {
|
|
228
|
-
await savePageState(pagesFolder, 'cachepage', '<html>original</html>');
|
|
229
|
-
await loadPageState(pagesFolder, 'cachepage', false);
|
|
230
|
-
|
|
231
|
-
updatePageState('cachepage', '<html>updated</html>');
|
|
232
|
-
|
|
233
|
-
// Loading without reset should return updated cache
|
|
234
|
-
const loaded = await loadPageState(pagesFolder, 'cachepage', false);
|
|
235
|
-
assert.strictEqual(loaded, '<html>updated</html>');
|
|
236
|
-
});
|
|
237
223
|
});
|
|
238
224
|
|
|
239
225
|
// -- listPages -----------------------------------------------------------
|
|
240
226
|
|
|
241
227
|
describe('listPages', () => {
|
|
242
228
|
it('lists folder-based pages', async () => {
|
|
243
|
-
await savePageState(
|
|
244
|
-
await savePageState(
|
|
245
|
-
const pages = await listPages(
|
|
229
|
+
await savePageState(config, 'alpha', '<html>A</html>');
|
|
230
|
+
await savePageState(config, 'beta', '<html>B</html>');
|
|
231
|
+
const pages = await listPages(config, [fallbackFolder]);
|
|
246
232
|
const names = pages.map(p => p.name);
|
|
247
233
|
assert.ok(names.includes('alpha'));
|
|
248
234
|
assert.ok(names.includes('beta'));
|
|
249
235
|
});
|
|
250
236
|
|
|
251
237
|
it('lists legacy flat HTML files', async () => {
|
|
252
|
-
await fs.writeFile(path.join(pagesFolder, 'legacy.html'), '<html>Legacy</html>');
|
|
253
|
-
const pages = await listPages(
|
|
238
|
+
await fs.writeFile(path.join(config.pagesFolder, 'legacy.html'), '<html>Legacy</html>');
|
|
239
|
+
const pages = await listPages(config, [fallbackFolder]);
|
|
254
240
|
const legacy = pages.find(p => p.name === 'legacy');
|
|
255
241
|
assert.ok(legacy);
|
|
256
242
|
assert.strictEqual(legacy.pageVersion, 1);
|
|
257
243
|
});
|
|
258
244
|
|
|
259
|
-
it('lists fallback (required) pages', async () => {
|
|
260
|
-
await fs.writeFile(path.join(fallbackFolder, 'builder.html'), '<html>Builder</html>');
|
|
261
|
-
const pages = await listPages(pagesFolder, [fallbackFolder]);
|
|
262
|
-
const builder = pages.find(p => p.name === 'builder');
|
|
263
|
-
assert.ok(builder);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
245
|
it('returns pages sorted alphabetically', async () => {
|
|
267
|
-
await savePageState(
|
|
268
|
-
await savePageState(
|
|
269
|
-
const pages = await listPages(
|
|
246
|
+
await savePageState(config, 'zebra', '<html></html>');
|
|
247
|
+
await savePageState(config, 'apple', '<html></html>');
|
|
248
|
+
const pages = await listPages(config, [fallbackFolder]);
|
|
270
249
|
const names = pages.map(p => p.name);
|
|
271
250
|
const sortedNames = [...names].sort();
|
|
272
251
|
assert.deepStrictEqual(names, sortedNames);
|
|
@@ -276,19 +255,18 @@ describe('page I/O', () => {
|
|
|
276
255
|
// -- deletePage ----------------------------------------------------------
|
|
277
256
|
|
|
278
257
|
describe('deletePage', () => {
|
|
279
|
-
it('removes folder-based page
|
|
280
|
-
await savePageState(
|
|
281
|
-
await loadPageState(pagesFolder, 'doomed', false);
|
|
258
|
+
it('removes folder-based page', async () => {
|
|
259
|
+
await savePageState(config, 'doomed', '<html>bye</html>');
|
|
282
260
|
|
|
283
|
-
await deletePage(
|
|
261
|
+
await deletePage(config, 'doomed');
|
|
284
262
|
|
|
285
263
|
// Folder should be gone
|
|
286
|
-
const folderExists = await fs.access(path.join(pagesFolder, 'pages', 'doomed'))
|
|
264
|
+
const folderExists = await fs.access(path.join(config.pagesFolder, 'pages', 'doomed'))
|
|
287
265
|
.then(() => true).catch(() => false);
|
|
288
266
|
assert.strictEqual(folderExists, false);
|
|
289
267
|
|
|
290
|
-
//
|
|
291
|
-
const loaded = await loadPageState(
|
|
268
|
+
// Loading should return undefined
|
|
269
|
+
const loaded = await loadPageState(config, 'doomed');
|
|
292
270
|
assert.strictEqual(loaded, undefined);
|
|
293
271
|
});
|
|
294
272
|
});
|
|
@@ -297,13 +275,13 @@ describe('page I/O', () => {
|
|
|
297
275
|
|
|
298
276
|
describe('copyPage', () => {
|
|
299
277
|
it('copies HTML and creates metadata with correct title/categories', async () => {
|
|
300
|
-
await savePageState(
|
|
301
|
-
await copyPage(
|
|
278
|
+
await savePageState(config, 'source', '<html>Source</html>');
|
|
279
|
+
await copyPage(config, 'source', 'target', 'Copied Page', ['Copy'], [fallbackFolder]);
|
|
302
280
|
|
|
303
|
-
const html = await loadPageState(
|
|
281
|
+
const html = await loadPageState(config, 'target');
|
|
304
282
|
assert.strictEqual(html, '<html>Source</html>');
|
|
305
283
|
|
|
306
|
-
const meta = await loadPageMetadata(
|
|
284
|
+
const meta = await loadPageMetadata(config, 'target');
|
|
307
285
|
assert.ok(meta);
|
|
308
286
|
assert.strictEqual(meta.title, 'Copied Page');
|
|
309
287
|
assert.deepStrictEqual(meta.categories, ['Copy']);
|
|
@@ -312,17 +290,9 @@ describe('page I/O', () => {
|
|
|
312
290
|
|
|
313
291
|
it('throws when source page does not exist', async () => {
|
|
314
292
|
await assert.rejects(
|
|
315
|
-
() => copyPage(
|
|
293
|
+
() => copyPage(config, 'ghost', 'target', 'T', [], [fallbackFolder]),
|
|
316
294
|
/Source page "ghost" not found/,
|
|
317
295
|
);
|
|
318
296
|
});
|
|
319
|
-
|
|
320
|
-
it('copies from required pages folder as fallback', async () => {
|
|
321
|
-
await fs.writeFile(path.join(fallbackFolder, 'system.html'), '<html>System</html>');
|
|
322
|
-
await copyPage(pagesFolder, 'system', 'mycopy', 'My Copy', ['User'], [fallbackFolder]);
|
|
323
|
-
|
|
324
|
-
const html = await loadPageState(pagesFolder, 'mycopy', true);
|
|
325
|
-
assert.strictEqual(html, '<html>System</html>');
|
|
326
|
-
});
|
|
327
297
|
});
|
|
328
298
|
});
|
package/tests/providers.spec.ts
CHANGED
|
@@ -39,7 +39,7 @@ describe('getProvider', () => {
|
|
|
39
39
|
|
|
40
40
|
describe('detectProvider', () => {
|
|
41
41
|
it('detects Anthropic from claude- prefix', () => {
|
|
42
|
-
const p = detectProvider('claude-sonnet-4-
|
|
42
|
+
const p = detectProvider('claude-sonnet-4-6');
|
|
43
43
|
assert.strictEqual(p?.name, 'Anthropic');
|
|
44
44
|
});
|
|
45
45
|
|