@pulse-editor/cli 0.1.1-beta.9 → 0.1.3

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.
Files changed (59) hide show
  1. package/dist/app.js +4 -1
  2. package/dist/components/commands/build.js +18 -33
  3. package/dist/components/commands/code.d.ts +5 -0
  4. package/dist/components/commands/code.js +342 -0
  5. package/dist/components/commands/create.js +38 -12
  6. package/dist/components/commands/dev.js +35 -8
  7. package/dist/components/commands/login.d.ts +2 -2
  8. package/dist/components/commands/login.js +110 -26
  9. package/dist/components/commands/preview.js +50 -11
  10. package/dist/components/commands/publish.js +23 -37
  11. package/dist/components/commands/{start copy.d.ts → skill.d.ts} +1 -1
  12. package/dist/components/commands/skill.js +230 -0
  13. package/dist/components/commands/{preview copy.d.ts → upgrade.d.ts} +1 -1
  14. package/dist/components/commands/upgrade.js +53 -0
  15. package/dist/lib/backend/publish-app.d.ts +1 -0
  16. package/dist/lib/backend/publish-app.js +26 -0
  17. package/dist/lib/backend-url.d.ts +1 -0
  18. package/dist/lib/backend-url.js +3 -0
  19. package/dist/lib/cli-flags.d.ts +22 -0
  20. package/dist/lib/cli-flags.js +22 -0
  21. package/dist/lib/manual.js +40 -0
  22. package/dist/lib/server/express.js +81 -41
  23. package/dist/lib/server/preview/backend/load-remote.cjs +28 -18
  24. package/dist/lib/server/utils.js +3 -3
  25. package/dist/lib/token.js +2 -3
  26. package/dist/lib/webpack/compile.d.ts +2 -0
  27. package/dist/lib/webpack/compile.js +30 -0
  28. package/dist/lib/webpack/configs/mf-client.d.ts +3 -0
  29. package/dist/lib/webpack/configs/mf-client.js +184 -0
  30. package/dist/lib/webpack/configs/mf-server.d.ts +2 -0
  31. package/dist/lib/webpack/configs/mf-server.js +463 -0
  32. package/dist/lib/webpack/configs/preview.d.ts +3 -0
  33. package/dist/lib/webpack/configs/preview.js +117 -0
  34. package/dist/lib/webpack/configs/utils.d.ts +10 -0
  35. package/dist/lib/webpack/configs/utils.js +172 -0
  36. package/dist/lib/webpack/dist/pregistered-actions.d.ts +2 -0
  37. package/dist/lib/webpack/dist/pulse.config.d.ts +7 -0
  38. package/dist/lib/webpack/dist/src/lib/agents/code-modifier-agent.d.ts +2 -0
  39. package/dist/lib/webpack/dist/src/lib/agents/vibe-coding-agent.d.ts +3 -0
  40. package/dist/lib/webpack/dist/src/lib/mcp/utils.d.ts +3 -0
  41. package/dist/lib/webpack/dist/src/lib/streaming/message-stream-controller.d.ts +10 -0
  42. package/dist/lib/webpack/dist/src/lib/types.d.ts +58 -0
  43. package/dist/lib/webpack/dist/src/server-function/generate-code/v1/generate.d.ts +5 -0
  44. package/dist/lib/webpack/dist/src/server-function/generate-code/v2/generate.d.ts +1 -0
  45. package/dist/lib/webpack/tsconfig.server.json +19 -0
  46. package/dist/lib/webpack/webpack-config.d.ts +1 -0
  47. package/dist/lib/webpack/webpack-config.js +24 -0
  48. package/dist/lib/webpack/webpack.config.d.ts +2 -0
  49. package/dist/lib/webpack/webpack.config.js +527 -0
  50. package/package.json +31 -20
  51. package/readme.md +7 -1
  52. package/dist/components/commands/preview copy.js +0 -14
  53. package/dist/components/commands/start copy.js +0 -14
  54. package/dist/lib/deps.d.ts +0 -1
  55. package/dist/lib/deps.js +0 -5
  56. package/dist/lib/node_module_bin.d.ts +0 -1
  57. package/dist/lib/node_module_bin.js +0 -7
  58. package/dist/lib/server/preview/backend/index.d.ts +0 -1
  59. package/dist/lib/server/preview/backend/index.js +0 -23
