webspresso 0.0.73 → 0.0.75
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/README.md +44 -4
- package/bin/commands/orm-map.js +139 -0
- package/bin/commands/skill.js +22 -8
- package/bin/commands/upgrade.js +146 -0
- package/bin/utils/orm-map-html.js +689 -0
- package/bin/utils/orm-map-load.js +85 -0
- package/bin/utils/orm-map-snapshot.js +179 -0
- package/bin/utils/resolve-webspresso-orm.js +23 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/manager.js +14 -1
- package/core/kernel/app.js +96 -0
- package/core/kernel/base-repository.js +143 -0
- package/core/kernel/events.js +101 -0
- package/core/kernel/flow.js +22 -0
- package/core/kernel/index.js +17 -0
- package/core/kernel/plugin.js +23 -0
- package/core/kernel/plugins/sample-seo.js +26 -0
- package/core/kernel/run-demo.js +58 -0
- package/core/kernel/view.js +167 -0
- package/core/openapi/build-from-api-routes.js +8 -2
- package/core/orm/model.js +3 -1
- package/core/url-path-normalize.js +30 -0
- package/index.d.ts +168 -1
- package/index.js +20 -2
- package/package.json +11 -1
- package/plugins/admin-panel/api.js +43 -15
- package/plugins/admin-panel/app.js +109 -0
- package/plugins/admin-panel/client/README.md +39 -0
- package/plugins/admin-panel/client/load-parts.js +74 -0
- package/plugins/admin-panel/client/manifest.parts.json +12 -0
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
- package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
- package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
- package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
- package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
- package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
- package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
- package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
- package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
- package/plugins/admin-panel/components.js +4 -2640
- package/plugins/admin-panel/core/api-extensions.js +100 -10
- package/plugins/admin-panel/index.js +3 -0
- package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
- package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
- package/plugins/admin-panel/modules/dashboard.js +17 -13
- package/plugins/admin-panel/modules/user-management.js +118 -27
- package/plugins/data-exchange/export-xlsx.js +3 -0
- package/plugins/data-exchange/record-selection.js +21 -5
- package/plugins/index.js +4 -0
- package/plugins/rate-limit/index.js +178 -0
- package/plugins/redirect/index.js +204 -0
- package/plugins/rest-resources/index.js +2 -1
- package/plugins/site-analytics/admin-component.js +88 -78
- package/plugins/swagger.js +2 -1
- package/plugins/upload/local-file-provider.js +6 -2
- package/src/file-router.js +270 -53
- package/src/njk-frontmatter.js +156 -0
- package/src/plugin-manager.js +4 -2
- package/src/server.js +28 -9
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +276 -0
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
- package/templates/skills/webspresso-usage/SKILL.md +29 -275
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
|
|
|
19
19
|
- **Session authentication** (optional): `createAuth` / `quickAuth` in **`webspresso/core/auth`** — pass the manager to **`createApp({ auth })`** for `express-session`, `req.user` / `req.auth`, remember-me tokens, and policy-style authorization. Full walkthrough: **[`doc/index.html#authentication`](doc/index.html#authentication)**.
|
|
20
20
|
- **Optional client runtime** (Alpine.js + [swup](https://swup.js.org/)): **`createApp({ clientRuntime: { alpine, swup } })`** serves scripts under **`/__webspresso/client-runtime/`** and exposes **`clientRuntime`** in Nunjucks; layouts can include **`views/partials/webspresso-client-runtime.njk`**. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`**. Demo: **`examples/alpine-swup-demo/`**. Details: **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
|
|
21
21
|
- **TypeScript**: Published **`index.d.ts`** (via `package.json` `"types"`) for `createApp`, ORM, plugins, and router helpers — use from TS/JS with IDE autocomplete; runtime stays CommonJS
|
|
22
|
+
- **Application kernel (optional)**: In-process **`kernel`** API (`require('webspresso').kernel`) — event bus (`dispatch` / `publish`), **`kernel.createApp()`** (namespaced differently from SSR **`createApp`**), **`definePlugin`** / **`defineFlow`**, minimal **`{{ }}` view resolver**, and simulated **`BaseRepository`** with `orm.<resource>.*` events. Ships as **`core/kernel/`** on npm. Demo: **`node core/kernel/run-demo.js`**. Docs: **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)**.
|
|
22
23
|
|
|
23
24
|
## Installation
|
|
24
25
|
|
|
@@ -40,6 +41,18 @@ Install **`@types/express`** in your app if you want full **`Express.Application
|
|
|
40
41
|
|
|
41
42
|
Framework development (this repo): run **`npm run check:types`** to typecheck the declarations against a small smoke file (`tests/ts-smoke/`).
|
|
42
43
|
|
|
44
|
+
## Application kernel (`kernel`)
|
|
45
|
+
|
|
46
|
+
Do **not** confuse **`kernel.createApp()`** with the package root **`createApp`** used for SSR—it returns a different object (event bus, optional flows, and a minimal view resolver). It does **not** modify Knex ORM behavior or Express routing.
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
const { kernel } = require('webspresso');
|
|
50
|
+
const app = kernel.createApp();
|
|
51
|
+
app.events.on('orm.post.afterCreate', async (ctx) => { /* ... */ });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
See **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)** · source modules: [`core/kernel/`](core/kernel/). Cursor skill: **`REFERENCE-kernel.md`** (installed via `webspresso skill --preset webspresso`).
|
|
55
|
+
|
|
43
56
|
## Quick Start
|
|
44
57
|
|
|
45
58
|
### Using CLI (Recommended)
|
|
@@ -234,7 +247,7 @@ webspresso doctor --strict
|
|
|
234
247
|
|
|
235
248
|
Scaffold a **Cursor Agent Skill**: creates `.cursor/skills/<name>/SKILL.md` with valid YAML frontmatter (`name`, `description`) and a short markdown template for AI tooling. Use `--global` to write under `~/.cursor/skills/` instead of the current project.
|
|
236
249
|
|
|
237
|
-
**Bundled preset:** `--preset webspresso` copies
|
|
250
|
+
**Bundled preset:** `--preset webspresso` copies **`SKILL.md`** (short index), **`REFERENCE-framework.md`** (SSR `createApp`, routes, ORM, auth, plugins, CLI), and **`REFERENCE-kernel.md`** (`kernel` event bus / flows) into **`.cursor/skills/webspresso-usage/`** — no prompts.
|
|
238
251
|
|
|
239
252
|
```bash
|
|
240
253
|
webspresso skill my-workflow
|
|
@@ -533,6 +546,14 @@ Error templates receive these variables:
|
|
|
533
546
|
- `500.njk`: `{ error, status, isDev }`
|
|
534
547
|
- `503.njk`: `{ url, method, isDev }`
|
|
535
548
|
|
|
549
|
+
**How errors reach this handler**
|
|
550
|
+
|
|
551
|
+
Unhandled errors from file-based routes (`pages/**/*.njk` `load()` / middleware / render, and `pages/api/**/*.js` handlers) are forwarded with `next(err)`, so they go through this 4-argument Express error middleware. That means `errorPages.serverError` and `errorPages.timeout` apply to those failures as well (not only to routes you add with `setupRoutes`).
|
|
552
|
+
|
|
553
|
+
- **`pages/_hooks.js` `onError(ctx, err)`** runs **before** the central handler (for both SSR and API file routes). Use it for logging or APM (`Sentry.captureException`, `newrelic.noticeError`, etc.). The error is also on **`ctx.error`**.
|
|
554
|
+
|
|
555
|
+
- **JSON vs HTML:** Requests whose path starts with **`/api`** always get a **JSON** error body from the default branch (and the default **string** `serverError` / `timeout` Nunjucks templates are skipped for those paths). Other clients use **`Accept`** as before: prefer HTML when the client accepts HTML.
|
|
556
|
+
|
|
536
557
|
**Request Timeout:**
|
|
537
558
|
|
|
538
559
|
Configure request timeout with `connect-timeout`:
|
|
@@ -682,6 +703,22 @@ Options:
|
|
|
682
703
|
- `path` - Custom dashboard path (default: `/_webspresso`)
|
|
683
704
|
- `enabled` - Force enable/disable (default: auto based on NODE_ENV)
|
|
684
705
|
|
|
706
|
+
**Redirect plugin:**
|
|
707
|
+
- Runs in `register()` **before** file-based routes, so configured paths override SSR pages.
|
|
708
|
+
- Rules: `from` (string or `RegExp`), `to` (path or URL), optional `status` (301/302/303/307/308), optional `methods` (`'*'` or a list; default plugin methods are `GET` and `HEAD` only).
|
|
709
|
+
- `preserveQuery` (default `true`) appends the request query when `to` has no `?`. External `to` values require `allowExternal: true`.
|
|
710
|
+
|
|
711
|
+
```javascript
|
|
712
|
+
const { redirectPlugin } = require('webspresso/plugins');
|
|
713
|
+
|
|
714
|
+
redirectPlugin({
|
|
715
|
+
rules: [
|
|
716
|
+
{ from: '/old-blog', to: '/blog', status: 301 },
|
|
717
|
+
{ from: /^\/wiki\/(.*)$/, to: '/docs' },
|
|
718
|
+
],
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
685
722
|
**Sitemap Plugin:**
|
|
686
723
|
- Generates `/sitemap.xml` from routes automatically
|
|
687
724
|
- **Dynamic Database Content**: Generate URLs from database records
|
|
@@ -764,10 +801,12 @@ const { app } = createApp({
|
|
|
764
801
|
Options:
|
|
765
802
|
- `db` (required) - Database instance
|
|
766
803
|
- `path` - Admin panel path (default: `/_admin`)
|
|
767
|
-
- `auth` -
|
|
768
|
-
- `userManagement` -
|
|
804
|
+
- `auth` - Same **`AuthManager`** instance as **`createApp({ auth })`** when you use **`userManagement`** — enables **Active Sessions** / revoke APIs if **`rememberTokens`** (remember-me) is configured; optional for user CRUD-only
|
|
805
|
+
- `userManagement` - Site-user admin UI (`enabled`, `model` matching ORM user table, optional `fields` map). SPA routes: `/_admin/users`, `/_admin/users/new`, …; APIs: `/_admin/api/users*`. Admin staff still use **`admin_users`** / `/_admin` login; this is separate from **`req.user`** on the public site
|
|
769
806
|
- `configure` - Callback `(registry) => void` for manual setup
|
|
770
807
|
|
|
808
|
+
See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.cursor/skills/webspresso-usage/REFERENCE-framework.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
|
|
809
|
+
|
|
771
810
|
**Custom Admin Pages (registerModule):**
|
|
772
811
|
|
|
773
812
|
Plugins can add custom admin pages using `registerModule` in `onRoutesReady`:
|
|
@@ -2208,8 +2247,9 @@ npm run test:watch
|
|
|
2208
2247
|
# Run tests with coverage
|
|
2209
2248
|
npm run test:coverage
|
|
2210
2249
|
|
|
2211
|
-
# Micro-benchmarks (Vitest bench;
|
|
2250
|
+
# Micro-benchmarks (Vitest bench; CI: main push uploads benchmark-baseline artifact, PRs compare against it)
|
|
2212
2251
|
npm run bench
|
|
2252
|
+
# Local baseline + compare: npm run bench:ci:local
|
|
2213
2253
|
```
|
|
2214
2254
|
|
|
2215
2255
|
## License
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORM map — interactive single-page HTML of models and relationships
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
const { getWebspressoOrmForProject } = require('../utils/resolve-webspresso-orm');
|
|
10
|
+
const { loadProjectModels } = require('../utils/orm-map-load');
|
|
11
|
+
const { buildSnapshot, buildMermaidErDiagram } = require('../utils/orm-map-snapshot');
|
|
12
|
+
const { buildOrmMapHtml, readPackageName } = require('../utils/orm-map-html');
|
|
13
|
+
|
|
14
|
+
function ensureOpenInsideTrustedRoots(absFile, cwd) {
|
|
15
|
+
if (!fs.existsSync(absFile)) {
|
|
16
|
+
throw new Error(`File does not exist: ${absFile}`);
|
|
17
|
+
}
|
|
18
|
+
let realFile;
|
|
19
|
+
try {
|
|
20
|
+
realFile = fs.realpathSync(absFile);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
throw new Error(`Cannot resolve file path: ${absFile}`, { cause: e });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @type {string[]} */
|
|
26
|
+
const roots = [];
|
|
27
|
+
for (const base of [os.tmpdir(), cwd]) {
|
|
28
|
+
const resolvedBase = path.resolve(base);
|
|
29
|
+
try {
|
|
30
|
+
roots.push(fs.realpathSync(resolvedBase));
|
|
31
|
+
} catch {
|
|
32
|
+
roots.push(resolvedBase);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fileNorm = path.normalize(realFile + path.sep);
|
|
37
|
+
const trusted = roots.some((rootRaw) => {
|
|
38
|
+
const rr =
|
|
39
|
+
typeof rootRaw === 'string' && fs.existsSync(rootRaw)
|
|
40
|
+
? fs.realpathSync(rootRaw)
|
|
41
|
+
: path.normalize(rootRaw);
|
|
42
|
+
const rp = rr.endsWith(path.sep) ? rr : rr + path.sep;
|
|
43
|
+
return realFile === rr || fileNorm.startsWith(path.normalize(rp));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!trusted) {
|
|
47
|
+
throw new Error('Refused to open file outside cwd or OS temp dir');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function openFile(filePath, cwd) {
|
|
52
|
+
const fp = path.resolve(filePath);
|
|
53
|
+
ensureOpenInsideTrustedRoots(fp, cwd);
|
|
54
|
+
if (process.platform === 'darwin') {
|
|
55
|
+
execFileSync('open', [fp], { stdio: 'ignore' });
|
|
56
|
+
} else if (process.platform === 'win32') {
|
|
57
|
+
execFileSync('cmd', ['/c', 'start', '', fp], { stdio: 'ignore' });
|
|
58
|
+
} else {
|
|
59
|
+
execFileSync('xdg-open', [fp], { stdio: 'ignore' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function registerCommand(program) {
|
|
64
|
+
program
|
|
65
|
+
.command('orm:map')
|
|
66
|
+
.description(
|
|
67
|
+
'Generate a self-contained HTML map of ORM models and relations (opens in browser by default)',
|
|
68
|
+
)
|
|
69
|
+
.option('-o, --output <file>', 'Write HTML to this path instead of a temp file')
|
|
70
|
+
.option('--no-open', 'Do not open the browser')
|
|
71
|
+
.option('-c, --config <path>', 'Database config file (webspresso.db.js / knexfile.js) for models path')
|
|
72
|
+
.option('-e, --env <environment>', 'Config environment', 'development')
|
|
73
|
+
.option('-m, --models <dir>', 'Models directory (overrides config default ./models)')
|
|
74
|
+
.action((options) => {
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
const { modelsDir, loaded, errors } = loadProjectModels(cwd, {
|
|
77
|
+
modelsOverride: options.models,
|
|
78
|
+
configPath: options.config,
|
|
79
|
+
env: options.env,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
for (const err of errors) {
|
|
83
|
+
if (!err.file) {
|
|
84
|
+
console.error(`❌ ${err.message}`);
|
|
85
|
+
} else {
|
|
86
|
+
console.warn(`⚠️ ${err.file}: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (loaded.length === 0 && errors.some((e) => e.file === '')) {
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const orm = getWebspressoOrmForProject(cwd);
|
|
95
|
+
const registry = orm.getAllModels();
|
|
96
|
+
if (registry.size === 0) {
|
|
97
|
+
console.error(
|
|
98
|
+
`❌ No models registered. Checked: ${modelsDir}\n` +
|
|
99
|
+
' Add defineModel() files or pass --models <dir>.',
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const snapshot = buildSnapshot(registry);
|
|
105
|
+
const mermaid = buildMermaidErDiagram(snapshot);
|
|
106
|
+
const pkg = readPackageName(cwd);
|
|
107
|
+
const html = buildOrmMapHtml(snapshot, mermaid, {
|
|
108
|
+
title: 'ORM model map',
|
|
109
|
+
packageName: pkg,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let outPath;
|
|
113
|
+
if (options.output) {
|
|
114
|
+
outPath = path.resolve(cwd, options.output);
|
|
115
|
+
} else {
|
|
116
|
+
outPath = path.join(
|
|
117
|
+
os.tmpdir(),
|
|
118
|
+
`webspresso-orm-map-${Date.now()}.html`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
123
|
+
fs.writeFileSync(outPath, html, 'utf8');
|
|
124
|
+
|
|
125
|
+
console.log(`\n✅ ORM map (${registry.size} model(s))`);
|
|
126
|
+
console.log(` Models dir: ${modelsDir}`);
|
|
127
|
+
console.log(` Written: ${outPath}\n`);
|
|
128
|
+
|
|
129
|
+
if (options.open !== false) {
|
|
130
|
+
try {
|
|
131
|
+
openFile(outPath, cwd);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.warn(`⚠️ Could not open browser: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { registerCommand };
|
package/bin/commands/skill.js
CHANGED
|
@@ -28,11 +28,11 @@ function defaultDescription(skillName) {
|
|
|
28
28
|
return `Guides the agent through tasks for ${label}. Use when the user works on ${label} or asks about related workflows.`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** Bundled presets: CLI flag → template
|
|
31
|
+
/** Bundled presets: CLI flag → template directory (all *.md copied) */
|
|
32
32
|
const PRESETS = {
|
|
33
33
|
webspresso: {
|
|
34
34
|
skillName: 'webspresso-usage',
|
|
35
|
-
|
|
35
|
+
templateDir: path.join(__dirname, '../../templates/skills/webspresso-usage'),
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -76,7 +76,10 @@ function registerCommand(program) {
|
|
|
76
76
|
.option('-g, --global', 'Write to ~/.cursor/skills/ instead of ./.cursor/skills/')
|
|
77
77
|
.option('-d, --description <text>', 'Skill description (for YAML frontmatter)')
|
|
78
78
|
.option('-f, --force', 'Overwrite existing SKILL.md')
|
|
79
|
-
.option(
|
|
79
|
+
.option(
|
|
80
|
+
'-p, --preset <name>',
|
|
81
|
+
'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .cursor/skills/webspresso-usage/)',
|
|
82
|
+
)
|
|
80
83
|
.action(async (nameArg, options) => {
|
|
81
84
|
const presetKey = options.preset ? String(options.preset).trim().toLowerCase() : '';
|
|
82
85
|
|
|
@@ -86,8 +89,16 @@ function registerCommand(program) {
|
|
|
86
89
|
console.error(`❌ Unknown preset "${presetKey}". Available: ${Object.keys(PRESETS).join(', ')}`);
|
|
87
90
|
process.exit(1);
|
|
88
91
|
}
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
const templateDir = preset.templateDir;
|
|
93
|
+
if (!templateDir || !fs.existsSync(templateDir)) {
|
|
94
|
+
console.error(`❌ Preset template directory missing: ${templateDir}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const mdFiles = fs
|
|
98
|
+
.readdirSync(templateDir)
|
|
99
|
+
.filter((f) => f.endsWith('.md'));
|
|
100
|
+
if (mdFiles.length === 0) {
|
|
101
|
+
console.error(`❌ No .md files in preset: ${templateDir}`);
|
|
91
102
|
process.exit(1);
|
|
92
103
|
}
|
|
93
104
|
const root = options.global ? os.homedir() : process.cwd();
|
|
@@ -101,10 +112,13 @@ function registerCommand(program) {
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
-
|
|
115
|
+
for (const file of mdFiles) {
|
|
116
|
+
fs.copyFileSync(path.join(templateDir, file), path.join(dir, file));
|
|
117
|
+
}
|
|
105
118
|
|
|
106
|
-
console.log(`\n✅ Installed bundled skill "${presetKey}" →\n ${
|
|
107
|
-
console.log(
|
|
119
|
+
console.log(`\n✅ Installed bundled skill "${presetKey}" →\n ${dir}/\n`);
|
|
120
|
+
console.log(` Files: ${mdFiles.join(', ')}\n`);
|
|
121
|
+
console.log(' Edit if needed, then restart Cursor or reload the window.\n');
|
|
108
122
|
return;
|
|
109
123
|
}
|
|
110
124
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade Command — bump the webspresso dependency in the current project
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const PKG_NAME = 'webspresso';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} cwd
|
|
13
|
+
* @returns {'npm'|'pnpm'|'yarn'}
|
|
14
|
+
*/
|
|
15
|
+
function detectPackageManager(cwd) {
|
|
16
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
17
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
|
18
|
+
return 'npm';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} pkg
|
|
23
|
+
* @returns {{ specifier: string, saveDev: boolean } | null}
|
|
24
|
+
*/
|
|
25
|
+
function findWebspressoDep(pkg) {
|
|
26
|
+
if (!pkg || typeof pkg !== 'object') return null;
|
|
27
|
+
const prod = pkg.dependencies && pkg.dependencies[PKG_NAME];
|
|
28
|
+
const dev = pkg.devDependencies && pkg.devDependencies[PKG_NAME];
|
|
29
|
+
if (prod) return { specifier: String(prod), saveDev: false };
|
|
30
|
+
if (dev) return { specifier: String(dev), saveDev: true };
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isNonRegistrySpecifier(spec) {
|
|
35
|
+
return /^(file:|link:|workspace:|git\+|github:|http:|https:)/i.test(spec.trim());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {'npm'|'pnpm'|'yarn'} pm
|
|
40
|
+
* @param {string} tag
|
|
41
|
+
* @param {boolean} saveDev
|
|
42
|
+
* @returns {string[]}
|
|
43
|
+
*/
|
|
44
|
+
function buildInstallArgs(pm, tag, saveDev) {
|
|
45
|
+
const spec = `${PKG_NAME}@${tag}`;
|
|
46
|
+
if (pm === 'npm') {
|
|
47
|
+
return saveDev
|
|
48
|
+
? ['install', spec, '--save-dev']
|
|
49
|
+
: ['install', spec, '--save'];
|
|
50
|
+
}
|
|
51
|
+
if (pm === 'pnpm') {
|
|
52
|
+
return saveDev ? ['add', '-D', spec] : ['add', spec];
|
|
53
|
+
}
|
|
54
|
+
/* yarn */
|
|
55
|
+
return saveDev ? ['add', '-D', spec] : ['add', spec];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function registerCommand(program) {
|
|
59
|
+
program
|
|
60
|
+
.command('upgrade')
|
|
61
|
+
.description(
|
|
62
|
+
'Upgrade the webspresso package in the current project (npm/pnpm/yarn)'
|
|
63
|
+
)
|
|
64
|
+
.option(
|
|
65
|
+
'-t, --tag <dist-tag>',
|
|
66
|
+
'npm dist-tag (e.g. latest, next)',
|
|
67
|
+
'latest'
|
|
68
|
+
)
|
|
69
|
+
.option('--pm <manager>', 'Force package manager: npm, pnpm, or yarn')
|
|
70
|
+
.option('--dry-run', 'Print the install command without running it')
|
|
71
|
+
.action((options) => {
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(pkgPath)) {
|
|
76
|
+
console.error('❌ package.json not found. Run this from your project root.');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let pkg;
|
|
81
|
+
try {
|
|
82
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error(`❌ Could not read package.json: ${e.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const found = findWebspressoDep(pkg);
|
|
89
|
+
if (!found) {
|
|
90
|
+
console.error(
|
|
91
|
+
`❌ No "${PKG_NAME}" entry in dependencies or devDependencies.`
|
|
92
|
+
);
|
|
93
|
+
console.error(' Add it first, e.g. npm install webspresso');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isNonRegistrySpecifier(found.specifier)) {
|
|
98
|
+
console.error(
|
|
99
|
+
`❌ "${PKG_NAME}" is not a registry semver range (current: ${found.specifier}).`
|
|
100
|
+
);
|
|
101
|
+
console.error(
|
|
102
|
+
' For file:/link:/workspace: installs, change package.json manually or point to a published version.'
|
|
103
|
+
);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let pm = options.pm;
|
|
108
|
+
if (pm) {
|
|
109
|
+
pm = String(pm).toLowerCase();
|
|
110
|
+
if (!['npm', 'pnpm', 'yarn'].includes(pm)) {
|
|
111
|
+
console.error('❌ --pm must be npm, pnpm, or yarn');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
pm = detectPackageManager(cwd);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const args = buildInstallArgs(pm, options.tag, found.saveDev);
|
|
119
|
+
const cmd = `${pm} ${args.join(' ')}`;
|
|
120
|
+
|
|
121
|
+
console.log('\nWebspresso upgrade');
|
|
122
|
+
console.log('==================\n');
|
|
123
|
+
console.log(` Package manager: ${pm}`);
|
|
124
|
+
console.log(` Target: ${PKG_NAME}@${options.tag}`);
|
|
125
|
+
console.log(` Save as: ${found.saveDev ? 'devDependency' : 'dependency'}`);
|
|
126
|
+
console.log(` Current range: ${found.specifier}\n`);
|
|
127
|
+
|
|
128
|
+
if (options.dryRun) {
|
|
129
|
+
console.log(`Dry run — would run:\n ${cmd}\n`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
execSync(cmd, { stdio: 'inherit', cwd, shell: true });
|
|
135
|
+
console.log(
|
|
136
|
+
'\n✅ Upgrade finished. If you use native addons (better-sqlite3, bcrypt, sharp), run:\n' +
|
|
137
|
+
' npm run rebuild:native\n' +
|
|
138
|
+
' (or: npm rebuild better-sqlite3 bcrypt sharp)\n'
|
|
139
|
+
);
|
|
140
|
+
} catch {
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { registerCommand, detectPackageManager, findWebspressoDep };
|