@pulse-editor/cli 0.1.1-beta.36 → 0.1.1-beta.38

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/dist/app.js CHANGED
@@ -13,6 +13,7 @@ import Preview from './components/commands/preview.js';
13
13
  import Start from './components/commands/start.js';
14
14
  import Clean from './components/commands/clean.js';
15
15
  import Upgrade from './components/commands/upgrade.js';
16
+ import Skill from './components/commands/skill.js';
16
17
  export default function App({ cli }) {
17
18
  const [command, setCommand] = useState(undefined);
18
19
  if (cli.flags.stage) {
@@ -25,5 +26,5 @@ export default function App({ cli }) {
25
26
  const cmd = cli.input[0] ?? 'help';
26
27
  setCommand(cmd);
27
28
  }, [cli.input]);
28
- return (_jsxs(_Fragment, { children: [cli.flags.stage && (_jsx(Text, { color: 'yellow', children: "\u26A0\uFE0F You are in development mode." })), command === 'help' ? (_jsx(Help, { cli: cli })) : command === 'chat' ? (_jsx(Chat, { cli: cli })) : command === 'login' ? (_jsx(Login, { cli: cli })) : command === 'logout' ? (_jsx(Logout, { cli: cli })) : command === 'publish' ? (_jsx(Publish, { cli: cli })) : command === 'create' ? (_jsx(Create, { cli: cli })) : command === 'dev' ? (_jsx(Dev, { cli: cli })) : command === 'build' ? (_jsx(Build, { cli: cli })) : command === 'preview' ? (_jsx(Preview, { cli: cli })) : command === 'start' ? (_jsx(Start, { cli: cli })) : command === 'clean' ? (_jsx(Clean, { cli: cli })) : command === 'upgrade' ? (_jsx(Upgrade, { cli: cli })) : (command !== undefined && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: 'redBright', children: ["Invalid command: ", command] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: 'blueBright', children: "pulse help" }), " to see the list of available commands."] })] })))] }));
29
+ return (_jsxs(_Fragment, { children: [cli.flags.stage && (_jsx(Text, { color: 'yellow', children: "\u26A0\uFE0F You are in development mode." })), command === 'help' ? (_jsx(Help, { cli: cli })) : command === 'chat' ? (_jsx(Chat, { cli: cli })) : command === 'login' ? (_jsx(Login, { cli: cli })) : command === 'logout' ? (_jsx(Logout, { cli: cli })) : command === 'publish' ? (_jsx(Publish, { cli: cli })) : command === 'create' ? (_jsx(Create, { cli: cli })) : command === 'dev' ? (_jsx(Dev, { cli: cli })) : command === 'build' ? (_jsx(Build, { cli: cli })) : command === 'preview' ? (_jsx(Preview, { cli: cli })) : command === 'start' ? (_jsx(Start, { cli: cli })) : command === 'clean' ? (_jsx(Clean, { cli: cli })) : command === 'upgrade' ? (_jsx(Upgrade, { cli: cli })) : command === 'skill' ? (_jsx(Skill, { cli: cli })) : (command !== undefined && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: 'redBright', children: ["Invalid command: ", command] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: 'blueBright', children: "pulse help" }), " to see the list of available commands."] })] })))] }));
29
30
  }
