remobi 0.1.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/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/dist/build.d.mts +17 -0
- package/dist/build.d.mts.map +1 -0
- package/dist/build.mjs +115 -0
- package/dist/build.mjs.map +1 -0
- package/dist/catppuccin-mocha-CGTshAuT.mjs +48 -0
- package/dist/catppuccin-mocha-CGTshAuT.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +945 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/node-compat-BzXgbTV9.mjs +83 -0
- package/dist/node-compat-BzXgbTV9.mjs.map +1 -0
- package/dist/overlay.iife.js +1 -0
- package/dist/src/config.d.mts +22 -0
- package/dist/src/config.d.mts.map +1 -0
- package/dist/src/config.mjs +384 -0
- package/dist/src/config.mjs.map +1 -0
- package/dist/src/index.d.mts +76 -0
- package/dist/src/index.d.mts.map +1 -0
- package/dist/src/index.mjs +1637 -0
- package/dist/src/index.mjs.map +1 -0
- package/dist/src/types.d.mts +183 -0
- package/dist/src/types.d.mts.map +1 -0
- package/dist/src/types.mjs +1 -0
- package/package.json +88 -0
- package/src/pwa/icons/icon-180.png +0 -0
- package/src/pwa/icons/icon-192.png +0 -0
- package/src/pwa/icons/icon-512.png +0 -0
- package/styles/base.css +475 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as sleep, r as spawnProcess, t as readStdin } from "./node-compat-BzXgbTV9.mjs";
|
|
3
|
+
import { build, bundleOverlay, injectFromStdin, injectOverlay } from "./build.mjs";
|
|
4
|
+
import { defaultConfig, defineConfig, mergeConfig, serialiseThemeForTtyd } from "./src/config.mjs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import * as v from "valibot";
|
|
9
|
+
import { serve } from "@hono/node-server";
|
|
10
|
+
import { createNodeWebSocket } from "@hono/node-ws";
|
|
11
|
+
import { Hono } from "hono";
|
|
12
|
+
import WebSocket from "ws";
|
|
13
|
+
|
|
14
|
+
//#region src/cli/args.ts
|
|
15
|
+
function isHelpCommand(value) {
|
|
16
|
+
return value === "--help" || value === "-h" || value === "help";
|
|
17
|
+
}
|
|
18
|
+
function isVersionCommand(value) {
|
|
19
|
+
return value === "--version" || value === "-v" || value === "version";
|
|
20
|
+
}
|
|
21
|
+
function isMissingOptionValue(value) {
|
|
22
|
+
return value === void 0 || value.startsWith("-");
|
|
23
|
+
}
|
|
24
|
+
function parseCliArgs(args) {
|
|
25
|
+
const commandToken = args[0];
|
|
26
|
+
if (!commandToken || isHelpCommand(commandToken)) return {
|
|
27
|
+
ok: true,
|
|
28
|
+
value: {
|
|
29
|
+
command: "help",
|
|
30
|
+
dryRun: false,
|
|
31
|
+
noSleep: false,
|
|
32
|
+
command_: []
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (isVersionCommand(commandToken)) return {
|
|
36
|
+
ok: true,
|
|
37
|
+
value: {
|
|
38
|
+
command: "version",
|
|
39
|
+
dryRun: false,
|
|
40
|
+
noSleep: false,
|
|
41
|
+
command_: []
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
if (commandToken !== "build" && commandToken !== "inject" && commandToken !== "init" && commandToken !== "serve") return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: `Unknown command: ${commandToken}`
|
|
47
|
+
};
|
|
48
|
+
let configPath;
|
|
49
|
+
let outputPath;
|
|
50
|
+
let dryRun = false;
|
|
51
|
+
let port;
|
|
52
|
+
let noSleep = false;
|
|
53
|
+
let trailingCommand = [];
|
|
54
|
+
for (let index = 1; index < args.length; index++) {
|
|
55
|
+
const arg = args[index];
|
|
56
|
+
const nextArg = args[index + 1];
|
|
57
|
+
if (!arg) return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: "Invalid argument list"
|
|
60
|
+
};
|
|
61
|
+
if (arg === "--") {
|
|
62
|
+
trailingCommand = args.slice(index + 1);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--help" || arg === "-h") return {
|
|
66
|
+
ok: true,
|
|
67
|
+
value: {
|
|
68
|
+
command: "help",
|
|
69
|
+
dryRun: false,
|
|
70
|
+
noSleep: false,
|
|
71
|
+
command_: []
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (!arg.startsWith("-")) return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: `Unexpected positional argument: ${arg}`
|
|
77
|
+
};
|
|
78
|
+
if (arg === "--config" || arg === "-c") {
|
|
79
|
+
if (commandToken !== "build" && commandToken !== "inject" && commandToken !== "serve") return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `${arg} is only valid for 'build', 'inject', or 'serve'`
|
|
82
|
+
};
|
|
83
|
+
if (isMissingOptionValue(nextArg)) return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: "Missing value for --config"
|
|
86
|
+
};
|
|
87
|
+
configPath = nextArg;
|
|
88
|
+
index++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === "--output" || arg === "-o") {
|
|
92
|
+
if (commandToken !== "build") return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: `${arg} is only valid for 'build'`
|
|
95
|
+
};
|
|
96
|
+
if (isMissingOptionValue(nextArg)) return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: "Missing value for --output"
|
|
99
|
+
};
|
|
100
|
+
outputPath = nextArg;
|
|
101
|
+
index++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (arg === "--dry-run" || arg === "-n") {
|
|
105
|
+
if (commandToken !== "build" && commandToken !== "inject") return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: `${arg} is only valid for 'build' or 'inject'`
|
|
108
|
+
};
|
|
109
|
+
dryRun = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg === "--port" || arg === "-p") {
|
|
113
|
+
if (commandToken !== "serve") return {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: `${arg} is only valid for 'serve'`
|
|
116
|
+
};
|
|
117
|
+
if (isMissingOptionValue(nextArg)) return {
|
|
118
|
+
ok: false,
|
|
119
|
+
error: "Missing value for --port"
|
|
120
|
+
};
|
|
121
|
+
const portNum = Number(nextArg);
|
|
122
|
+
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: `Invalid port: ${nextArg}`
|
|
125
|
+
};
|
|
126
|
+
port = portNum;
|
|
127
|
+
index++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (arg === "--no-sleep") {
|
|
131
|
+
if (commandToken !== "serve") return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: `${arg} is only valid for 'serve'`
|
|
134
|
+
};
|
|
135
|
+
noSleep = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (isVersionCommand(arg)) return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: `${arg} is only valid as a top-level command`
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: `Unknown flag: ${arg}`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
value: {
|
|
150
|
+
command: commandToken,
|
|
151
|
+
configPath,
|
|
152
|
+
outputPath,
|
|
153
|
+
dryRun,
|
|
154
|
+
port,
|
|
155
|
+
noSleep,
|
|
156
|
+
command_: trailingCommand
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/config-schema.ts
|
|
163
|
+
/**
|
|
164
|
+
* Valibot schemas for remobi config validation.
|
|
165
|
+
* Only used at CLI time (build/inject/serve) — never in the browser bundle.
|
|
166
|
+
*/
|
|
167
|
+
const finiteNumber = v.pipe(v.number(), v.finite());
|
|
168
|
+
const sendActionSchema = v.strictObject({
|
|
169
|
+
type: v.literal("send"),
|
|
170
|
+
data: v.string(),
|
|
171
|
+
keyLabel: v.optional(v.string())
|
|
172
|
+
});
|
|
173
|
+
const ctrlModifierActionSchema = v.strictObject({ type: v.literal("ctrl-modifier") });
|
|
174
|
+
const pasteActionSchema = v.strictObject({ type: v.literal("paste") });
|
|
175
|
+
const comboPickerActionSchema = v.strictObject({ type: v.literal("combo-picker") });
|
|
176
|
+
const drawerToggleActionSchema = v.strictObject({ type: v.literal("drawer-toggle") });
|
|
177
|
+
const buttonActionSchema = v.variant("type", [
|
|
178
|
+
sendActionSchema,
|
|
179
|
+
ctrlModifierActionSchema,
|
|
180
|
+
pasteActionSchema,
|
|
181
|
+
comboPickerActionSchema,
|
|
182
|
+
drawerToggleActionSchema
|
|
183
|
+
]);
|
|
184
|
+
const controlButtonSchema = v.strictObject({
|
|
185
|
+
id: v.string(),
|
|
186
|
+
label: v.string(),
|
|
187
|
+
description: v.string(),
|
|
188
|
+
action: buttonActionSchema
|
|
189
|
+
});
|
|
190
|
+
const buttonArrayInputSchema = v.pipe(v.custom((input) => Array.isArray(input) || typeof input === "function", "array or function"), v.rawCheck(({ dataset, addIssue }) => {
|
|
191
|
+
if (!dataset.typed || !Array.isArray(dataset.value)) return;
|
|
192
|
+
const arr = dataset.value;
|
|
193
|
+
for (let i = 0; i < arr.length; i++) {
|
|
194
|
+
const result = v.safeParse(controlButtonSchema, arr[i]);
|
|
195
|
+
if (!result.success) for (const issue of result.issues) addIssue({
|
|
196
|
+
message: issue.message,
|
|
197
|
+
expected: issue.expected,
|
|
198
|
+
received: issue.received,
|
|
199
|
+
path: [{
|
|
200
|
+
type: "array",
|
|
201
|
+
origin: "value",
|
|
202
|
+
input: arr,
|
|
203
|
+
key: i,
|
|
204
|
+
value: arr[i]
|
|
205
|
+
}, ...issue.path ?? []]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}));
|
|
209
|
+
const themeColourSchema = v.optional(v.string());
|
|
210
|
+
const termThemeOverridesSchema = v.strictObject({
|
|
211
|
+
background: themeColourSchema,
|
|
212
|
+
foreground: themeColourSchema,
|
|
213
|
+
cursor: themeColourSchema,
|
|
214
|
+
cursorAccent: themeColourSchema,
|
|
215
|
+
selectionBackground: themeColourSchema,
|
|
216
|
+
black: themeColourSchema,
|
|
217
|
+
red: themeColourSchema,
|
|
218
|
+
green: themeColourSchema,
|
|
219
|
+
yellow: themeColourSchema,
|
|
220
|
+
blue: themeColourSchema,
|
|
221
|
+
magenta: themeColourSchema,
|
|
222
|
+
cyan: themeColourSchema,
|
|
223
|
+
white: themeColourSchema,
|
|
224
|
+
brightBlack: themeColourSchema,
|
|
225
|
+
brightRed: themeColourSchema,
|
|
226
|
+
brightGreen: themeColourSchema,
|
|
227
|
+
brightYellow: themeColourSchema,
|
|
228
|
+
brightBlue: themeColourSchema,
|
|
229
|
+
brightMagenta: themeColourSchema,
|
|
230
|
+
brightCyan: themeColourSchema,
|
|
231
|
+
brightWhite: themeColourSchema
|
|
232
|
+
});
|
|
233
|
+
const termThemeResolvedSchema = v.strictObject({
|
|
234
|
+
background: v.string(),
|
|
235
|
+
foreground: v.string(),
|
|
236
|
+
cursor: v.string(),
|
|
237
|
+
cursorAccent: v.string(),
|
|
238
|
+
selectionBackground: v.string(),
|
|
239
|
+
black: v.string(),
|
|
240
|
+
red: v.string(),
|
|
241
|
+
green: v.string(),
|
|
242
|
+
yellow: v.string(),
|
|
243
|
+
blue: v.string(),
|
|
244
|
+
magenta: v.string(),
|
|
245
|
+
cyan: v.string(),
|
|
246
|
+
white: v.string(),
|
|
247
|
+
brightBlack: v.string(),
|
|
248
|
+
brightRed: v.string(),
|
|
249
|
+
brightGreen: v.string(),
|
|
250
|
+
brightYellow: v.string(),
|
|
251
|
+
brightBlue: v.string(),
|
|
252
|
+
brightMagenta: v.string(),
|
|
253
|
+
brightCyan: v.string(),
|
|
254
|
+
brightWhite: v.string()
|
|
255
|
+
});
|
|
256
|
+
const fontOverridesSchema = v.strictObject({
|
|
257
|
+
family: v.optional(v.string()),
|
|
258
|
+
cdnUrl: v.optional(v.string()),
|
|
259
|
+
mobileSizeDefault: v.optional(finiteNumber),
|
|
260
|
+
sizeRange: v.optional(v.pipe(v.tuple([finiteNumber, finiteNumber])))
|
|
261
|
+
});
|
|
262
|
+
const fontResolvedSchema = v.strictObject({
|
|
263
|
+
family: v.string(),
|
|
264
|
+
cdnUrl: v.string(),
|
|
265
|
+
mobileSizeDefault: finiteNumber,
|
|
266
|
+
sizeRange: v.pipe(v.tuple([finiteNumber, finiteNumber]))
|
|
267
|
+
});
|
|
268
|
+
const swipeOverridesSchema = v.strictObject({
|
|
269
|
+
enabled: v.optional(v.boolean()),
|
|
270
|
+
threshold: v.optional(finiteNumber),
|
|
271
|
+
maxDuration: v.optional(finiteNumber),
|
|
272
|
+
left: v.optional(v.string()),
|
|
273
|
+
right: v.optional(v.string()),
|
|
274
|
+
leftLabel: v.optional(v.string()),
|
|
275
|
+
rightLabel: v.optional(v.string())
|
|
276
|
+
});
|
|
277
|
+
const swipeResolvedSchema = v.strictObject({
|
|
278
|
+
enabled: v.boolean(),
|
|
279
|
+
threshold: finiteNumber,
|
|
280
|
+
maxDuration: finiteNumber,
|
|
281
|
+
left: v.string(),
|
|
282
|
+
right: v.string(),
|
|
283
|
+
leftLabel: v.string(),
|
|
284
|
+
rightLabel: v.string()
|
|
285
|
+
});
|
|
286
|
+
const pinchOverridesSchema = v.strictObject({ enabled: v.optional(v.boolean()) });
|
|
287
|
+
const pinchResolvedSchema = v.strictObject({ enabled: v.boolean() });
|
|
288
|
+
const scrollStrategySchema = v.picklist(["keys", "wheel"]);
|
|
289
|
+
const scrollOverridesSchema = v.strictObject({
|
|
290
|
+
enabled: v.optional(v.boolean()),
|
|
291
|
+
sensitivity: v.optional(finiteNumber),
|
|
292
|
+
strategy: v.optional(scrollStrategySchema),
|
|
293
|
+
wheelIntervalMs: v.optional(finiteNumber)
|
|
294
|
+
});
|
|
295
|
+
const scrollResolvedSchema = v.strictObject({
|
|
296
|
+
enabled: v.boolean(),
|
|
297
|
+
sensitivity: finiteNumber,
|
|
298
|
+
strategy: scrollStrategySchema,
|
|
299
|
+
wheelIntervalMs: finiteNumber
|
|
300
|
+
});
|
|
301
|
+
const gestureOverridesSchema = v.strictObject({
|
|
302
|
+
swipe: v.optional(swipeOverridesSchema),
|
|
303
|
+
pinch: v.optional(pinchOverridesSchema),
|
|
304
|
+
scroll: v.optional(scrollOverridesSchema)
|
|
305
|
+
});
|
|
306
|
+
const gestureResolvedSchema = v.strictObject({
|
|
307
|
+
swipe: swipeResolvedSchema,
|
|
308
|
+
pinch: pinchResolvedSchema,
|
|
309
|
+
scroll: scrollResolvedSchema
|
|
310
|
+
});
|
|
311
|
+
const mobileOverridesSchema = v.strictObject({
|
|
312
|
+
initData: v.optional(v.nullable(v.string())),
|
|
313
|
+
widthThreshold: v.optional(finiteNumber)
|
|
314
|
+
});
|
|
315
|
+
const mobileResolvedSchema = v.strictObject({
|
|
316
|
+
initData: v.nullable(v.string()),
|
|
317
|
+
widthThreshold: finiteNumber
|
|
318
|
+
});
|
|
319
|
+
const floatingPositionSchema = v.picklist([
|
|
320
|
+
"top-left",
|
|
321
|
+
"top-right",
|
|
322
|
+
"top-centre",
|
|
323
|
+
"bottom-left",
|
|
324
|
+
"bottom-right",
|
|
325
|
+
"bottom-centre",
|
|
326
|
+
"centre-left",
|
|
327
|
+
"centre-right"
|
|
328
|
+
]);
|
|
329
|
+
const floatingDirectionSchema = v.picklist(["row", "column"]);
|
|
330
|
+
const floatingButtonGroupSchema = v.strictObject({
|
|
331
|
+
position: floatingPositionSchema,
|
|
332
|
+
direction: v.optional(floatingDirectionSchema),
|
|
333
|
+
buttons: v.array(controlButtonSchema)
|
|
334
|
+
});
|
|
335
|
+
const pwaOverridesSchema = v.strictObject({
|
|
336
|
+
enabled: v.optional(v.boolean()),
|
|
337
|
+
shortName: v.optional(v.string()),
|
|
338
|
+
themeColor: v.optional(v.string())
|
|
339
|
+
});
|
|
340
|
+
const pwaResolvedSchema = v.strictObject({
|
|
341
|
+
enabled: v.boolean(),
|
|
342
|
+
shortName: v.optional(v.string()),
|
|
343
|
+
themeColor: v.string()
|
|
344
|
+
});
|
|
345
|
+
const reconnectOverridesSchema = v.strictObject({ enabled: v.optional(v.boolean()) });
|
|
346
|
+
const reconnectResolvedSchema = v.strictObject({ enabled: v.boolean() });
|
|
347
|
+
/** Schema for config overrides (all fields optional, button arrays accept array | function) */
|
|
348
|
+
const remobiConfigOverridesSchema = v.strictObject({
|
|
349
|
+
name: v.optional(v.string()),
|
|
350
|
+
theme: v.optional(termThemeOverridesSchema),
|
|
351
|
+
font: v.optional(fontOverridesSchema),
|
|
352
|
+
toolbar: v.optional(v.strictObject({
|
|
353
|
+
row1: v.optional(buttonArrayInputSchema),
|
|
354
|
+
row2: v.optional(buttonArrayInputSchema)
|
|
355
|
+
})),
|
|
356
|
+
drawer: v.optional(v.strictObject({ buttons: v.optional(buttonArrayInputSchema) })),
|
|
357
|
+
gestures: v.optional(gestureOverridesSchema),
|
|
358
|
+
mobile: v.optional(mobileOverridesSchema),
|
|
359
|
+
floatingButtons: v.optional(v.array(floatingButtonGroupSchema)),
|
|
360
|
+
pwa: v.optional(pwaOverridesSchema),
|
|
361
|
+
reconnect: v.optional(reconnectOverridesSchema)
|
|
362
|
+
});
|
|
363
|
+
/** Schema for fully resolved config (all required fields, plain button arrays) */
|
|
364
|
+
const remobiConfigResolvedSchema = v.strictObject({
|
|
365
|
+
name: v.string(),
|
|
366
|
+
theme: termThemeResolvedSchema,
|
|
367
|
+
font: fontResolvedSchema,
|
|
368
|
+
toolbar: v.strictObject({
|
|
369
|
+
row1: v.array(controlButtonSchema),
|
|
370
|
+
row2: v.array(controlButtonSchema)
|
|
371
|
+
}),
|
|
372
|
+
drawer: v.strictObject({ buttons: v.array(controlButtonSchema) }),
|
|
373
|
+
gestures: gestureResolvedSchema,
|
|
374
|
+
mobile: mobileResolvedSchema,
|
|
375
|
+
floatingButtons: v.array(floatingButtonGroupSchema),
|
|
376
|
+
pwa: pwaResolvedSchema,
|
|
377
|
+
reconnect: reconnectResolvedSchema
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/config-validate.ts
|
|
382
|
+
var ConfigValidationError = class extends Error {
|
|
383
|
+
issues;
|
|
384
|
+
constructor(issues) {
|
|
385
|
+
super(formatIssues(issues));
|
|
386
|
+
this.name = "ConfigValidationError";
|
|
387
|
+
this.issues = issues;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function formatIssues(issues) {
|
|
391
|
+
const lines = ["Invalid remobi config:"];
|
|
392
|
+
for (const issue of issues) lines.push(`- ${issue.path}: expected ${issue.expected}, received ${issue.received}`);
|
|
393
|
+
return lines.join("\n");
|
|
394
|
+
}
|
|
395
|
+
function truncate(value, maxLength) {
|
|
396
|
+
if (value.length <= maxLength) return value;
|
|
397
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
398
|
+
}
|
|
399
|
+
function describeReceived(value) {
|
|
400
|
+
if (value === null) return "null";
|
|
401
|
+
if (Array.isArray(value)) return `array(len=${value.length})`;
|
|
402
|
+
if (typeof value === "string") return `string(${JSON.stringify(truncate(value, 80))})`;
|
|
403
|
+
if (typeof value === "number") return `number(${String(value)})`;
|
|
404
|
+
if (typeof value === "boolean") return `boolean(${String(value)})`;
|
|
405
|
+
if (typeof value === "bigint") return `bigint(${String(value)})`;
|
|
406
|
+
if (typeof value === "undefined") return "undefined";
|
|
407
|
+
if (typeof value === "function") return value.name.length > 0 ? `function(${value.name})` : "function";
|
|
408
|
+
if (typeof value === "object") {
|
|
409
|
+
const keys = Object.keys(value);
|
|
410
|
+
if (keys.length === 0) return "object(empty)";
|
|
411
|
+
return `object(keys: ${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""})`;
|
|
412
|
+
}
|
|
413
|
+
return typeof value;
|
|
414
|
+
}
|
|
415
|
+
/** Convert Valibot issue path to dotted string */
|
|
416
|
+
function issuePath(issue) {
|
|
417
|
+
if (!issue.path || issue.path.length === 0) return "config";
|
|
418
|
+
const segments = ["config"];
|
|
419
|
+
for (const segment of issue.path) if (typeof segment.key === "number") segments.push(`[${String(segment.key)}]`);
|
|
420
|
+
else segments.push(`.${String(segment.key)}`);
|
|
421
|
+
return segments.join("").replaceAll(".[", "[");
|
|
422
|
+
}
|
|
423
|
+
/** Extract human-readable expected string from a Valibot issue */
|
|
424
|
+
function issueExpected(issue) {
|
|
425
|
+
if (issue.expected === "unknown" || !issue.expected) return issue.message;
|
|
426
|
+
return issue.expected;
|
|
427
|
+
}
|
|
428
|
+
/** Map Valibot flat issues to our ValidationIssue format */
|
|
429
|
+
function toValidationIssues(issues) {
|
|
430
|
+
const result = [];
|
|
431
|
+
for (const issue of issues) {
|
|
432
|
+
if (issue.issues && issue.issues.length > 0) {
|
|
433
|
+
result.push(...toValidationIssues(issue.issues));
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const received = issue.input !== void 0 ? issue.input : void 0;
|
|
437
|
+
result.push({
|
|
438
|
+
path: issuePath(issue),
|
|
439
|
+
expected: issueExpected(issue),
|
|
440
|
+
received: describeReceived(received)
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
function assertValidConfigOverrides(value) {
|
|
446
|
+
const result = v.safeParse(remobiConfigOverridesSchema, value);
|
|
447
|
+
if (!result.success) throw new ConfigValidationError(toValidationIssues(result.issues));
|
|
448
|
+
}
|
|
449
|
+
function assertValidResolvedConfig(value) {
|
|
450
|
+
const result = v.safeParse(remobiConfigResolvedSchema, value);
|
|
451
|
+
if (!result.success) throw new ConfigValidationError(toValidationIssues(result.issues));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/pwa/manifest.ts
|
|
456
|
+
/** Generate a web app manifest object from pwa config */
|
|
457
|
+
function generateManifest(name, pwa) {
|
|
458
|
+
return {
|
|
459
|
+
name,
|
|
460
|
+
short_name: pwa.shortName ?? name,
|
|
461
|
+
start_url: "/",
|
|
462
|
+
display: "standalone",
|
|
463
|
+
background_color: pwa.themeColor,
|
|
464
|
+
theme_color: pwa.themeColor,
|
|
465
|
+
icons: [{
|
|
466
|
+
src: "/icon-192.png",
|
|
467
|
+
sizes: "192x192",
|
|
468
|
+
type: "image/png",
|
|
469
|
+
purpose: "any maskable"
|
|
470
|
+
}, {
|
|
471
|
+
src: "/icon-512.png",
|
|
472
|
+
sizes: "512x512",
|
|
473
|
+
type: "image/png"
|
|
474
|
+
}]
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/** Serialise manifest to JSON string */
|
|
478
|
+
function manifestToJson(name, pwa) {
|
|
479
|
+
return JSON.stringify(generateManifest(name, pwa), null, 2);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/serve.ts
|
|
484
|
+
const DEFAULT_PORT = 7681;
|
|
485
|
+
const DEFAULT_COMMAND = [
|
|
486
|
+
"tmux",
|
|
487
|
+
"new-session",
|
|
488
|
+
"-A",
|
|
489
|
+
"-s",
|
|
490
|
+
"main"
|
|
491
|
+
];
|
|
492
|
+
function findIconsDir() {
|
|
493
|
+
let dir = import.meta.dirname;
|
|
494
|
+
for (let i = 0; i < 5; i++) {
|
|
495
|
+
const candidate = resolve(dir, "src/pwa/icons");
|
|
496
|
+
if (existsSync(candidate)) return candidate;
|
|
497
|
+
dir = dirname(dir);
|
|
498
|
+
}
|
|
499
|
+
return resolve(import.meta.dirname, "pwa/icons");
|
|
500
|
+
}
|
|
501
|
+
const ICONS_DIR = findIconsDir();
|
|
502
|
+
/** Poll until ttyd is accepting connections on the given port */
|
|
503
|
+
async function waitForTtyd(port, retries = 40, intervalMs = 200) {
|
|
504
|
+
for (let i = 0; i < retries; i++) {
|
|
505
|
+
try {
|
|
506
|
+
if ((await fetch(`http://127.0.0.1:${port}/`)).ok) return;
|
|
507
|
+
} catch {}
|
|
508
|
+
await sleep(intervalMs);
|
|
509
|
+
}
|
|
510
|
+
throw new Error(`ttyd did not start on port ${port} — is ttyd installed and on PATH?\nInstall ttyd: macOS \`brew install ttyd\`; Linux use your distro package manager or build from source: https://github.com/tsl0922/ttyd#installation`);
|
|
511
|
+
}
|
|
512
|
+
/** Pick a random internal port */
|
|
513
|
+
function randomInternalPort() {
|
|
514
|
+
return 19e3 + Math.floor(Math.random() * 1e3);
|
|
515
|
+
}
|
|
516
|
+
/** Build ttyd args from remobi config */
|
|
517
|
+
function buildTtydArgs(config, internalPort, command) {
|
|
518
|
+
return [
|
|
519
|
+
"--writable",
|
|
520
|
+
"-i",
|
|
521
|
+
"127.0.0.1",
|
|
522
|
+
"--port",
|
|
523
|
+
String(internalPort),
|
|
524
|
+
"-t",
|
|
525
|
+
`theme=${serialiseThemeForTtyd(config)}`,
|
|
526
|
+
"-t",
|
|
527
|
+
`fontFamily="${config.font.family}"`,
|
|
528
|
+
"-t",
|
|
529
|
+
"scrollSensitivity=3",
|
|
530
|
+
"-t",
|
|
531
|
+
"disableLeaveAlert=true",
|
|
532
|
+
...command
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
/** Read a PNG icon, returns undefined if not found */
|
|
536
|
+
function readIcon(filename) {
|
|
537
|
+
try {
|
|
538
|
+
return readFileSync(resolve(ICONS_DIR, filename));
|
|
539
|
+
} catch {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/** Spawn caffeinate to prevent system sleep while ttyd is running (macOS only).
|
|
544
|
+
* Uses -s (system sleep on AC) and -w <pid> so the assertion drops when ttyd exits. */
|
|
545
|
+
function spawnCaffeinate(pid) {
|
|
546
|
+
try {
|
|
547
|
+
const proc = spawnProcess([
|
|
548
|
+
"caffeinate",
|
|
549
|
+
"-s",
|
|
550
|
+
"-w",
|
|
551
|
+
String(pid)
|
|
552
|
+
], {
|
|
553
|
+
stdout: "ignore",
|
|
554
|
+
stderr: "ignore"
|
|
555
|
+
});
|
|
556
|
+
proc.exited.catch(() => {
|
|
557
|
+
console.warn("remobi: --no-sleep requires caffeinate (macOS only), ignoring");
|
|
558
|
+
});
|
|
559
|
+
console.log(`remobi: sleep prevention active (caffeinate -s -w ${pid})`);
|
|
560
|
+
return proc;
|
|
561
|
+
} catch {
|
|
562
|
+
console.warn("remobi: --no-sleep requires caffeinate (macOS only), ignoring");
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** Start remobi serve: builds overlay in memory, manages ttyd, serves HTTP + WS */
|
|
567
|
+
async function serve$1(config, port = DEFAULT_PORT, command = DEFAULT_COMMAND, noSleep = false) {
|
|
568
|
+
console.log("remobi: building overlay...");
|
|
569
|
+
const { js, css } = await bundleOverlay(config);
|
|
570
|
+
const internalPort = randomInternalPort();
|
|
571
|
+
const ttydArgs = buildTtydArgs(config, internalPort, command);
|
|
572
|
+
console.log(`remobi: starting ttyd on internal port ${internalPort}...`);
|
|
573
|
+
const ttydProc = spawnProcess(["ttyd", ...ttydArgs], {
|
|
574
|
+
stdout: "ignore",
|
|
575
|
+
stderr: "ignore"
|
|
576
|
+
});
|
|
577
|
+
const caffeinateProc = noSleep && ttydProc.pid ? spawnCaffeinate(ttydProc.pid) : null;
|
|
578
|
+
await waitForTtyd(internalPort);
|
|
579
|
+
const html = injectOverlay(await (await fetch(`http://127.0.0.1:${internalPort}/`)).text(), js, css, config);
|
|
580
|
+
console.log("remobi: overlay ready");
|
|
581
|
+
const manifestJson = config.pwa.enabled ? manifestToJson(config.name, config.pwa) : null;
|
|
582
|
+
const icon180 = readIcon("icon-180.png");
|
|
583
|
+
const icon192 = readIcon("icon-192.png");
|
|
584
|
+
const icon512 = readIcon("icon-512.png");
|
|
585
|
+
const connections = /* @__PURE__ */ new WeakMap();
|
|
586
|
+
const app = new Hono();
|
|
587
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
588
|
+
app.get("/ws", upgradeWebSocket(() => ({
|
|
589
|
+
onOpen(_event, ws) {
|
|
590
|
+
const raw = ws.raw;
|
|
591
|
+
if (!raw) return;
|
|
592
|
+
const data = {
|
|
593
|
+
backend: null,
|
|
594
|
+
buffer: []
|
|
595
|
+
};
|
|
596
|
+
connections.set(raw, data);
|
|
597
|
+
const backend = new WebSocket(`ws://127.0.0.1:${internalPort}/ws`, ["tty"]);
|
|
598
|
+
backend.binaryType = "arraybuffer";
|
|
599
|
+
data.backend = backend;
|
|
600
|
+
backend.on("open", () => {
|
|
601
|
+
for (const msg of data.buffer) backend.send(msg);
|
|
602
|
+
data.buffer = [];
|
|
603
|
+
});
|
|
604
|
+
backend.on("message", (message, isBinary) => {
|
|
605
|
+
if (isBinary && message instanceof ArrayBuffer) ws.send(new Uint8Array(message));
|
|
606
|
+
else ws.send(message.toString());
|
|
607
|
+
});
|
|
608
|
+
backend.on("error", () => {
|
|
609
|
+
ws.close();
|
|
610
|
+
});
|
|
611
|
+
backend.on("close", () => {
|
|
612
|
+
ws.close();
|
|
613
|
+
});
|
|
614
|
+
},
|
|
615
|
+
onMessage(event, ws) {
|
|
616
|
+
const raw = ws.raw;
|
|
617
|
+
if (!raw) return;
|
|
618
|
+
const data = connections.get(raw);
|
|
619
|
+
if (!data) return;
|
|
620
|
+
const { backend, buffer } = data;
|
|
621
|
+
if (backend !== null && backend.readyState === WebSocket.OPEN) backend.send(event.data);
|
|
622
|
+
else {
|
|
623
|
+
const msg = event.data;
|
|
624
|
+
buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg));
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
onClose(_event, ws) {
|
|
628
|
+
const raw = ws.raw;
|
|
629
|
+
if (!raw) return;
|
|
630
|
+
connections.get(raw)?.backend?.close();
|
|
631
|
+
connections.delete(raw);
|
|
632
|
+
}
|
|
633
|
+
})));
|
|
634
|
+
app.get("/", (c) => c.html(html));
|
|
635
|
+
if (manifestJson !== null) app.get("/manifest.json", (c) => {
|
|
636
|
+
return c.json(JSON.parse(manifestJson));
|
|
637
|
+
});
|
|
638
|
+
if (icon180) app.get("/apple-touch-icon.png", () => {
|
|
639
|
+
return new Response(Uint8Array.from(icon180), { headers: { "content-type": "image/png" } });
|
|
640
|
+
});
|
|
641
|
+
if (icon192) app.get("/icon-192.png", () => {
|
|
642
|
+
return new Response(Uint8Array.from(icon192), { headers: { "content-type": "image/png" } });
|
|
643
|
+
});
|
|
644
|
+
if (icon512) app.get("/icon-512.png", () => {
|
|
645
|
+
return new Response(Uint8Array.from(icon512), { headers: { "content-type": "image/png" } });
|
|
646
|
+
});
|
|
647
|
+
app.all("/*", async (c) => {
|
|
648
|
+
const url = new URL(c.req.url);
|
|
649
|
+
const backendUrl = `http://127.0.0.1:${internalPort}${url.pathname}${url.search}`;
|
|
650
|
+
const resp = await fetch(backendUrl, {
|
|
651
|
+
method: c.req.method,
|
|
652
|
+
headers: c.req.raw.headers,
|
|
653
|
+
body: c.req.raw.body
|
|
654
|
+
});
|
|
655
|
+
return new Response(resp.body, {
|
|
656
|
+
status: resp.status,
|
|
657
|
+
headers: resp.headers
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
const server = serve({
|
|
661
|
+
fetch: app.fetch,
|
|
662
|
+
port
|
|
663
|
+
});
|
|
664
|
+
injectWebSocket(server);
|
|
665
|
+
console.log(`remobi: serving on http://localhost:${port}`);
|
|
666
|
+
const cleanup = () => {
|
|
667
|
+
console.log("\nremobi: shutting down...");
|
|
668
|
+
server.close();
|
|
669
|
+
ttydProc.kill();
|
|
670
|
+
caffeinateProc?.kill();
|
|
671
|
+
process.exit(0);
|
|
672
|
+
};
|
|
673
|
+
process.on("SIGINT", cleanup);
|
|
674
|
+
process.on("SIGTERM", cleanup);
|
|
675
|
+
await ttydProc.exited;
|
|
676
|
+
server.close();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region cli.ts
|
|
681
|
+
function loadPackageVersion() {
|
|
682
|
+
let dir = import.meta.dirname;
|
|
683
|
+
for (let i = 0; i < 5; i++) try {
|
|
684
|
+
const content = readFileSync(resolve(dir, "package.json"), "utf-8");
|
|
685
|
+
return JSON.parse(content).version;
|
|
686
|
+
} catch {
|
|
687
|
+
dir = dirname(dir);
|
|
688
|
+
}
|
|
689
|
+
return "0.0.0";
|
|
690
|
+
}
|
|
691
|
+
const VERSION = loadPackageVersion();
|
|
692
|
+
function usage() {
|
|
693
|
+
console.log(`remobi v${VERSION} — mobile-friendly terminal overlay for ttyd + tmux
|
|
694
|
+
|
|
695
|
+
Usage:
|
|
696
|
+
remobi serve [--config <path>] [--port <n>] [--no-sleep] [-- <command...>]
|
|
697
|
+
Build overlay in memory, manage ttyd, serve with PWA support.
|
|
698
|
+
Default port: 7681. Default command: tmux new-session -A -s main
|
|
699
|
+
|
|
700
|
+
remobi build [--config <path>] [--output <path>] [--dry-run]
|
|
701
|
+
Build patched index.html for ttyd --index flag.
|
|
702
|
+
Starts temp ttyd, fetches base HTML, injects overlay.
|
|
703
|
+
|
|
704
|
+
remobi inject [--config <path>] [--dry-run]
|
|
705
|
+
Pipe mode: reads ttyd HTML from stdin, outputs patched HTML to stdout.
|
|
706
|
+
|
|
707
|
+
remobi init
|
|
708
|
+
Scaffold a remobi.config.ts with defaults.
|
|
709
|
+
|
|
710
|
+
remobi --version
|
|
711
|
+
Print version.
|
|
712
|
+
|
|
713
|
+
remobi --help
|
|
714
|
+
Show this help.
|
|
715
|
+
|
|
716
|
+
Flags:
|
|
717
|
+
-c, --config <path> Use a specific config file (build/inject/serve)
|
|
718
|
+
-o, --output <path> Build output path (build only)
|
|
719
|
+
-p, --port <n> Port to serve on (serve only, default 7681)
|
|
720
|
+
-n, --dry-run Validate + print plan only (build/inject)
|
|
721
|
+
--no-sleep Prevent macOS sleep while serving (caffeinate -s, serve only)
|
|
722
|
+
|
|
723
|
+
Examples:
|
|
724
|
+
remobi serve
|
|
725
|
+
remobi serve --no-sleep
|
|
726
|
+
remobi serve --port 8080 -- tmux new -As dev
|
|
727
|
+
remobi build -c ./remobi.config.ts -o ./dist/index.html
|
|
728
|
+
remobi build --dry-run
|
|
729
|
+
curl -s http://127.0.0.1:7681/ | remobi inject --dry-run`);
|
|
730
|
+
}
|
|
731
|
+
function isRecord(value) {
|
|
732
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
733
|
+
}
|
|
734
|
+
function extractDefaultExport(value) {
|
|
735
|
+
if (!isRecord(value)) return void 0;
|
|
736
|
+
if (!("default" in value)) return void 0;
|
|
737
|
+
return value.default;
|
|
738
|
+
}
|
|
739
|
+
function ensureInjectInputMode(context) {
|
|
740
|
+
if (process.stdin.isTTY) throw new Error(`${context} expects piped ttyd HTML on stdin`);
|
|
741
|
+
}
|
|
742
|
+
function throwConfigValidationError(source, error) {
|
|
743
|
+
throw new Error(`Config validation failed for ${source}\n${error.message}`);
|
|
744
|
+
}
|
|
745
|
+
/** Convert a config path to its .local sibling, e.g. remobi.config.ts → remobi.config.local.ts */
|
|
746
|
+
function toLocalPath(configPath) {
|
|
747
|
+
const dotIdx = configPath.lastIndexOf(".");
|
|
748
|
+
if (dotIdx === -1) return `${configPath}.local`;
|
|
749
|
+
return `${configPath.slice(0, dotIdx)}.local${configPath.slice(dotIdx)}`;
|
|
750
|
+
}
|
|
751
|
+
/** Try to load a .local config override file. Returns undefined if the file does not exist. */
|
|
752
|
+
async function loadLocalOverrides(localPath) {
|
|
753
|
+
if (!existsSync(localPath)) return;
|
|
754
|
+
const defaultExport = extractDefaultExport(await import(localPath));
|
|
755
|
+
if (defaultExport === void 0) throw new Error(`Local config file has no default export: ${localPath}`);
|
|
756
|
+
assertValidOverridesOrThrow(defaultExport, localPath);
|
|
757
|
+
return defaultExport;
|
|
758
|
+
}
|
|
759
|
+
function assertValidOverridesOrThrow(value, source) {
|
|
760
|
+
try {
|
|
761
|
+
assertValidConfigOverrides(value);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (error instanceof ConfigValidationError) throwConfigValidationError(source, error);
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function assertValidResolvedOrThrow(value, source) {
|
|
768
|
+
try {
|
|
769
|
+
assertValidResolvedConfig(value);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (error instanceof ConfigValidationError) throwConfigValidationError(source, error);
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function loadConfig(configPath) {
|
|
776
|
+
let resolved = configPath;
|
|
777
|
+
if (!resolved) {
|
|
778
|
+
const names = ["remobi.config.ts", "remobi.config.js"];
|
|
779
|
+
const searchDirs = [process.cwd(), join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "remobi")];
|
|
780
|
+
for (const dir of searchDirs) {
|
|
781
|
+
for (const name of names) {
|
|
782
|
+
const full = join(dir, name);
|
|
783
|
+
if (existsSync(full)) {
|
|
784
|
+
resolved = full;
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (resolved) break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (resolved) {
|
|
792
|
+
const abs = resolve(process.cwd(), resolved);
|
|
793
|
+
const defaultExport = extractDefaultExport(await import(abs));
|
|
794
|
+
if (defaultExport === void 0) throw new Error(`Config file has no default export: ${abs}`);
|
|
795
|
+
assertValidOverridesOrThrow(defaultExport, abs);
|
|
796
|
+
const sharedConfig = defineConfig(defaultExport);
|
|
797
|
+
const localPath = toLocalPath(abs);
|
|
798
|
+
const localOverrides = await loadLocalOverrides(localPath);
|
|
799
|
+
const config = localOverrides !== void 0 ? mergeConfig(sharedConfig, localOverrides) : sharedConfig;
|
|
800
|
+
const sourceLabel = localOverrides !== void 0 ? `${abs} + ${localPath}` : abs;
|
|
801
|
+
assertValidResolvedOrThrow(config, sourceLabel);
|
|
802
|
+
return {
|
|
803
|
+
config,
|
|
804
|
+
source: sourceLabel
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
assertValidResolvedOrThrow(defaultConfig, "built-in defaults");
|
|
808
|
+
return {
|
|
809
|
+
config: defaultConfig,
|
|
810
|
+
source: "built-in defaults"
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async function main() {
|
|
814
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
815
|
+
if (!parsed.ok) {
|
|
816
|
+
console.error(parsed.error);
|
|
817
|
+
usage();
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
const { command, configPath, outputPath, dryRun, port, noSleep, command_ } = parsed.value;
|
|
821
|
+
switch (command) {
|
|
822
|
+
case "serve":
|
|
823
|
+
await serve$1((await loadConfig(configPath)).config, port, command_.length > 0 ? command_ : void 0, noSleep);
|
|
824
|
+
break;
|
|
825
|
+
case "build": {
|
|
826
|
+
const loaded = await loadConfig(configPath);
|
|
827
|
+
const targetPath = outputPath ? resolve(process.cwd(), outputPath) : resolve(process.cwd(), "dist/index.html");
|
|
828
|
+
if (dryRun) {
|
|
829
|
+
console.log("Dry run: build");
|
|
830
|
+
console.log(`- config: ${loaded.source}`);
|
|
831
|
+
console.log(`- output: ${targetPath}`);
|
|
832
|
+
console.log("- action: would bundle overlay, fetch ttyd base HTML, inject, and write file");
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
836
|
+
await build(loaded.config, targetPath);
|
|
837
|
+
console.log(`Built: ${targetPath}`);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
case "inject": {
|
|
841
|
+
const loaded = await loadConfig(configPath);
|
|
842
|
+
if (dryRun) {
|
|
843
|
+
ensureInjectInputMode("remobi inject --dry-run");
|
|
844
|
+
if ((await readStdin()).trim().length === 0) throw new Error("remobi inject --dry-run expects piped ttyd HTML on stdin");
|
|
845
|
+
console.log("Dry run: inject");
|
|
846
|
+
console.log(`- config: ${loaded.source}`);
|
|
847
|
+
console.log("- stdin: piped input detected");
|
|
848
|
+
console.log("- action: would read stdin HTML, inject overlay, and write to stdout");
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
ensureInjectInputMode("remobi inject");
|
|
852
|
+
const result = await injectFromStdin(loaded.config);
|
|
853
|
+
process.stdout.write(result);
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
case "init": {
|
|
857
|
+
const targetPath = resolve(process.cwd(), "remobi.config.ts");
|
|
858
|
+
if (existsSync(targetPath)) {
|
|
859
|
+
console.error("remobi.config.ts already exists");
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
writeFileSync(targetPath, `import { defineConfig } from 'remobi'
|
|
863
|
+
|
|
864
|
+
export default defineConfig({
|
|
865
|
+
// name: 'remobi', // app name (tab title, PWA home screen label)
|
|
866
|
+
// theme: 'catppuccin-mocha',
|
|
867
|
+
// font: {
|
|
868
|
+
// family: 'JetBrainsMono NFM, monospace',
|
|
869
|
+
// mobileSizeDefault: 16,
|
|
870
|
+
// sizeRange: [8, 32],
|
|
871
|
+
// },
|
|
872
|
+
//
|
|
873
|
+
// Toolbar/drawer accept a plain array (replace) or a function (transform):
|
|
874
|
+
//
|
|
875
|
+
// toolbar: { row1: [{ id, label, description, action }], row2: [...] },
|
|
876
|
+
//
|
|
877
|
+
// drawer: {
|
|
878
|
+
// buttons: [
|
|
879
|
+
// { id: 'sessions', label: 'Sessions', description: 'Choose tmux session', action: { type: 'send', data: '\\x02s' } },
|
|
880
|
+
// ],
|
|
881
|
+
// },
|
|
882
|
+
//
|
|
883
|
+
// toolbar: {
|
|
884
|
+
// row2: (defaults) => defaults.filter((b) => b.id !== 'q'),
|
|
885
|
+
// },
|
|
886
|
+
//
|
|
887
|
+
// drawer: {
|
|
888
|
+
// buttons: (defaults) => [
|
|
889
|
+
// ...defaults,
|
|
890
|
+
// { id: 'my-btn', label: 'X', description: 'Send x', action: { type: 'send', data: 'x' } },
|
|
891
|
+
// ],
|
|
892
|
+
// },
|
|
893
|
+
//
|
|
894
|
+
// gestures: {
|
|
895
|
+
// swipe: {
|
|
896
|
+
// enabled: true,
|
|
897
|
+
// left: '\\x02n', // data sent on swipe left (default: next tmux window)
|
|
898
|
+
// right: '\\x02p', // data sent on swipe right (default: prev tmux window)
|
|
899
|
+
// leftLabel: 'Next tmux window',
|
|
900
|
+
// rightLabel: 'Previous tmux window',
|
|
901
|
+
// },
|
|
902
|
+
// pinch: { enabled: true },
|
|
903
|
+
// scroll: { strategy: 'wheel', sensitivity: 40 },
|
|
904
|
+
// },
|
|
905
|
+
// mobile: {
|
|
906
|
+
// initData: '\\x02z', // send on load when viewport < widthThreshold (auto-zoom pane)
|
|
907
|
+
// widthThreshold: 768, // px — default matches phone/tablet breakpoint
|
|
908
|
+
// },
|
|
909
|
+
// floatingButtons: [
|
|
910
|
+
// // Always-visible top-left buttons (touch devices only)
|
|
911
|
+
// { position: 'top-left', buttons: [{ id: 'zoom', label: 'Zoom', description: 'Toggle pane zoom', action: { type: 'send', data: '\\x02z' } }] },
|
|
912
|
+
// ],
|
|
913
|
+
// pwa: {
|
|
914
|
+
// enabled: true, // enable PWA manifest + meta tags (used by remobi serve)
|
|
915
|
+
// shortName: 'remobi', // short name for home screen icon (defaults to name)
|
|
916
|
+
// themeColor: '#1e1e2e', // theme-color meta tag + manifest
|
|
917
|
+
// },
|
|
918
|
+
// reconnect: {
|
|
919
|
+
// enabled: true, // show overlay + auto-reload on connection loss (default true)
|
|
920
|
+
// },
|
|
921
|
+
})
|
|
922
|
+
`);
|
|
923
|
+
console.log(`Created: ${targetPath}`);
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
case "version":
|
|
927
|
+
console.log(VERSION);
|
|
928
|
+
break;
|
|
929
|
+
case "help":
|
|
930
|
+
usage();
|
|
931
|
+
break;
|
|
932
|
+
default:
|
|
933
|
+
console.error(`Unknown command: ${command}`);
|
|
934
|
+
usage();
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (import.meta.filename === process.argv[1]) main().catch((err) => {
|
|
939
|
+
console.error(err);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
//#endregion
|
|
944
|
+
export { };
|
|
945
|
+
//# sourceMappingURL=cli.mjs.map
|