sliccy 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.
Files changed (63) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +512 -0
  3. package/dist/cli/chrome-launch.d.ts +47 -0
  4. package/dist/cli/chrome-launch.js +324 -0
  5. package/dist/cli/cli-log-dedup.d.ts +19 -0
  6. package/dist/cli/cli-log-dedup.js +62 -0
  7. package/dist/cli/electron-controller.d.ts +38 -0
  8. package/dist/cli/electron-controller.js +287 -0
  9. package/dist/cli/electron-main.d.ts +1 -0
  10. package/dist/cli/electron-main.js +183 -0
  11. package/dist/cli/electron-runtime.d.ts +58 -0
  12. package/dist/cli/electron-runtime.js +133 -0
  13. package/dist/cli/file-logger.d.ts +38 -0
  14. package/dist/cli/file-logger.js +207 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +1023 -0
  17. package/dist/cli/launch-url.d.ts +9 -0
  18. package/dist/cli/launch-url.js +34 -0
  19. package/dist/cli/qa-setup.d.ts +1 -0
  20. package/dist/cli/qa-setup.js +36 -0
  21. package/dist/cli/release-package.d.ts +17 -0
  22. package/dist/cli/release-package.js +232 -0
  23. package/dist/cli/runtime-flags.d.ts +21 -0
  24. package/dist/cli/runtime-flags.js +154 -0
  25. package/dist/cli/sync-release-version.d.ts +2 -0
  26. package/dist/cli/sync-release-version.js +34 -0
  27. package/dist/tray-url-shared.d.ts +11 -0
  28. package/dist/tray-url-shared.js +56 -0
  29. package/dist/ui/assets/___vite-browser-external_commonjs-proxy-7ULRRj69.js +1 -0
  30. package/dist/ui/assets/__vite-browser-external-D7Ct-6yo.js +1 -0
  31. package/dist/ui/assets/addon-fit-DOCEibfw.js +12 -0
  32. package/dist/ui/assets/bsh-watchdog-D19WB0U1.js +2 -0
  33. package/dist/ui/assets/index-BVQAdk-Y.js +8 -0
  34. package/dist/ui/assets/index-BjJrFm2K.js +43 -0
  35. package/dist/ui/assets/index-C1dglHrI.js +3 -0
  36. package/dist/ui/assets/index-D3Enm5ux.js +13091 -0
  37. package/dist/ui/assets/index-D7hjyFh1.js +2 -0
  38. package/dist/ui/assets/index-D8uSC2sl.js +45 -0
  39. package/dist/ui/assets/index-DEglHp2j.js +1 -0
  40. package/dist/ui/assets/index-DvjzakYY.js +14406 -0
  41. package/dist/ui/assets/index-deZeJCgO.js +1 -0
  42. package/dist/ui/assets/index-mz3VYh0I.js +131 -0
  43. package/dist/ui/assets/index-r2m8Dpaz.js +54 -0
  44. package/dist/ui/assets/index-ygVJ3eFG.js +11 -0
  45. package/dist/ui/assets/lick-manager-proxy-G3WuipZ-.js +1 -0
  46. package/dist/ui/assets/magic-string.es-BPLJknd7.js +10 -0
  47. package/dist/ui/assets/oauth-service-DIahkF-o.js +1 -0
  48. package/dist/ui/assets/offscreen-client-ByVIJGHW.js +1 -0
  49. package/dist/ui/assets/pdfjs-uyZuKmOq.js +59 -0
  50. package/dist/ui/assets/pyodide-D73G_Ygx.mjs +4 -0
  51. package/dist/ui/assets/sql-wasm-BggYNCID.js +2 -0
  52. package/dist/ui/assets/stream-lEC9OYG2.js +1 -0
  53. package/dist/ui/assets/xterm-Bb8UKAlD.js +27 -0
  54. package/dist/ui/assets/xterm-DOrYoP_4.css +32 -0
  55. package/dist/ui/electron-overlay-entry.js +360 -0
  56. package/dist/ui/favicon.png +0 -0
  57. package/dist/ui/fonts/AdobeClean-Bold.otf +0 -0
  58. package/dist/ui/fonts/AdobeClean-ExtraBold.otf +0 -0
  59. package/dist/ui/fonts/AdobeClean-Medium.otf +0 -0
  60. package/dist/ui/fonts/AdobeClean-Regular.otf +0 -0
  61. package/dist/ui/index.html +1981 -0
  62. package/dist/ui/preview-sw.js +4 -0
  63. package/package.json +81 -0