@@ -0,0 +1,5 @@
1
+ import { Result } from 'meow';
2
+ import { Flags } from '../../lib/cli-flags.js';
3
+ export default function Skill({ cli }: {
4
+ cli: Result<Flags>;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,230 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { checkToken, getToken } from '../../lib/token.js';
8
+ import { getBackendUrl } from '../../lib/backend-url.js';
9
+ // ---------------------------------------------------------------------------
10
+ // MultilineInput
11
+ // ---------------------------------------------------------------------------
12
+ function MultilineInput({ onSubmit, focus, }) {
13
+ const [lines, setLines] = useState(['']);
14
+ useInput((input, key) => {
15
+ // Regular Enter (\r) → submit
16
+ // Shift+Enter → terminals send \n (0x0A) rather than setting key.shift+key.return
17
+ if (input === '\n') {
18
+ setLines(prev => [...prev, '']);
19
+ return;
20
+ }
21
+ if (key.return) {
22
+ const value = lines.join('\n').trim();
23
+ if (value)
24
+ onSubmit(value);
25
+ return;
26
+ }
27
+ if (key.backspace || key.delete) {
28
+ setLines(prev => {
29
+ const next = [...prev];
30
+ const last = next[next.length - 1];
31
+ if (last.length > 0) {
32
+ next[next.length - 1] = last.slice(0, -1);
33
+ }
34
+ else if (next.length > 1) {
35
+ next.pop();
36
+ }
37
+ return next;
38
+ });
39
+ return;
40
+ }
41
+ if (input) {
42
+ setLines(prev => {
43
+ const next = [...prev];
44
+ next[next.length - 1] += input;
45
+ return next;
46
+ });
47
+ }
48
+ }, { isActive: focus });
49
+ return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: lines.map((line, i) => (_jsxs(Text, { children: [line, i === lines.length - 1 ? _jsx(Text, { backgroundColor: "white", children: " " }) : ''] }, i))) }));
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // SkillCreate
53
+ // ---------------------------------------------------------------------------
54
+ function SkillCreate({ cli }) {
55
+ const [skillName, setSkillName] = useState(cli.input[2]);
56
+ const [description, setDescription] = useState(cli.flags.description);
57
+ const [status, setStatus] = useState();
58
+ const [errorMessage, setErrorMessage] = useState('');
59
+ const [chunkCount, setChunkCount] = useState(0);
60
+ // Once both fields are collected, authenticate
61
+ useEffect(() => {
62
+ if (!skillName || !description)
63
+ return;
64
+ setStatus('authenticating');
65
+ }, [skillName, description]);
66
+ useEffect(() => {
67
+ if (status !== 'authenticating')
68
+ return;
69
+ async function authenticate() {
70
+ const token = getToken(cli.flags.stage);
71
+ if (token && (await checkToken(token, cli.flags.stage))) {
72
+ setStatus('generating');
73
+ }
74
+ else {
75
+ setErrorMessage('You are not authenticated. Please run pulse login first.');
76
+ setStatus('error');
77
+ setTimeout(() => process.exit(1), 0);
78
+ }
79
+ }
80
+ authenticate();
81
+ }, [status]);
82
+ useEffect(() => {
83
+ if (status !== 'generating' || !skillName || !description)
84
+ return;
85
+ async function generate() {
86
+ const token = getToken(cli.flags.stage);
87
+ const backendUrl = getBackendUrl(cli.flags.stage);
88
+ try {
89
+ const res = await fetch(`${backendUrl}/api/inference/cli/skill/create`, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
94
+ },
95
+ body: JSON.stringify({ description }),
96
+ });
97
+ if (!res.ok) {
98
+ setErrorMessage(`Server returned error code ${res.status}.`);
99
+ setStatus('error');
100
+ setTimeout(() => process.exit(), 0);
101
+ return;
102
+ }
103
+ const reader = res.body?.getReader();
104
+ const decoder = new TextDecoder();
105
+ let code = '';
106
+ if (reader) {
107
+ while (true) {
108
+ const { done, value } = await reader.read();
109
+ if (done)
110
+ break;
111
+ code += decoder.decode(value, { stream: true });
112
+ setChunkCount(n => n + 1);
113
+ }
114
+ }
115
+ const skillDir = path.join(process.cwd(), 'src', 'skill', skillName);
116
+ fs.mkdirSync(skillDir, { recursive: true });
117
+ fs.writeFileSync(path.join(skillDir, 'action.ts'), code, 'utf-8');
118
+ setStatus('done');
119
+ setTimeout(() => process.exit(), 0);
120
+ }
121
+ catch (err) {
122
+ setErrorMessage(err?.message ?? String(err));
123
+ setStatus('error');
124
+ setTimeout(() => process.exit(), 0);
125
+ }
126
+ }
127
+ generate();
128
+ }, [status]);
129
+ return (_jsxs(_Fragment, { children: [!skillName && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Skill name:" }), _jsx(MultilineInput, { onSubmit: value => setTimeout(() => setSkillName(value), 0), focus: !skillName })] })), skillName && !description && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["What should this skill do?", ' ', _jsx(Text, { color: "blueBright", children: "(Shift+Enter for newline, Enter to confirm)" })] }), _jsx(MultilineInput, { onSubmit: value => setTimeout(() => setDescription(value), 0), focus: !!skillName && !description })] })), status === 'authenticating' && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Checking authentication..." })] })), status === 'generating' && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsxs(Text, { children: [" Generating skill action for \"", skillName, "\"... "] }), _jsxs(Text, { color: "blueBright", children: ["[", chunkCount, " chunks received]"] })] })), status === 'done' && (_jsxs(Text, { color: "greenBright", children: ["\u2705 Skill action created at src/skill/", skillName, "/action.ts"] })), status === 'error' && (_jsxs(Text, { color: "redBright", children: ["\u274C ", errorMessage] }))] }));
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // SkillFix
133
+ // ---------------------------------------------------------------------------
134
+ function SkillFix({ cli }) {
135
+ const [skillName, setSkillName] = useState(cli.input[2]);
136
+ const [status, setStatus] = useState();
137
+ const [errorMessage, setErrorMessage] = useState('');
138
+ const [chunkCount, setChunkCount] = useState(0);
139
+ // Once skill name is collected, authenticate
140
+ useEffect(() => {
141
+ if (!skillName)
142
+ return;
143
+ setStatus('authenticating');
144
+ }, [skillName]);
145
+ useEffect(() => {
146
+ if (status !== 'authenticating')
147
+ return;
148
+ async function authenticate() {
149
+ const token = getToken(cli.flags.stage);
150
+ if (token && (await checkToken(token, cli.flags.stage))) {
151
+ // Validate file exists before fixing
152
+ const actionPath = path.join(process.cwd(), 'src', 'skill', skillName, 'action.ts');
153
+ if (!fs.existsSync(actionPath)) {
154
+ setErrorMessage(`Action file not found: src/skill/${skillName}/action.ts`);
155
+ setStatus('error');
156
+ setTimeout(() => process.exit(), 0);
157
+ return;
158
+ }
159
+ setStatus('fixing');
160
+ }
161
+ else {
162
+ setErrorMessage('You are not authenticated. Please run pulse login first.');
163
+ setStatus('error');
164
+ setTimeout(() => process.exit(1), 0);
165
+ }
166
+ }
167
+ authenticate();
168
+ }, [status]);
169
+ useEffect(() => {
170
+ if (status !== 'fixing' || !skillName)
171
+ return;
172
+ async function fix() {
173
+ const actionPath = path.join(process.cwd(), 'src', 'skill', skillName, 'action.ts');
174
+ const code = fs.readFileSync(actionPath, 'utf-8');
175
+ const token = getToken(cli.flags.stage);
176
+ const backendUrl = getBackendUrl(cli.flags.stage);
177
+ try {
178
+ const res = await fetch(`${backendUrl}/api/inference/cli/skill/fix`, {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
183
+ },
184
+ body: JSON.stringify({ code }),
185
+ });
186
+ if (!res.ok) {
187
+ setErrorMessage(`Server returned error code ${res.status}.`);
188
+ setStatus('error');
189
+ setTimeout(() => process.exit(), 0);
190
+ return;
191
+ }
192
+ const reader = res.body?.getReader();
193
+ const decoder = new TextDecoder();
194
+ let fixed = '';
195
+ if (reader) {
196
+ while (true) {
197
+ const { done, value } = await reader.read();
198
+ if (done)
199
+ break;
200
+ fixed += decoder.decode(value, { stream: true });
201
+ setChunkCount(n => n + 1);
202
+ }
203
+ }
204
+ fs.writeFileSync(actionPath, fixed, 'utf-8');
205
+ setStatus('done');
206
+ setTimeout(() => process.exit(), 0);
207
+ }
208
+ catch (err) {
209
+ setErrorMessage(err?.message ?? String(err));
210
+ setStatus('error');
211
+ setTimeout(() => process.exit(), 0);
212
+ }
213
+ }
214
+ fix();
215
+ }, [status]);
216
+ return (_jsxs(_Fragment, { children: [!skillName && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Action name:" }), _jsx(MultilineInput, { onSubmit: value => setTimeout(() => setSkillName(value), 0), focus: !skillName })] })), status === 'authenticating' && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Checking authentication..." })] })), status === 'fixing' && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsxs(Text, { children: [" Fixing JSDoc for skill \"", skillName, "\"... "] }), _jsxs(Text, { color: "blueBright", children: ["[", chunkCount, " chunks received]"] })] })), status === 'done' && (_jsxs(Text, { color: "greenBright", children: ["\u2705 JSDoc fixed and saved to src/skill/", skillName, "/action.ts"] })), status === 'error' && (_jsxs(Text, { color: "redBright", children: ["\u274C ", errorMessage] }))] }));
217
+ }
218
+ // ---------------------------------------------------------------------------
219
+ // Skill (top-level router)
220
+ // ---------------------------------------------------------------------------
221
+ export default function Skill({ cli }) {
222
+ const subCommand = cli.input[1];
223
+ if (subCommand === 'create') {
224
+ return _jsx(SkillCreate, { cli: cli });
225
+ }
226
+ if (subCommand === 'fix') {
227
+ return _jsx(SkillFix, { cli: cli });
228
+ }
229
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "redBright", children: ["Unknown subcommand: ", subCommand ?? '(none)'] }), _jsxs(Text, { children: ["Available subcommands:", '\n', ' ', "pulse skill create ", '<skill-name>', " --description \"", '<description>', "\"", '\n', ' ', "pulse skill fix ", '<action-name>'] })] }));
230
+ }
@@ -1,4 +1,5 @@
1
1
  import { getToken } from '../token.js';
