@pulse-editor/cli 0.1.5 → 0.1.7

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.
@@ -29,6 +29,8 @@ if (isDev || isPreview) {
29
29
  console.log("✅ LiveReload connected");
30
30
  });
31
31
  }
32
+ const skillActions = pulseConfig?.actions || [];
33
+ const skillActionNames = skillActions.map((a) => a.name);
32
34
  const app = express();
33
35
  app.use(cors());
34
36
  // Inject the client-side livereload script into HTML responses
@@ -108,8 +110,6 @@ if (isPreview) {
108
110
  });
109
111
  app.use(express.static("dist/client"));
110
112
  // Expose skill actions as REST API endpoints in dev and preview modes
111
- const skillActions = pulseConfig?.actions || [];
112
- const skillActionNames = skillActions.map((a) => a.name);
113
113
  app.post("/skill/:actionName", async (req, res) => {
114
114
  const { actionName } = req.params;
115
115
  if (skillActionNames.length > 0 && !skillActionNames.includes(actionName)) {
@@ -137,6 +137,29 @@ if (isPreview) {
137
137
  else if (isDev) {
138
138
  /* Dev mode */
139
139
  app.use(`/${pulseConfig.id}/${pulseConfig.version}`, express.static("dist"));
140
+ // Expose skill actions as REST API endpoints in dev and preview modes
141
+ app.post(`/skill/:actionName`, async (req, res) => {
142
+ const { actionName } = req.params;
143
+ if (skillActionNames.length > 0 && !skillActionNames.includes(actionName)) {
144
+ res
145
+ .status(404)
146
+ .json({ error: `Skill action "${actionName}" not found.` });
147
+ return;
148
+ }
149
+ const dir = path.resolve("node_modules/@pulse-editor/cli/dist/lib/server/preview/backend/load-remote.cjs");
150
+ const fileUrl = pathToFileURL(dir).href;
151
+ const { loadFunc } = await import(fileUrl);
152
+ try {
153
+ const action = await loadFunc(`skill/${actionName}`, pulseConfig.id, "http://localhost:3030", pulseConfig.version);
154
+ const result = await action(req.body);
155
+ res.json(result);
156
+ }
157
+ catch (err) {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ console.error(`❌ Error running skill action "${actionName}": ${message}`);
160
+ res.status(500).json({ error: message });
161
+ }
162
+ });
140
163
  app.listen(3030, "0.0.0.0");
141
164
  }
142
165
  else {
@@ -1,6 +1,47 @@
1
1
  const {createInstance} = require('@module-federation/runtime');
2
2
 
3
+ const FETCH_PATCH_STATE_KEY = '__pulseFetchPatchState__';
4
+
5
+ function isAbsoluteUrl(url) {
6
+ return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(url);
7
+ }
8
+
9
+ function ensureFetchPatched(origin) {
10
+ if (typeof globalThis.fetch !== 'function') {
11
+ return;
12
+ }
13
+
14
+ if (!globalThis[FETCH_PATCH_STATE_KEY]) {
15
+ const originalFetch = globalThis.fetch.bind(globalThis);
16
+ const state = {
17
+ baseOrigin: origin,
18
+ originalFetch,
19
+ };
20
+
21
+ globalThis.fetch = (input, init) => {
22
+ const currentBaseOrigin = state.baseOrigin;
23
+
24
+ if (
25
+ typeof input === 'string' &&
26
+ currentBaseOrigin &&
27
+ !isAbsoluteUrl(input)
28
+ ) {
29
+ return state.originalFetch(new URL(input, currentBaseOrigin).toString(), init);
30
+ }
31
+
32
+ return state.originalFetch(input, init);
33
+ };
34
+
35
+ globalThis[FETCH_PATCH_STATE_KEY] = state;
36
+ return;
37
+ }
38
+
39
+ globalThis[FETCH_PATCH_STATE_KEY].baseOrigin = origin;
40
+ }
41
+
3
42
  async function importRemoteModule(func, appId, origin, version) {
43
+ ensureFetchPatched(origin);
44
+
4
45
  const instance = createInstance({
5
46
  name: 'server_function_runner',
6
47
  remotes: [
@@ -46,7 +46,42 @@ class ErrorBoundary extends React.Component {
46
46
  }
47
47
  }
48
48
  const root = ReactDOM.createRoot(document.getElementById('root'));
49
+ const PREVIEW_MODE_ERRORS = [
50
+ "Current window's ID is not defined.",
51
+ ];
52
+ function isPreviewModeError(error) {
53
+ const message = error instanceof Error ? error.message : String(error);
54
+ return PREVIEW_MODE_ERRORS.some((msg) => message.includes(msg));
55
+ }
56
+ function showPreviewModeWarning() {
57
+ const existing = document.getElementById('__pulse_preview_warning__');
58
+ if (existing)
59
+ return;
60
+ const banner = document.createElement('div');
61
+ banner.id = '__pulse_preview_warning__';
62
+ Object.assign(banner.style, {
63
+ position: 'fixed',
64
+ bottom: '1rem',
65
+ right: '1rem',
66
+ background: '#2a2000',
67
+ border: '1px solid #665500',
68
+ borderRadius: '8px',
69
+ padding: '0.75rem 1rem',
70
+ fontFamily: 'monospace',
71
+ fontSize: '0.8rem',
72
+ color: '#ffcc00',
73
+ maxWidth: '320px',
74
+ zIndex: '9999',
75
+ lineHeight: '1.4',
76
+ });
77
+ banner.textContent = '⚠ Preview mode: some Pulse Editor platform APIs are not available and will not work.';
78
+ document.body.appendChild(banner);
79
+ }
49
80
  function showError(error) {
81
+ if (isPreviewModeError(error)) {
82
+ showPreviewModeWarning();
83
+ return;
84
+ }
50
85
  root.render(_jsx(ErrorPage, { error: error }));
51
86
  }
52
87
  // Fallback: catch errors that slip past the React error boundary
@@ -0,0 +1 @@
1
+ export declare function readConfigFile(): Promise<any>;
@@ -0,0 +1,17 @@
1
+ import fs from 'fs/promises';
2
+ export async function readConfigFile() {
3
+ // Read pulse.config.json from dist/client
4
+ // Wait until dist/pulse.config.json exists
5
+ while (true) {
6
+ try {
7
+ await fs.access('dist/pulse.config.json');
8
+ break;
9
+ }
10
+ catch (err) {
11
+ // Wait for 100ms before trying again
12
+ await new Promise(resolve => setTimeout(resolve, 100));
13
+ }
14
+ }
15
+ const data = await fs.readFile('dist/pulse.config.json', 'utf-8');
16
+ return JSON.parse(data);
17
+ }
@@ -51,8 +51,6 @@ class MFClientPlugin {
51
51
  else {
52
52
  console.log("[client] ✅ Reload finished.");
53
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
54
  });
57
55
  }
58
56
  else {
@@ -63,8 +61,6 @@ class MFClientPlugin {
63
61
  }
64
62
  else {
65
63
  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
64
  }
69
65
  });
70
66
  }
@@ -106,6 +102,13 @@ export async function makeMFClientConfig(mode) {
106
102
  const pulseConfig = await loadPulseConfig();
107
103
  const mainComponent = "./src/main.tsx";
108
104
  const actions = discoverAppSkillActions();
105
+ console.log(`\n🧩 App skill actions:
106
+ ${Object.entries(actions)
107
+ .map(([name, file]) => {
108
+ return ` - ${name.slice(2)} (from ${file})`;
109
+ })
110
+ .join("\n")}
111
+ `);
109
112
  return {
110
113
  mode: mode,
111
114
  name: "client",
@@ -46,13 +46,15 @@ class MFServerPlugin {
46
46
  : false;
47
47
  if (isActionChange) {
48
48
  console.log(`[Server] Detected changes in actions. Recompiling...`);
49
- compileAppActionSkills(this.pulseConfig);
49
+ this.pulseConfig = compileAppActionSkills(this.pulseConfig);
50
+ this.saveConfig();
50
51
  }
51
52
  }
52
53
  else {
53
54
  console.log(`[Server] 🔄 Building app...`);
54
55
  await this.compileServerFunctions(compiler);
55
- compileAppActionSkills(this.pulseConfig);
56
+ this.pulseConfig = compileAppActionSkills(this.pulseConfig);
57
+ this.saveConfig();
56
58
  console.log(`[Server] ✅ Successfully built server.`);
57
59
  const funcs = discoverServerFunctions();
58
60
  console.log(`\n🛜 Server functions:
@@ -83,7 +85,8 @@ ${Object.entries(funcs)
83
85
  else {
84
86
  try {
85
87
  await this.compileServerFunctions(compiler);
86
- compileAppActionSkills(this.pulseConfig);
88
+ this.pulseConfig = compileAppActionSkills(this.pulseConfig);
89
+ this.saveConfig();
87
90
  }
88
91
  catch (err) {
89
92
  console.error(`[Server] ❌ Error during compilation:`, err);
@@ -104,6 +107,10 @@ ${Object.entries(funcs)
104
107
  console.log("Continuing...");
105
108
  }
106
109
  }
110
+ saveConfig() {
111
+ const filePath = path.resolve(this.projectDirName, "dist/pulse.config.json");
112
+ fs.writeFileSync(filePath, JSON.stringify(this.pulseConfig, null, 2));
113
+ }
107
114
  /**
108
115
  * Programmatically call webpack to compile server functions
109
116
  * whenever there are changes in the src/server-function directory.
@@ -1,4 +1,3 @@
1
1
  import { Configuration as WebpackConfig } from "webpack";
2
2
  import { Configuration as DevServerConfig } from "webpack-dev-server";
3
- export declare function makeMFPreviewConfig(): Promise<WebpackConfig>;
4
3
  export declare function makePreviewClientConfig(mode: "development" | "production"): Promise<WebpackConfig & DevServerConfig>;
@@ -1,133 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import mfNode from "@module-federation/node";
3
2
  import CopyWebpackPlugin from "copy-webpack-plugin";
4
3
  import fs from "fs";
5
4
  import HtmlWebpackPlugin from "html-webpack-plugin";
6
5
  import MiniCssExtractPlugin from "mini-css-extract-plugin";
7
6
  import path from "path";
8
- import wp from "webpack";
9
- import { compileAppActionSkills, discoverAppSkillActions, getLocalNetworkIP, loadPulseConfig, } from "./utils.js";
10
- const { NodeFederationPlugin } = mfNode;
11
- const { webpack } = wp;
12
- class MFPreviewPlugin {
13
- projectDirName;
14
- pulseConfig;
15
- constructor(pulseConfig) {
16
- this.projectDirName = process.cwd();
17
- this.pulseConfig = pulseConfig;
18
- }
19
- apply(compiler) {
20
- let isFirstRun = true;
21
- compiler.hooks.environment.tap("WatchSkillPlugin", () => {
22
- compiler.hooks.thisCompilation.tap("WatchActions", (compilation) => {
23
- compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/skill"));
24
- });
25
- });
26
- compiler.hooks.watchRun.tap("MFPreviewPlugin", async () => {
27
- if (!isFirstRun) {
28
- const isActionChange = compiler.modifiedFiles
29
- ? Array.from(compiler.modifiedFiles).some((file) => file.includes("src/skill"))
30
- : false;
31
- if (isActionChange) {
32
- console.log("[preview-server] Detected changes in actions. Recompiling...");
33
- await this.compileActions(compiler);
34
- }
35
- }
36
- else {
37
- console.log("[preview-server] 🔄 Building...");
38
- await this.compileActions(compiler);
39
- console.log("[preview-server] ✅ Successfully built preview server.");
40
- }
41
- });
42
- compiler.hooks.done.tap("MFPreviewPlugin", () => {
43
- if (isFirstRun) {
44
- isFirstRun = false;
45
- }
46
- else {
47
- console.log("[preview-server] ✅ Reload finished.");
48
- }
49
- fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
50
- });
51
- }
52
- makeNodeFederationPlugin() {
53
- const actions = discoverAppSkillActions();
54
- return new NodeFederationPlugin({
55
- name: this.pulseConfig.id + "_server",
56
- remoteType: "script",
57
- useRuntimePlugin: true,
58
- library: { type: "commonjs-module" },
59
- filename: "remoteEntry.js",
60
- exposes: { ...actions },
61
- }, {});
62
- }
63
- async compileActions(compiler) {
64
- compileAppActionSkills(this.pulseConfig);
65
- const options = {
66
- ...compiler.options,
67
- watch: false,
68
- plugins: [this.makeNodeFederationPlugin()],
69
- };
70
- const newCompiler = webpack(options);
71
- return new Promise((resolve, reject) => {
72
- newCompiler?.run((err, stats) => {
73
- if (err) {
74
- console.error("[preview-server] ❌ Error during recompilation:", err);
75
- reject(err);
76
- }
77
- else if (stats?.hasErrors()) {
78
- console.error("[preview-server] ❌ Compilation errors:", stats.toJson().errors);
79
- reject(new Error("Compilation errors"));
80
- }
81
- else {
82
- console.log("[preview-server] ✅ Compiled actions successfully.");
83
- resolve();
84
- }
85
- });
86
- });
87
- }
88
- }
89
- export async function makeMFPreviewConfig() {
90
- const projectDirName = process.cwd();
91
- const pulseConfig = await loadPulseConfig();
92
- return {
93
- mode: "development",
94
- name: "preview-server",
95
- entry: {},
96
- target: "async-node",
97
- output: {
98
- publicPath: "auto",
99
- path: path.resolve(projectDirName, "dist/server"),
100
- },
101
- resolve: {
102
- extensions: [".ts", ".js"],
103
- },
104
- plugins: [new MFPreviewPlugin(pulseConfig)],
105
- module: {
106
- rules: [
107
- {
108
- test: /\.tsx?$/,
109
- use: {
110
- loader: "ts-loader",
111
- options: {
112
- configFile: "node_modules/.pulse/tsconfig.server.json",
113
- },
114
- },
115
- exclude: [/node_modules/, /dist/],
116
- },
117
- ],
118
- },
119
- stats: {
120
- all: false,
121
- errors: true,
122
- warnings: true,
123
- logging: "warn",
124
- colors: true,
125
- },
126
- infrastructureLogging: {
127
- level: "warn",
128
- },
129
- };
130
- }
7
+ import { getLocalNetworkIP, loadPulseConfig } from "./utils.js";
131
8
  class PreviewClientPlugin {
132
9
  projectDirName;
133
10
  pulseConfig;
@@ -151,13 +28,13 @@ class PreviewClientPlugin {
151
28
  // After build finishes
152
29
  compiler.hooks.done.tap("ReloadMessagePlugin", () => {
153
30
  if (isFirstRun) {
154
- const previewStartupMessage = `
155
- 🎉 Your Pulse extension preview \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
156
-
157
- ⚡️ Local: http://localhost:3030
158
- ⚡️ Network: http://${this.origin}:3030
159
-
160
- ✨ Try it out in your browser and let the magic happen! 🚀
31
+ const previewStartupMessage = `
32
+ 🎉 Your Pulse extension preview \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
33
+
34
+ ⚡️ Local: http://localhost:3030
35
+ ⚡️ Network: http://${this.origin}:3030
36
+
37
+ ✨ Try it out in your browser and let the magic happen! 🚀
161
38
  `;
162
39
  console.log("[client-preview] ✅ Successfully built preview.");
163
40
  const skillActions = this.pulseConfig?.actions || [];
@@ -173,6 +50,8 @@ class PreviewClientPlugin {
173
50
  else {
174
51
  console.log("[client-preview] ✅ Reload finished");
175
52
  }
53
+ // Write pulse config to dist
54
+ fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
176
55
  });
177
56
  }
178
57
  }
@@ -8,7 +8,7 @@ export declare function discoverServerFunctions(): {
8
8
  };
9
9
  export declare function discoverAppSkillActions(): {
10
10
  [x: string]: string;
11
- } | null;
11
+ };
12
12
  export declare function parseTypeDefs(jsDocs: JSDoc[]): Record<string, Record<string, {
13
13
  type: string;
14
14
  description: string;
@@ -17,5 +17,5 @@ export declare function normalizeJSDocPropertyName(name: string | undefined): st
17
17
  export declare function isPromiseLikeType(type: import("ts-morph").Type): boolean;
18
18
  export declare function unwrapPromiseLikeType(type: import("ts-morph").Type): import("ts-morph").Type<import("ts-morph").ts.Type>;
19
19
  export declare function getActionType(text: string): TypedVariableType;
20
- export declare function compileAppActionSkills(pulseConfig: any): void;
20
+ export declare function compileAppActionSkills(pulseConfig: any): any;
21
21
  export declare function generateTempTsConfig(): void;
@@ -137,6 +137,7 @@ export function discoverAppSkillActions() {
137
137
  ["./skill/" + actionName]: "./" + file,
138
138
  };
139
139
  })
140
+ .filter((entry) => entry !== null)
140
141
  .reduce((acc, curr) => {
141
142
  return { ...acc, ...curr };
142
143
  }, {});
@@ -225,6 +226,8 @@ export function compileAppActionSkills(pulseConfig) {
225
226
  if (declaration.getKind() !== SyntaxKind.FunctionDeclaration)
226
227
  return;
227
228
  const funcDecl = declaration.asKindOrThrow(SyntaxKind.FunctionDeclaration);
229
+ // Get action name from path `src/skill/{actionName}/action.ts`
230
+ // Match `*/src/skill/{actionName}/action.ts` and extract {actionName}
228
231
  const pattern = /src\/skill\/([^\/]+)\/action\.ts$/;
229
232
  const match = file.replaceAll("\\", "/").match(pattern);
230
233
  if (!match) {
@@ -300,9 +303,11 @@ export function compileAppActionSkills(pulseConfig) {
300
303
  params[name] = variable;
301
304
  });
302
305
  }
306
+ /* Extract return type from JSDoc */
303
307
  const rawReturnType = funcDecl.getReturnType();
304
308
  const isPromiseLikeReturn = isPromiseLikeType(rawReturnType);
305
309
  const returnType = unwrapPromiseLikeType(rawReturnType);
310
+ // Check if the return type is an object
306
311
  if (!returnType.isObject()) {
307
312
  console.warn(`[Action Registration] Function ${actionName}'s return type should be an object. Skipping...`);
308
313
  return;
@@ -311,13 +316,17 @@ export function compileAppActionSkills(pulseConfig) {
311
316
  const returnProperties = returnType.getProperties();
312
317
  const outputTypeDef = typeDefs["output"] ?? {};
313
318
  const hasOutputTypeDef = !!typeDefs["output"];
314
- if (returnProperties.length > 0 && !hasOutputTypeDef && !isPromiseLikeReturn) {
319
+ if (returnProperties.length > 0 &&
320
+ !hasOutputTypeDef &&
321
+ !isPromiseLikeReturn) {
315
322
  throw new Error(`[Action Validation] Action "${actionName}" in ${file} returns properties but is missing an ` +
316
323
  `"@typedef {Output}" JSDoc block. Please document all return values with ` +
317
324
  `@typedef {Output} and @property tags.` +
318
325
  `Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
319
326
  }
320
- if (returnProperties.length > 0 && !hasOutputTypeDef && isPromiseLikeReturn) {
327
+ if (returnProperties.length > 0 &&
328
+ !hasOutputTypeDef &&
329
+ isPromiseLikeReturn) {
321
330
  console.warn(`[Action Validation] Action "${actionName}" in ${file} is missing an "@typedef {Object} output" JSDoc block. ` +
322
331
  `Falling back to TypeScript-inferred return metadata because the action returns a Promise.`);
323
332
  }
@@ -361,6 +370,7 @@ export function compileAppActionSkills(pulseConfig) {
361
370
  });
362
371
  });
363
372
  pulseConfig.actions = actions;
373
+ return pulseConfig;
364
374
  }
365
375
  // Generate tsconfig for server functions
366
376
  export function generateTempTsConfig() {
@@ -0,0 +1,2 @@
1
+ import { Action } from "@pulse-editor/shared-utils";
2
+ export declare const preRegisteredActions: Record<string, Action>;
@@ -0,0 +1,7 @@
1
+ import { AppConfig } from "@pulse-editor/shared-utils";
2
+ /**
3
+ * Pulse Editor Extension Config
4
+ *
5
+ */
6
+ declare const config: AppConfig;
7
+ export default config;
@@ -0,0 +1,2 @@
1
+ import { ChatPromptTemplate } from "@langchain/core/prompts";
2
+ export declare const codeModifierAgentPrompt: ChatPromptTemplate<any, any>;
@@ -0,0 +1,3 @@
1
+ import { MultiServerMCPClient } from "@langchain/mcp-adapters";
2
+ import { MessageStreamController } from "../streaming/message-stream-controller";
3
+ export declare function runVibeCoding(mcpClient: MultiServerMCPClient, userPrompt: string, controller: MessageStreamController): Promise<void>;
@@ -0,0 +1,3 @@
1
+ import { MultiServerMCPClient } from "@langchain/mcp-adapters";
2
+ export declare function writeFileToFS(mcpClient: MultiServerMCPClient, uri: string, content: string): Promise<string>;
3
+ export declare function callTerminal(mcpClient: MultiServerMCPClient, command: string): Promise<string>;
@@ -0,0 +1,10 @@
1
+ import { AgentTaskMessageData } from "../types";
2
+ export declare class MessageStreamController {
3
+ private controller;
4
+ private msgCounter;
5
+ private messages;
6
+ private timeCountMap;
7
+ constructor(controller: ReadableStreamDefaultController);
8
+ enqueueNew(data: AgentTaskMessageData, isFinal: boolean): number;
9
+ enqueueUpdate(data: AgentTaskMessageData, isFinal: boolean): void;
10
+ }
@@ -0,0 +1,58 @@
1
+ export type VibeDevFlowNode = {
2
+ id: string;
3
+ children: VibeDevFlowNode[];
4
+ };
5
+ export declare enum AgentTaskMessageType {
6
+ Creation = "creation",
7
+ Update = "update"
8
+ }
9
+ export declare enum AgentTaskMessageDataType {
10
+ Notification = "notification",
11
+ ToolCall = "toolCall",
12
+ ArtifactOutput = "artifactOutput"
13
+ }
14
+ /**
15
+ * Data associated with an AgentTaskItem.
16
+ * The fields included depend on the type of the task item.
17
+ *
18
+ * @property type - The type of the task item, defined by the AgentTaskItemType enum.
19
+ * @property title - (Optional) A brief title or summary of the task item.
20
+ * @property description - (Optional) A detailed description of the task item.
21
+ * @property toolName - (Optional) The name of the tool being called (if applicable).
22
+ * @property parameters - (Optional) A record of parameters associated with the tool call (if applicable).
23
+ * @property error - (Optional) An error message if the task item represents an error.
24
+ * @property result - (Optional) The result or output of the task item (if applicable).
25
+ */
26
+ export type AgentTaskMessageData = {
27
+ type?: AgentTaskMessageDataType;
28
+ title?: string;
29
+ description?: string;
30
+ toolName?: string;
31
+ parameters?: Record<string, unknown>;
32
+ error?: string;
33
+ result?: string;
34
+ };
35
+ /**
36
+ * Represents a single task item generated by the agent.
37
+ * Each task item can be of different types such as tool calls, notifications, or errors.
38
+ *
39
+ * @property type - The type of the task item, defined by the AgentTaskItemType enum.
40
+ * @property messageId - The unique identifier for the task item.
41
+ * This is an incremental number representing the n-th task item.
42
+ * @property data - The data associated with the task item, which varies based on the type.
43
+ * @property isFinal - (Optional) Indicates if the task item is final and no further updates are expected.
44
+ */
45
+ export type AgentTaskMessage = {
46
+ type: AgentTaskMessageType;
47
+ messageId: number;
48
+ data: AgentTaskMessageData;
49
+ isFinal?: boolean;
50
+ };
51
+ export type AgentTaskMessageUpdate = {
52
+ type: AgentTaskMessageType;
53
+ messageId: number;
54
+ updateType: "append";
55
+ delta: AgentTaskMessageData;
56
+ isFinal?: boolean;
57
+ timeUsedSec?: string;
58
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * An example function to echo the body of a POST request.
3
+ * This route is accessible at /server-function/echo
4
+ */
5
+ export default function generate(req: Request): Promise<Response>;
@@ -0,0 +1 @@
1
+ export default function handler(req: Request): Promise<Response>;
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "outDir": "dist",
9
+ },
10
+ "include": [
11
+ "../../../../../../src/server-function/**/*",
12
+ "../../../../../../pulse.config.ts",
13
+ "../../../../../../global.d.ts",
14
+ ],
15
+ "exclude": [
16
+ "../../../../../../node_modules",
17
+ "../../../../../../dist",
18
+ ]
19
+ }
@@ -1,12 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { makeMFClientConfig } from "./configs/mf-client.js";
3
3
  import { makeMFServerConfig } from "./configs/mf-server.js";
4
- import { makeMFPreviewConfig, makePreviewClientConfig } from "./configs/preview.js";
4
+ import { makePreviewClientConfig } from "./configs/preview.js";
5
5
  export async function createWebpackConfig(isPreview, buildTarget, mode) {
6
6
  if (isPreview) {
7
7
  const previewClientConfig = await makePreviewClientConfig("development");
8
- const mfPreviewConfig = await makeMFPreviewConfig();
9
- return [previewClientConfig, mfPreviewConfig];
8
+ const mfServerConfig = await makeMFServerConfig("development");
9
+ return [previewClientConfig, mfServerConfig];
10
10
  }
11
11
  else if (buildTarget === "server") {
12
12
  const mfServerConfig = await makeMFServerConfig(mode);
@@ -0,0 +1,2 @@
1
+ import wp from 'webpack';
2
+ export declare function createWebpackConfig(isPreview: boolean, buildTarget: 'client' | 'server' | 'both', mode: 'development' | 'production'): Promise<wp.Configuration[]>;
@@ -0,0 +1,527 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
3
+ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
4
+ import HtmlWebpackPlugin from 'html-webpack-plugin';
5
+ import { networkInterfaces } from 'os';
6
+ import path from 'path';
7
+ import { globSync } from 'glob';
8
+ import fs from 'fs';
9
+ import CopyWebpackPlugin from 'copy-webpack-plugin';
10
+ import ts from 'typescript';
11
+ import { pathToFileURL } from 'url';
12
+ import mfNode from '@module-federation/node';
13
+ const { NodeFederationPlugin } = mfNode;
14
+ import wp from 'webpack';
15
+ const { webpack } = wp;
16
+ export async function createWebpackConfig(isPreview, buildTarget, mode) {
17
+ const projectDirName = process.cwd();
18
+ async function loadPulseConfig() {
19
+ // compile to js file and import
20
+ const program = ts.createProgram({
21
+ rootNames: [path.join(projectDirName, 'pulse.config.ts')],
22
+ options: {
23
+ module: ts.ModuleKind.ESNext,
24
+ target: ts.ScriptTarget.ES2020,
25
+ outDir: path.join(projectDirName, 'node_modules/.pulse/config'),
26
+ esModuleInterop: true,
27
+ skipLibCheck: true,
28
+ forceConsistentCasingInFileNames: true,
29
+ },
30
+ });
31
+ program.emit();
32
+ // Fix imports in the generated js file for all files in node_modules/.pulse/config
33
+ globSync('node_modules/.pulse/config/**/*.js', {
34
+ cwd: projectDirName,
35
+ absolute: true,
36
+ }).forEach(jsFile => {
37
+ let content = fs.readFileSync(jsFile, 'utf-8');
38
+ content = content.replace(/(from\s+["']\.\/[^\s"']+)(["'])/g, (match, p1, p2) => {
39
+ // No change if the import already has any extension
40
+ if (p1.match(/\.(js|cjs|mjs|ts|tsx|json)$/)) {
41
+ return match; // No change needed
42
+ }
43
+ return `${p1}.js${p2}`;
44
+ });
45
+ fs.writeFileSync(jsFile, content);
46
+ });
47
+ // Copy package.json if exists
48
+ const pkgPath = path.join(projectDirName, 'package.json');
49
+ if (fs.existsSync(pkgPath)) {
50
+ const destPath = path.join(projectDirName, 'node_modules/.pulse/config/package.json');
51
+ fs.copyFileSync(pkgPath, destPath);
52
+ }
53
+ const compiledConfig = path.join(projectDirName, 'node_modules/.pulse/config/pulse.config.js');
54
+ const mod = await import(pathToFileURL(compiledConfig).href);
55
+ // delete the compiled config after importing
56
+ fs.rmSync(path.join(projectDirName, 'node_modules/.pulse/config'), {
57
+ recursive: true,
58
+ force: true,
59
+ });
60
+ return mod.default;
61
+ }
62
+ const pulseConfig = await loadPulseConfig();
63
+ function getLocalNetworkIP() {
64
+ const interfaces = networkInterfaces();
65
+ for (const iface of Object.values(interfaces)) {
66
+ if (!iface)
67
+ continue;
68
+ for (const config of iface) {
69
+ if (config.family === 'IPv4' && !config.internal) {
70
+ return config.address; // Returns the first non-internal IPv4 address
71
+ }
72
+ }
73
+ }
74
+ return 'localhost'; // Fallback
75
+ }
76
+ const origin = getLocalNetworkIP();
77
+ const previewStartupMessage = `
78
+ 🎉 Your Pulse extension preview \x1b[1m${pulseConfig.displayName}\x1b[0m is LIVE!
79
+
80
+ ⚡️ Local: http://localhost:3030
81
+ ⚡️ Network: http://${origin}:3030
82
+
83
+ ✨ Try it out in your browser and let the magic happen! 🚀
84
+ `;
85
+ const devStartupMessage = `
86
+ 🎉 Your Pulse extension \x1b[1m${pulseConfig.displayName}\x1b[0m is LIVE!
87
+
88
+ ⚡️ Local: http://localhost:3030/${pulseConfig.id}/${pulseConfig.version}/
89
+ ⚡️ Network: http://${origin}:3030/${pulseConfig.id}/${pulseConfig.version}/
90
+
91
+ ✨ Try it out in the Pulse Editor and let the magic happen! 🚀
92
+ `;
93
+ // #region Node Federation Plugin for Server Functions
94
+ function makeNodeFederationPlugin() {
95
+ function discoverServerFunctions() {
96
+ // Get all .ts files under src/server-function and read use default exports as entry points
97
+ const files = globSync('./src/server-function/**/*.ts');
98
+ const entryPoints = files
99
+ .map(file => file.replaceAll('\\', '/'))
100
+ .map(file => {
101
+ return {
102
+ ['./' +
103
+ file.replace('src/server-function/', '').replace(/\.ts$/, '')]: './' + file,
104
+ };
105
+ })
106
+ .reduce((acc, curr) => {
107
+ return { ...acc, ...curr };
108
+ }, {});
109
+ return entryPoints;
110
+ }
111
+ const funcs = discoverServerFunctions();
112
+ console.log(`Discovered server functions:
113
+ ${Object.entries(funcs)
114
+ .map(([name, file]) => {
115
+ return ` - ${name.slice(2)} (from ${file})`;
116
+ })
117
+ .join('\n')}
118
+ `);
119
+ return new NodeFederationPlugin({
120
+ name: pulseConfig.id + '_server',
121
+ remoteType: 'script',
122
+ useRuntimePlugin: true,
123
+ library: { type: 'commonjs-module' },
124
+ filename: 'remoteEntry.js',
125
+ exposes: {
126
+ ...funcs,
127
+ },
128
+ }, {});
129
+ }
130
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
+ function compileServerFunctions(compiler) {
132
+ // Remove existing entry points
133
+ try {
134
+ fs.rmSync('dist/server', { recursive: true, force: true });
135
+ }
136
+ catch (e) {
137
+ console.error('Error removing dist/server:', e);
138
+ console.log('Continuing...');
139
+ }
140
+ // Generate tsconfig for server functions
141
+ function generateTempTsConfig() {
142
+ const tempTsConfigPath = path.join(process.cwd(), 'node_modules/.pulse/tsconfig.server.json');
143
+ const tsConfig = {
144
+ compilerOptions: {
145
+ target: 'ES2020',
146
+ module: 'esnext',
147
+ moduleResolution: 'bundler',
148
+ strict: true,
149
+ declaration: true,
150
+ outDir: path.join(process.cwd(), 'dist'),
151
+ },
152
+ include: [
153
+ path.join(process.cwd(), 'src/server-function/**/*'),
154
+ path.join(process.cwd(), 'pulse.config.ts'),
155
+ path.join(process.cwd(), 'global.d.ts'),
156
+ ],
157
+ exclude: [
158
+ path.join(process.cwd(), 'node_modules'),
159
+ path.join(process.cwd(), 'dist'),
160
+ ],
161
+ };
162
+ fs.writeFileSync(tempTsConfigPath, JSON.stringify(tsConfig, null, 2));
163
+ }
164
+ generateTempTsConfig();
165
+ // Run a new webpack compilation to pick up new server functions
166
+ const options = {
167
+ ...compiler.options,
168
+ watch: false,
169
+ plugins: [
170
+ // Add a new NodeFederationPlugin with updated entry points
171
+ makeNodeFederationPlugin(),
172
+ ],
173
+ };
174
+ const newCompiler = webpack(options);
175
+ // Run the new compiler
176
+ newCompiler?.run((err, stats) => {
177
+ if (err) {
178
+ console.error(`[Server] ❌ Error during recompilation:`, err);
179
+ }
180
+ else if (stats?.hasErrors()) {
181
+ console.error(`[Server] ❌ Compilation errors:`, stats.toJson().errors);
182
+ }
183
+ else {
184
+ console.log(`[Server] ✅ Compiled server functions successfully.`);
185
+ }
186
+ });
187
+ }
188
+ // #endregion
189
+ // #region Source file parser for Pulse Config plugin
190
+ class PulseConfigPlugin {
191
+ requireFS = false;
192
+ apply(compiler) {
193
+ compiler.hooks.beforeCompile.tap('PulseConfigPlugin', () => {
194
+ this.requireFS = false;
195
+ globSync(['src/**/*.tsx', 'src/**/*.ts']).forEach(file => {
196
+ const source = fs.readFileSync(file, 'utf8');
197
+ this.scanSource(source);
198
+ });
199
+ // Persist result
200
+ pulseConfig.requireWorkspace = this.requireFS;
201
+ });
202
+ }
203
+ isWorkspaceHook(node) {
204
+ return (ts.isCallExpression(node) &&
205
+ ts.isIdentifier(node.expression) &&
206
+ [
207
+ 'useFileSystem',
208
+ 'useFile',
209
+ 'useReceiveFile',
210
+ 'useTerminal',
211
+ 'useWorkspaceInfo',
212
+ ].includes(node.expression.text));
213
+ }
214
+ scanSource(sourceText) {
215
+ const sourceFile = ts.createSourceFile('temp.tsx', sourceText, ts.ScriptTarget.Latest, true);
216
+ const visit = (node) => {
217
+ // Detect: useFileSystem(...)
218
+ if (this.isWorkspaceHook(node)) {
219
+ this.requireFS = true;
220
+ }
221
+ ts.forEachChild(node, visit);
222
+ };
223
+ visit(sourceFile);
224
+ }
225
+ }
226
+ // #endregion
227
+ // #region Webpack Configs
228
+ const previewClientConfig = {
229
+ mode: mode,
230
+ entry: {
231
+ main: './node_modules/.pulse/server/preview/frontend/index.js',
232
+ },
233
+ output: {
234
+ path: path.resolve(projectDirName, 'dist/client'),
235
+ },
236
+ resolve: {
237
+ extensions: ['.ts', '.tsx', '.js'],
238
+ },
239
+ plugins: [
240
+ new PulseConfigPlugin(),
241
+ new HtmlWebpackPlugin({
242
+ template: './node_modules/.pulse/server/preview/frontend/index.html',
243
+ }),
244
+ new MiniCssExtractPlugin({
245
+ filename: 'globals.css',
246
+ }),
247
+ new CopyWebpackPlugin({
248
+ patterns: [{ from: 'src/assets', to: 'assets' }],
249
+ }),
250
+ {
251
+ apply: compiler => {
252
+ let isFirstRun = true;
253
+ // Before build starts
254
+ compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
255
+ if (!isFirstRun) {
256
+ console.log('[client-preview] 🔄 Reloading app...');
257
+ }
258
+ else {
259
+ console.log('[client-preview] 🔄 Building app...');
260
+ }
261
+ });
262
+ // After build finishes
263
+ compiler.hooks.done.tap('ReloadMessagePlugin', () => {
264
+ if (isFirstRun) {
265
+ console.log('[client-preview] ✅ Successfully built preview.');
266
+ console.log(previewStartupMessage);
267
+ isFirstRun = false;
268
+ }
269
+ else {
270
+ console.log('[client-preview] ✅ Reload finished');
271
+ }
272
+ // Write pulse config to dist
273
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
274
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/server/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
275
+ });
276
+ },
277
+ },
278
+ ],
279
+ watchOptions: {
280
+ ignored: /src\/server-function/,
281
+ },
282
+ module: {
283
+ rules: [
284
+ {
285
+ test: /\.tsx?$/,
286
+ use: 'ts-loader',
287
+ exclude: [/node_modules/, /dist/],
288
+ },
289
+ {
290
+ test: /\.css$/i,
291
+ use: [
292
+ MiniCssExtractPlugin.loader,
293
+ 'css-loader',
294
+ {
295
+ loader: 'postcss-loader',
296
+ },
297
+ ],
298
+ },
299
+ ],
300
+ },
301
+ stats: {
302
+ all: false,
303
+ errors: true,
304
+ warnings: true,
305
+ logging: 'warn',
306
+ colors: true,
307
+ },
308
+ infrastructureLogging: {
309
+ level: 'warn',
310
+ },
311
+ };
312
+ const mfClientConfig = {
313
+ mode: mode,
314
+ name: 'client',
315
+ entry: './src/main.tsx',
316
+ output: {
317
+ publicPath: 'auto',
318
+ path: path.resolve(projectDirName, 'dist/client'),
319
+ },
320
+ resolve: {
321
+ extensions: ['.ts', '.tsx', '.js'],
322
+ },
323
+ plugins: [
324
+ new PulseConfigPlugin(),
325
+ new MiniCssExtractPlugin({
326
+ filename: 'globals.css',
327
+ }),
328
+ // Copy assets to dist
329
+ new CopyWebpackPlugin({
330
+ patterns: [{ from: 'src/assets', to: 'assets' }],
331
+ }),
332
+ new ModuleFederationPlugin({
333
+ // Do not use hyphen character '-' in the name
334
+ name: pulseConfig.id,
335
+ filename: 'remoteEntry.js',
336
+ exposes: {
337
+ './main': './src/main.tsx',
338
+ },
339
+ shared: {
340
+ react: {
341
+ requiredVersion: '19.2.0',
342
+ import: 'react', // the "react" package will be used a provided and fallback module
343
+ shareKey: 'react', // under this name the shared module will be placed in the share scope
344
+ shareScope: 'default', // share scope with this name will be used
345
+ singleton: true, // only a single version of the shared module is allowed
346
+ },
347
+ 'react-dom': {
348
+ requiredVersion: '19.2.0',
349
+ singleton: true, // only a single version of the shared module is allowed
350
+ },
351
+ },
352
+ }),
353
+ {
354
+ apply: compiler => {
355
+ if (compiler.options.mode === 'development') {
356
+ let isFirstRun = true;
357
+ // Before build starts
358
+ compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
359
+ if (!isFirstRun) {
360
+ console.log('[client] 🔄 reloading app...');
361
+ }
362
+ else {
363
+ console.log('[client] 🔄 building app...');
364
+ }
365
+ });
366
+ // Log file updates
367
+ compiler.hooks.invalid.tap('LogFileUpdates', (file, changeTime) => {
368
+ console.log(`[watch] change detected in: ${file} at ${new Date(changeTime || Date.now()).toLocaleTimeString()}`);
369
+ });
370
+ // After build finishes
371
+ compiler.hooks.done.tap('ReloadMessagePlugin', () => {
372
+ if (isFirstRun) {
373
+ console.log('[client] ✅ Successfully built client.');
374
+ console.log(devStartupMessage);
375
+ isFirstRun = false;
376
+ }
377
+ else {
378
+ console.log('[client] ✅ Reload finished.');
379
+ }
380
+ // Write pulse config to dist
381
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
382
+ });
383
+ }
384
+ else {
385
+ // Print build success/failed message
386
+ compiler.hooks.done.tap('BuildMessagePlugin', stats => {
387
+ if (stats.hasErrors()) {
388
+ console.log(`[client] ❌ Failed to build client.`);
389
+ }
390
+ else {
391
+ console.log(`[client] ✅ Successfully built client.`);
392
+ // Write pulse config to dist
393
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
394
+ }
395
+ });
396
+ }
397
+ },
398
+ },
399
+ ],
400
+ module: {
401
+ rules: [
402
+ {
403
+ test: /\.tsx?$/,
404
+ use: 'ts-loader',
405
+ exclude: [/node_modules/, /dist/],
406
+ },
407
+ {
408
+ test: /\.css$/i,
409
+ use: [
410
+ MiniCssExtractPlugin.loader,
411
+ 'css-loader',
412
+ {
413
+ loader: 'postcss-loader',
414
+ },
415
+ ],
416
+ exclude: [/dist/],
417
+ },
418
+ ],
419
+ },
420
+ stats: {
421
+ all: false,
422
+ errors: true,
423
+ warnings: true,
424
+ logging: 'warn',
425
+ colors: true,
426
+ assets: false,
427
+ },
428
+ infrastructureLogging: {
429
+ level: 'warn',
430
+ },
431
+ };
432
+ const mfServerConfig = {
433
+ mode: mode,
434
+ name: 'server',
435
+ entry: {},
436
+ target: 'async-node',
437
+ output: {
438
+ publicPath: 'auto',
439
+ path: path.resolve(projectDirName, 'dist/server'),
440
+ },
441
+ resolve: {
442
+ extensions: ['.ts', '.js'],
443
+ },
444
+ plugins: [
445
+ {
446
+ apply: compiler => {
447
+ if (compiler.options.mode === 'development') {
448
+ let isFirstRun = true;
449
+ // Before build starts
450
+ compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
451
+ if (!isFirstRun) {
452
+ console.log(`[Server] 🔄 Reloading app...`);
453
+ }
454
+ else {
455
+ console.log(`[Server] 🔄 Building app...`);
456
+ }
457
+ compileServerFunctions(compiler);
458
+ });
459
+ // After build finishes
460
+ compiler.hooks.done.tap('ReloadMessagePlugin', () => {
461
+ if (isFirstRun) {
462
+ console.log(`[Server] ✅ Successfully built server.`);
463
+ isFirstRun = false;
464
+ }
465
+ else {
466
+ console.log(`[Server] ✅ Reload finished.`);
467
+ }
468
+ });
469
+ // Watch for changes in the server-function directory to trigger rebuilds
470
+ compiler.hooks.thisCompilation.tap('WatchServerFunctions', compilation => {
471
+ compilation.contextDependencies.add(path.resolve(projectDirName, 'src/server-function'));
472
+ });
473
+ }
474
+ else {
475
+ // Print build success/failed message
476
+ compiler.hooks.done.tap('BuildMessagePlugin', stats => {
477
+ if (stats.hasErrors()) {
478
+ console.log(`[Server] ❌ Failed to build server.`);
479
+ }
480
+ else {
481
+ compileServerFunctions(compiler);
482
+ console.log(`[Server] ✅ Successfully built server.`);
483
+ }
484
+ });
485
+ }
486
+ },
487
+ },
488
+ ],
489
+ module: {
490
+ rules: [
491
+ {
492
+ test: /\.tsx?$/,
493
+ use: {
494
+ loader: 'ts-loader',
495
+ options: {
496
+ configFile: 'node_modules/.pulse/tsconfig.server.json',
497
+ },
498
+ },
499
+ exclude: [/node_modules/, /dist/],
500
+ },
501
+ ],
502
+ },
503
+ stats: {
504
+ all: false,
505
+ errors: true,
506
+ warnings: true,
507
+ logging: 'warn',
508
+ colors: true,
509
+ },
510
+ infrastructureLogging: {
511
+ level: 'warn',
512
+ },
513
+ };
514
+ // #endregion
515
+ if (isPreview) {
516
+ return [previewClientConfig, mfServerConfig];
517
+ }
518
+ else if (buildTarget === 'server') {
519
+ return [mfServerConfig];
520
+ }
521
+ else if (buildTarget === 'client') {
522
+ return [mfClientConfig];
523
+ }
524
+ else {
525
+ return [mfClientConfig, mfServerConfig];
526
+ }
527
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-editor/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "pulse": "dist/cli.js"
@@ -13,7 +13,8 @@
13
13
  "build": "tsx build.ts",
14
14
  "dev": "tsx watch --include \"./src/**/*\" build.ts",
15
15
  "test": "prettier --check . && xo && ava",
16
- "link": "npm link"
16
+ "link": "npm link",
17
+ "pack": "npm run build && npm pack --pack-destination dist"
17
18
  },
18
19
  "files": [
19
20
  "dist"