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

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 (57) hide show
  1. package/dist/app.js +3 -1
  2. package/dist/components/commands/build.js +18 -33
  3. package/dist/components/commands/create.js +38 -12
  4. package/dist/components/commands/dev.js +35 -8
  5. package/dist/components/commands/login.d.ts +2 -2
  6. package/dist/components/commands/login.js +110 -26
  7. package/dist/components/commands/preview.js +50 -11
  8. package/dist/components/commands/publish.js +23 -37
  9. package/dist/components/commands/{start copy.d.ts → skill.d.ts} +1 -1
  10. package/dist/components/commands/skill.js +230 -0
  11. package/dist/components/commands/{preview copy.d.ts → upgrade.d.ts} +1 -1
  12. package/dist/components/commands/upgrade.js +53 -0
  13. package/dist/lib/backend/publish-app.d.ts +1 -0
  14. package/dist/lib/backend/publish-app.js +26 -0
  15. package/dist/lib/backend-url.d.ts +1 -0
  16. package/dist/lib/backend-url.js +3 -0
  17. package/dist/lib/cli-flags.d.ts +18 -0
  18. package/dist/lib/cli-flags.js +18 -0
  19. package/dist/lib/manual.js +28 -0
  20. package/dist/lib/server/express.js +81 -41
  21. package/dist/lib/server/preview/backend/load-remote.cjs +28 -18
  22. package/dist/lib/server/utils.js +3 -3
  23. package/dist/lib/token.js +2 -3
  24. package/dist/lib/webpack/compile.d.ts +2 -0
  25. package/dist/lib/webpack/compile.js +30 -0
  26. package/dist/lib/webpack/configs/mf-client.d.ts +3 -0
  27. package/dist/lib/webpack/configs/mf-client.js +184 -0
  28. package/dist/lib/webpack/configs/mf-server.d.ts +2 -0
  29. package/dist/lib/webpack/configs/mf-server.js +463 -0
  30. package/dist/lib/webpack/configs/preview.d.ts +3 -0
  31. package/dist/lib/webpack/configs/preview.js +117 -0
  32. package/dist/lib/webpack/configs/utils.d.ts +10 -0
  33. package/dist/lib/webpack/configs/utils.js +172 -0
  34. package/dist/lib/webpack/dist/pregistered-actions.d.ts +2 -0
  35. package/dist/lib/webpack/dist/pulse.config.d.ts +7 -0
  36. package/dist/lib/webpack/dist/src/lib/agents/code-modifier-agent.d.ts +2 -0
  37. package/dist/lib/webpack/dist/src/lib/agents/vibe-coding-agent.d.ts +3 -0
  38. package/dist/lib/webpack/dist/src/lib/mcp/utils.d.ts +3 -0
  39. package/dist/lib/webpack/dist/src/lib/streaming/message-stream-controller.d.ts +10 -0
  40. package/dist/lib/webpack/dist/src/lib/types.d.ts +58 -0
  41. package/dist/lib/webpack/dist/src/server-function/generate-code/v1/generate.d.ts +5 -0
  42. package/dist/lib/webpack/dist/src/server-function/generate-code/v2/generate.d.ts +1 -0
  43. package/dist/lib/webpack/tsconfig.server.json +19 -0
  44. package/dist/lib/webpack/webpack-config.d.ts +1 -0
  45. package/dist/lib/webpack/webpack-config.js +24 -0
  46. package/dist/lib/webpack/webpack.config.d.ts +2 -0
  47. package/dist/lib/webpack/webpack.config.js +527 -0
  48. package/package.json +29 -20
  49. package/readme.md +7 -1
  50. package/dist/components/commands/preview copy.js +0 -14
  51. package/dist/components/commands/start copy.js +0 -14
  52. package/dist/lib/deps.d.ts +0 -1
  53. package/dist/lib/deps.js +0 -5
  54. package/dist/lib/node_module_bin.d.ts +0 -1
  55. package/dist/lib/node_module_bin.js +0 -7
  56. package/dist/lib/server/preview/backend/index.d.ts +0 -1
  57. package/dist/lib/server/preview/backend/index.js +0 -23
package/dist/app.js CHANGED
@@ -12,6 +12,8 @@ import Build from './components/commands/build.js';
12
12
  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
