@bytetrue/byspace 0.11.3 → 0.12.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/node_modules/@getpaseo/client/dist/daemon-client.d.ts +6 -5
- package/node_modules/@getpaseo/client/dist/daemon-client.js +9 -1
- package/node_modules/@getpaseo/client/package.json +3 -3
- package/node_modules/@getpaseo/highlight/package.json +1 -1
- package/node_modules/@getpaseo/plugin/package.json +5 -5
- package/node_modules/@getpaseo/protocol/dist/generated/validation/ws-outbound.aot.js +3610 -3533
- package/node_modules/@getpaseo/protocol/dist/messages.d.ts +89 -0
- package/node_modules/@getpaseo/protocol/dist/messages.js +35 -0
- package/node_modules/@getpaseo/protocol/dist/validation/ws-outbound-schema-metadata.d.ts +14 -0
- package/node_modules/@getpaseo/protocol/package.json +1 -1
- package/node_modules/@getpaseo/relay/package.json +1 -1
- package/node_modules/@getpaseo/server/dist/server/server/agent/providers/pi/agent.d.ts +14 -0
- package/node_modules/@getpaseo/server/dist/server/server/agent/providers/pi/agent.js +79 -4
- package/node_modules/@getpaseo/server/dist/server/server/authorization/operation-permissions.js +2 -0
- package/node_modules/@getpaseo/server/dist/server/server/bootstrap.d.ts +1 -0
- package/node_modules/@getpaseo/server/dist/server/server/bootstrap.js +31 -8
- package/node_modules/@getpaseo/server/dist/server/server/config.js +5 -1
- package/node_modules/@getpaseo/server/dist/server/server/daemon-config-store.js +8 -0
- package/node_modules/@getpaseo/server/dist/server/server/persisted-config.d.ts +3 -0
- package/node_modules/@getpaseo/server/dist/server/server/persisted-config.js +3 -0
- package/node_modules/@getpaseo/server/dist/server/server/session.js +1 -0
- package/node_modules/@getpaseo/server/dist/server/server/websocket-server.js +4 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/shell-detect.d.ts +51 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/shell-detect.js +196 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-manager-factory.d.ts +2 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-manager.d.ts +1 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-manager.js +1 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-persistence.d.ts +15 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-persistence.js +127 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-session-controller.d.ts +4 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-session-controller.js +35 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-session-store.d.ts +19 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-session-store.js +59 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/terminal-worker-protocol.d.ts +1 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/worker-terminal-manager.d.ts +2 -0
- package/node_modules/@getpaseo/server/dist/server/terminal/worker-terminal-manager.js +2 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-8569047c23404e68f926a8a0f411b731.js → desktop-attachment-bridge-d6e29a06a5bfb4d50647dcdf2dfc2edc.js} +1 -1
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-8569047c23404e68f926a8a0f411b731.js.br → desktop-attachment-bridge-d6e29a06a5bfb4d50647dcdf2dfc2edc.js.br} +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-d6e29a06a5bfb4d50647dcdf2dfc2edc.js.gz +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-49a16787c67d7db16d4dfbaebda6f27b.js → desktop-attachment-store-9917f2388804e462b0df3cf5f1b5df19.js} +1 -1
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-49a16787c67d7db16d4dfbaebda6f27b.js.br → desktop-attachment-store-9917f2388804e462b0df3cf5f1b5df19.js.br} +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-49a16787c67d7db16d4dfbaebda6f27b.js.gz → desktop-attachment-store-9917f2388804e462b0df3cf5f1b5df19.js.gz} +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{index-dc967c46563553e5ecaba6b78fe47585.js → index-d66590127d71cde67bbb3776075bb688.js} +64 -63
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-d66590127d71cde67bbb3776075bb688.js.br +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-d66590127d71cde67bbb3776075bb688.js.gz +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-b87d428d16d555a6e2a897903a24bc52.js → indexeddb-attachment-store-35cbb0fd3d120aac9caca241e4dedf6c.js} +1 -1
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-b87d428d16d555a6e2a897903a24bc52.js.br → indexeddb-attachment-store-35cbb0fd3d120aac9caca241e4dedf6c.js.br} +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-35cbb0fd3d120aac9caca241e4dedf6c.js.gz +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-44c208843bd97caf1582f78c04c2fa76.js → native-file-attachment-store-278a4b844d28852fc6ef5dfde6d70583.js} +3 -3
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-278a4b844d28852fc6ef5dfde6d70583.js.br +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-278a4b844d28852fc6ef5dfde6d70583.js.gz +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/index.html +1 -1
- package/node_modules/@getpaseo/server/dist/server/web-ui/index.html.br +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/index.html.gz +0 -0
- package/node_modules/@getpaseo/server/dist/src/server/persisted-config.js +3 -0
- package/node_modules/@getpaseo/server/package.json +6 -6
- package/package.json +8 -8
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-8569047c23404e68f926a8a0f411b731.js.gz +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-dc967c46563553e5ecaba6b78fe47585.js.br +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-dc967c46563553e5ecaba6b78fe47585.js.gz +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b87d428d16d555a6e2a897903a24bc52.js.gz +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-44c208843bd97caf1582f78c04c2fa76.js.br +0 -0
- package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-44c208843bd97caf1582f78c04c2fa76.js.gz +0 -0
|
@@ -120,6 +120,8 @@ import { archiveByScope, archivePersistedWorkspaceRecord, killTerminalsForWorksp
|
|
|
120
120
|
import { setupAutoArchiveOnMerge } from "./auto-archive-on-merge/index.js";
|
|
121
121
|
import { wrapSessionMessage } from "./messages.js";
|
|
122
122
|
import { createConfiguredTerminalManager } from "../terminal/terminal-manager-factory.js";
|
|
123
|
+
import { createPersistingTerminalManager, restorePersistedTerminals, } from "../terminal/terminal-persistence.js";
|
|
124
|
+
import { TerminalSessionStore } from "../terminal/terminal-session-store.js";
|
|
123
125
|
import { applyTerminalAgentHookSetting } from "../terminal/agent-hooks/terminal-agent-hook-setting.js";
|
|
124
126
|
import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
|
|
125
127
|
import { createRelayRuntime } from "./relay-runtime.js";
|
|
@@ -278,6 +280,17 @@ function mountWebUi(app, config, logger) {
|
|
|
278
280
|
function resolveExpressTrustProxySetting(config) {
|
|
279
281
|
return config.trustedProxies ?? ["loopback"];
|
|
280
282
|
}
|
|
283
|
+
function applyOptionalConfigLists(initialConfig, config) {
|
|
284
|
+
if (config.terminalProfiles !== undefined) {
|
|
285
|
+
initialConfig.terminalProfiles = config.terminalProfiles;
|
|
286
|
+
}
|
|
287
|
+
if (config.terminalDefaultShell !== undefined) {
|
|
288
|
+
initialConfig.terminalDefaultShell = config.terminalDefaultShell;
|
|
289
|
+
}
|
|
290
|
+
if (config.agentProfiles !== undefined) {
|
|
291
|
+
initialConfig.agentProfiles = config.agentProfiles;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
281
294
|
function createInitialMutableDaemonConfig(config, defaultAppBaseUrl) {
|
|
282
295
|
const providers = config.providerOverrides ?? {};
|
|
283
296
|
const initialConfig = {
|
|
@@ -307,12 +320,7 @@ function createInitialMutableDaemonConfig(config, defaultAppBaseUrl) {
|
|
|
307
320
|
plugins: config.plugins ?? {},
|
|
308
321
|
skills: { selection: config.skillSelection },
|
|
309
322
|
};
|
|
310
|
-
|
|
311
|
-
initialConfig.terminalProfiles = config.terminalProfiles;
|
|
312
|
-
}
|
|
313
|
-
if (config.agentProfiles !== undefined) {
|
|
314
|
-
initialConfig.agentProfiles = config.agentProfiles;
|
|
315
|
-
}
|
|
323
|
+
applyOptionalConfigLists(initialConfig, config);
|
|
316
324
|
return initialConfig;
|
|
317
325
|
}
|
|
318
326
|
export async function createPaseoDaemon(config, rootLogger, dependencies = {}) {
|
|
@@ -386,8 +394,14 @@ export async function createPaseoDaemon(config, rootLogger, dependencies = {}) {
|
|
|
386
394
|
});
|
|
387
395
|
let boundListenTarget = null;
|
|
388
396
|
let workspaceRegistry = null;
|
|
389
|
-
const
|
|
390
|
-
|
|
397
|
+
const terminalSessionStore = new TerminalSessionStore(path.join(config.paseoHome, "terminals", "terminals.json"));
|
|
398
|
+
const terminalManager = createPersistingTerminalManager({
|
|
399
|
+
inner: createConfiguredTerminalManager({
|
|
400
|
+
getTerminalActivityUrl: () => createTerminalActivityUrl(boundListenTarget),
|
|
401
|
+
getDefaultShell: () => daemonConfigStore.get().terminalDefaultShell,
|
|
402
|
+
}),
|
|
403
|
+
store: terminalSessionStore,
|
|
404
|
+
logger,
|
|
391
405
|
});
|
|
392
406
|
applyTerminalAgentHookSetting({ store: daemonConfigStore, logger });
|
|
393
407
|
const serviceProxyPublicBaseUrl = config.serviceProxy?.publicBaseUrl
|
|
@@ -642,6 +656,15 @@ export async function createPaseoDaemon(config, rootLogger, dependencies = {}) {
|
|
|
642
656
|
});
|
|
643
657
|
await workspaceLabelService.initialize();
|
|
644
658
|
logger.info({ elapsed: elapsed() }, "Workspace registries bootstrapped");
|
|
659
|
+
await restorePersistedTerminals({
|
|
660
|
+
manager: terminalManager,
|
|
661
|
+
store: terminalSessionStore,
|
|
662
|
+
isWorkspaceActive: async (workspaceId) => {
|
|
663
|
+
const record = await workspaceRegistry.get(workspaceId);
|
|
664
|
+
return record !== null && record.archivedAt === null;
|
|
665
|
+
},
|
|
666
|
+
logger,
|
|
667
|
+
});
|
|
645
668
|
const teardownArchivedWorkspaceRuntime = (workspaceId) => {
|
|
646
669
|
scriptRuntimeStore.removeForWorkspace(workspaceId);
|
|
647
670
|
releaseWorkspaceServicePortPlan(workspaceId);
|
|
@@ -355,6 +355,9 @@ function resolveStaticLoadConfigSettings(env, cli, persisted, hostedAppBaseUrl)
|
|
|
355
355
|
autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
|
|
356
356
|
appendSystemPrompt: resolveAppendSystemPrompt(persisted),
|
|
357
357
|
...resolveProfileLists(persisted),
|
|
358
|
+
// null in the persisted file means "auto"; the daemon config omits it so
|
|
359
|
+
// MutableDaemonConfig keeps the absent = auto semantics.
|
|
360
|
+
terminalDefaultShell: persisted.daemon?.terminalDefaultShell ?? undefined,
|
|
358
361
|
hostnames: mergeHostnames([
|
|
359
362
|
persisted.daemon?.hostnames,
|
|
360
363
|
parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
|
|
@@ -373,7 +376,7 @@ export function resolveConfigFromPersisted(paseoHome, persisted, options) {
|
|
|
373
376
|
const cli = resolvedOptions.cli;
|
|
374
377
|
const relayEnabledFallback = resolvedOptions.relayEnabledFallback ?? persisted.daemon?.relay?.enabled === undefined;
|
|
375
378
|
const listen = resolveListenAddress(env, cli, persisted);
|
|
376
|
-
const { mcpEnabled, mcpInjectIntoAgents, browserToolsEnabled, autoArchiveAfterMerge, appendSystemPrompt, terminalProfiles, agentProfiles, hostnames, trustedProxies, appBaseUrl, } = resolveStaticLoadConfigSettings(env, cli, persisted, hostedAppBaseUrl);
|
|
379
|
+
const { mcpEnabled, mcpInjectIntoAgents, browserToolsEnabled, autoArchiveAfterMerge, appendSystemPrompt, terminalProfiles, terminalDefaultShell, agentProfiles, hostnames, trustedProxies, appBaseUrl, } = resolveStaticLoadConfigSettings(env, cli, persisted, hostedAppBaseUrl);
|
|
377
380
|
const relay = resolveRelayConfig({
|
|
378
381
|
env,
|
|
379
382
|
persisted,
|
|
@@ -411,6 +414,7 @@ export function resolveConfigFromPersisted(paseoHome, persisted, options) {
|
|
|
411
414
|
: {}),
|
|
412
415
|
appendSystemPrompt,
|
|
413
416
|
terminalProfiles,
|
|
417
|
+
terminalDefaultShell,
|
|
414
418
|
agentProfiles,
|
|
415
419
|
skillSelection: persisted.agents?.skills?.selection,
|
|
416
420
|
pluginsEnabled: persisted.pluginsEnabled ?? false,
|
|
@@ -179,6 +179,9 @@ function pickSupportedPatchFields(patch) {
|
|
|
179
179
|
? { appendSystemPrompt: patch.appendSystemPrompt }
|
|
180
180
|
: {}),
|
|
181
181
|
...(patch.terminalProfiles !== undefined ? { terminalProfiles: patch.terminalProfiles } : {}),
|
|
182
|
+
...(patch.terminalDefaultShell !== undefined
|
|
183
|
+
? { terminalDefaultShell: patch.terminalDefaultShell }
|
|
184
|
+
: {}),
|
|
182
185
|
...(patch.agentProfiles !== undefined ? { agentProfiles: patch.agentProfiles } : {}),
|
|
183
186
|
...(patch.pluginsEnabled !== undefined ? { pluginsEnabled: patch.pluginsEnabled } : {}),
|
|
184
187
|
...(patch.plugins !== undefined ? { plugins: patch.plugins } : {}),
|
|
@@ -498,6 +501,11 @@ function mergeMutableDaemonPatch(persistedDaemon, patch, persistRelayEnabled) {
|
|
|
498
501
|
next.appendSystemPrompt = patch.appendSystemPrompt;
|
|
499
502
|
if (patch.terminalProfiles !== undefined)
|
|
500
503
|
next.terminalProfiles = patch.terminalProfiles;
|
|
504
|
+
// null clears the configured shell (client "Auto" selection); the field stays
|
|
505
|
+
// in the persisted JSON as null so a later non-null patch replaces it.
|
|
506
|
+
if (patch.terminalDefaultShell !== undefined) {
|
|
507
|
+
next.terminalDefaultShell = patch.terminalDefaultShell;
|
|
508
|
+
}
|
|
501
509
|
if (patch.agentProfiles !== undefined)
|
|
502
510
|
next.agentProfiles = patch.agentProfiles;
|
|
503
511
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
@@ -42,6 +42,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
42
42
|
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
43
43
|
icon: z.ZodOptional<z.ZodString>;
|
|
44
44
|
}, z.core.$loose>>>;
|
|
45
|
+
terminalDefaultShell: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
45
46
|
agentProfiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
46
47
|
id: z.ZodString;
|
|
47
48
|
name: z.ZodString;
|
|
@@ -101,6 +102,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
101
102
|
args?: string[] | undefined;
|
|
102
103
|
icon?: string | undefined;
|
|
103
104
|
}[] | undefined;
|
|
105
|
+
terminalDefaultShell?: string | null | undefined;
|
|
104
106
|
agentProfiles?: {
|
|
105
107
|
[x: string]: unknown;
|
|
106
108
|
id: string;
|
|
@@ -162,6 +164,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
162
164
|
args?: string[] | undefined;
|
|
163
165
|
icon?: string | undefined;
|
|
164
166
|
}[] | undefined;
|
|
167
|
+
terminalDefaultShell?: string | null | undefined;
|
|
165
168
|
agentProfiles?: {
|
|
166
169
|
[x: string]: unknown;
|
|
167
170
|
id: string;
|
|
@@ -222,6 +222,9 @@ export const PersistedConfigSchema = z
|
|
|
222
222
|
terminalAgentHooks: TerminalAgentHookSettingsSchema.optional(),
|
|
223
223
|
appendSystemPrompt: z.string().optional(),
|
|
224
224
|
terminalProfiles: z.array(TerminalProfileSchema).optional(),
|
|
225
|
+
// null is a valid persisted value meaning "auto"; config.ts omits it
|
|
226
|
+
// when resolving so MutableDaemonConfig keeps absent = auto.
|
|
227
|
+
terminalDefaultShell: z.string().min(1).nullish(),
|
|
225
228
|
agentProfiles: z.array(AgentProfileSchema).optional(),
|
|
226
229
|
cors: z
|
|
227
230
|
.object({
|
|
@@ -437,6 +437,7 @@ export class Session {
|
|
|
437
437
|
listTerminalWorkspaceRefs: () => this.listActiveWorkspaceRefs(),
|
|
438
438
|
clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
|
|
439
439
|
getClientBufferedAmount: () => this.getTransportBufferedAmount(),
|
|
440
|
+
getConfiguredDefaultShell: () => daemonConfigStore.get().terminalDefaultShell,
|
|
440
441
|
});
|
|
441
442
|
this.agentUpdates = createAgentUpdatesService({
|
|
442
443
|
emit: (message) => this.emit(message),
|
|
@@ -1184,6 +1184,10 @@ export class VoiceAssistantWebSocketServer {
|
|
|
1184
1184
|
projectSetupSkill: true,
|
|
1185
1185
|
// COMPAT(terminalAgentHookProviders): added in v0.7.1, remove gate after 2027-09-01.
|
|
1186
1186
|
terminalAgentHookProviders: true,
|
|
1187
|
+
// COMPAT(terminalShellConfig): added in v0.7.x, remove gate after 2027-09-01.
|
|
1188
|
+
// Gates terminal.shell.detect plus the terminalDefaultShell config field;
|
|
1189
|
+
// clients without the gate never send either.
|
|
1190
|
+
terminalShellConfig: true,
|
|
1187
1191
|
// COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23.
|
|
1188
1192
|
"terminal-restore-modes": true,
|
|
1189
1193
|
// COMPAT(terminalInputModeReplay): added in v0.2.6, remove gate after 2027-02-02.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface DetectedShell {
|
|
2
|
+
path: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ShellCandidate {
|
|
6
|
+
location: string;
|
|
7
|
+
source: "env" | "dscl" | "etc-shells" | "well-known";
|
|
8
|
+
}
|
|
9
|
+
export interface DetectShellsOptions {
|
|
10
|
+
platform?: NodeJS.Platform;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
homeDir?: string;
|
|
13
|
+
/** Overrides candidate discovery; tests inject fixtures here. */
|
|
14
|
+
candidates?: readonly ShellCandidate[];
|
|
15
|
+
/** Overrides /etc/shells reading; tests inject fixture text here. */
|
|
16
|
+
readEtcShells?: () => string | null;
|
|
17
|
+
/** Overrides the macOS login-shell lookup; tests inject fixture text here. */
|
|
18
|
+
readMacUserShell?: () => string | null;
|
|
19
|
+
/** Resolves bare names against PATH. Defaults to PATH walk. */
|
|
20
|
+
resolveOnPath?: (name: string) => string | null;
|
|
21
|
+
exists?: (path: string) => boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Executable suffixes to probe, derived from PATHEXT. The bare name is always
|
|
25
|
+
* the final fallback for names that already carry an extension.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolvePathExtSuffixes(env: NodeJS.ProcessEnv): readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Parses `/etc/shells` text into candidate paths. Comments and blank lines are
|
|
30
|
+
* skipped; entries must be absolute.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseEtcShells(text: string): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Parses `dscl . -read /Users/<user> UserShell` output ("UserShell: /bin/zsh").
|
|
35
|
+
* Returns null when the line is missing or malformed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseDsclUserShell(text: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Runs `dscl . -read /Users/<user> UserShell` and parses the login shell path.
|
|
40
|
+
* Returns null when dscl is unavailable, fails, or the output is unusable —
|
|
41
|
+
* detection degrades to the remaining sources rather than failing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readMacUserShell(homeDir: string): Promise<string | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Shells installed on this machine, deduped by absolute path, in discovery
|
|
46
|
+
* order: environment shell first (the strongest signal of what the user
|
|
47
|
+
* actually uses), then the macOS login shell, then /etc/shells, then
|
|
48
|
+
* well-known locations. Only paths that exist are returned.
|
|
49
|
+
*/
|
|
50
|
+
export declare function detectInstalledShells(options?: DetectShellsOptions): Promise<DetectedShell[]>;
|
|
51
|
+
//# sourceMappingURL=shell-detect.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { basename, delimiter, isAbsolute, join, posix, win32 } from "node:path";
|
|
4
|
+
const WELL_KNOWN_UNIX_SHELLS = [
|
|
5
|
+
{ location: "/bin/bash", source: "well-known" },
|
|
6
|
+
{ location: "/bin/zsh", source: "well-known" },
|
|
7
|
+
{ location: "/usr/bin/bash", source: "well-known" },
|
|
8
|
+
{ location: "/usr/bin/zsh", source: "well-known" },
|
|
9
|
+
{ location: "/usr/local/bin/bash", source: "well-known" },
|
|
10
|
+
{ location: "/usr/local/bin/zsh", source: "well-known" },
|
|
11
|
+
{ location: "/usr/local/bin/fish", source: "well-known" },
|
|
12
|
+
{ location: "/opt/homebrew/bin/bash", source: "well-known" },
|
|
13
|
+
{ location: "/opt/homebrew/bin/zsh", source: "well-known" },
|
|
14
|
+
{ location: "/opt/homebrew/bin/fish", source: "well-known" },
|
|
15
|
+
{ location: "/opt/homebrew/bin/nu", source: "well-known" },
|
|
16
|
+
{ location: "/usr/local/bin/nu", source: "well-known" },
|
|
17
|
+
];
|
|
18
|
+
const WELL_KNOWN_WINDOWS_SHELLS = [
|
|
19
|
+
{ location: "pwsh.exe", source: "well-known" },
|
|
20
|
+
{ location: "powershell.exe", source: "well-known" },
|
|
21
|
+
{ location: "cmd.exe", source: "well-known" },
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Resolves a bare executable name against PATH. On Windows, executables live
|
|
25
|
+
* under PATHEXT suffixes (.exe and friends), so each PATH entry is probed with
|
|
26
|
+
* each suffix before the bare name.
|
|
27
|
+
*/
|
|
28
|
+
function resolveOnPathVariable(name, env, suffixes) {
|
|
29
|
+
const path = env.PATH ?? "";
|
|
30
|
+
for (const dir of path.split(delimiter)) {
|
|
31
|
+
if (!dir)
|
|
32
|
+
continue;
|
|
33
|
+
for (const suffix of suffixes) {
|
|
34
|
+
const candidate = join(dir, `${name}${suffix}`);
|
|
35
|
+
if (existsSync(candidate)) {
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Executable suffixes to probe, derived from PATHEXT. The bare name is always
|
|
44
|
+
* the final fallback for names that already carry an extension.
|
|
45
|
+
*/
|
|
46
|
+
export function resolvePathExtSuffixes(env) {
|
|
47
|
+
const pathExt = env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
48
|
+
const suffixes = pathExt
|
|
49
|
+
.split(";")
|
|
50
|
+
.map((suffix) => suffix.trim())
|
|
51
|
+
.filter((suffix) => suffix.length > 0);
|
|
52
|
+
return [...suffixes, ""];
|
|
53
|
+
}
|
|
54
|
+
function defaultResolveOnPath(env, platform) {
|
|
55
|
+
const suffixes = platform === "win32" ? resolvePathExtSuffixes(env) : [""];
|
|
56
|
+
return (name) => resolveOnPathVariable(name, env, suffixes);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parses `/etc/shells` text into candidate paths. Comments and blank lines are
|
|
60
|
+
* skipped; entries must be absolute.
|
|
61
|
+
*/
|
|
62
|
+
export function parseEtcShells(text) {
|
|
63
|
+
return text
|
|
64
|
+
.split("\n")
|
|
65
|
+
.map((line) => line.trim())
|
|
66
|
+
.filter((line) => line.length > 0 && !line.startsWith("#") && isAbsolute(line));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parses `dscl . -read /Users/<user> UserShell` output ("UserShell: /bin/zsh").
|
|
70
|
+
* Returns null when the line is missing or malformed.
|
|
71
|
+
*/
|
|
72
|
+
export function parseDsclUserShell(text) {
|
|
73
|
+
const line = text
|
|
74
|
+
.split("\n")
|
|
75
|
+
.map((value) => value.trim())
|
|
76
|
+
.find((value) => value.startsWith("UserShell:"));
|
|
77
|
+
if (!line) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const value = line.slice("UserShell:".length).trim();
|
|
81
|
+
return isAbsolute(value) ? value : null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Runs `dscl . -read /Users/<user> UserShell` and parses the login shell path.
|
|
85
|
+
* Returns null when dscl is unavailable, fails, or the output is unusable —
|
|
86
|
+
* detection degrades to the remaining sources rather than failing.
|
|
87
|
+
*/
|
|
88
|
+
export async function readMacUserShell(homeDir) {
|
|
89
|
+
const user = basename(homeDir);
|
|
90
|
+
try {
|
|
91
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
92
|
+
// Domain-joined macOS hosts can make dscl hang on directory-service
|
|
93
|
+
// lookups; the timeout degrades to the remaining detection sources.
|
|
94
|
+
execFile("dscl", [".", "-read", `/Users/${user}`, "UserShell"], { timeout: 2000 }, (error, result) => {
|
|
95
|
+
if (error)
|
|
96
|
+
reject(error);
|
|
97
|
+
else
|
|
98
|
+
resolve(result);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
return parseDsclUserShell(stdout);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function toDetectedShell(path, platform) {
|
|
108
|
+
// basename follows the running host; the name must follow the detected
|
|
109
|
+
// platform so backslash paths name correctly in tests run on POSIX hosts.
|
|
110
|
+
const base = platform === "win32" ? win32.basename(path) : posix.basename(path);
|
|
111
|
+
return { path, name: base };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolves and dedupes candidate shell paths into detected shells. Bare names
|
|
115
|
+
* resolve against PATH; duplicates and non-existent paths are dropped.
|
|
116
|
+
* Absoluteness follows the detected platform so Windows paths classify
|
|
117
|
+
* correctly even when detection runs on a POSIX host.
|
|
118
|
+
*/
|
|
119
|
+
async function resolveDetectedShells(candidates, platform, resolveOnPath, exists) {
|
|
120
|
+
const isCandidateAbsolute = (location) => platform === "win32" ? win32.isAbsolute(location) : posix.isAbsolute(location);
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const shells = [];
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const path = isCandidateAbsolute(candidate.location)
|
|
125
|
+
? candidate.location
|
|
126
|
+
: resolveOnPath(candidate.location);
|
|
127
|
+
if (!path || seen.has(path) || !exists(path)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
seen.add(path);
|
|
131
|
+
shells.push(toDetectedShell(path, platform));
|
|
132
|
+
}
|
|
133
|
+
return shells;
|
|
134
|
+
}
|
|
135
|
+
async function gatherUnixCandidates(options, platform, env, homeDir) {
|
|
136
|
+
const candidates = [];
|
|
137
|
+
const envShell = env.SHELL;
|
|
138
|
+
if (envShell && isAbsolute(envShell)) {
|
|
139
|
+
candidates.push({ location: envShell, source: "env" });
|
|
140
|
+
}
|
|
141
|
+
if (platform === "darwin") {
|
|
142
|
+
const userShell = options.readMacUserShell
|
|
143
|
+
? options.readMacUserShell()
|
|
144
|
+
: await readMacUserShell(homeDir);
|
|
145
|
+
if (userShell) {
|
|
146
|
+
candidates.push({ location: userShell, source: "dscl" });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const etcShells = options.readEtcShells ? options.readEtcShells() : readEtcShellsFile();
|
|
150
|
+
if (etcShells) {
|
|
151
|
+
for (const entry of parseEtcShells(etcShells)) {
|
|
152
|
+
candidates.push({ location: entry, source: "etc-shells" });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
candidates.push(...WELL_KNOWN_UNIX_SHELLS);
|
|
156
|
+
return candidates;
|
|
157
|
+
}
|
|
158
|
+
async function gatherCandidates(options, platform, env, homeDir) {
|
|
159
|
+
if (options.candidates) {
|
|
160
|
+
return [...options.candidates];
|
|
161
|
+
}
|
|
162
|
+
if (platform === "win32") {
|
|
163
|
+
// ComSpec mirrors the Unix $SHELL env candidate: the shell the host
|
|
164
|
+
// itself considers the default, ahead of the well-known probes.
|
|
165
|
+
const comspec = env.ComSpec || env.COMSPEC;
|
|
166
|
+
return [
|
|
167
|
+
...(comspec ? [{ location: comspec, source: "env" }] : []),
|
|
168
|
+
...WELL_KNOWN_WINDOWS_SHELLS,
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
return gatherUnixCandidates(options, platform, env, homeDir);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Shells installed on this machine, deduped by absolute path, in discovery
|
|
175
|
+
* order: environment shell first (the strongest signal of what the user
|
|
176
|
+
* actually uses), then the macOS login shell, then /etc/shells, then
|
|
177
|
+
* well-known locations. Only paths that exist are returned.
|
|
178
|
+
*/
|
|
179
|
+
export async function detectInstalledShells(options = {}) {
|
|
180
|
+
const platform = options.platform ?? process.platform;
|
|
181
|
+
const env = options.env ?? process.env;
|
|
182
|
+
const homeDir = options.homeDir ?? env.HOME ?? "";
|
|
183
|
+
const exists = options.exists ?? existsSync;
|
|
184
|
+
const resolveOnPath = options.resolveOnPath ?? defaultResolveOnPath(env, platform);
|
|
185
|
+
const candidates = await gatherCandidates(options, platform, env, homeDir);
|
|
186
|
+
return resolveDetectedShells(candidates, platform, resolveOnPath, exists);
|
|
187
|
+
}
|
|
188
|
+
function readEtcShellsFile() {
|
|
189
|
+
try {
|
|
190
|
+
return readFileSync("/etc/shells", "utf8");
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=shell-detect.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TerminalManager } from "./terminal-manager.js";
|
|
2
2
|
export interface ConfiguredTerminalManagerOptions {
|
|
3
3
|
getTerminalActivityUrl?: () => string | null;
|
|
4
|
+
/** Reads daemon.terminalDefaultShell; nullish means auto. */
|
|
5
|
+
getDefaultShell?: () => string | null | undefined;
|
|
4
6
|
}
|
|
5
7
|
export declare function createConfiguredTerminalManager(options?: ConfiguredTerminalManagerOptions): TerminalManager;
|
|
6
8
|
//# sourceMappingURL=terminal-manager-factory.d.ts.map
|
|
@@ -215,6 +215,7 @@ export function createTerminalManager(managerOptions = {}) {
|
|
|
215
215
|
workspaceId: options.workspaceId,
|
|
216
216
|
name: options.name ?? defaultName,
|
|
217
217
|
...(options.title ? { title: options.title } : {}),
|
|
218
|
+
...(options.shell && !options.command ? { shell: options.shell } : {}),
|
|
218
219
|
...(options.command ? { command: options.command } : {}),
|
|
219
220
|
...(options.args ? { args: options.args } : {}),
|
|
220
221
|
...(options.rows !== undefined ? { rows: options.rows } : {}),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import type { TerminalManager } from "./terminal-manager.js";
|
|
3
|
+
import type { TerminalSessionStore } from "./terminal-session-store.js";
|
|
4
|
+
export declare function createPersistingTerminalManager(input: {
|
|
5
|
+
inner: TerminalManager;
|
|
6
|
+
store: TerminalSessionStore;
|
|
7
|
+
logger: Logger;
|
|
8
|
+
}): TerminalManager;
|
|
9
|
+
export declare function restorePersistedTerminals(input: {
|
|
10
|
+
manager: TerminalManager;
|
|
11
|
+
store: TerminalSessionStore;
|
|
12
|
+
isWorkspaceActive: (workspaceId: string) => Promise<boolean>;
|
|
13
|
+
logger: Logger;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=terminal-persistence.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
// Daemon restarts — crash recovery, auto-update, manual restart, a machine
|
|
3
|
+
// sleep that took the daemon down — otherwise lose every terminal tab, because
|
|
4
|
+
// sessions live only in the terminal worker process. This wrapper mirrors
|
|
5
|
+
// membership changes (create / exit / kill) into a TerminalSessionStore so the
|
|
6
|
+
// daemon can recreate the tabs at startup. Processes and scrollback are not
|
|
7
|
+
// preserved: restored terminals are fresh shells that reuse the original id,
|
|
8
|
+
// so client-side layout references keep resolving.
|
|
9
|
+
export function createPersistingTerminalManager(input) {
|
|
10
|
+
const { inner, store, logger } = input;
|
|
11
|
+
const recordsById = new Map();
|
|
12
|
+
let lastWrittenJson = "";
|
|
13
|
+
let frozen = false;
|
|
14
|
+
function scheduleWrite() {
|
|
15
|
+
const records = Array.from(recordsById.values());
|
|
16
|
+
const json = JSON.stringify(records);
|
|
17
|
+
if (json === lastWrittenJson) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
lastWrittenJson = json;
|
|
21
|
+
store.replace(records).catch((error) => {
|
|
22
|
+
logger.warn({ err: error }, "Failed to persist terminal session metadata");
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const unsubscribe = inner.subscribeTerminalsChanged((event) => {
|
|
26
|
+
if (frozen) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// The event carries the authoritative post-mutation list for one cwd
|
|
30
|
+
// bucket. Creations are recorded by the wrapped createTerminal; here we
|
|
31
|
+
// only mirror removals (shell exit, killTerminal, workspace teardown).
|
|
32
|
+
const liveIds = new Set(event.terminals.map((terminal) => terminal.id));
|
|
33
|
+
let changed = false;
|
|
34
|
+
for (const record of recordsById.values()) {
|
|
35
|
+
if (record.cwd === event.cwd && !liveIds.has(record.id)) {
|
|
36
|
+
recordsById.delete(record.id);
|
|
37
|
+
changed = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (changed) {
|
|
41
|
+
scheduleWrite();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
void unsubscribe;
|
|
45
|
+
return {
|
|
46
|
+
...inner,
|
|
47
|
+
async createTerminal(options) {
|
|
48
|
+
const session = await inner.createTerminal(options);
|
|
49
|
+
if (frozen) {
|
|
50
|
+
return session;
|
|
51
|
+
}
|
|
52
|
+
// A short-lived command can exit before createTerminal resolves; its exit
|
|
53
|
+
// was mirrored (or dropped) before this record existed. Recording it now
|
|
54
|
+
// would resurrect a dead terminal on the next boot.
|
|
55
|
+
if (!inner.getTerminal(session.id)) {
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
const record = {
|
|
59
|
+
id: session.id,
|
|
60
|
+
cwd: session.cwd,
|
|
61
|
+
workspaceId: session.workspaceId,
|
|
62
|
+
name: session.name,
|
|
63
|
+
...(options.command ? { command: options.command } : {}),
|
|
64
|
+
...(options.args ? { args: options.args } : {}),
|
|
65
|
+
};
|
|
66
|
+
recordsById.set(record.id, record);
|
|
67
|
+
scheduleWrite();
|
|
68
|
+
return session;
|
|
69
|
+
},
|
|
70
|
+
killAll() {
|
|
71
|
+
// killAll is shutdown-only (bootstrap stop / worker teardown). Terminals
|
|
72
|
+
// killed here must survive in the store so the next daemon boot restores
|
|
73
|
+
// their tabs, so the mirror is frozen permanently instead of tracking
|
|
74
|
+
// these removals. The daemon process does not use the manager afterwards.
|
|
75
|
+
frozen = true;
|
|
76
|
+
inner.killAll();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function restorePersistedTerminals(input) {
|
|
81
|
+
const { manager, store, isWorkspaceActive, logger } = input;
|
|
82
|
+
let records;
|
|
83
|
+
try {
|
|
84
|
+
records = await store.load();
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.warn({ err: error }, "Skipping terminal tab restore: store unreadable");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (records.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const survivors = [];
|
|
94
|
+
for (const record of records) {
|
|
95
|
+
try {
|
|
96
|
+
if (!(await isWorkspaceActive(record.workspaceId))) {
|
|
97
|
+
logger.info({ terminalId: record.id, workspaceId: record.workspaceId }, "Dropping persisted terminal: workspace gone");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const stat = await fs.stat(record.cwd);
|
|
101
|
+
if (!stat.isDirectory()) {
|
|
102
|
+
logger.info({ terminalId: record.id, cwd: record.cwd }, "Dropping persisted terminal: cwd missing");
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
await manager.createTerminal({
|
|
106
|
+
id: record.id,
|
|
107
|
+
cwd: record.cwd,
|
|
108
|
+
workspaceId: record.workspaceId,
|
|
109
|
+
name: record.name,
|
|
110
|
+
...(record.command ? { command: record.command } : {}),
|
|
111
|
+
...(record.args ? { args: record.args } : {}),
|
|
112
|
+
});
|
|
113
|
+
survivors.push(record);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
logger.warn({ err: error, terminalId: record.id, cwd: record.cwd }, "Failed to restore persisted terminal");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await store.replace(survivors);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.warn({ err: error }, "Failed to rewrite terminal session store after restore");
|
|
124
|
+
}
|
|
125
|
+
logger.info({ restored: survivors.length, dropped: records.length - survivors.length }, "Terminal tabs restored");
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=terminal-persistence.js.map
|
|
@@ -13,6 +13,8 @@ export interface TerminalSessionControllerOptions {
|
|
|
13
13
|
listTerminalWorkspaceRoots?: () => Promise<readonly string[]>;
|
|
14
14
|
clientSupportsWrapReflow?: () => boolean;
|
|
15
15
|
getClientBufferedAmount?: () => number | null;
|
|
16
|
+
/** Reads daemon.terminalDefaultShell; nullish means auto. */
|
|
17
|
+
getConfiguredDefaultShell?: () => string | null | undefined;
|
|
16
18
|
}
|
|
17
19
|
interface TerminalWorkspaceRef {
|
|
18
20
|
workspaceId: string;
|
|
@@ -33,6 +35,7 @@ export declare class TerminalSessionController {
|
|
|
33
35
|
private readonly listTerminalWorkspaceRoots;
|
|
34
36
|
private readonly clientSupportsWrapReflow;
|
|
35
37
|
private readonly getClientBufferedAmount;
|
|
38
|
+
private readonly getConfiguredDefaultShell;
|
|
36
39
|
private readonly terminalSizeOwner;
|
|
37
40
|
private readonly subscribedDirectories;
|
|
38
41
|
private unsubscribeTerminalsChanged;
|
|
@@ -74,6 +77,7 @@ export declare class TerminalSessionController {
|
|
|
74
77
|
private handleCreateTerminalRequest;
|
|
75
78
|
private resolveLegacyTerminalWorkspaceId;
|
|
76
79
|
private handleRenameTerminalRequest;
|
|
80
|
+
private handleShellDetectRequest;
|
|
77
81
|
private handleSubscribeTerminalRequest;
|
|
78
82
|
private handleUnsubscribeTerminalRequest;
|
|
79
83
|
private handleTerminalInput;
|