@pi-chamber/web 0.1.9 → 0.2.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/bin/cli.test.js +48 -0
- package/bin/lib/DOCUMENTATION.md +2 -2
- package/bin/lib/cli-args.js +38 -11
- package/bin/lib/cli-startup.js +60 -13
- package/bin/lib/commands-startup.js +20 -5
- package/bin/lib/commands-update.js +91 -34
- package/bin/lib/commands-update.test.js +57 -42
- package/dist/assets/CommitSection-LewzHfDH.js +1 -0
- package/dist/assets/DiagramEditor-BOIJVnh3.js +1 -0
- package/dist/assets/DiagramView-CdNHezna.js +1 -0
- package/dist/assets/{DiffView-k3lX2PiV.js → DiffView-Cmw-RHIH.js} +2 -2
- package/dist/assets/{FilesView-C39IOFfs.js → FilesView-BODbL0Xm.js} +6 -6
- package/dist/assets/{GitView-CkztSaT7.js → GitView-DqYF-n_h.js} +4 -4
- package/dist/assets/JsonTreeView-DzEjA1ht.js +1 -0
- package/dist/assets/{MarkdownRendererImpl-CF12jEtB.js → MarkdownRendererImpl-Bfq0bOSM.js} +2 -2
- package/dist/assets/PatchDiff-CtVByTC4.js +1 -0
- package/dist/assets/{PierreDiffViewer-DSQwk05R.js → PierreDiffViewer-Cwoewmiu.js} +2 -2
- package/dist/assets/{useAppFontEffects-DqOhrZWx.js → SessionSwitcherDropdown-DNjCJjzF.js} +97 -97
- package/dist/assets/{SettingsView-Bf4vWwVK.js → SettingsView-BtLvHlj4.js} +1 -1
- package/dist/assets/SettingsWindow-CPd7JmJs.js +1 -0
- package/dist/assets/{ToolOutputDialog-Brg2Humk.js → ToolOutputDialog-CFFC5eI-.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-yFq1DKSA.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BELx71mz.js → VirtualizedFileDiff-CC5lTPJS.js} +1 -1
- package/dist/assets/collapsible-D_555Hw_.js +1 -0
- package/dist/assets/computeVirtualFileMetrics-BcVuV0Ic.js +3 -0
- package/dist/assets/contextFileOpenGuard-BygvjHuE.js +1 -0
- package/dist/assets/index-C9CLY-is.js +1 -0
- package/dist/assets/{index-Dnby-sDS.js → index-DQr8-8RF.js} +2 -2
- package/dist/assets/index-DYrJXRkU.css +1 -0
- package/dist/assets/isDiffPlainText-Bher0UxW.js +1 -0
- package/dist/assets/isFilePlainText-EIViqOlI.js +1 -0
- package/dist/assets/main-1bIgjP48.js +2 -0
- package/dist/assets/main-JDtt2bQS.js +133 -0
- package/dist/assets/{markdownCore-BK98alzY.js → markdownCore-Bm4FxtK1.js} +2 -2
- package/dist/assets/miniChat-B7kKqKHH.js +2 -0
- package/dist/assets/mobile-DZPRce5z.js +2 -0
- package/dist/assets/renderElectronMiniChatApp-Dq0GdThJ.js +1 -0
- package/dist/assets/renderMobileApp-DlKnjObC.js +23 -0
- package/dist/assets/{runtimeSurface-DMXukysz.js → runtimeSurface-B6sKTjo7.js} +1 -1
- package/dist/assets/{useInlineCommentController-ruw8wb2Y.js → useInlineCommentController-CSR_Ju0a.js} +2 -2
- package/dist/assets/usePanelSlide-BkonU4LS.js +47 -0
- package/dist/assets/{useWindowTitle-qt23iibv.js → useWindowTitle-rt_8xbd9.js} +3 -3
- package/dist/assets/{vendor-codemirror-language-data-DVYP3hf_.js → vendor-codemirror-language-data-Be-zDX0u.js} +2 -2
- package/dist/assets/{web-components-P2py0R8j.js → web-components-DgFD1Jms.js} +4 -4
- package/dist/index.html +4 -4
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/lib/package-manager.js +44 -65
- package/server/lib/package-manager.test.js +27 -0
- package/server/lib/pi/routes.js +6 -0
- package/server/lib/pi/routes.test.js +32 -0
- package/server/lib/pi/session-daemon/DOCUMENTATION.md +1 -1
- package/server/lib/pi/session-daemon/session-daemon.js +11 -4
- package/server/lib/pi/session-daemon/session-daemon.test.js +24 -0
- package/dist/assets/CommitSection-DSFuOWuL.js +0 -1
- package/dist/assets/DiagramEditor-D6W6zeV7.js +0 -1
- package/dist/assets/DiagramView-r89Qi5ON.js +0 -1
- package/dist/assets/JsonTreeView-C6qt2uIj.js +0 -1
- package/dist/assets/PatchDiff-D6In_lUU.js +0 -1
- package/dist/assets/SessionSwitcherDropdown-CvE7hsxG.js +0 -1
- package/dist/assets/SettingsWindow-B8IV35H4.js +0 -1
- package/dist/assets/ToolPartDiffPreview-rlz2ipa2.js +0 -8
- package/dist/assets/collapsible-QoPrpZP_.js +0 -1
- package/dist/assets/computeVirtualFileMetrics-CInbUZiB.js +0 -3
- package/dist/assets/contextFileOpenGuard-BOtkNMZy.js +0 -1
- package/dist/assets/index-4sRd3SSM.js +0 -1
- package/dist/assets/index-NYG3k04a.css +0 -1
- package/dist/assets/isDiffPlainText-BquUayus.js +0 -1
- package/dist/assets/isFilePlainText-D4iUTBdr.js +0 -1
- package/dist/assets/main-Cp3YZZm1.js +0 -2
- package/dist/assets/main-CyshB9VI.js +0 -134
- package/dist/assets/miniChat-C_A4lnrT.js +0 -2
- package/dist/assets/mobile-2dXx6cUI.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-HL15h5xo.js +0 -1
- package/dist/assets/renderMobileApp-w6YFCJhq.js +0 -23
- package/dist/assets/runtimeEndpointReset-DiVqZFZB.js +0 -46
- package/dist/assets/useOpenInAppsStore-DVr2oGEm.js +0 -1
- /package/dist/assets/{runtimeEndpointReset-BV3KOtdA.css → usePanelSlide-BV3KOtdA.css} +0 -0
package/bin/cli.test.js
CHANGED
|
@@ -289,6 +289,17 @@ describe('cli args', () => {
|
|
|
289
289
|
expect(parsed.options.port).toBe(3002);
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
+
it('parses startup lan bind and port flags', () => {
|
|
293
|
+
const parsed = parseArgs(['startup', 'enable', '--lan', '--port', '3002', '--ui-password', 'secret']);
|
|
294
|
+
|
|
295
|
+
expect(parsed.command).toBe('startup');
|
|
296
|
+
expect(parsed.startupAction).toBe('enable');
|
|
297
|
+
expect(parsed.options.lan).toBe(true);
|
|
298
|
+
expect(parsed.options.host).toBe('0.0.0.0');
|
|
299
|
+
expect(parsed.options.port).toBe(3002);
|
|
300
|
+
expect(parsed.options.uiPassword).toBe('secret');
|
|
301
|
+
});
|
|
302
|
+
|
|
292
303
|
it('parses tunnel auto-start server options', () => {
|
|
293
304
|
const parsed = parseArgs(['tunnel', 'start', '--port', '3002', '--api-only', '--lan', '--ui-password', 'secret']);
|
|
294
305
|
|
|
@@ -321,6 +332,43 @@ describe('cli args', () => {
|
|
|
321
332
|
});
|
|
322
333
|
});
|
|
323
334
|
|
|
335
|
+
describe('startup enable command helper', () => {
|
|
336
|
+
it('formats stored serve flags for a local port', async () => {
|
|
337
|
+
const { formatStartupServeCommand } = await import('./lib/cli-startup.js');
|
|
338
|
+
expect(formatStartupServeCommand({ port: 8080 })).toBe('pichamber serve --foreground --port 8080');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('formats lan, port, and password flags without echoing the secret', async () => {
|
|
342
|
+
const { formatStartupServeCommand } = await import('./lib/cli-startup.js');
|
|
343
|
+
expect(formatStartupServeCommand({
|
|
344
|
+
lan: true,
|
|
345
|
+
host: '0.0.0.0',
|
|
346
|
+
port: 3002,
|
|
347
|
+
uiPassword: 'secret',
|
|
348
|
+
})).toBe('pichamber serve --foreground --port 3002 --lan --ui-password');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('documents lan, port, and ui-password in startup help', async () => {
|
|
352
|
+
const { showStartupHelp } = await import('./lib/cli-args.js');
|
|
353
|
+
const lines = [];
|
|
354
|
+
const original = console.log;
|
|
355
|
+
console.log = (message) => {
|
|
356
|
+
lines.push(String(message));
|
|
357
|
+
};
|
|
358
|
+
try {
|
|
359
|
+
showStartupHelp();
|
|
360
|
+
} finally {
|
|
361
|
+
console.log = original;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const text = lines.join('\n');
|
|
365
|
+
expect(text).toContain('--lan');
|
|
366
|
+
expect(text).toContain('--port <port>');
|
|
367
|
+
expect(text).toContain('--ui-password');
|
|
368
|
+
expect(text).toContain('pichamber startup enable --lan --port 3002 --ui-password');
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
324
372
|
describe('cli API target resolution', () => {
|
|
325
373
|
it('uses an explicit port without discovery', async () => {
|
|
326
374
|
await expect(resolveTargetPort(
|
package/bin/lib/DOCUMENTATION.md
CHANGED
|
@@ -8,9 +8,9 @@ This directory contains non-entrypoint PiChamber CLI implementation. `../cli.js`
|
|
|
8
8
|
- `commands-lifecycle.js`: stop and restart behavior.
|
|
9
9
|
- `commands-status.js`: running-instance and tunnel status presentation.
|
|
10
10
|
- `commands-logs.js`: log discovery, tailing, and follow behavior.
|
|
11
|
-
- `commands-startup.js`: native startup service management.
|
|
11
|
+
- `commands-startup.js`: native startup service management. `startup enable` stores serve flags (`--port`, `--lan`/`--host`, `--ui-password`, `--api-only`) in the login service. `pichamber startup --help` documents those flags. LAN binds require a UI password, matching `pichamber serve`.
|
|
12
12
|
- `commands-connect-url.js`: authenticated direct/relay pairing links.
|
|
13
|
-
- `commands-update.js`: package update and restart coordination.
|
|
13
|
+
- `commands-update.js`: package update and restart coordination. Updates only a global install owned by the running CLI. A Linux user systemd unit is restarted in place; the command does not spawn a replacement server on the same port.
|
|
14
14
|
- `commands-tunnel.js`: tunnel lifecycle and profile management.
|
|
15
15
|
|
|
16
16
|
## Rules
|
package/bin/lib/cli-args.js
CHANGED
|
@@ -522,7 +522,8 @@ EXAMPLES:
|
|
|
522
522
|
pichamber serve --foreground # Start in foreground (for systemd Type=simple)
|
|
523
523
|
pichamber connect-url --port 3000 --qr
|
|
524
524
|
pichamber connect-url --server https://pichamber.example.com
|
|
525
|
-
pichamber startup enable
|
|
525
|
+
pichamber startup enable --lan --port 3002 --ui-password
|
|
526
|
+
# Login service on LAN at 0.0.0.0:3002
|
|
526
527
|
pichamber tunnel help # Show tunnel lifecycle help
|
|
527
528
|
pichamber logs # Follow logs for latest running instance
|
|
528
529
|
`);
|
|
@@ -535,25 +536,51 @@ function showStartupHelp() {
|
|
|
535
536
|
USAGE:
|
|
536
537
|
pichamber startup <SUBCOMMAND> [OPTIONS]
|
|
537
538
|
|
|
539
|
+
DESCRIPTION:
|
|
540
|
+
Installs a user-login service that runs pichamber serve --foreground.
|
|
541
|
+
Flags passed to startup enable are stored in that service. Re-run enable
|
|
542
|
+
to replace the stored command.
|
|
543
|
+
|
|
538
544
|
SUBCOMMANDS:
|
|
539
545
|
status Show startup integration status
|
|
540
546
|
enable Install and start native user startup integration
|
|
541
547
|
disable Stop and remove native user startup integration
|
|
542
548
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
--
|
|
548
|
-
--
|
|
549
|
-
--
|
|
549
|
+
ENABLE FLAGS:
|
|
550
|
+
These become the serve command the login service runs.
|
|
551
|
+
|
|
552
|
+
-p, --port <port> Listen port (default: ${DEFAULT_PORT})
|
|
553
|
+
--host <address> Bind address (default: 127.0.0.1, this machine only)
|
|
554
|
+
--hostname <address> Alias for --host
|
|
555
|
+
--lan Same as --host 0.0.0.0 — reachable on your LAN/Wi-Fi
|
|
556
|
+
--ui-password [secret] Required with --lan (omit the value to generate one)
|
|
557
|
+
--api-only API routes only, no browser UI
|
|
558
|
+
--no-env-snapshot Do not copy the current shell environment
|
|
559
|
+
--json Machine-readable JSON
|
|
550
560
|
-q, --quiet Suppress non-essential output
|
|
551
561
|
|
|
562
|
+
LAN ACCESS:
|
|
563
|
+
Other devices on the same network need --lan and a UI password:
|
|
564
|
+
|
|
565
|
+
pichamber startup enable --lan --port 3002 --ui-password
|
|
566
|
+
|
|
567
|
+
Then open http://<this-computer-lan-ip>:3002 on those devices.
|
|
568
|
+
|
|
552
569
|
EXAMPLES:
|
|
553
570
|
pichamber startup enable
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
pichamber startup
|
|
571
|
+
This machine only, port ${DEFAULT_PORT}
|
|
572
|
+
|
|
573
|
+
pichamber startup enable --port 8080
|
|
574
|
+
This machine only, port 8080
|
|
575
|
+
|
|
576
|
+
pichamber startup enable --lan --port 3002 --ui-password
|
|
577
|
+
LAN/Wi-Fi on port 3002 (password generated if omitted)
|
|
578
|
+
|
|
579
|
+
pichamber startup enable --host 0.0.0.0 --port 3002 --ui-password secret
|
|
580
|
+
Same as --lan, with an explicit password
|
|
581
|
+
|
|
582
|
+
pichamber startup status
|
|
583
|
+
pichamber startup disable
|
|
557
584
|
`);
|
|
558
585
|
}
|
|
559
586
|
|
package/bin/lib/cli-startup.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawnSync } from 'child_process';
|
|
|
5
5
|
import { DEFAULT_PORT } from './cli-args.js';
|
|
6
6
|
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
|
|
7
7
|
import { getDataDir } from './cli-paths.js';
|
|
8
|
-
import { hasUiPasswordConfigured } from './cli-network.js';
|
|
8
|
+
import { assertAuthenticatedNetworkExposure, hasUiPasswordConfigured, resolveServeUiPassword } from './cli-network.js';
|
|
9
9
|
|
|
10
10
|
const STARTUP_SERVICE_ID = 'dev.pichamber.web';
|
|
11
11
|
|
|
@@ -161,11 +161,24 @@ function resolveCliEntrypoint() {
|
|
|
161
161
|
const entry = typeof process.argv[1] === 'string' && process.argv[1].trim().length > 0
|
|
162
162
|
? process.argv[1]
|
|
163
163
|
: path.join(__dirname, 'cli.js');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
// Keep the invoked path, not realpath(). pnpm/npm shims point at a stable
|
|
165
|
+
// node_modules entry; resolving through the store pins a versioned folder
|
|
166
|
+
// that breaks after `pichamber update`.
|
|
167
|
+
return path.resolve(entry);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isUserStartupServiceActive() {
|
|
171
|
+
if (process.platform !== 'linux') return false;
|
|
172
|
+
const result = runStartupCommand('systemctl', ['--user', 'is-active', 'pichamber.service'], { allowFailure: true });
|
|
173
|
+
return (result.stdout || '').trim() === 'active';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function restartUserStartupService() {
|
|
177
|
+
if (process.platform !== 'linux') {
|
|
178
|
+
throw new Error('Startup service restart is only supported on Linux systemd user units.');
|
|
168
179
|
}
|
|
180
|
+
runStartupCommand('systemctl', ['--user', 'daemon-reload']);
|
|
181
|
+
runStartupCommand('systemctl', ['--user', 'restart', 'pichamber.service']);
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
function buildStartupArgs(options = {}) {
|
|
@@ -179,6 +192,22 @@ function buildStartupArgs(options = {}) {
|
|
|
179
192
|
return args;
|
|
180
193
|
}
|
|
181
194
|
|
|
195
|
+
function formatStartupServeCommand(options = {}) {
|
|
196
|
+
const parts = ['pichamber', 'serve', '--foreground', '--port', String(options.port || DEFAULT_PORT)];
|
|
197
|
+
if (options.lan === true && (typeof options.host !== 'string' || options.host === '0.0.0.0')) {
|
|
198
|
+
parts.push('--lan');
|
|
199
|
+
} else if (typeof options.host === 'string' && options.host.length > 0) {
|
|
200
|
+
parts.push('--host', options.host);
|
|
201
|
+
}
|
|
202
|
+
if (options.apiOnly === true) {
|
|
203
|
+
parts.push('--api-only');
|
|
204
|
+
}
|
|
205
|
+
if (options.explicitUiPassword === true || hasUiPasswordConfigured(options.uiPassword)) {
|
|
206
|
+
parts.push('--ui-password');
|
|
207
|
+
}
|
|
208
|
+
return parts.join(' ');
|
|
209
|
+
}
|
|
210
|
+
|
|
182
211
|
function writeMacosStartupWrapper(options = {}) {
|
|
183
212
|
const wrapperPath = getMacosStartupWrapperPath();
|
|
184
213
|
const args = buildStartupArgs(options).map(startupShellQuote).join(' ');
|
|
@@ -299,28 +328,43 @@ function enableStartupService(options = {}) {
|
|
|
299
328
|
throw new TunnelCliError(`Startup integration is not supported on ${paths.platform}.`, EXIT_CODE.USAGE_ERROR);
|
|
300
329
|
}
|
|
301
330
|
|
|
331
|
+
const resolvedUiPassword = resolveServeUiPassword(options);
|
|
332
|
+
const serveOptions = {
|
|
333
|
+
...options,
|
|
334
|
+
...(resolvedUiPassword.password ? { uiPassword: resolvedUiPassword.password } : {}),
|
|
335
|
+
};
|
|
336
|
+
assertAuthenticatedNetworkExposure({
|
|
337
|
+
host: serveOptions.host,
|
|
338
|
+
uiPassword: serveOptions.uiPassword,
|
|
339
|
+
});
|
|
340
|
+
const finish = (status) => (
|
|
341
|
+
resolvedUiPassword.generated === true
|
|
342
|
+
? { ...status, generatedUiPassword: resolvedUiPassword.password }
|
|
343
|
+
: status
|
|
344
|
+
);
|
|
345
|
+
|
|
302
346
|
if (paths.platform === 'macos') {
|
|
303
347
|
removeStartupEnvFile();
|
|
304
348
|
fs.mkdirSync(path.dirname(paths.servicePath), { recursive: true, mode: 0o700 });
|
|
305
349
|
fs.mkdirSync(path.join(os.homedir(), 'Library', 'Logs', 'PiChamber'), { recursive: true, mode: 0o700 });
|
|
306
|
-
fs.writeFileSync(paths.servicePath, buildMacosLaunchAgent(
|
|
350
|
+
fs.writeFileSync(paths.servicePath, buildMacosLaunchAgent(serveOptions), { mode: 0o600 });
|
|
307
351
|
runStartupCommand('/bin/launchctl', ['bootout', `gui/${process.getuid()}`, paths.servicePath], { allowFailure: true });
|
|
308
352
|
runStartupCommand('/bin/launchctl', ['bootstrap', `gui/${process.getuid()}`, paths.servicePath]);
|
|
309
353
|
runStartupCommand('/bin/launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${STARTUP_SERVICE_ID}`], { allowFailure: true });
|
|
310
|
-
return getStartupStatus();
|
|
354
|
+
return finish(getStartupStatus());
|
|
311
355
|
}
|
|
312
356
|
|
|
313
357
|
if (paths.platform === 'linux') {
|
|
314
|
-
writeStartupEnvFile(
|
|
358
|
+
writeStartupEnvFile(serveOptions, { quoteValue: systemdEnvFileQuote });
|
|
315
359
|
fs.mkdirSync(path.dirname(paths.servicePath), { recursive: true, mode: 0o700 });
|
|
316
|
-
fs.writeFileSync(paths.servicePath, buildSystemdUserService(
|
|
360
|
+
fs.writeFileSync(paths.servicePath, buildSystemdUserService(serveOptions), { mode: 0o600 });
|
|
317
361
|
runStartupCommand('systemctl', ['--user', 'daemon-reload']);
|
|
318
362
|
runStartupCommand('systemctl', ['--user', 'enable', '--now', 'pichamber.service']);
|
|
319
|
-
return getStartupStatus();
|
|
363
|
+
return finish(getStartupStatus());
|
|
320
364
|
}
|
|
321
365
|
|
|
322
|
-
const envFilePath = writeStartupEnvFile(
|
|
323
|
-
const startupArgs = buildStartupArgs(
|
|
366
|
+
const envFilePath = writeStartupEnvFile(serveOptions);
|
|
367
|
+
const startupArgs = buildStartupArgs(serveOptions).map(powershellQuote).join(', ');
|
|
324
368
|
const powerShellCommand = [
|
|
325
369
|
`$envFile=${powershellQuote(envFilePath)}`,
|
|
326
370
|
`if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { $v=$matches[2]; if ($v.StartsWith("'") -and $v.EndsWith("'")) { $v=$v.Substring(1,$v.Length-2).Replace("'\\''","'") }; [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process') } } }`,
|
|
@@ -336,7 +380,7 @@ function enableStartupService(options = {}) {
|
|
|
336
380
|
'/TR', taskArgs,
|
|
337
381
|
]);
|
|
338
382
|
runStartupCommand('schtasks.exe', ['/Run', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
|
339
|
-
return getStartupStatus();
|
|
383
|
+
return finish(getStartupStatus());
|
|
340
384
|
}
|
|
341
385
|
|
|
342
386
|
function disableStartupService() {
|
|
@@ -368,4 +412,7 @@ export {
|
|
|
368
412
|
getStartupStatus,
|
|
369
413
|
enableStartupService,
|
|
370
414
|
disableStartupService,
|
|
415
|
+
isUserStartupServiceActive,
|
|
416
|
+
restartUserStartupService,
|
|
417
|
+
formatStartupServeCommand,
|
|
371
418
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
|
|
2
|
-
import { getStartupStatus, enableStartupService, disableStartupService } from './cli-startup.js';
|
|
2
|
+
import { getStartupStatus, enableStartupService, disableStartupService, formatStartupServeCommand } from './cli-startup.js';
|
|
3
3
|
import {
|
|
4
4
|
intro as clackIntro,
|
|
5
5
|
outro as clackOutro,
|
|
@@ -27,7 +27,18 @@ async function startupCommand(options, action = 'status') {
|
|
|
27
27
|
status = getStartupStatus();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const generatedUiPassword = typeof status.generatedUiPassword === 'string' && status.generatedUiPassword.length > 0
|
|
31
|
+
? status.generatedUiPassword
|
|
32
|
+
: undefined;
|
|
33
|
+
const { generatedUiPassword: _generatedUiPassword, ...publicStatus } = status;
|
|
34
|
+
void _generatedUiPassword;
|
|
35
|
+
const serveCommand = normalized === 'enable' ? formatStartupServeCommand(options) : undefined;
|
|
36
|
+
const result = {
|
|
37
|
+
action: normalized,
|
|
38
|
+
...publicStatus,
|
|
39
|
+
...(serveCommand ? { serveCommand } : {}),
|
|
40
|
+
...(generatedUiPassword ? { password: generatedUiPassword } : {}),
|
|
41
|
+
};
|
|
31
42
|
if (!result.supported) {
|
|
32
43
|
throw new TunnelCliError(
|
|
33
44
|
`Startup integration is not supported on ${result.platform}.`,
|
|
@@ -46,7 +57,7 @@ async function startupCommand(options, action = 'status') {
|
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
if (isQuietMode(options)) {
|
|
49
|
-
process.stdout.write(`startup ${result.enabled ? 'enabled' : 'disabled'} platform:${result.platform} supported:${result.supported ? 'yes' : 'no'}${result.servicePath ? ` path:${result.servicePath}` : ''}\n`);
|
|
60
|
+
process.stdout.write(`startup ${result.enabled ? 'enabled' : 'disabled'} platform:${result.platform} supported:${result.supported ? 'yes' : 'no'}${result.servicePath ? ` path:${result.servicePath}` : ''}${serveCommand ? ` command:${serveCommand}` : ''}${generatedUiPassword ? ` pass:${generatedUiPassword}` : ''}\n`);
|
|
50
61
|
return;
|
|
51
62
|
}
|
|
52
63
|
|
|
@@ -55,8 +66,12 @@ async function startupCommand(options, action = 'status') {
|
|
|
55
66
|
if (typeof result.activeState === 'string') {
|
|
56
67
|
logStatus(result.active ? 'success' : result.activeState === 'failed' ? 'error' : 'warning', `service ${result.activeState}`);
|
|
57
68
|
}
|
|
58
|
-
if (
|
|
59
|
-
logStatus('info', 'service command',
|
|
69
|
+
if (serveCommand) {
|
|
70
|
+
logStatus('info', 'service command', serveCommand);
|
|
71
|
+
}
|
|
72
|
+
if (generatedUiPassword) {
|
|
73
|
+
logStatus('success', 'UI password', generatedUiPassword);
|
|
74
|
+
logStatus('warning', 'save this password', 'it is not shown again');
|
|
60
75
|
}
|
|
61
76
|
clackOutro(normalized === 'status' ? 'status complete' : `${normalized} complete`);
|
|
62
77
|
}
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
removePidFile,
|
|
7
7
|
stopInstanceProcess,
|
|
8
8
|
} from './cli-process.js';
|
|
9
|
+
import {
|
|
10
|
+
isUserStartupServiceActive as defaultIsUserStartupServiceActive,
|
|
11
|
+
restartUserStartupService as defaultRestartUserStartupService,
|
|
12
|
+
} from './cli-startup.js';
|
|
9
13
|
import {
|
|
10
14
|
intro as clackIntro,
|
|
11
15
|
outro as clackOutro,
|
|
@@ -17,7 +21,23 @@ import {
|
|
|
17
21
|
logStatus,
|
|
18
22
|
} from '../cli-output.js';
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
const UNOWNED_INSTALL_MESSAGE = [
|
|
25
|
+
'This PiChamber copy is not a global package-manager install.',
|
|
26
|
+
'Install one copy, then update that same copy:',
|
|
27
|
+
' bun add -g @pi-chamber/web',
|
|
28
|
+
' npm install -g @pi-chamber/web',
|
|
29
|
+
' pnpm add -g @pi-chamber/web',
|
|
30
|
+
' yarn global add @pi-chamber/web',
|
|
31
|
+
].join('\n');
|
|
32
|
+
|
|
33
|
+
function createUpdateCommand({
|
|
34
|
+
importFromFilePath,
|
|
35
|
+
packageManagerPath,
|
|
36
|
+
serveCommand,
|
|
37
|
+
isInsideSystemdService = () => Boolean(process.env.INVOCATION_ID) || Boolean(process.env.PICHAMBER_SYSTEMD_UNIT),
|
|
38
|
+
isUserStartupServiceActive = defaultIsUserStartupServiceActive,
|
|
39
|
+
restartUserStartupService = defaultRestartUserStartupService,
|
|
40
|
+
}) {
|
|
21
41
|
return async function updateCommand(options = {}) {
|
|
22
42
|
const showOutput = shouldRenderHumanOutput(options);
|
|
23
43
|
const updateSpin = createSpinner(options);
|
|
@@ -25,7 +45,7 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
25
45
|
const {
|
|
26
46
|
checkForUpdates,
|
|
27
47
|
executeUpdate,
|
|
28
|
-
|
|
48
|
+
resolveTrustedUpdatePackageManager,
|
|
29
49
|
getCurrentVersion,
|
|
30
50
|
} = await importFromFilePath(packageManagerPath);
|
|
31
51
|
|
|
@@ -94,9 +114,8 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
94
114
|
throw new Error(msg);
|
|
95
115
|
}
|
|
96
116
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const msg = 'systemd deployments must be updated through package management and service restart (e.g. systemctl --user restart pichamber.service).';
|
|
117
|
+
if (isInsideSystemdService()) {
|
|
118
|
+
const msg = 'pichamber update cannot replace this process while it is running as a systemd service. Run it from a terminal instead.';
|
|
100
119
|
updateSpin?.error('systemd service deployment detected');
|
|
101
120
|
if (isJsonMode(options)) {
|
|
102
121
|
printJson({
|
|
@@ -113,12 +132,29 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
113
132
|
throw new Error(msg);
|
|
114
133
|
}
|
|
115
134
|
|
|
135
|
+
const pm = resolveTrustedUpdatePackageManager();
|
|
136
|
+
if (!pm) {
|
|
137
|
+
updateSpin?.error('No global package-manager install');
|
|
138
|
+
if (isJsonMode(options)) {
|
|
139
|
+
printJson({
|
|
140
|
+
currentVersion,
|
|
141
|
+
latestVersion: updateInfo.version || 'latest',
|
|
142
|
+
updated: false,
|
|
143
|
+
error: UNOWNED_INSTALL_MESSAGE,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (showOutput) {
|
|
148
|
+
clackOutro('update skipped');
|
|
149
|
+
}
|
|
150
|
+
throw new Error(UNOWNED_INSTALL_MESSAGE);
|
|
151
|
+
}
|
|
152
|
+
|
|
116
153
|
if (showOutput && !updateSpin) {
|
|
117
|
-
logStatus('info', `updating ${updateInfo.currentVersion || currentVersion} -> ${updateInfo.version || 'latest'}`);
|
|
154
|
+
logStatus('info', `updating ${updateInfo.currentVersion || currentVersion} -> ${updateInfo.version || 'latest'} with ${pm}`);
|
|
118
155
|
}
|
|
119
156
|
updateSpin?.message(`Updating to ${updateInfo.version || 'latest'}...`);
|
|
120
157
|
|
|
121
|
-
const pm = detectPackageManager();
|
|
122
158
|
const result = executeUpdate(pm, { silent: isJsonMode(options) || isQuietMode(options) });
|
|
123
159
|
if (!result.success) {
|
|
124
160
|
updateSpin?.error('Update failed');
|
|
@@ -128,34 +164,54 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
128
164
|
throw new Error(`Update failed with exit code ${result.exitCode}`);
|
|
129
165
|
}
|
|
130
166
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
let restartedCount = 0;
|
|
168
|
+
let startupServiceRestarted = false;
|
|
169
|
+
|
|
170
|
+
if (isUserStartupServiceActive()) {
|
|
171
|
+
updateSpin?.message('Restarting systemd user service...');
|
|
172
|
+
try {
|
|
173
|
+
restartUserStartupService();
|
|
174
|
+
startupServiceRestarted = true;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
177
|
+
const msg = `Package updated but failed to restart pichamber.service. Run: systemctl --user restart pichamber.service (${detail})`;
|
|
178
|
+
updateSpin?.error('Startup service restart failed');
|
|
179
|
+
if (showOutput) {
|
|
180
|
+
clackOutro('update incomplete');
|
|
144
181
|
}
|
|
182
|
+
throw new Error(msg);
|
|
145
183
|
}
|
|
184
|
+
} else {
|
|
185
|
+
const runningInstances = await discoverRunningInstances();
|
|
186
|
+
if (runningInstances.length > 0) {
|
|
187
|
+
updateSpin?.message(`Stopping ${runningInstances.length} running instance(s)...`);
|
|
188
|
+
for (const instance of runningInstances) {
|
|
189
|
+
try {
|
|
190
|
+
const requested = await requestServerShutdown(instance.port, instance.host);
|
|
191
|
+
await stopInstanceProcess(instance.pid, {
|
|
192
|
+
shutdownWaitMs: requested ? 5000 : 0,
|
|
193
|
+
gracefulTimeoutMs: 2500,
|
|
194
|
+
forceTimeoutMs: 3000,
|
|
195
|
+
});
|
|
196
|
+
removePidFile(instance.pidFilePath);
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
146
200
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
201
|
+
updateSpin?.message(`Restarting ${runningInstances.length} instance(s)...`);
|
|
202
|
+
for (const instance of runningInstances) {
|
|
203
|
+
const storedOptions = readInstanceOptions(instance.instanceFilePath) || { port: instance.port };
|
|
204
|
+
await serveCommand({
|
|
205
|
+
port: storedOptions.port || instance.port,
|
|
206
|
+
host: storedOptions.host,
|
|
207
|
+
explicitPort: true,
|
|
208
|
+
uiPassword: storedOptions.uiPassword,
|
|
209
|
+
suppressStartupSummary: true,
|
|
210
|
+
suppressUiPasswordWarning: true,
|
|
211
|
+
quiet: true,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
restartedCount = runningInstances.length;
|
|
159
215
|
}
|
|
160
216
|
}
|
|
161
217
|
|
|
@@ -168,7 +224,9 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
168
224
|
currentVersion,
|
|
169
225
|
latestVersion: updateInfo.version || 'latest',
|
|
170
226
|
updated: true,
|
|
171
|
-
|
|
227
|
+
packageManager: pm,
|
|
228
|
+
restartedCount,
|
|
229
|
+
startupServiceRestarted,
|
|
172
230
|
});
|
|
173
231
|
return;
|
|
174
232
|
}
|
|
@@ -181,4 +239,3 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
181
239
|
}
|
|
182
240
|
|
|
183
241
|
export { createUpdateCommand };
|
|
184
|
-
|
|
@@ -21,22 +21,32 @@ async function withTempPiChamberDataDir(fn) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function createTestUpdateCommand(overrides = {}) {
|
|
25
|
+
const executeUpdate = overrides.executeUpdate || vi.fn(() => ({ success: true, exitCode: 0 }));
|
|
26
|
+
const serveCommand = overrides.serveCommand || vi.fn();
|
|
27
|
+
const restartUserStartupService = overrides.restartUserStartupService || vi.fn();
|
|
28
|
+
const updateCommand = createUpdateCommand({
|
|
29
|
+
packageManagerPath: '/fake/package-manager.js',
|
|
30
|
+
serveCommand,
|
|
31
|
+
isInsideSystemdService: overrides.isInsideSystemdService || (() => false),
|
|
32
|
+
isUserStartupServiceActive: overrides.isUserStartupServiceActive || (() => false),
|
|
33
|
+
restartUserStartupService,
|
|
34
|
+
importFromFilePath: vi.fn(async () => ({
|
|
35
|
+
checkForUpdates: overrides.checkForUpdates || vi.fn(async () => ({ available: true, version: '9.9.9' })),
|
|
36
|
+
resolveTrustedUpdatePackageManager: overrides.resolveTrustedUpdatePackageManager || vi.fn(() => 'npm'),
|
|
37
|
+
executeUpdate,
|
|
38
|
+
getCurrentVersion: vi.fn(() => '1.0.0'),
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
return { updateCommand, executeUpdate, serveCommand, restartUserStartupService };
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
describe('update command', () => {
|
|
25
45
|
it('uses the package-manager helpers on the update-available path', async () => {
|
|
26
46
|
await withTempPiChamberDataDir(async () => {
|
|
27
47
|
const originalWrite = process.stdout.write;
|
|
28
48
|
process.stdout.write = vi.fn(() => true);
|
|
29
|
-
const
|
|
30
|
-
const updateCommand = createUpdateCommand({
|
|
31
|
-
packageManagerPath: '/fake/package-manager.js',
|
|
32
|
-
serveCommand: vi.fn(),
|
|
33
|
-
importFromFilePath: vi.fn(async () => ({
|
|
34
|
-
checkForUpdates: vi.fn(async () => ({ available: true, version: '9.9.9' })),
|
|
35
|
-
detectPackageManager: vi.fn(() => 'npm'),
|
|
36
|
-
executeUpdate,
|
|
37
|
-
getCurrentVersion: vi.fn(() => '1.0.0'),
|
|
38
|
-
})),
|
|
39
|
-
});
|
|
49
|
+
const { updateCommand, executeUpdate } = createTestUpdateCommand();
|
|
40
50
|
|
|
41
51
|
try {
|
|
42
52
|
await updateCommand({ json: true });
|
|
@@ -50,48 +60,53 @@ describe('update command', () => {
|
|
|
50
60
|
|
|
51
61
|
it('handles failed update execution safely without touching instances', async () => {
|
|
52
62
|
await withTempPiChamberDataDir(async () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
packageManagerPath: '/fake/package-manager.js',
|
|
56
|
-
serveCommand: vi.fn(),
|
|
57
|
-
importFromFilePath: vi.fn(async () => ({
|
|
58
|
-
checkForUpdates: vi.fn(async () => ({ available: true, version: '9.9.9' })),
|
|
59
|
-
detectPackageManager: vi.fn(() => 'npm'),
|
|
60
|
-
executeUpdate,
|
|
61
|
-
getCurrentVersion: vi.fn(() => '1.0.0'),
|
|
62
|
-
})),
|
|
63
|
+
const { updateCommand, executeUpdate, serveCommand, restartUserStartupService } = createTestUpdateCommand({
|
|
64
|
+
executeUpdate: vi.fn(() => ({ success: false, exitCode: 1 })),
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
await expect(updateCommand({ quiet: true })).rejects.toThrow('Update failed with exit code 1');
|
|
66
68
|
expect(executeUpdate).toHaveBeenCalledWith('npm', { silent: true });
|
|
69
|
+
expect(serveCommand).not.toHaveBeenCalled();
|
|
70
|
+
expect(restartUserStartupService).not.toHaveBeenCalled();
|
|
67
71
|
});
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
it('skips in-app update when running inside a systemd service unit', async () => {
|
|
71
75
|
await withTempPiChamberDataDir(async () => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
const { updateCommand, executeUpdate } = createTestUpdateCommand({
|
|
77
|
+
isInsideSystemdService: () => true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await expect(updateCommand({ quiet: true })).rejects.toThrow('pichamber update cannot replace this process while it is running as a systemd service');
|
|
81
|
+
expect(executeUpdate).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('refuses to guess a package manager when the running copy is not a global install', async () => {
|
|
86
|
+
await withTempPiChamberDataDir(async () => {
|
|
87
|
+
const { updateCommand, executeUpdate } = createTestUpdateCommand({
|
|
88
|
+
resolveTrustedUpdatePackageManager: vi.fn(() => null),
|
|
89
|
+
});
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
await expect(updateCommand({ quiet: true })).rejects.toThrow('This PiChamber copy is not a global package-manager install.');
|
|
92
|
+
expect(executeUpdate).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('restarts a systemd user unit instead of spawning a new server', async () => {
|
|
97
|
+
await withTempPiChamberDataDir(async () => {
|
|
98
|
+
const originalWrite = process.stdout.write;
|
|
99
|
+
process.stdout.write = vi.fn(() => true);
|
|
100
|
+
const { updateCommand, serveCommand, restartUserStartupService } = createTestUpdateCommand({
|
|
101
|
+
isUserStartupServiceActive: () => true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await updateCommand({ json: true });
|
|
106
|
+
expect(restartUserStartupService).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(serveCommand).not.toHaveBeenCalled();
|
|
89
108
|
} finally {
|
|
90
|
-
|
|
91
|
-
process.env.INVOCATION_ID = prevInvocationId;
|
|
92
|
-
} else {
|
|
93
|
-
delete process.env.INVOCATION_ID;
|
|
94
|
-
}
|
|
109
|
+
process.stdout.write = originalWrite;
|
|
95
110
|
}
|
|
96
111
|
});
|
|
97
112
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{R as C,j as e}from"./vendor-react-DYyjh7Q0.js";import{f as z,T as y,y as _,B as c,I as a,z as v,aW as I}from"./SessionSwitcherDropdown-DNjCJjzF.js";import{u as H}from"./index-DQr8-8RF.js";const N=38,h=200,k=({value:t,onChange:o,placeholder:m,disabled:i=!1,hasTouchInput:r=!1,isMobile:u=!1})=>{const n=C.useRef(null),j=H(s=>s.inputSpellcheckEnabled);return C.useLayoutEffect(()=>{const s=n.current;if(!s)return;const l=document.activeElement===s,d=()=>{s.style.height=`${N}px`;const p=s.scrollHeight,x=Math.min(Math.max(p,N),h);s.style.height=`${x}px`,s.style.overflowY=p>h?"auto":"hidden",p>h&&!l&&(s.scrollTop=s.scrollHeight)};d();const f=window.requestAnimationFrame(d);return()=>{window.cancelAnimationFrame(f)}},[t]),e.jsx("textarea",{ref:n,value:t,onChange:s=>o(s.target.value),placeholder:m??"Commit message",rows:1,disabled:i,autoCorrect:r?"on":"off",autoCapitalize:r?"sentences":"off",spellCheck:u||j,className:z("w-full rounded-lg border border-border/60 bg-surface-elevated px-3 py-2 typography-ui-label text-foreground placeholder:text-muted-foreground","resize-none outline-none transition-[border-color,box-shadow] duration-150","focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]",i&&"opacity-50"),style:{minHeight:N,maxHeight:h}})},F=({highlights:t,onInsert:o})=>{if(t.length===0)return null;const m=()=>{o(t)};return e.jsxs("div",{className:"space-y-2 rounded-xl border border-border/60 bg-transparent px-3 py-2",children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsx("p",{className:"typography-micro text-muted-foreground",children:"Highlights"}),e.jsxs(y,{children:[e.jsx(_,{asChild:!0,children:e.jsx(c,{variant:"ghost",size:"icon",className:"size-6",onClick:m,"aria-label":"Insert aria label",children:e.jsx(a,{name:"arrow-down",className:"size-4"})})}),e.jsx(v,{sideOffset:8,children:"Insert tooltip"})]})]}),e.jsx("ul",{className:"space-y-1",children:t.map((i,r)=>e.jsx("li",{className:"typography-meta text-foreground",children:i},`${i}-${r}`))})]})},q=({stagedCount:t,commitMessage:o,onCommitMessageChange:m,generatedHighlights:i,onInsertHighlights:r,onGenerateMessage:u,isGeneratingMessage:n,onCommit:j,onCommitAndPush:s,commitAction:l,hasPendingIndexMutation:d=!1,gitmojiEnabled:f,onOpenGitmojiPicker:p})=>{const x=t>0,b=o.trim()&&x&&l===null&&!d,{isMobile:g,hasTouchInput:w}=I();return e.jsxs("section",{className:"border-0 bg-transparent rounded-none",children:[e.jsxs("div",{className:"flex w-full items-baseline gap-2 px-0 pt-2 pb-1",children:[e.jsx("h3",{className:"typography-ui-header font-semibold text-foreground",children:"Commit"}),x?null:e.jsx("span",{className:"min-w-0 truncate typography-meta text-muted-foreground",children:"Stage files to enable commit."})]}),e.jsxs("div",{className:"flex flex-col gap-3 px-0 pt-1 pb-3",children:[e.jsx(F,{highlights:i,onInsert:r}),e.jsx(k,{value:o,onChange:m,placeholder:"Commit message",disabled:l!==null,hasTouchInput:w,isMobile:g}),f&&e.jsxs(c,{variant:"outline",size:"sm",onClick:p,className:"w-fit",type:"button",children:[e.jsx(a,{name:"emotion-happy",className:"size-4"}),"Add gitmoji"]}),e.jsxs("div",{className:"@container/commit-actions flex items-center gap-2 min-w-0",children:[e.jsxs(c,{variant:"outline",size:"sm",onClick:u,disabled:n||l!==null||d||t===0,type:"button","aria-label":"Generate commit message",className:"commit-actions__btn",children:[n?e.jsx(a,{name:"loader-4",className:"size-4 animate-spin"}):e.jsx(a,{name:"ai-generate-2",className:"size-4 text-primary"}),e.jsx("span",{className:"commit-actions__label",children:"Generate"})]}),e.jsx("div",{className:"flex-1"}),e.jsx(c,{size:"sm",variant:"outline",onClick:j,disabled:!b||n,className:"commit-actions__btn whitespace-nowrap","aria-label":"Commit aria label",children:l==="commit"?e.jsxs(e.Fragment,{children:[e.jsx(a,{name:"loader-4",className:"size-4 animate-spin"}),e.jsx("span",{className:"commit-actions__label",children:"Committing..."})]}):e.jsxs(e.Fragment,{children:[e.jsx(a,{name:"git-commit",className:"size-4"}),e.jsx("span",{className:"commit-actions__label",children:"Commit"})]})}),g?e.jsxs(y,{children:[e.jsx(_,{asChild:!0,children:e.jsx(c,{variant:"default",size:"sm",onClick:()=>s(),disabled:!b||n,className:"h-7 w-7 p-0","aria-label":"Commit and sync",children:l==="commitAndPush"?e.jsx(a,{name:"loader-4",className:"size-4 animate-spin"}):e.jsx(a,{name:"arrow-up",className:"size-3.5"})})}),e.jsx(v,{side:"top",children:e.jsx("p",{children:"Commit & sync"})})]}):e.jsx(c,{size:"sm",variant:"default",onClick:()=>s(),disabled:!b||n,className:"commit-actions__btn","aria-label":"Commit and sync",children:l==="commitAndPush"?e.jsxs(e.Fragment,{children:[e.jsx(a,{name:"loader-4",className:"size-4 animate-spin"}),e.jsx("span",{className:"commit-actions__label commit-actions__label--push",children:"Syncing..."})]}):e.jsxs(e.Fragment,{children:[e.jsx(a,{name:"arrow-up",className:"size-3.5"}),e.jsx("span",{className:"commit-actions__label commit-actions__label--push",children:"Commit & sync"})]})})]})]})]})};export{q as C};
|