@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 +2 -1
- package/dist/components/commands/skill.d.ts +5 -0
- package/dist/components/commands/skill.js +230 -0
- package/dist/lib/backend/publish-app.js +2 -3
- package/dist/lib/backend-url.d.ts +1 -0
- package/dist/lib/backend-url.js +3 -0
- package/dist/lib/cli-flags.d.ts +4 -0
- package/dist/lib/cli-flags.js +4 -0
- package/dist/lib/manual.js +14 -0
- package/dist/lib/server/express.js +25 -0
- package/dist/lib/token.js +2 -3
- package/dist/lib/webpack/configs/mf-client.js +1 -0
- package/dist/lib/webpack/configs/mf-server.js +89 -52
- package/dist/lib/webpack/configs/preview.js +7 -0
- package/dist/lib/webpack/configs/utils.d.ts +3 -0
- package/dist/lib/webpack/configs/utils.js +15 -0
- package/package.json +1 -1
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,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";
|
package/dist/lib/cli-flags.d.ts
CHANGED
package/dist/lib/cli-flags.js
CHANGED
package/dist/lib/manual.js
CHANGED
|
@@ -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.
|
|
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 =
|
|
27
|
-
? Array.from(
|
|
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 =
|
|
33
|
-
? Array.from(
|
|
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.
|
|
78
|
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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");
|