@timurproko/a1 0.1.8-dev.536 → 0.1.8-dev.546
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +4 -0
- package/bin/update-recovery.js +25 -3
- package/dist/app/session-shell/session-shell-root.js +15 -5
- package/dist/cli/dispatch.d.ts +5 -0
- package/dist/cli/dispatch.js +26 -0
- package/dist/cli/session-context.d.ts +9 -0
- package/dist/cli/session-context.js +29 -0
- package/dist/composition/owned-ui.js +6 -2
- package/dist/contracts/owned-ui/model.d.ts +5 -0
- package/dist/contracts/owned-ui/validation.js +6 -0
- package/dist/contracts/presentation/index.d.ts +13 -0
- package/dist/contracts/presentation/index.js +30 -0
- package/dist/features/launch/index.d.ts +2 -0
- package/dist/features/launch/index.js +1 -0
- package/dist/features/launch/session-context.d.ts +8 -0
- package/dist/features/launch/session-context.js +8 -0
- package/dist/features/owned-ui/project-trust-prompt.js +1 -1
- package/dist/features/owned-ui/reference-screen-app.js +7 -8
- package/dist/features/owned-ui/settings-app.js +9 -5
- package/dist/foundation/lifecycle/index.d.ts +2 -0
- package/dist/foundation/lifecycle/index.js +1 -0
- package/dist/foundation/lifecycle/session-repository-context.d.ts +18 -0
- package/dist/foundation/lifecycle/session-repository-context.js +173 -0
- package/dist/foundation/release/index.d.ts +2 -2
- package/dist/foundation/release/index.js +1 -1
- package/dist/foundation/release/update-recovery.d.ts +7 -1
- package/dist/foundation/release/update-recovery.js +39 -9
- package/dist/foundation/release/update.d.ts +7 -0
- package/dist/foundation/release/update.js +115 -24
- package/dist/integrations/pi/components/models-dialog.js +9 -9
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +27 -3
- package/dist/integrations/pi/components/shell-extension-ui.js +4 -2
- package/dist/integrations/pi/components/shell-footer-status.js +1 -1
- package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -2
- package/dist/integrations/pi/components/skills-command.d.ts +2 -0
- package/dist/integrations/pi/components/skills-command.js +11 -10
- package/dist/integrations/pi/components/skills-dialog.js +8 -3
- package/dist/integrations/pi/components/theme.d.ts +4 -0
- package/dist/integrations/pi/components/theme.js +10 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +4 -4
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +9 -10
- package/dist/integrations/pi/components/upstream/components/extension-input.d.ts +17 -0
- package/dist/integrations/pi/components/upstream/components/extension-input.js +72 -0
- package/dist/integrations/pi/components/upstream/components/extension-selector.d.ts +15 -0
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +92 -0
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +16 -15
- package/dist/integrations/pi/components/upstream/components/session-footer.js +25 -9
- package/dist/integrations/pi/components/upstream/components/session-selector.js +25 -16
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +6 -4
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +22 -12
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +3 -2
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +14 -12
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +11 -5
- package/dist/integrations/pi/engine/adapter.d.ts +2 -1
- package/dist/integrations/pi/engine/adapter.js +2 -0
- package/dist/integrations/pi/engine/message-values.js +1 -0
- package/dist/integrations/pi/engine/session-runtime.d.ts +11 -2
- package/dist/integrations/pi/engine/session-runtime.js +136 -33
- package/dist/integrations/pi/engine/transcript-projection.js +35 -0
- package/dist/integrations/pi/startup-public.manifest.json +2 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +2 -1
- package/dist/ui/components/dialog-panel.d.ts +2 -1
- package/dist/ui/components/dialog-panel.js +6 -1
- package/dist/ui/components/index.d.ts +2 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/shortcut-hints.d.ts +7 -0
- package/dist/ui/components/shortcut-hints.js +12 -0
- package/dist/ui/components/shortcuts.d.ts +2 -0
- package/dist/ui/components/shortcuts.js +5 -2
- package/dist/ui/components/transcript-viewport.d.ts +1 -1
- package/docs/ci-release-runbook.md +9 -7
- package/docs/openspec-archive-automation.md +6 -0
- package/docs/validation.md +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { open, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
6
|
+
import { resolveProductPaths } from "./paths.js";
|
|
7
|
+
const SCHEMA = "a1-session-repository-context-v1";
|
|
8
|
+
const MAX_RECORD_BYTES = 16 * 1024;
|
|
9
|
+
const MAX_HEADER_BYTES = 64 * 1024;
|
|
10
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
11
|
+
/** Persist one explicit same-repository worktree association for a stable Pi session. */
|
|
12
|
+
export async function setSessionRepositoryContext(identity, worktreePath, options = {}) {
|
|
13
|
+
const header = await readSessionHeader(identity);
|
|
14
|
+
const startup = await readGitWorktreeIdentity(header.cwd, false, options.signal);
|
|
15
|
+
const selected = await readGitWorktreeIdentity(worktreePath, true, options.signal);
|
|
16
|
+
if (!samePath(startup.commonDir, selected.commonDir)) {
|
|
17
|
+
throw new Error("associated worktree belongs to a different Git repository");
|
|
18
|
+
}
|
|
19
|
+
const record = {
|
|
20
|
+
schema: SCHEMA,
|
|
21
|
+
sessionId: header.id,
|
|
22
|
+
sessionFile: header.file,
|
|
23
|
+
startupRoot: startup.root,
|
|
24
|
+
startupCommonDir: startup.commonDir,
|
|
25
|
+
worktreeRoot: selected.root,
|
|
26
|
+
worktreeCommonDir: selected.commonDir,
|
|
27
|
+
worktreeGitDir: selected.gitDir,
|
|
28
|
+
updatedAt: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
const destination = recordPath(header, options);
|
|
31
|
+
const directory = dirname(destination);
|
|
32
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
33
|
+
const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
34
|
+
try {
|
|
35
|
+
await writeFile(temporary, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
36
|
+
await rename(temporary, destination);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
return { cwd: selected.root, branch: requireBranch(selected) };
|
|
42
|
+
}
|
|
43
|
+
/** Remove only the association belonging to the supplied stable session. */
|
|
44
|
+
export async function clearSessionRepositoryContext(identity, options = {}) {
|
|
45
|
+
const header = await readSessionHeader(identity);
|
|
46
|
+
await rm(recordPath(header, options), { force: true });
|
|
47
|
+
}
|
|
48
|
+
/** Read and revalidate a persisted association; malformed or stale state fails closed to absence. */
|
|
49
|
+
export async function readSessionRepositoryContext(identity, options = {}) {
|
|
50
|
+
let header;
|
|
51
|
+
try {
|
|
52
|
+
header = await readSessionHeader(identity);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const path = recordPath(header, options);
|
|
59
|
+
const metadata = await lstat(path);
|
|
60
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size === 0 || metadata.size > MAX_RECORD_BYTES)
|
|
61
|
+
return null;
|
|
62
|
+
const bytes = await readFile(path, { signal: options.signal });
|
|
63
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
64
|
+
if (!isRecord(value)
|
|
65
|
+
|| value.schema !== SCHEMA
|
|
66
|
+
|| value.sessionId !== header.id
|
|
67
|
+
|| value.sessionFile !== header.file
|
|
68
|
+
|| !validPath(value.startupRoot)
|
|
69
|
+
|| !validPath(value.startupCommonDir)
|
|
70
|
+
|| !validPath(value.worktreeRoot)
|
|
71
|
+
|| !validPath(value.worktreeCommonDir)
|
|
72
|
+
|| !validPath(value.worktreeGitDir)
|
|
73
|
+
|| typeof value.updatedAt !== "string")
|
|
74
|
+
return null;
|
|
75
|
+
const startup = await readGitWorktreeIdentity(header.cwd, false, options.signal);
|
|
76
|
+
const selected = await readGitWorktreeIdentity(value.worktreeRoot, true, options.signal);
|
|
77
|
+
if (!samePath(startup.root, value.startupRoot)
|
|
78
|
+
|| !samePath(startup.commonDir, value.startupCommonDir)
|
|
79
|
+
|| !samePath(selected.root, value.worktreeRoot)
|
|
80
|
+
|| !samePath(selected.commonDir, value.worktreeCommonDir)
|
|
81
|
+
|| !samePath(selected.gitDir, value.worktreeGitDir)
|
|
82
|
+
|| !samePath(startup.commonDir, selected.commonDir))
|
|
83
|
+
return null;
|
|
84
|
+
return { cwd: selected.root, branch: requireBranch(selected) };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readSessionHeader(identity) {
|
|
91
|
+
if (!validSessionId(identity.sessionId) || !validPath(identity.sessionFile))
|
|
92
|
+
throw new Error("active session identity is unavailable");
|
|
93
|
+
const file = await realpath(resolve(identity.sessionFile));
|
|
94
|
+
const handle = await open(file, "r");
|
|
95
|
+
try {
|
|
96
|
+
const buffer = Buffer.alloc(MAX_HEADER_BYTES);
|
|
97
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0);
|
|
98
|
+
const newline = buffer.subarray(0, bytesRead).indexOf(0x0a);
|
|
99
|
+
if (newline < 0)
|
|
100
|
+
throw new Error("session header is unavailable");
|
|
101
|
+
const value = JSON.parse(buffer.subarray(0, newline).toString("utf8"));
|
|
102
|
+
if (!isRecord(value) || value.type !== "session" || value.id !== identity.sessionId || !validPath(value.cwd)) {
|
|
103
|
+
throw new Error("active session identity does not match its session file");
|
|
104
|
+
}
|
|
105
|
+
return { id: identity.sessionId, cwd: await realpath(resolve(value.cwd)), file };
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await handle.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function readGitWorktreeIdentity(path, requireWorktreeRoot, signal) {
|
|
112
|
+
if (!validPath(path))
|
|
113
|
+
throw new Error("associated worktree path is invalid");
|
|
114
|
+
const cwd = await realpath(resolve(path));
|
|
115
|
+
const { stdout } = await executeGit(["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir", "--git-dir"], cwd, MAX_RECORD_BYTES, signal);
|
|
116
|
+
const lines = stdout.split(/\r?\n/u).filter(Boolean);
|
|
117
|
+
if (lines.length !== 3)
|
|
118
|
+
throw new Error("associated worktree identity is invalid");
|
|
119
|
+
const [rootValue, commonValue, gitValue] = lines;
|
|
120
|
+
if (!rootValue || !commonValue || !gitValue)
|
|
121
|
+
throw new Error("associated worktree identity is incomplete");
|
|
122
|
+
const root = await realpath(rootValue);
|
|
123
|
+
if (requireWorktreeRoot && !samePath(root, cwd))
|
|
124
|
+
throw new Error("associated path must be a Git worktree root");
|
|
125
|
+
const commonDir = await realpath(commonValue);
|
|
126
|
+
const gitDir = await realpath(gitValue);
|
|
127
|
+
let branch = null;
|
|
128
|
+
if (requireWorktreeRoot) {
|
|
129
|
+
const branchResult = await executeGit(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd, 4_096, signal);
|
|
130
|
+
branch = branchResult.stdout.trim();
|
|
131
|
+
if (branch.length === 0 || branch.length > 256)
|
|
132
|
+
throw new Error("associated worktree has a detached or invalid branch");
|
|
133
|
+
}
|
|
134
|
+
return { root, commonDir, gitDir, branch };
|
|
135
|
+
}
|
|
136
|
+
async function executeGit(arguments_, cwd, maxBuffer, signal) {
|
|
137
|
+
const { execFile } = await import("node:child_process");
|
|
138
|
+
const execute = promisify(execFile);
|
|
139
|
+
return await execute("git", [...arguments_], {
|
|
140
|
+
cwd,
|
|
141
|
+
windowsHide: true,
|
|
142
|
+
timeout: GIT_TIMEOUT_MS,
|
|
143
|
+
maxBuffer,
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
signal,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function requireBranch(identity) {
|
|
149
|
+
if (identity.branch === null)
|
|
150
|
+
throw new Error("associated worktree branch is unavailable");
|
|
151
|
+
return identity.branch;
|
|
152
|
+
}
|
|
153
|
+
function recordPath(header, options) {
|
|
154
|
+
const dataDir = options.dataDir ?? resolveProductPaths().dataDir;
|
|
155
|
+
const digest = createHash("sha256").update(header.id).update("\0").update(pathKey(header.file)).digest("hex");
|
|
156
|
+
return join(dataDir, PRODUCT_IDENTITY.state.sessionRepositoryDirectory, `${digest}.json`);
|
|
157
|
+
}
|
|
158
|
+
function samePath(left, right) {
|
|
159
|
+
return pathKey(left) === pathKey(right);
|
|
160
|
+
}
|
|
161
|
+
function pathKey(value) {
|
|
162
|
+
const normalized = resolve(value);
|
|
163
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
164
|
+
}
|
|
165
|
+
function validSessionId(value) {
|
|
166
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(value);
|
|
167
|
+
}
|
|
168
|
+
function validPath(value) {
|
|
169
|
+
return typeof value === "string" && value.length > 0 && value.length <= 32_768 && !value.includes("\0") && isAbsolute(value);
|
|
170
|
+
}
|
|
171
|
+
function isRecord(value) {
|
|
172
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
|
+
}
|
|
@@ -21,11 +21,11 @@ export type { RestartSeal, RestartValidationEvent } from "./restart-certificatio
|
|
|
21
21
|
export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
|
|
22
22
|
export type { StableRegistryState, StableRegistryVerificationOptions, StableReleaseEvidence, StableReleaseEvidenceInput } from "./stable-release.js";
|
|
23
23
|
export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
|
|
24
|
-
export type { ProcessRequest, ProcessResult, SelfUpdateOptions, UpdateActivationPhase, UpdateChannel, UpdateFileSystem, UpdateLifecycleCoordinator, UpdateMaterializationProgress, UpdateMeasuredPhase, UpdateOutput, UpdateOwnershipAction, UpdatePackageReplacementInput, UpdatePerformanceEvidence, UpdatePhaseTimingEvent, UpdateProcessRunner, UpdateTransactionJournal, } from "./update.js";
|
|
24
|
+
export type { ProcessRequest, ProcessResult, SelfUpdateOptions, UpdateActivationPhase, UpdateChannel, UpdateFileMetadata, UpdateFileSystem, UpdateLifecycleCoordinator, UpdateMaterializationProgress, UpdateMeasuredPhase, UpdateOutput, UpdateOwnershipAction, UpdatePackageReplacementInput, UpdatePerformanceEvidence, UpdatePhaseTimingEvent, UpdateProcessRunner, UpdateTransactionJournal, } from "./update.js";
|
|
25
25
|
export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
|
|
26
26
|
export type { UpdateActivationCallbacks, UpdateActivationEvent, UpdateActivationRequest } from "./update-activation.js";
|
|
27
27
|
export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
|
|
28
|
-
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
|
|
28
|
+
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, npmPrefixForGlobalRoot, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
|
|
29
29
|
export type { ProtectedPackageReplacementOptions, ProtectedPackageReplacementResult, UpdateRecoveryCapsule, UpdateRecoveryResult } from "./update-recovery.js";
|
|
30
30
|
export { UPDATE_JOURNAL_SCHEMA, UpdateTransactionStore } from "./update-transaction.js";
|
|
31
31
|
export type { UpdateRecoveryState, UpdateTransaction, UpdateTransactionPhase } from "./update-transaction.js";
|
|
@@ -12,6 +12,6 @@ export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistr
|
|
|
12
12
|
export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
|
|
13
13
|
export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
|
|
14
14
|
export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
|
|
15
|
-
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
|
|
15
|
+
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, npmPrefixForGlobalRoot, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
|
|
16
16
|
export { UPDATE_JOURNAL_SCHEMA, UpdateTransactionStore } from "./update-transaction.js";
|
|
17
17
|
export { warmMaterializedRelease } from "./warmup.js";
|
|
@@ -39,6 +39,8 @@ export interface UpdateRecoveryResult {
|
|
|
39
39
|
export interface ProtectedPackageReplacementOptions {
|
|
40
40
|
readonly dataDir: string;
|
|
41
41
|
readonly globalRoot: string;
|
|
42
|
+
/** Root used only to locate the active npm implementation; installation remains pinned to globalRoot. */
|
|
43
|
+
readonly npmCliRoot?: string;
|
|
42
44
|
readonly packageRoot: string;
|
|
43
45
|
readonly transaction: UpdateTransaction;
|
|
44
46
|
readonly priorRelease: {
|
|
@@ -58,7 +60,11 @@ export interface ProtectedPackageReplacementOptions {
|
|
|
58
60
|
export interface ProtectedPackageReplacementResult extends UpdateRecoveryResult {
|
|
59
61
|
readonly recovery: UpdateRecoveryState;
|
|
60
62
|
}
|
|
61
|
-
/**
|
|
63
|
+
/** Derive the only npm prefix that can own a platform's canonical global package root. */
|
|
64
|
+
export declare function npmPrefixForGlobalRoot(globalRoot: string, platform?: NodeJS.Platform): string;
|
|
65
|
+
/** Fixed replacement arguments pin npm to the installation that owns the invoked launcher. */
|
|
66
|
+
export declare function updateNpmInstallArguments(globalRoot: string, targetVersion: string, platform?: NodeJS.Platform): readonly string[];
|
|
67
|
+
/** Resolve the complete public launcher set npm owns for the selected platform and prefix. */
|
|
62
68
|
export declare function updateLauncherPaths(globalRoot: string, platform?: NodeJS.Platform): readonly string[];
|
|
63
69
|
/** Validate transaction-scoped recovery authority without trusting npm temporary names. */
|
|
64
70
|
export declare function readUpdateRecoveryCapsule(manifestPath: string, platform?: NodeJS.Platform): Promise<UpdateRecoveryCapsule>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
7
7
|
import { processIsAlive } from "./process-cleanup.js";
|
|
@@ -9,9 +9,29 @@ import { PRIVATE_LAUNCH_CONTRACT, assertCurrentLaunchContract } from "../launch-
|
|
|
9
9
|
export const UPDATE_RECOVERY_SCHEMA = "a1-update-recovery-v1";
|
|
10
10
|
const WORKER_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
11
11
|
const POLL_MS = 50;
|
|
12
|
-
/**
|
|
12
|
+
/** Derive the only npm prefix that can own a platform's canonical global package root. */
|
|
13
|
+
export function npmPrefixForGlobalRoot(globalRoot, platform = process.platform) {
|
|
14
|
+
const root = resolve(globalRoot);
|
|
15
|
+
if (!sameSegment(basename(root), "node_modules", platform))
|
|
16
|
+
throw new Error(`npm global root has an unsupported layout: ${globalRoot}`);
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
return dirname(root);
|
|
19
|
+
const libraryRoot = dirname(root);
|
|
20
|
+
if (!sameSegment(basename(libraryRoot), "lib", platform))
|
|
21
|
+
throw new Error(`npm global root has an unsupported layout: ${globalRoot}`);
|
|
22
|
+
return dirname(libraryRoot);
|
|
23
|
+
}
|
|
24
|
+
/** Fixed replacement arguments pin npm to the installation that owns the invoked launcher. */
|
|
25
|
+
export function updateNpmInstallArguments(globalRoot, targetVersion, platform = process.platform) {
|
|
26
|
+
return [
|
|
27
|
+
"install", "--global", "--prefix", npmPrefixForGlobalRoot(globalRoot, platform),
|
|
28
|
+
"--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_TEXT.packageName}@${targetVersion}`,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
/** Resolve the complete public launcher set npm owns for the selected platform and prefix. */
|
|
13
32
|
export function updateLauncherPaths(globalRoot, platform = process.platform) {
|
|
14
|
-
const
|
|
33
|
+
const prefix = npmPrefixForGlobalRoot(globalRoot, platform);
|
|
34
|
+
const launcherRoot = platform === "win32" ? prefix : resolve(prefix, "bin");
|
|
15
35
|
return platform === "win32"
|
|
16
36
|
? [resolve(launcherRoot, "a1"), resolve(launcherRoot, "a1.cmd"), resolve(launcherRoot, "a1.ps1")]
|
|
17
37
|
: [resolve(launcherRoot, "a1")];
|
|
@@ -33,9 +53,11 @@ export async function readUpdateRecoveryCapsule(manifestPath, platform = process
|
|
|
33
53
|
const expectedPackage = resolve(canonicalGlobal, ...capsule.packageName.split("/"));
|
|
34
54
|
if (!samePath(canonicalPackage, expectedPackage) || !containedBy(canonicalGlobal, canonicalPackage))
|
|
35
55
|
throw new Error("recovery package root is outside npm global root");
|
|
36
|
-
const expectedNpmArguments =
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
const expectedNpmArguments = updateNpmInstallArguments(canonicalGlobal, capsule.targetVersion, platform);
|
|
57
|
+
const legacyNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${capsule.packageName}@${capsule.targetVersion}`];
|
|
58
|
+
if (![expectedNpmArguments, legacyNpmArguments].some(arguments_ => sameArguments(capsule.npmArguments, arguments_))) {
|
|
59
|
+
throw new Error("recovery npm arguments differ from the selected target and prefix");
|
|
60
|
+
}
|
|
39
61
|
const expectedLaunchers = updateLauncherPaths(canonicalGlobal, platform);
|
|
40
62
|
if (JSON.stringify(capsule.launchers.map(path => resolve(path))) !== JSON.stringify(expectedLaunchers.map(path => resolve(path)))) {
|
|
41
63
|
throw new Error("recovery launcher set differs from the canonical npm launcher set");
|
|
@@ -94,8 +116,10 @@ export async function prepareUpdateRecoveryCapsule(options) {
|
|
|
94
116
|
await chmod(candidateEntry, 0o500);
|
|
95
117
|
const canonicalGlobal = await realpath(options.globalRoot);
|
|
96
118
|
const platform = options.platform ?? process.platform;
|
|
97
|
-
const
|
|
98
|
-
const
|
|
119
|
+
const prefix = npmPrefixForGlobalRoot(canonicalGlobal, platform);
|
|
120
|
+
const launcherRoot = platform === "win32" ? prefix : resolve(prefix, "bin");
|
|
121
|
+
const npmCliRoot = await realpath(options.npmCliRoot ?? canonicalGlobal);
|
|
122
|
+
const npmCli = await resolveNpmCli(npmCliRoot, options.environment ?? process.env);
|
|
99
123
|
const capsule = {
|
|
100
124
|
schema: UPDATE_RECOVERY_SCHEMA,
|
|
101
125
|
launchContract: PRIVATE_LAUNCH_CONTRACT,
|
|
@@ -114,7 +138,7 @@ export async function prepareUpdateRecoveryCapsule(options) {
|
|
|
114
138
|
recoveryEntryDigest: createHash("sha256").update(entryBytes).digest("hex"),
|
|
115
139
|
nodeExecutable: process.execPath,
|
|
116
140
|
npmCli,
|
|
117
|
-
npmArguments:
|
|
141
|
+
npmArguments: updateNpmInstallArguments(canonicalGlobal, options.transaction.targetVersion, platform),
|
|
118
142
|
cancellationPath: resolve(finalRoot, "cancel.json"),
|
|
119
143
|
resultPath: resolve(finalRoot, "result.json"),
|
|
120
144
|
ownerPath: resolve(finalRoot, "owner.json"),
|
|
@@ -341,6 +365,12 @@ async function readJson(path) { return JSON.parse(await readFile(path, "utf8"));
|
|
|
341
365
|
function samePath(left, right) {
|
|
342
366
|
return process.platform === "win32" ? resolve(left).toLowerCase() === resolve(right).toLowerCase() : resolve(left) === resolve(right);
|
|
343
367
|
}
|
|
368
|
+
function sameSegment(left, right, platform) {
|
|
369
|
+
return platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
370
|
+
}
|
|
371
|
+
function sameArguments(left, right) {
|
|
372
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
373
|
+
}
|
|
344
374
|
function containedBy(parent, child) {
|
|
345
375
|
const fromParent = relative(parent, child);
|
|
346
376
|
return fromParent.length > 0 && fromParent !== ".." && !fromParent.startsWith(`..${sep}`) && !isAbsolute(fromParent);
|
|
@@ -13,9 +13,15 @@ export interface ProcessResult {
|
|
|
13
13
|
stderr?: string;
|
|
14
14
|
}
|
|
15
15
|
export type UpdateProcessRunner = (command: string, arguments_: readonly string[], request: ProcessRequest) => Promise<ProcessResult>;
|
|
16
|
+
export interface UpdateFileMetadata {
|
|
17
|
+
readonly mode: number;
|
|
18
|
+
isFile(): boolean;
|
|
19
|
+
isSymbolicLink(): boolean;
|
|
20
|
+
}
|
|
16
21
|
export interface UpdateFileSystem {
|
|
17
22
|
readFile(path: string): Promise<string>;
|
|
18
23
|
realpath(path: string): Promise<string>;
|
|
24
|
+
lstat?(path: string): Promise<UpdateFileMetadata>;
|
|
19
25
|
access?(path: string): Promise<void>;
|
|
20
26
|
}
|
|
21
27
|
export interface UpdateOutput {
|
|
@@ -85,6 +91,7 @@ export interface UpdateTransactionJournal {
|
|
|
85
91
|
export interface UpdatePackageReplacementInput {
|
|
86
92
|
readonly dataDir: string;
|
|
87
93
|
readonly globalRoot: string;
|
|
94
|
+
readonly npmCliRoot: string;
|
|
88
95
|
readonly packageRoot: string;
|
|
89
96
|
readonly transaction: UpdateTransaction;
|
|
90
97
|
readonly priorRelease: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { access, lstat, readFile, realpath } from "node:fs/promises";
|
|
3
3
|
import { connect } from "node:net";
|
|
4
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import crossSpawn from "cross-spawn";
|
|
6
6
|
import { valid as validSemver } from "semver";
|
|
7
7
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
@@ -15,12 +15,13 @@ import { materializeRelease, readMaterializedRelease } from "./release-store.js"
|
|
|
15
15
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
16
16
|
import { UPDATE_ACTIVATION_CONTRACT, activateInstalledRelease, delegateActivation, readActivationContracts, } from "./update-activation.js";
|
|
17
17
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
18
|
-
import { removeUpdateRecoveryCapsule, runProtectedPackageReplacement } from "./update-recovery.js";
|
|
18
|
+
import { npmPrefixForGlobalRoot, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
|
|
19
19
|
export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
|
|
20
20
|
const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
|
|
21
21
|
const defaultFileSystem = {
|
|
22
22
|
async readFile(path) { return await readFile(path, "utf8"); },
|
|
23
23
|
realpath,
|
|
24
|
+
lstat,
|
|
24
25
|
access,
|
|
25
26
|
};
|
|
26
27
|
const defaultOutput = {
|
|
@@ -308,6 +309,106 @@ async function resolveRequestedPreview(runner, requested, output) {
|
|
|
308
309
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`published no preview for ${requested}.`)}\n`);
|
|
309
310
|
return { version: null, exitCode: 1 };
|
|
310
311
|
}
|
|
312
|
+
async function resolveManagedNpmInstallation(requestedPackageRoot, fileSystem, runner, output) {
|
|
313
|
+
const rootLookup = await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root");
|
|
314
|
+
if (rootLookup.result === null)
|
|
315
|
+
return { installation: null, exitCode: rootLookup.exitCode };
|
|
316
|
+
const activeRootText = rootLookup.result.stdout.trim();
|
|
317
|
+
if (activeRootText.length === 0) {
|
|
318
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic("could not verify its installation because npm returned an empty global package root.")}\n`);
|
|
319
|
+
return { installation: null, exitCode: 1 };
|
|
320
|
+
}
|
|
321
|
+
let packageRoot;
|
|
322
|
+
let activeGlobalRoot;
|
|
323
|
+
try {
|
|
324
|
+
[packageRoot, activeGlobalRoot] = await Promise.all([fileSystem.realpath(requestedPackageRoot), fileSystem.realpath(activeRootText)]);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not canonicalize the running and global npm paths: ${errorMessage(error)}`)}\n`);
|
|
328
|
+
return { installation: null, exitCode: 1 };
|
|
329
|
+
}
|
|
330
|
+
const expectedActivePackage = resolve(activeGlobalRoot, ...PRODUCT_PACKAGE.split("/"));
|
|
331
|
+
if (samePath(packageRoot, expectedActivePackage)) {
|
|
332
|
+
try {
|
|
333
|
+
npmPrefixForGlobalRoot(activeGlobalRoot);
|
|
334
|
+
return {
|
|
335
|
+
installation: {
|
|
336
|
+
packageRoot,
|
|
337
|
+
globalRoot: activeGlobalRoot,
|
|
338
|
+
npmCliRoot: activeGlobalRoot,
|
|
339
|
+
},
|
|
340
|
+
exitCode: 0,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not verify npm's active global package layout: ${errorMessage(error)}`)}\n`);
|
|
345
|
+
return { installation: null, exitCode: 1 };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let candidateGlobalRoot = packageRoot;
|
|
349
|
+
for (const _segment of PRODUCT_PACKAGE.split("/"))
|
|
350
|
+
candidateGlobalRoot = dirname(candidateGlobalRoot);
|
|
351
|
+
let candidatePrefix;
|
|
352
|
+
try {
|
|
353
|
+
candidatePrefix = npmPrefixForGlobalRoot(candidateGlobalRoot);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot);
|
|
357
|
+
return { installation: null, exitCode: 1 };
|
|
358
|
+
}
|
|
359
|
+
if (!samePath(packageRoot, resolve(candidateGlobalRoot, ...PRODUCT_PACKAGE.split("/")))) {
|
|
360
|
+
reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot);
|
|
361
|
+
return { installation: null, exitCode: 1 };
|
|
362
|
+
}
|
|
363
|
+
const confirmation = await runNpm(runner, ["root", "--global", "--prefix", candidatePrefix], true, output, "confirm npm ownership of the running installation");
|
|
364
|
+
if (confirmation.result === null)
|
|
365
|
+
return { installation: null, exitCode: confirmation.exitCode };
|
|
366
|
+
const confirmedRootText = confirmation.result.stdout.trim();
|
|
367
|
+
if (confirmedRootText.length === 0) {
|
|
368
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic("could not verify its installation because npm returned an empty global package root for the inferred prefix.")}\n`);
|
|
369
|
+
return { installation: null, exitCode: 1 };
|
|
370
|
+
}
|
|
371
|
+
let confirmedRoot;
|
|
372
|
+
try {
|
|
373
|
+
confirmedRoot = await fileSystem.realpath(confirmedRootText);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not canonicalize npm's confirmed global package root: ${errorMessage(error)}`)}\n`);
|
|
377
|
+
return { installation: null, exitCode: 1 };
|
|
378
|
+
}
|
|
379
|
+
if (!samePath(confirmedRoot, candidateGlobalRoot)
|
|
380
|
+
|| !await launchersOwnPackage(candidateGlobalRoot, fileSystem)) {
|
|
381
|
+
reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot);
|
|
382
|
+
return { installation: null, exitCode: 1 };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
installation: {
|
|
386
|
+
packageRoot,
|
|
387
|
+
globalRoot: candidateGlobalRoot,
|
|
388
|
+
npmCliRoot: activeGlobalRoot,
|
|
389
|
+
},
|
|
390
|
+
exitCode: 0,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async function launchersOwnPackage(globalRoot, fileSystem) {
|
|
394
|
+
const token = `node_modules/${PRODUCT_PACKAGE}/bin/cli.js`;
|
|
395
|
+
try {
|
|
396
|
+
for (const launcher of updateLauncherPaths(globalRoot)) {
|
|
397
|
+
const metadata = await (fileSystem.lstat ?? lstat)(launcher);
|
|
398
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || (process.platform !== "win32" && (metadata.mode & 0o111) === 0))
|
|
399
|
+
return false;
|
|
400
|
+
if (!(await fileSystem.readFile(launcher)).replaceAll("\\", "/").includes(token))
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot) {
|
|
410
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`refused to update automatically because ${packageRoot} could not be verified as an npm-managed ${PRODUCT_PACKAGE} installation; npm's active global package root is ${activeGlobalRoot}. Run npm install --global ${PRODUCT_PACKAGE} to install it manually.`)}\n`);
|
|
411
|
+
}
|
|
311
412
|
export async function runSelfUpdate(options) {
|
|
312
413
|
const fileSystem = options.fileSystem ?? defaultFileSystem;
|
|
313
414
|
const output = options.output ?? defaultOutput;
|
|
@@ -328,6 +429,8 @@ export async function runSelfUpdate(options) {
|
|
|
328
429
|
try {
|
|
329
430
|
const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
|
|
330
431
|
const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
|
|
432
|
+
if (packageJson.name !== PRODUCT_PACKAGE)
|
|
433
|
+
throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
|
|
331
434
|
if (parsedVersion === null)
|
|
332
435
|
throw new Error("package.json does not contain a valid semantic version");
|
|
333
436
|
runningVersion = parsedVersion;
|
|
@@ -347,26 +450,10 @@ export async function runSelfUpdate(options) {
|
|
|
347
450
|
// and a trailing one reads as part of the version rather than as punctuation.
|
|
348
451
|
output.stdout(`${PRODUCT_TEXT.commandName} update: ${runningVersion} → ${targetVersion}\n`);
|
|
349
452
|
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic("could not verify its installation because npm returned an empty global package root.")}\n`);
|
|
355
|
-
return 1;
|
|
356
|
-
}
|
|
357
|
-
let packageRoot;
|
|
358
|
-
let globalRoot;
|
|
359
|
-
try {
|
|
360
|
-
[packageRoot, globalRoot] = await Promise.all([fileSystem.realpath(options.packageRoot), fileSystem.realpath(rootLookup.result.stdout.trim())]);
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not canonicalize the running and global npm paths: ${errorMessage(error)}`)}\n`);
|
|
364
|
-
return 1;
|
|
365
|
-
}
|
|
366
|
-
if (!isContainedBy(globalRoot, packageRoot)) {
|
|
367
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`refused to update automatically because ${packageRoot} is not managed beneath npm's global package root ${globalRoot}.`)}\n`);
|
|
368
|
-
return 1;
|
|
369
|
-
}
|
|
453
|
+
const managed = await measure("global-root", async () => await resolveManagedNpmInstallation(options.packageRoot, fileSystem, runner, output));
|
|
454
|
+
if (managed.installation === null)
|
|
455
|
+
return managed.exitCode;
|
|
456
|
+
const { packageRoot, globalRoot, npmCliRoot } = managed.installation;
|
|
370
457
|
const environment = options.environment ?? process.env;
|
|
371
458
|
const paths = resolveProductPaths(environment);
|
|
372
459
|
const lifecycle = options.lifecycle ?? createUpdateLifecycleCoordinator(environment, fileSystem, output);
|
|
@@ -427,6 +514,7 @@ export async function runSelfUpdate(options) {
|
|
|
427
514
|
const replacement = await measure("npm-install", async () => await (options.packageReplacement ?? runProtectedPackageReplacement)({
|
|
428
515
|
dataDir: paths.dataDir,
|
|
429
516
|
globalRoot,
|
|
517
|
+
npmCliRoot,
|
|
430
518
|
packageRoot,
|
|
431
519
|
transaction: replacementTransaction,
|
|
432
520
|
priorRelease,
|
|
@@ -453,7 +541,7 @@ export async function runSelfUpdate(options) {
|
|
|
453
541
|
}
|
|
454
542
|
}
|
|
455
543
|
else {
|
|
456
|
-
const installation = await measure("npm-install", async () => await runNpm(runner,
|
|
544
|
+
const installation = await measure("npm-install", async () => await runNpm(runner, updateNpmInstallArguments(globalRoot, targetVersion), true, output, "start the global npm installation", false));
|
|
457
545
|
if (installation.result === null)
|
|
458
546
|
throw new UpdateFailure(installation.exitCode, "npm process failed");
|
|
459
547
|
if (installation.result.code !== 0) {
|
|
@@ -624,6 +712,9 @@ async function canonicalImmutableRoot(dataDir, releaseRoot) {
|
|
|
624
712
|
return false;
|
|
625
713
|
}
|
|
626
714
|
}
|
|
715
|
+
function samePath(left, right) {
|
|
716
|
+
return process.platform === "win32" ? resolve(left).toLowerCase() === resolve(right).toLowerCase() : resolve(left) === resolve(right);
|
|
717
|
+
}
|
|
627
718
|
function isContainedBy(parent, child) {
|
|
628
719
|
const pathFromParent = relative(parent, child);
|
|
629
720
|
return pathFromParent.length > 0 && pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Input, Key, getKeybindings, matchesKey, truncateToWidth, } from "@earendil-works/pi-tui";
|
|
2
|
-
import { piTheme } from "./theme.js";
|
|
2
|
+
import { piTheme, renderPiModalShortcutHints } from "./theme.js";
|
|
3
3
|
const MAX_VISIBLE_ROWS = 10;
|
|
4
4
|
const MODELS_TITLE = "Models";
|
|
5
5
|
function fullModelId(model) {
|
|
@@ -228,7 +228,7 @@ export class ModelsDialogComponent {
|
|
|
228
228
|
push();
|
|
229
229
|
if (this.#refreshStatus !== undefined)
|
|
230
230
|
push(theme.fg(this.#refreshStatus.kind, ` ${this.#refreshStatus.message}`));
|
|
231
|
-
push(
|
|
231
|
+
push(renderPiModalShortcutHints(this.#hints(), 2));
|
|
232
232
|
push(border);
|
|
233
233
|
return lines;
|
|
234
234
|
}
|
|
@@ -237,13 +237,13 @@ export class ModelsDialogComponent {
|
|
|
237
237
|
const confirm = keyLabel("tui.select.confirm");
|
|
238
238
|
const save = keyLabel("app.models.save");
|
|
239
239
|
return [
|
|
240
|
-
"type to search",
|
|
241
|
-
"↑↓ navigate",
|
|
242
|
-
...(tab.length === 0 ? [] : [
|
|
243
|
-
...(confirm.length === 0 ? [] : [
|
|
244
|
-
"space scope",
|
|
245
|
-
...(save.length === 0 ? [] : [
|
|
246
|
-
"esc close",
|
|
240
|
+
{ action: "type to search" },
|
|
241
|
+
{ key: "↑↓", action: "navigate" },
|
|
242
|
+
...(tab.length === 0 ? [] : [{ key: tab, action: "filter" }]),
|
|
243
|
+
...(confirm.length === 0 ? [] : [{ key: confirm, action: "switch" }]),
|
|
244
|
+
{ key: "space", action: "scope" },
|
|
245
|
+
...(save.length === 0 ? [] : [{ key: save, action: "save" }]),
|
|
246
|
+
{ key: "esc", action: "close" },
|
|
247
247
|
];
|
|
248
248
|
}
|
|
249
249
|
#emptyText() {
|
|
@@ -132,9 +132,17 @@ export function createPiShellEditor(options) {
|
|
|
132
132
|
const commands = collapsed.commands;
|
|
133
133
|
const additions = new Map(commands.map(command => [command.name, command]));
|
|
134
134
|
const builtInNames = new Set(builtInCatalog.map(command => command.name));
|
|
135
|
-
const builtIns = builtInCatalog.
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
const builtIns = builtInCatalog.flatMap(command => [
|
|
136
|
+
autocompleteCommand(command, additions.get(command.name)),
|
|
137
|
+
// Rationale: the collapsed browser is a primary bare-A1 command, not a resource after quit.
|
|
138
|
+
...(command.name === "settings" && collapsed.collapsedCommand !== undefined
|
|
139
|
+
? [autocompleteCommand(collapsed.collapsedCommand)]
|
|
140
|
+
: []),
|
|
141
|
+
]);
|
|
142
|
+
// Invariant: built-in additions and the promoted collapsed browser never reappear as resources.
|
|
143
|
+
const resources = commands
|
|
144
|
+
.filter(command => command.name !== collapsed.collapsedCommand?.name && command.source !== "builtin" && !builtInNames.has(command.name))
|
|
145
|
+
.map(command => autocompleteCommand(command));
|
|
138
146
|
const combined = new CombinedAutocompleteProvider([...builtIns, ...resources], options.cwd ?? process.cwd());
|
|
139
147
|
autocompleteProvider = tunnelSkills.length === 0 ? combined : createSkillsTunnelProvider(combined, tunnelSkills);
|
|
140
148
|
editor.setAutocompleteProvider(autocompleteProvider);
|
|
@@ -229,10 +237,26 @@ export function createPiShellEditor(options) {
|
|
|
229
237
|
if (editorUx !== undefined && matchesKey(data, "shift+tab")
|
|
230
238
|
&& !Object.values(keybindings.getEffectiveConfig()).some(keys => (Array.isArray(keys) ? keys : [keys]).some(key => key !== undefined && matchesKey(data, key))))
|
|
231
239
|
return;
|
|
240
|
+
const hadDraft = editor.getText().length > 0;
|
|
232
241
|
if (editorUx === undefined)
|
|
233
242
|
editor.handleInput(data);
|
|
234
243
|
else
|
|
235
244
|
editorUx.handleInput(data);
|
|
245
|
+
if (editorUx === undefined || !hadDraft || editor.getText().length > 0)
|
|
246
|
+
return;
|
|
247
|
+
// Invariant: Pi's deletion paths can leave a visible or debounced autocomplete request owned by
|
|
248
|
+
// the removed draft. Reapplying empty text cancels both forms without a duplicate change notice.
|
|
249
|
+
const onChange = editor.onChange;
|
|
250
|
+
delete editor.onChange;
|
|
251
|
+
try {
|
|
252
|
+
editor.setText("");
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
if (onChange === undefined)
|
|
256
|
+
delete editor.onChange;
|
|
257
|
+
else
|
|
258
|
+
editor.onChange = onChange;
|
|
259
|
+
}
|
|
236
260
|
},
|
|
237
261
|
invalidate: () => editor.invalidate(),
|
|
238
262
|
setFocused: focused => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ExtensionEditorComponent
|
|
1
|
+
import { getSelectListTheme, } from "../startup-public.js";
|
|
2
|
+
import { ExtensionEditorComponent } from "./upstream/components/extension-editor.js";
|
|
3
|
+
import { ExtensionInputComponent } from "./upstream/components/extension-input.js";
|
|
4
|
+
import { ExtensionSelectorComponent } from "./upstream/components/extension-selector.js";
|
|
3
5
|
import { setKeybindings, Text, } from "@earendil-works/pi-tui";
|
|
4
6
|
import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
|
|
5
7
|
import { PINNED_PI_LAYOUT, applyPiTheme, applyPiThemeInstance, getAvailablePiThemes, loadPiTheme, piTheme, } from "./theme.js";
|
|
@@ -165,7 +165,7 @@ function liveWorkingText(view, workingOverride, progressPresentation) {
|
|
|
165
165
|
const message = workingOverride ?? view.status.workingMessage ?? "Working";
|
|
166
166
|
const progress = view.status.workingProgress;
|
|
167
167
|
return workingOverride === undefined && progressPresentation === "custom-viewport" && typeof progress === "number"
|
|
168
|
-
? `${message}
|
|
168
|
+
? `${message}(${progress}%)`
|
|
169
169
|
: message;
|
|
170
170
|
}
|
|
171
171
|
function statusComponent(view, ui, outputPad, liveStatusText, placement) {
|