@timurproko/a1 0.1.8-dev.0aceebe → 0.1.8-dev.235df0e
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/README.md +11 -7
- 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/src/cli/dispatch.js +53 -18
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.js +2 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +7 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.js +111 -22
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +129 -7
- package/dist/src/foundation/release/index.d.ts +0 -1
- package/dist/src/foundation/release/index.js +0 -1
- package/dist/src/foundation/release/update.js +4 -2
- package/package.json +1 -1
- package/dist/src/foundation/release/development-preview-release.d.ts +0 -49
- package/dist/src/foundation/release/development-preview-release.js +0 -114
package/README.md
CHANGED
|
@@ -30,16 +30,20 @@ Pi extension packages install into A1's own profile, so bare `a1` loads them and
|
|
|
30
30
|
`a1 pi` and `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a local path.
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
a1 install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
34
|
-
a1 remove npm:pi-mcp-adapter # remove it again (alias: a1 uninstall)
|
|
35
|
-
a1 list # list packages installed for a1
|
|
36
|
-
a1 update --extensions # update every installed package
|
|
37
|
-
a1 update npm:pi-mcp-adapter # update one of them
|
|
38
|
-
a1 update --models
|
|
33
|
+
a1 pi install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
34
|
+
a1 pi remove npm:pi-mcp-adapter # remove it again (alias: a1 pi uninstall)
|
|
35
|
+
a1 pi list # list packages installed for a1
|
|
36
|
+
a1 pi update --extensions # update every installed package
|
|
37
|
+
a1 pi update npm:pi-mcp-adapter # update one of them
|
|
38
|
+
a1 update --models # refresh A1's model catalogs
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
A running session loads a newly installed package after a restart. Pi's own
|
|
42
|
-
profile at `~/.pi/agent` is managed by Pi itself.
|
|
42
|
+
profile at `~/.pi/agent` is managed by Pi itself. Extension configuration is
|
|
43
|
+
isolated too: for example, MCP configuration for bare `a1` belongs under
|
|
44
|
+
`~/.a1/agent` (or the project), so `~/.pi/agent/mcp.json` is not read. Run
|
|
45
|
+
`/mcp setup` inside bare `a1` to configure it; the MCP footer status appears
|
|
46
|
+
when that A1 configuration contains a server.
|
|
43
47
|
|
|
44
48
|
## Develop
|
|
45
49
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "unsupported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-25T07:40:28.168Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-25T07:40:28.525Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-25T07:40:52.318Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "9a82f7885d1b3375ea0b78174e68461c176c26504ed5fb6740fcd4cb96516432",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
package/dist/src/cli/dispatch.js
CHANGED
|
@@ -5,12 +5,13 @@ export function cliUsage(capabilities) {
|
|
|
5
5
|
"",
|
|
6
6
|
...(capabilities.developmentProfiles ? ["pi", "sandbox"] : []),
|
|
7
7
|
"version",
|
|
8
|
-
"update [self
|
|
8
|
+
"update [self|--models]",
|
|
9
9
|
"update:next",
|
|
10
10
|
"update:<commit>",
|
|
11
|
-
"install <source>",
|
|
12
|
-
"remove <source>",
|
|
13
|
-
"list",
|
|
11
|
+
"pi install <source>",
|
|
12
|
+
"pi remove <source>",
|
|
13
|
+
"pi list",
|
|
14
|
+
"pi update [--extensions|<source>]",
|
|
14
15
|
]);
|
|
15
16
|
}
|
|
16
17
|
const PROFILE_WORDS = new Set(["pi", "sandbox"]);
|
|
@@ -32,7 +33,13 @@ export function parseCliCommand(arguments_, capabilities) {
|
|
|
32
33
|
if (arguments_.length === 0)
|
|
33
34
|
return { kind: "launch", profileId: "a1" };
|
|
34
35
|
const [command, ...rest] = arguments_;
|
|
35
|
-
if (
|
|
36
|
+
if (command === "pi") {
|
|
37
|
+
if (rest.length > 0)
|
|
38
|
+
return parsePiPackageCommand(rest);
|
|
39
|
+
if (capabilities.developmentProfiles)
|
|
40
|
+
return { kind: "launch", profileId: "pi" };
|
|
41
|
+
}
|
|
42
|
+
if (capabilities.developmentProfiles && command === "sandbox") {
|
|
36
43
|
return withoutArguments(rest, { kind: "launch", profileId: command });
|
|
37
44
|
}
|
|
38
45
|
if (command === "version")
|
|
@@ -41,11 +48,9 @@ export function parseCliCommand(arguments_, capabilities) {
|
|
|
41
48
|
return parseColonUpdate(command.slice("update:".length), rest);
|
|
42
49
|
if (command === "update")
|
|
43
50
|
return parseUpdate(rest);
|
|
44
|
-
if (command === "install" || command === "remove" || command === "uninstall") {
|
|
45
|
-
return
|
|
51
|
+
if (command === "install" || command === "remove" || command === "uninstall" || command === "list") {
|
|
52
|
+
return packageNamespaceRejection(command, rest.join(" ") || undefined);
|
|
46
53
|
}
|
|
47
|
-
if (command === "list")
|
|
48
|
-
return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
|
|
49
54
|
if (command === "ui")
|
|
50
55
|
return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
|
|
51
56
|
if (command === "agent")
|
|
@@ -71,10 +76,9 @@ function parseColonUpdate(suffix, rest) {
|
|
|
71
76
|
return { kind: "update", channel: "next", target: suffix };
|
|
72
77
|
}
|
|
73
78
|
/**
|
|
74
|
-
* `update`
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* invalidate what was certified.
|
|
79
|
+
* Top-level `update` owns A1 itself and A1's model catalogs. Extension package
|
|
80
|
+
* maintenance lives under the `pi` compatibility namespace, while updating the
|
|
81
|
+
* pinned Pi runtime remains impossible.
|
|
78
82
|
*/
|
|
79
83
|
function parseUpdate(rest) {
|
|
80
84
|
if (rest.length === 0)
|
|
@@ -90,18 +94,42 @@ function parseUpdate(rest) {
|
|
|
90
94
|
message: PRODUCT_TEXT.diagnostic(`pins the Pi version it was certified against; run ${PRODUCT_TEXT.commandName} update to move ${PRODUCT_TEXT.displayName} itself.`),
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
|
-
// A release channel is spelled with the colon. Taking the bare word as a package
|
|
94
|
-
// source would turn a near miss into a confident search for a package nobody has.
|
|
95
97
|
if (target === "next" || target === "stable") {
|
|
96
98
|
const form = target === "next" ? `${PRODUCT_TEXT.commandName} update:next` : `${PRODUCT_TEXT.commandName} update`;
|
|
97
99
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`selects a release channel with a colon; run ${form}.`) };
|
|
98
100
|
}
|
|
99
|
-
if (target === "--extensions")
|
|
100
|
-
return { kind: "packages", request: { verb: "update", source: null } };
|
|
101
101
|
if (target === "--models")
|
|
102
102
|
return { kind: "packages", request: { verb: "refresh-models", source: null } };
|
|
103
|
+
if (target === "--extensions" || (target !== undefined && !target.startsWith("-"))) {
|
|
104
|
+
return packageNamespaceRejection("update", target === "--extensions" ? "--extensions" : target);
|
|
105
|
+
}
|
|
106
|
+
return unknownOption(target ?? "", "update");
|
|
107
|
+
}
|
|
108
|
+
function parsePiPackageCommand(arguments_) {
|
|
109
|
+
const [verb, ...rest] = arguments_;
|
|
110
|
+
if (verb === "install" || verb === "remove" || verb === "uninstall") {
|
|
111
|
+
return parseSourceCommand(verb === "install" ? "install" : "remove", rest);
|
|
112
|
+
}
|
|
113
|
+
if (verb === "list")
|
|
114
|
+
return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
|
|
115
|
+
if (verb === "update")
|
|
116
|
+
return parsePiPackageUpdate(rest);
|
|
117
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown pi package command: ${verb ?? ""}`) };
|
|
118
|
+
}
|
|
119
|
+
function parsePiPackageUpdate(rest) {
|
|
120
|
+
if (rest.length === 0) {
|
|
121
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`pi update needs --extensions or a package source.`) };
|
|
122
|
+
}
|
|
123
|
+
if (rest.length > 1)
|
|
124
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic("pi update accepts one target.") };
|
|
125
|
+
const [target] = rest;
|
|
126
|
+
if (target === "--extensions")
|
|
127
|
+
return { kind: "packages", request: { verb: "update", source: null } };
|
|
128
|
+
if (target === "--models") {
|
|
129
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`refreshes its model catalogs at the top level; run ${PRODUCT_TEXT.commandName} update --models.`) };
|
|
130
|
+
}
|
|
103
131
|
if (target === undefined || target.startsWith("-"))
|
|
104
|
-
return unknownOption(target ?? "", "update");
|
|
132
|
+
return unknownOption(target ?? "", "pi update");
|
|
105
133
|
if (PROFILE_WORDS.has(target))
|
|
106
134
|
return profileRejection("update");
|
|
107
135
|
return { kind: "packages", request: { verb: "update", source: target } };
|
|
@@ -129,6 +157,13 @@ function withoutArguments(rest, command) {
|
|
|
129
157
|
return profileRejection("list");
|
|
130
158
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic("commands do not accept additional arguments.") };
|
|
131
159
|
}
|
|
160
|
+
function packageNamespaceRejection(verb, target) {
|
|
161
|
+
const suffix = target === undefined ? "" : ` ${target}`;
|
|
162
|
+
return {
|
|
163
|
+
kind: "error",
|
|
164
|
+
message: PRODUCT_TEXT.diagnostic(`manages extension packages under its pi namespace; run ${PRODUCT_TEXT.commandName} pi ${verb}${suffix}.`),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
132
167
|
function profileRejection(verb) {
|
|
133
168
|
return {
|
|
134
169
|
kind: "error",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AssistantMessageComponent, BashExecutionComponent, CompactionSummaryMessageComponent, CustomMessageComponent, DynamicBorder, getMarkdownTheme, parseSkillBlock, ToolExecutionComponent, UserMessageComponent, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { SkillInvocationMessageComponent, } from "./upstream/components/skill-invocation-message.js";
|
|
3
3
|
import { Container, Markdown, Spacer, Text, } from "#pi-tui";
|
|
4
|
+
import { PRODUCT_TEXT } from "../../product-identity.js";
|
|
4
5
|
import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
|
|
5
6
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
6
7
|
import { componentPort, createTuiFacade, ensureTheme, formatSessionTokens, isRecord, } from "./shell-shared-facade.js";
|
|
@@ -168,7 +169,7 @@ export function renderPiShellPackageUpdateNotice(packages, width) {
|
|
|
168
169
|
container.addChild(new Spacer(1));
|
|
169
170
|
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
170
171
|
container.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n`
|
|
171
|
-
+ `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent",
|
|
172
|
+
+ `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent", `${PRODUCT_TEXT.commandName} pi update --extensions`)}\n`
|
|
172
173
|
+ `${theme.fg("muted", "Packages:")}\n`
|
|
173
174
|
+ packages.map(name => `- ${name}`).join("\n"), 1, 0));
|
|
174
175
|
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
@@ -60,11 +60,18 @@ export interface OwnedPiResourceSummary {
|
|
|
60
60
|
readonly sourcePath: string | null;
|
|
61
61
|
readonly diagnostic: string | null;
|
|
62
62
|
}
|
|
63
|
+
export interface OwnedPiExtensionSourceSummary {
|
|
64
|
+
readonly source: string;
|
|
65
|
+
readonly scope: "user" | "project" | "temporary";
|
|
66
|
+
readonly origin: "package" | "top-level";
|
|
67
|
+
readonly baseDir: string | null;
|
|
68
|
+
}
|
|
63
69
|
export interface OwnedPiExtensionResourceSummary {
|
|
64
70
|
readonly kind: "extension";
|
|
65
71
|
readonly id: string;
|
|
66
72
|
readonly sourcePath: string | null;
|
|
67
73
|
readonly resolvedPath: string | null;
|
|
74
|
+
readonly sourceInfo: OwnedPiExtensionSourceSummary | null;
|
|
68
75
|
readonly loaded: boolean;
|
|
69
76
|
readonly hidden: boolean;
|
|
70
77
|
readonly diagnostic: string | null;
|
|
@@ -11,6 +11,12 @@ import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
|
11
11
|
import { PiSessionCommandIntegration } from "./session-integration.js";
|
|
12
12
|
import { PiSettingsIntegration } from "./settings-integration.js";
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
|
+
/**
|
|
15
|
+
* Engine events delivered before the queue hands the event loop a turn. Small enough
|
|
16
|
+
* that a streaming burst never holds input, large enough that an ordinary turn is one
|
|
17
|
+
* batch.
|
|
18
|
+
*/
|
|
19
|
+
const EVENT_DELIVERY_BATCH = 16;
|
|
14
20
|
const DEFAULT_SURFACE = {
|
|
15
21
|
columns: 100,
|
|
16
22
|
rows: 32,
|
|
@@ -61,6 +67,8 @@ export class PiEngineAdapter {
|
|
|
61
67
|
#eventQueue = [];
|
|
62
68
|
#eventQueueProcessing;
|
|
63
69
|
#droppedEventCount = 0;
|
|
70
|
+
#agentRunActive = false;
|
|
71
|
+
#statusKind = null;
|
|
64
72
|
#sessionCommands;
|
|
65
73
|
#gitBranch = null;
|
|
66
74
|
#extensionUi;
|
|
@@ -143,7 +151,7 @@ export class PiEngineAdapter {
|
|
|
143
151
|
if (this.#disposed || updates.length === 0)
|
|
144
152
|
return;
|
|
145
153
|
const packages = updates.map(name => `- ${name}`).join("\n");
|
|
146
|
-
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run pi update --extensions\nPackages:\n${packages}`, true);
|
|
154
|
+
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run ${PRODUCT_IDENTITY.commandName} pi update --extensions\nPackages:\n${packages}`, true);
|
|
147
155
|
this.#emitView();
|
|
148
156
|
}
|
|
149
157
|
onEvent(listener) {
|
|
@@ -278,6 +286,7 @@ export class PiEngineAdapter {
|
|
|
278
286
|
id: `extension-${resources.length}`,
|
|
279
287
|
sourcePath: extension.path,
|
|
280
288
|
resolvedPath: extension.resolvedPath,
|
|
289
|
+
sourceInfo: extensionSourceSummary(extension.sourceInfo),
|
|
281
290
|
loaded: true,
|
|
282
291
|
hidden: extension.hidden === true,
|
|
283
292
|
diagnostic: null,
|
|
@@ -373,9 +382,10 @@ export class PiEngineAdapter {
|
|
|
373
382
|
}
|
|
374
383
|
const extensionCommands = this.#session?.extensionRunner?.getRegisteredCommands?.();
|
|
375
384
|
if (Array.isArray(extensionCommands)) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
385
|
+
const registered = extensionCommands.filter(isRecord);
|
|
386
|
+
for (const command of registered) {
|
|
387
|
+
const name = stringProperty(command, "invocationName") ?? stringProperty(command, "name");
|
|
388
|
+
if (!name || usedNames.has(name) || isPiPrefixedCompatibilityAlias(command, registered))
|
|
379
389
|
continue;
|
|
380
390
|
commands.push({ name, description: stringProperty(command, "description") ?? "Extension command", source: "extension" });
|
|
381
391
|
usedNames.add(name);
|
|
@@ -1350,6 +1360,8 @@ export class PiEngineAdapter {
|
|
|
1350
1360
|
submitEnabled: true,
|
|
1351
1361
|
};
|
|
1352
1362
|
this.#status = { ...this.#status, workingMessage: null, badges: [] };
|
|
1363
|
+
this.#statusKind = null;
|
|
1364
|
+
this.#agentRunActive = false;
|
|
1353
1365
|
this.#activeModel = readModel(session.model);
|
|
1354
1366
|
this.#reconcileActiveModelAvailability();
|
|
1355
1367
|
this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
|
|
@@ -1402,15 +1414,45 @@ export class PiEngineAdapter {
|
|
|
1402
1414
|
this.#emitView();
|
|
1403
1415
|
}
|
|
1404
1416
|
}
|
|
1417
|
+
/** Shows the state named by `kind`, which becomes the state a later end can clear. */
|
|
1418
|
+
#enterWorkState(kind, message) {
|
|
1419
|
+
const wasBusy = this.#lifecycle === "busy";
|
|
1420
|
+
this.#statusKind = kind;
|
|
1421
|
+
this.#lifecycle = "busy";
|
|
1422
|
+
this.#status = { ...this.#status, workingMessage: message };
|
|
1423
|
+
if (!wasBusy)
|
|
1424
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1425
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Ends one named state. A state the shell is not in is left alone, so a finished
|
|
1429
|
+
* compaction or retry cannot clear the working state it never replaced. While the run
|
|
1430
|
+
* continues, ending either of those returns to working rather than to idle.
|
|
1431
|
+
*/
|
|
1432
|
+
#endWorkState(kind) {
|
|
1433
|
+
if (this.#statusKind !== kind)
|
|
1434
|
+
return;
|
|
1435
|
+
if (this.#agentRunActive) {
|
|
1436
|
+
this.#enterWorkState("working", "Working...");
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
this.#leaveWorkStates();
|
|
1440
|
+
}
|
|
1441
|
+
/** Leaves every work state and reports the session idle. */
|
|
1442
|
+
#leaveWorkStates() {
|
|
1443
|
+
this.#statusKind = null;
|
|
1444
|
+
this.#lifecycle = "ready";
|
|
1445
|
+
this.#status = { ...this.#status, workingMessage: null };
|
|
1446
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
1447
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1448
|
+
}
|
|
1405
1449
|
#handlePiEvent(event) {
|
|
1406
1450
|
if (!isRecord(event) || typeof event.type !== "string")
|
|
1407
1451
|
return;
|
|
1408
1452
|
switch (event.type) {
|
|
1409
1453
|
case "agent_start":
|
|
1410
|
-
this.#
|
|
1411
|
-
this.#
|
|
1412
|
-
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1413
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1454
|
+
this.#agentRunActive = true;
|
|
1455
|
+
this.#enterWorkState("working", "Working...");
|
|
1414
1456
|
return;
|
|
1415
1457
|
case "message_start":
|
|
1416
1458
|
this.#upsertMessageBlock(event.message, "live");
|
|
@@ -1453,10 +1495,17 @@ export class PiEngineAdapter {
|
|
|
1453
1495
|
this.#rebuildTranscript(finalMessages, "finalized");
|
|
1454
1496
|
else
|
|
1455
1497
|
this.#transcript = this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block);
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1498
|
+
// Ending a turn leaves the working state, as the recorded pinned baseline does, but
|
|
1499
|
+
// it leaves only that state: a compaction or retry being shown outlives the turn
|
|
1500
|
+
// that ended under it. Settlement ends the run, and with it every state — the
|
|
1501
|
+
// engine ends a turn for each continuation it makes and settles once.
|
|
1502
|
+
if (event.type === "agent_settled") {
|
|
1503
|
+
this.#agentRunActive = false;
|
|
1504
|
+
this.#leaveWorkStates();
|
|
1505
|
+
}
|
|
1506
|
+
else if (this.#statusKind === null || this.#statusKind === "working") {
|
|
1507
|
+
this.#leaveWorkStates();
|
|
1508
|
+
}
|
|
1460
1509
|
this.#emitView();
|
|
1461
1510
|
return;
|
|
1462
1511
|
}
|
|
@@ -1472,20 +1521,16 @@ export class PiEngineAdapter {
|
|
|
1472
1521
|
return;
|
|
1473
1522
|
}
|
|
1474
1523
|
case "auto_retry_start":
|
|
1475
|
-
this.#
|
|
1476
|
-
this.#status = { ...this.#status, workingMessage: "Retrying…" };
|
|
1477
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1524
|
+
this.#enterWorkState("retry", "Retrying…");
|
|
1478
1525
|
return;
|
|
1479
1526
|
case "auto_retry_end":
|
|
1480
|
-
|
|
1481
|
-
this.#lifecycle = "ready";
|
|
1482
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
1483
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1527
|
+
this.#endWorkState("retry");
|
|
1484
1528
|
return;
|
|
1485
1529
|
case "compaction_start":
|
|
1486
|
-
this.#
|
|
1487
|
-
|
|
1488
|
-
|
|
1530
|
+
this.#enterWorkState("compaction", "Compacting…");
|
|
1531
|
+
return;
|
|
1532
|
+
case "compaction_end":
|
|
1533
|
+
this.#endWorkState("compaction");
|
|
1489
1534
|
return;
|
|
1490
1535
|
case "thinking_level_changed":
|
|
1491
1536
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -1847,6 +1892,7 @@ export class PiEngineAdapter {
|
|
|
1847
1892
|
}
|
|
1848
1893
|
async #processEventQueue() {
|
|
1849
1894
|
try {
|
|
1895
|
+
let deliveredSinceYield = 0;
|
|
1850
1896
|
while (this.#eventQueue.length > 0) {
|
|
1851
1897
|
const event = this.#eventQueue.shift();
|
|
1852
1898
|
if (!event)
|
|
@@ -1859,6 +1905,14 @@ export class PiEngineAdapter {
|
|
|
1859
1905
|
this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
|
|
1860
1906
|
}
|
|
1861
1907
|
}
|
|
1908
|
+
deliveredSinceYield += 1;
|
|
1909
|
+
// A microtask chain runs to exhaustion before the loop turns, so a streaming
|
|
1910
|
+
// burst would hold typed input, pointer reports, and timed indicators until it
|
|
1911
|
+
// drained. Yielding on a macrotask hands those their turn between batches.
|
|
1912
|
+
if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
|
|
1913
|
+
deliveredSinceYield = 0;
|
|
1914
|
+
await new Promise(resolve => { setImmediate(resolve); });
|
|
1915
|
+
}
|
|
1862
1916
|
}
|
|
1863
1917
|
}
|
|
1864
1918
|
finally {
|
|
@@ -2245,11 +2299,26 @@ function extensionResourceDiagnostic(index, sourcePath, diagnostic) {
|
|
|
2245
2299
|
id: `extension-diagnostic-${index}`,
|
|
2246
2300
|
sourcePath,
|
|
2247
2301
|
resolvedPath: null,
|
|
2302
|
+
sourceInfo: null,
|
|
2248
2303
|
loaded: false,
|
|
2249
2304
|
hidden: false,
|
|
2250
2305
|
diagnostic,
|
|
2251
2306
|
};
|
|
2252
2307
|
}
|
|
2308
|
+
function extensionSourceSummary(value) {
|
|
2309
|
+
if (!isRecord(value))
|
|
2310
|
+
return null;
|
|
2311
|
+
const source = stringProperty(value, "source");
|
|
2312
|
+
const scope = value.scope;
|
|
2313
|
+
const origin = value.origin;
|
|
2314
|
+
const baseDir = value.baseDir;
|
|
2315
|
+
if (!source
|
|
2316
|
+
|| (scope !== "user" && scope !== "project" && scope !== "temporary")
|
|
2317
|
+
|| (origin !== "package" && origin !== "top-level")
|
|
2318
|
+
|| (baseDir !== undefined && typeof baseDir !== "string"))
|
|
2319
|
+
return null;
|
|
2320
|
+
return { source, scope, origin, baseDir: baseDir ?? null };
|
|
2321
|
+
}
|
|
2253
2322
|
function collectionResult(value, key) {
|
|
2254
2323
|
if (!isRecord(value))
|
|
2255
2324
|
return { values: [], diagnostics: [] };
|
|
@@ -2274,6 +2343,26 @@ function stringProperty(value, key) {
|
|
|
2274
2343
|
const item = value[key];
|
|
2275
2344
|
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
2276
2345
|
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
|
|
2348
|
+
* their unprefixed command. A1 presents the product-neutral command once while
|
|
2349
|
+
* leaving Pi's runner free to accept the compatibility alias when typed.
|
|
2350
|
+
*/
|
|
2351
|
+
function isPiPrefixedCompatibilityAlias(command, commands) {
|
|
2352
|
+
const name = stringProperty(command, "name");
|
|
2353
|
+
if (!name?.startsWith("pi-") || name.length === 3)
|
|
2354
|
+
return false;
|
|
2355
|
+
const canonicalName = name.slice(3);
|
|
2356
|
+
const description = stringProperty(command, "description");
|
|
2357
|
+
const sourcePath = extensionCommandSourcePath(command);
|
|
2358
|
+
return commands.some(candidate => candidate !== command
|
|
2359
|
+
&& stringProperty(candidate, "name") === canonicalName
|
|
2360
|
+
&& stringProperty(candidate, "description") === description
|
|
2361
|
+
&& extensionCommandSourcePath(candidate) === sourcePath);
|
|
2362
|
+
}
|
|
2363
|
+
function extensionCommandSourcePath(command) {
|
|
2364
|
+
return isRecord(command) ? stringProperty(command.sourceInfo, "path") : undefined;
|
|
2365
|
+
}
|
|
2277
2366
|
function compactResourceLabel(path) {
|
|
2278
2367
|
const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
2279
2368
|
return segments.at(-1) ?? path;
|
|
@@ -525,12 +525,14 @@ function shellResourceEntries(backend) {
|
|
|
525
525
|
sourcePath: resource.sourcePath,
|
|
526
526
|
diagnostic: resource.diagnostic,
|
|
527
527
|
}));
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
528
|
+
const extensions = backend.extensionResources().filter(extension => !extension.hidden);
|
|
529
|
+
const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
|
|
530
|
+
const extensionLabels = compactExtensionLabels(loadedExtensions);
|
|
531
|
+
for (const extension of extensions) {
|
|
532
|
+
const labelIndex = loadedExtensions.indexOf(extension);
|
|
531
533
|
resources.push({
|
|
532
534
|
section: "Extensions",
|
|
533
|
-
label: compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
535
|
+
label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
|
|
534
536
|
sourcePath: extension.sourcePath ?? extension.resolvedPath,
|
|
535
537
|
diagnostic: extension.diagnostic,
|
|
536
538
|
});
|
|
@@ -538,12 +540,129 @@ function shellResourceEntries(backend) {
|
|
|
538
540
|
return resources;
|
|
539
541
|
}
|
|
540
542
|
function compactResourceLabel(path) {
|
|
541
|
-
const segments = path
|
|
543
|
+
const segments = compactPathSegments(path);
|
|
542
544
|
const leaf = segments.at(-1) ?? path;
|
|
543
545
|
if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
|
|
544
546
|
return segments.at(-2) ?? leaf;
|
|
545
547
|
return leaf;
|
|
546
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Pinned from InteractiveMode's compact extension-label helpers at Pi commit
|
|
551
|
+
* 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
|
|
552
|
+
* A1-owned boundary first; the owned shell never inspects Pi's private root.
|
|
553
|
+
*/
|
|
554
|
+
function compactExtensionLabels(extensions) {
|
|
555
|
+
const localExtensions = extensions
|
|
556
|
+
.filter(extension => !isPackageExtensionSource(extension.sourceInfo))
|
|
557
|
+
.map(extension => {
|
|
558
|
+
const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
559
|
+
const segments = compactPathSegments(path);
|
|
560
|
+
if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
|
|
561
|
+
segments.pop();
|
|
562
|
+
return { extension, segments };
|
|
563
|
+
});
|
|
564
|
+
return extensions.map(extension => {
|
|
565
|
+
const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
|
|
566
|
+
if (isPackageExtensionSource(extension.sourceInfo)) {
|
|
567
|
+
return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
|
|
568
|
+
}
|
|
569
|
+
const localIndex = localExtensions.findIndex(item => item.extension === extension);
|
|
570
|
+
const segments = localExtensions[localIndex]?.segments;
|
|
571
|
+
if (!segments || segments.length === 0)
|
|
572
|
+
return compactResourceLabel(resourcePath);
|
|
573
|
+
for (let count = 1; count <= segments.length; count += 1) {
|
|
574
|
+
const candidate = segments.slice(-count).join("/");
|
|
575
|
+
if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
|
|
576
|
+
return candidate;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return segments.join("/");
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function compactPackageExtensionLabel(resourcePath, sourceInfo) {
|
|
583
|
+
const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
|
|
584
|
+
if (!sourceLabel)
|
|
585
|
+
return compactResourceLabel(resourcePath);
|
|
586
|
+
const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
|
|
587
|
+
const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
|
|
588
|
+
const slash = packagePath.lastIndexOf("/");
|
|
589
|
+
const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
|
|
590
|
+
const directory = slash < 0 ? "" : packagePath.slice(0, slash);
|
|
591
|
+
const extension = fileName.lastIndexOf(".");
|
|
592
|
+
const name = extension <= 0 ? fileName : fileName.slice(0, extension);
|
|
593
|
+
if (name === "index")
|
|
594
|
+
return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
|
|
595
|
+
return `${sourceLabel}:${packagePath}`;
|
|
596
|
+
}
|
|
597
|
+
function compactPackageSourceLabel(source) {
|
|
598
|
+
if (source.startsWith("npm:"))
|
|
599
|
+
return source.slice("npm:".length) || source;
|
|
600
|
+
if (!source.startsWith("git:"))
|
|
601
|
+
return source;
|
|
602
|
+
const gitSource = source.slice("git:".length).trim();
|
|
603
|
+
let repositoryPath;
|
|
604
|
+
const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
|
|
605
|
+
if (scpLike?.[1]) {
|
|
606
|
+
repositoryPath = scpLike[1];
|
|
607
|
+
}
|
|
608
|
+
else if (/^[a-z]+:\/\//i.test(gitSource)) {
|
|
609
|
+
try {
|
|
610
|
+
repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return source;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
const slash = gitSource.indexOf("/");
|
|
618
|
+
if (slash >= 0)
|
|
619
|
+
repositoryPath = gitSource.slice(slash + 1);
|
|
620
|
+
}
|
|
621
|
+
if (!repositoryPath)
|
|
622
|
+
return source;
|
|
623
|
+
const ref = repositoryPath.indexOf("@");
|
|
624
|
+
const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
|
|
625
|
+
return withoutRef.replace(/\.git$/, "") || source;
|
|
626
|
+
}
|
|
627
|
+
function shortPackagePath(resourcePath, sourceInfo) {
|
|
628
|
+
const fullPath = normalizeResourcePath(resourcePath);
|
|
629
|
+
const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
|
|
630
|
+
if (baseDir) {
|
|
631
|
+
const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
|
|
632
|
+
if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
|
|
633
|
+
return relativeResourcePath(baseDir, fullPath);
|
|
634
|
+
if (fullPath === baseDir)
|
|
635
|
+
return ".";
|
|
636
|
+
if (fullPath.startsWith(`${baseDir}/`))
|
|
637
|
+
return fullPath.slice(baseDir.length + 1);
|
|
638
|
+
}
|
|
639
|
+
const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
|
|
640
|
+
if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
|
|
641
|
+
return npmMatch[2];
|
|
642
|
+
const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
|
|
643
|
+
if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
|
|
644
|
+
return gitMatch[1];
|
|
645
|
+
return resourcePath;
|
|
646
|
+
}
|
|
647
|
+
function relativeResourcePath(from, to) {
|
|
648
|
+
const fromSegments = from.split("/").filter(Boolean);
|
|
649
|
+
const toSegments = to.split("/").filter(Boolean);
|
|
650
|
+
let common = 0;
|
|
651
|
+
while (common < fromSegments.length && common < toSegments.length
|
|
652
|
+
&& fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
|
|
653
|
+
common += 1;
|
|
654
|
+
return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
|
|
655
|
+
}
|
|
656
|
+
function compactPathSegments(path) {
|
|
657
|
+
return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
|
|
658
|
+
}
|
|
659
|
+
function normalizeResourcePath(path) {
|
|
660
|
+
return path.replaceAll("\\", "/");
|
|
661
|
+
}
|
|
662
|
+
function isPackageExtensionSource(sourceInfo) {
|
|
663
|
+
const source = sourceInfo?.source ?? "";
|
|
664
|
+
return source.startsWith("npm:") || source.startsWith("git:");
|
|
665
|
+
}
|
|
547
666
|
export class OwnedUiSessionShell {
|
|
548
667
|
backend;
|
|
549
668
|
root;
|
|
@@ -638,8 +757,10 @@ export class OwnedUiSessionShell {
|
|
|
638
757
|
});
|
|
639
758
|
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
640
759
|
this.#unsubscribe = this.backend.onEvent(event => {
|
|
641
|
-
|
|
642
|
-
|
|
760
|
+
// One view read per event: the model is built by the backend, and building it
|
|
761
|
+
// twice per streamed chunk is what made a long session cost more per chunk.
|
|
762
|
+
const view = this.#syncView();
|
|
763
|
+
if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
|
|
643
764
|
void this.#flushCompactionQueue();
|
|
644
765
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
645
766
|
this.#resolveStopped?.();
|
|
@@ -1257,6 +1378,7 @@ export class OwnedUiSessionShell {
|
|
|
1257
1378
|
this.runtime.requestRender();
|
|
1258
1379
|
for (const listener of this.#listeners)
|
|
1259
1380
|
listener(view);
|
|
1381
|
+
return view;
|
|
1260
1382
|
}
|
|
1261
1383
|
#openOwnedRoute(route) {
|
|
1262
1384
|
const surface = this.#routeHost?.open(route) ?? null;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from "./bootstrap.js";
|
|
2
2
|
export * from "./cohort-selection.js";
|
|
3
3
|
export * from "./cohort-state.js";
|
|
4
|
-
export * from "./development-preview-release.js";
|
|
5
4
|
export * from "./process-cleanup.js";
|
|
6
5
|
export * from "./release.js";
|
|
7
6
|
export * from "./release-gc.js";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from "./bootstrap.js";
|
|
2
2
|
export * from "./cohort-selection.js";
|
|
3
3
|
export * from "./cohort-state.js";
|
|
4
|
-
export * from "./development-preview-release.js";
|
|
5
4
|
export * from "./process-cleanup.js";
|
|
6
5
|
export * from "./release.js";
|
|
7
6
|
export * from "./release-gc.js";
|
|
@@ -314,7 +314,9 @@ export async function runSelfUpdate(options) {
|
|
|
314
314
|
if (resolved.version === null)
|
|
315
315
|
return resolved.exitCode;
|
|
316
316
|
const targetVersion = resolved.version;
|
|
317
|
-
|
|
317
|
+
// No full stop after a version: it already ends in a dot-separated identifier,
|
|
318
|
+
// and a trailing one reads as part of the version rather than as punctuation.
|
|
319
|
+
output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} → ${targetVersion}\n`);
|
|
318
320
|
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
|
|
319
321
|
const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
|
|
320
322
|
if (rootLookup.result === null)
|
|
@@ -403,7 +405,7 @@ export async function runSelfUpdate(options) {
|
|
|
403
405
|
await transactionStore.clearCompleted();
|
|
404
406
|
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
405
407
|
progress.finish();
|
|
406
|
-
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}
|
|
408
|
+
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
|
|
407
409
|
return 0;
|
|
408
410
|
}
|
|
409
411
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export declare const PREVIEW_RELEASE_SCHEMA: string;
|
|
2
|
-
export interface DevelopmentPreviewCandidate {
|
|
3
|
-
readonly version: string;
|
|
4
|
-
readonly requiresVersionCommit: boolean;
|
|
5
|
-
}
|
|
6
|
-
export interface DevelopmentPreviewRegistryState {
|
|
7
|
-
readonly published: boolean;
|
|
8
|
-
readonly nextVersion: string | null;
|
|
9
|
-
}
|
|
10
|
-
export interface DevelopmentPreviewVerificationOptions {
|
|
11
|
-
readonly attempts?: number;
|
|
12
|
-
readonly delayMs?: number;
|
|
13
|
-
readonly delay?: (milliseconds: number) => Promise<void>;
|
|
14
|
-
}
|
|
15
|
-
export interface DevelopmentPreviewPublishResult {
|
|
16
|
-
readonly published: boolean;
|
|
17
|
-
readonly recoveredPublishError: unknown | null;
|
|
18
|
-
}
|
|
19
|
-
export interface UncertifiedDevelopmentPreviewEvidenceInput {
|
|
20
|
-
readonly packageName: string;
|
|
21
|
-
readonly version: string;
|
|
22
|
-
readonly commit: string;
|
|
23
|
-
readonly tarball: string;
|
|
24
|
-
readonly integrity: string;
|
|
25
|
-
readonly shasum: string;
|
|
26
|
-
readonly platform: NodeJS.Platform;
|
|
27
|
-
readonly architecture: string;
|
|
28
|
-
readonly recordedAt: string;
|
|
29
|
-
}
|
|
30
|
-
export interface UncertifiedDevelopmentPreviewEvidence extends UncertifiedDevelopmentPreviewEvidenceInput {
|
|
31
|
-
readonly schema: typeof PREVIEW_RELEASE_SCHEMA;
|
|
32
|
-
readonly channel: "next";
|
|
33
|
-
readonly certificationStatus: "uncertified-development-preview";
|
|
34
|
-
readonly terminalCapability: "owned-ui";
|
|
35
|
-
readonly manualAcceptance: "accepted";
|
|
36
|
-
readonly physicalHostCertification: "deferred";
|
|
37
|
-
readonly crossPlatformCertification: "deferred";
|
|
38
|
-
readonly stableReleaseEligible: false;
|
|
39
|
-
}
|
|
40
|
-
export declare function createUncertifiedDevelopmentPreviewEvidence(input: UncertifiedDevelopmentPreviewEvidenceInput): UncertifiedDevelopmentPreviewEvidence;
|
|
41
|
-
export declare function requireManuallyAcceptedDevelopmentPreview(version: string, acceptedVersion: string): void;
|
|
42
|
-
export declare function selectDevelopmentPreviewCandidate(currentVersion: string, publishedVersions: readonly string[]): DevelopmentPreviewCandidate;
|
|
43
|
-
/**
|
|
44
|
-
* Treats npm's process result as provisional: browser-auth completion can fail
|
|
45
|
-
* after the immutable upload succeeds. Registry identity remains authoritative.
|
|
46
|
-
*/
|
|
47
|
-
export declare function publishDevelopmentPreviewWithRecovery(publish: () => Promise<void>, verify: () => Promise<void>): Promise<DevelopmentPreviewPublishResult>;
|
|
48
|
-
export declare function verifyDevelopmentPreviewRegistry(version: string, observe: () => Promise<DevelopmentPreviewRegistryState>, repairNextTag: () => Promise<void>, options?: DevelopmentPreviewVerificationOptions): Promise<void>;
|
|
49
|
-
export declare function developmentPreviewTarballName(packageName: string, version: string): string;
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { compare, inc, prerelease, valid } from "semver";
|
|
2
|
-
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
3
|
-
export const PREVIEW_RELEASE_SCHEMA = PRODUCT_IDENTITY.evidence.previewReleaseSchema;
|
|
4
|
-
export function createUncertifiedDevelopmentPreviewEvidence(input) {
|
|
5
|
-
const prereleaseParts = prerelease(input.version);
|
|
6
|
-
if (valid(input.version) === null || prereleaseParts?.[0] !== "dev") {
|
|
7
|
-
throw new Error(`uncertified preview requires a development prerelease: ${input.version}`);
|
|
8
|
-
}
|
|
9
|
-
return {
|
|
10
|
-
schema: PREVIEW_RELEASE_SCHEMA,
|
|
11
|
-
channel: "next",
|
|
12
|
-
certificationStatus: "uncertified-development-preview",
|
|
13
|
-
terminalCapability: "owned-ui",
|
|
14
|
-
manualAcceptance: "accepted",
|
|
15
|
-
physicalHostCertification: "deferred",
|
|
16
|
-
crossPlatformCertification: "deferred",
|
|
17
|
-
stableReleaseEligible: false,
|
|
18
|
-
...input,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export function requireManuallyAcceptedDevelopmentPreview(version, acceptedVersion) {
|
|
22
|
-
const acceptedPrerelease = prerelease(acceptedVersion);
|
|
23
|
-
if (valid(acceptedVersion) === null || acceptedPrerelease?.[0] !== "dev") {
|
|
24
|
-
throw new Error(`invalid manually accepted development preview: ${acceptedVersion}`);
|
|
25
|
-
}
|
|
26
|
-
if (version !== acceptedVersion) {
|
|
27
|
-
throw new Error(`development preview ${version} has no exact manual acceptance; accepted version is ${acceptedVersion}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export function selectDevelopmentPreviewCandidate(currentVersion, publishedVersions) {
|
|
31
|
-
if (valid(currentVersion) === null)
|
|
32
|
-
throw new Error(`invalid current package version: ${currentVersion}`);
|
|
33
|
-
const published = publishedVersions.map(version => {
|
|
34
|
-
if (valid(version) === null)
|
|
35
|
-
throw new Error(`invalid published package version: ${version}`);
|
|
36
|
-
return version;
|
|
37
|
-
});
|
|
38
|
-
const publishedSet = new Set(published);
|
|
39
|
-
const highestPublished = published.reduce((highest, version) => highest === null || compare(version, highest) > 0 ? version : highest, null);
|
|
40
|
-
const currentPrerelease = prerelease(currentVersion);
|
|
41
|
-
const currentIsUnpublishedLeadingDev = currentPrerelease?.[0] === "dev"
|
|
42
|
-
&& !publishedSet.has(currentVersion)
|
|
43
|
-
&& (highestPublished === null || compare(currentVersion, highestPublished) > 0);
|
|
44
|
-
if (currentIsUnpublishedLeadingDev)
|
|
45
|
-
return { version: currentVersion, requiresVersionCommit: false };
|
|
46
|
-
const base = highestPublished === null || compare(currentVersion, highestPublished) > 0
|
|
47
|
-
? currentVersion
|
|
48
|
-
: highestPublished;
|
|
49
|
-
const basePrerelease = prerelease(base);
|
|
50
|
-
let candidate = basePrerelease?.[0] === "dev"
|
|
51
|
-
? inc(base, "prerelease", "dev")
|
|
52
|
-
: inc(base, "prepatch", "dev");
|
|
53
|
-
if (candidate === null)
|
|
54
|
-
throw new Error(`could not increment development preview from ${base}`);
|
|
55
|
-
while (publishedSet.has(candidate)) {
|
|
56
|
-
candidate = inc(candidate, "prerelease", "dev");
|
|
57
|
-
if (candidate === null)
|
|
58
|
-
throw new Error(`could not increment development preview from ${base}`);
|
|
59
|
-
}
|
|
60
|
-
return { version: candidate, requiresVersionCommit: candidate !== currentVersion };
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Treats npm's process result as provisional: browser-auth completion can fail
|
|
64
|
-
* after the immutable upload succeeds. Registry identity remains authoritative.
|
|
65
|
-
*/
|
|
66
|
-
export async function publishDevelopmentPreviewWithRecovery(publish, verify) {
|
|
67
|
-
let publishError = null;
|
|
68
|
-
try {
|
|
69
|
-
await publish();
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
publishError = error;
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
await verify();
|
|
76
|
-
return { published: true, recoveredPublishError: publishError };
|
|
77
|
-
}
|
|
78
|
-
catch (verificationError) {
|
|
79
|
-
if (publishError !== null)
|
|
80
|
-
throw new AggregateError([publishError, verificationError], "npm publish failed and the exact version could not be verified in the registry");
|
|
81
|
-
throw verificationError;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export async function verifyDevelopmentPreviewRegistry(version, observe, repairNextTag, options = {}) {
|
|
85
|
-
const attempts = options.attempts ?? 12;
|
|
86
|
-
const delayMs = options.delayMs ?? 2_000;
|
|
87
|
-
const delay = options.delay ?? (async (milliseconds) => await new Promise(resolvePromise => setTimeout(resolvePromise, milliseconds)));
|
|
88
|
-
if (!Number.isInteger(attempts) || attempts < 1)
|
|
89
|
-
throw new Error(`invalid registry verification attempts: ${attempts}`);
|
|
90
|
-
let repaired = false;
|
|
91
|
-
let last = { published: false, nextVersion: null };
|
|
92
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
93
|
-
last = await observe();
|
|
94
|
-
if (last.published && last.nextVersion === version)
|
|
95
|
-
return;
|
|
96
|
-
if (last.published && !repaired) {
|
|
97
|
-
await repairNextTag();
|
|
98
|
-
repaired = true;
|
|
99
|
-
}
|
|
100
|
-
if (attempt < attempts)
|
|
101
|
-
await delay(delayMs);
|
|
102
|
-
}
|
|
103
|
-
if (!last.published)
|
|
104
|
-
throw new Error(`npm registry did not expose published version ${version} after ${attempts} attempts`);
|
|
105
|
-
throw new Error(`npm next resolved ${last.nextVersion ?? "nothing"}; expected ${version} after ${attempts} attempts`);
|
|
106
|
-
}
|
|
107
|
-
export function developmentPreviewTarballName(packageName, version) {
|
|
108
|
-
if (valid(version) === null)
|
|
109
|
-
throw new Error(`invalid development preview version: ${version}`);
|
|
110
|
-
const unscopedName = packageName.startsWith("@") ? packageName.slice(1).replace("/", "-") : packageName;
|
|
111
|
-
if (!/^[a-z0-9._-]+$/i.test(unscopedName))
|
|
112
|
-
throw new Error(`invalid package name: ${packageName}`);
|
|
113
|
-
return `${unscopedName}-${version}.tgz`;
|
|
114
|
-
}
|