beads-ui 0.5.0 → 0.7.0
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/CHANGES.md +42 -0
- package/README.md +4 -2
- package/app/main.bundle.js +232 -203
- package/app/main.bundle.js.map +3 -3
- package/app/protocol.js +5 -2
- package/app/styles.css +64 -0
- package/package.json +1 -1
- package/server/bd.js +32 -0
- package/server/cli/commands.js +14 -4
- package/server/cli/daemon.js +11 -2
- package/server/cli/index.js +29 -11
- package/server/cli/usage.js +5 -3
- package/server/config.js +2 -1
- package/server/index.js +10 -0
- package/server/ws.js +73 -1
package/app/protocol.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Server can also send unsolicited events (e.g., subscription `snapshot`).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
/** @typedef {'list-issues'|'update-status'|'edit-text'|'update-priority'|'create-issue'|'list-ready'|'dep-add'|'dep-remove'|'epic-status'|'update-assignee'|'label-add'|'label-remove'|'subscribe-list'|'unsubscribe-list'|'snapshot'|'upsert'|'delete'} MessageType */
|
|
12
|
+
/** @typedef {'list-issues'|'update-status'|'edit-text'|'update-priority'|'create-issue'|'list-ready'|'dep-add'|'dep-remove'|'epic-status'|'update-assignee'|'label-add'|'label-remove'|'subscribe-list'|'unsubscribe-list'|'snapshot'|'upsert'|'delete'|'get-comments'|'add-comment'} MessageType */
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {Object} RequestEnvelope
|
|
@@ -53,7 +53,10 @@ export const MESSAGE_TYPES = /** @type {const} */ ([
|
|
|
53
53
|
// vNext per-subscription full-issue push events
|
|
54
54
|
'snapshot',
|
|
55
55
|
'upsert',
|
|
56
|
-
'delete'
|
|
56
|
+
'delete',
|
|
57
|
+
// Comments
|
|
58
|
+
'get-comments',
|
|
59
|
+
'add-comment'
|
|
57
60
|
]);
|
|
58
61
|
|
|
59
62
|
/**
|
package/app/styles.css
CHANGED
|
@@ -1656,3 +1656,67 @@ html[data-theme='dark'] {
|
|
|
1656
1656
|
grid-template-columns: 1fr;
|
|
1657
1657
|
}
|
|
1658
1658
|
}
|
|
1659
|
+
|
|
1660
|
+
/* Comments section */
|
|
1661
|
+
.comments {
|
|
1662
|
+
margin-top: 24px;
|
|
1663
|
+
padding-top: 16px;
|
|
1664
|
+
border-top: 1px solid var(--border);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
.comment-item {
|
|
1668
|
+
padding: 12px;
|
|
1669
|
+
margin-bottom: 8px;
|
|
1670
|
+
background: color-mix(in srgb, var(--panel-bg) 95%, transparent);
|
|
1671
|
+
border: 1px solid var(--border);
|
|
1672
|
+
border-radius: 4px;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.comment-header {
|
|
1676
|
+
display: flex;
|
|
1677
|
+
justify-content: space-between;
|
|
1678
|
+
align-items: center;
|
|
1679
|
+
margin-bottom: 6px;
|
|
1680
|
+
font-size: 12px;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
.comment-author {
|
|
1684
|
+
font-weight: 600;
|
|
1685
|
+
color: var(--fg);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.comment-date {
|
|
1689
|
+
color: var(--muted);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
.comment-text {
|
|
1693
|
+
font-size: 14px;
|
|
1694
|
+
line-height: 1.5;
|
|
1695
|
+
white-space: pre-wrap;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.comment-input {
|
|
1699
|
+
margin-top: 12px;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
.comment-input textarea {
|
|
1703
|
+
width: 100%;
|
|
1704
|
+
min-height: 60px;
|
|
1705
|
+
padding: 8px;
|
|
1706
|
+
border: 1px solid var(--control-border);
|
|
1707
|
+
border-radius: 4px;
|
|
1708
|
+
background: var(--control-bg);
|
|
1709
|
+
color: var(--fg);
|
|
1710
|
+
font-family: inherit;
|
|
1711
|
+
font-size: 14px;
|
|
1712
|
+
resize: vertical;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
.comment-input textarea:focus {
|
|
1716
|
+
outline: none;
|
|
1717
|
+
border-color: var(--link);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
.comment-input button {
|
|
1721
|
+
margin-top: 8px;
|
|
1722
|
+
}
|
package/package.json
CHANGED
package/server/bd.js
CHANGED
|
@@ -4,6 +4,38 @@ import { debug } from './logging.js';
|
|
|
4
4
|
|
|
5
5
|
const log = debug('bd');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Get the git user name from git config.
|
|
9
|
+
*
|
|
10
|
+
* @param {{ cwd?: string }} [options]
|
|
11
|
+
* @returns {Promise<string>}
|
|
12
|
+
*/
|
|
13
|
+
export async function getGitUserName(options = {}) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const child = spawn('git', ['config', 'user.name'], {
|
|
16
|
+
cwd: options.cwd || process.cwd(),
|
|
17
|
+
shell: false
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** @type {string[]} */
|
|
21
|
+
const chunks = [];
|
|
22
|
+
|
|
23
|
+
if (child.stdout) {
|
|
24
|
+
child.stdout.setEncoding('utf8');
|
|
25
|
+
child.stdout.on('data', (chunk) => chunks.push(String(chunk)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
child.on('error', () => resolve(''));
|
|
29
|
+
child.on('close', (code) => {
|
|
30
|
+
if (code !== 0) {
|
|
31
|
+
resolve('');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
resolve(chunks.join('').trim());
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
7
39
|
/**
|
|
8
40
|
* Resolve the bd executable path.
|
|
9
41
|
*
|
package/server/cli/commands.js
CHANGED
|
@@ -14,11 +14,9 @@ import { openUrl, waitForServer } from './open.js';
|
|
|
14
14
|
* - Spawns a detached server process, writes PID file, returns 0.
|
|
15
15
|
* - If already running (PID file present and process alive), prints URL and returns 0.
|
|
16
16
|
*
|
|
17
|
+
* @param {{ open?: boolean, is_debug?: boolean, host?: string, port?: number }} [options]
|
|
17
18
|
* @returns {Promise<number>} Exit code (0 on success)
|
|
18
19
|
*/
|
|
19
|
-
/**
|
|
20
|
-
* @param {{ open?: boolean, is_debug?: boolean }} [options]
|
|
21
|
-
*/
|
|
22
20
|
export async function handleStart(options) {
|
|
23
21
|
// Default: do not open a browser unless explicitly requested via `open: true`.
|
|
24
22
|
const should_open = options?.open === true;
|
|
@@ -32,7 +30,19 @@ export async function handleStart(options) {
|
|
|
32
30
|
removePidFile();
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
// Set env vars in current process so getConfig() reflects the overrides
|
|
34
|
+
if (options?.host) {
|
|
35
|
+
process.env.HOST = options.host;
|
|
36
|
+
}
|
|
37
|
+
if (options?.port) {
|
|
38
|
+
process.env.PORT = String(options.port);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const started = startDaemon({
|
|
42
|
+
is_debug: options?.is_debug,
|
|
43
|
+
host: options?.host,
|
|
44
|
+
port: options?.port
|
|
45
|
+
});
|
|
36
46
|
if (started && started.pid > 0) {
|
|
37
47
|
printServerUrl();
|
|
38
48
|
// Auto-open the browser once for a fresh daemon start
|
package/server/cli/daemon.js
CHANGED
|
@@ -140,7 +140,7 @@ export function getServerEntryPath() {
|
|
|
140
140
|
* Spawn the server as a detached daemon, redirecting stdio to the log file.
|
|
141
141
|
* Writes the PID file upon success.
|
|
142
142
|
*
|
|
143
|
-
* @param {{ is_debug?: boolean }} [options]
|
|
143
|
+
* @param {{ is_debug?: boolean, host?: string, port?: number }} [options]
|
|
144
144
|
* @returns {{ pid: number } | null} Returns child PID on success; null on failure.
|
|
145
145
|
*/
|
|
146
146
|
export function startDaemon(options = {}) {
|
|
@@ -160,10 +160,19 @@ export function startDaemon(options = {}) {
|
|
|
160
160
|
log_fd = -1;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** @type {Record<string, string | undefined>} */
|
|
164
|
+
const spawn_env = { ...process.env };
|
|
165
|
+
if (options.host) {
|
|
166
|
+
spawn_env.HOST = options.host;
|
|
167
|
+
}
|
|
168
|
+
if (options.port) {
|
|
169
|
+
spawn_env.PORT = String(options.port);
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
/** @type {SpawnOptions} */
|
|
164
173
|
const opts = {
|
|
165
174
|
detached: true,
|
|
166
|
-
env:
|
|
175
|
+
env: spawn_env,
|
|
167
176
|
stdio: log_fd >= 0 ? ['ignore', log_fd, log_fd] : 'ignore',
|
|
168
177
|
windowsHide: true
|
|
169
178
|
};
|
package/server/cli/index.js
CHANGED
|
@@ -3,18 +3,21 @@ import { handleRestart, handleStart, handleStop } from './commands.js';
|
|
|
3
3
|
import { printUsage } from './usage.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Parse argv into a command token and
|
|
6
|
+
* Parse argv into a command token, flags, and options.
|
|
7
7
|
*
|
|
8
8
|
* @param {string[]} args
|
|
9
|
-
* @returns {{ command: string | null, flags: string[] }}
|
|
9
|
+
* @returns {{ command: string | null, flags: string[], options: { host?: string, port?: number } }}
|
|
10
10
|
*/
|
|
11
11
|
export function parseArgs(args) {
|
|
12
12
|
/** @type {string[]} */
|
|
13
13
|
const flags = [];
|
|
14
14
|
/** @type {string | null} */
|
|
15
15
|
let command = null;
|
|
16
|
+
/** @type {{ host?: string, port?: number }} */
|
|
17
|
+
const options = {};
|
|
16
18
|
|
|
17
|
-
for (
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const token = args[i];
|
|
18
21
|
if (token === '--help' || token === '-h') {
|
|
19
22
|
flags.push('help');
|
|
20
23
|
continue;
|
|
@@ -27,6 +30,17 @@ export function parseArgs(args) {
|
|
|
27
30
|
flags.push('open');
|
|
28
31
|
continue;
|
|
29
32
|
}
|
|
33
|
+
if (token === '--host' && i + 1 < args.length) {
|
|
34
|
+
options.host = args[++i];
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (token === '--port' && i + 1 < args.length) {
|
|
38
|
+
const port_value = Number.parseInt(args[++i], 10);
|
|
39
|
+
if (Number.isFinite(port_value) && port_value > 0) {
|
|
40
|
+
options.port = port_value;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
30
44
|
if (
|
|
31
45
|
!command &&
|
|
32
46
|
(token === 'start' || token === 'stop' || token === 'restart')
|
|
@@ -37,7 +51,7 @@ export function parseArgs(args) {
|
|
|
37
51
|
// Ignore unrecognized tokens for now; future flags may be parsed here.
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
return { command, flags };
|
|
54
|
+
return { command, flags, options };
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
@@ -48,7 +62,7 @@ export function parseArgs(args) {
|
|
|
48
62
|
* @returns {Promise<number>}
|
|
49
63
|
*/
|
|
50
64
|
export async function main(args) {
|
|
51
|
-
const { command, flags } = parseArgs(args);
|
|
65
|
+
const { command, flags, options } = parseArgs(args);
|
|
52
66
|
|
|
53
67
|
const is_debug = flags.includes('debug');
|
|
54
68
|
if (is_debug) {
|
|
@@ -68,21 +82,25 @@ export async function main(args) {
|
|
|
68
82
|
/**
|
|
69
83
|
* Default behavior: do NOT open a browser. `--open` explicitly opens.
|
|
70
84
|
*/
|
|
71
|
-
const
|
|
85
|
+
const start_options = {
|
|
72
86
|
open: flags.includes('open'),
|
|
73
|
-
is_debug: is_debug || Boolean(process.env.DEBUG)
|
|
87
|
+
is_debug: is_debug || Boolean(process.env.DEBUG),
|
|
88
|
+
host: options.host,
|
|
89
|
+
port: options.port
|
|
74
90
|
};
|
|
75
|
-
return await handleStart(
|
|
91
|
+
return await handleStart(start_options);
|
|
76
92
|
}
|
|
77
93
|
if (command === 'stop') {
|
|
78
94
|
return await handleStop();
|
|
79
95
|
}
|
|
80
96
|
if (command === 'restart') {
|
|
81
|
-
const
|
|
97
|
+
const restart_options = {
|
|
82
98
|
open: flags.includes('open'),
|
|
83
|
-
is_debug: is_debug || Boolean(process.env.DEBUG)
|
|
99
|
+
is_debug: is_debug || Boolean(process.env.DEBUG),
|
|
100
|
+
host: options.host,
|
|
101
|
+
port: options.port
|
|
84
102
|
};
|
|
85
|
-
return await handleRestart(
|
|
103
|
+
return await handleRestart(restart_options);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
// Unknown command path (should not happen due to parseArgs guard)
|
package/server/cli/usage.js
CHANGED
|
@@ -13,9 +13,11 @@ export function printUsage(out_stream) {
|
|
|
13
13
|
' restart Restart the UI server',
|
|
14
14
|
'',
|
|
15
15
|
'Options:',
|
|
16
|
-
' -h, --help
|
|
17
|
-
' -d, --debug
|
|
18
|
-
' --open
|
|
16
|
+
' -h, --help Show this help message',
|
|
17
|
+
' -d, --debug Enable debug logging',
|
|
18
|
+
' --open Open the browser after start/restart',
|
|
19
|
+
' --host <addr> Bind to a specific host (default: 127.0.0.1)',
|
|
20
|
+
' --port <num> Bind to a specific port (default: 3000)',
|
|
19
21
|
''
|
|
20
22
|
];
|
|
21
23
|
for (const line of lines) {
|
package/server/config.js
CHANGED
package/server/index.js
CHANGED
|
@@ -10,6 +10,16 @@ if (process.argv.includes('--debug') || process.argv.includes('-d')) {
|
|
|
10
10
|
enableAllDebug();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Parse --host and --port from argv and set env vars before getConfig()
|
|
14
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
15
|
+
if (process.argv[i] === '--host' && process.argv[i + 1]) {
|
|
16
|
+
process.env.HOST = process.argv[++i];
|
|
17
|
+
}
|
|
18
|
+
if (process.argv[i] === '--port' && process.argv[i + 1]) {
|
|
19
|
+
process.env.PORT = process.argv[++i];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
const config = getConfig();
|
|
14
24
|
const app = createApp(config);
|
|
15
25
|
const server = createServer(app);
|
package/server/ws.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { WebSocketServer } from 'ws';
|
|
7
7
|
import { isRequest, makeError, makeOk } from '../app/protocol.js';
|
|
8
|
-
import { runBd, runBdJson } from './bd.js';
|
|
8
|
+
import { getGitUserName, runBd, runBdJson } from './bd.js';
|
|
9
9
|
import { fetchListForSubscription } from './list-adapters.js';
|
|
10
10
|
import { debug } from './logging.js';
|
|
11
11
|
import { keyOf, registry } from './subscriptions.js';
|
|
@@ -1081,6 +1081,78 @@ export async function handleMessage(ws, data) {
|
|
|
1081
1081
|
return;
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
1084
|
+
// get-comments: payload { id: string }
|
|
1085
|
+
if (req.type === 'get-comments') {
|
|
1086
|
+
const { id } = /** @type {any} */ (req.payload || {});
|
|
1087
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
1088
|
+
ws.send(
|
|
1089
|
+
JSON.stringify(
|
|
1090
|
+
makeError(req, 'bad_request', 'payload requires { id: string }')
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const res = await runBdJson(['comments', id, '--json']);
|
|
1096
|
+
if (res.code !== 0) {
|
|
1097
|
+
ws.send(
|
|
1098
|
+
JSON.stringify(makeError(req, 'bd_error', res.stderr || 'bd failed'))
|
|
1099
|
+
);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
ws.send(JSON.stringify(makeOk(req, res.stdoutJson || [])));
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// add-comment: payload { id: string, text: string }
|
|
1107
|
+
if (req.type === 'add-comment') {
|
|
1108
|
+
const { id, text } = /** @type {any} */ (req.payload || {});
|
|
1109
|
+
if (
|
|
1110
|
+
typeof id !== 'string' ||
|
|
1111
|
+
id.length === 0 ||
|
|
1112
|
+
typeof text !== 'string' ||
|
|
1113
|
+
text.trim().length === 0
|
|
1114
|
+
) {
|
|
1115
|
+
ws.send(
|
|
1116
|
+
JSON.stringify(
|
|
1117
|
+
makeError(
|
|
1118
|
+
req,
|
|
1119
|
+
'bad_request',
|
|
1120
|
+
'payload requires { id: string, text: non-empty string }'
|
|
1121
|
+
)
|
|
1122
|
+
)
|
|
1123
|
+
);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Get git user name for author attribution
|
|
1128
|
+
const author = await getGitUserName();
|
|
1129
|
+
const args = ['comment', id, text.trim()];
|
|
1130
|
+
if (author) {
|
|
1131
|
+
args.push('--author', author);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const res = await runBd(args);
|
|
1135
|
+
if (res.code !== 0) {
|
|
1136
|
+
ws.send(
|
|
1137
|
+
JSON.stringify(makeError(req, 'bd_error', res.stderr || 'bd failed'))
|
|
1138
|
+
);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Return updated comments list
|
|
1143
|
+
const comments = await runBdJson(['comments', id, '--json']);
|
|
1144
|
+
if (comments.code !== 0) {
|
|
1145
|
+
ws.send(
|
|
1146
|
+
JSON.stringify(
|
|
1147
|
+
makeError(req, 'bd_error', comments.stderr || 'bd failed')
|
|
1148
|
+
)
|
|
1149
|
+
);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
ws.send(JSON.stringify(makeOk(req, comments.stdoutJson || [])));
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1084
1156
|
// Unknown type
|
|
1085
1157
|
const err = makeError(
|
|
1086
1158
|
req,
|