@@ -0,0 +1,287 @@
1
+ import { execFile as nodeExecFile, spawn } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { readFile } from 'fs/promises';
4
+ import { promisify } from 'util';
5
+ import { WebSocket } from 'ws';
6
+ import { buildElectronAppLaunchSpec, buildElectronOverlayAppUrl, buildElectronOverlayBootstrapScript, buildElectronOverlayEntryUrl, getElectronOverlayEntryDistPath, getElectronServeOrigin, shouldInjectElectronOverlayTarget, } from './electron-runtime.js';
7
+ const execFile = promisify(nodeExecFile);
8
+ const ELECTRON_OVERLAY_SYNC_INTERVAL_MS = 1500;
9
+ function commandLineExecutableMatchesPattern(commandLine, pattern) {
10
+ // Extract the executable (first whitespace-separated token) from the command line.
11
+ // Only match when the target app path is the executable itself, not an argument —
12
+ // this avoids false positives when the path appears as a CLI flag (e.g. --kill /App.app).
13
+ const executable = commandLine.trimStart().split(/\s+/)[0] ?? '';
14
+ return executable === pattern
15
+ || executable.startsWith(pattern + '/')
16
+ || executable.startsWith(pattern + '\\');
17
+ }
18
+ export function findMatchingElectronAppPids(runningProcesses, processMatchPatterns, currentPid = process.pid) {
19
+ const matches = runningProcesses.filter((processInfo) => {
20
+ // Skip Node.js tool-chain processes and shell wrappers — they may have the app path
21
+ // as a CLI argument but are not the Electron app itself
22
+ // (e.g. npx tsx src/cli/index.ts --electron /Applications/Slack.app)
23
+ // Shell wrappers like `zsh -c ... /Applications/Slack.app --kill` or
24
+ // `timeout 30 npm run dev:electron -- /Applications/Slack.app` also match.
25
+ const cmdTrimmed = processInfo.commandLine.trimStart();
26
+ if (/^(\/\S*\/)?(node|npx|tsx|npm|open|bash|zsh|sh|csh|fish|dash|timeout|env|sudo|caffeinate)\b/i.test(cmdTrimmed))
27
+ return false;
28
+ return processMatchPatterns.some((pattern) => {
29
+ return commandLineExecutableMatchesPattern(processInfo.commandLine, pattern)
30
+ || (processInfo.executablePath?.includes(pattern) ?? false);
31
+ });
32
+ });
33
+ return Array.from(new Set(matches.map((processInfo) => processInfo.pid).filter((pid) => pid !== currentPid)));
34
+ }
35
+ export class ElectronAppAlreadyRunningError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = 'ElectronAppAlreadyRunningError';
39
+ }
40
+ }
41
+ function parseUnixProcessList(stdout) {
42
+ const processes = [];
43
+ for (const rawLine of stdout.split('\n')) {
44
+ const line = rawLine.trim();
45
+ if (!line)
46
+ continue;
47
+ const match = line.match(/^(\d+)\s+(.*)$/);
48
+ if (!match)
49
+ continue;
50
+ const pid = Number.parseInt(match[1] ?? '', 10);
51
+ if (!Number.isFinite(pid) || pid <= 0)
52
+ continue;
53
+ processes.push({
54
+ pid,
55
+ commandLine: match[2] ?? '',
56
+ executablePath: null,
57
+ });
58
+ }
59
+ return processes;
60
+ }
61
+ function parseWindowsProcessList(stdout) {
62
+ const trimmed = stdout.trim();
63
+ if (!trimmed)
64
+ return [];
65
+ const parsed = JSON.parse(trimmed);
66
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
67
+ return entries
68
+ .map((entry) => ({
69
+ pid: Number.parseInt(String(entry['ProcessId'] ?? ''), 10),
70
+ commandLine: String(entry['CommandLine'] ?? ''),
71
+ executablePath: entry['ExecutablePath'] == null ? null : String(entry['ExecutablePath']),
72
+ }))
73
+ .filter((processInfo) => Number.isFinite(processInfo.pid) && processInfo.pid > 0);
74
+ }
75
+ async function listRunningProcesses(platform = process.platform) {
76
+ if (platform === 'win32') {
77
+ const { stdout } = await execFile('powershell', [
78
+ '-NoProfile',
79
+ '-Command',
80
+ 'Get-CimInstance Win32_Process | Select-Object ProcessId, ExecutablePath, CommandLine | ConvertTo-Json -Compress',
81
+ ]);
82
+ return parseWindowsProcessList(stdout);
83
+ }
84
+ const { stdout } = await execFile('ps', ['-ax', '-o', 'pid=', '-o', 'command=']);
85
+ return parseUnixProcessList(stdout);
86
+ }
87
+ function isPidAlive(pid) {
88
+ try {
89
+ process.kill(pid, 0);
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ async function waitForPidsToExit(pids, timeoutMs = 5000) {
97
+ const deadline = Date.now() + timeoutMs;
98
+ while (Date.now() < deadline) {
99
+ if (pids.every((pid) => !isPidAlive(pid)))
100
+ return true;
101
+ await new Promise((resolve) => setTimeout(resolve, 100));
102
+ }
103
+ return pids.every((pid) => !isPidAlive(pid));
104
+ }
105
+ async function terminateRunningApp(pids) {
106
+ for (const pid of pids) {
107
+ if (!isPidAlive(pid))
108
+ continue;
109
+ try {
110
+ process.kill(pid);
111
+ }
112
+ catch {
113
+ // Ignore individual termination failures and fall back to force-kill below if needed.
114
+ }
115
+ }
116
+ if (await waitForPidsToExit(pids))
117
+ return;
118
+ for (const pid of pids) {
119
+ if (!isPidAlive(pid))
120
+ continue;
121
+ try {
122
+ process.kill(pid, 'SIGKILL');
123
+ }
124
+ catch {
125
+ // Ignore final cleanup failures.
126
+ }
127
+ }
128
+ await waitForPidsToExit(pids, 3000);
129
+ }
130
+ async function findRunningElectronAppPids(appPath, platform = process.platform) {
131
+ const { processMatchPatterns } = buildElectronAppLaunchSpec(appPath, { cdpPort: 0, platform });
132
+ const runningProcesses = await listRunningProcesses(platform);
133
+ return findMatchingElectronAppPids(runningProcesses, processMatchPatterns);
134
+ }
135
+ export async function launchElectronApp(options) {
136
+ const launchSpec = buildElectronAppLaunchSpec(options.appPath, {
137
+ cdpPort: options.cdpPort,
138
+ platform: options.platform,
139
+ });
140
+ if (!existsSync(launchSpec.resolvedAppPath)) {
141
+ throw new Error(`Electron app not found at ${launchSpec.resolvedAppPath}`);
142
+ }
143
+ if (!existsSync(launchSpec.command)) {
144
+ throw new Error(`Electron executable not found at ${launchSpec.command}. Pass the app executable path directly if needed.`);
145
+ }
146
+ const runningPids = await findRunningElectronAppPids(launchSpec.resolvedAppPath, options.platform);
147
+ const platform = options.platform ?? process.platform;
148
+ const isMacAppBundle = platform === 'darwin' && launchSpec.resolvedAppPath.toLowerCase().endsWith('.app');
149
+ if (runningPids.length > 0 && !options.kill) {
150
+ throw new ElectronAppAlreadyRunningError(`${launchSpec.displayName} is already running. Re-run with --kill to relaunch it with remote debugging enabled.`);
151
+ }
152
+ if (runningPids.length > 0) {
153
+ await terminateRunningApp(runningPids);
154
+ }
155
+ const child = isMacAppBundle
156
+ ? spawn('open', ['-n', '-a', launchSpec.resolvedAppPath, '-W', '--args', ...launchSpec.args], {
157
+ env: process.env,
158
+ stdio: ['ignore', 'pipe', 'pipe'],
159
+ detached: false,
160
+ })
161
+ : spawn(launchSpec.command, launchSpec.args, {
162
+ env: process.env,
163
+ stdio: ['ignore', 'pipe', 'pipe'],
164
+ detached: false,
165
+ });
166
+ return {
167
+ child,
168
+ displayName: launchSpec.displayName,
169
+ };
170
+ }
171
+ async function loadElectronOverlayBundleSource(options) {
172
+ const serveOrigin = getElectronServeOrigin(options.servePort);
173
+ if (options.dev) {
174
+ const response = await fetch(buildElectronOverlayEntryUrl(serveOrigin));
175
+ if (!response.ok) {
176
+ throw new Error(`Failed to fetch electron overlay entry: ${response.status} ${response.statusText}`);
177
+ }
178
+ return await response.text();
179
+ }
180
+ return await readFile(getElectronOverlayEntryDistPath(options.projectRoot), 'utf8');
181
+ }
182
+ export class ElectronOverlayInjector {
183
+ cdpPort;
184
+ bootstrapScript;
185
+ connections = new Map();
186
+ syncTimer = null;
187
+ syncing = false;
188
+ constructor(cdpPort, bootstrapScript) {
189
+ this.cdpPort = cdpPort;
190
+ this.bootstrapScript = bootstrapScript;
191
+ }
192
+ static async create(options) {
193
+ const bundleSource = await loadElectronOverlayBundleSource(options);
194
+ const bootstrapScript = buildElectronOverlayBootstrapScript({
195
+ bundleSource,
196
+ appUrl: buildElectronOverlayAppUrl(getElectronServeOrigin(options.servePort)),
197
+ });
198
+ return new ElectronOverlayInjector(options.cdpPort, bootstrapScript);
199
+ }
200
+ async start() {
201
+ await this.syncTargets();
202
+ this.syncTimer = setInterval(() => {
203
+ void this.syncTargets();
204
+ }, ELECTRON_OVERLAY_SYNC_INTERVAL_MS);
205
+ }
206
+ stop() {
207
+ if (this.syncTimer) {
208
+ clearInterval(this.syncTimer);
209
+ this.syncTimer = null;
210
+ }
211
+ for (const connection of this.connections.values()) {
212
+ try {
213
+ connection.close();
214
+ }
215
+ catch {
216
+ // Ignore connection cleanup failures.
217
+ }
218
+ }
219
+ this.connections.clear();
220
+ }
221
+ async syncTargets() {
222
+ if (this.syncing)
223
+ return;
224
+ this.syncing = true;
225
+ try {
226
+ const response = await fetch(`http://127.0.0.1:${this.cdpPort}/json/list`);
227
+ if (!response.ok) {
228
+ throw new Error(`CDP target listing failed with ${response.status} ${response.statusText}`);
229
+ }
230
+ const targets = (await response.json());
231
+ const injectableTargets = targets.filter(shouldInjectElectronOverlayTarget);
232
+ const liveConnectionIds = new Set(injectableTargets.map((target) => target.webSocketDebuggerUrl));
233
+ for (const [targetId, connection] of this.connections.entries()) {
234
+ if (liveConnectionIds.has(targetId))
235
+ continue;
236
+ try {
237
+ connection.close();
238
+ }
239
+ catch {
240
+ // Ignore stale connection cleanup failures.
241
+ }
242
+ this.connections.delete(targetId);
243
+ }
244
+ for (const target of injectableTargets) {
245
+ const targetId = target.webSocketDebuggerUrl;
246
+ if (this.connections.has(targetId))
247
+ continue;
248
+ this.connectToTarget(target);
249
+ }
250
+ }
251
+ catch (error) {
252
+ const message = error instanceof Error ? error.message : String(error);
253
+ console.error('[electron-float] Overlay sync failed:', message);
254
+ }
255
+ finally {
256
+ this.syncing = false;
257
+ }
258
+ }
259
+ connectToTarget(target) {
260
+ const targetId = target.webSocketDebuggerUrl;
261
+ const ws = new WebSocket(targetId);
262
+ this.connections.set(targetId, ws);
263
+ let messageId = 1;
264
+ const send = (method, params) => {
265
+ ws.send(JSON.stringify({ id: messageId++, method, params }));
266
+ };
267
+ ws.on('open', () => {
268
+ send('Page.enable');
269
+ send('Runtime.enable');
270
+ send('Page.addScriptToEvaluateOnNewDocument', { source: this.bootstrapScript });
271
+ send('Runtime.evaluate', { expression: this.bootstrapScript, awaitPromise: false });
272
+ console.log(`[electron-float] Overlay injector attached to ${target.url}`);
273
+ });
274
+ ws.on('close', () => {
275
+ if (this.connections.get(targetId) === ws) {
276
+ this.connections.delete(targetId);
277
+ }
278
+ });
279
+ ws.on('error', (error) => {
280
+ const message = error instanceof Error ? error.message : String(error);
281
+ console.error(`[electron-float] Overlay target connection failed for ${target.url}:`, message);
282
+ if (this.connections.get(targetId) === ws) {
283
+ this.connections.delete(targetId);
284
+ }
285
+ });
286
+ }
287
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,183 @@
1
+ import { spawn } from 'child_process';
2
+ import { readFile } from 'fs/promises';
3
+ import { resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { app, BrowserWindow, session } from 'electron';
6
+ import { buildElectronOverlayAppUrl, buildElectronOverlayEntryUrl, buildElectronOverlayInjectionCall, buildElectronServerSpawnConfig, getElectronOverlayEntryDistPath, getElectronServeOrigin, parseElectronFloatFlags, } from './electron-runtime.js';
7
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
8
+ const PROJECT_ROOT = resolve(__dirname, '..', '..');
9
+ const FLAGS = parseElectronFloatFlags(process.argv.slice(2));
10
+ const SERVE_ORIGIN = getElectronServeOrigin(FLAGS.servePort);
11
+ const OVERLAY_APP_URL = buildElectronOverlayAppUrl(SERVE_ORIGIN);
12
+ const ELECTRON_PARTITION = 'persist:slicc-electron-float';
13
+ app.commandLine.appendSwitch('remote-debugging-port', String(FLAGS.cdpPort));
14
+ let cliServerProcess = null;
15
+ let quitting = false;
16
+ function pipeChildOutput(child, label) {
17
+ child.stdout?.on('data', (data) => {
18
+ process.stdout.write(`[${label}:out] ${data}`);
19
+ });
20
+ child.stderr?.on('data', (data) => {
21
+ process.stderr.write(`[${label}:err] ${data}`);
22
+ });
23
+ }
24
+ async function waitForServerReady(origin, retries = 60, delayMs = 500) {
25
+ for (let i = 0; i < retries; i += 1) {
26
+ try {
27
+ const response = await fetch(origin);
28
+ if (response.ok || response.status < 500)
29
+ return;
30
+ }
31
+ catch {
32
+ // Retry until the server starts listening.
33
+ }
34
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, delayMs));
35
+ }
36
+ throw new Error(`Electron float server did not become ready at ${origin}`);
37
+ }
38
+ async function loadOverlayBundleSource() {
39
+ if (FLAGS.dev) {
40
+ const response = await fetch(buildElectronOverlayEntryUrl(SERVE_ORIGIN));
41
+ if (!response.ok) {
42
+ throw new Error(`Failed to fetch electron overlay entry: ${response.status} ${response.statusText}`);
43
+ }
44
+ return await response.text();
45
+ }
46
+ return await readFile(getElectronOverlayEntryDistPath(PROJECT_ROOT), 'utf8');
47
+ }
48
+ async function injectOverlay(window) {
49
+ const bundleSource = await loadOverlayBundleSource();
50
+ await window.webContents.executeJavaScript(bundleSource, true);
51
+ await window.webContents.executeJavaScript(buildElectronOverlayInjectionCall({ appUrl: OVERLAY_APP_URL }), true);
52
+ }
53
+ function wireOverlayReinjection(window) {
54
+ const reinject = () => {
55
+ void injectOverlay(window).catch((error) => {
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ console.error('[electron-float] Overlay injection failed:', message);
58
+ });
59
+ };
60
+ window.webContents.on('did-finish-load', reinject);
61
+ window.webContents.on('did-navigate-in-page', reinject);
62
+ }
63
+ function configureElectronSession() {
64
+ const electronSession = session.fromPartition(ELECTRON_PARTITION);
65
+ electronSession.webRequest.onHeadersReceived((details, callback) => {
66
+ const responseHeaders = { ...(details.responseHeaders ?? {}) };
67
+ delete responseHeaders['content-security-policy'];
68
+ delete responseHeaders['Content-Security-Policy'];
69
+ delete responseHeaders['content-security-policy-report-only'];
70
+ delete responseHeaders['Content-Security-Policy-Report-Only'];
71
+ callback({ responseHeaders });
72
+ });
73
+ }
74
+ async function createFloatWindow(targetUrl) {
75
+ const window = new BrowserWindow({
76
+ width: 1440,
77
+ height: 960,
78
+ minWidth: 1024,
79
+ minHeight: 720,
80
+ autoHideMenuBar: true,
81
+ title: 'slicc electron float',
82
+ webPreferences: {
83
+ partition: ELECTRON_PARTITION,
84
+ contextIsolation: true,
85
+ nodeIntegration: false,
86
+ sandbox: true,
87
+ allowRunningInsecureContent: true,
88
+ },
89
+ });
90
+ wireOverlayReinjection(window);
91
+ window.webContents.setWindowOpenHandler(({ url }) => {
92
+ void createFloatWindow(url);
93
+ return { action: 'deny' };
94
+ });
95
+ try {
96
+ await window.loadURL(targetUrl);
97
+ }
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ console.error(`[electron-float] Failed to load ${targetUrl}: ${message}`);
101
+ await window.loadURL('about:blank');
102
+ }
103
+ return window;
104
+ }
105
+ function startCliServer() {
106
+ const spawnConfig = buildElectronServerSpawnConfig(PROJECT_ROOT, {
107
+ dev: FLAGS.dev,
108
+ cdpPort: FLAGS.cdpPort,
109
+ nodePath: process.env['npm_node_execpath'] ?? 'node',
110
+ });
111
+ const child = spawn(spawnConfig.command, spawnConfig.args, {
112
+ cwd: PROJECT_ROOT,
113
+ env: {
114
+ ...process.env,
115
+ PORT: String(FLAGS.servePort),
116
+ },
117
+ stdio: ['ignore', 'pipe', 'pipe'],
118
+ });
119
+ pipeChildOutput(child, 'electron-server');
120
+ child.on('exit', (code) => {
121
+ if (quitting)
122
+ return;
123
+ console.error(`[electron-float] CLI server exited unexpectedly with code ${code}`);
124
+ app.quit();
125
+ });
126
+ return child;
127
+ }
128
+ async function stopCliServer() {
129
+ if (!cliServerProcess)
130
+ return;
131
+ const child = cliServerProcess;
132
+ cliServerProcess = null;
133
+ await new Promise((resolvePromise) => {
134
+ let resolved = false;
135
+ const finish = () => {
136
+ if (resolved)
137
+ return;
138
+ resolved = true;
139
+ resolvePromise();
140
+ };
141
+ child.once('exit', finish);
142
+ child.kill('SIGTERM');
143
+ setTimeout(() => {
144
+ if (child.exitCode === null) {
145
+ try {
146
+ child.kill('SIGKILL');
147
+ }
148
+ catch {
149
+ // Ignore final cleanup failures.
150
+ }
151
+ }
152
+ finish();
153
+ }, 3000);
154
+ });
155
+ }
156
+ async function main() {
157
+ await app.whenReady();
158
+ configureElectronSession();
159
+ cliServerProcess = startCliServer();
160
+ await waitForServerReady(SERVE_ORIGIN);
161
+ await createFloatWindow(FLAGS.targetUrl);
162
+ app.on('activate', () => {
163
+ if (BrowserWindow.getAllWindows().length === 0) {
164
+ void createFloatWindow(FLAGS.targetUrl);
165
+ }
166
+ });
167
+ }
168
+ app.on('before-quit', () => {
169
+ quitting = true;
170
+ });
171
+ app.on('window-all-closed', () => {
172
+ app.quit();
173
+ });
174
+ app.on('will-quit', () => {
175
+ void stopCliServer();
176
+ });
177
+ main().catch((error) => {
178
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
179
+ console.error('[electron-float] Fatal error:', message);
180
+ void stopCliServer().finally(() => {
181
+ app.exit(1);
182
+ });
183
+ });
@@ -0,0 +1,58 @@
1
+ export interface ElectronFloatFlags {
2
+ dev: boolean;
3
+ cdpPort: number;
4
+ servePort: number;
5
+ targetUrl: string;
6
+ }
7
+ export interface ElectronServerSpawnConfig {
8
+ command: string;
9
+ args: string[];
10
+ }
11
+ export interface ElectronAppLaunchSpec {
12
+ command: string;
13
+ args: string[];
14
+ displayName: string;
15
+ resolvedAppPath: string;
16
+ processMatchPatterns: string[];
17
+ }
18
+ export interface ElectronInspectableTarget {
19
+ type: string;
20
+ url: string;
21
+ webSocketDebuggerUrl?: string;
22
+ }
23
+ export declare const DEFAULT_ELECTRON_SERVE_PORT = 5710;
24
+ export declare const DEFAULT_ELECTRON_SERVE_HOST = "localhost";
25
+ export declare const DEFAULT_ELECTRON_CDP_PORT = 9223;
26
+ export declare const DEFAULT_ELECTRON_TARGET_URL = "about:blank";
27
+ export declare const DEFAULT_ELECTRON_OVERLAY_TAB = "chat";
28
+ export declare const ELECTRON_OVERLAY_APP_PATH = "/electron";
29
+ export declare function getElectronAppDisplayName(appPath: string): string;
30
+ export declare function resolveElectronAppExecutablePath(appPath: string, platform?: NodeJS.Platform): string;
31
+ export declare function buildElectronAppProcessMatchPatterns(appPath: string, platform?: NodeJS.Platform): string[];
32
+ export declare function buildElectronAppLaunchSpec(appPath: string, options: {
33
+ cdpPort: number;
34
+ platform?: NodeJS.Platform;
35
+ }): ElectronAppLaunchSpec;
36
+ export declare function parseElectronFloatFlags(argv: string[], env?: Record<string, string | undefined>): ElectronFloatFlags;
37
+ export declare function buildElectronServerSpawnConfig(projectRoot: string, options: {
38
+ dev: boolean;
39
+ cdpPort: number;
40
+ platform?: NodeJS.Platform;
41
+ nodePath?: string;
42
+ }): ElectronServerSpawnConfig;
43
+ export declare function getElectronServeOrigin(servePort: number): string;
44
+ export declare function buildElectronOverlayAppUrl(serveOrigin: string, activeTab?: string): string;
45
+ export declare function buildElectronOverlayEntryUrl(serveOrigin: string): string;
46
+ export declare function getElectronOverlayEntryDistPath(projectRoot: string): string;
47
+ export declare function buildElectronOverlayInjectionCall(options: {
48
+ appUrl: string;
49
+ open?: boolean;
50
+ activeTab?: string;
51
+ }): string;
52
+ export declare function buildElectronOverlayBootstrapScript(options: {
53
+ bundleSource: string;
54
+ appUrl: string;
55
+ open?: boolean;
56
+ activeTab?: string;
57
+ }): string;
58
+ export declare function shouldInjectElectronOverlayTarget(target: ElectronInspectableTarget): boolean;
@@ -0,0 +1,133 @@
1
+ import { basename, join, resolve } from 'path';
2
+ export const DEFAULT_ELECTRON_SERVE_PORT = 5710;
3
+ export const DEFAULT_ELECTRON_SERVE_HOST = 'localhost';
4
+ export const DEFAULT_ELECTRON_CDP_PORT = 9223;
5
+ export const DEFAULT_ELECTRON_TARGET_URL = 'about:blank';
6
+ export const DEFAULT_ELECTRON_OVERLAY_TAB = 'chat';
7
+ export const ELECTRON_OVERLAY_APP_PATH = '/electron';
8
+ export function getElectronAppDisplayName(appPath) {
9
+ const trimmedPath = appPath.replace(/[\\/]+$/, '');
10
+ const fileName = basename(trimmedPath);
11
+ if (fileName.toLowerCase().endsWith('.app')) {
12
+ return fileName.slice(0, -'.app'.length) || fileName;
13
+ }
14
+ return fileName || trimmedPath;
15
+ }
16
+ export function resolveElectronAppExecutablePath(appPath, platform = process.platform) {
17
+ const resolvedAppPath = resolve(appPath);
18
+ if (platform === 'darwin' && resolvedAppPath.toLowerCase().endsWith('.app')) {
19
+ return join(resolvedAppPath, 'Contents', 'MacOS', getElectronAppDisplayName(resolvedAppPath));
20
+ }
21
+ return resolvedAppPath;
22
+ }
23
+ export function buildElectronAppProcessMatchPatterns(appPath, platform = process.platform) {
24
+ return Array.from(new Set([
25
+ resolve(appPath),
26
+ resolveElectronAppExecutablePath(appPath, platform),
27
+ ]));
28
+ }
29
+ export function buildElectronAppLaunchSpec(appPath, options) {
30
+ const platform = options.platform ?? process.platform;
31
+ const resolvedAppPath = resolve(appPath);
32
+ const displayName = getElectronAppDisplayName(resolvedAppPath);
33
+ const executablePath = resolveElectronAppExecutablePath(resolvedAppPath, platform);
34
+ return {
35
+ command: executablePath,
36
+ args: [`--remote-debugging-port=${options.cdpPort}`],
37
+ displayName,
38
+ resolvedAppPath,
39
+ processMatchPatterns: buildElectronAppProcessMatchPatterns(resolvedAppPath, platform),
40
+ };
41
+ }
42
+ function parsePositiveInt(value, fallback) {
43
+ if (!value)
44
+ return fallback;
45
+ const parsed = Number.parseInt(value, 10);
46
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
47
+ }
48
+ export function parseElectronFloatFlags(argv, env = process.env) {
49
+ let dev = false;
50
+ let cdpPort = DEFAULT_ELECTRON_CDP_PORT;
51
+ let targetUrl = DEFAULT_ELECTRON_TARGET_URL;
52
+ for (const arg of argv) {
53
+ if (arg === '--dev') {
54
+ dev = true;
55
+ continue;
56
+ }
57
+ if (arg.startsWith('--cdp-port=')) {
58
+ cdpPort = parsePositiveInt(arg.slice('--cdp-port='.length), DEFAULT_ELECTRON_CDP_PORT);
59
+ continue;
60
+ }
61
+ if (arg.startsWith('--target-url=')) {
62
+ const value = arg.slice('--target-url='.length).trim();
63
+ targetUrl = value || DEFAULT_ELECTRON_TARGET_URL;
64
+ continue;
65
+ }
66
+ if (!arg.startsWith('--')) {
67
+ targetUrl = arg.trim() || DEFAULT_ELECTRON_TARGET_URL;
68
+ }
69
+ }
70
+ return {
71
+ dev,
72
+ cdpPort,
73
+ servePort: parsePositiveInt(env['PORT'], DEFAULT_ELECTRON_SERVE_PORT),
74
+ targetUrl,
75
+ };
76
+ }
77
+ export function buildElectronServerSpawnConfig(projectRoot, options) {
78
+ if (options.dev) {
79
+ return {
80
+ command: (options.platform ?? process.platform) === 'win32' ? 'npx.cmd' : 'npx',
81
+ args: ['tsx', 'src/cli/index.ts', '--dev', '--serve-only', `--cdp-port=${options.cdpPort}`],
82
+ };
83
+ }
84
+ return {
85
+ command: options.nodePath ?? process.env['npm_node_execpath'] ?? 'node',
86
+ args: [resolve(projectRoot, 'dist/cli/index.js'), '--serve-only', `--cdp-port=${options.cdpPort}`],
87
+ };
88
+ }
89
+ export function getElectronServeOrigin(servePort) {
90
+ return `http://${DEFAULT_ELECTRON_SERVE_HOST}:${servePort}`;
91
+ }
92
+ export function buildElectronOverlayAppUrl(serveOrigin, activeTab = DEFAULT_ELECTRON_OVERLAY_TAB) {
93
+ const url = new URL(ELECTRON_OVERLAY_APP_PATH, serveOrigin);
94
+ if (activeTab && activeTab !== DEFAULT_ELECTRON_OVERLAY_TAB) {
95
+ url.searchParams.set('tab', activeTab);
96
+ }
97
+ return url.toString();
98
+ }
99
+ export function buildElectronOverlayEntryUrl(serveOrigin) {
100
+ return new URL('/electron-overlay-entry.js', serveOrigin).toString();
101
+ }
102
+ export function getElectronOverlayEntryDistPath(projectRoot) {
103
+ return resolve(projectRoot, 'dist/ui/electron-overlay-entry.js');
104
+ }
105
+ export function buildElectronOverlayInjectionCall(options) {
106
+ const payload = {
107
+ appUrl: options.appUrl,
108
+ };
109
+ if (typeof options.open === 'boolean') {
110
+ payload['open'] = options.open;
111
+ }
112
+ if (options.activeTab) {
113
+ payload['activeTab'] = options.activeTab;
114
+ }
115
+ return `window.__SLICC_ELECTRON_OVERLAY__?.inject(${JSON.stringify(payload)});`;
116
+ }
117
+ export function buildElectronOverlayBootstrapScript(options) {
118
+ return `${options.bundleSource}\n${buildElectronOverlayInjectionCall(options)}`;
119
+ }
120
+ export function shouldInjectElectronOverlayTarget(target) {
121
+ if (target.type !== 'page' || !target.webSocketDebuggerUrl)
122
+ return false;
123
+ const url = target.url.trim();
124
+ if (!url)
125
+ return false;
126
+ if (url.startsWith('devtools://'))
127
+ return false;
128
+ if (url.startsWith('chrome://'))
129
+ return false;
130
+ if (url.startsWith('chrome-extension://'))
131
+ return false;
132
+ return true;
133
+ }