@timurproko/a1 0.1.1-dev.7 → 0.1.1-dev.8
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/{a1-guardian.js → guardian.js} +1 -1
- package/bin/{a1-ui.js → ui.js} +1 -8
- package/dist/native/darwin-arm64/manifest.json +3 -3
- package/dist/native/darwin-arm64/{a1-process-guardian → process-guardian} +0 -0
- package/dist/native/linux-x64/manifest.json +3 -3
- package/dist/native/linux-x64/{a1-process-guardian → process-guardian} +0 -0
- package/dist/native/win32-x64/manifest.json +3 -3
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/src/composition/index.js +7 -1
- package/dist/src/features/launch/initialize-profile.js +23 -2
- package/dist/src/features/launch/profiles.d.ts +1 -1
- package/dist/src/features/launch/profiles.js +2 -2
- package/dist/src/features/launch/runtime-selection.d.ts +13 -9
- package/dist/src/features/launch/runtime-selection.js +6 -6
- package/dist/src/features/owned-ui/settings-app.js +6 -4
- package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/index.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.d.ts +8 -0
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.js +12 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +5 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.js +7 -0
- package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +0 -2
- package/dist/src/foundation/pi-engine-adapter/settings-integration.js +24 -37
- package/dist/src/foundation/release/bootstrap.js +2 -2
- package/dist/src/foundation/ui-components/list-view.js +4 -4
- package/dist/src/foundation/ui-components/theme.d.ts +6 -0
- package/dist/src/foundation/ui-components/theme.js +1 -0
- package/dist/src/product-identity.json +7 -7
- package/docs/architecture/process-guardian-provenance.md +5 -5
- package/docs/architecture/toolchain.md +2 -2
- package/docs/manual-terminal-colour-check.md +55 -0
- package/package.json +6 -6
- package/dist/native/win32-x64/a1-process-guardian.exe +0 -0
- package/dist/src/composition/transparent-runtime.d.ts +0 -6
- package/dist/src/composition/transparent-runtime.js +0 -19
- package/dist/src/foundation/transparent-terminal/command-resolution.d.ts +0 -13
- package/dist/src/foundation/transparent-terminal/command-resolution.js +0 -72
- package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +0 -24
- package/dist/src/foundation/transparent-terminal/foreground-broker.js +0 -30
- package/dist/src/foundation/transparent-terminal/index.d.ts +0 -4
- package/dist/src/foundation/transparent-terminal/index.js +0 -4
- package/dist/src/foundation/transparent-terminal/main.d.ts +0 -9
- package/dist/src/foundation/transparent-terminal/main.js +0 -87
- package/dist/src/foundation/transparent-terminal/native-launcher.d.ts +0 -25
- package/dist/src/foundation/transparent-terminal/native-launcher.js +0 -146
- /package/bin/{a1.js → cli.js} +0 -0
- /package/bin/{a1-supervisor.js → supervisor.js} +0 -0
|
@@ -10,7 +10,7 @@ const profileId = process.env.A1_LAUNCH_PROFILE ?? "a1";
|
|
|
10
10
|
runLaunchGuardian({
|
|
11
11
|
profileId,
|
|
12
12
|
releaseRoot,
|
|
13
|
-
uiEntry: resolve(releaseRoot, "bin", "
|
|
13
|
+
uiEntry: resolve(releaseRoot, "bin", "ui.js"),
|
|
14
14
|
environment: process.env,
|
|
15
15
|
cwd: process.cwd(),
|
|
16
16
|
}).then(
|
package/bin/{a1-ui.js → ui.js}
RENAMED
|
@@ -3,21 +3,14 @@
|
|
|
3
3
|
const { runSelectedInteractiveRuntime } = await import("../dist/src/features/launch/index.js");
|
|
4
4
|
|
|
5
5
|
runSelectedInteractiveRuntime(process.env.A1_LAUNCH_PROFILE ?? "a1", {
|
|
6
|
-
ownedUi: async profileId => {
|
|
6
|
+
ownedUi: async (profileId, ownedSurfaces) => {
|
|
7
7
|
const [{ runOwnedUi }, { composeOwnedUi }] = await Promise.all([
|
|
8
8
|
import("../dist/src/features/owned-ui/index.js"),
|
|
9
9
|
import("../dist/src/composition/index.js"),
|
|
10
10
|
]);
|
|
11
|
-
// A1_OWNED_SURFACES=off presents pinned Pi's interface through A1's own
|
|
12
|
-
// rendering and input, which is what the parity comparison measures.
|
|
13
|
-
const ownedSurfaces = process.env.A1_OWNED_SURFACES === "off" ? "off" : "on";
|
|
14
11
|
const { application, settings } = await composeOwnedUi({ cwd: process.cwd(), profileId, ownedSurfaces });
|
|
15
12
|
return await runOwnedUi({ application, ...(settings === null ? {} : { settings }) });
|
|
16
13
|
},
|
|
17
|
-
transparent: async profileId => {
|
|
18
|
-
const { runSelectedTransparentRuntime } = await import("../dist/src/composition/transparent-runtime.js");
|
|
19
|
-
return await runSelectedTransparentRuntime(profileId);
|
|
20
|
-
},
|
|
21
14
|
}).then(
|
|
22
15
|
code => { process.exitCode = code; },
|
|
23
16
|
error => {
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "unsupported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-22T17:52:01.473Z",
|
|
9
9
|
"artifact": {
|
|
10
|
-
"filename": "
|
|
11
|
-
"sha256": "
|
|
10
|
+
"filename": "process-guardian",
|
|
11
|
+
"sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
|
|
12
12
|
"size": 356368
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-22T17:52:01.995Z",
|
|
9
9
|
"artifact": {
|
|
10
|
-
"filename": "
|
|
11
|
-
"sha256": "
|
|
10
|
+
"filename": "process-guardian",
|
|
11
|
+
"sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
|
|
12
12
|
"size": 415696
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-22T17:52:47.054Z",
|
|
9
9
|
"artifact": {
|
|
10
|
-
"filename": "
|
|
11
|
-
"sha256": "
|
|
10
|
+
"filename": "process-guardian.exe",
|
|
11
|
+
"sha256": "ed14b51aeb577ecd86df4a8a9ca3ea33c27885da8d3d196b26b0d705d70a0b71",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -5,7 +5,7 @@ import { assertPresentationComponent, assertPresentationRuntime, } from "../foun
|
|
|
5
5
|
import { OwnedUiSessionShell } from "../foundation/pi-owned-ui-integration/index.js";
|
|
6
6
|
import { OwnedUiSettingsSession, OwnedUiSettingsStore } from "../foundation/owned-ui-settings/index.js";
|
|
7
7
|
import { UiAppHost, UiAppRegistry } from "../foundation/ui-apps/index.js";
|
|
8
|
-
import { getAvailablePiThemes, piTheme } from "../foundation/pi-component-adapter/index.js";
|
|
8
|
+
import { applyConfiguredPiTheme, getAvailablePiThemes, piTheme } from "../foundation/pi-component-adapter/index.js";
|
|
9
9
|
import { faint } from "../foundation/ui-components/index.js";
|
|
10
10
|
import { SETTINGS_APP_ID, SETTINGS_ROUTE, SettingsApp } from "../features/owned-ui/index.js";
|
|
11
11
|
import { resolveProductPaths } from "../foundation/lifecycle/index.js";
|
|
@@ -29,6 +29,9 @@ export async function composeOwnedUi(options = {}) {
|
|
|
29
29
|
store: new OwnedUiSettingsStore({ configDir: resolveProductPaths().configDir, profileId: options.profileId }),
|
|
30
30
|
agentProvider: () => adapter.settingsPort(),
|
|
31
31
|
});
|
|
32
|
+
// Before anything is drawn, so every surface uses the configured theme rather
|
|
33
|
+
// than the one guessed from the terminal's background.
|
|
34
|
+
applyConfiguredPiTheme(adapter.configuredTheme());
|
|
32
35
|
const routeHost = settings === null || options.ownedSurfaces === "off" ? null : createOwnedRouteHost(settings);
|
|
33
36
|
const shell = new OwnedUiSessionShell({
|
|
34
37
|
backend: adapter,
|
|
@@ -243,6 +246,9 @@ function pinnedTheme() {
|
|
|
243
246
|
return {
|
|
244
247
|
fg: (token, text) => piTheme().fg(token, text),
|
|
245
248
|
bold: (text) => piTheme().bold(text),
|
|
249
|
+
// Unpainted, so a list reads against the terminal the reader actually has
|
|
250
|
+
// rather than against the background the configured theme was built for.
|
|
251
|
+
plain: (text) => text,
|
|
246
252
|
// Quiet, and quieter again: the terminal's own faint attribute over the
|
|
247
253
|
// dimmest colour the theme has.
|
|
248
254
|
disabled: (text) => faint(piTheme().fg("dim", text)),
|
|
@@ -38,7 +38,7 @@ async function ensureSafeDirectoryChain(root) {
|
|
|
38
38
|
current = parent;
|
|
39
39
|
}
|
|
40
40
|
for (const path of missing.reverse())
|
|
41
|
-
await
|
|
41
|
+
await createOwnedDirectory(path, "profile path");
|
|
42
42
|
}
|
|
43
43
|
async function ensureOwnedDirectory(path) {
|
|
44
44
|
const metadata = await lstat(path).catch(error => {
|
|
@@ -51,7 +51,28 @@ async function ensureOwnedDirectory(path) {
|
|
|
51
51
|
throw new Error(`profile resource path is not an owned directory: ${path}`);
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
|
-
await
|
|
54
|
+
await createOwnedDirectory(path, "profile resource path");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates a directory, treating "it already exists" as another launch having won
|
|
58
|
+
* the race rather than as a failure — but still refusing what it finds if that is
|
|
59
|
+
* not an owned directory.
|
|
60
|
+
*/
|
|
61
|
+
async function createOwnedDirectory(path, subject) {
|
|
62
|
+
try {
|
|
63
|
+
await mkdir(path, { mode: 0o700 });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!isAlreadyThere(error))
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const metadata = await lstat(path);
|
|
71
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
72
|
+
throw new Error(`${subject} is not an owned directory: ${path}`);
|
|
73
|
+
}
|
|
74
|
+
function isAlreadyThere(error) {
|
|
75
|
+
return typeof error === "object" && error !== null && error.code === "EEXIST";
|
|
55
76
|
}
|
|
56
77
|
function isMissing(error) {
|
|
57
78
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
@@ -7,6 +7,6 @@ export interface LaunchProfileContract {
|
|
|
7
7
|
readonly productSurface: "agent" | "vanilla-baseline" | "isolated-profile";
|
|
8
8
|
readonly configurationRootPolicy: ConfigurationRootPolicy;
|
|
9
9
|
readonly projectTrust: ProjectTrustPolicy;
|
|
10
|
-
readonly terminalCapability: "owned-ui"
|
|
10
|
+
readonly terminalCapability: "owned-ui";
|
|
11
11
|
}
|
|
12
12
|
export declare function launchProfileContract(id: LaunchProfileId): LaunchProfileContract;
|
|
@@ -11,14 +11,14 @@ const contracts = Object.freeze({
|
|
|
11
11
|
productSurface: "vanilla-baseline",
|
|
12
12
|
configurationRootPolicy: "pi-default",
|
|
13
13
|
projectTrust: "pi-default",
|
|
14
|
-
terminalCapability: "
|
|
14
|
+
terminalCapability: "owned-ui",
|
|
15
15
|
}),
|
|
16
16
|
sandbox: Object.freeze({
|
|
17
17
|
id: "sandbox",
|
|
18
18
|
productSurface: "isolated-profile",
|
|
19
19
|
configurationRootPolicy: "sandbox-profile",
|
|
20
20
|
projectTrust: "ignore",
|
|
21
|
-
terminalCapability: "
|
|
21
|
+
terminalCapability: "owned-ui",
|
|
22
22
|
}),
|
|
23
23
|
});
|
|
24
24
|
export function launchProfileContract(id) {
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { type LaunchProfileId } from "../../foundation/lifecycle/index.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Every interactive command is the same composition. What differs is the
|
|
4
|
+
* configuration root each one reads and whether A1's own screens are reachable:
|
|
5
|
+
* bare `a1` is the product, while `a1 pi` and `a1 sandbox` present pinned Pi's
|
|
6
|
+
* interface and nothing of A1's own, against Pi's own profile and against an
|
|
7
|
+
* isolated one.
|
|
8
|
+
*/
|
|
9
|
+
export type OwnedUiProfileId = LaunchProfileId;
|
|
10
|
+
export interface InteractiveRuntimeSelection {
|
|
4
11
|
readonly kind: "owned-ui";
|
|
5
|
-
readonly profileId:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly profileId: TransparentInteractiveProfileId;
|
|
9
|
-
};
|
|
12
|
+
readonly profileId: OwnedUiProfileId;
|
|
13
|
+
readonly ownedSurfaces: "on" | "off";
|
|
14
|
+
}
|
|
10
15
|
export interface InteractiveRuntimeRunners {
|
|
11
|
-
readonly ownedUi: (profileId:
|
|
12
|
-
readonly transparent: (profileId: TransparentInteractiveProfileId) => Promise<number>;
|
|
16
|
+
readonly ownedUi: (profileId: OwnedUiProfileId, ownedSurfaces: "on" | "off") => Promise<number>;
|
|
13
17
|
}
|
|
14
18
|
export declare function selectInteractiveRuntime(profileId: string): InteractiveRuntimeSelection;
|
|
15
19
|
export declare function runSelectedInteractiveRuntime(profileId: string, runners: InteractiveRuntimeRunners): Promise<number>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { assertLaunchProfileId } from "../../foundation/lifecycle/index.js";
|
|
2
2
|
export function selectInteractiveRuntime(profileId) {
|
|
3
3
|
assertLaunchProfileId(profileId);
|
|
4
|
-
return
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
kind: "owned-ui",
|
|
6
|
+
profileId,
|
|
7
|
+
ownedSurfaces: profileId === "a1" ? "on" : "off",
|
|
8
|
+
});
|
|
7
9
|
}
|
|
8
10
|
export async function runSelectedInteractiveRuntime(profileId, runners) {
|
|
9
11
|
const selection = selectInteractiveRuntime(profileId);
|
|
10
|
-
return selection.
|
|
11
|
-
? await runners.ownedUi(selection.profileId)
|
|
12
|
-
: await runners.transparent(selection.profileId);
|
|
12
|
+
return await runners.ownedUi(selection.profileId, selection.ownedSurfaces);
|
|
13
13
|
}
|
|
@@ -208,10 +208,12 @@ export class SettingsApp {
|
|
|
208
208
|
const overRow = menuRowAt(frame, event.row - 1, event.column);
|
|
209
209
|
const overMenu = overRow !== null;
|
|
210
210
|
if (event.kind === "motion") {
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
this.#hoverKey
|
|
211
|
+
// The menu owns the pointer, but the row it came from is still the thing
|
|
212
|
+
// being changed, so its value keeps reading as the one under the pointer.
|
|
213
|
+
// Only a dialog that takes the screen puts that out.
|
|
214
|
+
const hadHover = this.#hoverKey !== menu.anchorKey || this.#hoverRegion !== "value";
|
|
215
|
+
this.#hoverKey = menu.anchorKey;
|
|
216
|
+
this.#hoverRegion = "value";
|
|
215
217
|
if (!overMenu) {
|
|
216
218
|
const cleared = menu.index !== -1;
|
|
217
219
|
menu.index = -1;
|
|
@@ -3,7 +3,7 @@ export * from "./conformance.js";
|
|
|
3
3
|
export * from "./shell-components.js";
|
|
4
4
|
export * from "./theme.js";
|
|
5
5
|
export * from "./upstream/theme/theme-controller.js";
|
|
6
|
-
export { getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
6
|
+
export { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
7
7
|
export * from "./upstream/components/countdown-timer.js";
|
|
8
8
|
export * from "./upstream/components/extension-editor.js";
|
|
9
9
|
export * from "./upstream/components/session-selector.js";
|
|
@@ -3,7 +3,7 @@ export * from "./conformance.js";
|
|
|
3
3
|
export * from "./shell-components.js";
|
|
4
4
|
export * from "./theme.js";
|
|
5
5
|
export * from "./upstream/theme/theme-controller.js";
|
|
6
|
-
export { getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
6
|
+
export { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
7
7
|
export * from "./upstream/components/countdown-timer.js";
|
|
8
8
|
export * from "./upstream/components/extension-editor.js";
|
|
9
9
|
export * from "./upstream/components/session-selector.js";
|
|
@@ -51,3 +51,11 @@ export declare function resolvePiThemeSetting(setting: string | undefined, termi
|
|
|
51
51
|
export declare function detectPiTerminalBackgroundFromEnv(environment?: NodeJS.ProcessEnv): PiTerminalThemeDetection;
|
|
52
52
|
export declare function detectPiTerminalBackgroundTheme(ui: PiTerminalThemeDetector, timeoutMs: number, environment?: NodeJS.ProcessEnv): Promise<PiTerminalThemeDetection>;
|
|
53
53
|
export declare function detectPiTerminalThemeForAuto(ui: PiTerminalThemeDetector, timeoutMs: number, environment?: NodeJS.ProcessEnv): Promise<PiTerminalTheme>;
|
|
54
|
+
/**
|
|
55
|
+
* Applies the theme the reader configured, as the engine does at startup. The
|
|
56
|
+
* setting is either a theme's name or the `light/dark` pair meaning "follow the
|
|
57
|
+
* terminal", and only the pair consults the terminal's own background. An
|
|
58
|
+
* unreadable or absent setting falls back to that detection, which is what the
|
|
59
|
+
* engine does with an unset theme.
|
|
60
|
+
*/
|
|
61
|
+
export declare function applyConfiguredPiTheme(setting: string | undefined): PiThemeResult;
|
|
@@ -312,3 +312,15 @@ function ansi256ToHex(index) {
|
|
|
312
312
|
function isRecord(value) {
|
|
313
313
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
314
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Applies the theme the reader configured, as the engine does at startup. The
|
|
317
|
+
* setting is either a theme's name or the `light/dark` pair meaning "follow the
|
|
318
|
+
* terminal", and only the pair consults the terminal's own background. An
|
|
319
|
+
* unreadable or absent setting falls back to that detection, which is what the
|
|
320
|
+
* engine does with an unset theme.
|
|
321
|
+
*/
|
|
322
|
+
export function applyConfiguredPiTheme(setting) {
|
|
323
|
+
const detected = detectPiTerminalBackgroundFromEnv();
|
|
324
|
+
const resolved = resolvePiThemeSetting(setting, detected.theme);
|
|
325
|
+
return applyPiTheme(resolved ?? detected.theme);
|
|
326
|
+
}
|
|
@@ -113,6 +113,11 @@ export declare class PiEngineAdapter {
|
|
|
113
113
|
unbindExtensionUi(): Promise<void>;
|
|
114
114
|
workflowAutocompleteCommands(): readonly PiWorkflowAutocompleteCommand[];
|
|
115
115
|
cycleModelWorkflow(direction: "forward" | "backward"): Promise<PiWorkflowResult>;
|
|
116
|
+
/**
|
|
117
|
+
* The theme the engine is configured with, in the engine's own grammar: a
|
|
118
|
+
* theme's name, or a `light/dark` pair meaning "follow the terminal".
|
|
119
|
+
*/
|
|
120
|
+
configuredTheme(): string | undefined;
|
|
116
121
|
/** Settings port for the live runtime, or null before the runtime is available. */
|
|
117
122
|
settingsPort(): AgentSettingsPort | null;
|
|
118
123
|
pinnedModelSelectorContext(): {
|
|
@@ -380,6 +380,13 @@ export class PiEngineAdapter {
|
|
|
380
380
|
return workflowResult("model", "failed", error instanceof Error ? error.message : String(error));
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* The theme the engine is configured with, in the engine's own grammar: a
|
|
385
|
+
* theme's name, or a `light/dark` pair meaning "follow the terminal".
|
|
386
|
+
*/
|
|
387
|
+
configuredTheme() {
|
|
388
|
+
return this.#runtime?.services.settingsManager.getThemeSetting();
|
|
389
|
+
}
|
|
383
390
|
/** Settings port for the live runtime, or null before the runtime is available. */
|
|
384
391
|
settingsPort() {
|
|
385
392
|
const settings = this.#runtime?.services.settingsManager;
|
|
@@ -17,8 +17,6 @@ export interface PiSettingsProviders {
|
|
|
17
17
|
* entry per terminal appearance.
|
|
18
18
|
*/
|
|
19
19
|
export declare const AUTOMATIC_THEME = "automatic";
|
|
20
|
-
export declare const LIGHT_THEME_KEY = "themeLight";
|
|
21
|
-
export declare const DARK_THEME_KEY = "themeDark";
|
|
22
20
|
interface AutomaticTheme {
|
|
23
21
|
readonly light: string;
|
|
24
22
|
readonly dark: string;
|
|
@@ -7,8 +7,9 @@ import piSettingsMetadata from "./pi-settings-metadata.json" with { type: "json"
|
|
|
7
7
|
* entry per terminal appearance.
|
|
8
8
|
*/
|
|
9
9
|
export const AUTOMATIC_THEME = "automatic";
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** The themes an automatic setting names for each terminal appearance. */
|
|
11
|
+
const LIGHT_APPEARANCE_THEME = "light";
|
|
12
|
+
const DARK_APPEARANCE_THEME = "dark";
|
|
12
13
|
const THEME_KEY = "theme";
|
|
13
14
|
const THINKING_KEY = "thinkingLevel";
|
|
14
15
|
/** A stored theme is automatic when it names one theme per terminal appearance. */
|
|
@@ -47,10 +48,6 @@ export function settingsInventoryDrift(presented, mapped) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
/** What the engine offers for a setting, as the strings it words them with. */
|
|
50
|
-
/** One appearance of an automatic theme, offered as an ordinary setting. */
|
|
51
|
-
function appearance(key, label, description, themes) {
|
|
52
|
-
return { key, valueType: "enum", writable: true, choices: themes, label, description, resolvedWhenRead: true };
|
|
53
|
-
}
|
|
54
51
|
function offered(key) {
|
|
55
52
|
return PRESENTATION.settings[key]?.values ?? [];
|
|
56
53
|
}
|
|
@@ -95,15 +92,9 @@ export class PiSettingsIntegration {
|
|
|
95
92
|
})
|
|
96
93
|
.map(descriptor => this.#resolved(descriptor))
|
|
97
94
|
.sort((left, right) => rank(left.key) - rank(right.key));
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const themes = this.#themes();
|
|
102
|
-
return [
|
|
103
|
-
...listed,
|
|
104
|
-
appearance(LIGHT_THEME_KEY, "Light theme", "Theme to use in automatic mode when the terminal is light", themes),
|
|
105
|
-
appearance(DARK_THEME_KEY, "Dark theme", "Theme to use in automatic mode when the terminal is dark", themes),
|
|
106
|
-
];
|
|
95
|
+
// Following the terminal is one choice on the theme itself, not two further
|
|
96
|
+
// settings: the pair behind it is the engine's grammar, not a reader's task.
|
|
97
|
+
return listed;
|
|
107
98
|
}
|
|
108
99
|
/** Overlays the values a setting can only offer once something is running. */
|
|
109
100
|
#resolved(descriptor) {
|
|
@@ -129,20 +120,16 @@ export class PiSettingsIntegration {
|
|
|
129
120
|
return typeof stored === "string" ? stored : "";
|
|
130
121
|
}
|
|
131
122
|
async readSetting(key) {
|
|
132
|
-
if (key === THEME_KEY
|
|
123
|
+
if (key === THEME_KEY) {
|
|
133
124
|
const pair = parseAutomaticTheme(this.#storedTheme());
|
|
134
|
-
|
|
135
|
-
return pair === null ? this.#operations.get(key)?.read() : AUTOMATIC_THEME;
|
|
136
|
-
if (pair === null)
|
|
137
|
-
return undefined;
|
|
138
|
-
return key === LIGHT_THEME_KEY ? pair.light : pair.dark;
|
|
125
|
+
return pair === null ? this.#operations.get(key)?.read() : AUTOMATIC_THEME;
|
|
139
126
|
}
|
|
140
127
|
return this.#operations.get(key)?.read();
|
|
141
128
|
}
|
|
142
129
|
async writeSetting(key, value) { this.writeSettingNow(key, value); }
|
|
143
130
|
writeSettingNow(key, value) {
|
|
144
|
-
if (typeof value === "string" &&
|
|
145
|
-
this.#writeTheme(
|
|
131
|
+
if (typeof value === "string" && key === THEME_KEY) {
|
|
132
|
+
this.#writeTheme(value);
|
|
146
133
|
return;
|
|
147
134
|
}
|
|
148
135
|
const operation = this.#operations.get(key);
|
|
@@ -151,26 +138,26 @@ export class PiSettingsIntegration {
|
|
|
151
138
|
operation.write(value);
|
|
152
139
|
}
|
|
153
140
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
141
|
+
* Writes the theme in the engine's own grammar. Following the terminal is
|
|
142
|
+
* stored as the pair the engine reads for it, made of the themes named for
|
|
143
|
+
* each appearance when they are installed; where they are not, the theme
|
|
144
|
+
* already in use stands for both, so the setting still round-trips.
|
|
157
145
|
*/
|
|
158
|
-
#writeTheme(
|
|
146
|
+
#writeTheme(value) {
|
|
159
147
|
const theme = this.#operations.get(THEME_KEY);
|
|
160
148
|
if (!theme)
|
|
161
|
-
throw new Error(`setting is unavailable: ${
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (key === THEME_KEY) {
|
|
165
|
-
if (value !== AUTOMATIC_THEME)
|
|
166
|
-
theme.write(value);
|
|
167
|
-
else if (pair === null)
|
|
168
|
-
theme.write(`${current}/${current}`);
|
|
149
|
+
throw new Error(`setting is unavailable: ${THEME_KEY}`);
|
|
150
|
+
if (value !== AUTOMATIC_THEME) {
|
|
151
|
+
theme.write(value);
|
|
169
152
|
return;
|
|
170
153
|
}
|
|
171
|
-
if (
|
|
154
|
+
if (parseAutomaticTheme(this.#storedTheme()) !== null)
|
|
172
155
|
return;
|
|
173
|
-
|
|
156
|
+
const installed = this.#themes();
|
|
157
|
+
const current = this.#storedTheme();
|
|
158
|
+
const light = installed.includes(LIGHT_APPEARANCE_THEME) ? LIGHT_APPEARANCE_THEME : current;
|
|
159
|
+
const dark = installed.includes(DARK_APPEARANCE_THEME) ? DARK_APPEARANCE_THEME : current;
|
|
160
|
+
theme.write(`${light}/${dark}`);
|
|
174
161
|
}
|
|
175
162
|
async flush() { await this.settings.flush(); }
|
|
176
163
|
}
|
|
@@ -138,7 +138,7 @@ export async function certifyMaterializedRelease(release, dataDir, verification
|
|
|
138
138
|
return path;
|
|
139
139
|
}
|
|
140
140
|
export async function startSupervisor(release, environment) {
|
|
141
|
-
const entry = await resolveReleaseEntryPoint(release, "bin/
|
|
141
|
+
const entry = await resolveReleaseEntryPoint(release, "bin/supervisor.js");
|
|
142
142
|
const child = spawn(process.execPath, [entry], {
|
|
143
143
|
detached: true,
|
|
144
144
|
env: releaseEnvironment(environment, release),
|
|
@@ -152,7 +152,7 @@ export async function startSupervisor(release, environment) {
|
|
|
152
152
|
child.unref();
|
|
153
153
|
}
|
|
154
154
|
async function launchUi(release, environment) {
|
|
155
|
-
const entry = await resolveReleaseEntryPoint(release, "bin/
|
|
155
|
+
const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
|
|
156
156
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
157
157
|
const child = spawn(process.execPath, [entry], {
|
|
158
158
|
env: releaseEnvironment(environment, release),
|
|
@@ -23,21 +23,21 @@ export function renderGroupHeader(title, width, theme) {
|
|
|
23
23
|
export function renderListRow(row, state, valueColumn, width, theme) {
|
|
24
24
|
const cursor = state.selected ? "→ " : " ";
|
|
25
25
|
const leftRaw = `${cursor} ${row.label}`;
|
|
26
|
-
const left = `${theme.fg("accent", cursor)} ${
|
|
26
|
+
const left = `${theme.fg("accent", cursor)} ${state.selected ? theme.fg("accent", row.label) : theme.plain(row.label)}`;
|
|
27
27
|
const gap = Math.max(2, valueColumn - displayWidth(leftRaw));
|
|
28
28
|
// Pointing anywhere on the row is pointing at the item; pointing at the value
|
|
29
29
|
// is what brightens it. The selection speaks through the label alone.
|
|
30
30
|
const valueHovered = state.hovered && state.region !== "label";
|
|
31
31
|
const stepper = row.stepper !== undefined && valueHovered;
|
|
32
|
-
const value =
|
|
32
|
+
const value = valueHovered ? theme.plain(row.value) : theme.fg("muted", row.value);
|
|
33
33
|
const minus = stepper
|
|
34
34
|
? row.stepper?.lower === true
|
|
35
|
-
?
|
|
35
|
+
? state.region === "minus" ? theme.plain("- ") : theme.fg("dim", "- ")
|
|
36
36
|
: theme.disabled("- ")
|
|
37
37
|
: "";
|
|
38
38
|
const plus = stepper
|
|
39
39
|
? row.stepper?.raise === true
|
|
40
|
-
?
|
|
40
|
+
? state.region === "plus" ? theme.plain(" +") : theme.fg("dim", " +")
|
|
41
41
|
: theme.disabled(" +")
|
|
42
42
|
: "";
|
|
43
43
|
const indent = Math.max(2, stepper ? gap - STEPPER_RESERVE : gap);
|
|
@@ -6,6 +6,12 @@ export type UiThemeToken = "accent" | "text" | "muted" | "dim" | "border" | "err
|
|
|
6
6
|
export interface UiTheme {
|
|
7
7
|
fg(token: UiThemeToken, text: string): string;
|
|
8
8
|
bold(text: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The terminal's own foreground, left unpainted. What a list's own text is:
|
|
11
|
+
* a theme built for a light background would otherwise write near-black rows
|
|
12
|
+
* into a dark terminal, which is how the engine leaves its lists too.
|
|
13
|
+
*/
|
|
14
|
+
plain(text: string): string;
|
|
9
15
|
/** Inverted styling for the active row of a floating surface. */
|
|
10
16
|
highlight(text: string): string;
|
|
11
17
|
/** A control that is present but cannot act: quieter than quiet text. */
|
|
@@ -86,13 +86,13 @@
|
|
|
86
86
|
"piEventFrameParitySchema": "a1-pi-event-frame-parity-v1"
|
|
87
87
|
},
|
|
88
88
|
"artifacts": {
|
|
89
|
-
"cliEntry": "bin/
|
|
90
|
-
"supervisorEntry": "bin/
|
|
91
|
-
"guardianEntry": "bin/
|
|
92
|
-
"uiEntry": "bin/
|
|
93
|
-
"nativeExecutable": "
|
|
94
|
-
"nativeCrate": "
|
|
95
|
-
"processGuardianExecutable": "
|
|
89
|
+
"cliEntry": "bin/cli.js",
|
|
90
|
+
"supervisorEntry": "bin/supervisor.js",
|
|
91
|
+
"guardianEntry": "bin/guardian.js",
|
|
92
|
+
"uiEntry": "bin/ui.js",
|
|
93
|
+
"nativeExecutable": "terminal-host",
|
|
94
|
+
"nativeCrate": "terminal-host",
|
|
95
|
+
"processGuardianExecutable": "process-guardian",
|
|
96
96
|
"releaseTarballStem": "timurproko-a1",
|
|
97
97
|
"diagnosticStem": "a1"
|
|
98
98
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Process guardian provenance
|
|
2
2
|
|
|
3
|
-
A1 launch-instance containment uses a standalone native executable named `
|
|
3
|
+
A1 launch-instance containment uses a standalone native executable named `process-guardian`. The approved implementation is a separate Rust crate at `native/process-guardian`; it is not a Node-API addon and does not reuse or resume the held composed terminal-host implementation.
|
|
4
4
|
|
|
5
5
|
## Boundary
|
|
6
6
|
|
|
@@ -13,10 +13,10 @@ On Windows it uses `windows-sys 0.61.2` to create a Job Object, enables `JOB_OBJ
|
|
|
13
13
|
Release builds place the exact executable at:
|
|
14
14
|
|
|
15
15
|
```text
|
|
16
|
-
dist/native/win32-x64/
|
|
17
|
-
dist/native/linux-x64/
|
|
18
|
-
dist/native/darwin-x64/
|
|
19
|
-
dist/native/darwin-arm64/
|
|
16
|
+
dist/native/win32-x64/process-guardian.exe
|
|
17
|
+
dist/native/linux-x64/process-guardian
|
|
18
|
+
dist/native/darwin-x64/process-guardian
|
|
19
|
+
dist/native/darwin-arm64/process-guardian
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Each supported artifact must be named in the release integrity manifest with target triple, crate version, guardian protocol version, SHA-256, byte size, build provenance, and signature/attestation status. A missing, altered, incompatible, or wrong-platform artifact is a concise unsupported-containment launch error; A1 never silently falls back to PID-only cleanup.
|
|
@@ -65,9 +65,9 @@ This mandatory candidate workflow checks documented package-root exports and the
|
|
|
65
65
|
npm run sync:pi-ui
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
This optional, mutating presentation-maintenance workflow regenerates component and event-frame parity evidence when maintainers deliberately adopt upstream UI changes. Review generated diffs, attribution, source-ledger records
|
|
68
|
+
This optional, mutating presentation-maintenance workflow regenerates component and event-frame parity evidence when maintainers deliberately adopt upstream UI changes. Review generated diffs, attribution, and source-ledger records before committing them. It is never an engine candidate acceptance gate and is not run by `check`, `prepack`, or release publication.
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Presentation acceptance is the reader comparing `a1 pi` with pinned Pi. `node scripts/check-pinned-pi-source-ledger.mjs` validates accepted provenance; its `--engine-only` mode validates the ownership partition without comparing private upstream source.
|
|
71
71
|
|
|
72
72
|
## Preview publication
|
|
73
73
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Terminal colour check
|
|
2
|
+
|
|
3
|
+
## What can go wrong
|
|
4
|
+
|
|
5
|
+
Every colour the interface draws is 24-bit: the pinned dark theme's accent is
|
|
6
|
+
`#8abeb7`, its border `#5f87ff`, its headings `#f0c674`. On Windows, Node decides
|
|
7
|
+
once at startup whether it may send those sequences to the terminal at all. It
|
|
8
|
+
asks the console to enable virtual terminal processing; when that fails it renders
|
|
9
|
+
ANSI itself, and its renderer knows only the sixteen SGR colours. Each 24-bit
|
|
10
|
+
colour is then replaced by the terminal's nearest palette entry, so the whole
|
|
11
|
+
interface shifts at once — accent to the terminal's cyan, headings to its yellow —
|
|
12
|
+
while the application still writes exactly the bytes it always wrote.
|
|
13
|
+
|
|
14
|
+
That decision is made per process, from the console handle the process was given,
|
|
15
|
+
and every process it launches inherits the result. It is a property of how the
|
|
16
|
+
session was started, not of what is drawn.
|
|
17
|
+
|
|
18
|
+
## Launching so colour survives
|
|
19
|
+
|
|
20
|
+
The installed command is an npm shim — a shell script that `exec`s Node — and that
|
|
21
|
+
shape keeps 24-bit colour. So does `pi`, for the same reason.
|
|
22
|
+
|
|
23
|
+
- Installed: `a1`, `a1 pi`
|
|
24
|
+
- From a checkout: `scripts/dev` and `scripts/dev pi`
|
|
25
|
+
|
|
26
|
+
`npm start` and `npm run start:pi` build first and then launch through the same
|
|
27
|
+
entry, but a package manager on Windows hands the script to `cmd.exe`, which can
|
|
28
|
+
leave the launch without a terminal on its input; the launcher says so and stops
|
|
29
|
+
rather than opening a session that closes as it starts. Running `scripts/dev`
|
|
30
|
+
from the shell is the direct path.
|
|
31
|
+
|
|
32
|
+
A directly launched `node scripts/start-local.mjs` under Git Bash does not, and
|
|
33
|
+
neither does a directly launched `node …/pi/dist/cli.js`: the collapse belongs to
|
|
34
|
+
the launch, not to either application.
|
|
35
|
+
|
|
36
|
+
## Checking a terminal
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
node scripts/check-terminal-colour.mjs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It prints the same colour as 24-bit, as a 256-colour index, and as the terminal's
|
|
43
|
+
own palette entry. When the first block is indistinguishable from the third, this
|
|
44
|
+
launch collapses colour and any comparison made in it is a comparison of two wrong
|
|
45
|
+
screens.
|
|
46
|
+
|
|
47
|
+
## What the gates hold
|
|
48
|
+
|
|
49
|
+
- `test/features/launch/terminal-colour-fidelity.test.ts` holds the emitted bytes:
|
|
50
|
+
the pinned palette resolves to 24-bit sequences, a 256-colour terminal stays on
|
|
51
|
+
indices, and the launch chain keeps inheriting the terminal it was started from
|
|
52
|
+
rather than opening a console of its own.
|
|
53
|
+
- The reader compares `a1 pi` with pinned Pi in the same terminal. Run the colour
|
|
54
|
+
check first: a launch that collapses colour makes both sessions equally wrong, so
|
|
55
|
+
they would agree with each other and disagree with what a user sees.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.8",
|
|
4
4
|
"description": "Standalone terminal workspace for supervised native and managed agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.13.0",
|
|
7
7
|
"bin": {
|
|
8
|
-
"a1": "bin/
|
|
8
|
+
"a1": "bin/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
"test:full": "node scripts/run-validation-tier.mjs full-release",
|
|
35
35
|
"test:release": "node scripts/run-release-gates.mjs",
|
|
36
36
|
"test:terminal-host": "node scripts/run-terminal-host-probe.mjs",
|
|
37
|
-
"test:pi-terminal-parity": "npm run build --silent && node scripts/run-pi-terminal-parity.mjs",
|
|
38
37
|
"test:pi-engine-conformance": "vitest run test/foundation/pi-engine-adapter/conformance.test.ts test/foundation/pi-engine-adapter/runtime-integration.test.ts test/foundation/pi-engine-adapter/session-integration.test.ts",
|
|
39
38
|
"report:pi-engine-conformance": "npm run build --silent && node scripts/run-pi-engine-conformance.mjs",
|
|
40
39
|
"sync:pi-ui": "npm run update:pi-component-parity && npm run update:pi-event-frame-parity && npm run update:pi-settings-metadata",
|
|
41
40
|
"update:pi-component-parity": "tsx scripts/update-pi-component-parity.ts",
|
|
42
41
|
"update:pi-event-frame-parity": "tsx scripts/update-pi-event-frame-parity.ts",
|
|
43
42
|
"proof:terminal-host": "node scripts/run-terminal-host.mjs",
|
|
44
|
-
"start": "npm run build && node scripts/
|
|
43
|
+
"start": "npm run build && node scripts/dev-launch.mjs",
|
|
44
|
+
"start:pi": "npm run build && node scripts/dev-launch.mjs pi",
|
|
45
|
+
"start:sandbox": "npm run build && node scripts/dev-launch.mjs sandbox",
|
|
45
46
|
"prepack": "node scripts/prepack-gate.mjs",
|
|
46
47
|
"prepublishOnly": "npm run test:release",
|
|
47
48
|
"prepare": "npm run build",
|
|
48
|
-
"update:pi-settings-metadata": "node scripts/update-pi-settings-metadata.mjs"
|
|
49
|
-
"clean:worktrees": "node scripts/clean-worktrees.mjs"
|
|
49
|
+
"update:pi-settings-metadata": "node scripts/update-pi-settings-metadata.mjs"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
Binary file
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { LaunchProfileId } from "../foundation/lifecycle/index.js";
|
|
2
|
-
export declare function selectTransparentChild(environment?: NodeJS.ProcessEnv, executable?: string, compositionUrl?: string): {
|
|
3
|
-
readonly executable: string;
|
|
4
|
-
readonly arguments: readonly string[];
|
|
5
|
-
};
|
|
6
|
-
export declare function runSelectedTransparentRuntime(profileId: Exclude<LaunchProfileId, "a1">, environment?: NodeJS.ProcessEnv): Promise<number>;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { runTransparentForeground } from "../foundation/transparent-terminal/index.js";
|
|
3
|
-
export function selectTransparentChild(environment = process.env, executable = process.execPath, compositionUrl = import.meta.url) {
|
|
4
|
-
const entry = fileURLToPath(new URL("../foundation/pi-engine-adapter/public-main-entry.js", compositionUrl));
|
|
5
|
-
return { executable, arguments: [entry, ...parseSelectedArguments(environment.A1_LAUNCH_ARGUMENTS_JSON)] };
|
|
6
|
-
}
|
|
7
|
-
export async function runSelectedTransparentRuntime(profileId, environment = process.env) {
|
|
8
|
-
const child = selectTransparentChild(environment);
|
|
9
|
-
return await runTransparentForeground({ profileId, environment, ...child });
|
|
10
|
-
}
|
|
11
|
-
function parseSelectedArguments(source) {
|
|
12
|
-
if (!source)
|
|
13
|
-
return [];
|
|
14
|
-
const value = JSON.parse(source);
|
|
15
|
-
if (!Array.isArray(value) || value.some(item => typeof item !== "string")) {
|
|
16
|
-
throw new TypeError("A1_LAUNCH_ARGUMENTS_JSON must be a JSON array of strings");
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface ResolvedTransparentCommand {
|
|
2
|
-
readonly executable: string;
|
|
3
|
-
readonly arguments: readonly string[];
|
|
4
|
-
readonly source: "exact" | "path" | "npm-windows-shim";
|
|
5
|
-
}
|
|
6
|
-
export interface TransparentCommandResolutionOptions {
|
|
7
|
-
readonly platform?: NodeJS.Platform;
|
|
8
|
-
readonly cwd: string;
|
|
9
|
-
readonly environment: Readonly<Record<string, string>>;
|
|
10
|
-
readonly nodeExecutable?: string;
|
|
11
|
-
}
|
|
12
|
-
/** Resolve a command without invoking a command interpreter. */
|
|
13
|
-
export declare function resolveTransparentCommand(executable: string, arguments_: readonly string[], options: TransparentCommandResolutionOptions): Promise<ResolvedTransparentCommand>;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { access, readFile } from "node:fs/promises";
|
|
2
|
-
import { extname, isAbsolute, resolve } from "node:path";
|
|
3
|
-
/** Resolve a command without invoking a command interpreter. */
|
|
4
|
-
export async function resolveTransparentCommand(executable, arguments_, options) {
|
|
5
|
-
const platform = options.platform ?? process.platform;
|
|
6
|
-
return platform === "win32"
|
|
7
|
-
? await resolveWindowsCommand(executable, arguments_, options)
|
|
8
|
-
: { executable, arguments: [...arguments_], source: "exact" };
|
|
9
|
-
}
|
|
10
|
-
async function resolveWindowsCommand(command, commandArguments, options) {
|
|
11
|
-
const resolved = await resolveWindowsPathCommand(command, options.cwd, options.environment);
|
|
12
|
-
const selected = resolved ?? throwCommandNotFound(command);
|
|
13
|
-
if (extname(selected).toLowerCase() !== ".cmd") {
|
|
14
|
-
return { executable: selected, arguments: [...commandArguments], source: "path" };
|
|
15
|
-
}
|
|
16
|
-
return await unwrapNpmWindowsShim(selected, commandArguments, options.nodeExecutable ?? process.execPath);
|
|
17
|
-
}
|
|
18
|
-
async function resolveWindowsPathCommand(executable, cwd, environment) {
|
|
19
|
-
const hasPath = isAbsolute(executable) || executable.includes("/") || executable.includes("\\");
|
|
20
|
-
const roots = hasPath ? [cwd] : windowsPath(environment).split(";").filter(Boolean);
|
|
21
|
-
const extension = extname(executable);
|
|
22
|
-
const extensions = extension ? [""] : windowsExecutableExtensions(environment);
|
|
23
|
-
for (const root of roots) {
|
|
24
|
-
const base = hasPath ? resolve(cwd, executable) : resolve(root.replace(/^"|"$/g, ""), executable);
|
|
25
|
-
for (const suffix of extensions) {
|
|
26
|
-
const candidate = `${base}${suffix}`;
|
|
27
|
-
if (await isFile(candidate))
|
|
28
|
-
return candidate;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
async function unwrapNpmWindowsShim(shimPath, arguments_, fallbackNodeExecutable) {
|
|
34
|
-
const source = (await readFile(shimPath, "utf8")).replace(/^\uFEFF/, "");
|
|
35
|
-
const targetMatch = source.match(/"%_prog%"\s+"%dp0%\\([^"\r\n]+)"\s+%\*/i);
|
|
36
|
-
if (!targetMatch?.[1]) {
|
|
37
|
-
throw Object.assign(new Error(`transparent launch refuses arbitrary Windows command script ${shimPath}; configure an executable binary instead`), { code: "UNSUPPORTED_WINDOWS_COMMAND_SHIM" });
|
|
38
|
-
}
|
|
39
|
-
const shimRoot = resolve(shimPath, "..");
|
|
40
|
-
const target = resolve(shimRoot, targetMatch[1].replaceAll("\\", "/"));
|
|
41
|
-
if (!await isFile(target))
|
|
42
|
-
throw Object.assign(new Error(`npm command shim target is missing: ${target}`), { code: "ENOENT" });
|
|
43
|
-
const localNode = resolve(shimRoot, "node.exe");
|
|
44
|
-
const nodeExecutable = await isFile(localNode) ? localNode : fallbackNodeExecutable;
|
|
45
|
-
return {
|
|
46
|
-
executable: nodeExecutable,
|
|
47
|
-
arguments: [target, ...arguments_],
|
|
48
|
-
source: "npm-windows-shim",
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function throwCommandNotFound(command) {
|
|
52
|
-
throw Object.assign(new Error(`spawn ${command} ENOENT`), { code: "ENOENT" });
|
|
53
|
-
}
|
|
54
|
-
function windowsPath(environment) {
|
|
55
|
-
return environment.Path ?? environment.PATH ?? "";
|
|
56
|
-
}
|
|
57
|
-
function windowsExecutableExtensions(environment) {
|
|
58
|
-
const source = environment.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
59
|
-
return [...new Set(source.split(";").filter(Boolean).flatMap(value => {
|
|
60
|
-
const extension = value.startsWith(".") ? value : `.${value}`;
|
|
61
|
-
return [extension, extension.toLowerCase()];
|
|
62
|
-
}))];
|
|
63
|
-
}
|
|
64
|
-
async function isFile(path) {
|
|
65
|
-
try {
|
|
66
|
-
await access(path);
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { NativeProcessIdentity, TransparentTerminalLaunchProfile, TransparentTerminalLifecycleOutcome } from "../lifecycle/index.js";
|
|
2
|
-
export type TransparentStopReason = "owner-disconnect" | "user-request" | "update";
|
|
3
|
-
export interface TransparentChildHandle {
|
|
4
|
-
readonly processIdentity: NativeProcessIdentity;
|
|
5
|
-
readonly outcome: Promise<TransparentTerminalLifecycleOutcome>;
|
|
6
|
-
stop(reason: TransparentStopReason): Promise<TransparentTerminalLifecycleOutcome>;
|
|
7
|
-
}
|
|
8
|
-
export interface TransparentNativeLauncher {
|
|
9
|
-
launch(profile: TransparentTerminalLaunchProfile): Promise<TransparentChildHandle>;
|
|
10
|
-
}
|
|
11
|
-
export interface ForegroundBrokerRequest {
|
|
12
|
-
readonly profile: TransparentTerminalLaunchProfile;
|
|
13
|
-
readonly stopRequested?: Promise<TransparentStopReason>;
|
|
14
|
-
}
|
|
15
|
-
export interface ForegroundBrokerResult {
|
|
16
|
-
readonly processIdentity: NativeProcessIdentity | null;
|
|
17
|
-
readonly outcome: TransparentTerminalLifecycleOutcome;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Waits for one directly attached child. Launch-instance registration and
|
|
21
|
-
* process-tree containment belong to the outer profile-neutral guardian.
|
|
22
|
-
* Ordinary terminal data never enters this object.
|
|
23
|
-
*/
|
|
24
|
-
export declare function runForegroundBroker(request: ForegroundBrokerRequest, launcher: TransparentNativeLauncher): Promise<ForegroundBrokerResult>;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { assertNativeProcessIdentity, assertTransparentTerminalLaunchProfile } from "../lifecycle/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* Waits for one directly attached child. Launch-instance registration and
|
|
4
|
-
* process-tree containment belong to the outer profile-neutral guardian.
|
|
5
|
-
* Ordinary terminal data never enters this object.
|
|
6
|
-
*/
|
|
7
|
-
export async function runForegroundBroker(request, launcher) {
|
|
8
|
-
assertTransparentTerminalLaunchProfile(request.profile);
|
|
9
|
-
let handle;
|
|
10
|
-
try {
|
|
11
|
-
handle = await launcher.launch(request.profile);
|
|
12
|
-
assertNativeProcessIdentity(handle.processIdentity);
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
return {
|
|
16
|
-
processIdentity: null,
|
|
17
|
-
outcome: { kind: "spawn-error", message: errorMessage(error), code: errorCode(error) },
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
const outcome = request.stopRequested
|
|
21
|
-
? await Promise.race([handle.outcome, request.stopRequested.then(async (reason) => await handle.stop(reason))])
|
|
22
|
-
: await handle.outcome;
|
|
23
|
-
return { processIdentity: handle.processIdentity, outcome };
|
|
24
|
-
}
|
|
25
|
-
function errorMessage(error) {
|
|
26
|
-
return error instanceof Error ? error.message : String(error);
|
|
27
|
-
}
|
|
28
|
-
function errorCode(error) {
|
|
29
|
-
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
30
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type LaunchProfileId } from "../lifecycle/index.js";
|
|
2
|
-
export interface TransparentForegroundOptions {
|
|
3
|
-
readonly environment?: NodeJS.ProcessEnv;
|
|
4
|
-
readonly profileId?: LaunchProfileId;
|
|
5
|
-
readonly cwd?: string;
|
|
6
|
-
readonly executable: string;
|
|
7
|
-
readonly arguments: readonly string[];
|
|
8
|
-
}
|
|
9
|
-
export declare function runTransparentForeground(options: TransparentForegroundOptions): Promise<number>;
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { realpath } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { assertLaunchProfileId } from "../lifecycle/index.js";
|
|
5
|
-
import { runForegroundBroker } from "./foreground-broker.js";
|
|
6
|
-
import { createPlatformTransparentLauncher } from "./native-launcher.js";
|
|
7
|
-
export async function runTransparentForeground(options) {
|
|
8
|
-
const environment = { ...(options.environment ?? process.env) };
|
|
9
|
-
const profileId = options.profileId ?? environment.A1_LAUNCH_PROFILE ?? "a1";
|
|
10
|
-
assertLaunchProfileId(profileId);
|
|
11
|
-
const profile = await launchProfile(environment, options, profileId);
|
|
12
|
-
const stop = stopSignal();
|
|
13
|
-
try {
|
|
14
|
-
const result = await runForegroundBroker({ profile, stopRequested: stop.requested }, createPlatformTransparentLauncher());
|
|
15
|
-
if (result.outcome.kind === "exited")
|
|
16
|
-
return result.outcome.exitCode;
|
|
17
|
-
if (result.outcome.kind === "signaled")
|
|
18
|
-
return 1;
|
|
19
|
-
if (result.outcome.kind === "stopped" || result.outcome.kind === "detached")
|
|
20
|
-
return 0;
|
|
21
|
-
throw Object.assign(new Error(result.outcome.message), { code: result.outcome.code ?? undefined });
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
stop.dispose();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
async function launchProfile(environment, options, profileId) {
|
|
28
|
-
const executable = options.executable;
|
|
29
|
-
const arguments_ = options.arguments;
|
|
30
|
-
const cwd = await realpath(options.cwd ?? process.cwd());
|
|
31
|
-
const terminalType = environment.TERM?.trim() || "xterm-256color";
|
|
32
|
-
return {
|
|
33
|
-
id: `${profileId}-transparent-${randomUUID()}`,
|
|
34
|
-
terminalCapability: "transparent",
|
|
35
|
-
executable,
|
|
36
|
-
arguments: arguments_,
|
|
37
|
-
cwd,
|
|
38
|
-
environment: selectedChildEnvironment(environment),
|
|
39
|
-
terminalType,
|
|
40
|
-
dimensions: terminalDimensions(environment),
|
|
41
|
-
ownerDisconnect: "stop",
|
|
42
|
-
recovery: "none",
|
|
43
|
-
surface: "none",
|
|
44
|
-
visualReconnection: "none",
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function selectedChildEnvironment(environment) {
|
|
48
|
-
const selected = {};
|
|
49
|
-
for (const [name, value] of Object.entries(environment)) {
|
|
50
|
-
if (value !== undefined && !name.startsWith("A1_"))
|
|
51
|
-
selected[name] = value;
|
|
52
|
-
}
|
|
53
|
-
return selected;
|
|
54
|
-
}
|
|
55
|
-
function terminalDimensions(environment) {
|
|
56
|
-
const columns = positiveInteger(environment.COLUMNS) ?? 80;
|
|
57
|
-
const rows = positiveInteger(environment.LINES) ?? 24;
|
|
58
|
-
return { columns: Math.min(500, Math.max(2, columns)), rows: Math.min(300, Math.max(1, rows)) };
|
|
59
|
-
}
|
|
60
|
-
function positiveInteger(value) {
|
|
61
|
-
const parsed = Number(value);
|
|
62
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
63
|
-
}
|
|
64
|
-
function stopSignal() {
|
|
65
|
-
let resolveStop;
|
|
66
|
-
let settled = false;
|
|
67
|
-
const requested = new Promise(resolve => { resolveStop = resolve; });
|
|
68
|
-
const request = (reason) => {
|
|
69
|
-
if (settled)
|
|
70
|
-
return;
|
|
71
|
-
settled = true;
|
|
72
|
-
resolveStop(reason);
|
|
73
|
-
};
|
|
74
|
-
const onSigterm = () => request("user-request");
|
|
75
|
-
// The attached child shares foreground signal delivery. Keep the broker alive
|
|
76
|
-
// without translating or forwarding Ctrl+C.
|
|
77
|
-
const onSigint = () => undefined;
|
|
78
|
-
process.once("SIGTERM", onSigterm);
|
|
79
|
-
process.on("SIGINT", onSigint);
|
|
80
|
-
return {
|
|
81
|
-
requested,
|
|
82
|
-
dispose() {
|
|
83
|
-
process.off("SIGTERM", onSigterm);
|
|
84
|
-
process.off("SIGINT", onSigint);
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
-
import type { TransparentTerminalLaunchProfile } from "../lifecycle/index.js";
|
|
3
|
-
import type { TransparentChildHandle, TransparentNativeLauncher } from "./foreground-broker.js";
|
|
4
|
-
export interface NativeSpawnAdapter {
|
|
5
|
-
resolveCommand(profile: TransparentTerminalLaunchProfile): Promise<{
|
|
6
|
-
readonly executable: string;
|
|
7
|
-
readonly arguments: readonly string[];
|
|
8
|
-
}>;
|
|
9
|
-
spawn(executable: string, arguments_: readonly string[], options: SpawnOptions): ChildProcess;
|
|
10
|
-
observeStartIdentity(child: ChildProcess): Promise<string>;
|
|
11
|
-
identityMatches(child: ChildProcess, expectedStartIdentity: string): Promise<boolean>;
|
|
12
|
-
stop(child: ChildProcess, force: boolean): Promise<void>;
|
|
13
|
-
waitForExit(child: ChildProcess, timeoutMs: number): Promise<boolean>;
|
|
14
|
-
}
|
|
15
|
-
export declare function createPlatformTransparentLauncher(platform?: NodeJS.Platform, adapter?: NativeSpawnAdapter): TransparentNativeLauncher;
|
|
16
|
-
export declare class WindowsTransparentLauncher implements TransparentNativeLauncher {
|
|
17
|
-
private readonly adapter;
|
|
18
|
-
constructor(adapter?: NativeSpawnAdapter);
|
|
19
|
-
launch(profile: TransparentTerminalLaunchProfile): Promise<TransparentChildHandle>;
|
|
20
|
-
}
|
|
21
|
-
export declare class UnixTransparentLauncher implements TransparentNativeLauncher {
|
|
22
|
-
private readonly adapter;
|
|
23
|
-
constructor(adapter?: NativeSpawnAdapter);
|
|
24
|
-
launch(profile: TransparentTerminalLaunchProfile): Promise<TransparentChildHandle>;
|
|
25
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { resolveTransparentCommand } from "./command-resolution.js";
|
|
4
|
-
export function createPlatformTransparentLauncher(platform = process.platform, adapter = defaultSpawnAdapter) {
|
|
5
|
-
if (platform === "win32")
|
|
6
|
-
return new WindowsTransparentLauncher(adapter);
|
|
7
|
-
if (platform === "linux" || platform === "darwin")
|
|
8
|
-
return new UnixTransparentLauncher(adapter);
|
|
9
|
-
throw new Error(`transparent terminal attachment is unsupported on ${platform}`);
|
|
10
|
-
}
|
|
11
|
-
export class WindowsTransparentLauncher {
|
|
12
|
-
adapter;
|
|
13
|
-
constructor(adapter = defaultSpawnAdapter) {
|
|
14
|
-
this.adapter = adapter;
|
|
15
|
-
}
|
|
16
|
-
async launch(profile) {
|
|
17
|
-
return await launchInherited(profile, this.adapter, {
|
|
18
|
-
windowsHide: true,
|
|
19
|
-
windowsVerbatimArguments: false,
|
|
20
|
-
detached: false,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export class UnixTransparentLauncher {
|
|
25
|
-
adapter;
|
|
26
|
-
constructor(adapter = defaultSpawnAdapter) {
|
|
27
|
-
this.adapter = adapter;
|
|
28
|
-
}
|
|
29
|
-
async launch(profile) {
|
|
30
|
-
// Staying in the foreground process group preserves the inherited controlling
|
|
31
|
-
// terminal without an intermediary or synthetic terminal restoration.
|
|
32
|
-
return await launchInherited(profile, this.adapter, {
|
|
33
|
-
detached: false,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
async function launchInherited(profile, adapter, platformOptions) {
|
|
38
|
-
const command = await adapter.resolveCommand(profile);
|
|
39
|
-
const child = adapter.spawn(command.executable, command.arguments, {
|
|
40
|
-
...platformOptions,
|
|
41
|
-
cwd: profile.cwd,
|
|
42
|
-
env: { ...profile.environment, TERM: profile.terminalType },
|
|
43
|
-
shell: false,
|
|
44
|
-
stdio: "inherit",
|
|
45
|
-
});
|
|
46
|
-
await spawned(child);
|
|
47
|
-
if (!child.pid)
|
|
48
|
-
throw Object.assign(new Error("transparent child has no process identity"), { code: "NO_PROCESS_ID" });
|
|
49
|
-
const outcome = childOutcome(child);
|
|
50
|
-
const startIdentity = await adapter.observeStartIdentity(child);
|
|
51
|
-
const processIdentity = { pid: child.pid, startIdentity };
|
|
52
|
-
let stopOutcome = null;
|
|
53
|
-
return {
|
|
54
|
-
processIdentity,
|
|
55
|
-
outcome,
|
|
56
|
-
stop(reason) {
|
|
57
|
-
stopOutcome ??= stopOwnedChild(child, adapter, processIdentity.startIdentity, reason);
|
|
58
|
-
return stopOutcome;
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
function spawned(child) {
|
|
63
|
-
return new Promise((resolve, reject) => {
|
|
64
|
-
if (child.pid) {
|
|
65
|
-
resolve();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
child.once("spawn", resolve);
|
|
69
|
-
child.once("error", reject);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
function childOutcome(child) {
|
|
73
|
-
return new Promise(resolve => {
|
|
74
|
-
child.once("error", error => resolve({ kind: "broker-error", message: error.message, code: errorCode(error) }));
|
|
75
|
-
child.once("close", (exitCode, signal) => {
|
|
76
|
-
if (signal)
|
|
77
|
-
resolve({ kind: "signaled", signal });
|
|
78
|
-
else
|
|
79
|
-
resolve({ kind: "exited", exitCode: exitCode ?? 1 });
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
async function stopOwnedChild(child, adapter, expectedStartIdentity, reason) {
|
|
84
|
-
if (!await adapter.identityMatches(child, expectedStartIdentity)) {
|
|
85
|
-
return { kind: "broker-error", message: "refusing cleanup because transparent child ownership changed", code: "PROCESS_IDENTITY_MISMATCH" };
|
|
86
|
-
}
|
|
87
|
-
await adapter.stop(child, false);
|
|
88
|
-
if (!await adapter.waitForExit(child, 1_500)) {
|
|
89
|
-
if (!await adapter.identityMatches(child, expectedStartIdentity)) {
|
|
90
|
-
return { kind: "broker-error", message: "refusing forced cleanup because transparent child ownership changed", code: "PROCESS_IDENTITY_MISMATCH" };
|
|
91
|
-
}
|
|
92
|
-
await adapter.stop(child, true);
|
|
93
|
-
if (!await adapter.waitForExit(child, 1_500)) {
|
|
94
|
-
return { kind: "broker-error", message: "transparent child did not exit within the bounded cleanup deadline", code: "CLEANUP_TIMEOUT" };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return { kind: "stopped", reason };
|
|
98
|
-
}
|
|
99
|
-
function errorCode(error) {
|
|
100
|
-
return "code" in error && typeof error.code === "string" ? error.code : null;
|
|
101
|
-
}
|
|
102
|
-
const observedStartIdentities = new WeakMap();
|
|
103
|
-
const defaultSpawnAdapter = {
|
|
104
|
-
async resolveCommand(profile) {
|
|
105
|
-
return await resolveTransparentCommand(profile.executable, profile.arguments, {
|
|
106
|
-
cwd: profile.cwd,
|
|
107
|
-
environment: profile.environment,
|
|
108
|
-
});
|
|
109
|
-
},
|
|
110
|
-
spawn(executable, arguments_, options) {
|
|
111
|
-
return spawn(executable, [...arguments_], options);
|
|
112
|
-
},
|
|
113
|
-
async observeStartIdentity(child) {
|
|
114
|
-
if (!child.pid)
|
|
115
|
-
throw new Error("transparent child has no PID");
|
|
116
|
-
const identity = `${child.pid}:${randomUUID()}`;
|
|
117
|
-
observedStartIdentities.set(child, identity);
|
|
118
|
-
return identity;
|
|
119
|
-
},
|
|
120
|
-
async identityMatches(child, expectedStartIdentity) {
|
|
121
|
-
return child.pid !== undefined
|
|
122
|
-
&& child.exitCode === null
|
|
123
|
-
&& child.signalCode === null
|
|
124
|
-
&& observedStartIdentities.get(child) === expectedStartIdentity;
|
|
125
|
-
},
|
|
126
|
-
async stop(child, force) {
|
|
127
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
128
|
-
return;
|
|
129
|
-
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
130
|
-
},
|
|
131
|
-
async waitForExit(child, timeoutMs) {
|
|
132
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
133
|
-
return true;
|
|
134
|
-
return await new Promise(resolve => {
|
|
135
|
-
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
136
|
-
timer.unref();
|
|
137
|
-
const finish = (exited) => {
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
child.off("close", onClose);
|
|
140
|
-
resolve(exited);
|
|
141
|
-
};
|
|
142
|
-
const onClose = () => finish(true);
|
|
143
|
-
child.once("close", onClose);
|
|
144
|
-
});
|
|
145
|
-
},
|
|
146
|
-
};
|
/package/bin/{a1.js → cli.js}
RENAMED
|
File without changes
|
|
File without changes
|