+ import Upgrade from './components/commands/upgrade.js';
16
+ import Skill from './components/commands/skill.js';
15
17
  export default function App({ cli }) {
16
18
  const [command, setCommand] = useState(undefined);
17
19
  if (cli.flags.stage) {
@@ -24,5 +26,5 @@ export default function App({ cli }) {
24
26
  const cmd = cli.input[0] ?? 'help';
25
27
  setCommand(cmd);
26
28
  }, [cli.input]);
27
- 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 !== 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."] })] })))] }));
28
30
  }
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
3
3
  import { execa } from 'execa';
4
4
  import fs from 'fs';
5
5
  import { cleanDist } from '../../lib/execa-utils/clean.js';
6
+ import { webpackCompile } from '../../lib/webpack/compile.js';
6
7
  export default function Build({ cli }) {
7
8
  useEffect(() => {
8
9
  async function buildProd() {
@@ -23,6 +24,19 @@ export default function Build({ cli }) {
23
24
  });
24
25
  }
25
26
  }
27
+ // Create node_modules/.pulse/server directory
28
+ if (!fs.existsSync('node_modules/.pulse/server')) {
29
+ if (process.platform === 'win32') {
30
+ await execa('mkdir node_modules\\.pulse\\server', {
31
+ shell: true,
32
+ });
33
+ }
34
+ else {
35
+ await execa('mkdir -p node_modules/.pulse/server', {
36
+ shell: true,
37
+ });
38
+ }
39
+ }
26
40
  if (process.platform === 'win32') {
27
41
  await execa('xcopy /E /I node_modules\\@pulse-editor\\cli\\dist\\lib\\server node_modules\\.pulse\\server', {
28
42
  shell: true,
@@ -35,40 +49,11 @@ export default function Build({ cli }) {
35
49
  }
36
50
  }
37
51
  await cleanDist();
38
- if (buildTarget === undefined) {
39
- // Start building
40
- await execa('npx webpack --mode production', {
41
- stdio: 'inherit',
42
- shell: true,
43
- env: {
44
- NODE_OPTIONS: '--import=tsx',
45
- },
46
- });
47
- }
48
- else if (buildTarget === 'client') {
49
- // Start building client only
50
- await execa('npx webpack --mode production', {
51
- stdio: 'inherit',
52
- shell: true,
53
- env: {
54
- NODE_OPTIONS: '--import=tsx',
55
- BUILD_TARGET: 'client',
56
- },
57
- });
58
- }
59
- else if (buildTarget === 'server') {
60
- // Start building server only
61
- await execa('npx webpack --mode production', {
62
- stdio: 'inherit',
63
- shell: true,
64
- env: {
65
- NODE_OPTIONS: '--import=tsx',
66
- BUILD_TARGET: 'server',
67
- },
68
- });
52
+ try {
53
+ await webpackCompile('production', buildTarget);
69
54
  }
70
- else {
71
- console.error(`❌ Unknown build target: ${buildTarget}`);
55
+ catch (err) {
56
+ console.error('❌ Webpack build failed', err);
72
57
  }
73
58
  }
74
59
  buildProd();
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
2
+ import { useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Text, useApp } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
5
  import { $, execa } from 'execa';
@@ -10,9 +10,14 @@ import path from 'path';
10
10
  export default function Create({ cli }) {
11
11
  const [framework, setFramework] = useState(undefined);
12
12
  const [projectName, setProjectName] = useState(undefined);
13
+ const [displayName, setDisplayName] = useState(undefined);
13
14
  const [visibility, setVisibility] = useState(undefined);
15
+ const projectPath = useMemo(() => {
16
+ return cli.flags.path ?? projectName;
17
+ }, [projectName, cli]);
14
18
  const [isShowFrameworkSelect, setIsShowFrameworkSelect] = useState(true);
15
19
  const [isShowProjectNameInput, setIsShowProjectNameInput] = useState(false);
20
+ const [isShowDisplayNameInput, setIsShowDisplayNameInput] = useState(false);
16
21
  const [isShowVisibilitySelect, setIsShowVisibilitySelect] = useState(false);
17
22
  const [createMessage, setCreateMessage] = useState();
18
23
  const [errorMessage, setErrorMessage] = useState();
@@ -59,13 +64,26 @@ export default function Create({ cli }) {
59
64
  if (projectName) {
60
65
  // Check if the project already exists
61
66
  const projectPath = path.join(process.cwd(), projectName);
62
- if (fs.existsSync(projectPath)) {
67
+ if (fs.existsSync(projectPath) &&
68
+ fs.lstatSync(projectPath).isDirectory() &&
69
+ fs.readdirSync(projectPath).length > 0) {
63
70
  setErrorMessage(_jsx(Text, { color: "redBright", children: "\u274C A project with same name already exists in current path." }));
64
71
  setTimeout(() => {
65
72
  exit();
66
73
  }, 0);
67
74
  return;
68
75
  }
76
+ const displayName = cli.flags.displayName;
77
+ if (displayName) {
78
+ setDisplayName(displayName);
79
+ }
80
+ else {
81
+ setIsShowDisplayNameInput(true);
82
+ }
83
+ }
84
+ }, [projectName, cli]);
85
+ useEffect(() => {
86
+ if (displayName) {
69
87
  const visibility = cli.flags.visibility;
70
88
  if (visibility) {
71
89
  setVisibility(visibility);
@@ -74,50 +92,55 @@ export default function Create({ cli }) {
74
92
  setIsShowVisibilitySelect(true);
75
93
  }
76
94
  }
77
- }, [projectName, cli]);
95
+ }, [displayName, cli]);
78
96
  useEffect(() => {
79
97
  if (visibility && projectName) {
80
98
  createFromTemplate(projectName, visibility);
81
99
  }
82
100
  }, [visibility, projectName]);
83
101
  async function createFromTemplate(name, visibility) {
102
+ if (!projectPath) {
103
+ setErrorMessage(_jsx(Text, { color: "redBright", children: "\u274C Project path is not defined." }));
104
+ return;
105
+ }
84
106
  if (framework === 'react') {
85
107
  // Clone the template repository
86
108
  setCreateMessage(_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Creating a new Pulse Editor app using React template..." })] }));
87
109
  try {
88
- await $ `git clone --depth 1 https://github.com/ClayPulse/pulse-app-template.git ${name}`;
110
+ await $ `git clone --depth 1 https://github.com/ClayPulse/pulse-app-template.git ${projectPath}`;
89
111
  }
90
112
  catch (error) {
91
- setCreateMessage(_jsx(Text, { color: "redBright", children: "\u274C Failed to clone the template. Please check your internet connection and try again." }));
113
+ setCreateMessage(_jsxs(Text, { color: "redBright", children: ["\u274C Failed to clone the template. ", error.message] }));
92
114
  return;
93
115
  }
94
116
  // Modify the package.json file to update the name
95
117
  setCreateMessage(_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Initializing project..." })] }));
96
118
  /* Setup pulse.config.ts */
97
- const pulseConfigPath = path.join(process.cwd(), name, 'pulse.config.ts');
119
+ const pulseConfigPath = path.join(process.cwd(), projectPath, 'pulse.config.ts');
98
120
  let pulseConfig = fs.readFileSync(pulseConfigPath, 'utf8');
99
121
  // Modify visibility by matching the block that starts with 'visibility:',
100
122
  // and replacing the entire line with the new visibility value.
101
123
  pulseConfig = pulseConfig.replace(/visibility:\s*['"`](public|unlisted|private)['"`],?/, `visibility: '${visibility}',`);
102
124
  fs.writeFileSync(pulseConfigPath, pulseConfig);
103
125
  /* Setup packages.json */
104
- const packageJsonPath = path.join(process.cwd(), name, 'package.json');
126
+ const packageJsonPath = path.join(process.cwd(), projectPath, 'package.json');
105
127
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
106
128
  packageJson.name = name.replaceAll('-', '_');
129
+ packageJson.displayName = displayName;
107
130
  // Write the modified package.json back to the file
108
131
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
109
132
  // Remove the .git directory
110
- const gitDirPath = path.join(process.cwd(), name, '.git');
133
+ const gitDirPath = path.join(process.cwd(), projectPath, '.git');
111
134
  if (fs.existsSync(gitDirPath)) {
112
135
  fs.rmSync(gitDirPath, { recursive: true, force: true });
113
136
  }
114
137
  // Remove the .github directory
115
- const githubDirPath = path.join(process.cwd(), name, '.github');
138
+ const githubDirPath = path.join(process.cwd(), projectPath, '.github');
116
139
  if (fs.existsSync(githubDirPath)) {
117
140
  fs.rmSync(githubDirPath, { recursive: true, force: true });
118
141
  }
119
142
  // Remove LICENSE file
120
- const licenseFilePath = path.join(process.cwd(), name, 'LICENSE');
143
+ const licenseFilePath = path.join(process.cwd(), projectPath, 'LICENSE');
121
144
  if (fs.existsSync(licenseFilePath)) {
122
145
  fs.rmSync(licenseFilePath, { force: true });
123
146
  }
@@ -125,7 +148,7 @@ export default function Create({ cli }) {
125
148
  // Run `npm i`
126
149
  try {
127
150
  await execa(`npm install`, {
128
- cwd: path.join(process.cwd(), name),
151
+ cwd: path.join(process.cwd(), projectPath),
129
152
  shell: true,
130
153
  });
131
154
  }
@@ -136,7 +159,7 @@ export default function Create({ cli }) {
136
159
  setCreateMessage(_jsx(Text, { children: "\uD83D\uDE80 Pulse Editor React app project created successfully!" }));
137
160
  }
138
161
  }
139
- return (_jsxs(_Fragment, { children: [isShowFrameworkSelect && (_jsx(FrameworkSelect, { cli: cli, frameworkItems: frameworkItems, framework: framework, setFramework: setFramework })), isShowProjectNameInput && (_jsx(ProjectNameInput, { projectName: projectName, setProjectName: setProjectName })), isShowVisibilitySelect && (_jsx(VisibilitySelect, { visibility: visibility, setVisibility: setVisibility })), visibility !== undefined && (_jsxs(_Fragment, { children: [framework === 'react' && _jsx(_Fragment, { children: createMessage }), framework !== 'react' && (_jsx(Text, { children: "\uD83D\uDEA7 Currently not available. We'd like to invite you to work on these frameworks if you are interested in! Check out our tutorial to integrate your favorite web framework with Pulse Editor using Module Federation." }))] })), _jsx(Text, { children: errorMessage })] }));
162
+ return (_jsxs(_Fragment, { children: [isShowFrameworkSelect && (_jsx(FrameworkSelect, { cli: cli, frameworkItems: frameworkItems, framework: framework, setFramework: setFramework })), isShowProjectNameInput && (_jsx(ProjectNameInput, { projectName: projectName, setProjectName: setProjectName })), isShowDisplayNameInput && (_jsx(DisplayNameInput, { projectName: projectName ?? '', displayName: displayName, setDisplayName: setDisplayName })), isShowVisibilitySelect && (_jsx(VisibilitySelect, { visibility: visibility, setVisibility: setVisibility })), visibility !== undefined && (_jsxs(_Fragment, { children: [framework === 'react' && _jsx(_Fragment, { children: createMessage }), framework !== 'react' && (_jsx(Text, { children: "\uD83D\uDEA7 Currently not available. We'd like to invite you to work on these frameworks if you are interested in! Check out our tutorial to integrate your favorite web framework with Pulse Editor using Module Federation." }))] })), _jsx(Text, { children: errorMessage })] }));
140
163
  }
141
164
  function FrameworkSelect({ cli, frameworkItems, framework, setFramework, }) {
142
165
  return (_jsx(_Fragment, { children: !cli.flags.framework && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "\uD83D\uDEA9Create a new Pulse Editor app using your favorite web framework!" }), _jsx(SelectInput, { items: frameworkItems, onSelect: item => {
@@ -146,6 +169,9 @@ function FrameworkSelect({ cli, frameworkItems, framework, setFramework, }) {
146
169
  function ProjectNameInput({ projectName, setProjectName, }) {
147
170
  return (_jsx(_Fragment, { children: _jsxs(Box, { children: [_jsx(Text, { children: "Enter your project name: " }), _jsx(UncontrolledTextInput, { onSubmit: value => setTimeout(() => setProjectName(value), 0), focus: projectName === undefined })] }) }));
148
171
  }
172
+ function DisplayNameInput({ projectName, displayName, setDisplayName, }) {
173
+ return (_jsx(_Fragment, { children: _jsxs(Box, { children: [_jsxs(Text, { children: ["Enter your project display name: (default: ", projectName, ")"] }), _jsx(UncontrolledTextInput, { onSubmit: value => setTimeout(() => setDisplayName(value.length > 0 ? value : projectName), 0), focus: displayName === undefined })] }) }));
174
+ }
149
175
  function VisibilitySelect({ visibility, setVisibility, }) {
150
176
  return (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Enter marketplace visibility for your project:" }), _jsx(SelectInput, { items: [
151
177
  { label: 'Public', value: 'public' },
@@ -1,10 +1,10 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Text } from 'ink';
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
2
  import { useEffect } from 'react';
4
3
  import { execa } from 'execa';
5
4
  import fs from 'fs';
6
5
  import { getDepsBinPath } from '../../lib/execa-utils/deps.js';
7
6
  import { cleanDist } from '../../lib/execa-utils/clean.js';
7
+ import { webpackCompile } from '../../lib/webpack/compile.js';
8
8
  export default function Dev({ cli }) {
9
9
  useEffect(() => {
10
10
  async function startDevServer() {
@@ -22,6 +22,19 @@ export default function Dev({ cli }) {
22
22
  });
23
23
  }
24
24
  }
25
+ // Create node_modules/.pulse/server directory
26
+ if (!fs.existsSync('node_modules/.pulse/server')) {
27
+ if (process.platform === 'win32') {
28
+ await execa('mkdir node_modules\\.pulse\\server', {
29
+ shell: true,
30
+ });
31
+ }
32
+ else {
33
+ await execa('mkdir -p node_modules/.pulse/server', {
34
+ shell: true,
35
+ });
36
+ }
37
+ }
25
38
  if (process.platform === 'win32') {
26
39
  await execa('xcopy /E /I node_modules\\@pulse-editor\\cli\\dist\\lib\\server node_modules\\.pulse\\server', {
27
40
  shell: true,
@@ -34,11 +47,13 @@ export default function Dev({ cli }) {
34
47
  }
35
48
  // Start dev server
36
49
  await cleanDist();
37
- await execa(getDepsBinPath('concurrently'), [
38
- '--prefix',
39
- 'none',
40
- '"npx webpack --mode development --watch"',
41
- '"tsx watch --clear-screen=false node_modules/@pulse-editor/cli/dist/lib/server/express.js"',
50
+ // Start webpack in dev watch mode and watch for changes
51
+ const compiler = await webpackCompile('development', undefined, true);
52
+ // Start server with tsx
53
+ await execa(getDepsBinPath('tsx'), [
54
+ 'watch',
55
+ '--clear-screen=false',
56
+ 'node_modules/@pulse-editor/cli/dist/lib/server/express.js',
42
57
  ], {
43
58
  stdio: 'inherit',
44
59
  shell: true,
@@ -47,8 +62,20 @@ export default function Dev({ cli }) {
47
62
  NODE_ENV: 'development',
48
63
  },
49
64
  });
65
+ // Handle process exit to close webpack compiler
66
+ process.on('SIGINT', () => {
67
+ if (compiler && typeof compiler.close === 'function') {
68
+ compiler.close(() => {
69
+ process.exit();
70
+ });
71
+ }
72
+ else {
73
+ process.exit();
74
+ }
75
+ });
50
76
  }
77
+ console.log('🚀 Starting development server...');
51
78
  startDevServer();
52
79
  }, []);
53
- return (_jsx(_Fragment, { children: _jsx(Text, { children: "Starting dev server..." }) }));
80
+ return _jsx(_Fragment, {});
54
81
  }
@@ -1,5 +1,5 @@
1
- import { Flags } from '../../lib/cli-flags.js';
2
- import { Result } from 'meow';
1
+ import { Result } from "meow";
2
+ import { Flags } from "../../lib/cli-flags.js";
3
3
  export default function Login({ cli }: {
4
4
  cli: Result<Flags>;
5
5
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,30 +1,58 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { Box, Text, useApp } from 'ink';
4
- import SelectInput from 'ink-select-input';
5
- import TextInput from 'ink-text-input';
6
- import Spinner from 'ink-spinner';
7
- import os from 'os';
8
- import path from 'path';
9
- import { checkToken, getToken, isTokenInEnv, saveToken, } from '../../lib/token.js';
2
+ import { exec } from "child_process";
3
+ import http from "http";
4
+ import { Box, Text, useApp, useInput } from "ink";
5
+ import SelectInput from "ink-select-input";
6
+ import Spinner from "ink-spinner";
7
+ import TextInput from "ink-text-input";
8
+ import os from "os";
9
+ import path from "path";
10
+ import { useEffect, useState } from "react";
11
+ import { getBackendUrl } from "../../lib/backend-url.js";
12
+ import { checkToken, getToken, isTokenInEnv, saveToken, } from "../../lib/token.js";
13
+ function openBrowser(url) {
14
+ const platform = process.platform;
15
+ let cmd;
16
+ if (platform === "win32") {
17
+ cmd = `start "" "${url}"`;
18
+ }
19
+ else if (platform === "darwin") {
20
+ cmd = `open "${url}"`;
21
+ }
22
+ else {
23
+ cmd = `xdg-open "${url}"`;
24
+ }
25
+ exec(cmd);
26
+ }
27
+ // Render a terminal hyperlink using ANSI OSC 8 escape sequences
28
+ function TerminalLink({ url, label }) {
29
+ const text = label ?? url;
30
+ // OSC 8 hyperlink: ESC]8;;URL BEL text ESC]8;; BEL
31
+ const hyperlink = `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
32
+ return _jsx(Text, { children: hyperlink });
33
+ }
10
34
  export default function Login({ cli }) {
11
35
  const [loginMethod, setLoginMethod] = useState(undefined);
12
36
  const [isShowLoginMethod, setIsShowLoginMethod] = useState(false);
13
37
  const [isMethodSelected, setIsMethodSelected] = useState(false);
14
38
  const [isCheckingAuth, setIsCheckingAuth] = useState(true);
15
39
  const [isAuthenticated, setIsAuthenticated] = useState(false);
16
- const [token, setToken] = useState('');
40
+ const [token, setToken] = useState("");
17
41
  const [isTokenSaved, setIsTokenSaved] = useState(false);
18
- const [tokenInput, setTokenInput] = useState('');
19
- const [saveTokenInput, setSaveTokenInput] = useState('');
42
+ const [tokenInput, setTokenInput] = useState("");
43
+ const [saveTokenInput, setSaveTokenInput] = useState("");
44
+ // Browser flow state
45
+ const [flowState, setFlowState] = useState("idle");
46
+ const [authUrl, setAuthUrl] = useState("");
47
+ const [flowError, setFlowError] = useState("");
20
48
  const loginMethodItems = [
21
49
  {
22
- label: 'Login using access token',
23
- value: 'token',
50
+ label: "Login in browser",
51
+ value: "flow",
24
52
  },
25
53
  {
26
- label: '(WIP) Login in browser',
27
- value: 'flow',
54
+ label: "Login using access token",
55
+ value: "token",
28
56
  },
29
57
  ];
30
58
  const { exit } = useApp();
@@ -33,22 +61,22 @@ export default function Login({ cli }) {
33
61
  const savedToken = getToken(cli.flags.stage);
34
62
  setIsShowLoginMethod(!savedToken && !cli.flags.token && !cli.flags.flow);
35
63
  if (savedToken) {
36
- setLoginMethod('token');
64
+ setLoginMethod("token");
37
65
  setToken(savedToken);
38
66
  return;
39
67
  }
40
68
  else if (cli.flags.token) {
41
- setLoginMethod('token');
69
+ setLoginMethod("token");
42
70
  }
43
71
  else if (cli.flags.flow) {
44
- setLoginMethod('flow');
72
+ setLoginMethod("flow");
45
73
  }
46
74
  }, [cli]);
47
75
  // Check token validity
48
76
  useEffect(() => {
49
77
  // Only check token validity when it is set
50
- if (loginMethod === 'token' && token.length > 0) {
51
- checkToken(token, cli.flags.stage).then(isValid => {
78
+ if (loginMethod === "token" && token.length > 0) {
79
+ checkToken(token, cli.flags.stage).then((isValid) => {
52
80
  setIsAuthenticated(isValid);
53
81
  setIsCheckingAuth(false);
54
82
  });
@@ -59,21 +87,77 @@ export default function Login({ cli }) {
59
87
  setIsMethodSelected(loginMethod !== undefined);
60
88
  }, 0);
61
89
  }, [loginMethod]);
62
- return (_jsxs(_Fragment, { children: [isShowLoginMethod && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Login to the Pulse Editor Platform" }), _jsx(SelectInput, { items: loginMethodItems, onSelect: item => {
90
+ // Browser flow: start local server and open browser
91
+ useEffect(() => {
92
+ if (loginMethod !== "flow" || !isMethodSelected)
93
+ return;
94
+ const baseUrl = getBackendUrl(cli.flags.stage);
95
+ const server = http.createServer((req, res) => {
96
+ const reqUrl = new URL(req.url ?? "/", "http://localhost");
97
+ const receivedToken = reqUrl.searchParams.get("token");
98
+ if (receivedToken) {
99
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
100
+ res.end('<html><body style="font-family:sans-serif;text-align:center;padding:60px">' +
101
+ "<h2>Login successful!</h2><p>You can close this tab and return to the terminal.</p>" +
102
+ "</body></html>");
103
+ server.close();
104
+ saveToken(receivedToken, cli.flags.stage);
105
+ setToken(receivedToken);
106
+ setFlowState("success");
107
+ setTimeout(() => {
108
+ exit();
109
+ }, 1500);
110
+ }
111
+ else {
112
+ res.writeHead(400, { "Content-Type": "text/plain" });
113
+ res.end("Missing token parameter.");
114
+ }
115
+ });
116
+ server.listen(0, "127.0.0.1", () => {
117
+ const port = server.address().port;
118
+ const callbackUrl = `http://127.0.0.1:${port}`;
119
+ const url = `${baseUrl}/api/auth/cli?redirect=${encodeURIComponent(callbackUrl)}`;
120
+ setAuthUrl(url);
121
+ setFlowState("idle");
122
+ });
123
+ server.on("error", () => {
124
+ setFlowError("Failed to start local server for authentication.");
125
+ setFlowState("error");
126
+ });
127
+ return () => {
128
+ server.close();
129
+ };
130
+ }, [loginMethod, isMethodSelected]);
131
+ // Handle Enter key to open browser (when in flow mode and idle)
132
+ useInput((_, key) => {
133
+ if (key.return &&
134
+ loginMethod === "flow" &&
135
+ flowState === "idle" &&
136
+ authUrl) {
137
+ setFlowState("opening");
138
+ openBrowser(authUrl);
139
+ setTimeout(() => {
140
+ setFlowState("waiting");
141
+ }, 500);
142
+ }
143
+ }, {
144
+ isActive: loginMethod === "flow" && flowState === "idle" && Boolean(authUrl),
145
+ });
146
+ return (_jsxs(_Fragment, { children: [isShowLoginMethod && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Login to the Pulse Editor Platform" }), _jsx(SelectInput, { items: loginMethodItems, onSelect: (item) => {
63
147
  setLoginMethod(item.value);
64
148
  }, isFocused: loginMethod === undefined }), _jsx(Text, { children: " " })] })), isMethodSelected &&
65
- loginMethod === 'token' &&
66
- (token.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Enter your Pulse Editor access token:" }), _jsx(TextInput, { mask: "*", value: tokenInput, onChange: setTokenInput, onSubmit: value => {
149
+ loginMethod === "token" &&
150
+ (token.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Enter your Pulse Editor access token:" }), _jsx(TextInput, { mask: "*", value: tokenInput, onChange: setTokenInput, onSubmit: (value) => {
67
151
  if (value.length === 0) {
68
152
  return;
69
153
  }
70
154
  setToken(value);
71
155
  } })] })) : isCheckingAuth ? (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Checking authentication..." })] })) : isAuthenticated ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: "\u2705 You are signed in successfully." }), !isTokenInEnv(cli.flags.stage) &&
72
- getToken(cli.flags.stage) !== token && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "\uD83D\uDFE2 It is recommended to save your access token as an environment variable PE_ACCESS_TOKEN." }), _jsxs(Box, { children: [_jsxs(Text, { children: ["\u26A0\uFE0F (NOT recommended) Or, do you want to save access token to user home directory? (y/n)", ' '] }), _jsx(TextInput, { value: saveTokenInput, onChange: setSaveTokenInput, onSubmit: value => {
156
+ getToken(cli.flags.stage) !== token && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "\uD83D\uDFE2 It is recommended to save your access token as an environment variable PE_ACCESS_TOKEN." }), _jsxs(Box, { children: [_jsxs(Text, { children: ["\u26A0\uFE0F (NOT recommended) Or, do you want to save access token to user home directory? (y/n)", " "] }), _jsx(TextInput, { value: saveTokenInput, onChange: setSaveTokenInput, onSubmit: (value) => {
73
157
  if (value.length === 0) {
74
158
  return;
75
159
  }
76
- if (value === 'y') {
160
+ if (value === "y") {
77
161
  saveToken(token, cli.flags.stage);
78
162
  setIsTokenSaved(true);
79
163
  setTimeout(() => {
@@ -83,5 +167,5 @@ export default function Login({ cli }) {
83
167
  else {
84
168
  exit();
85
169
  }
86
- } })] })] })), isTokenSaved && (_jsxs(Text, { children: ["Token saved to ", path.join(os.homedir(), '.pulse-editor')] }))] })) : (_jsx(Text, { children: "Authentication error: please enter valid credentials." }))), isMethodSelected && loginMethod === 'flow' && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "(WIP) Open the following URL in your browser:" }), _jsx(Text, { children: "https://pulse-editor.com/login" })] }))] }));
170
+ } })] })] })), isTokenSaved && (_jsxs(Text, { children: ["Token saved to ", path.join(os.homedir(), ".pulse-editor")] }))] })) : (_jsx(Text, { children: "Authentication error: please enter valid credentials." }))), isMethodSelected && loginMethod === "flow" && (_jsx(_Fragment, { children: flowState === "error" ? (_jsxs(Text, { color: "red", children: ["Error: ", flowError] })) : flowState === "success" ? (_jsx(Text, { children: "\u2705 Login successful! Saving credentials..." })) : flowState === "waiting" || flowState === "opening" ? (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Waiting for browser authentication..." })] }), _jsx(Text, { dimColor: true, children: "If the browser did not open, visit: " }), authUrl && _jsx(TerminalLink, { url: authUrl })] })) : authUrl ? (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { children: ["Press ", _jsx(Text, { bold: true, children: "Enter" }), " to open your browser and login:"] }), _jsx(TerminalLink, { url: authUrl, label: "Open browser to login \u2192" })] })) : (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Preparing login..." })] })) }))] }));
87
171
  }