@@ -0,0 +1,184 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ModuleFederationPlugin } from "@module-federation/enhanced/webpack";
3
+ import CopyWebpackPlugin from "copy-webpack-plugin";
4
+ import fs from "fs";
5
+ import { globSync } from "glob";
6
+ import MiniCssExtractPlugin from "mini-css-extract-plugin";
7
+ import path from "path";
8
+ import ts from "typescript";
9
+ import { discoverAppSkillActions, getLocalNetworkIP, loadPulseConfig, } from "./utils.js";
10
+ class MFClientPlugin {
11
+ projectDirName;
12
+ pulseConfig;
13
+ origin;
14
+ constructor(pulseConfig) {
15
+ this.projectDirName = process.cwd();
16
+ this.pulseConfig = pulseConfig;
17
+ this.origin = getLocalNetworkIP();
18
+ }
19
+ apply(compiler) {
20
+ if (compiler.options.mode === "development") {
21
+ let isFirstRun = true;
22
+ // Before build starts
23
+ // When a file changes and triggers a new compilation
24
+ compiler.hooks.watchRun.tap("ReloadMessagePlugin", () => {
25
+ if (!isFirstRun) {
26
+ console.log("[client] 🔄 reloading app...");
27
+ }
28
+ else {
29
+ console.log("[client] 🔄 building app...");
30
+ }
31
+ });
32
+ // Log file updates
33
+ compiler.hooks.invalid.tap("LogFileUpdates", (file, changeTime) => {
34
+ console.log(`[watch] change detected in: ${file} at ${new Date(changeTime || Date.now()).toLocaleTimeString()}`);
35
+ });
36
+ const devStartupMessage = `
37
+ 🎉 Your Pulse extension \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
38
+
39
+ ⚡️ Local: http://localhost:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
40
+ ⚡️ Network: http://${this.origin}:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
41
+
42
+ ✨ Try it out in the Pulse Editor and let the magic happen! 🚀
43
+ `;
44
+ // After build finishes
45
+ compiler.hooks.done.tap("ReloadMessagePlugin", () => {
46
+ if (isFirstRun) {
47
+ console.log("[client] ✅ Successfully built client.");
48
+ console.log(devStartupMessage);
49
+ isFirstRun = false;
50
+ }
51
+ else {
52
+ console.log("[client] ✅ Reload finished.");
53
+ }
54
+ // Write pulse config to dist
55
+ fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
56
+ });
57
+ }
58
+ else {
59
+ // Print build success/failed message
60
+ compiler.hooks.done.tap("BuildMessagePlugin", (stats) => {
61
+ if (stats.hasErrors()) {
62
+ console.log(`[client] ❌ Failed to build client.`);
63
+ }
64
+ else {
65
+ console.log(`[client] ✅ Successfully built client.`);
66
+ // Write pulse config to dist
67
+ fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
68
+ }
69
+ });
70
+ }
71
+ compiler.hooks.beforeCompile.tap("PulseConfigPlugin", () => {
72
+ let requireFS = false;
73
+ function isWorkspaceHook(node) {
74
+ return (ts.isCallExpression(node) &&
75
+ ts.isIdentifier(node.expression) &&
76
+ [
77
+ "useFileSystem",
78
+ "useFile",
79
+ "useReceiveFile",
80
+ "useTerminal",
81
+ "useWorkspaceInfo",
82
+ ].includes(node.expression.text));
83
+ }
84
+ function scanSource(sourceText) {
85
+ const sourceFile = ts.createSourceFile("temp.tsx", sourceText, ts.ScriptTarget.Latest, true);
86
+ const visit = (node) => {
87
+ // Detect: useFileSystem(...)
88
+ if (isWorkspaceHook(node)) {
89
+ requireFS = true;
90
+ }
91
+ ts.forEachChild(node, visit);
92
+ };
93
+ visit(sourceFile);
94
+ }
95
+ globSync(["src/**/*.tsx", "src/**/*.ts"]).forEach((file) => {
96
+ const source = fs.readFileSync(file, "utf8");
97
+ scanSource(source);
98
+ });
99
+ // Persist result
100
+ this.pulseConfig.requireWorkspace = requireFS;
101
+ });
102
+ }
103
+ }
104
+ export async function makeMFClientConfig(mode) {
105
+ const projectDirName = process.cwd();
106
+ const pulseConfig = await loadPulseConfig();
107
+ const mainComponent = "./src/main.tsx";
108
+ const actions = discoverAppSkillActions();
109
+ return {
110
+ mode: mode,
111
+ name: "client",
112
+ entry: mainComponent,
113
+ output: {
114
+ publicPath: "auto",
115
+ path: path.resolve(projectDirName, "dist/client"),
116
+ },
117
+ resolve: {
118
+ extensions: [".ts", ".tsx", ".js"],
119
+ },
120
+ plugins: [
121
+ new MiniCssExtractPlugin({
122
+ filename: "globals.css",
123
+ }),
124
+ // Copy assets to dist
125
+ new CopyWebpackPlugin({
126
+ patterns: [{ from: "src/assets", to: "assets" }],
127
+ }),
128
+ new ModuleFederationPlugin({
129
+ // Do not use hyphen character '-' in the name
130
+ name: pulseConfig.id + "_client",
131
+ filename: "remoteEntry.js",
132
+ exposes: {
133
+ "./main": mainComponent,
134
+ ...actions,
135
+ },
136
+ shared: {
137
+ react: {
138
+ requiredVersion: "19.2.0",
139
+ import: "react", // the "react" package will be used a provided and fallback module
140
+ shareKey: "react", // under this name the shared module will be placed in the share scope
141
+ shareScope: "default", // share scope with this name will be used
142
+ singleton: true, // only a single version of the shared module is allowed
143
+ },
144
+ "react-dom": {
145
+ requiredVersion: "19.2.0",
146
+ singleton: true, // only a single version of the shared module is allowed
147
+ },
148
+ },
149
+ }),
150
+ new MFClientPlugin(pulseConfig),
151
+ ],
152
+ module: {
153
+ rules: [
154
+ {
155
+ test: /\.tsx?$/,
156
+ use: "ts-loader",
157
+ exclude: [/node_modules/, /dist/],
158
+ },
159
+ {
160
+ test: /\.css$/i,
161
+ use: [
162
+ MiniCssExtractPlugin.loader,
163
+ "css-loader",
164
+ {
165
+ loader: "postcss-loader",
166
+ },
167
+ ],
168
+ exclude: [/dist/],
169
+ },
170
+ ],
171
+ },
172
+ stats: {
173
+ all: false,
174
+ errors: true,
175
+ warnings: true,
176
+ logging: "warn",
177
+ colors: true,
178
+ assets: false,
179
+ },
180
+ infrastructureLogging: {
181
+ level: "warn",
182
+ },
183
+ };
184
+ }
@@ -0,0 +1,2 @@
1
+ import { Configuration as WebpackConfig } from "webpack";
2
+ export declare function makeMFServerConfig(mode: "development" | "production"): Promise<WebpackConfig>;
@@ -0,0 +1,463 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import mfNode from "@module-federation/node";
3
+ import fs from "fs";
4
+ import { globSync } from "glob";
5
+ import path from "path";
6
+ import { Node, Project, SyntaxKind } from "ts-morph";
7
+ import wp from "webpack";
8
+ import { discoverAppSkillActions, discoverServerFunctions, loadPulseConfig, } from "./utils.js";
9
+ const { NodeFederationPlugin } = mfNode;
10
+ const { webpack } = wp;
11
+ class MFServerPlugin {
12
+ projectDirName;
13
+ pulseConfig;
14
+ constructor(pulseConfig) {
15
+ this.projectDirName = process.cwd();
16
+ this.pulseConfig = pulseConfig;
17
+ }
18
+ apply(compiler) {
19
+ if (compiler.options.mode === "development") {
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
+ });
31
+ // Before build starts
32
+ compiler.hooks.beforeRun.tap("CleanDistPlugin", () => {
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);
38
+ if (!isFirstRun) {
39
+ console.log(`[Server] 🔄 Reloading app...`);
40
+ const isServerFunctionChange = compiler.modifiedFiles
41
+ ? Array.from(compiler.modifiedFiles).some((file) => file.includes("src/server-function"))
42
+ : false;
43
+ if (isServerFunctionChange) {
44
+ await this.compileServerFunctions(compiler);
45
+ }
46
+ const isActionChange = compiler.modifiedFiles
47
+ ? Array.from(compiler.modifiedFiles).some((file) => file.includes("src/skill"))
48
+ : false;
49
+ if (isActionChange) {
50
+ console.log(`[Server] Detected changes in actions. Recompiling...`);
51
+ this.compileAppActionSkills();
52
+ }
53
+ }
54
+ else {
55
+ console.log(`[Server] 🔄 Building app...`);
56
+ await this.compileServerFunctions(compiler);
57
+ this.compileAppActionSkills();
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
+ `);
67
+ }
68
+ });
69
+ // After build finishes
70
+ compiler.hooks.done.tap("ReloadMessagePlugin", () => {
71
+ if (isFirstRun) {
72
+ isFirstRun = false;
73
+ }
74
+ else {
75
+ console.log(`[Server] ✅ Reload finished.`);
76
+ }
77
+ });
78
+ }
79
+ else {
80
+ // Print build success/failed message
81
+ compiler.hooks.done.tap("BuildMessagePlugin", async (stats) => {
82
+ if (stats.hasErrors()) {
83
+ console.log(`[Server] ❌ Failed to build server.`);
84
+ }
85
+ else {
86
+ try {
87
+ await this.compileServerFunctions(compiler);
88
+ this.compileAppActionSkills();
89
+ }
90
+ catch (err) {
91
+ console.error(`[Server] ❌ Error during compilation:`, err);
92
+ process.exit(1);
93
+ }
94
+ console.log(`[Server] ✅ Successfully built server.`);
95
+ }
96
+ });
97
+ }
98
+ }
99
+ cleanServerDist() {
100
+ // Remove existing entry points
101
+ try {
102
+ fs.rmSync("dist/server", { recursive: true, force: true });
103
+ }
104
+ catch (e) {
105
+ console.error("Error removing dist/server:", e);
106
+ console.log("Continuing...");
107
+ }
108
+ }
109
+ /**
110
+ * Programmatically call webpack to compile server functions
111
+ * whenever there are changes in the src/server-function directory.
112
+ * This is necessary because Module Federation needs to know about
113
+ * all the exposed modules at build time, so we have to trigger a
114
+ * new compilation whenever server functions are added/removed/changed.
115
+ * @param compiler
116
+ */
117
+ async compileServerFunctions(compiler) {
118
+ // Run a new webpack compilation to pick up new server functions
119
+ const options = {
120
+ ...compiler.options,
121
+ watch: false,
122
+ plugins: [
123
+ // Add a new NodeFederationPlugin with updated entry points
124
+ this.makeNodeFederationPlugin(),
125
+ ],
126
+ };
127
+ const newCompiler = webpack(options);
128
+ // Run the new compiler
129
+ return new Promise((resolve, reject) => {
130
+ newCompiler?.run((err, stats) => {
131
+ if (err) {
132
+ console.error(`[Server] ❌ Error during recompilation:`, err);
133
+ reject(err);
134
+ }
135
+ else if (stats?.hasErrors()) {
136
+ console.error(`[Server] ❌ Compilation errors:`, stats.toJson().errors);
137
+ reject(new Error("Compilation errors"));
138
+ }
139
+ else {
140
+ console.log(`[Server] ✅ Compiled server functions successfully.`);
141
+ resolve();
142
+ }
143
+ });
144
+ });
145
+ }
146
+ makeNodeFederationPlugin() {
147
+ const funcs = discoverServerFunctions();
148
+ const actions = discoverAppSkillActions();
149
+ return new NodeFederationPlugin({
150
+ name: this.pulseConfig.id + "_server",
151
+ remoteType: "script",
152
+ useRuntimePlugin: true,
153
+ library: { type: "commonjs-module" },
154
+ filename: "remoteEntry.js",
155
+ exposes: {
156
+ ...funcs,
157
+ ...actions,
158
+ },
159
+ }, {});
160
+ }
161
+ /**
162
+ * Register default functions defined in src/skill as exposed modules in Module Federation.
163
+ * This will:
164
+ * 1. Search for all .ts files under src/skill
165
+ * 2. Use ts-morph to get the default function information, including function name, parameters, and JSDoc comments
166
+ * 3. Organize the functions' information into a list of Action
167
+ * @param compiler
168
+ */
169
+ compileAppActionSkills() {
170
+ // 1. Get all TypeScript files under src/skill
171
+ const files = globSync("./src/skill/*/action.ts");
172
+ const project = new Project({
173
+ tsConfigFilePath: path.join(process.cwd(), "node_modules/.pulse/tsconfig.server.json"),
174
+ });
175
+ const actions = [];
176
+ files.forEach((file) => {
177
+ const sourceFile = project.addSourceFileAtPath(file);
178
+ const defaultExportSymbol = sourceFile.getDefaultExportSymbol();
179
+ if (!defaultExportSymbol)
180
+ return;
181
+ const defaultExportDeclarations = defaultExportSymbol.getDeclarations();
182
+ defaultExportDeclarations.forEach((declaration) => {
183
+ if (declaration.getKind() !== SyntaxKind.FunctionDeclaration)
184
+ return;
185
+ const funcDecl = declaration.asKindOrThrow(SyntaxKind.FunctionDeclaration);
186
+ // Get action name from path `src/skill/{actionName}/action.ts`
187
+ // Match `*/src/skill/{actionName}/action.ts` and extract {actionName}
188
+ const pattern = /src\/skill\/([^\/]+)\/action\.ts$/;
189
+ const match = file.replaceAll("\\", "/").match(pattern);
190
+ if (!match) {
191
+ console.warn(`File path ${file} does not match pattern ${pattern}. Skipping...`);
192
+ return;
193
+ }
194
+ const actionName = match[1];
195
+ if (!actionName) {
196
+ console.warn(`Could not extract action name from file path ${file}. Skipping...`);
197
+ return;
198
+ }
199
+ // Throw an error if the funcName is duplicated with an existing action to prevent accidental overwriting
200
+ if (actions.some((action) => action.name === actionName)) {
201
+ throw new Error(`Duplicate action name "${actionName}" detected in file ${file}. Please ensure all actions have unique names to avoid conflicts.`);
202
+ }
203
+ const defaultExportJSDocs = funcDecl.getJsDocs();
204
+ // Validate that the function has a JSDoc description
205
+ const descriptionText = defaultExportJSDocs
206
+ .map((doc) => doc.getDescription().replace(/^\*+/gm, "").trim())
207
+ .join("\n")
208
+ .trim();
209
+ if (defaultExportJSDocs.length === 0 || !descriptionText) {
210
+ throw new Error(`[Action Validation] Action "${actionName}" in ${file} is missing a JSDoc description. ` +
211
+ `Please add a JSDoc comment block with a description above the function.` +
212
+ `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
213
+ }
214
+ const description = defaultExportJSDocs
215
+ .map((doc) => doc.getFullText())
216
+ .join("\n");
217
+ const allJSDocs = sourceFile.getDescendantsOfKind(SyntaxKind.JSDoc);
218
+ const typeDefs = this.parseTypeDefs(allJSDocs);
219
+ /* Extract parameter descriptions from JSDoc */
220
+ const funcParam = funcDecl.getParameters()[0];
221
+ const params = {};
222
+ if (funcParam) {
223
+ /**
224
+ * Extract default values from the destructured parameter
225
+ * (ObjectBindingPattern → BindingElement initializer)
226
+ */
227
+ const defaults = new Map();
228
+ const nameNode = funcParam.getNameNode();
229
+ if (Node.isObjectBindingPattern(nameNode)) {
230
+ nameNode.getElements().forEach((el) => {
231
+ if (!Node.isBindingElement(el))
232
+ return;
233
+ const name = el.getName();
234
+ const initializer = el.getInitializer()?.getText();
235
+ if (initializer) {
236
+ defaults.set(name, initializer);
237
+ }
238
+ });
239
+ }
240
+ const paramProperties = funcParam.getType().getProperties();
241
+ const inputTypeDef = typeDefs["input"] ?? {};
242
+ if (paramProperties.length > 0 && !typeDefs["input"]) {
243
+ throw new Error(`[Action Validation] Action "${actionName}" in ${file} has parameters but is missing an ` +
244
+ `"@typedef {Object} input" JSDoc block. Please document all parameters with ` +
245
+ `@typedef {Object} input and @property tags.` +
246
+ `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
247
+ }
248
+ paramProperties.forEach((prop) => {
249
+ const name = prop.getName();
250
+ if (!inputTypeDef[name]) {
251
+ throw new Error(`[Action Validation] Action "${actionName}" in ${file}: parameter "${name}" is missing ` +
252
+ `a @property entry in the "input" JSDoc typedef. Please add ` +
253
+ `"@property {type} ${name} - description" to the JSDoc.` +
254
+ `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
255
+ }
256
+ if (!inputTypeDef[name]?.description?.trim()) {
257
+ throw new Error(`[Action Validation] Action "${actionName}" in ${file}: parameter "${name}" has an empty ` +
258
+ `description in the JSDoc @property. Please provide a meaningful description.` +
259
+ `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
260
+ }
261
+ const variable = {
262
+ description: inputTypeDef[name]?.description ?? "",
263
+ type: this.getType(inputTypeDef[name]?.type ?? ""),
264
+ optional: prop.isOptional() ? true : undefined,
265
+ defaultValue: defaults.get(name),
266
+ };
267
+ params[name] = variable;
268
+ });
269
+ }
270
+ /* Extract return type from JSDoc */
271
+ const rawReturnType = funcDecl.getReturnType();
272
+ const isPromiseLikeReturn = this.isPromiseLikeType(rawReturnType);
273
+ const returnType = this.unwrapPromiseLikeType(rawReturnType);
274
+ // Check if the return type is an object
275
+ if (!returnType.isObject()) {
276
+ console.warn(`[Action Registration] Function ${actionName}'s return type should be an object. Skipping...`);
277
+ return;
278
+ }
279
+ const returns = {};
280
+ const returnProperties = returnType.getProperties();
281
+ const outputTypeDef = typeDefs["output"] ?? {};
282
+ const hasOutputTypeDef = !!typeDefs["output"];
283
+ if (returnProperties.length > 0 &&
284
+ !hasOutputTypeDef &&
285
+ !isPromiseLikeReturn) {
286
+ throw new Error(`[Action Validation] Action "${actionName}" in ${file} returns properties but is missing an ` +
287
+ `"@typedef {Output}" JSDoc block. Please document all return values with ` +
288
+ `@typedef {Output} and @property tags.` +
289
+ `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
290
+ }
291
+ if (returnProperties.length > 0 && !hasOutputTypeDef && isPromiseLikeReturn) {
292
+ console.warn(`[Action Validation] Action "${actionName}" in ${file} is missing an "@typedef {Object} output" JSDoc block. ` +
293
+ `Falling back to TypeScript-inferred return metadata because the action returns a Promise.`);
294
+ }
295
+ returnProperties.forEach((prop) => {
296
+ const name = prop.getName();
297
+ if (!hasOutputTypeDef) {
298
+ const variable = {
299
+ description: "",
300
+ type: this.getType(prop.getTypeAtLocation(funcDecl).getText()),
301
+ optional: prop.isOptional() ? true : undefined,
302
+ defaultValue: undefined,
303
+ };
304
+ returns[name] = variable;
305
+ return;
306
+ }
307
+ if (!outputTypeDef[name]) {
308
+ throw new Error(`[Action Validation] Action "${actionName}" in ${file}: return property "${name}" is missing ` +
309
+ `a @property entry in the "output" JSDoc typedef. Please add ` +
310
+ `"@property {type} ${name} - description" to the JSDoc.` +
311
+ `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
312
+ }
313
+ if (!outputTypeDef[name]?.description?.trim()) {
314
+ throw new Error(`[Action Validation] Action "${actionName}" in ${file}: return property "${name}" has an empty ` +
315
+ `description in the JSDoc @property. Please provide a meaningful description.` +
316
+ `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
317
+ }
318
+ const variable = {
319
+ description: outputTypeDef[name]?.description ?? "",
320
+ type: this.getType(outputTypeDef[name]?.type ?? ""),
321
+ optional: prop.isOptional() ? true : undefined,
322
+ defaultValue: undefined,
323
+ };
324
+ returns[name] = variable;
325
+ });
326
+ actions.push({
327
+ name: actionName,
328
+ description,
329
+ parameters: params,
330
+ returns,
331
+ });
332
+ });
333
+ });
334
+ // You can now register `actions` in Module Federation or expose them as needed
335
+ // Register actions in pulse config for runtime access
336
+ this.pulseConfig.actions = actions;
337
+ }
338
+ parseTypeDefs(jsDocs) {
339
+ const typeDefs = {};
340
+ // Match @typedef {Type} Name
341
+ const typedefRegex = /@typedef\s+{([^}]+)}\s+([^\s-]+)/g;
342
+ // Match @property {Type} [name] Description text...
343
+ const propertyRegex = /@property\s+{([^}]+)}\s+(\[?[^\]\s]+\]?)\s*-?\s*(.*)/g;
344
+ jsDocs.forEach((doc) => {
345
+ const text = doc.getFullText();
346
+ let typedefMatches;
347
+ while ((typedefMatches = typedefRegex.exec(text)) !== null) {
348
+ const typeName = typedefMatches[2];
349
+ if (!typeName)
350
+ continue;
351
+ const properties = {};
352
+ let propertyMatches;
353
+ while ((propertyMatches = propertyRegex.exec(text)) !== null) {
354
+ const propName = this.normalizeJSDocPropertyName(propertyMatches[2]);
355
+ const propType = propertyMatches[1];
356
+ const propDescription = propertyMatches[3] || "";
357
+ if (propName && propType) {
358
+ properties[propName] = {
359
+ type: propType,
360
+ description: propDescription.trim(),
361
+ };
362
+ }
363
+ }
364
+ typeDefs[typeName.toLowerCase()] = properties;
365
+ }
366
+ });
367
+ return typeDefs;
368
+ }
369
+ normalizeJSDocPropertyName(name) {
370
+ if (!name)
371
+ return "";
372
+ return name
373
+ .trim()
374
+ .replace(/^\[/, "")
375
+ .replace(/\]$/, "")
376
+ .split("=")[0]
377
+ ?.trim();
378
+ }
379
+ isPromiseLikeType(type) {
380
+ const symbolName = type.getSymbol()?.getName();
381
+ return symbolName === "Promise" || symbolName === "PromiseLike";
382
+ }
383
+ unwrapPromiseLikeType(type) {
384
+ const symbolName = type.getSymbol()?.getName();
385
+ if ((symbolName === "Promise" || symbolName === "PromiseLike") &&
386
+ type.getTypeArguments().length > 0) {
387
+ return type.getTypeArguments()[0] ?? type;
388
+ }
389
+ return type;
390
+ }
391
+ getType(text) {
392
+ if (text === "string")
393
+ return "string";
394
+ if (text === "number")
395
+ return "number";
396
+ if (text === "boolean")
397
+ return "boolean";
398
+ if (text === "any")
399
+ return "object";
400
+ if (text.endsWith("[]"))
401
+ return [this.getType(text.slice(0, -2))];
402
+ if (text.length === 0)
403
+ return "undefined";
404
+ console.warn(`[Type Warning] Unrecognized type "${text}". Consider adding explicit types in your action's JSDoc comments for better type safety and documentation.`);
405
+ return text;
406
+ }
407
+ printChanges(compiler) {
408
+ const modified = compiler.modifiedFiles
409
+ ? Array.from(compiler.modifiedFiles)
410
+ : [];
411
+ const removed = compiler.removedFiles
412
+ ? Array.from(compiler.removedFiles)
413
+ : [];
414
+ const allChanges = [...modified, ...removed];
415
+ if (allChanges.length > 0) {
416
+ console.log(`[Server] ✏️ Detected file changes:\n${allChanges
417
+ .map((file) => ` - ${file}`)
418
+ .join("\n")}`);
419
+ }
420
+ }
421
+ }
422
+ export async function makeMFServerConfig(mode) {
423
+ const projectDirName = process.cwd();
424
+ const pulseConfig = await loadPulseConfig();
425
+ return {
426
+ mode: mode,
427
+ name: "server",
428
+ entry: {},
429
+ target: "async-node",
430
+ output: {
431
+ publicPath: "auto",
432
+ path: path.resolve(projectDirName, "dist/server"),
433
+ },
434
+ resolve: {
435
+ extensions: [".ts", ".js"],
436
+ },
437
+ plugins: [new MFServerPlugin(pulseConfig)],
438
+ module: {
439
+ rules: [
440
+ {
441
+ test: /\.tsx?$/,
442
+ use: {
443
+ loader: "ts-loader",
444
+ options: {
445
+ configFile: "node_modules/.pulse/tsconfig.server.json",
446
+ },
447
+ },
448
+ exclude: [/node_modules/, /dist/],
449
+ },
450
+ ],
451
+ },
452
+ stats: {
453
+ all: false,
454
+ errors: true,
455
+ warnings: true,
456
+ logging: "warn",
457
+ colors: true,
458
+ },
459
+ infrastructureLogging: {
460
+ level: "warn",
461
+ },
462
+ };
463
+ }
@@ -0,0 +1,3 @@
1
+ import { Configuration as WebpackConfig } from "webpack";
2
+ import { Configuration as DevServerConfig } from "webpack-dev-server";
3
+ export declare function makePreviewClientConfig(mode: "development" | "production"): Promise<WebpackConfig & DevServerConfig>;