@crab-dev/wake 0.1.19 → 0.1.20
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/CHANGELOG.md +8 -0
- package/README.md +9 -0
- package/bin/console.mjs +247 -0
- package/bin/terminal.mjs +276 -43
- package/bin/wake.mjs +1 -1
- package/index.cjs +1 -5
- package/index.d.ts +15 -0
- package/package.json +11 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.20
|
|
4
|
+
|
|
5
|
+
- Added a modern interactive terminal console with editable commands, history, Unicode-aware selection, clipboard copy and paste, and safe development-server opening across Rust and npm CLIs.
|
|
6
|
+
- Added source-located compiler diagnostics with numbered code frames across Rust and npm build, bundle, watch, and development terminals.
|
|
7
|
+
- Added binding-aware Crab CSS semantic values, scoped automatic property and value completions, and deterministic suggestion ranking in the VS Code language service.
|
|
8
|
+
- Made Wake Docs accent colors optional and confined accent-derived component tokens to Demo previews without rewriting global workbench semantics.
|
|
9
|
+
- Preserved side effects inside `void` expressions during minification so editor and application commands cannot be deleted.
|
|
10
|
+
|
|
3
11
|
## 0.1.19
|
|
4
12
|
|
|
5
13
|
- Fixed preserve-module CommonJS output so every generated file defines the default and namespace interop helpers it uses.
|
package/README.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
Wake is a Rust-native web builder exposed as both a Node.js library and the
|
|
4
4
|
`wake` command.
|
|
5
5
|
|
|
6
|
+
Interactive `dev` and `docs dev` sessions use the full-screen TUI when stdin
|
|
7
|
+
and stderr are terminals. The command line accepts `help`, `clear`, `open`,
|
|
8
|
+
and `quit` (with optional `/` prefixes), keeps in-memory history, supports
|
|
9
|
+
paste, and copies mouse-selected screen text to the clipboard. Use
|
|
10
|
+
`--ui plain` for stable non-interactive logs; Ctrl-C always interrupts.
|
|
11
|
+
Compiler failures include `path:line:column`, a numbered source line, and a caret when the diagnostic
|
|
12
|
+
has a valid source span. The same structured location is available from Node build errors and server
|
|
13
|
+
`diagnostic` events.
|
|
14
|
+
|
|
6
15
|
```sh
|
|
7
16
|
npm install --save-dev @crab-dev/wake
|
|
8
17
|
npx wake build
|
package/bin/console.mjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder'
|
|
2
|
+
import stringWidth from 'string-width'
|
|
3
|
+
|
|
4
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
5
|
+
const MAX_INPUT = 4096
|
|
6
|
+
const MAX_HISTORY = 50
|
|
7
|
+
|
|
8
|
+
export function graphemes(value) {
|
|
9
|
+
return [...segmenter.segment(String(value))].map((part) => part.segment)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseConsoleCommand(value) {
|
|
13
|
+
const original = String(value).trim()
|
|
14
|
+
if (!original) throw new Error('Enter a command. Type help to list available commands.')
|
|
15
|
+
const normalized = (original.startsWith('/') ? original.slice(1) : original).toLowerCase()
|
|
16
|
+
if (normalized === 'help') return 'help'
|
|
17
|
+
if (normalized === 'clear') return 'clear'
|
|
18
|
+
if (normalized === 'open') return 'open'
|
|
19
|
+
if (normalized === 'quit' || normalized === 'q') return 'quit'
|
|
20
|
+
throw new Error(`Unknown command: ${original}. Type help for available commands.`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class InputEditor {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.value = ''
|
|
26
|
+
this.cursor = 0
|
|
27
|
+
this.history = []
|
|
28
|
+
this.historyIndex = undefined
|
|
29
|
+
this.draft = ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clear() {
|
|
33
|
+
this.value = ''
|
|
34
|
+
this.cursor = 0
|
|
35
|
+
this.historyIndex = undefined
|
|
36
|
+
this.draft = ''
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
insert(value) {
|
|
40
|
+
const current = graphemes(this.value)
|
|
41
|
+
const addition = graphemes(value).slice(0, Math.max(0, MAX_INPUT - current.length))
|
|
42
|
+
current.splice(this.cursor, 0, ...addition)
|
|
43
|
+
this.value = current.join('')
|
|
44
|
+
this.cursor += addition.length
|
|
45
|
+
this.historyIndex = undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
insertPaste(value) {
|
|
49
|
+
this.insert(String(value).replace(/[\r\n\t\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, ' '))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
moveLeft() { this.cursor = Math.max(0, this.cursor - 1) }
|
|
53
|
+
moveRight() { this.cursor = Math.min(graphemes(this.value).length, this.cursor + 1) }
|
|
54
|
+
moveHome() { this.cursor = 0 }
|
|
55
|
+
moveEnd() { this.cursor = graphemes(this.value).length }
|
|
56
|
+
|
|
57
|
+
backspace() {
|
|
58
|
+
if (this.cursor === 0) return
|
|
59
|
+
const values = graphemes(this.value)
|
|
60
|
+
values.splice(this.cursor - 1, 1)
|
|
61
|
+
this.value = values.join('')
|
|
62
|
+
this.cursor -= 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
delete() {
|
|
66
|
+
const values = graphemes(this.value)
|
|
67
|
+
if (this.cursor >= values.length) return
|
|
68
|
+
values.splice(this.cursor, 1)
|
|
69
|
+
this.value = values.join('')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
historyPrevious() {
|
|
73
|
+
if (this.history.length === 0) return
|
|
74
|
+
if (this.historyIndex === undefined) {
|
|
75
|
+
this.draft = this.value
|
|
76
|
+
this.historyIndex = this.history.length - 1
|
|
77
|
+
} else {
|
|
78
|
+
this.historyIndex = Math.max(0, this.historyIndex - 1)
|
|
79
|
+
}
|
|
80
|
+
this.value = this.history[this.historyIndex]
|
|
81
|
+
this.moveEnd()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
historyNext() {
|
|
85
|
+
if (this.historyIndex === undefined) return
|
|
86
|
+
if (this.historyIndex + 1 < this.history.length) {
|
|
87
|
+
this.historyIndex += 1
|
|
88
|
+
this.value = this.history[this.historyIndex]
|
|
89
|
+
} else {
|
|
90
|
+
this.historyIndex = undefined
|
|
91
|
+
this.value = this.draft
|
|
92
|
+
this.draft = ''
|
|
93
|
+
}
|
|
94
|
+
this.moveEnd()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
submit() {
|
|
98
|
+
const submitted = this.value.trim()
|
|
99
|
+
let result
|
|
100
|
+
try {
|
|
101
|
+
result = { command: parseConsoleCommand(submitted) }
|
|
102
|
+
} catch (error) {
|
|
103
|
+
result = { error: error.message }
|
|
104
|
+
}
|
|
105
|
+
if (submitted && this.history.at(-1) !== submitted) {
|
|
106
|
+
this.history.push(submitted)
|
|
107
|
+
if (this.history.length > MAX_HISTORY) this.history.shift()
|
|
108
|
+
}
|
|
109
|
+
this.clear()
|
|
110
|
+
return result
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
cursorCell() {
|
|
114
|
+
return graphemes(this.value).slice(0, this.cursor).reduce((sum, value) => sum + stringWidth(value), 0)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setCursorFromCell(cell) {
|
|
118
|
+
let width = 0
|
|
119
|
+
let cursor = 0
|
|
120
|
+
for (const value of graphemes(this.value)) {
|
|
121
|
+
const next = width + stringWidth(value)
|
|
122
|
+
if (cell < next) break
|
|
123
|
+
width = next
|
|
124
|
+
cursor += 1
|
|
125
|
+
}
|
|
126
|
+
this.cursor = cursor
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
visible(width) {
|
|
130
|
+
if (width <= 0) return { text: '', cursor: 0 }
|
|
131
|
+
const cursor = this.cursorCell()
|
|
132
|
+
const start = cursor >= width ? cursor + 1 - width : 0
|
|
133
|
+
let position = 0
|
|
134
|
+
let text = ''
|
|
135
|
+
for (const value of graphemes(this.value)) {
|
|
136
|
+
const next = position + stringWidth(value)
|
|
137
|
+
if (next > start && position < start + width) text += value
|
|
138
|
+
position = next
|
|
139
|
+
if (position >= start + width) break
|
|
140
|
+
}
|
|
141
|
+
return { text, cursor: Math.min(width, Math.max(0, cursor - start)) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function lineToCells(value, width) {
|
|
146
|
+
const cells = []
|
|
147
|
+
for (const valuePart of graphemes(value)) {
|
|
148
|
+
const cellWidth = Math.max(0, stringWidth(valuePart))
|
|
149
|
+
if (cells.length + cellWidth > width) break
|
|
150
|
+
cells.push(valuePart)
|
|
151
|
+
for (let index = 1; index < cellWidth; index += 1) cells.push('')
|
|
152
|
+
}
|
|
153
|
+
while (cells.length < width) cells.push(' ')
|
|
154
|
+
return cells.slice(0, width)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function orderedSelection(selection) {
|
|
158
|
+
const forward = selection.start.y < selection.end.y
|
|
159
|
+
|| (selection.start.y === selection.end.y && selection.start.x <= selection.end.x)
|
|
160
|
+
return forward ? [selection.start, selection.end] : [selection.end, selection.start]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function selectionContains(selection, x, y) {
|
|
164
|
+
if (!selection) return false
|
|
165
|
+
const [start, end] = orderedSelection(selection)
|
|
166
|
+
if (y < start.y || y > end.y) return false
|
|
167
|
+
return (y !== start.y || x >= start.x) && (y !== end.y || x <= end.x)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function extractSelection(rows, selection) {
|
|
171
|
+
if (!selection || (selection.start.x === selection.end.x && selection.start.y === selection.end.y)) return ''
|
|
172
|
+
const [start, end] = orderedSelection(selection)
|
|
173
|
+
const lines = []
|
|
174
|
+
for (let y = start.y; y <= Math.min(end.y, rows.length - 1); y += 1) {
|
|
175
|
+
const row = rows[y] || []
|
|
176
|
+
const from = y === start.y ? start.x : 0
|
|
177
|
+
const to = y === end.y ? Math.min(end.x, row.length - 1) : row.length - 1
|
|
178
|
+
lines.push(row.slice(from, to + 1).join('').replace(/ +$/u, ''))
|
|
179
|
+
}
|
|
180
|
+
return lines.join('\n').replace(/\n+$/u, '')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const KEY_SEQUENCES = new Map([
|
|
184
|
+
['\x1b[A', 'up'], ['\x1b[B', 'down'], ['\x1b[C', 'right'], ['\x1b[D', 'left'],
|
|
185
|
+
['\x1bOA', 'up'], ['\x1bOB', 'down'], ['\x1bOC', 'right'], ['\x1bOD', 'left'],
|
|
186
|
+
['\x1b[H', 'home'], ['\x1b[F', 'end'], ['\x1b[1~', 'home'], ['\x1b[4~', 'end'],
|
|
187
|
+
['\x1bOH', 'home'], ['\x1bOF', 'end'],
|
|
188
|
+
['\x1b[3~', 'delete'], ['\x1b[5~', 'pageup'], ['\x1b[6~', 'pagedown'],
|
|
189
|
+
['\x1b[1;5F', 'ctrl-end'],
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
export class TerminalInputDecoder {
|
|
193
|
+
constructor() {
|
|
194
|
+
this.decoder = new StringDecoder('utf8')
|
|
195
|
+
this.buffer = ''
|
|
196
|
+
this.paste = false
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
push(chunk) {
|
|
200
|
+
this.buffer += this.decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
201
|
+
const events = []
|
|
202
|
+
while (this.buffer) {
|
|
203
|
+
if (this.paste) {
|
|
204
|
+
const end = this.buffer.indexOf('\x1b[201~')
|
|
205
|
+
if (end === -1) break
|
|
206
|
+
events.push({ type: 'paste', value: this.buffer.slice(0, end) })
|
|
207
|
+
this.buffer = this.buffer.slice(end + 6)
|
|
208
|
+
this.paste = false
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if (this.buffer.startsWith('\x1b[200~')) {
|
|
212
|
+
this.buffer = this.buffer.slice(6)
|
|
213
|
+
this.paste = true
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
const mouse = /^\x1b\[<(\d+);(\d+);(\d+)([mM])/.exec(this.buffer)
|
|
217
|
+
if (mouse) {
|
|
218
|
+
const code = Number(mouse[1])
|
|
219
|
+
const suffix = mouse[4]
|
|
220
|
+
let kind = 'down'
|
|
221
|
+
let button = ['left', 'middle', 'right'][code & 3] || 'left'
|
|
222
|
+
if ((code & 64) !== 0) kind = (code & 1) === 0 ? 'scroll-up' : 'scroll-down'
|
|
223
|
+
else if ((code & 32) !== 0) kind = 'drag'
|
|
224
|
+
else if (suffix === 'm') kind = 'up'
|
|
225
|
+
events.push({ type: 'mouse', kind, button, x: Number(mouse[2]) - 1, y: Number(mouse[3]) - 1 })
|
|
226
|
+
this.buffer = this.buffer.slice(mouse[0].length)
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
const matched = [...KEY_SEQUENCES].find(([sequence]) => this.buffer.startsWith(sequence))
|
|
230
|
+
if (matched) {
|
|
231
|
+
events.push({ type: 'key', key: matched[1] })
|
|
232
|
+
this.buffer = this.buffer.slice(matched[0].length)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
if (this.buffer.startsWith('\x1b') && [...KEY_SEQUENCES.keys(), '\x1b[200~', '\x1b[<'].some((value) => value.startsWith(this.buffer))) break
|
|
236
|
+
const first = this.buffer.codePointAt(0)
|
|
237
|
+
const value = String.fromCodePoint(first)
|
|
238
|
+
this.buffer = this.buffer.slice(value.length)
|
|
239
|
+
const control = {
|
|
240
|
+
'\u0003': 'ctrl-c', '\u0019': 'ctrl-y', '\u0016': 'ctrl-v', '\r': 'enter', '\n': 'enter',
|
|
241
|
+
'\u007f': 'backspace', '\u0008': 'backspace', '\u001b': 'escape',
|
|
242
|
+
}[value]
|
|
243
|
+
events.push(control ? { type: 'key', key: control } : { type: 'text', value })
|
|
244
|
+
}
|
|
245
|
+
return events
|
|
246
|
+
}
|
|
247
|
+
}
|
package/bin/terminal.mjs
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import clipboard from 'clipboardy'
|
|
2
|
+
import open from 'open'
|
|
3
|
+
import stringWidth from 'string-width'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
InputEditor,
|
|
7
|
+
TerminalInputDecoder,
|
|
8
|
+
extractSelection,
|
|
9
|
+
lineToCells,
|
|
10
|
+
selectionContains,
|
|
11
|
+
} from './console.mjs'
|
|
12
|
+
|
|
1
13
|
const RESET = '\x1b[0m'
|
|
2
14
|
const BOLD = '\x1b[1m'
|
|
3
15
|
const DIM = '\x1b[2m'
|
|
@@ -81,7 +93,7 @@ export function formatBuildResult(ui, result, label = 'Built', extra = '') {
|
|
|
81
93
|
]
|
|
82
94
|
if (result.outputDir) lines.push(` ${ui.dim('Output')} ${ui.accent(result.outputDir)}`)
|
|
83
95
|
for (const diagnostic of result.diagnostics || []) {
|
|
84
|
-
lines.push(
|
|
96
|
+
lines.push(...formatDiagnostic(ui, diagnostic).map((line) => ` ${line}`))
|
|
85
97
|
}
|
|
86
98
|
lines.push('')
|
|
87
99
|
return lines
|
|
@@ -119,16 +131,69 @@ export function formatError(ui, error) {
|
|
|
119
131
|
]
|
|
120
132
|
if (error?.path) lines.push(` ${ui.dim('Path')} ${ui.accent(error.path)}`)
|
|
121
133
|
for (const diagnostic of error?.diagnostics || []) {
|
|
122
|
-
|
|
123
|
-
lines.push(
|
|
124
|
-
` ${ui.warn(String(diagnostic.severity || 'error').toUpperCase())} ${diagnosticCode}${diagnostic.message}`,
|
|
125
|
-
)
|
|
126
|
-
for (const note of diagnostic.notes || []) lines.push(` ${ui.dim('·')} ${note}`)
|
|
134
|
+
lines.push(...formatDiagnostic(ui, diagnostic).map((line) => ` ${line}`))
|
|
127
135
|
}
|
|
128
136
|
lines.push('')
|
|
129
137
|
return lines
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
export function formatDiagnostic(ui, diagnostic) {
|
|
141
|
+
const severity = String(diagnostic?.severity || 'error').toUpperCase()
|
|
142
|
+
const heading = diagnostic?.code
|
|
143
|
+
? `${severity} [${diagnostic.code}]: ${diagnostic.message}`
|
|
144
|
+
: `${severity}: ${diagnostic?.message || ''}`
|
|
145
|
+
const lines = [
|
|
146
|
+
diagnostic?.severity === 'warning'
|
|
147
|
+
? ui.warn(heading)
|
|
148
|
+
: diagnostic?.severity === 'error'
|
|
149
|
+
? ui.error(heading)
|
|
150
|
+
: ui.accent(heading),
|
|
151
|
+
]
|
|
152
|
+
const location = diagnostic?.location
|
|
153
|
+
if (diagnostic?.path && location) {
|
|
154
|
+
lines.push(` ${ui.dim('-->')} ${ui.accent(diagnostic.path)}:${location.line}:${location.column}`)
|
|
155
|
+
} else if (diagnostic?.path) {
|
|
156
|
+
lines.push(` ${ui.dim('-->')} ${ui.accent(diagnostic.path)}`)
|
|
157
|
+
}
|
|
158
|
+
if (location) {
|
|
159
|
+
const lineText = expandTabs(String(location.lineText || ''))
|
|
160
|
+
const gutter = String(location.line).length
|
|
161
|
+
const start = displayColumn(String(location.lineText || ''), location.column)
|
|
162
|
+
const end = location.endLine === location.line
|
|
163
|
+
? displayColumn(String(location.lineText || ''), location.endColumn)
|
|
164
|
+
: stringWidth(lineText)
|
|
165
|
+
const width = Math.max(1, end - start)
|
|
166
|
+
const label = location.label ? ` ${location.label}` : ''
|
|
167
|
+
lines.push(`${' '.repeat(gutter)} ${ui.dim('|')}`)
|
|
168
|
+
lines.push(`${String(location.line).padStart(gutter)} ${ui.dim('|')} ${lineText}`)
|
|
169
|
+
lines.push(`${' '.repeat(gutter)} ${ui.dim('|')} ${ui.error(`${' '.repeat(start)}${'^'.repeat(width)}${label}`)}`)
|
|
170
|
+
}
|
|
171
|
+
for (const note of diagnostic?.notes || []) lines.push(` ${ui.dim('=')} note: ${note}`)
|
|
172
|
+
return lines
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function displayColumn(line, oneBasedColumn) {
|
|
176
|
+
const prefix = [...line].slice(0, Math.max(0, Number(oneBasedColumn || 1) - 1)).join('')
|
|
177
|
+
return stringWidth(expandTabs(prefix))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function expandTabs(line) {
|
|
181
|
+
const tabStop = 4
|
|
182
|
+
let width = 0
|
|
183
|
+
let value = ''
|
|
184
|
+
for (const character of line) {
|
|
185
|
+
if (character === '\t') {
|
|
186
|
+
const spaces = tabStop - (width % tabStop)
|
|
187
|
+
value += ' '.repeat(spaces)
|
|
188
|
+
width += spaces
|
|
189
|
+
} else {
|
|
190
|
+
value += character
|
|
191
|
+
width += stringWidth(character)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return value
|
|
195
|
+
}
|
|
196
|
+
|
|
132
197
|
export function formatFinalSummary(ui, state, reason, label = 'Server stopped') {
|
|
133
198
|
const lines = [
|
|
134
199
|
'',
|
|
@@ -161,8 +226,9 @@ export function observeServer(server, ui, output = console) {
|
|
|
161
226
|
)
|
|
162
227
|
}
|
|
163
228
|
const onDiagnostic = (diagnostic) => {
|
|
164
|
-
const
|
|
165
|
-
|
|
229
|
+
for (const [index, line] of formatDiagnostic(ui, diagnostic).entries()) {
|
|
230
|
+
output.error(index === 0 ? ` ${ui.error('✗')} ${ui.bold(line)}` : ` ${line}`)
|
|
231
|
+
}
|
|
166
232
|
}
|
|
167
233
|
|
|
168
234
|
server.on('rebuildStart', onRebuildStart)
|
|
@@ -240,7 +306,7 @@ export function applyDashboardEvent(state, event) {
|
|
|
240
306
|
)
|
|
241
307
|
} else if (event.type === 'diagnostic') {
|
|
242
308
|
state.status = 'error'
|
|
243
|
-
pushActivity(state, 'error', event.
|
|
309
|
+
pushActivity(state, 'error', formatDiagnostic(createUi(false), event.diagnostic).join('\n'))
|
|
244
310
|
} else if (event.type === 'closed') {
|
|
245
311
|
state.status = 'stopped'
|
|
246
312
|
pushActivity(state, 'info', 'Wake stopped')
|
|
@@ -286,14 +352,19 @@ function elapsedStamp(durationMs) {
|
|
|
286
352
|
}
|
|
287
353
|
|
|
288
354
|
function charLength(text) {
|
|
289
|
-
return
|
|
355
|
+
return stringWidth(stripAnsi(String(text)))
|
|
290
356
|
}
|
|
291
357
|
|
|
292
358
|
function truncate(text, width) {
|
|
293
359
|
const chars = [...String(text)]
|
|
294
|
-
if (
|
|
360
|
+
if (charLength(text) <= width) return String(text)
|
|
295
361
|
if (width <= 1) return chars.slice(0, width).join('')
|
|
296
|
-
|
|
362
|
+
let value = ''
|
|
363
|
+
for (const character of chars) {
|
|
364
|
+
if (charLength(value + character) > width - 1) break
|
|
365
|
+
value += character
|
|
366
|
+
}
|
|
367
|
+
return `${value}…`
|
|
297
368
|
}
|
|
298
369
|
|
|
299
370
|
function pad(text, width) {
|
|
@@ -330,15 +401,22 @@ function metricsText(state) {
|
|
|
330
401
|
}
|
|
331
402
|
|
|
332
403
|
function activityRows(state, available) {
|
|
333
|
-
const
|
|
334
|
-
const start = Math.max(0, end - Math.max(1, available))
|
|
335
|
-
return state.activity.slice(start, end).map((item) => {
|
|
404
|
+
const rows = state.activity.flatMap((item) => {
|
|
336
405
|
const symbol = { info: '·', success: '✓', warning: '↻', error: '✗' }[item.level]
|
|
337
|
-
return
|
|
406
|
+
return String(item.message).split('\n').map((line, index) => index === 0
|
|
407
|
+
? `${elapsedStamp(item.elapsedMs)} ${symbol} ${line}`
|
|
408
|
+
: ` ${line}`)
|
|
338
409
|
})
|
|
410
|
+
const end = Math.max(0, rows.length - state.scrollFromBottom)
|
|
411
|
+
const start = Math.max(0, end - Math.max(1, available))
|
|
412
|
+
return rows.slice(start, end)
|
|
339
413
|
}
|
|
340
414
|
|
|
341
|
-
function
|
|
415
|
+
function activityRowCount(state) {
|
|
416
|
+
return state.activity.reduce((count, item) => count + String(item.message).split('\n').length, 0)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function plainFrame(state, width, height, editor = new InputEditor(), notice) {
|
|
342
420
|
width = Math.max(10, width || 80)
|
|
343
421
|
height = Math.max(6, height || 24)
|
|
344
422
|
const runtime = humanRuntime(Date.now() - state.startedAt)
|
|
@@ -348,13 +426,13 @@ function plainFrame(state, width, height) {
|
|
|
348
426
|
let activityHeight
|
|
349
427
|
|
|
350
428
|
if (width < 60 || height < 14) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
boxLine(
|
|
356
|
-
|
|
357
|
-
|
|
429
|
+
const latest = String(state.activity.at(-1)?.message || 'Starting Wake…').split('\n')
|
|
430
|
+
fixed = [topBorder(state, width), boxLine(header, width)]
|
|
431
|
+
const diagnosticRows = latest.length > 1
|
|
432
|
+
for (const line of latest.slice(0, Math.max(1, height - (diagnosticRows ? 5 : 6)))) {
|
|
433
|
+
fixed.push(boxLine(line, width))
|
|
434
|
+
}
|
|
435
|
+
if (!diagnosticRows) fixed.push(boxLine('Resize for details · type help for commands', width))
|
|
358
436
|
activityHeight = 0
|
|
359
437
|
} else if (width < 80 || height < 20) {
|
|
360
438
|
fixed = [
|
|
@@ -381,10 +459,18 @@ function plainFrame(state, width, height) {
|
|
|
381
459
|
}
|
|
382
460
|
|
|
383
461
|
for (const row of activityRows(state, activityHeight)) fixed.push(boxLine(row, width))
|
|
384
|
-
while (
|
|
385
|
-
|
|
462
|
+
while (fixed.length < height - 3) fixed.push(boxLine('', width))
|
|
463
|
+
fixed.splice(Math.max(0, height - 3))
|
|
464
|
+
const visible = editor.visible(Math.max(0, width - 6))
|
|
465
|
+
fixed.push(boxLine(
|
|
466
|
+
notice
|
|
467
|
+
? `${notice.error ? '✗' : '✓'} ${notice.message}`
|
|
468
|
+
: 'Enter command · PgUp/PgDn logs · drag to copy · Ctrl-C quit',
|
|
469
|
+
width,
|
|
470
|
+
))
|
|
471
|
+
fixed.push(boxLine(`› ${visible.text}`, width))
|
|
386
472
|
fixed.push(bottomBorder(width))
|
|
387
|
-
return fixed.slice(0, height)
|
|
473
|
+
return { lines: fixed.slice(0, height), cursor: visible.cursor }
|
|
388
474
|
}
|
|
389
475
|
|
|
390
476
|
function colorizeLine(line, ui) {
|
|
@@ -398,16 +484,67 @@ function colorizeLine(line, ui) {
|
|
|
398
484
|
return value
|
|
399
485
|
}
|
|
400
486
|
|
|
487
|
+
function renderDashboardFrame(
|
|
488
|
+
state,
|
|
489
|
+
width = 80,
|
|
490
|
+
height = 24,
|
|
491
|
+
ui = createUi(false),
|
|
492
|
+
editor = new InputEditor(),
|
|
493
|
+
selection,
|
|
494
|
+
notice,
|
|
495
|
+
) {
|
|
496
|
+
const frame = plainFrame(state, width, height, editor, notice)
|
|
497
|
+
const rows = frame.lines.map((line) => lineToCells(line, width))
|
|
498
|
+
const rendered = rows.map((cells, y) => {
|
|
499
|
+
let selected = false
|
|
500
|
+
let line = ''
|
|
501
|
+
for (let x = 0; x < cells.length; x += 1) {
|
|
502
|
+
const nextSelected = selectionContains(selection, x, y)
|
|
503
|
+
if (nextSelected !== selected) {
|
|
504
|
+
line += nextSelected ? '\x1b[7m' : RESET
|
|
505
|
+
selected = nextSelected
|
|
506
|
+
}
|
|
507
|
+
line += cells[x]
|
|
508
|
+
}
|
|
509
|
+
if (selected) line += RESET
|
|
510
|
+
return colorizeLine(line, ui)
|
|
511
|
+
})
|
|
512
|
+
return {
|
|
513
|
+
text: rendered.join('\n'),
|
|
514
|
+
rows,
|
|
515
|
+
inputY: Math.max(0, rows.length - 2),
|
|
516
|
+
inputX: 4,
|
|
517
|
+
cursorX: Math.min(width - 2, 4 + frame.cursor),
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
401
521
|
export function renderDashboard(state, width = 80, height = 24, ui = createUi(false)) {
|
|
402
|
-
return
|
|
522
|
+
return renderDashboardFrame(state, width, height, ui).text
|
|
403
523
|
}
|
|
404
524
|
|
|
405
525
|
export function createDashboardSession(
|
|
406
526
|
state,
|
|
407
|
-
{
|
|
527
|
+
{
|
|
528
|
+
input = process.stdin,
|
|
529
|
+
output = process.stderr,
|
|
530
|
+
ui = createUi(),
|
|
531
|
+
clipboardAdapter = clipboard,
|
|
532
|
+
openUrl = open,
|
|
533
|
+
env = process.env,
|
|
534
|
+
} = {},
|
|
408
535
|
) {
|
|
409
536
|
let closed = false
|
|
410
537
|
let previousRaw = false
|
|
538
|
+
const editor = new InputEditor()
|
|
539
|
+
const decoder = new TerminalInputDecoder()
|
|
540
|
+
let rows = []
|
|
541
|
+
let inputY = 0
|
|
542
|
+
let inputX = 0
|
|
543
|
+
let selection
|
|
544
|
+
let dragStart
|
|
545
|
+
let lastSelection = ''
|
|
546
|
+
let notice
|
|
547
|
+
let eventQueue = Promise.resolve()
|
|
411
548
|
let resolveExit
|
|
412
549
|
const exit = new Promise((resolve) => { resolveExit = resolve })
|
|
413
550
|
|
|
@@ -415,26 +552,122 @@ export function createDashboardSession(
|
|
|
415
552
|
if (closed) return
|
|
416
553
|
const width = output.columns || 80
|
|
417
554
|
const height = output.rows || 24
|
|
418
|
-
|
|
555
|
+
if (notice && Date.now() - notice.startedAt >= 1500) notice = undefined
|
|
556
|
+
const frame = renderDashboardFrame(state, width, height, ui, editor, selection, notice)
|
|
557
|
+
rows = frame.rows
|
|
558
|
+
inputY = frame.inputY
|
|
559
|
+
inputX = frame.inputX
|
|
560
|
+
output.write(`\x1b[H${frame.text}\x1b[J\x1b[${inputY + 1};${frame.cursorX + 1}H\x1b[?25h`)
|
|
419
561
|
}
|
|
420
562
|
const requestExit = (reason) => {
|
|
421
563
|
if (!closed) resolveExit(reason)
|
|
422
564
|
}
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
565
|
+
const setNotice = (message, error = false) => {
|
|
566
|
+
notice = { message, error, startedAt: Date.now() }
|
|
567
|
+
draw()
|
|
568
|
+
}
|
|
569
|
+
const osc52 = (value) => {
|
|
570
|
+
const sequence = `\x1b]52;c;${Buffer.from(value).toString('base64')}\x07`
|
|
571
|
+
output.write(env.TMUX ? `\x1bPtmux;${sequence.replaceAll('\x1b', '\x1b\x1b')}\x1b\\` : sequence)
|
|
572
|
+
}
|
|
573
|
+
const copyText = async (value) => {
|
|
574
|
+
if (env.SSH_CONNECTION || env.SSH_TTY) {
|
|
575
|
+
osc52(value)
|
|
576
|
+
setNotice('Copied to clipboard')
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
await clipboardAdapter.write(value)
|
|
581
|
+
setNotice('Copied to clipboard')
|
|
582
|
+
} catch {
|
|
583
|
+
try {
|
|
584
|
+
osc52(value)
|
|
585
|
+
setNotice('Copied to clipboard')
|
|
586
|
+
} catch {
|
|
587
|
+
setNotice('Failed to copy; terminal clipboard is unavailable', true)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const pasteClipboard = async () => {
|
|
592
|
+
try {
|
|
593
|
+
editor.insertPaste(await clipboardAdapter.read())
|
|
594
|
+
selection = undefined
|
|
595
|
+
draw()
|
|
596
|
+
} catch {
|
|
597
|
+
setNotice('Clipboard paste is unavailable; use terminal paste', true)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const submit = async () => {
|
|
601
|
+
const result = editor.submit()
|
|
602
|
+
if (result.error) pushActivity(state, 'warning', result.error)
|
|
603
|
+
else if (result.command === 'help') pushActivity(state, 'info', 'Commands: help · clear · open · quit (a leading / is optional)')
|
|
604
|
+
else if (result.command === 'clear') {
|
|
428
605
|
state.activity.length = 0
|
|
429
606
|
state.scrollFromBottom = 0
|
|
430
|
-
|
|
431
|
-
} else if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
607
|
+
setNotice('Activity cleared')
|
|
608
|
+
} else if (result.command === 'open') {
|
|
609
|
+
if (!/^https?:\/\//u.test(state.endpoint)) pushActivity(state, 'warning', 'No development-server URL is available to open')
|
|
610
|
+
else {
|
|
611
|
+
try {
|
|
612
|
+
await openUrl(state.endpoint)
|
|
613
|
+
setNotice('Opened development server')
|
|
614
|
+
} catch (error) {
|
|
615
|
+
pushActivity(state, 'warning', `Failed to open ${state.endpoint}: ${error.message || error}`)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} else if (result.command === 'quit') requestExit('q')
|
|
436
619
|
draw()
|
|
437
620
|
}
|
|
621
|
+
const handleEvent = async (event) => {
|
|
622
|
+
if (event.type === 'paste') editor.insertPaste(event.value)
|
|
623
|
+
else if (event.type === 'text') {
|
|
624
|
+
editor.insert(event.value)
|
|
625
|
+
selection = undefined
|
|
626
|
+
} else if (event.type === 'mouse') {
|
|
627
|
+
const position = { x: event.x, y: event.y }
|
|
628
|
+
if (event.kind === 'down' && event.button === 'left') {
|
|
629
|
+
dragStart = position
|
|
630
|
+
selection = { start: position, end: position }
|
|
631
|
+
} else if (event.kind === 'drag' && dragStart) selection = { start: dragStart, end: position }
|
|
632
|
+
else if (event.kind === 'up' && event.button === 'left' && dragStart) {
|
|
633
|
+
selection = { start: dragStart, end: position }
|
|
634
|
+
dragStart = undefined
|
|
635
|
+
const value = extractSelection(rows, selection)
|
|
636
|
+
if (value) {
|
|
637
|
+
lastSelection = value
|
|
638
|
+
await copyText(value)
|
|
639
|
+
} else {
|
|
640
|
+
if (position.y === inputY) editor.setCursorFromCell(Math.max(0, position.x - inputX))
|
|
641
|
+
selection = undefined
|
|
642
|
+
}
|
|
643
|
+
} else if (event.kind === 'down' && event.button === 'right' && position.y === inputY) await pasteClipboard()
|
|
644
|
+
else if (event.kind === 'scroll-up') state.scrollFromBottom = Math.max(0, Math.min(activityRowCount(state) - 1, state.scrollFromBottom + 3))
|
|
645
|
+
else if (event.kind === 'scroll-down') state.scrollFromBottom = Math.max(0, state.scrollFromBottom - 3)
|
|
646
|
+
} else if (event.type === 'key') {
|
|
647
|
+
if (event.key === 'ctrl-c') requestExit('SIGINT')
|
|
648
|
+
else if (event.key === 'ctrl-y') lastSelection ? await copyText(lastSelection) : setNotice('No selected text to copy', true)
|
|
649
|
+
else if (event.key === 'ctrl-v') await pasteClipboard()
|
|
650
|
+
else if (event.key === 'enter') await submit()
|
|
651
|
+
else if (event.key === 'escape') { editor.clear(); selection = undefined }
|
|
652
|
+
else if (event.key === 'left') editor.moveLeft()
|
|
653
|
+
else if (event.key === 'right') editor.moveRight()
|
|
654
|
+
else if (event.key === 'home') editor.moveHome()
|
|
655
|
+
else if (event.key === 'end') editor.moveEnd()
|
|
656
|
+
else if (event.key === 'ctrl-end') state.scrollFromBottom = 0
|
|
657
|
+
else if (event.key === 'backspace') editor.backspace()
|
|
658
|
+
else if (event.key === 'delete') editor.delete()
|
|
659
|
+
else if (event.key === 'up') editor.historyPrevious()
|
|
660
|
+
else if (event.key === 'down') editor.historyNext()
|
|
661
|
+
else if (event.key === 'pageup') state.scrollFromBottom = Math.max(0, Math.min(activityRowCount(state) - 1, state.scrollFromBottom + 10))
|
|
662
|
+
else if (event.key === 'pagedown') state.scrollFromBottom = Math.max(0, state.scrollFromBottom - 10)
|
|
663
|
+
}
|
|
664
|
+
draw()
|
|
665
|
+
}
|
|
666
|
+
const onData = (chunk) => {
|
|
667
|
+
for (const event of decoder.push(chunk)) {
|
|
668
|
+
eventQueue = eventQueue.then(() => handleEvent(event))
|
|
669
|
+
}
|
|
670
|
+
}
|
|
438
671
|
const onResize = () => draw()
|
|
439
672
|
|
|
440
673
|
previousRaw = input.isRaw === true
|
|
@@ -442,7 +675,7 @@ export function createDashboardSession(
|
|
|
442
675
|
input.resume?.()
|
|
443
676
|
input.on('data', onData)
|
|
444
677
|
output.on?.('resize', onResize)
|
|
445
|
-
output.write('\x1b[?1049h\x1b[?25l\x1b[2J')
|
|
678
|
+
output.write('\x1b[?1049h\x1b[?1002h\x1b[?1006h\x1b[?2004h\x1b[?25l\x1b[2J')
|
|
446
679
|
const timer = setInterval(draw, 100)
|
|
447
680
|
timer.unref?.()
|
|
448
681
|
draw()
|
|
@@ -459,7 +692,7 @@ export function createDashboardSession(
|
|
|
459
692
|
output.off?.('resize', onResize)
|
|
460
693
|
if (typeof input.setRawMode === 'function') input.setRawMode(previousRaw)
|
|
461
694
|
if (!previousRaw) input.pause?.()
|
|
462
|
-
output.write('\x1b[?25h\x1b[?1049l')
|
|
695
|
+
output.write('\x1b[?2004l\x1b[?1006l\x1b[?1002l\x1b[?25h\x1b[?1049l')
|
|
463
696
|
},
|
|
464
697
|
}
|
|
465
698
|
}
|
package/bin/wake.mjs
CHANGED
|
@@ -184,7 +184,7 @@ async function runServer(factory, options, command, root, ui, uiMode) {
|
|
|
184
184
|
dashboard?.draw()
|
|
185
185
|
}
|
|
186
186
|
const onDiagnostic = (diagnostic) => {
|
|
187
|
-
applyDashboardEvent(state, { type: 'diagnostic',
|
|
187
|
+
applyDashboardEvent(state, { type: 'diagnostic', diagnostic })
|
|
188
188
|
dashboard?.draw()
|
|
189
189
|
}
|
|
190
190
|
const onClosed = () => {
|
package/index.cjs
CHANGED
|
@@ -124,11 +124,7 @@ class DevServer extends EventEmitter {
|
|
|
124
124
|
} else if (event.type === 'rebuilt') {
|
|
125
125
|
this.emit('rebuilt', event)
|
|
126
126
|
} else if (event.type === 'diagnostic') {
|
|
127
|
-
this.emit('diagnostic',
|
|
128
|
-
severity: 'error',
|
|
129
|
-
code: 'WAKE_BUILD',
|
|
130
|
-
message: event.message,
|
|
131
|
-
})
|
|
127
|
+
this.emit('diagnostic', event.diagnostic)
|
|
132
128
|
} else if (event.type === 'closed' && !this.#closed) {
|
|
133
129
|
this.#closed = true
|
|
134
130
|
clearInterval(this.#eventTimer)
|
package/index.d.ts
CHANGED
|
@@ -21,6 +21,20 @@ export type WakeErrorCode =
|
|
|
21
21
|
| 'WAKE_LIBRARY_TYPE'
|
|
22
22
|
| 'WAKE_LIBRARY_OUTPUT'
|
|
23
23
|
|
|
24
|
+
export interface DiagnosticLocation {
|
|
25
|
+
/** One-based source line. */
|
|
26
|
+
line: number
|
|
27
|
+
/** One-based Unicode-scalar column. */
|
|
28
|
+
column: number
|
|
29
|
+
/** One-based line containing the exclusive end position. */
|
|
30
|
+
endLine: number
|
|
31
|
+
/** One-based Unicode-scalar column of the exclusive end position. */
|
|
32
|
+
endColumn: number
|
|
33
|
+
/** Exact source line without its line terminator. */
|
|
34
|
+
lineText: string
|
|
35
|
+
label?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
export interface Diagnostic {
|
|
25
39
|
severity: 'error' | 'warning' | 'note' | 'help'
|
|
26
40
|
code?: string
|
|
@@ -28,6 +42,7 @@ export interface Diagnostic {
|
|
|
28
42
|
path?: string
|
|
29
43
|
start?: number
|
|
30
44
|
end?: number
|
|
45
|
+
location?: DiagnosticLocation
|
|
31
46
|
notes?: string[]
|
|
32
47
|
}
|
|
33
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crab-dev/wake",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Wake native web build tools for Node.js",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"node": ">=22.14 <27"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@crab-dev/css": "0.1.20",
|
|
53
54
|
"@crab-dev/rc-alert": "^0.0.2",
|
|
54
55
|
"@crab-dev/rc-button": "^0.0.2",
|
|
55
56
|
"@crab-dev/rc-dialog": "^0.0.2",
|
|
@@ -64,19 +65,21 @@
|
|
|
64
65
|
"@crab-dev/rc-text-edit": "^0.0.1",
|
|
65
66
|
"@crab-dev/rc-tooltip": "^0.0.2",
|
|
66
67
|
"@crab-dev/rc-tree": "^0.1.2",
|
|
67
|
-
"
|
|
68
|
-
"lucide-react": "^1.23.0"
|
|
68
|
+
"clipboardy": "5.3.2",
|
|
69
|
+
"lucide-react": "^1.23.0",
|
|
70
|
+
"open": "11.0.0",
|
|
71
|
+
"string-width": "8.2.2"
|
|
69
72
|
},
|
|
70
73
|
"peerDependencies": {
|
|
71
74
|
"react": "^19.2.8",
|
|
72
75
|
"react-dom": "^19.2.8"
|
|
73
76
|
},
|
|
74
77
|
"optionalDependencies": {
|
|
75
|
-
"@crab-dev/wake-darwin-arm64": "0.1.
|
|
76
|
-
"@crab-dev/wake-darwin-x64": "0.1.
|
|
77
|
-
"@crab-dev/wake-linux-arm64-gnu": "0.1.
|
|
78
|
-
"@crab-dev/wake-linux-x64-gnu": "0.1.
|
|
79
|
-
"@crab-dev/wake-win32-x64-msvc": "0.1.
|
|
78
|
+
"@crab-dev/wake-darwin-arm64": "0.1.20",
|
|
79
|
+
"@crab-dev/wake-darwin-x64": "0.1.20",
|
|
80
|
+
"@crab-dev/wake-linux-arm64-gnu": "0.1.20",
|
|
81
|
+
"@crab-dev/wake-linux-x64-gnu": "0.1.20",
|
|
82
|
+
"@crab-dev/wake-win32-x64-msvc": "0.1.20"
|
|
80
83
|
},
|
|
81
84
|
"publishConfig": {
|
|
82
85
|
"access": "public",
|