@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 +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/compile.d.ts +2 -2
- package/dist/lib/webpack/compile.js +11 -9
- package/dist/lib/webpack/configs/mf-client.js +1 -0
- package/dist/lib/webpack/configs/mf-server.js +89 -77
- package/dist/lib/webpack/configs/preview.js +7 -0
- package/dist/lib/webpack/configs/utils.d.ts +4 -0
- package/dist/lib/webpack/configs/utils.js +43 -1
- 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',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import webpack from
|
|
2
|
-
export declare function webpackCompile(mode:
|
|
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
|
|
2
|
-
import {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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(
|
|
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.
|
|
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
|
}
|
|
@@ -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/
|
|
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/
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
+
}
|