@@ -1,10 +1,10 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Text } from 'ink';
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
2
  import { useEffect } from 'react';
4
3
  import { execa } from 'execa';
5
4
  import fs from 'fs';
6
5
  import { getDepsBinPath } from '../../lib/execa-utils/deps.js';
7
6
  import { cleanDist } from '../../lib/execa-utils/clean.js';
7
+ import { webpackCompile } from '../../lib/webpack/compile.js';
8
8
  export default function Preview({ cli }) {
9
9
  useEffect(() => {
10
10
  async function startPreviewServer() {
@@ -22,9 +22,19 @@ export default function Preview({ cli }) {
22
22
  });
23
23
  }
24
24
  }
25
- await execa('mkdir "node_modules/.pulse/server"', {
26
- shell: true,
27
- });
25
+ // Create node_modules/.pulse/server directory
26
+ if (!fs.existsSync('node_modules/.pulse/server')) {
27
+ if (process.platform === 'win32') {
28
+ await execa('mkdir node_modules\\.pulse\\server', {
29
+ shell: true,
30
+ });
31
+ }
32
+ else {
33
+ await execa('mkdir -p node_modules/.pulse/server', {
34
+ shell: true,
35
+ });
36
+ }
37
+ }
28
38
  if (process.platform === 'win32') {
29
39
  await execa('xcopy /E /I node_modules\\@pulse-editor\\cli\\dist\\lib\\server node_modules\\.pulse\\server', {
30
40
  shell: true,
@@ -37,11 +47,28 @@ export default function Preview({ cli }) {
37
47
  }
38
48
  // Start preview server
39
49
  await cleanDist();
40
- await execa(getDepsBinPath('concurrently'), [
41
- '--prefix',
42
- 'none',
43
- '"npx webpack --mode development --watch"',
44
- '"tsx watch --clear-screen=false node_modules/@pulse-editor/cli/dist/lib/server/express.js"',
50
+ // await execa(
51
+ // getDepsBinPath('concurrently'),
52
+ // [
53
+ // '--prefix',
54
+ // 'none',
55
+ // '"npx webpack --mode development --watch"',
56
+ // '"tsx watch --clear-screen=false node_modules/@pulse-editor/cli/dist/lib/server/express.js"',
57
+ // ],
58
+ // {
59
+ // stdio: 'inherit',
60
+ // shell: true,
61
+ // env: {
62
+ // NODE_OPTIONS: '--import=tsx',
63
+ // PREVIEW: 'true',
64
+ // },
65
+ // },
66
+ // );
67
+ const compiler = await webpackCompile('preview', undefined, true);
68
+ await execa(getDepsBinPath('tsx'), [
69
+ 'watch',
70
+ '--clear-screen=false',
71
+ 'node_modules/@pulse-editor/cli/dist/lib/server/express.js',
45
72
  ], {
46
73
  stdio: 'inherit',
47
74
  shell: true,
@@ -50,8 +77,20 @@ export default function Preview({ cli }) {
50
77
  PREVIEW: 'true',
51
78
  },
52
79
  });
80
+ // Handle process exit to close webpack compiler
81
+ process.on('SIGINT', () => {
82
+ if (compiler && typeof compiler.close === 'function') {
83
+ compiler.close(() => {
84
+ process.exit();
85
+ });
86
+ }
87
+ else {
88
+ process.exit();
89
+ }
90
+ });
53
91
  }
92
+ console.log('🚀 Starting preview server...');
54
93
  startPreviewServer();
55
94
  }, []);
56
- return (_jsx(_Fragment, { children: _jsx(Text, { children: "Starting preview server..." }) }));
95
+ return _jsx(_Fragment, {});
57
96
  }