2
+ import { getBackendUrl } from '../backend-url.js';
2
3
  import fs from 'fs';
3
4
  export async function publishApp(isStage) {
4
5
  // Upload the zip file to the server
@@ -14,9 +15,7 @@ export async function publishApp(isStage) {
14
15
  formData.append('file', blob, 'dist.zip');
15
16
  formData.append('visibility', visibility);
16
17
  // Send the file to the server
17
- const res = await fetch(isStage
18
- ? 'https://localhost:8080/api/app/publish'
19
- : 'https://pulse-editor.com/api/app/publish', {
18
+ const res = await fetch(`${getBackendUrl(isStage)}/api/app/publish`, {
20
19
  method: 'POST',
21
20
  headers: {
22
21
  Authorization: `Bearer ${getToken(isStage)}`,
@@ -0,0 +1 @@
1
+ export declare function getBackendUrl(stage: boolean): "https://localhost:8080" | "https://pulse-editor.com";
@@ -0,0 +1,3 @@
1
+ export function getBackendUrl(stage) {
2
+ return stage ? 'https://localhost:8080' : 'https://pulse-editor.com';
3
+ }
@@ -39,5 +39,9 @@ export declare const flags: {
39
39
  displayName: {
40
40
  type: "string";
41
41
  };
42
+ description: {
43
+ type: "string";
44
+ shortFlag: string;
45
+ };
42
46
  };
43
47
  export type Flags = typeof flags;
@@ -43,4 +43,8 @@ export const flags = defineFlags({
43
43
  displayName: {
44
44
  type: 'string',
45
45
  },
46
+ description: {
47
+ type: 'string',
48
+ shortFlag: 'd',
49
+ },
46
50
  });
@@ -84,6 +84,19 @@ const upgrade = `\
84
84
  --beta
85
85
  Upgrade to the latest beta version.
86
86
 
87
+ `;
88
+ const skill = `\
89
+ skill Manage skill actions for the current Pulse App.
90
+
91
+ Subcommands:
92
+ create <skill-name> --description "<description>"
93
+ Generate a new skill action using AI and write it to
94
+ src/skill/<skill-name>/action.ts.
95
+
96
+ fix <action-name>
97
+ Fix and apply valid JSDoc comments to an existing skill
98
+ action at src/skill/<action-name>/action.ts using AI.
99
+
87
100
  `;
88
101
  export const commandsManual = {
89
102
  help,
@@ -98,4 +111,5 @@ export const commandsManual = {
98
111
  start,
99
112
  clean,
100
113
  upgrade,
114
+ skill,
101
115
  };
@@ -98,6 +98,31 @@ app.all(/^\/server-function\/(.*)/, async (req, res) => {
98
98
  if (isPreview) {
99
99
  /* Preview mode */
100
100
  app.use(express.static("dist/client"));
101
+ // Expose skill actions as REST API endpoints in dev and preview modes
102
+ const skillActions = pulseConfig?.actions || [];
103
+ const skillActionNames = skillActions.map((a) => a.name);
104
+ app.post("/skill/:actionName", async (req, res) => {
105
+ const { actionName } = req.params;
106
+ if (skillActionNames.length > 0 && !skillActionNames.includes(actionName)) {
107
+ res
108
+ .status(404)
109
+ .json({ error: `Skill action "${actionName}" not found.` });
110
+ return;
111
+ }
112
+ const dir = path.resolve("node_modules/@pulse-editor/cli/dist/lib/server/preview/backend/load-remote.cjs");
113
+ const fileUrl = pathToFileURL(dir).href;
114
+ const { loadFunc } = await import(fileUrl);
115
+ try {
116
+ const action = await loadFunc(`skill/${actionName}`, pulseConfig.id, "http://localhost:3030", pulseConfig.version);
117
+ const result = await action(req.body);
118
+ res.json(result);
119
+ }
120
+ catch (err) {
121
+ const message = err instanceof Error ? err.message : String(err);
122
+ console.error(`❌ Error running skill action "${actionName}":`, message);
123
+ res.status(500).json({ error: message });
124
+ }
125
+ });
101
126
  app.listen(3030, "0.0.0.0");
102
127
  }
103
128
  else if (isDev) {
package/dist/lib/token.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
+ import { getBackendUrl } from './backend-url.js';
4
5
  export function saveToken(token, devMode) {
5
6
  // Save the token to .pulse-editor/config.json in user home directory
6
7
  const configDir = path.join(os.homedir(), '.pulse-editor');
@@ -62,9 +63,7 @@ export function isTokenInEnv(devMode) {
62
63
  return false;
63
64
  }
64
65
  export async function checkToken(token, devMode) {
65
- const res = await fetch(devMode
66
- ? 'https://localhost:8080/api/api-keys/check'
67
- : 'https://pulse-editor.com/api/api-keys/check', {
66
+ const res = await fetch(`${getBackendUrl(devMode)}/api/api-keys/check`, {
68
67
  body: JSON.stringify({ token }),
69
68
  headers: {
70
69
  'Content-Type': 'application/json',
@@ -1,2 +1,2 @@
1
- import webpack from 'webpack';
2
- export declare function webpackCompile(mode: 'development' | 'production' | 'preview', buildTarget?: 'client' | 'server', isWatchMode?: boolean): Promise<void | webpack.MultiCompiler>;
1
+ import webpack from "webpack";
2
+ export declare function webpackCompile(mode: "development" | "production" | "preview", buildTarget?: "client" | "server", isWatchMode?: boolean): Promise<void | webpack.MultiCompiler>;
@@ -1,23 +1,25 @@
1
- import webpack from 'webpack';
2
- import { createWebpackConfig } from './webpack-config.js';
1
+ import webpack from "webpack";
2
+ import { generateTempTsConfig } from "./configs/utils.js";
3
+ import { createWebpackConfig } from "./webpack-config.js";
3
4
  export async function webpackCompile(mode, buildTarget, isWatchMode = false) {
4
- const configs = await createWebpackConfig(mode === 'preview', buildTarget ?? 'both', mode === 'development'
5
- ? 'development'
6
- : mode === 'preview'
7
- ? 'development'
8
- : 'production');
5
+ generateTempTsConfig();
6
+ const configs = await createWebpackConfig(mode === "preview", buildTarget ?? "both", mode === "development"
7
+ ? "development"
8
+ : mode === "preview"
9
+ ? "development"
10
+ : "production");
9
11
  const compiler = webpack(configs);
10
12
  if (isWatchMode) {
11
13
  compiler.watch({}, (err, stats) => {
12
14
  if (err) {
13
- console.error('❌ Webpack build failed', err);
15
+ console.error("❌ Webpack build failed", err);
14
16
  return;
15
17
  }
16
18
  });
17
19
  return compiler;
18
20
  }
19
21
  return new Promise((resolve, reject) => {
20
- compiler.run(err => {
22
+ compiler.run((err) => {
21
23
  if (err) {
22
24
  reject(err);
23
25
  return;
@@ -20,6 +20,7 @@ class MFClientPlugin {
20
20
  if (compiler.options.mode === "development") {
21
21
  let isFirstRun = true;
22
22
  // Before build starts
23
+ // When a file changes and triggers a new compilation
23
24
  compiler.hooks.watchRun.tap("ReloadMessagePlugin", () => {
24
25
  if (!isFirstRun) {
25
26
  console.log("[client] 🔄 reloading app...");
@@ -5,7 +5,7 @@ import { globSync } from "glob";
5
5
  import path from "path";
6
6
  import { Node, Project, SyntaxKind } from "ts-morph";
7
7
  import wp from "webpack";
8
- import { discoverAppSkillActions, loadPulseConfig } from "./utils.js";
8
+ import { discoverAppSkillActions, discoverServerFunctions, loadPulseConfig, } from "./utils.js";
9
9
  const { NodeFederationPlugin } = mfNode;
10
10
  const { webpack } = wp;
11
11
  class MFServerPlugin {
@@ -18,19 +18,33 @@ class MFServerPlugin {
18
18
  apply(compiler) {
19
19
  if (compiler.options.mode === "development") {
20
20
  let isFirstRun = true;
21
+ compiler.hooks.environment.tap("WatchFileChangesPlugin", () => {
22
+ // Watch for file changes in the server-function directory to trigger server-function rebuilds
23
+ compiler.hooks.thisCompilation.tap("WatchServerFunctions", (compilation) => {
24
+ compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/server-function"));
25
+ });
26
+ // Watch for file changes in the action directory to trigger action rebuilds
27
+ compiler.hooks.thisCompilation.tap("WatchActions", (compilation) => {
28
+ compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/skill"));
29
+ });
30
+ });
21
31
  // Before build starts
22
- compiler.hooks.watchRun.tap("ReloadMessagePlugin", async (compilation) => {
32
+ compiler.hooks.beforeRun.tap("CleanDistPlugin", () => {
23
33
  this.cleanServerDist();
34
+ });
35
+ // When a file changes and triggers a new compilation
36
+ compiler.hooks.watchRun.tap("ReloadMessagePlugin", async (compiler) => {
37
+ this.printChanges(compiler);
24
38
  if (!isFirstRun) {
25
39
  console.log(`[Server] 🔄 Reloading app...`);
26
- const isServerFunctionChange = compilation.modifiedFiles
27
- ? Array.from(compilation.modifiedFiles).some((file) => file.includes("src/server-function"))
40
+ const isServerFunctionChange = compiler.modifiedFiles
41
+ ? Array.from(compiler.modifiedFiles).some((file) => file.includes("src/server-function"))
28
42
  : false;
29
43
  if (isServerFunctionChange) {
30
44
  await this.compileServerFunctions(compiler);
31
45
  }
32
- const isActionChange = compilation.modifiedFiles
33
- ? Array.from(compilation.modifiedFiles).some((file) => file.includes("src/action"))
46
+ const isActionChange = compiler.modifiedFiles
47
+ ? Array.from(compiler.modifiedFiles).some((file) => file.includes("src/skill"))
34
48
  : false;
35
49
  if (isActionChange) {
36
50
  console.log(`[Server] Detected changes in actions. Recompiling...`);
@@ -42,6 +56,14 @@ class MFServerPlugin {
42
56
  await this.compileServerFunctions(compiler);
43
57
  this.compileAppActionSkills();
44
58
  console.log(`[Server] ✅ Successfully built server.`);
59
+ const funcs = discoverServerFunctions();
60
+ console.log(`\n🛜 Server functions:
61
+ ${Object.entries(funcs)
62
+ .map(([name, file]) => {
63
+ return ` - ${name.slice(2)} (from ${file})`;
64
+ })
65
+ .join("\n")}
66
+ `);
45
67
  }
46
68
  });
47
69
  // After build finishes
@@ -53,14 +75,6 @@ class MFServerPlugin {
53
75
  console.log(`[Server] ✅ Reload finished.`);
54
76
  }
55
77
  });
56
- // Watch for file changes in the server-function directory to trigger server-function rebuilds
57
- compiler.hooks.thisCompilation.tap("WatchServerFunctions", (compilation) => {
58
- compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/server-function"));
59
- });
60
- // Watch for file changes in the action directory to trigger action rebuilds
61
- compiler.hooks.thisCompilation.tap("WatchActions", (compilation) => {
62
- compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/action"));
63
- });
64
78
  }
65
79
  else {
66
80
  // Print build success/failed message
@@ -74,8 +88,8 @@ class MFServerPlugin {
74
88
  this.compileAppActionSkills();
75
89
  }
76
90
  catch (err) {
77
- console.log(`[Server] ❌ Error during compilation:`, err);
78
- return;
91
+ console.error(`[Server] ❌ Error during compilation:`, err);
92
+ process.exit(1);
79
93
  }
80
94
  console.log(`[Server] ✅ Successfully built server.`);
81
95
  }
@@ -101,31 +115,6 @@ class MFServerPlugin {
101
115
  * @param compiler
102
116
  */
103
117
  async compileServerFunctions(compiler) {
104
- // Generate tsconfig for server functions
105
- function generateTempTsConfig() {
106
- const tempTsConfigPath = path.join(process.cwd(), "node_modules/.pulse/tsconfig.server.json");
107
- const tsConfig = {
108
- compilerOptions: {
109
- target: "ES2020",
110
- module: "esnext",
111
- moduleResolution: "bundler",
112
- strict: true,
113
- declaration: true,
114
- outDir: path.join(process.cwd(), "dist"),
115
- },
116
- include: [
117
- path.join(process.cwd(), "src/server-function/**/*"),
118
- path.join(process.cwd(), "pulse.config.ts"),
119
- path.join(process.cwd(), "global.d.ts"),
120
- ],
121
- exclude: [
122
- path.join(process.cwd(), "node_modules"),
123
- path.join(process.cwd(), "dist"),
124
- ],
125
- };
126
- fs.writeFileSync(tempTsConfigPath, JSON.stringify(tsConfig, null, 2));
127
- }
128
- generateTempTsConfig();
129
118
  // Run a new webpack compilation to pick up new server functions
130
119
  const options = {
131
120
  ...compiler.options,
@@ -155,31 +144,8 @@ class MFServerPlugin {
155
144
  });
156
145
  }
157
146
  makeNodeFederationPlugin() {
158
- function discoverServerFunctions() {
159
- // Get all .ts files under src/server-function and read use default exports as entry points
160
- const files = globSync("./src/server-function/**/*.ts");
161
- const entryPoints = files
162
- .map((file) => file.replaceAll("\\", "/"))
163
- .map((file) => {
164
- return {
165
- ["./" +
166
- file.replace("src/server-function/", "").replace(/\.ts$/, "")]: "./" + file,
167
- };
168
- })
169
- .reduce((acc, curr) => {
170
- return { ...acc, ...curr };
171
- }, {});
172
- return entryPoints;
173
- }
174
147
  const funcs = discoverServerFunctions();
175
148
  const actions = discoverAppSkillActions();
176
- console.log(`Discovered server functions:
177
- ${Object.entries(funcs)
178
- .map(([name, file]) => {
179
- return ` - ${name.slice(2)} (from ${file})`;
180
- })
181
- .join("\n")}
182
- `);
183
149
  return new NodeFederationPlugin({
184
150
  name: this.pulseConfig.id + "_server",
185
151
  remoteType: "script",
@@ -193,9 +159,9 @@ ${Object.entries(funcs)
193
159
  }, {});
194
160
  }
195
161
  /**
196
- * Register default functions defined in src/action as exposed modules in Module Federation.
162
+ * Register default functions defined in src/skill as exposed modules in Module Federation.
197
163
  * This will:
198
- * 1. Search for all .ts files under src/action
164
+ * 1. Search for all .ts files under src/skill
199
165
  * 2. Use ts-morph to get the default function information, including function name, parameters, and JSDoc comments
200
166
  * 3. Organize the functions' information into a list of Action
201
167
  * @param compiler
@@ -224,6 +190,15 @@ ${Object.entries(funcs)
224
190
  throw new Error(`Duplicate action name "${funcName}" detected in file ${file}. Please ensure all actions have unique names to avoid conflicts.`);
225
191
  }
226
192
  const defaultExportJSDocs = funcDecl.getJsDocs();
193
+ // Validate that the function has a JSDoc description
194
+ const descriptionText = defaultExportJSDocs
195
+ .map((doc) => doc.getDescription().replace(/^\*+/gm, "").trim())
196
+ .join("\n")
197
+ .trim();
198
+ if (defaultExportJSDocs.length === 0 || !descriptionText) {
199
+ throw new Error(`[Action Validation] Action "${funcName}" in ${file} is missing a JSDoc description. ` +
200
+ `Please add a JSDoc comment block with a description above the function.`);
201
+ }
227
202
  const description = defaultExportJSDocs
228
203
  .map((doc) => doc.getFullText())
229
204
  .join("\n");
@@ -250,12 +225,24 @@ ${Object.entries(funcs)
250
225
  }
251
226
  });
252
227
  }
253
- funcParam
254
- .getType()
255
- .getProperties()
256
- .forEach((prop) => {
228
+ const paramProperties = funcParam.getType().getProperties();
229
+ const inputTypeDef = typeDefs["input"] ?? {};
230
+ if (paramProperties.length > 0 && !typeDefs["input"]) {
231
+ throw new Error(`[Action Validation] Action "${funcName}" in ${file} has parameters but is missing an ` +
232
+ `"@typedef {Object} input" JSDoc block. Please document all parameters with ` +
233
+ `@typedef {Object} input and @property tags.`);
234
+ }
235
+ paramProperties.forEach((prop) => {
257
236
  const name = prop.getName();
258
- const inputTypeDef = typeDefs["input"] ?? {};
237
+ if (!inputTypeDef[name]) {
238
+ throw new Error(`[Action Validation] Action "${funcName}" in ${file}: parameter "${name}" is missing ` +
239
+ `a @property entry in the "input" JSDoc typedef. Please add ` +
240
+ `"@property {type} ${name} - description" to the JSDoc.`);
241
+ }
242
+ if (!inputTypeDef[name]?.description?.trim()) {
243
+ throw new Error(`[Action Validation] Action "${funcName}" in ${file}: parameter "${name}" has an empty ` +
244
+ `description in the JSDoc @property. Please provide a meaningful description.`);
245
+ }
259
246
  const variable = {
260
247
  description: inputTypeDef[name]?.description ?? "",
261
248
  type: this.getType(inputTypeDef[name]?.type ?? ""),
@@ -272,12 +259,24 @@ ${Object.entries(funcs)
272
259
  return;
273
260
  }
274
261
  const returns = {};
275
- funcDecl
276
- .getReturnType()
277
- .getProperties()
278
- .forEach((prop) => {
262
+ const returnProperties = funcDecl.getReturnType().getProperties();
263
+ const outputTypeDef = typeDefs["output"] ?? {};
264
+ if (returnProperties.length > 0 && !typeDefs["output"]) {
265
+ throw new Error(`[Action Validation] Action "${funcName}" in ${file} returns properties but is missing an ` +
266
+ `"@typedef {Object} output" JSDoc block. Please document all return values with ` +
267
+ `@typedef {Object} output and @property tags.`);
268
+ }
269
+ returnProperties.forEach((prop) => {
279
270
  const name = prop.getName();
280
- const outputTypeDef = typeDefs["output"] ?? {};
271
+ if (!outputTypeDef[name]) {
272
+ throw new Error(`[Action Validation] Action "${funcName}" in ${file}: return property "${name}" is missing ` +
273
+ `a @property entry in the "output" JSDoc typedef. Please add ` +
274
+ `"@property {type} ${name} - description" to the JSDoc.`);
275
+ }
276
+ if (!outputTypeDef[name]?.description?.trim()) {
277
+ throw new Error(`[Action Validation] Action "${funcName}" in ${file}: return property "${name}" has an empty ` +
278
+ `description in the JSDoc @property. Please provide a meaningful description.`);
279
+ }
281
280
  const variable = {
282
281
  description: outputTypeDef[name]?.description ?? "",
283
282
  type: this.getType(outputTypeDef[name]?.type ?? ""),
@@ -295,7 +294,6 @@ ${Object.entries(funcs)
295
294
  });
296
295
  });
297
296
  // You can now register `actions` in Module Federation or expose them as needed
298
- console.log("Discovered skill actions:\n", actions.map((a) => "- " + a.name).join("\n"));
299
297
  // Register actions in pulse config for runtime access
300
298
  this.pulseConfig.actions = actions;
301
299
  }
@@ -346,6 +344,20 @@ ${Object.entries(funcs)
346
344
  console.warn(`[Type Warning] Unrecognized type "${text}". Consider adding explicit types in your action's JSDoc comments for better type safety and documentation.`);
347
345
  return text;
348
346
  }
347
+ printChanges(compiler) {
348
+ const modified = compiler.modifiedFiles
349
+ ? Array.from(compiler.modifiedFiles)
350
+ : [];
351
+ const removed = compiler.removedFiles
352
+ ? Array.from(compiler.removedFiles)
353
+ : [];
354
+ const allChanges = [...modified, ...removed];
355
+ if (allChanges.length > 0) {
356
+ console.log(`[Server] ✏️ Detected file changes:\n${allChanges
357
+ .map((file) => ` - ${file}`)
358
+ .join("\n")}`);
359
+ }
360
+ }
349
361
  }
350
362
  export async function makeMFServerConfig(mode) {
351
363
  const projectDirName = process.cwd();
@@ -37,6 +37,13 @@ class PreviewClientPlugin {
37
37
  ✨ Try it out in your browser and let the magic happen! 🚀
38
38
  `;
39
39
  console.log("[client-preview] ✅ Successfully built preview.");
40
+ const skillActions = this.pulseConfig?.actions || [];
41
+ const actionNames = skillActions.map((a) => a.name);
42
+ if (actionNames.length > 0) {
43
+ console.log("\n🎯 Skill action endpoints:\n" +
44
+ actionNames.map((n) => ` - /skill/${n}`).join("\n") +
45
+ "\n");
46
+ }
40
47
  console.log(previewStartupMessage);
41
48
  isFirstRun = false;
42
49
  }
@@ -1,6 +1,10 @@
1
1
  export declare function loadPulseConfig(): Promise<any>;
2
2
  export declare function getLocalNetworkIP(): string;
3
3
  export declare function readConfigFile(): Promise<any>;
4
+ export declare function discoverServerFunctions(): {
5
+ [x: string]: string;
6
+ };
4
7
  export declare function discoverAppSkillActions(): {
5
8
  [x: string]: string;
6
9
  } | null;
10
+ export declare function generateTempTsConfig(): void;
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { existsSync } from "fs";
2
+ import { existsSync, writeFileSync } from "fs";
3
3
  import fs from "fs/promises";
4
4
  import { globSync } from "glob";
5
5
  import { networkInterfaces } from "os";
@@ -81,6 +81,21 @@ export async function readConfigFile() {
81
81
  const data = await fs.readFile("dist/pulse.config.json", "utf-8");
82
82
  return JSON.parse(data);
83
83
  }
84
+ export function discoverServerFunctions() {
85
+ // Get all .ts files under src/server-function and read use default exports as entry points
86
+ const files = globSync("./src/server-function/**/*.ts");
87
+ const entryPoints = files
88
+ .map((file) => file.replaceAll("\\", "/"))
89
+ .map((file) => {
90
+ return {
91
+ ["./" + file.replace("src/server-function/", "").replace(/\.ts$/, "")]: "./" + file,
92
+ };
93
+ })
94
+ .reduce((acc, curr) => {
95
+ return { ...acc, ...curr };
96
+ }, {});
97
+ return entryPoints;
98
+ }
84
99
  export function discoverAppSkillActions() {
85
100
  // Get all .ts files under src/skill and read use default exports as entry points
86
101
  const files = globSync("./src/skill/*/action.ts");
@@ -116,3 +131,30 @@ export function discoverAppSkillActions() {
116
131
  }, {});
117
132
  return entryPoints;
118
133
  }
134
+ // Generate tsconfig for server functions
135
+ export function generateTempTsConfig() {
136
+ const tempTsConfigPath = path.join(process.cwd(), "node_modules/.pulse/tsconfig.server.json");
137
+ if (existsSync(tempTsConfigPath)) {
138
+ return;
139
+ }
140
+ const tsConfig = {
141
+ compilerOptions: {
142
+ target: "ES2020",
143
+ module: "esnext",
144
+ moduleResolution: "bundler",
145
+ strict: true,
146
+ declaration: true,
147
+ outDir: path.join(process.cwd(), "dist"),
148
+ },
149
+ include: [
150
+ path.join(process.cwd(), "src/server-function/**/*"),
151
+ path.join(process.cwd(), "pulse.config.ts"),
152
+ path.join(process.cwd(), "global.d.ts"),
153
+ ],
154
+ exclude: [
155
+ path.join(process.cwd(), "node_modules"),
156
+ path.join(process.cwd(), "dist"),
157
+ ],
158
+ };
159
+ writeFileSync(tempTsConfigPath, JSON.stringify(tsConfig, null, 2));
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-editor/cli",
3
- "version": "0.1.1-beta.36",
3
+ "version": "0.1.1-beta.38",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "pulse": "dist/cli.js"