@jay-framework/jay-stack-cli 0.17.4 → 0.18.0

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.
@@ -17,6 +17,43 @@ return phaseOutput(
17
17
 
18
18
  CarryForward is available in the next phase via `props.carryForward` but is not part of the ViewState.
19
19
 
20
+ ### Response Headers (fast phase only)
21
+
22
+ The third parameter accepts `responseHeaders` to set HTTP headers on the page response:
23
+
24
+ ```typescript
25
+ return phaseOutput(
26
+ { memberName: member.name },
27
+ {},
28
+ { responseHeaders: { 'Cache-Control': 'no-store' } },
29
+ );
30
+ ```
31
+
32
+ Use this when the page renders per-user data that must not be cached by CDN or browser. Can be combined with `headTags` in the same options object.
33
+
34
+ ### Cookies (fast phase only)
35
+
36
+ The fast phase receives `props.cookies` — a `Record<string, string>` parsed from the HTTP `Cookie` header:
37
+
38
+ ```typescript
39
+ .withFastRender(async (props, memberService) => {
40
+ const token = props.cookies['session-token'];
41
+ if (!token) return redirect3xx(302, '/login');
42
+
43
+ const member = await memberService.validate(token);
44
+ if (!member) return redirect3xx(302, '/login');
45
+
46
+ return phaseOutput(
47
+ { memberName: member.name },
48
+ {},
49
+ { responseHeaders: { 'Cache-Control': 'no-store' } },
50
+ );
51
+ })
52
+ ```
53
+
54
+ - `props.cookies` is `Record<string, string>` — empty `{}` when no cookies
55
+ - Not available in the slow phase (compile error) — same as `props.query`
56
+
20
57
  ## Error Results
21
58
 
22
59
  Return errors to stop rendering and show an error page:
@@ -160,6 +160,31 @@ URL query parameters (`?page=2&sort=price`) are available in the **fast render p
160
160
  - Not available in the slow phase (compile error) — slow results are cached by path params only
161
161
  - In the interactive phase, use `new URLSearchParams(window.location.search)` directly
162
162
 
163
+ ## Cookies
164
+
165
+ HTTP cookies are available in the **fast render phase only** via `props.cookies`:
166
+
167
+ ```typescript
168
+ .withFastRender(async (props, carryForward, memberService) => {
169
+ const token = props.cookies['session-token'];
170
+ if (!token) return redirect3xx(302, '/login');
171
+
172
+ const member = await memberService.validate(token);
173
+ if (!member) return redirect3xx(302, '/login');
174
+
175
+ return phaseOutput(
176
+ { memberName: member.name },
177
+ {},
178
+ { responseHeaders: { 'Cache-Control': 'no-store' } },
179
+ );
180
+ })
181
+ ```
182
+
183
+ - `props.cookies` is `Record<string, string>` — empty `{}` when no cookies
184
+ - Not available in the slow phase (compile error) — same as query params
185
+ - In the interactive phase, use `document.cookie` directly
186
+ - To set response headers (e.g. `Cache-Control: no-store`), use `responseHeaders` in `phaseOutput()` options
187
+
163
188
  ## Plugin Routes
164
189
 
165
190
  Plugins can provide their own pages via `routes` in `plugin.yaml`. These are backoffice tools, admin dashboards, or editors with boxed designs that don't need per-site customization.
@@ -9,15 +9,16 @@ The devops role handles the production lifecycle: building artifacts, configurin
9
9
  ## Workflow
10
10
 
11
11
  1. **Build** — `jay-stack build` to compile all pages into production artifacts
12
- 2. **Deploy** — upload `frontend/` to CDN, deploy `backend/` to server container
12
+ 2. **Deploy** — upload `frontend/` to CDN, deploy `backend/` to server container. Plugins can provide deploy commands via `jay-stack run <plugin>/deploy`
13
13
  3. **Serve** — start the production server with environment-appropriate flags
14
14
  4. **Invalidate** — rebuild specific pages when data changes
15
+ 5. **Admin** — run plugin CLI commands via `jay-stack run <plugin>/<command>` (media upload, data sync, cache purge)
15
16
 
16
17
  ## Guides
17
18
 
18
- | File | Topic |
19
- | ------------------------------------------ | -------------------------------------------------------- |
20
- | [production-build.md](production-build.md) | Build pipeline, output structure, frontend/backend split |
21
- | [serving-modes.md](serving-modes.md) | Self-hosted, CDN, BaaS (fetch handler), CLI flags |
22
- | [fetch-handler.md](fetch-handler.md) | @jay-framework/jay-fetch-handler for BaaS integration |
23
- | [invalidation.md](invalidation.md) | Rebuild, renderer server, cleanup |
19
+ | File | Topic |
20
+ | ------------------------------------------ | ---------------------------------------------------------- |
21
+ | [production-build.md](production-build.md) | Build pipeline, output structure, frontend/backend split |
22
+ | [serving-modes.md](serving-modes.md) | Self-hosted, CDN, BaaS (fetch handler), CLI flags |
23
+ | [fetch-handler.md](fetch-handler.md) | Fetch handler, ArtifactStore interface, BaaS custom stores |
24
+ | [invalidation.md](invalidation.md) | Rebuild, renderer server, cleanup |
@@ -23,23 +23,45 @@ const handler = createJayFetchHandler(options);
23
23
 
24
24
  ```typescript
25
25
  interface JayFetchHandlerOptions {
26
- backendDir: string; // Path to build/v{n}/backend/
26
+ // Artifact source (one required)
27
+ backendDir?: string; // Path to build/v{n}/backend/ (creates FilesystemArtifactStore)
28
+ artifactStore?: ArtifactStore; // Custom store for non-filesystem backends (DL#143)
29
+
27
30
  staticBaseUrl?: string; // Base URL for browser assets (default: '/')
28
31
  frontendDir?: string; // When set, serves static files from this directory
32
+
33
+ // Pre-imported modules — for bundled entry.mjs (DL#143)
34
+ plugins?: PreImportedPlugin[];
35
+ actionModules?: Array<{ module: Record<string, unknown>; name: string }>;
29
36
  }
30
37
  ```
31
38
 
32
39
  | Option | Required | Description |
33
40
  | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
34
- | `backendDir` | Yes | Path to the backend build directory containing manifest, server modules, and pre-rendered files |
41
+ | `backendDir` | \* | Path to the backend build directory. Creates a `FilesystemArtifactStore` internally |
42
+ | `artifactStore` | \* | Custom `ArtifactStore` implementation (e.g., cloud storage). Use instead of `backendDir` |
35
43
  | `staticBaseUrl` | No | URL prefix for import maps, CSS links, and client bundles. Set to your CDN URL for external hosting. Default: `/` |
36
44
  | `frontendDir` | No | When provided, the handler serves static files from this directory. Omit for CDN deployments where static files are hosted elsewhere |
45
+ | `plugins` | No | Pre-imported plugin init modules. Bypasses filesystem discovery — use for bundled deployments |
46
+ | `actionModules` | No | Pre-imported action modules. Bypasses filesystem discovery — use for bundled deployments |
37
47
 
38
- ## Usage Wix BaaS
48
+ \* One of `backendDir` or `artifactStore` is required.
49
+
50
+ ## Usage — Self-Hosted
39
51
 
40
52
  ```typescript
41
53
  import { createJayFetchHandler } from '@jay-framework/jay-fetch-handler';
42
54
 
55
+ const handler = createJayFetchHandler({
56
+ backendDir: './build/v1/backend',
57
+ staticBaseUrl: '/',
58
+ frontendDir: './build/v1/frontend',
59
+ });
60
+ ```
61
+
62
+ ## Usage — CDN Mode
63
+
64
+ ```typescript
43
65
  const handler = createJayFetchHandler({
44
66
  backendDir: './build/v1/backend',
45
67
  staticBaseUrl: 'https://static.parastorage.com/services/my-app/1.0.0/',
@@ -50,6 +72,47 @@ export default { fetch: handler };
50
72
 
51
73
  The BaaS runtime calls `handler(request)` for each incoming HTTP request.
52
74
 
75
+ ## Usage — BaaS with Custom Artifact Store
76
+
77
+ For deployments where backend files are not on the local filesystem (e.g., stored in a cloud database), provide a custom `ArtifactStore` and pre-imported modules:
78
+
79
+ ```typescript
80
+ import { createJayFetchHandler } from '@jay-framework/jay-fetch-handler';
81
+ import { WixDataArtifactStore } from '@jay-framework/wix-baas-adapter';
82
+ import { init as wixStoresInit } from '@jay-framework/wix-stores';
83
+ import * as wixStoresModule from '@jay-framework/wix-stores';
84
+
85
+ const handler = createJayFetchHandler({
86
+ artifactStore: new WixDataArtifactStore({
87
+ collectionId: 'jay-backend-files',
88
+ cacheDir: '/tmp/jay-backend',
89
+ }),
90
+ staticBaseUrl: 'https://static.parastorage.com/services/my-app/1.0.0/',
91
+ plugins: [{ name: 'wix-stores', init: wixStoresInit }],
92
+ actionModules: [{ module: wixStoresModule, name: 'wix-stores' }],
93
+ });
94
+
95
+ export default { fetch: handler };
96
+ ```
97
+
98
+ The `ArtifactStore` interface:
99
+
100
+ ```typescript
101
+ interface ArtifactStore {
102
+ readManifest(): Promise<RouteManifest>;
103
+ readCacheData(relativePath: string): Promise<CacheEntry>;
104
+ readPagePartsConfig(relativePath: string): Promise<any>;
105
+ loadServerElement(relativePath: string): Promise<ServerElementModule>;
106
+ loadModule(modulePath: string, local?: boolean): Promise<any>;
107
+ getAssetPath(relativePath: string): string;
108
+ getBuildDir(): string;
109
+ }
110
+ ```
111
+
112
+ `loadModule` handles all module loading — server elements, page components, headless components. The `local` flag indicates whether the path is relative to the build directory (`true`) or an npm package (`false`). For filesystem deployments, local modules resolve from `basePath` and npm modules use bare `import()`. BaaS implementations resolve all modules from their pre-bundled registry, ignoring the `local` flag.
113
+
114
+ For serve-only imports (no build-time dependencies), use `@jay-framework/production-server/serve`.
115
+
53
116
  ## Usage — Cloudflare Workers
54
117
 
55
118
  ```typescript
@@ -43,6 +43,33 @@ jay-stack serve --port 4000 \
43
43
 
44
44
  The server generates import maps, CSS links, and client bundle URLs prefixed with `--static-base-url`. It does not serve static files itself.
45
45
 
46
+ ## BaaS Mode (Custom Artifact Store)
47
+
48
+ For platforms where backend files are not on the local filesystem (e.g., stored in a cloud database), use `createJayFetchHandler` with a custom `ArtifactStore` and pre-imported modules:
49
+
50
+ ```typescript
51
+ import { createJayFetchHandler } from '@jay-framework/jay-fetch-handler';
52
+
53
+ const handler = createJayFetchHandler({
54
+ artifactStore: customStore, // Custom ArtifactStore implementation
55
+ staticBaseUrl: 'https://cdn.example.com/app/1.0.0/',
56
+ plugins: [
57
+ // Pre-imported plugin init modules
58
+ { name: 'my-plugin', init: myPluginInit },
59
+ ],
60
+ actionModules: [
61
+ // Pre-imported action modules
62
+ { module: myPluginModule, name: 'my-plugin' },
63
+ ],
64
+ });
65
+
66
+ export default { fetch: handler };
67
+ ```
68
+
69
+ Pre-imported modules bypass filesystem discovery — the entry file bundles everything with esbuild. See [fetch-handler.md](fetch-handler.md) for the `ArtifactStore` interface and full BaaS example.
70
+
71
+ For serve-only imports without build-time dependencies, use `@jay-framework/production-server/serve`.
72
+
46
73
  ## CLI Flags
47
74
 
48
75
  ### jay-stack serve
@@ -33,6 +33,7 @@ A plugin provides headless components (data + interactions, no UI) that project
33
33
  | [services-guide.md](services-guide.md) | createJayService, makeJayInit |
34
34
  | [plugin-routes.md](plugin-routes.md) | Plugin-provided pages: routes, jay-html templates, page components |
35
35
  | [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
36
+ | [commands-guide.md](commands-guide.md) | makeCliCommand, .jay-command files, CONSOLE_CONTEXT, jay-stack run |
36
37
  | [validation.md](validation.md) | jay-stack validate-plugin usage |
37
38
  | [dev-server-service.md](dev-server-service.md) | Dev server service API: routes, params, freeze management |
38
39
  | `../references/<plugin>/` | Plugin reference data |
@@ -38,7 +38,7 @@ makeJayAction('name')
38
38
  .withServices(SERVICE1, SERVICE2) // Inject services
39
39
  .withMethod('PUT') // Override HTTP method (default: POST for actions)
40
40
  .withCaching({ maxAge: 60 }) // Enable caching (queries only)
41
- .withFiles({ maxFileSize: 5_000_000 }) // Accept file uploads (multipart/form-data)
41
+ .withFiles() // Accept file uploads (multipart/form-data)
42
42
  .withHandler(async (input, svc1, svc2) => {
43
43
  // Define handler
44
44
  return result;
@@ -55,7 +55,7 @@ import { makeJayAction, type JayFile } from '@jay-framework/fullstack-component'
55
55
  import fs from 'fs';
56
56
 
57
57
  export const uploadPhoto = makeJayAction('photos.upload')
58
- .withFiles({ maxFileSize: 5 * 1024 * 1024 }) // 5MB limit, 10 files max (default)
58
+ .withFiles()
59
59
  .withHandler(async (input: { caption: string; photo: JayFile }) => {
60
60
  // JayFile provides: name, type, size, path (temp file on disk)
61
61
  const data = fs.readFileSync(input.photo.path);
@@ -123,14 +123,6 @@ refs.uploadBtn.onClick(async () => {
123
123
  });
124
124
  ```
125
125
 
126
- ### FileUploadOptions
127
-
128
- ```typescript
129
- .withFiles() // Defaults: 10MB per file, 10 files max
130
- .withFiles({ maxFileSize: 2 * 1024 * 1024 }) // 2MB limit
131
- .withFiles({ maxFileSize: 20_000_000, maxFiles: 5 }) // 20MB, 5 files
132
- ```
133
-
134
126
  ## ActionError
135
127
 
136
128
  Throw typed errors from action handlers:
@@ -0,0 +1,121 @@
1
+ # CLI Commands
2
+
3
+ Plugins can expose CLI commands for admin and batch operations. Run via `jay-stack run <plugin>/<command>`.
4
+
5
+ ## When to Use
6
+
7
+ - Media upload (public folder → cloud storage)
8
+ - Data sync (external CMS → local references)
9
+ - Deployment (build artifacts → cloud provider)
10
+ - Cache management (CDN purge, rebuild triggers)
11
+
12
+ For request-response operations, use [actions](actions-guide.md) instead.
13
+
14
+ ## Creating a Command
15
+
16
+ ### 1. Build the handler
17
+
18
+ ```typescript
19
+ import { makeCliCommand, CONSOLE_CONTEXT } from '@jay-framework/fullstack-component';
20
+ import { MEDIA_SERVICE } from './services';
21
+
22
+ export const uploadPublic = makeCliCommand('upload-public')
23
+ .withServices(MEDIA_SERVICE, CONSOLE_CONTEXT)
24
+ .withHandler(async (input: { folder?: string; dryRun?: boolean }, mediaService, console) => {
25
+ const path = await import('node:path');
26
+ const fs = await import('node:fs/promises');
27
+
28
+ const dir = path.resolve(console.publicFolder, input.folder || '');
29
+ const files = await fs.readdir(dir, { recursive: true });
30
+
31
+ for (const file of files) {
32
+ const filePath = path.join(dir, String(file));
33
+ const stat = await fs.stat(filePath);
34
+ if (!stat.isFile()) continue;
35
+
36
+ if (input.dryRun) {
37
+ console.log(`[dry-run] Would upload ${file}`);
38
+ continue;
39
+ }
40
+
41
+ const url = await mediaService.upload(filePath);
42
+ console.log(`Uploaded ${file} → ${url}`);
43
+ }
44
+
45
+ return { success: true };
46
+ });
47
+ ```
48
+
49
+ ### 2. Create the metadata file
50
+
51
+ Place `.jay-command` files in a `commands/` folder (alongside `contracts/` and `actions/`):
52
+
53
+ ```yaml
54
+ # commands/upload-public.jay-command
55
+ name: upload-public
56
+ description: Upload public folder files to cloud storage
57
+
58
+ inputSchema:
59
+ folder?: string
60
+ dryRun?: boolean
61
+ ```
62
+
63
+ The `inputSchema` auto-generates CLI flags:
64
+
65
+ - `folder?: string` → `--folder <value>` (optional)
66
+ - `dryRun?: boolean` → `--dry-run` (optional flag)
67
+
68
+ Required fields (no `?`) are validated before the handler runs.
69
+
70
+ ### 3. Declare in plugin.yaml
71
+
72
+ ```yaml
73
+ commands:
74
+ - name: upload-public
75
+ command: commands/upload-public.jay-command
76
+ ```
77
+
78
+ ## `CONSOLE_CONTEXT` Service
79
+
80
+ A framework-provided service with project info and a logger:
81
+
82
+ ```typescript
83
+ interface ConsoleContext {
84
+ projectRoot: string;
85
+ publicFolder: string;
86
+ build: {
87
+ frontend: string; // Build output: JS, CSS, public assets
88
+ backend: string; // Build output: server modules, pre-rendered HTML
89
+ };
90
+ verbose: boolean;
91
+ log: (message: string) => void;
92
+ warn: (message: string) => void;
93
+ error: (message: string) => void;
94
+ }
95
+ ```
96
+
97
+ Request it via `.withServices(CONSOLE_CONTEXT)`. Commands that don't need it simply don't request it.
98
+
99
+ ## Running Commands
100
+
101
+ ```bash
102
+ # Run a command
103
+ jay-stack run media/upload-public --folder images --dry-run
104
+
105
+ # List all available commands
106
+ jay-stack run --list
107
+
108
+ # Verbose output
109
+ jay-stack run media/upload-public -v
110
+ ```
111
+
112
+ ## Input Type Mapping
113
+
114
+ | Schema type | CLI flag | Example |
115
+ | ----------------- | ----------------------------------- | ------------------ |
116
+ | `field: string` | `--field <value>` (required) | `--env production` |
117
+ | `field?: string` | `--field <value>` (optional) | `--folder images` |
118
+ | `field?: boolean` | `--field` (optional flag) | `--dry-run` |
119
+ | `field: number` | `--field <value>` (required number) | `--concurrency 4` |
120
+
121
+ camelCase names become kebab-case flags: `dryRun` → `--dry-run`.
@@ -87,7 +87,7 @@ CarryForward data is passed to the fast phase but not included in the ViewState.
87
87
 
88
88
  ### `.withFastRender(fn)` — Request-time rendering
89
89
 
90
- Runs on each request. Receives props (including `query` for query parameters) and carry-forward from slow phase.
90
+ Runs on each request. Receives props (including `query` for query parameters and `cookies` for HTTP cookies) and carry-forward from slow phase. Can set HTTP response headers via `phaseOutput()` options.
91
91
 
92
92
  ```typescript
93
93
  .withFastRender(async (props, db) => {
@@ -96,6 +96,30 @@ Runs on each request. Receives props (including `query` for query parameters) an
96
96
  })
97
97
  ```
98
98
 
99
+ #### Cookies
100
+
101
+ `props.cookies` is a `Record<string, string>` parsed from the HTTP `Cookie` header. Use for auth checks:
102
+
103
+ ```typescript
104
+ .withFastRender(async (props, memberService) => {
105
+ const token = props.cookies['session-token'];
106
+ if (!token) return redirect3xx(302, '/login');
107
+
108
+ const member = await memberService.validate(token);
109
+ if (!member) return redirect3xx(302, '/login');
110
+
111
+ return phaseOutput(
112
+ { isLoggedIn: true, memberName: member.name },
113
+ {},
114
+ { responseHeaders: { 'Cache-Control': 'no-store' } },
115
+ );
116
+ })
117
+ ```
118
+
119
+ - Empty `{}` when no cookies are present
120
+ - Not available in the slow phase (compile error)
121
+ - `responseHeaders` in `phaseOutput()` options sets HTTP headers on the response (e.g. `Cache-Control: no-store` for per-user pages)
122
+
99
123
  ### `.withClientDefaults(fn)` — Defaults for dynamically created forEach items
100
124
 
101
125
  Required only when the component is used inside a `forEach` where new items can be added on the client. When a user adds a new item to a forEach array, the new instance has no server data — `withClientDefaults` provides the initial ViewState.
@@ -134,7 +158,7 @@ The interactive phase runs in the browser. Use hooks here (see component-state.m
134
158
 
135
159
  Each phase can return:
136
160
 
137
- - `phaseOutput(viewState, carryForward)` — success
161
+ - `phaseOutput(viewState, carryForward, options?)` — success (options: `{ headTags?, responseHeaders? }`)
138
162
  - `notFound()`, `badRequest()`, `unauthorized()`, `forbidden()` — client errors
139
163
  - `serverError5xx(status, message)` — server errors
140
164
  - `redirect3xx(status, location)` — redirects
@@ -55,6 +55,12 @@ routes:
55
55
  component: ./pages/admin/page.ts
56
56
  description: Admin dashboard with product stats
57
57
 
58
+ commands:
59
+ - name: upload-public
60
+ command: commands/upload-public.jay-command
61
+ - name: sync-catalog
62
+ command: commands/sync-catalog.jay-command
63
+
58
64
  setup:
59
65
  handler: setup-handler
60
66
  references: references-handler
@@ -172,6 +178,13 @@ services:
172
178
 
173
179
  Plugin routes are served by the dev server alongside project routes. If a project defines the same route path, the project's page takes precedence.
174
180
 
181
+ ### Command Entry Fields
182
+
183
+ - `name` — Command name (used with `jay-stack run <plugin>/<command>`)
184
+ - `command` — (optional) Path to `.jay-command` metadata file (declares description and input schema)
185
+
186
+ Commands are CLI operations run via `jay-stack run`. Use `makeCliCommand()` to create handlers with service injection. See [commands-guide.md](commands-guide.md).
187
+
175
188
  ### Setup Fields
176
189
 
177
190
  - `handler` — Setup handler for `jay-stack setup` (handles config, credentials)
@@ -193,6 +206,8 @@ my-plugin/
193
206
  │ ├── actions/
194
207
  │ │ ├── search-products.jay-action
195
208
  │ │ └── add-to-cart.jay-action
209
+ │ ├── commands/
210
+ │ │ └── upload-public.jay-command
196
211
  │ ├── webhooks/
197
212
  │ │ └── on-product-change.ts
198
213
  │ ├── components/
@@ -17,6 +17,43 @@ return phaseOutput(
17
17
 
18
18
  CarryForward is available in the next phase via `props.carryForward` but is not part of the ViewState.
19
19
 
20
+ ### Response Headers (fast phase only)
21
+
22
+ The third parameter accepts `responseHeaders` to set HTTP headers on the page response:
23
+
24
+ ```typescript
25
+ return phaseOutput(
26
+ { memberName: member.name },
27
+ {},
28
+ { responseHeaders: { 'Cache-Control': 'no-store' } },
29
+ );
30
+ ```
31
+
32
+ Use this when the component renders per-user data that must not be cached by CDN or browser. Can be combined with `headTags` in the same options object.
33
+
34
+ ### Cookies (fast phase only)
35
+
36
+ The fast phase receives `props.cookies` — a `Record<string, string>` parsed from the HTTP `Cookie` header:
37
+
38
+ ```typescript
39
+ .withFastRender(async (props, memberService) => {
40
+ const token = props.cookies['session-token'];
41
+ if (!token) return redirect3xx(302, '/login');
42
+
43
+ const member = await memberService.validate(token);
44
+ if (!member) return redirect3xx(302, '/login');
45
+
46
+ return phaseOutput(
47
+ { isLoggedIn: true, memberName: member.name },
48
+ {},
49
+ { responseHeaders: { 'Cache-Control': 'no-store' } },
50
+ );
51
+ })
52
+ ```
53
+
54
+ - `props.cookies` is `Record<string, string>` — empty `{}` when no cookies
55
+ - Not available in the slow phase (compile error) — same as `props.query`
56
+
20
57
  ## Error Results
21
58
 
22
59
  Return errors to stop rendering and show an error page:
package/dist/index.js CHANGED
@@ -2530,6 +2530,11 @@ async function startDevServer(options = {}) {
2530
2530
  routes.forEach((route) => {
2531
2531
  app.get(route.path, route.handler);
2532
2532
  });
2533
+ service.attachRouteRegistrar((added) => {
2534
+ added.forEach((route) => {
2535
+ app.get(route.path, route.handler);
2536
+ });
2537
+ });
2533
2538
  generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath, process.cwd());
2534
2539
  httpServer.listen(devServerPort, () => {
2535
2540
  log.important(`🚀 Jay Stack dev server started successfully!`);
@@ -2643,7 +2648,7 @@ async function resolveProductionContext(projectPath, versionOverride) {
2643
2648
  pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
2644
2649
  } catch {
2645
2650
  }
2646
- const version = versionOverride ? parseInt(versionOverride, 10) : await resolveVersionFromPackageJson(resolvedPath);
2651
+ const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
2647
2652
  return {
2648
2653
  resolvedPath,
2649
2654
  pagesRoot: path$1.resolve(resolvedPath, pagesBase),
@@ -2658,14 +2663,11 @@ async function resolveVersionFromPackageJson(projectRoot) {
2658
2663
  await fs$1.readFile(path$1.join(projectRoot, "package.json"), "utf-8")
2659
2664
  );
2660
2665
  if (pkgJson.version) {
2661
- const major = parseInt(pkgJson.version.split(".")[0], 10);
2662
- const minor = parseInt(pkgJson.version.split(".")[1] || "0", 10);
2663
- const patch = parseInt(pkgJson.version.split(".")[2] || "0", 10);
2664
- return major * 1e4 + minor * 100 + patch;
2666
+ return pkgJson.version;
2665
2667
  }
2666
2668
  } catch {
2667
2669
  }
2668
- return 1;
2670
+ return "1";
2669
2671
  }
2670
2672
  function initLogger(verbose) {
2671
2673
  const logLevel = verbose ? "verbose" : "info";
@@ -2750,6 +2752,7 @@ const runProduction = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defin
2750
2752
  __proto__: null,
2751
2753
  initLogger,
2752
2754
  resolveProductionContext,
2755
+ resolveVersionFromPackageJson,
2753
2756
  runBuild,
2754
2757
  runRebuild,
2755
2758
  runServe
@@ -3776,9 +3779,6 @@ const SKIP_ATTRS = /* @__PURE__ */ new Set([
3776
3779
  "if",
3777
3780
  "ref",
3778
3781
  "trackBy",
3779
- "slowForEach",
3780
- "jayIndex",
3781
- "jayTrackBy",
3782
3782
  "when-resolved",
3783
3783
  "when-loading",
3784
3784
  "when-rejected",
@@ -4502,7 +4502,7 @@ function printValidationResult(result, verbose) {
4502
4502
  logger.important(chalk.red(`${result.errors.length} errors found.`));
4503
4503
  }
4504
4504
  }
4505
- const ALL_ROLES = ["designer", "developer", "plugin"];
4505
+ const ALL_ROLES = ["designer", "developer", "plugin", "devops"];
4506
4506
  async function runAgentKit(options) {
4507
4507
  const projectRoot = process.cwd();
4508
4508
  const { initErrors, viteServer } = await runMaterialize(
@@ -5029,6 +5029,161 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
5029
5029
  }
5030
5030
  }
5031
5031
  }
5032
+ async function runCommand(commandRef, rawArgs, options, projectRoot, initializeServices) {
5033
+ let viteServer;
5034
+ try {
5035
+ const {
5036
+ discoverPluginCommands,
5037
+ commandSchemaToFlags,
5038
+ parseInputFromFlags,
5039
+ executePluginCommand
5040
+ } = await import("@jay-framework/stack-server-runtime");
5041
+ const commands = await discoverPluginCommands({
5042
+ projectRoot,
5043
+ verbose: options.verbose
5044
+ });
5045
+ if (options.list || !commandRef) {
5046
+ printCommandList(commands);
5047
+ return;
5048
+ }
5049
+ const slashIndex = commandRef.indexOf("/");
5050
+ if (slashIndex === -1) {
5051
+ getLogger().error(
5052
+ chalk.red("Invalid command reference. Use format: <plugin>/<command>")
5053
+ );
5054
+ getLogger().error(chalk.gray("Example: jay-stack run media/upload-public"));
5055
+ process.exit(1);
5056
+ }
5057
+ const pluginName = commandRef.substring(0, slashIndex);
5058
+ const commandName = commandRef.substring(slashIndex + 1);
5059
+ const command = commands.find(
5060
+ (c) => (c.pluginName === pluginName || c.packageName === pluginName) && c.commandName === commandName
5061
+ );
5062
+ if (!command) {
5063
+ getLogger().error(chalk.red(`Command "${commandRef}" not found.`));
5064
+ const pluginCommands = commands.filter(
5065
+ (c) => c.pluginName === pluginName || c.packageName === pluginName
5066
+ );
5067
+ if (pluginCommands.length > 0) {
5068
+ getLogger().error(`Available commands for ${pluginName}:`);
5069
+ for (const c of pluginCommands) {
5070
+ getLogger().error(
5071
+ ` ${c.commandName}${c.metadata?.description ? " " + c.metadata.description : ""}`
5072
+ );
5073
+ }
5074
+ } else if (commands.length > 0) {
5075
+ getLogger().error("Available plugins with commands:");
5076
+ const plugins = [...new Set(commands.map((c) => c.pluginName))];
5077
+ for (const p of plugins) {
5078
+ getLogger().error(` ${p}`);
5079
+ }
5080
+ } else {
5081
+ getLogger().error("No plugins with CLI commands found.");
5082
+ }
5083
+ process.exit(1);
5084
+ }
5085
+ let input = {};
5086
+ if (command.metadata?.inputSchema) {
5087
+ const flagDefs = commandSchemaToFlags(command.metadata.inputSchema);
5088
+ const rawOptions = parseRawFlags(rawArgs, flagDefs);
5089
+ input = parseInputFromFlags(rawOptions, command.metadata.inputSchema);
5090
+ }
5091
+ if (options.verbose) {
5092
+ getLogger().info("Starting Vite for TypeScript support...");
5093
+ }
5094
+ viteServer = await createViteForCli({ projectRoot });
5095
+ await initializeServices(projectRoot, viteServer);
5096
+ const { registerService } = await import("@jay-framework/stack-server-runtime");
5097
+ const { CONSOLE_CONTEXT } = await import("@jay-framework/fullstack-component");
5098
+ const jayConfig = loadConfig();
5099
+ const publicFolder = path$1.resolve(
5100
+ projectRoot,
5101
+ jayConfig.devServer?.publicFolder || "public"
5102
+ );
5103
+ const version = await resolveVersionFromPackageJson(projectRoot);
5104
+ const buildRoot = path$1.resolve(projectRoot, `build/v${version}`);
5105
+ registerService(CONSOLE_CONTEXT, {
5106
+ projectRoot,
5107
+ publicFolder,
5108
+ build: {
5109
+ frontend: path$1.join(buildRoot, "frontend"),
5110
+ backend: path$1.join(buildRoot, "backend")
5111
+ },
5112
+ verbose: options.verbose ?? false,
5113
+ log: (msg) => getLogger().important(msg),
5114
+ warn: (msg) => getLogger().warn(chalk.yellow(msg)),
5115
+ error: (msg) => getLogger().error(chalk.red(msg))
5116
+ });
5117
+ if (options.verbose) {
5118
+ getLogger().info(`Executing ${command.pluginName}/${command.commandName}...`);
5119
+ }
5120
+ const result = await executePluginCommand(command, input, viteServer);
5121
+ if (!result.success) {
5122
+ process.exit(1);
5123
+ }
5124
+ } catch (error) {
5125
+ getLogger().error(chalk.red("Command failed:") + " " + error.message);
5126
+ if (options.verbose) {
5127
+ getLogger().error(error.stack);
5128
+ }
5129
+ process.exit(1);
5130
+ } finally {
5131
+ if (viteServer) {
5132
+ await viteServer.close();
5133
+ }
5134
+ }
5135
+ }
5136
+ function printCommandList(commands) {
5137
+ const logger = getLogger();
5138
+ if (commands.length === 0) {
5139
+ logger.important(chalk.gray("No plugins with CLI commands found."));
5140
+ return;
5141
+ }
5142
+ logger.important("\nAvailable plugin commands:\n");
5143
+ const byPlugin = /* @__PURE__ */ new Map();
5144
+ for (const cmd of commands) {
5145
+ if (!byPlugin.has(cmd.pluginName))
5146
+ byPlugin.set(cmd.pluginName, []);
5147
+ byPlugin.get(cmd.pluginName).push(cmd);
5148
+ }
5149
+ for (const [pluginName, cmds] of byPlugin) {
5150
+ logger.important(chalk.bold(` ${pluginName}`));
5151
+ for (const cmd of cmds) {
5152
+ const desc = cmd.metadata?.description ? ` ${cmd.metadata.description}` : "";
5153
+ logger.important(` ${cmd.commandName}${desc}`);
5154
+ }
5155
+ logger.important("");
5156
+ }
5157
+ }
5158
+ function parseRawFlags(args, flagDefs) {
5159
+ const result = {};
5160
+ const booleanFlags = new Set(
5161
+ flagDefs.filter((f) => f.type === "boolean").map((f) => {
5162
+ const match = f.flag.match(/^--(\S+)/);
5163
+ return match ? match[1] : "";
5164
+ })
5165
+ );
5166
+ let i = 0;
5167
+ while (i < args.length) {
5168
+ const arg = args[i];
5169
+ if (arg.startsWith("--")) {
5170
+ const name = arg.slice(2);
5171
+ if (booleanFlags.has(name)) {
5172
+ result[name] = true;
5173
+ i++;
5174
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
5175
+ result[name] = args[i + 1];
5176
+ i += 2;
5177
+ } else {
5178
+ result[name] = true;
5179
+ i++;
5180
+ }
5181
+ } else {
5182
+ i++;
5183
+ }
5184
+ }
5185
+ return result;
5186
+ }
5032
5187
  const program = new Command();
5033
5188
  program.name("jay-stack").description("Jay Stack CLI - Development server and plugin validation").version("0.9.0");
5034
5189
  program.command("dev").description("Start the Jay Stack development server").option("-p, --path <path>", "Project root (default: cwd)").option("-v, --verbose", "Enable verbose logging output").option("-q, --quiet", "Suppress all non-error output").option("--test-mode", "Enable test endpoints (/_jay/health, /_jay/shutdown)").option("--timeout <seconds>", "Auto-shutdown after N seconds (implies --test-mode)", parseInt).action(async (options) => {
@@ -5115,6 +5270,13 @@ program.command("agent-kit").description("Prepare agent kit: materialize contrac
5115
5270
  program.command("action <plugin/action>").description("Run a plugin action (e.g., jay-stack action wix-stores/searchProducts)").option("--input <json>", "JSON input for the action (default: {})").option("--yaml", "Output result as YAML instead of JSON").option("-v, --verbose", "Show detailed output").action(async (actionRef, options) => {
5116
5271
  await runAction(actionRef, options, process.cwd(), initializeServicesForCli);
5117
5272
  });
5273
+ program.command("run [plugin/command]").description("Run a plugin CLI command (e.g., jay-stack run media/upload-public)").option("--list", "List all available plugin commands").option("-v, --verbose", "Show detailed output").allowUnknownOption().allowExcessArguments().action(async (commandRef, options, command) => {
5274
+ const rawArgs = command.parent?.rawArgs.slice(3) ?? [];
5275
+ const extraArgs = rawArgs.filter(
5276
+ (a) => a !== commandRef && a !== "-v" && a !== "--verbose" && a !== "--list"
5277
+ );
5278
+ await runCommand(commandRef, extraArgs, options, process.cwd(), initializeServicesForCli);
5279
+ });
5118
5280
  program.command("params <plugin/contract>").description("Discover load param values for a contract (NDJSON or YAML)").option("--yaml", "Output as YAML instead of NDJSON").option("-v, --verbose", "Show detailed output").action(async (contractRef, options) => {
5119
5281
  await runParams(contractRef, options, process.cwd(), initializeServicesForCli);
5120
5282
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/jay-stack-cli",
3
- "version": "0.17.4",
3
+ "version": "0.18.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,15 +24,15 @@
24
24
  "test:watch": "vitest"
25
25
  },
26
26
  "dependencies": {
27
- "@jay-framework/compiler-jay-html": "^0.17.4",
28
- "@jay-framework/compiler-shared": "^0.17.4",
29
- "@jay-framework/dev-server": "^0.17.4",
30
- "@jay-framework/editor-server": "^0.17.4",
31
- "@jay-framework/fullstack-component": "^0.17.4",
32
- "@jay-framework/logger": "^0.17.4",
33
- "@jay-framework/plugin-validator": "^0.17.4",
34
- "@jay-framework/production-server": "^0.17.4",
35
- "@jay-framework/stack-server-runtime": "^0.17.4",
27
+ "@jay-framework/compiler-jay-html": "^0.18.0",
28
+ "@jay-framework/compiler-shared": "^0.18.0",
29
+ "@jay-framework/dev-server": "^0.18.0",
30
+ "@jay-framework/editor-server": "^0.18.0",
31
+ "@jay-framework/fullstack-component": "^0.18.0",
32
+ "@jay-framework/logger": "^0.18.0",
33
+ "@jay-framework/plugin-validator": "^0.18.0",
34
+ "@jay-framework/production-server": "^0.18.0",
35
+ "@jay-framework/stack-server-runtime": "^0.18.0",
36
36
  "chalk": "^4.1.2",
37
37
  "commander": "^14.0.0",
38
38
  "express": "^5.0.1",
@@ -43,7 +43,7 @@
43
43
  "yaml": "^2.3.4"
44
44
  },
45
45
  "devDependencies": {
46
- "@jay-framework/dev-environment": "^0.17.4",
46
+ "@jay-framework/dev-environment": "^0.18.0",
47
47
  "@types/express": "^5.0.2",
48
48
  "@types/node": "^22.15.21",
49
49
  "nodemon": "^3.0.3",