gcf-board 0.2.0 → 0.2.1
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/README.md +9 -1
- package/dist/server/desktop.js +67 -0
- package/dist/server/main.js +29 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,15 @@ gcf-board # http://127.0.0.1:4317
|
|
|
17
17
|
gcf-board --port 4400
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
It
|
|
20
|
+
It opens the board in your browser (the URL with the token, `http://127.0.0.1:4317/?t=<token>`, is printed too; `--no-open` skips the browser). Started again while it runs, it just opens the running board. Add a project by its folder (one that has `gcf/`) under **Projects**.
|
|
21
|
+
|
|
22
|
+
**Desktop shortcut**: run once
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gcf-board shortcut # or: gcf-board shortcut --port 4400
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It puts **GCF Board** on the desktop (Windows: a shortcut whose server console starts minimized; macOS: `GCF Board.command`; Linux: an application entry). Double-click it: the board starts and opens, or opens if it already runs. Run the command again after moving Node.js.
|
|
21
29
|
|
|
22
30
|
## What it does
|
|
23
31
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The board without a console: open it in the browser, and a desktop shortcut that starts it.
|
|
2
|
+
import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
const NAME = 'GCF Board';
|
|
7
|
+
/** Open a URL in the default browser; best effort, the URL is printed anyway. */
|
|
8
|
+
export function openInBrowser(url) {
|
|
9
|
+
const [cmd, args] = process.platform === 'win32'
|
|
10
|
+
? ['cmd', ['/c', 'start', '""', url]]
|
|
11
|
+
: process.platform === 'darwin'
|
|
12
|
+
? ['open', [url]]
|
|
13
|
+
: ['xdg-open', [url]];
|
|
14
|
+
try {
|
|
15
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// No browser to open: the operator opens the printed URL.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** A PowerShell single-quoted string: a quote inside is doubled. */
|
|
22
|
+
const psQuote = (s) => `'${s.replaceAll("'", "''")}'`;
|
|
23
|
+
export function shortcutPlan(platform, node, main, port) {
|
|
24
|
+
const args = ['--port', String(port)];
|
|
25
|
+
if (platform === 'win32')
|
|
26
|
+
return {
|
|
27
|
+
// A .lnk is a binary file: WScript.Shell writes it. Minimized: the server's console stays out
|
|
28
|
+
// of the way.
|
|
29
|
+
powershell: [
|
|
30
|
+
`$d = [Environment]::GetFolderPath('Desktop')`,
|
|
31
|
+
`$s = (New-Object -ComObject WScript.Shell).CreateShortcut((Join-Path $d '${NAME}.lnk'))`,
|
|
32
|
+
`$s.TargetPath = ${psQuote(node)}`,
|
|
33
|
+
`$s.Arguments = ${psQuote(`"${main}" ${args.join(' ')}`)}`,
|
|
34
|
+
`$s.WindowStyle = 7`,
|
|
35
|
+
`$s.Description = 'Start the GCF board and open it in the browser'`,
|
|
36
|
+
`$s.Save()`,
|
|
37
|
+
`Join-Path $d '${NAME}.lnk'`,
|
|
38
|
+
].join('; '),
|
|
39
|
+
};
|
|
40
|
+
const command = `"${node}" "${main}" ${args.join(' ')}`;
|
|
41
|
+
if (platform === 'darwin')
|
|
42
|
+
return {
|
|
43
|
+
file: path.join(os.homedir(), 'Desktop', `${NAME}.command`),
|
|
44
|
+
text: `#!/bin/sh\nexec ${command}\n`,
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
file: path.join(os.homedir(), '.local', 'share', 'applications', 'gcf-board.desktop'),
|
|
48
|
+
text: `[Desktop Entry]\nType=Application\nName=${NAME}\nExec=${command}\nTerminal=false\n` +
|
|
49
|
+
'Categories=Development;\n',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Write the shortcut for this machine; returns where it is. */
|
|
53
|
+
export function createShortcut(main, port) {
|
|
54
|
+
const plan = shortcutPlan(process.platform, process.execPath, main, port);
|
|
55
|
+
if ('powershell' in plan) {
|
|
56
|
+
const r = spawnSync('powershell', ['-NoProfile', '-Command', plan.powershell], {
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
});
|
|
59
|
+
if (r.status !== 0)
|
|
60
|
+
throw new Error(`the shortcut was not made: ${r.stderr.trim()}`);
|
|
61
|
+
return r.stdout.trim();
|
|
62
|
+
}
|
|
63
|
+
mkdirSync(path.dirname(plan.file), { recursive: true });
|
|
64
|
+
writeFileSync(plan.file, plan.text);
|
|
65
|
+
chmodSync(plan.file, 0o755);
|
|
66
|
+
return plan.file;
|
|
67
|
+
}
|
package/dist/server/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `gcf-board [--port <n>]`: the board on http://127.0.0.1:<port>, 4317 by default
|
|
3
|
-
//
|
|
2
|
+
// `gcf-board [--port <n>] [--no-open]`: the board on http://127.0.0.1:<port>, 4317 by default,
|
|
3
|
+
// opened in the browser with this machine's token. A board already on the port is opened, not
|
|
4
|
+
// started twice. `gcf-board shortcut [--port <n>]` puts a desktop shortcut that does the same.
|
|
4
5
|
import { existsSync } from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -8,6 +9,7 @@ import { parseArgs } from 'node:util';
|
|
|
8
9
|
import { boardToken } from './auth.js';
|
|
9
10
|
import { aiFactoryEntry } from './cli.js';
|
|
10
11
|
import { createApp } from './app.js';
|
|
12
|
+
import { createShortcut, openInBrowser } from './desktop.js';
|
|
11
13
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
14
|
/**
|
|
13
15
|
* The built page: `dist/web` beside `server/` when run from the source (the source `web/` is
|
|
@@ -20,14 +22,37 @@ function webDir() {
|
|
|
20
22
|
: path.resolve(HERE, '..', 'web');
|
|
21
23
|
}
|
|
22
24
|
async function main() {
|
|
23
|
-
const { values } = parseArgs({
|
|
25
|
+
const { values, positionals } = parseArgs({
|
|
26
|
+
allowPositionals: true,
|
|
27
|
+
options: {
|
|
28
|
+
port: { type: 'string', default: '4317' },
|
|
29
|
+
'no-open': { type: 'boolean', default: false },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
24
32
|
const port = Number(values.port);
|
|
25
33
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
26
34
|
throw new Error('--port is a number from 1 to 65535');
|
|
35
|
+
if (positionals[0] === 'shortcut') {
|
|
36
|
+
console.log(`gcf-board: shortcut ${createShortcut(fileURLToPath(import.meta.url), port)}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (positionals.length)
|
|
40
|
+
throw new Error(`unknown command: ${positionals.join(' ')}`);
|
|
27
41
|
const token = await boardToken();
|
|
42
|
+
const url = `http://127.0.0.1:${port}/?t=${token}`;
|
|
43
|
+
const open = () => !values['no-open'] && openInBrowser(url);
|
|
28
44
|
const app = createApp({ entry: aiFactoryEntry(), token, port, web: webDir() });
|
|
45
|
+
app.on('error', (e) => {
|
|
46
|
+
if (e.code !== 'EADDRINUSE')
|
|
47
|
+
throw e;
|
|
48
|
+
// A second start, from the shortcut most likely: the board runs already, show it.
|
|
49
|
+
console.log(`gcf-board: already running on port ${port}, opening it`);
|
|
50
|
+
open();
|
|
51
|
+
setTimeout(() => process.exit(0), 500);
|
|
52
|
+
});
|
|
29
53
|
app.listen(port, '127.0.0.1', () => {
|
|
30
|
-
console.log(`gcf-board:
|
|
54
|
+
console.log(`gcf-board: ${url}`);
|
|
55
|
+
open();
|
|
31
56
|
});
|
|
32
57
|
}
|
|
33
58
|
main().catch((e) => {
|
package/package.json
CHANGED