@timurproko/a1 0.1.7 → 0.1.8-dev.0b6d8cc
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 +24 -13
- package/bin/cli.js +2 -2
- 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.d.ts +2 -1
- package/dist/src/cli/dispatch.js +75 -21
- 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/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 +141 -31
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +189 -13
- 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.d.ts +2 -0
- package/dist/src/foundation/release/update.js +82 -11
- package/docs/ci-release-runbook.md +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
|
@@ -9,10 +9,11 @@ npm install --global @timurproko/a1@latest
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
a1
|
|
13
|
-
a1 version
|
|
14
|
-
a1 update
|
|
15
|
-
a1 update:next
|
|
12
|
+
a1 # A1-owned UI and profile: ~/.a1/agent
|
|
13
|
+
a1 version # show Installed, Release (latest), and Next versions
|
|
14
|
+
a1 update # update to npm latest
|
|
15
|
+
a1 update:next # update to npm next (or a1 update:<commit> for a specific preview)
|
|
16
|
+
a1 update --models # refresh A1's model catalogs
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
Prerelease builds — what `a1 update:next` installs — add two development profiles
|
|
@@ -30,16 +31,19 @@ Pi extension packages install into A1's own profile, so bare `a1` loads them and
|
|
|
30
31
|
`a1 pi` and `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a local path.
|
|
31
32
|
|
|
32
33
|
```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 # refresh model catalogs
|
|
34
|
+
a1 pi install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
35
|
+
a1 pi remove npm:pi-mcp-adapter # remove it again (alias: a1 pi uninstall)
|
|
36
|
+
a1 pi list # list packages installed for a1
|
|
37
|
+
a1 pi update --extensions # update every installed package
|
|
38
|
+
a1 pi update npm:pi-mcp-adapter # update one of them
|
|
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
|
|
|
@@ -68,9 +72,16 @@ time and never committed, so previews cost no commits and need no command, and a
|
|
|
68
72
|
installed preview says exactly which source produced it.
|
|
69
73
|
|
|
70
74
|
```sh
|
|
71
|
-
a1 update:next
|
|
75
|
+
a1 update:next # install the newest preview
|
|
76
|
+
a1 update:7eabe9e # install the preview built from that commit
|
|
72
77
|
```
|
|
73
78
|
|
|
79
|
+
Naming a commit is what the version suffix is for: read it off `a1 version`, a
|
|
80
|
+
pull request, or a commit list, and install exactly that build — you never need to
|
|
81
|
+
know which version it went out under. A commit that was never published is refused
|
|
82
|
+
rather than guessed at. A full preview version works in the same place, so a string
|
|
83
|
+
pasted back from `a1 version` is understood too.
|
|
84
|
+
|
|
74
85
|
### Stable — npm `latest`
|
|
75
86
|
|
|
76
87
|
One command, from a clean `develop` that matches its remote:
|
|
@@ -86,7 +97,7 @@ It lands the version on `develop` through a pull request that merges itself.
|
|
|
86
97
|
**Landing it is what publishes** — the workflow builds, validates the packed release
|
|
87
98
|
on Windows, Linux, and macOS, publishes to npm `latest` with provenance, and only
|
|
88
99
|
then writes the `v<version>` tag and records the GitHub Release. The command waits
|
|
89
|
-
for that to succeed, then
|
|
100
|
+
for that to succeed, then opens the next `-dev` line so previews resume
|
|
90
101
|
immediately.
|
|
91
102
|
|
|
92
103
|
```sh
|
package/bin/cli.js
CHANGED
|
@@ -22,9 +22,9 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
|
|
|
22
22
|
const { runVersionStats } = await import("../dist/src/cli/index.js");
|
|
23
23
|
return await runVersionStats({ packageRoot: fileURLToPath(packageRoot) });
|
|
24
24
|
},
|
|
25
|
-
update: async channel => {
|
|
25
|
+
update: async (channel, target) => {
|
|
26
26
|
const { runSelfUpdate } = await import("../dist/src/foundation/release/index.js");
|
|
27
|
-
return await runSelfUpdate({ packageRoot: fileURLToPath(packageRoot), channel });
|
|
27
|
+
return await runSelfUpdate({ packageRoot: fileURLToPath(packageRoot), channel, ...(target === undefined ? {} : { target }) });
|
|
28
28
|
},
|
|
29
29
|
packages: async request => {
|
|
30
30
|
const [{ runPackageCommand }, { createPiPackagesPort }] = await Promise.all([
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "unsupported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-25T08:02:28.988Z",
|
|
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-25T08:02:31.153Z",
|
|
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-25T08:02:58.479Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "fdbde2408643ffb897f5107d96ed40e8c8467505ef7daa64f86f9c2497c304ca",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -5,7 +5,7 @@ export type UpdateChannel = "stable" | "next";
|
|
|
5
5
|
export interface CliHandlers {
|
|
6
6
|
readonly launch: (intent: InteractiveLaunchIntent) => Promise<number>;
|
|
7
7
|
readonly version: () => Promise<number>;
|
|
8
|
-
readonly update: (channel: UpdateChannel) => Promise<number>;
|
|
8
|
+
readonly update: (channel: UpdateChannel, target?: string) => Promise<number>;
|
|
9
9
|
readonly packages: (request: PackageCommandRequest) => Promise<number>;
|
|
10
10
|
}
|
|
11
11
|
export interface CliOutput {
|
|
@@ -21,6 +21,7 @@ export type CliCommand = {
|
|
|
21
21
|
} | {
|
|
22
22
|
readonly kind: "update";
|
|
23
23
|
readonly channel: UpdateChannel;
|
|
24
|
+
readonly target?: string;
|
|
24
25
|
} | {
|
|
25
26
|
readonly kind: "packages";
|
|
26
27
|
readonly request: PackageCommandRequest;
|
package/dist/src/cli/dispatch.js
CHANGED
|
@@ -5,11 +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
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
"update:<commit>",
|
|
11
|
+
"pi install <source>",
|
|
12
|
+
"pi remove <source>",
|
|
13
|
+
"pi list",
|
|
14
|
+
"pi update [--extensions|<source>]",
|
|
13
15
|
]);
|
|
14
16
|
}
|
|
15
17
|
const PROFILE_WORDS = new Set(["pi", "sandbox"]);
|
|
@@ -25,26 +27,30 @@ export async function dispatchCli(arguments_, handlers, output, capabilities) {
|
|
|
25
27
|
return await handlers.version();
|
|
26
28
|
if (command.kind === "packages")
|
|
27
29
|
return await handlers.packages(command.request);
|
|
28
|
-
return await handlers.update(command.channel);
|
|
30
|
+
return await handlers.update(command.channel, command.target);
|
|
29
31
|
}
|
|
30
32
|
export function parseCliCommand(arguments_, capabilities) {
|
|
31
33
|
if (arguments_.length === 0)
|
|
32
34
|
return { kind: "launch", profileId: "a1" };
|
|
33
35
|
const [command, ...rest] = arguments_;
|
|
34
|
-
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") {
|
|
35
43
|
return withoutArguments(rest, { kind: "launch", profileId: command });
|
|
36
44
|
}
|
|
37
45
|
if (command === "version")
|
|
38
46
|
return withoutArguments(rest, { kind: "version" });
|
|
39
|
-
if (command
|
|
40
|
-
return
|
|
47
|
+
if (command !== undefined && command.startsWith("update:"))
|
|
48
|
+
return parseColonUpdate(command.slice("update:".length), rest);
|
|
41
49
|
if (command === "update")
|
|
42
50
|
return parseUpdate(rest);
|
|
43
|
-
if (command === "install" || command === "remove" || command === "uninstall") {
|
|
44
|
-
return
|
|
51
|
+
if (command === "install" || command === "remove" || command === "uninstall" || command === "list") {
|
|
52
|
+
return packageNamespaceRejection(command, rest.join(" ") || undefined);
|
|
45
53
|
}
|
|
46
|
-
if (command === "list")
|
|
47
|
-
return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
|
|
48
54
|
if (command === "ui")
|
|
49
55
|
return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
|
|
50
56
|
if (command === "agent")
|
|
@@ -52,10 +58,27 @@ export function parseCliCommand(arguments_, capabilities) {
|
|
|
52
58
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown command: ${command ?? ""}`) };
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
61
|
+
* What follows the colon says which build to move to. `next` is the newest
|
|
62
|
+
* preview; anything else names one outright, by the commit it was built from or by
|
|
63
|
+
* its full version — a preview is published as `<version>-dev.<commit>`, so the
|
|
64
|
+
* commit alone is enough to find it.
|
|
65
|
+
*/
|
|
66
|
+
function parseColonUpdate(suffix, rest) {
|
|
67
|
+
if (rest.length > 0)
|
|
68
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic("update takes what to move to after the colon, and nothing else.") };
|
|
69
|
+
if (suffix === "next")
|
|
70
|
+
return { kind: "update", channel: "next" };
|
|
71
|
+
if (suffix.length === 0)
|
|
72
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`update: needs a preview after the colon, as in ${PRODUCT_TEXT.commandName} update:next.`) };
|
|
73
|
+
if (!/^[0-9a-z][0-9a-z.+-]*$/i.test(suffix)) {
|
|
74
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unusable preview: ${suffix}`) };
|
|
75
|
+
}
|
|
76
|
+
return { kind: "update", channel: "next", target: suffix };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
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.
|
|
59
82
|
*/
|
|
60
83
|
function parseUpdate(rest) {
|
|
61
84
|
if (rest.length === 0)
|
|
@@ -71,18 +94,42 @@ function parseUpdate(rest) {
|
|
|
71
94
|
message: PRODUCT_TEXT.diagnostic(`pins the Pi version it was certified against; run ${PRODUCT_TEXT.commandName} update to move ${PRODUCT_TEXT.displayName} itself.`),
|
|
72
95
|
};
|
|
73
96
|
}
|
|
74
|
-
// A release channel is spelled with the colon. Taking the bare word as a package
|
|
75
|
-
// source would turn a near miss into a confident search for a package nobody has.
|
|
76
97
|
if (target === "next" || target === "stable") {
|
|
77
98
|
const form = target === "next" ? `${PRODUCT_TEXT.commandName} update:next` : `${PRODUCT_TEXT.commandName} update`;
|
|
78
99
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`selects a release channel with a colon; run ${form}.`) };
|
|
79
100
|
}
|
|
80
|
-
if (target === "--extensions")
|
|
81
|
-
return { kind: "packages", request: { verb: "update", source: null } };
|
|
82
101
|
if (target === "--models")
|
|
83
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
|
+
}
|
|
84
131
|
if (target === undefined || target.startsWith("-"))
|
|
85
|
-
return unknownOption(target ?? "", "update");
|
|
132
|
+
return unknownOption(target ?? "", "pi update");
|
|
86
133
|
if (PROFILE_WORDS.has(target))
|
|
87
134
|
return profileRejection("update");
|
|
88
135
|
return { kind: "packages", request: { verb: "update", source: target } };
|
|
@@ -110,6 +157,13 @@ function withoutArguments(rest, command) {
|
|
|
110
157
|
return profileRejection("list");
|
|
111
158
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic("commands do not accept additional arguments.") };
|
|
112
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
|
+
}
|
|
113
167
|
function profileRejection(verb) {
|
|
114
168
|
return {
|
|
115
169
|
kind: "error",
|
|
@@ -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 { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
6
|
+
export { applyConfiguredPiTheme, getAvailablePiThemes, onPiThemeChange } 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 { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
|
|
6
|
+
export { applyConfiguredPiTheme, getAvailablePiThemes, onPiThemeChange } 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";
|
|
@@ -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;
|