webspresso 0.0.94 → 0.0.96
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/.agents/ADMIN.md +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/agents-init.js +169 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/new.js +13 -2
- package/bin/commands/page.js +75 -89
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +19 -6
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +207 -0
- package/plugins/index.js +8 -0
- package/plugins/polar/CHANGELOG.md +18 -0
- package/plugins/polar/README.md +30 -7
- package/plugins/polar/index.js +6 -1
- package/plugins/polar/src/billing-hooks.js +51 -0
- package/plugins/polar/src/config.js +2 -0
- package/plugins/polar/src/rate-limit.js +96 -28
- package/plugins/polar/src/routes.js +3 -2
- package/plugins/polar/src/urls.js +26 -2
- package/plugins/polar/src/user-resolver.js +5 -1
- package/plugins/rate-limit/index.js +5 -1
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +6 -0
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/index.js +2 -0
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- package/templates/skills/webspresso-usage/SKILL.md +0 -48
package/src/server.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
9
10
|
const express = require('express');
|
|
10
11
|
const helmet = require('helmet');
|
|
@@ -18,6 +19,10 @@ const { mountPages, detectLocale, loadI18n, createTranslator } = require('./file
|
|
|
18
19
|
const { configureAssets, createHelpers, getScriptInjector } = require('./helpers');
|
|
19
20
|
const { createPluginManager } = require('./plugin-manager');
|
|
20
21
|
const { createServiceRegistry } = require('./services');
|
|
22
|
+
const { scanRoutes } = require('./discovery/scan-routes');
|
|
23
|
+
const { compileRouteTable } = require('./routing/route-table');
|
|
24
|
+
const { mountDiscoveredRoutes } = require('./routing/mount-discovered-routes');
|
|
25
|
+
const { discoverModuleDetails } = require('./modules/module-discovery');
|
|
21
26
|
const { ShutdownManager, NodeHttpAdapter } = require('../core/shutdown');
|
|
22
27
|
const { createCompressionMiddleware } = require('../core/compression');
|
|
23
28
|
const {
|
|
@@ -390,7 +395,7 @@ function createApp(options = {}) {
|
|
|
390
395
|
const isTest = NODE_ENV === 'test';
|
|
391
396
|
|
|
392
397
|
const {
|
|
393
|
-
pagesDir,
|
|
398
|
+
pagesDir: initialPagesDir,
|
|
394
399
|
viewsDir,
|
|
395
400
|
publicDir,
|
|
396
401
|
logging = isDev && !isTest,
|
|
@@ -404,6 +409,26 @@ function createApp(options = {}) {
|
|
|
404
409
|
setupRoutes,
|
|
405
410
|
} = options;
|
|
406
411
|
|
|
412
|
+
if (!initialPagesDir && !options.pages && !options.api && !options.modules && !options.routes && !options.rootDir) {
|
|
413
|
+
throw new Error('pagesDir is required');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const rootDir = options.rootDir || process.cwd();
|
|
417
|
+
let pagesDir = initialPagesDir;
|
|
418
|
+
if (!pagesDir) {
|
|
419
|
+
if (typeof options.pages === 'string') {
|
|
420
|
+
pagesDir = path.isAbsolute(options.pages) ? options.pages : path.join(rootDir, options.pages);
|
|
421
|
+
} else if (typeof options.pages === 'object' && options.pages?.dir) {
|
|
422
|
+
pagesDir = path.isAbsolute(options.pages.dir) ? options.pages.dir : path.join(rootDir, options.pages.dir);
|
|
423
|
+
} else if (fs.existsSync(path.join(rootDir, 'src/pages'))) {
|
|
424
|
+
pagesDir = path.join(rootDir, 'src/pages');
|
|
425
|
+
} else if (fs.existsSync(path.join(rootDir, 'pages'))) {
|
|
426
|
+
pagesDir = path.join(rootDir, 'pages');
|
|
427
|
+
} else {
|
|
428
|
+
pagesDir = path.join(rootDir, 'pages');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
407
432
|
// Create plugin manager
|
|
408
433
|
const pluginManager = createPluginManager();
|
|
409
434
|
|
|
@@ -413,10 +438,6 @@ function createApp(options = {}) {
|
|
|
413
438
|
...assetsConfig
|
|
414
439
|
});
|
|
415
440
|
|
|
416
|
-
if (!pagesDir) {
|
|
417
|
-
throw new Error('pagesDir is required');
|
|
418
|
-
}
|
|
419
|
-
|
|
420
441
|
const clientRuntime = resolveClientRuntime(options);
|
|
421
442
|
|
|
422
443
|
const shutdownConfig = {
|
|
@@ -474,6 +495,14 @@ function createApp(options = {}) {
|
|
|
474
495
|
const app = express();
|
|
475
496
|
app.serviceRegistry = serviceRegistry;
|
|
476
497
|
|
|
498
|
+
// Correlation Request ID middleware
|
|
499
|
+
app.use((req, res, next) => {
|
|
500
|
+
const reqId = (req.headers && req.headers['x-request-id']) ? req.headers['x-request-id'] : crypto.randomUUID();
|
|
501
|
+
req.id = reqId;
|
|
502
|
+
res.setHeader('X-Request-Id', reqId);
|
|
503
|
+
next();
|
|
504
|
+
});
|
|
505
|
+
|
|
477
506
|
// Request context & services caller middleware
|
|
478
507
|
app.use((req, res, next) => {
|
|
479
508
|
if (options.db) {
|
|
@@ -685,7 +714,10 @@ function createApp(options = {}) {
|
|
|
685
714
|
mountClientRuntime(app, clientRuntime);
|
|
686
715
|
|
|
687
716
|
// Configure Nunjucks with viewsDir priority and circular extension guard
|
|
688
|
-
const templateDirs =
|
|
717
|
+
const templateDirs = [viewsDir, pagesDir].filter(Boolean);
|
|
718
|
+
if (templateDirs.length === 0) {
|
|
719
|
+
templateDirs.push(path.join(rootDir, 'pages'));
|
|
720
|
+
}
|
|
689
721
|
|
|
690
722
|
const nunjucksEnv = configureSafeNunjucks(templateDirs, {
|
|
691
723
|
autoescape: true,
|
|
@@ -731,7 +763,33 @@ function createApp(options = {}) {
|
|
|
731
763
|
}
|
|
732
764
|
return d.toString();
|
|
733
765
|
});
|
|
734
|
-
|
|
766
|
+
|
|
767
|
+
// Auto-discover module services and middlewares if modules directory exists
|
|
768
|
+
const modulesDirCandidate = options.modules?.dir
|
|
769
|
+
? (path.isAbsolute(options.modules.dir) ? options.modules.dir : path.join(rootDir, options.modules.dir))
|
|
770
|
+
: (fs.existsSync(path.join(rootDir, 'src/modules')) ? path.join(rootDir, 'src/modules') : (fs.existsSync(path.join(rootDir, 'modules')) ? path.join(rootDir, 'modules') : null));
|
|
771
|
+
|
|
772
|
+
if (modulesDirCandidate && fs.existsSync(modulesDirCandidate) && options.modules !== false) {
|
|
773
|
+
try {
|
|
774
|
+
const entries = fs.readdirSync(modulesDirCandidate, { withFileTypes: true });
|
|
775
|
+
for (const entry of entries) {
|
|
776
|
+
if (entry.isDirectory() && !entry.name.startsWith('_') && !entry.name.startsWith('.')) {
|
|
777
|
+
const details = discoverModuleDetails(path.join(modulesDirCandidate, entry.name), entry.name);
|
|
778
|
+
for (const [sName, sDef] of Object.entries(details.services)) {
|
|
779
|
+
if (!serviceRegistry.has(sName)) {
|
|
780
|
+
serviceRegistry.register(sName, sDef);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
for (const [mName, mFn] of Object.entries(details.middlewares)) {
|
|
784
|
+
middlewares[`${entry.name}.${mName}`] = mFn;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
} catch (mErr) {
|
|
789
|
+
console.warn('[webspresso] Warning discovering modules:', mErr.message);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
735
793
|
// Register plugins (sync) — middlewares is the same object later passed to mountPages
|
|
736
794
|
const pluginContext = { app, nunjucksEnv, options, middlewares, shutdownManager };
|
|
737
795
|
pluginManager.registerSync(plugins, pluginContext);
|
|
@@ -748,12 +806,29 @@ function createApp(options = {}) {
|
|
|
748
806
|
});
|
|
749
807
|
}
|
|
750
808
|
|
|
751
|
-
//
|
|
752
|
-
|
|
809
|
+
// 1. Scan and compile discovered routes (src/pages, src/api, modules/*)
|
|
810
|
+
const discoveredDescriptors = scanRoutes({
|
|
811
|
+
rootDir,
|
|
812
|
+
pages: options.pages !== undefined ? options.pages : (options.pagesDir ? { dir: options.pagesDir } : true),
|
|
813
|
+
api: options.api,
|
|
814
|
+
modules: options.modules,
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const routeTable = compileRouteTable(discoveredDescriptors, {
|
|
818
|
+
explicitRoutes: options.routes || [],
|
|
819
|
+
isDev,
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Expose RouteTable on app instance
|
|
823
|
+
app.routes = routeTable;
|
|
824
|
+
app.routeTable = routeTable;
|
|
825
|
+
|
|
826
|
+
if (!isTest && routeTable.size > 0) {
|
|
753
827
|
console.log('\nMounting routes:');
|
|
754
828
|
}
|
|
755
|
-
|
|
756
|
-
|
|
829
|
+
|
|
830
|
+
// Mount discovered routes
|
|
831
|
+
const { routeMetadata: discoveredMeta, registerDynamicDiscoveredRoutes } = mountDiscoveredRoutes(app, routeTable, {
|
|
757
832
|
nunjucks: nunjucksEnv,
|
|
758
833
|
middlewares,
|
|
759
834
|
pluginManager,
|
|
@@ -762,10 +837,32 @@ function createApp(options = {}) {
|
|
|
762
837
|
clientRuntime,
|
|
763
838
|
pageAssets: options.pageAssets,
|
|
764
839
|
serviceRegistry,
|
|
840
|
+
options,
|
|
765
841
|
});
|
|
766
842
|
|
|
767
|
-
//
|
|
768
|
-
|
|
843
|
+
// 2. Mount classic file-based routes for backwards compatibility
|
|
844
|
+
let classicRouteMeta = [];
|
|
845
|
+
let registerDynamicFileRoutes = () => {};
|
|
846
|
+
if (pagesDir && fs.existsSync(path.resolve(pagesDir))) {
|
|
847
|
+
const classicMount = mountPages(app, {
|
|
848
|
+
pagesDir,
|
|
849
|
+
nunjucks: nunjucksEnv,
|
|
850
|
+
middlewares,
|
|
851
|
+
pluginManager,
|
|
852
|
+
silent: isTest,
|
|
853
|
+
db: options.db ?? null,
|
|
854
|
+
clientRuntime,
|
|
855
|
+
pageAssets: options.pageAssets,
|
|
856
|
+
serviceRegistry,
|
|
857
|
+
skipExistingRoutes: routeTable,
|
|
858
|
+
});
|
|
859
|
+
classicRouteMeta = classicMount.routeMetadata || [];
|
|
860
|
+
registerDynamicFileRoutes = classicMount.registerDynamicFileRoutes || (() => {});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Combine route metadata for plugin manager
|
|
864
|
+
const allRouteMeta = [...discoveredMeta, ...classicRouteMeta];
|
|
865
|
+
pluginManager.setRoutes(allRouteMeta);
|
|
769
866
|
|
|
770
867
|
// Call onRoutesReady hook synchronously (plugins should not be async in this phase)
|
|
771
868
|
// and mount any custom routes added by plugins
|
|
@@ -808,9 +905,9 @@ function createApp(options = {}) {
|
|
|
808
905
|
});
|
|
809
906
|
}
|
|
810
907
|
|
|
811
|
-
// Dynamic / catch-all file routes after plugins and setupRoutes
|
|
812
|
-
// or custom /login are not shadowed by pages/[slug].njk (/:slug).
|
|
908
|
+
// Dynamic / catch-all file routes after plugins and setupRoutes
|
|
813
909
|
registerDynamicFileRoutes();
|
|
910
|
+
registerDynamicDiscoveredRoutes();
|
|
814
911
|
|
|
815
912
|
// Helper to create error page context with fsy
|
|
816
913
|
function createErrorContext(req, extraData = {}) {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in File Manager Services
|
|
3
|
+
* @module src/services/builtins/file-manager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs/promises');
|
|
8
|
+
const { z } = require('zod');
|
|
9
|
+
const { ValidationError, NotFoundError } = require('../../../core/errors');
|
|
10
|
+
const {
|
|
11
|
+
listFiles,
|
|
12
|
+
createDirectory,
|
|
13
|
+
renameItem,
|
|
14
|
+
deleteItem,
|
|
15
|
+
moveItem,
|
|
16
|
+
saveUploadedFile,
|
|
17
|
+
} = require('../../../plugins/file-manager/core');
|
|
18
|
+
const { parseBase64 } = require('./media');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create built-in file manager services map
|
|
22
|
+
* @param {Object} [options]
|
|
23
|
+
* @param {string} [options.baseDir]
|
|
24
|
+
* @param {string} [options.publicBasePath]
|
|
25
|
+
* @returns {Record<string, Object>}
|
|
26
|
+
*/
|
|
27
|
+
function createFileManagerServices(options = {}) {
|
|
28
|
+
const defaultBaseDir = path.resolve(
|
|
29
|
+
options.baseDir || path.join(process.cwd(), 'public', 'uploads')
|
|
30
|
+
);
|
|
31
|
+
const defaultPublicBase = options.publicBasePath || '/uploads';
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
'fileManager.list': {
|
|
35
|
+
schema: z.object({
|
|
36
|
+
path: z.string().optional().default(''),
|
|
37
|
+
search: z.string().optional(),
|
|
38
|
+
type: z.string().optional(),
|
|
39
|
+
sort: z.enum(['name', 'size', 'mtime', 'ext']).optional().default('name'),
|
|
40
|
+
order: z.enum(['asc', 'desc']).optional().default('asc'),
|
|
41
|
+
page: z.number().int().positive().optional(),
|
|
42
|
+
perPage: z.number().int().positive().optional(),
|
|
43
|
+
baseDir: z.string().optional(),
|
|
44
|
+
publicBasePath: z.string().optional(),
|
|
45
|
+
}),
|
|
46
|
+
async handler(input) {
|
|
47
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
48
|
+
const publicBasePath = input.publicBasePath || defaultPublicBase;
|
|
49
|
+
|
|
50
|
+
return listFiles(baseDir, input.path, {
|
|
51
|
+
search: input.search,
|
|
52
|
+
type: input.type,
|
|
53
|
+
sort: input.sort,
|
|
54
|
+
order: input.order,
|
|
55
|
+
page: input.page,
|
|
56
|
+
perPage: input.perPage,
|
|
57
|
+
publicBasePath,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'fileManager.mkdir': {
|
|
63
|
+
schema: z.object({
|
|
64
|
+
path: z.string().optional().default(''),
|
|
65
|
+
name: z.string().min(1, 'Directory name is required'),
|
|
66
|
+
baseDir: z.string().optional(),
|
|
67
|
+
}),
|
|
68
|
+
async handler(input) {
|
|
69
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
70
|
+
return createDirectory(baseDir, input.path, input.name);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'fileManager.rename': {
|
|
75
|
+
schema: z.object({
|
|
76
|
+
path: z.string().min(1, 'Target path is required'),
|
|
77
|
+
newName: z.string().min(1, 'New name is required'),
|
|
78
|
+
baseDir: z.string().optional(),
|
|
79
|
+
}),
|
|
80
|
+
async handler(input) {
|
|
81
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
82
|
+
return renameItem(baseDir, input.path, input.newName);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
'fileManager.delete': {
|
|
87
|
+
schema: z.object({
|
|
88
|
+
path: z.string().min(1, 'Path is required'),
|
|
89
|
+
baseDir: z.string().optional(),
|
|
90
|
+
}),
|
|
91
|
+
async handler(input) {
|
|
92
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
93
|
+
return deleteItem(baseDir, input.path);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
'fileManager.move': {
|
|
98
|
+
schema: z.object({
|
|
99
|
+
source: z.string().min(1, 'Source path is required'),
|
|
100
|
+
destination: z.string().optional().default(''),
|
|
101
|
+
baseDir: z.string().optional(),
|
|
102
|
+
}),
|
|
103
|
+
async handler(input) {
|
|
104
|
+
const baseDir = input.baseDir ? path.resolve(input.baseDir) : defaultBaseDir;
|
|
105
|
+
return moveItem(baseDir, input.source, input.destination);
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
'fileManager.upload': {
|
|
110
|
+
schema: z.object({
|
|
111
|
+
path: z.string().optional().default(''),
|
|
112
|
+
buffer: z.any().optional(),
|
|
113
|
+
base64: z.string().optional(),
|
|
114
|
+
filePath: z.string().optional(),
|
|
115
|
+
originalName: z.string().min(1).default('upload.bin'),
|
|
116
|
+
mimeType: z.string().optional(),
|
|
117
|
+
baseDir: z.string().optional(),
|
|
118
|
+
publicBasePath: z.string().optional(),
|
|
119
|
+
}),
|
|
120
|
+
async handler(input) {
|
|
121
|
+
let {
|
|
122
|
+
path: targetRelPath,
|
|
123
|
+
buffer,
|
|
124
|
+
base64,
|
|
125
|
+
filePath,
|
|
126
|
+
originalName,
|
|
127
|
+
mimeType,
|
|
128
|
+
baseDir: customBaseDir,
|
|
129
|
+
publicBasePath: customPublicBase,
|
|
130
|
+
} = input;
|
|
131
|
+
const baseDir = customBaseDir ? path.resolve(customBaseDir) : defaultBaseDir;
|
|
132
|
+
const publicBasePath = customPublicBase || defaultPublicBase;
|
|
133
|
+
|
|
134
|
+
if (buffer && !(buffer instanceof Buffer) && !(buffer instanceof Uint8Array)) {
|
|
135
|
+
if (typeof buffer === 'string') {
|
|
136
|
+
buffer = Buffer.from(buffer);
|
|
137
|
+
} else {
|
|
138
|
+
throw new ValidationError('Invalid buffer provided');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!buffer && base64) {
|
|
143
|
+
const parsed = parseBase64(base64);
|
|
144
|
+
buffer = parsed.buffer;
|
|
145
|
+
if (parsed.mimeType && (!mimeType || mimeType === 'application/octet-stream')) {
|
|
146
|
+
mimeType = parsed.mimeType;
|
|
147
|
+
}
|
|
148
|
+
} else if (!buffer && filePath) {
|
|
149
|
+
try {
|
|
150
|
+
buffer = await fs.readFile(path.resolve(filePath));
|
|
151
|
+
if (!originalName || originalName === 'upload.bin') {
|
|
152
|
+
originalName = path.basename(filePath);
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
throw new NotFoundError(`Source file not found: ${filePath}`, { cause: err });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!buffer || buffer.length === 0) {
|
|
160
|
+
throw new ValidationError('No file content provided');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return saveUploadedFile(
|
|
164
|
+
baseDir,
|
|
165
|
+
targetRelPath,
|
|
166
|
+
buffer,
|
|
167
|
+
originalName,
|
|
168
|
+
mimeType,
|
|
169
|
+
publicBasePath
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
createFileManagerServices,
|
|
178
|
+
};
|
package/src/services/index.js
CHANGED
|
@@ -43,6 +43,7 @@ function createServiceRegistry(options = {}) {
|
|
|
43
43
|
const { createAuthServices } = require('./builtins/auth');
|
|
44
44
|
const { createMailServices } = require('./builtins/mail');
|
|
45
45
|
const { createMediaServices } = require('./builtins/media');
|
|
46
|
+
const { createFileManagerServices } = require('./builtins/file-manager');
|
|
46
47
|
const { createSystemServices } = require('./builtins/system');
|
|
47
48
|
const { createExchangeServices } = require('./builtins/exchange');
|
|
48
49
|
|
|
@@ -54,6 +55,7 @@ module.exports = {
|
|
|
54
55
|
createAuthServices,
|
|
55
56
|
createMailServices,
|
|
56
57
|
createMediaServices,
|
|
58
|
+
createFileManagerServices,
|
|
57
59
|
createSystemServices,
|
|
58
60
|
createExchangeServices,
|
|
59
61
|
memoize,
|
package/bin/commands/skill.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill Command — scaffold Agent Skill (SKILL.md) for Cursor / AI tools
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
let _inquirer;
|
|
9
|
-
const inquirer = {
|
|
10
|
-
prompt: (...args) => {
|
|
11
|
-
if (!_inquirer) _inquirer = require('inquirer');
|
|
12
|
-
return _inquirer.prompt(...args);
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$/;
|
|
17
|
-
|
|
18
|
-
function validateSkillName(name) {
|
|
19
|
-
const t = String(name).trim().toLowerCase();
|
|
20
|
-
if (!t) return 'Skill name is required';
|
|
21
|
-
if (t.length > 64) return 'Skill name must be at most 64 characters';
|
|
22
|
-
if (!NAME_PATTERN.test(t)) {
|
|
23
|
-
return 'Use lowercase letters, numbers, and hyphens only (e.g. my-skill-name)';
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function skillDir(base, skillName) {
|
|
29
|
-
return path.join(base, '.agents', 'skills', skillName);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function defaultDescription(skillName) {
|
|
34
|
-
const label = skillName.replace(/-/g, ' ');
|
|
35
|
-
return `Guides the agent through tasks for ${label}. Use when the user works on ${label} or asks about related workflows.`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Bundled presets: CLI flag → template directory (all *.md copied) */
|
|
39
|
-
const PRESETS = {
|
|
40
|
-
webspresso: {
|
|
41
|
-
skillName: 'webspresso-usage',
|
|
42
|
-
templateDir: path.join(__dirname, '../../templates/skills/webspresso-usage'),
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function buildSkillMarkdown(skillName, description) {
|
|
47
|
-
const title = skillName
|
|
48
|
-
.split('-')
|
|
49
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
50
|
-
.join(' ');
|
|
51
|
-
const descYaml = JSON.stringify(description.trim());
|
|
52
|
-
|
|
53
|
-
return `---
|
|
54
|
-
name: ${skillName}
|
|
55
|
-
description: ${descYaml}
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
# ${title}
|
|
59
|
-
|
|
60
|
-
## Instructions
|
|
61
|
-
|
|
62
|
-
- Describe step-by-step what the agent should do.
|
|
63
|
-
- Add constraints, file paths, and conventions for this project.
|
|
64
|
-
|
|
65
|
-
## When to use
|
|
66
|
-
|
|
67
|
-
- List concrete user phrases or tasks that should trigger this skill.
|
|
68
|
-
|
|
69
|
-
## Examples
|
|
70
|
-
|
|
71
|
-
\`\`\`
|
|
72
|
-
Example prompt or command
|
|
73
|
-
\`\`\`
|
|
74
|
-
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function registerCommand(program) {
|
|
80
|
-
program
|
|
81
|
-
.command('skill [name]')
|
|
82
|
-
.description('Create an Agent Skill folder with SKILL.md')
|
|
83
|
-
.option('-g, --global', 'Write to ~/.agents/skills/ instead of ./.agents/skills/')
|
|
84
|
-
.option('-d, --description <text>', 'Skill description (for YAML frontmatter)')
|
|
85
|
-
.option('-f, --force', 'Overwrite existing SKILL.md')
|
|
86
|
-
.option(
|
|
87
|
-
'-p, --preset <name>',
|
|
88
|
-
'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .agents/skills/webspresso-usage/)',
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
.action(async (nameArg, options) => {
|
|
92
|
-
const presetKey = options.preset ? String(options.preset).trim().toLowerCase() : '';
|
|
93
|
-
|
|
94
|
-
if (presetKey) {
|
|
95
|
-
const preset = PRESETS[presetKey];
|
|
96
|
-
if (!preset) {
|
|
97
|
-
console.error(`❌ Unknown preset "${presetKey}". Available: ${Object.keys(PRESETS).join(', ')}`);
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
const templateDir = preset.templateDir;
|
|
101
|
-
if (!templateDir || !fs.existsSync(templateDir)) {
|
|
102
|
-
console.error(`❌ Preset template directory missing: ${templateDir}`);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
const mdFiles = fs
|
|
106
|
-
.readdirSync(templateDir)
|
|
107
|
-
.filter((f) => f.endsWith('.md'));
|
|
108
|
-
if (mdFiles.length === 0) {
|
|
109
|
-
console.error(`❌ No .md files in preset: ${templateDir}`);
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
const root = options.global ? os.homedir() : process.cwd();
|
|
113
|
-
const dir = skillDir(root, preset.skillName);
|
|
114
|
-
const skillFile = path.join(dir, 'SKILL.md');
|
|
115
|
-
|
|
116
|
-
if (!options.force && fs.existsSync(skillFile)) {
|
|
117
|
-
console.error(`❌ Already exists: ${skillFile}`);
|
|
118
|
-
console.error(' Use --force to overwrite.');
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
123
|
-
for (const file of mdFiles) {
|
|
124
|
-
fs.copyFileSync(path.join(templateDir, file), path.join(dir, file));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
console.log(`\n✅ Installed bundled skill "${presetKey}" →\n ${dir}/\n`);
|
|
128
|
-
console.log(` Files: ${mdFiles.join(', ')}\n`);
|
|
129
|
-
console.log(' Edit if needed, then restart Cursor or reload the window.\n');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let skillName = nameArg ? String(nameArg).trim().toLowerCase() : '';
|
|
134
|
-
|
|
135
|
-
if (!skillName) {
|
|
136
|
-
const { name } = await inquirer.prompt([
|
|
137
|
-
{
|
|
138
|
-
type: 'input',
|
|
139
|
-
name: 'name',
|
|
140
|
-
message: 'Skill name (kebab-case, e.g. review-api):',
|
|
141
|
-
validate: validateSkillName,
|
|
142
|
-
},
|
|
143
|
-
]);
|
|
144
|
-
skillName = name.trim().toLowerCase();
|
|
145
|
-
} else {
|
|
146
|
-
const v = validateSkillName(skillName);
|
|
147
|
-
if (v !== true) {
|
|
148
|
-
console.error(`❌ ${v}`);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
skillName = skillName.trim().toLowerCase();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let description = options.description;
|
|
155
|
-
if (!description) {
|
|
156
|
-
const { desc } = await inquirer.prompt([
|
|
157
|
-
{
|
|
158
|
-
type: 'input',
|
|
159
|
-
name: 'desc',
|
|
160
|
-
message: 'Short description (what/when the agent should use this skill):',
|
|
161
|
-
default: defaultDescription(skillName),
|
|
162
|
-
},
|
|
163
|
-
]);
|
|
164
|
-
description = desc || defaultDescription(skillName);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const root = options.global ? os.homedir() : process.cwd();
|
|
168
|
-
const dir = skillDir(root, skillName);
|
|
169
|
-
const skillFile = path.join(dir, 'SKILL.md');
|
|
170
|
-
|
|
171
|
-
if (!options.force && fs.existsSync(skillFile)) {
|
|
172
|
-
console.error(`❌ Already exists: ${skillFile}`);
|
|
173
|
-
console.error(' Use --force to overwrite.');
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
178
|
-
|
|
179
|
-
const body = buildSkillMarkdown(skillName, description);
|
|
180
|
-
fs.writeFileSync(skillFile, body, 'utf8');
|
|
181
|
-
|
|
182
|
-
console.log(`\n✅ Agent skill created:\n ${skillFile}\n`);
|
|
183
|
-
console.log(' Edit SKILL.md, then restart Cursor or reload the window if needed.\n');
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
module.exports = { registerCommand };
|