@ebay/muse-runner 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.muserc +1 -0
- package/README.md +8 -0
- package/bin/muse-runner.js +19 -0
- package/lib/AppRunner.js +54 -0
- package/lib/Command.js +55 -0
- package/lib/MuseRunner.js +135 -0
- package/lib/PluginRunner.js +67 -0
- package/lib/apis/checkUpdate.js +25 -0
- package/lib/apis/gitStatus.js +89 -0
- package/lib/apis/settings.js +9 -0
- package/lib/apis/startPlugin.js +22 -0
- package/lib/apis/stopPlugin.js +8 -0
- package/lib/apis/terminals.js +118 -0
- package/lib/appWorker.js +325 -0
- package/lib/reactFastRefresh.js +452 -0
- package/lib/server.js +463 -0
- package/lib/test.js +20 -0
- package/lib/upgrades/up_240102.js +25 -0
- package/lib/utils.js +79 -0
- package/package.json +49 -0
- package/public/index.html +14 -0
- package/public/muse-assets/p/@ebay.muse-boot-default/v1.0.25/dist/asset-manifest.json +10 -0
- package/public/muse-assets/p/@ebay.muse-boot-default/v1.0.25/dist/boot.js +2 -0
- package/public/muse-assets/p/@ebay.muse-boot-default/v1.0.25/dist/static/media/logo.0629cb217459ef0a31a2.png +0 -0
- package/public/muse-assets/p/@ebay.muse-layout-antd/v1.1.26/dist/assets/index-BeGfgbtf.css +1 -0
- package/public/muse-assets/p/@ebay.muse-layout-antd/v1.1.26/dist/assets/muse-V3RbDVED.png +0 -0
- package/public/muse-assets/p/@ebay.muse-layout-antd/v1.1.26/dist/deps-manifest.json +19 -0
- package/public/muse-assets/p/@ebay.muse-layout-antd/v1.1.26/dist/main.js +16 -0
- package/public/muse-assets/p/@ebay.muse-layout-antd/v1.1.26/dist/readme.txt +1 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/asset-manifest.json +12 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/deps-manifest.json +26 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/info.json +3 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/lib-manifest.json +32768 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/main.js +3 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/main.js.LICENSE.txt +30 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/readme.txt +1 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/report.html +39 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/static/js/205.bfaa7a71.chunk.js +2 -0
- package/public/muse-assets/p/@ebay.muse-lib-antd/v1.2.22/dist/static/media/lightOn.f95be1d2e4156b9a7459.svg +1 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/asset-manifest.json +11 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/favicon.png +0 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/info.json +3 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/lib-manifest.json +3821 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/main.js +3 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/main.js.LICENSE.txt +136 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/static/media/logo.b23b880b0dac2229042c.png +0 -0
- package/public/muse-assets/p/@ebay.muse-lib-react/v1.2.20/dist/static/media/subAppLoading2.bf08007b83457287ade1cedb71bc70f7.svg +7 -0
- package/public/muse-assets/p/app-icon.muserunner/v0.0.1/dist/icon.png +0 -0
- package/public/muse-assets/p/muse-runner-ui/v1.0.26/dist/asset-manifest.json +10 -0
- package/public/muse-assets/p/muse-runner-ui/v1.0.26/dist/deps-manifest.json +43 -0
- package/public/muse-assets/p/muse-runner-ui/v1.0.26/dist/main.js +3 -0
- package/public/muse-assets/p/muse-runner-ui/v1.0.26/dist/main.js.LICENSE.txt +43 -0
- package/public/muse-assets/p/muse-runner-ui/v1.0.26/dist/readme.txt +2 -0
- package/public/muse-assets/p/muse-runner-ui/v1.0.26/dist/static/media/vscode.e44746dcd601802dfb4fe3374cfab5b0.svg +18 -0
- package/public/muse-sw.js +111 -0
package/.muserc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as url from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import upgrade240102 from '../lib/upgrades/up_240102.js';
|
|
6
|
+
|
|
7
|
+
// Enforce the cwd is the root of the project
|
|
8
|
+
// So that it won't use unexpected muse config file.
|
|
9
|
+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
10
|
+
process.chdir(path.join(__dirname, '../'));
|
|
11
|
+
|
|
12
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
13
|
+
const pkgJson = fs.readJsonSync('./package.json');
|
|
14
|
+
console.log(pkgJson.version);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
upgrade240102();
|
|
18
|
+
|
|
19
|
+
import('../lib/server.js');
|
package/lib/AppRunner.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as url from 'url';
|
|
4
|
+
import getPort, { portNumbers } from 'get-port';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
|
|
7
|
+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Start a Muse app at dev mode.
|
|
11
|
+
* It controls whether plusin are loaded from local bundles or deployed ones.
|
|
12
|
+
* It also allows to change app/env
|
|
13
|
+
*/
|
|
14
|
+
export default class AppRunner extends EventEmitter {
|
|
15
|
+
async start({ id, port, app, env = 'staging' }) {
|
|
16
|
+
this.id = id;
|
|
17
|
+
if (this.worker) {
|
|
18
|
+
throw new Error(`App already started.`);
|
|
19
|
+
}
|
|
20
|
+
const realPort = port || (await getPort({ port: portNumbers(50000, 50500) }));
|
|
21
|
+
|
|
22
|
+
// Use a worker, so that we can get seperate stdout of the current server.
|
|
23
|
+
const worker = new Worker(path.join(__dirname, './appWorker.js'), {
|
|
24
|
+
env: process.env,
|
|
25
|
+
workerData: {
|
|
26
|
+
id,
|
|
27
|
+
port: realPort,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
this.app = app;
|
|
31
|
+
this.env = env;
|
|
32
|
+
this.port = realPort;
|
|
33
|
+
|
|
34
|
+
worker.on('error', (err) => {
|
|
35
|
+
// TODO: handle error
|
|
36
|
+
console.log('[worker error]', err);
|
|
37
|
+
// this.emit('error', err);
|
|
38
|
+
});
|
|
39
|
+
worker.on('exit', (code) => {
|
|
40
|
+
this.emit('exit', code);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.worker = worker;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
port: realPort,
|
|
47
|
+
worker,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async stop() {
|
|
52
|
+
await this.worker.terminate();
|
|
53
|
+
}
|
|
54
|
+
}
|
package/lib/Command.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as pty from 'node-pty';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The reason we use node-pty is that it can keep output color?
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export default class Command extends EventEmitter {
|
|
10
|
+
cwd;
|
|
11
|
+
cmd;
|
|
12
|
+
port;
|
|
13
|
+
ptyArgs;
|
|
14
|
+
constructor({ cwd, cmd, env, ptyArgs = {} }) {
|
|
15
|
+
super();
|
|
16
|
+
this.cwd = cwd;
|
|
17
|
+
this.cmd = cmd;
|
|
18
|
+
this.ptyArgs = ptyArgs;
|
|
19
|
+
this.env = env;
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
23
|
+
|
|
24
|
+
const ptyProcess = pty.spawn(shell, ['-c', this.cmd], {
|
|
25
|
+
name: 'xterm-instance',
|
|
26
|
+
cols: 80,
|
|
27
|
+
rows: 30,
|
|
28
|
+
cwd: this.cwd,
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
...this.env,
|
|
32
|
+
},
|
|
33
|
+
...this.ptyArgs,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
ptyProcess.onData((data) => {
|
|
37
|
+
// process.stdout.write(data);
|
|
38
|
+
// ptyProcess.on('data');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
ptyProcess.onExit((code) => {
|
|
42
|
+
this.emit('exit', code);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ptyProcess.write('ls\r');
|
|
46
|
+
// ptyProcess.resize(100, 40);
|
|
47
|
+
// ptyProcess.write('ls\r');
|
|
48
|
+
this.ptyProcess = ptyProcess;
|
|
49
|
+
return ptyProcess;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
terminate() {
|
|
53
|
+
this.ptyProcess.kill();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import EventEmitter from 'node:events';
|
|
3
|
+
import museDevUtils from '@ebay/muse-dev-utils/lib/utils.js';
|
|
4
|
+
import AppRunner from './AppRunner.js';
|
|
5
|
+
import PluginRunner from './PluginRunner.js';
|
|
6
|
+
|
|
7
|
+
export default class MuseRunner extends EventEmitter {
|
|
8
|
+
runningApps = [];
|
|
9
|
+
runningPlugins = [];
|
|
10
|
+
async startApp({ id, app, env, port }) {
|
|
11
|
+
const found = this.runningApps.find((r) => r.id === id);
|
|
12
|
+
if (found) {
|
|
13
|
+
return found;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const appRunner = new AppRunner();
|
|
17
|
+
appRunner.on('exit', () => {
|
|
18
|
+
_.pull(this.runningApps, appRunner);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await appRunner.start({
|
|
22
|
+
id,
|
|
23
|
+
app,
|
|
24
|
+
env,
|
|
25
|
+
port,
|
|
26
|
+
});
|
|
27
|
+
this.runningApps.push(appRunner);
|
|
28
|
+
return appRunner;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async stopApp({ id }) {
|
|
32
|
+
const app = this.runningApps.find((r) => r.id === id);
|
|
33
|
+
if (!app) {
|
|
34
|
+
return;
|
|
35
|
+
// throw new Error(`Running app not found: ${id}.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await app.stop();
|
|
39
|
+
_.pull(this.runningApps, app);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getLinkedLibs(targetPlugin, changedLibPluginRunner) {
|
|
43
|
+
// Note, if no changedLibPluginRunner, means start a new plugin
|
|
44
|
+
const runningLibPlugins = this.runningPlugins.filter((p) => p.pluginInfo.type === 'lib');
|
|
45
|
+
|
|
46
|
+
const depLibs = museDevUtils.getMuseLibsByFolder(targetPlugin.dir);
|
|
47
|
+
if (
|
|
48
|
+
changedLibPluginRunner &&
|
|
49
|
+
!depLibs.find((d) => d.name === changedLibPluginRunner.pluginInfo.name)
|
|
50
|
+
) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const shouldLink = depLibs.reduce((acc, lib) => {
|
|
54
|
+
const found = runningLibPlugins.find((lp) => lp.pluginInfo.name === lib.name);
|
|
55
|
+
if (found) {
|
|
56
|
+
acc.push(found);
|
|
57
|
+
}
|
|
58
|
+
return acc;
|
|
59
|
+
}, []);
|
|
60
|
+
return shouldLink.map((d) => d.dir).join(';');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async updateMuseLinkedLibs(changedLibPluginRunner) {
|
|
64
|
+
// notify running plugins to update MUSE_LINKED_LIBS if it's changed
|
|
65
|
+
this.runningPlugins.forEach((rp) => {
|
|
66
|
+
const linkedLibs = this.getLinkedLibs(rp, changedLibPluginRunner);
|
|
67
|
+
if (linkedLibs !== null) {
|
|
68
|
+
// Means the linked libs of the plugin is changed
|
|
69
|
+
// Should ask the plugin dev server to restart
|
|
70
|
+
// console.log('updating linked libs: ', rp.name, linkedLibs);
|
|
71
|
+
// rp.cmd.ptyProcess.write(`MUSE_LINKED_LIBS=${linkedLibs}\n`);'
|
|
72
|
+
this.restartPlugin({ dir: rp.dir, env: {} });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async startPlugin({ dir, port, env }) {
|
|
78
|
+
const found = this.runningPlugins.find((r) => r.dir === dir);
|
|
79
|
+
if (found) {
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pluginRunner = new PluginRunner();
|
|
84
|
+
pluginRunner.on('exit', () => {
|
|
85
|
+
_.pull(this.runningPlugins, pluginRunner);
|
|
86
|
+
});
|
|
87
|
+
if (!env) env = {};
|
|
88
|
+
const linkedLibs = this.getLinkedLibs({ dir });
|
|
89
|
+
if (linkedLibs) {
|
|
90
|
+
env.MUSE_LINKED_LIBS = linkedLibs;
|
|
91
|
+
}
|
|
92
|
+
await pluginRunner.start({ dir, port, env });
|
|
93
|
+
this.runningPlugins.push(pluginRunner);
|
|
94
|
+
this.runningApps.forEach((appRunner) => {
|
|
95
|
+
appRunner.worker.postMessage({
|
|
96
|
+
type: 'running-plugins',
|
|
97
|
+
payload: this.runningPlugins.map((p) => ({
|
|
98
|
+
port: p.port,
|
|
99
|
+
name: p.pluginInfo.name,
|
|
100
|
+
type: p.pluginInfo.type,
|
|
101
|
+
})),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (pluginRunner.pluginInfo.type === 'lib') {
|
|
106
|
+
// If a lib plugin is started or stopped,
|
|
107
|
+
// need to update MUSE_LINKED_LIBS for all other running plugins
|
|
108
|
+
await this.updateMuseLinkedLibs(pluginRunner);
|
|
109
|
+
}
|
|
110
|
+
// Emit an event to allow UI part get the change
|
|
111
|
+
this.emit('start-plugin', { pluginRunner });
|
|
112
|
+
return pluginRunner;
|
|
113
|
+
}
|
|
114
|
+
async stopPlugin({ dir }) {
|
|
115
|
+
const pluginRunner = this.runningPlugins.find((r) => r.dir === dir);
|
|
116
|
+
if (!pluginRunner) {
|
|
117
|
+
// throw new Error(`Running plugin not found: ${dir}.`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await pluginRunner.stop();
|
|
122
|
+
_.pull(this.runningPlugins, pluginRunner);
|
|
123
|
+
if (pluginRunner.pluginInfo.type === 'lib') {
|
|
124
|
+
// If a lib plugin is started or stopped,
|
|
125
|
+
// need to update MUSE_LINKED_LIBS for all other running plugins
|
|
126
|
+
await this.updateMuseLinkedLibs(pluginRunner);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async restartPlugin({ dir, port, env }) {
|
|
131
|
+
console.log('restart plugin: ', dir);
|
|
132
|
+
await this.stopPlugin({ dir });
|
|
133
|
+
await this.startPlugin({ dir, port, env });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import getPort, { portNumbers } from 'get-port';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import Command from './Command.js';
|
|
4
|
+
import utils from './utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Start a Muse plugin project
|
|
8
|
+
* It controls whether plusin are loaded from local bundles or deployed ones.
|
|
9
|
+
*/
|
|
10
|
+
export default class PluginRunner extends EventEmitter {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
}
|
|
14
|
+
async start({ dir, port, env }) {
|
|
15
|
+
const realPort = port || (await getPort({ port: portNumbers(30000, 31000) }));
|
|
16
|
+
const pluginInfo = utils.getPluginInfo(dir);
|
|
17
|
+
pluginInfo.dir = dir;
|
|
18
|
+
const devScriptName = 'start';
|
|
19
|
+
const devScript = pluginInfo.pkgJson.scripts[devScriptName];
|
|
20
|
+
if (!devScript) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Missing "${devScriptName}" script in the package.json of the plugin: "${pluginInfo.pkgJson.name}".`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (devScript.split(/ /).some((s) => s.startsWith('PORT='))) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Should not have PORT env variable in the package script when using Muse Runner: "${devScriptName}": "${devScript}".`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pkgManager = utils.getPkgManager(dir);
|
|
33
|
+
|
|
34
|
+
const cmd = new Command({
|
|
35
|
+
cwd: dir,
|
|
36
|
+
cmd: `${pkgManager} run ${devScriptName}`,
|
|
37
|
+
env: {
|
|
38
|
+
PORT: realPort,
|
|
39
|
+
...env,
|
|
40
|
+
// SSL_CRT_FILE: '',
|
|
41
|
+
// SSL_KEY_FILE: '',
|
|
42
|
+
MUSE_CONFIG_FILE: '', // The config file should be empty, so that the plugin can use the default config.
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
cmd.start();
|
|
46
|
+
|
|
47
|
+
this.dir = dir;
|
|
48
|
+
this.port = realPort;
|
|
49
|
+
this.cmd = cmd;
|
|
50
|
+
this.pluginInfo = pluginInfo;
|
|
51
|
+
this.name = this.pluginInfo.name;
|
|
52
|
+
|
|
53
|
+
this.cmd.on('exit', (code) => {
|
|
54
|
+
this.emit('exit', code);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
port: realPort,
|
|
59
|
+
cmd,
|
|
60
|
+
pluginInfo,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async stop() {
|
|
65
|
+
this.cmd.terminate();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import * as url from 'url';
|
|
5
|
+
import pkgJson from 'package-json';
|
|
6
|
+
|
|
7
|
+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
8
|
+
|
|
9
|
+
// TODO: expose it as rest api in server.js
|
|
10
|
+
export default async function checkUpdate() {
|
|
11
|
+
const selfPkg = fs.readJsonSync(path.join(__dirname, '../package.json'));
|
|
12
|
+
|
|
13
|
+
const args = {};
|
|
14
|
+
if (process.env.RUNNER_NPM_REGISTRY) {
|
|
15
|
+
args.registryUrl = process.env.RUNNER_NPM_REGISTRY;
|
|
16
|
+
}
|
|
17
|
+
const publishedPkg = await pkgJson(selfPkg.name, args);
|
|
18
|
+
|
|
19
|
+
if (semver.lt(selfPkg.version, publishedPkg.version)) {
|
|
20
|
+
return {
|
|
21
|
+
newVersion: publishedPkg.version,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import simpleGit from 'simple-git';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import chokidar from 'chokidar';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
const gitDirMap = {};
|
|
8
|
+
const watcherMap = {};
|
|
9
|
+
const gitStatusMap = {};
|
|
10
|
+
|
|
11
|
+
const getGitDir = (dir) => {
|
|
12
|
+
if (gitDirMap[dir]) return gitDirMap[dir];
|
|
13
|
+
// find the nearest git dir
|
|
14
|
+
const git = simpleGit(dir);
|
|
15
|
+
return git.revparse(['--show-toplevel']).then((gitDir) => {
|
|
16
|
+
gitDirMap[dir] = gitDir;
|
|
17
|
+
return gitDir;
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const gitMap = {};
|
|
22
|
+
const getGitClient = (dir) => {
|
|
23
|
+
if (gitMap[dir]) return gitMap[dir];
|
|
24
|
+
gitMap[dir] = simpleGit(dir);
|
|
25
|
+
return gitMap[dir];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// watch git status of all plugins
|
|
29
|
+
// TODO: remove watcher if a folder is removed.
|
|
30
|
+
export default ({ app, io, config }) => {
|
|
31
|
+
const callbacks = {};
|
|
32
|
+
const handleGitChange = (gitDir) => {
|
|
33
|
+
if (callbacks[gitDir]) return callbacks[gitDir];
|
|
34
|
+
callbacks[gitDir] = _.debounce(async () => {
|
|
35
|
+
const git = getGitClient(gitDir);
|
|
36
|
+
const status = await git.status();
|
|
37
|
+
_.invertBy(gitDirMap)[gitDir]?.forEach((d) => {
|
|
38
|
+
gitStatusMap[d] = status;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
io.emit({
|
|
42
|
+
type: 'git-status-changed',
|
|
43
|
+
data: gitStatusMap,
|
|
44
|
+
});
|
|
45
|
+
}, 300);
|
|
46
|
+
return callbacks[gitDir];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleConfigChange = async () => {
|
|
50
|
+
const plugins = config.get('plugins', {});
|
|
51
|
+
|
|
52
|
+
for (let dir of Object.values(plugins).map((p) => p.dir)) {
|
|
53
|
+
if (!fs.existsSync(dir) || !fs.existsSync(path.join(dir, '.git'))) continue;
|
|
54
|
+
try {
|
|
55
|
+
const gitDir = await getGitDir(dir);
|
|
56
|
+
if (!watcherMap[gitDir]) {
|
|
57
|
+
watcherMap[gitDir] = chokidar.watch(gitDir, { ignored: /node_modules/ });
|
|
58
|
+
watcherMap[gitDir].on('all', handleGitChange(gitDir));
|
|
59
|
+
handleGitChange(gitDir)();
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// ignore
|
|
63
|
+
console.log('failed to watch git dir: ' + dir, e.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
config.onDidChange('plugins', handleConfigChange);
|
|
69
|
+
|
|
70
|
+
handleConfigChange();
|
|
71
|
+
app.get('/api/git-status', (req, res) => {
|
|
72
|
+
res.end(JSON.stringify(gitStatusMap));
|
|
73
|
+
// const dir = decodeURIComponent(req.query.dir);
|
|
74
|
+
// const git = simpleGit(dir);
|
|
75
|
+
// git.status((err, result) => {
|
|
76
|
+
// if (err) {
|
|
77
|
+
// res.end({
|
|
78
|
+
// code: 1,
|
|
79
|
+
// msg: err,
|
|
80
|
+
// });
|
|
81
|
+
// } else {
|
|
82
|
+
// res.end({
|
|
83
|
+
// code: 0,
|
|
84
|
+
// data: result,
|
|
85
|
+
// });
|
|
86
|
+
// }
|
|
87
|
+
// });
|
|
88
|
+
});
|
|
89
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { handleAsyncError } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
export default ({ config, runner }) =>
|
|
4
|
+
handleAsyncError(async (req, res) => {
|
|
5
|
+
const { pluginName } = req.body;
|
|
6
|
+
const plugins = config.get('plugins', {});
|
|
7
|
+
const dir = plugins[pluginName]?.dir;
|
|
8
|
+
if (!dir) throw new Error(`Plugin folder not found: ${pluginName}`);
|
|
9
|
+
|
|
10
|
+
const pluginRunner = await runner.startPlugin({
|
|
11
|
+
dir,
|
|
12
|
+
plugin: plugins[pluginName],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
res.setHeader('Content-Type', 'application/json');
|
|
16
|
+
res.end(
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
...pluginRunner.pluginInfo,
|
|
19
|
+
port: pluginRunner.port,
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import * as pty from 'node-pty';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
|
|
5
|
+
const getSehll = () => {
|
|
6
|
+
if (process.platform === 'win32') {
|
|
7
|
+
// For windows 10 use powershell
|
|
8
|
+
try {
|
|
9
|
+
const ver = os
|
|
10
|
+
.release()
|
|
11
|
+
.split('.')
|
|
12
|
+
.shift();
|
|
13
|
+
if (parseInt(ver, 10) >= 10) return 'powershell.exe';
|
|
14
|
+
// For windows 7 and below, use cmd.exe
|
|
15
|
+
return 'cmd.exe';
|
|
16
|
+
} catch (err) {
|
|
17
|
+
return 'cmd.exe';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// else {
|
|
21
|
+
// // Use system shell for Mac
|
|
22
|
+
|
|
23
|
+
// let source;
|
|
24
|
+
// ['.bash_profile', '.bashrc']
|
|
25
|
+
// .map(f => path.join(os.homedir(), f))
|
|
26
|
+
// .some(file => {
|
|
27
|
+
// if (fs.existsSync(file)) {
|
|
28
|
+
// source = file;
|
|
29
|
+
// return true;
|
|
30
|
+
// }
|
|
31
|
+
// return false;
|
|
32
|
+
// });
|
|
33
|
+
// console.log('bashrc: ', source);
|
|
34
|
+
// if (source) shellArgs.push('--rcfile', source);
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync('/bin/zsh')) return '/bin/zsh';
|
|
37
|
+
return '/bin/bash';
|
|
38
|
+
|
|
39
|
+
// }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default function setupTerminals({ app }) {
|
|
43
|
+
const terminals = {};
|
|
44
|
+
const logs = {};
|
|
45
|
+
|
|
46
|
+
// Create a terminal
|
|
47
|
+
app.post('/api/terminals', function(req, res) {
|
|
48
|
+
const { dir, cols, rows } = req.body;
|
|
49
|
+
res.setHeader('Content-Type', 'application/json');
|
|
50
|
+
if (terminals[dir]) {
|
|
51
|
+
res.end(JSON.stringify({ pid: terminals[dir].pid }));
|
|
52
|
+
res.end();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const shellArgs = [];
|
|
57
|
+
const term = pty.spawn(getSehll(), shellArgs, {
|
|
58
|
+
name: 'xterm-color',
|
|
59
|
+
cols: cols || 80,
|
|
60
|
+
rows: rows || 24,
|
|
61
|
+
cwd: dir,
|
|
62
|
+
env: process.env,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log('Created terminal with PID: ' + term.pid);
|
|
66
|
+
terminals[dir] = term;
|
|
67
|
+
logs[term.pid] = [];
|
|
68
|
+
term.on('data', function(data) {
|
|
69
|
+
const arr = logs[term.pid];
|
|
70
|
+
arr.push(data);
|
|
71
|
+
if (arr.length > 100) logs[term.pid] = arr.slice(arr.length - 100);
|
|
72
|
+
});
|
|
73
|
+
res.end(JSON.stringify({ pid: term.pid }));
|
|
74
|
+
res.end();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Resize the terminal
|
|
78
|
+
app.post('/api/terminals/:pid/size', function(req, res) {
|
|
79
|
+
const pid = parseInt(req.params.pid, 10);
|
|
80
|
+
const { cols, rows } = req.body;
|
|
81
|
+
const term = Object.values(terminals).find((t) => t.pid === pid);
|
|
82
|
+
if (!term) {
|
|
83
|
+
res.end(`No terminal with PID: ${pid}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
term.resize(cols, rows);
|
|
87
|
+
res.end('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Connect to the terminal
|
|
91
|
+
app.ws('/api/terminals/:pid', function(ws, req) {
|
|
92
|
+
console.log('Connecting to terminal ' + req.params.pid);
|
|
93
|
+
const term = Object.values(terminals).find((t) => t.pid === parseInt(req.params.pid, 10));
|
|
94
|
+
|
|
95
|
+
console.log('Connect to terminal ' + term.pid);
|
|
96
|
+
if (!term) throw new Error('Terminal not found');
|
|
97
|
+
|
|
98
|
+
ws.send(logs[term.pid].join(''));
|
|
99
|
+
|
|
100
|
+
term.on('data', function(data) {
|
|
101
|
+
try {
|
|
102
|
+
ws.send(data);
|
|
103
|
+
} catch (ex) {
|
|
104
|
+
// The WebSocket is not open, ignore
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
ws.on('message', function(msg) {
|
|
108
|
+
term.write(msg);
|
|
109
|
+
});
|
|
110
|
+
// ws.on('close', function () {
|
|
111
|
+
// console.log('Closed terminal ' + term.pid);
|
|
112
|
+
// term.kill();
|
|
113
|
+
// Clean things up
|
|
114
|
+
// delete terminals[term.pid];
|
|
115
|
+
// delete logs[term.pid];
|
|
116
|
+
// });
|
|
117
|
+
});
|
|
118
|
+
}
|