@pulse-editor/cli 0.1.1-beta.37 → 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',
@@ -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
  }
@@ -130,31 +144,8 @@ class MFServerPlugin {
130
144
  });
131
145
  }
132
146
  makeNodeFederationPlugin() {
133
- function discoverServerFunctions() {
134
- // Get all .ts files under src/server-function and read use default exports as entry points
135
- const files = globSync("./src/server-function/**/*.ts");
136
- const entryPoints = files
137
- .map((file) => file.replaceAll("\\", "/"))
138
- .map((file) => {
139
- return {
140
- ["./" +
141
- file.replace("src/server-function/", "").replace(/\.ts$/, "")]: "./" + file,
142
- };
143
- })
144
- .reduce((acc, curr) => {
145
- return { ...acc, ...curr };
146
- }, {});
147
- return entryPoints;
148
- }
149
147
  const funcs = discoverServerFunctions();
150
148
  const actions = discoverAppSkillActions();
151
- console.log(`Discovered server functions:
152
- ${Object.entries(funcs)
153
- .map(([name, file]) => {
154
- return ` - ${name.slice(2)} (from ${file})`;
155
- })
156
- .join("\n")}
157
- `);
158
149
  return new NodeFederationPlugin({
159
150
  name: this.pulseConfig.id + "_server",
160
151
  remoteType: "script",
@@ -168,9 +159,9 @@ ${Object.entries(funcs)
168
159
  }, {});
169
160
  }
170
161
  /**
171
- * 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.
172
163
  * This will:
173
- * 1. Search for all .ts files under src/action
164
+ * 1. Search for all .ts files under src/skill
174
165
  * 2. Use ts-morph to get the default function information, including function name, parameters, and JSDoc comments
175
166
  * 3. Organize the functions' information into a list of Action
176
167
  * @param compiler
@@ -199,6 +190,15 @@ ${Object.entries(funcs)
199
190
  throw new Error(`Duplicate action name "${funcName}" detected in file ${file}. Please ensure all actions have unique names to avoid conflicts.`);
200
191
  }
201
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
+ }
202
202
  const description = defaultExportJSDocs
203
203
  .map((doc) => doc.getFullText())
204
204
  .join("\n");
@@ -225,12 +225,24 @@ ${Object.entries(funcs)
225
225
  }
226
226
  });
227
227
  }
228
- funcParam
229
- .getType()
230
- .getProperties()
231
- .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) => {
232
236
  const name = prop.getName();
233
- 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
+ }
234
246
  const variable = {
235
247
  description: inputTypeDef[name]?.description ?? "",
236
248
  type: this.getType(inputTypeDef[name]?.type ?? ""),
@@ -247,12 +259,24 @@ ${Object.entries(funcs)
247
259
  return;
248
260
  }
249
261
  const returns = {};
250
- funcDecl
251
- .getReturnType()
252
- .getProperties()
253
- .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) => {
254
270
  const name = prop.getName();
255
- 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
+ }
256
280
  const variable = {
257
281
  description: outputTypeDef[name]?.description ?? "",
258
282
  type: this.getType(outputTypeDef[name]?.type ?? ""),
@@ -270,7 +294,6 @@ ${Object.entries(funcs)
270
294
  });
271
295
  });
272
296
  // You can now register `actions` in Module Federation or expose them as needed
273
- console.log("Discovered skill actions:\n", actions.map((a) => "- " + a.name).join("\n"));
274
297
  // Register actions in pulse config for runtime access
275
298
  this.pulseConfig.actions = actions;
276
299
  }
@@ -321,6 +344,20 @@ ${Object.entries(funcs)
321
344
  console.warn(`[Type Warning] Unrecognized type "${text}". Consider adding explicit types in your action's JSDoc comments for better type safety and documentation.`);
322
345
  return text;
323
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
+ }
324
361
  }
325
362
  export async function makeMFServerConfig(mode) {
326
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,9 @@
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;
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-editor/cli",
3
- "version": "0.1.1-beta.37",
3
+ "version": "0.1.1-beta.38",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "pulse": "dist/cli.js"