@cjhyy/code-shell-core 0.8.8 → 0.8.10
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/dist/automation/scheduler.js +49 -0
- package/dist/automation/store.d.ts +1 -1
- package/dist/automation/store.js +184 -10
- package/dist/cli/agent-server-stdio.js +7 -0
- package/dist/credentials/store.d.ts +14 -0
- package/dist/credentials/store.js +245 -42
- package/dist/engine/engine.js +81 -27
- package/dist/engine/file-history-hook.js +24 -5
- package/dist/engine/run-finalize.js +8 -6
- package/dist/engine/run-session-open.js +1 -1
- package/dist/engine/run-setup.d.ts +4 -0
- package/dist/engine/run-setup.js +5 -1
- package/dist/engine/run-tooling.js +7 -1
- package/dist/engine/run-types.d.ts +33 -0
- package/dist/engine/run-types.js +21 -0
- package/dist/engine/run-workspace.js +11 -5
- package/dist/engine/turn-loop.js +9 -8
- package/dist/goal/lifecycle.d.ts +2 -0
- package/dist/goal/lifecycle.js +56 -33
- package/dist/index.d.ts +2 -3
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.js +1 -0
- package/dist/index.js +2 -2
- package/dist/links/cli.d.ts +2 -0
- package/dist/links/cli.js +11 -4
- package/dist/model-catalog/index.js +19 -4
- package/dist/model-catalog/save-entry.js +122 -61
- package/dist/model-catalog/types.js +27 -23
- package/dist/panel-apps/installer.js +27 -14
- package/dist/panel-apps/manifest.d.ts +11 -11
- package/dist/panel-apps/manifest.js +3 -1
- package/dist/panel-apps/registry.js +60 -12
- package/dist/plugins/installedPlugins.d.ts +4 -0
- package/dist/plugins/installedPlugins.js +70 -30
- package/dist/plugins/installer/types.d.ts +12 -12
- package/dist/plugins/installer/update.js +37 -38
- package/dist/plugins/knownMarketplaces.d.ts +7 -3
- package/dist/plugins/knownMarketplaces.js +127 -23
- package/dist/plugins/pluginCatalog.js +18 -4
- package/dist/plugins/pluginHookApproval.js +56 -60
- package/dist/plugins/pluginMcpApproval.js +50 -52
- package/dist/profile/catalog-store.js +39 -4
- package/dist/profile/catalog.js +55 -15
- package/dist/profile/store.js +51 -21
- package/dist/prompt/composer.d.ts +8 -0
- package/dist/prompt/composer.js +8 -0
- package/dist/protocol/chat-session-manager.d.ts +9 -0
- package/dist/protocol/chat-session-manager.js +22 -0
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/server.d.ts +2 -0
- package/dist/protocol/server.js +112 -29
- package/dist/protocol/types.d.ts +20 -0
- package/dist/run/FileRunStore.d.ts +2 -0
- package/dist/run/FileRunStore.js +153 -18
- package/dist/run/Heartbeat.js +63 -4
- package/dist/services/auto-dream.js +39 -17
- package/dist/services/session-memory.js +107 -8
- package/dist/session/file-history.d.ts +63 -2
- package/dist/session/file-history.js +593 -86
- package/dist/session/session-manager.d.ts +4 -1
- package/dist/session/session-manager.js +94 -47
- package/dist/session/transcript.js +33 -3
- package/dist/session/undo-target.d.ts +15 -6
- package/dist/session/undo-target.js +26 -9
- package/dist/settings/manager.d.ts +22 -3
- package/dist/settings/manager.js +185 -50
- package/dist/settings/schema.d.ts +3 -3
- package/dist/sources/adapters/local-files.js +49 -4
- package/dist/sources/catalog.js +64 -18
- package/dist/sources/types.d.ts +3 -3
- package/dist/sources/types.js +7 -4
- package/dist/themes/installer.js +192 -28
- package/dist/tool-system/builtin/add-marketplace.js +21 -1
- package/dist/tool-system/builtin/cron.d.ts +2 -1
- package/dist/tool-system/builtin/cron.js +20 -6
- package/dist/tool-system/builtin/index.js +48 -1
- package/dist/tool-system/builtin/install-capability.d.ts +52 -0
- package/dist/tool-system/builtin/install-capability.js +1057 -0
- package/dist/tool-system/builtin/skill.js +3 -1
- package/dist/tool-system/executor.js +1 -0
- package/dist/tool-system/path-policy.js +25 -33
- package/dist/tool-system/registry.js +5 -0
- package/dist/utils/file-mutex.d.ts +2 -0
- package/dist/utils/file-mutex.js +29 -4
- package/package.json +2 -1
|
@@ -6,10 +6,91 @@
|
|
|
6
6
|
* here (those go through the user's own Edit, by design).
|
|
7
7
|
* See docs/superpowers/specs/2026-06-15-unified-model-catalog-design.md §7.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
10
11
|
import { dirname } from "node:path";
|
|
11
12
|
import { catalogEntrySchema, userCatalogFileSchema } from "./types.js";
|
|
12
13
|
import { upsertCatalogEntry } from "./upsert.js";
|
|
14
|
+
import { acquireFileLock } from "../utils/file-mutex.js";
|
|
15
|
+
const MAX_USER_CATALOG_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
const BACKUP_STAMP_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
17
|
+
function pathEntry(path) {
|
|
18
|
+
try {
|
|
19
|
+
return lstatSync(path);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === "ENOENT")
|
|
23
|
+
return undefined;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readExistingCatalog(path) {
|
|
28
|
+
const metadata = pathEntry(path);
|
|
29
|
+
if (!metadata)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (metadata.isSymbolicLink() ||
|
|
32
|
+
!metadata.isFile() ||
|
|
33
|
+
metadata.size > MAX_USER_CATALOG_BYTES) {
|
|
34
|
+
throw new Error("catalog must be a bounded regular file");
|
|
35
|
+
}
|
|
36
|
+
const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
37
|
+
try {
|
|
38
|
+
const opened = fstatSync(descriptor);
|
|
39
|
+
if (!opened.isFile() || opened.size > MAX_USER_CATALOG_BYTES) {
|
|
40
|
+
throw new Error("catalog must be a bounded regular file");
|
|
41
|
+
}
|
|
42
|
+
const raw = readFileSync(descriptor, "utf8");
|
|
43
|
+
try {
|
|
44
|
+
const parsed = userCatalogFileSchema.safeParse(JSON.parse(raw));
|
|
45
|
+
return { raw, entries: parsed.success ? parsed.data : [] };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { raw, entries: [] };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
closeSync(descriptor);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeBackup(path, raw) {
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(path, raw, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function writeCatalogAtomic(path, entries) {
|
|
65
|
+
const serialized = `${JSON.stringify(userCatalogFileSchema.parse(entries), null, 2)}\n`;
|
|
66
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_USER_CATALOG_BYTES) {
|
|
67
|
+
throw new Error("catalog exceeds its size limit");
|
|
68
|
+
}
|
|
69
|
+
const parent = dirname(path);
|
|
70
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
71
|
+
const parentInfo = lstatSync(parent);
|
|
72
|
+
if (parentInfo.isSymbolicLink() || !parentInfo.isDirectory()) {
|
|
73
|
+
throw new Error("catalog parent must be a real directory");
|
|
74
|
+
}
|
|
75
|
+
const target = pathEntry(path);
|
|
76
|
+
if (target && (target.isSymbolicLink() || !target.isFile())) {
|
|
77
|
+
throw new Error("catalog target must be a regular file");
|
|
78
|
+
}
|
|
79
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
80
|
+
try {
|
|
81
|
+
writeFileSync(temporary, serialized, {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
mode: 0o600,
|
|
84
|
+
flag: "wx",
|
|
85
|
+
});
|
|
86
|
+
renameSync(temporary, path);
|
|
87
|
+
if (process.platform !== "win32")
|
|
88
|
+
chmodSync(path, 0o600);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
rmSync(temporary, { force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
13
94
|
/**
|
|
14
95
|
* @param stamp caller-supplied unique suffix for the backup filename. Pass a
|
|
15
96
|
* timestamp/counter from the caller — core forbids Date.now() in some paths
|
|
@@ -22,47 +103,27 @@ export function saveCatalogEntry(entry, opts) {
|
|
|
22
103
|
return { ok: false, error: `invalid catalog entry: ${parsed.error.issues.map((i) => i.message).join("; ")}` };
|
|
23
104
|
}
|
|
24
105
|
const valid = parsed.data;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
if (existsSync(opts.path)) {
|
|
29
|
-
backup = `${opts.path}.bak-${opts.stamp}`;
|
|
30
|
-
try {
|
|
31
|
-
copyFileSync(opts.path, backup);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
backup = undefined; // best-effort; don't abort the write on backup failure
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const raw = JSON.parse(readFileSync(opts.path, "utf-8"));
|
|
38
|
-
const safe = userCatalogFileSchema.safeParse(raw);
|
|
39
|
-
current = safe.success ? safe.data : [];
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
current = []; // corrupt → start fresh (original preserved in backup)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const action = current.some((e) => e.id === valid.id) ? "updated" : "added";
|
|
46
|
-
const next = upsertCatalogEntry(current, valid);
|
|
47
|
-
// Ensure the parent dir exists — a first-ever catalog write on a machine
|
|
48
|
-
// whose ~/.code-shell hasn't been created yet would otherwise throw ENOENT
|
|
49
|
-
// (the agent-facing tool wants a clean {ok:false} or a real write, not a crash).
|
|
106
|
+
if (!BACKUP_STAMP_RE.test(opts.stamp))
|
|
107
|
+
return { ok: false, error: "invalid backup stamp" };
|
|
108
|
+
let release;
|
|
50
109
|
try {
|
|
51
|
-
|
|
110
|
+
release = acquireFileLock(opts.path);
|
|
111
|
+
const existing = readExistingCatalog(opts.path);
|
|
112
|
+
let backup = existing ? `${opts.path}.bak-${opts.stamp}` : undefined;
|
|
113
|
+
if (existing && !writeBackup(backup, existing.raw))
|
|
114
|
+
backup = undefined;
|
|
115
|
+
const action = existing?.entries.some((e) => e.id === valid.id)
|
|
116
|
+
? "updated"
|
|
117
|
+
: "added";
|
|
118
|
+
writeCatalogAtomic(opts.path, upsertCatalogEntry(existing?.entries ?? [], valid));
|
|
119
|
+
return { ok: true, action, backup };
|
|
52
120
|
}
|
|
53
121
|
catch (e) {
|
|
54
|
-
return { ok: false, error: `could not
|
|
122
|
+
return { ok: false, error: `could not write catalog: ${e instanceof Error ? e.message : String(e)}` };
|
|
55
123
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
// IO error (perms / disk full / bad path): return a clean {ok:false} with the
|
|
61
|
-
// backup filename preserved, never let the throw escape past the tool's
|
|
62
|
-
// expected result shape (the original file is intact — we only upsert-wrote).
|
|
63
|
-
return { ok: false, error: `could not write catalog: ${e instanceof Error ? e.message : String(e)}`, backup };
|
|
124
|
+
finally {
|
|
125
|
+
release?.();
|
|
64
126
|
}
|
|
65
|
-
return { ok: true, action, backup };
|
|
66
127
|
}
|
|
67
128
|
/**
|
|
68
129
|
* Remove the entry with `id` from the user catalog file. Mirrors saveCatalogEntry's
|
|
@@ -72,33 +133,33 @@ export function saveCatalogEntry(entry, opts) {
|
|
|
72
133
|
* built-in version ("reset" semantics).
|
|
73
134
|
*/
|
|
74
135
|
export function deleteUserCatalogEntry(id, opts) {
|
|
75
|
-
if (!
|
|
76
|
-
return { ok:
|
|
77
|
-
let backup = `${opts.path}.bak-${opts.stamp}`;
|
|
78
|
-
try {
|
|
79
|
-
copyFileSync(opts.path, backup);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
backup = undefined;
|
|
136
|
+
if (!BACKUP_STAMP_RE.test(opts.stamp)) {
|
|
137
|
+
return { ok: false, removed: false, error: "invalid backup stamp" };
|
|
83
138
|
}
|
|
84
|
-
let
|
|
139
|
+
let release;
|
|
85
140
|
try {
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
141
|
+
release = acquireFileLock(opts.path);
|
|
142
|
+
const existing = readExistingCatalog(opts.path);
|
|
143
|
+
if (!existing)
|
|
144
|
+
return { ok: true, removed: false };
|
|
145
|
+
let backup = `${opts.path}.bak-${opts.stamp}`;
|
|
146
|
+
if (!writeBackup(backup, existing.raw))
|
|
147
|
+
backup = undefined;
|
|
148
|
+
const next = existing.entries.filter((e) => e.id !== id);
|
|
149
|
+
const removed = next.length !== existing.entries.length;
|
|
150
|
+
if (!removed)
|
|
151
|
+
return { ok: true, removed: false, backup };
|
|
152
|
+
writeCatalogAtomic(opts.path, next);
|
|
153
|
+
return { ok: true, removed: true, backup };
|
|
99
154
|
}
|
|
100
155
|
catch (e) {
|
|
101
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
removed: false,
|
|
159
|
+
error: `could not write catalog: ${e instanceof Error ? e.message : String(e)}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
release?.();
|
|
102
164
|
}
|
|
103
|
-
return { ok: true, removed: true, backup };
|
|
104
165
|
}
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* ("openai"/"google"/"fal"). `shape` is documentation/future only.
|
|
13
13
|
*/
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
const id = z.string().min(1).max(128);
|
|
16
|
+
const shortText = z.string().max(256);
|
|
17
|
+
const longText = z.string().max(32_768);
|
|
18
|
+
const boundedNumber = z.number().finite().min(0).max(1_000_000_000);
|
|
15
19
|
/**
|
|
16
20
|
* How the param maps onto the outgoing request body. `field` is the request
|
|
17
21
|
* field this param lands on — same param name can land differently per
|
|
@@ -20,7 +24,7 @@ import { z } from "zod";
|
|
|
20
24
|
* in `if (kind === ...)` branches in the engine. Minimal v1: field only.
|
|
21
25
|
*/
|
|
22
26
|
export const wireSpecSchema = z.object({
|
|
23
|
-
field:
|
|
27
|
+
field: id,
|
|
24
28
|
});
|
|
25
29
|
/**
|
|
26
30
|
* One **generic** declarative param — no special-casing per param. reasoning
|
|
@@ -32,18 +36,18 @@ export const wireSpecSchema = z.object({
|
|
|
32
36
|
*/
|
|
33
37
|
export const paramSpecSchema = z.object({
|
|
34
38
|
/** Logical name, e.g. "reasoning" / "size" / "quality" / "temperature". */
|
|
35
|
-
name:
|
|
39
|
+
name: id,
|
|
36
40
|
/** UI control label (falls back to `name`). */
|
|
37
|
-
label:
|
|
41
|
+
label: shortText.optional(),
|
|
38
42
|
control: z.enum(["enum", "number", "toggle", "text"]),
|
|
39
43
|
/** control=enum allowed values, e.g. ["low","medium","high","xhigh"]. */
|
|
40
|
-
options: z.array(
|
|
44
|
+
options: z.array(shortText).max(64).optional(),
|
|
41
45
|
/** control=number bounds. */
|
|
42
|
-
min:
|
|
43
|
-
max:
|
|
44
|
-
default: z.union([
|
|
46
|
+
min: boundedNumber.optional(),
|
|
47
|
+
max: boundedNumber.optional(),
|
|
48
|
+
default: z.union([shortText, boundedNumber, z.boolean()]).optional(),
|
|
45
49
|
/** Natural-language usage note → injected into the tool description. */
|
|
46
|
-
doc: z.string().optional(),
|
|
50
|
+
doc: z.string().max(4_096).optional(),
|
|
47
51
|
/** How this param lands on the request body. */
|
|
48
52
|
wire: wireSpecSchema.optional(),
|
|
49
53
|
});
|
|
@@ -53,17 +57,17 @@ export const paramSpecSchema = z.object({
|
|
|
53
57
|
* can expose different params, since gateways normalize differently).
|
|
54
58
|
*/
|
|
55
59
|
export const modelPresetSchema = z.object({
|
|
56
|
-
value:
|
|
57
|
-
label:
|
|
58
|
-
maxContextTokens:
|
|
59
|
-
maxOutputTokens:
|
|
60
|
+
value: id,
|
|
61
|
+
label: shortText.optional(),
|
|
62
|
+
maxContextTokens: boundedNumber.optional(),
|
|
63
|
+
maxOutputTokens: boundedNumber.optional(),
|
|
60
64
|
supportsVision: z.boolean().optional(),
|
|
61
65
|
/** Params this model supports; absent → no adjustable knobs. */
|
|
62
|
-
params: z.array(paramSpecSchema).optional(),
|
|
66
|
+
params: z.array(paramSpecSchema).max(64).optional(),
|
|
63
67
|
});
|
|
64
68
|
export const catalogEntrySchema = z.object({
|
|
65
69
|
/** Template id, e.g. "openai" / "openai-images" / "fal-video". */
|
|
66
|
-
id
|
|
70
|
+
id,
|
|
67
71
|
/** Which 连接 page group this lands in. (audio = speech-to-text / voice input.) */
|
|
68
72
|
tag: z.enum(["text", "image", "video", "audio"]),
|
|
69
73
|
/**
|
|
@@ -71,15 +75,15 @@ export const catalogEntrySchema = z.object({
|
|
|
71
75
|
* runtime adapter for image/video. OpenRouter is `openrouter` even though its
|
|
72
76
|
* text protocol is `openai-compat`.
|
|
73
77
|
*/
|
|
74
|
-
adapterKind:
|
|
78
|
+
adapterKind: id,
|
|
75
79
|
/** LLM client protocol (text entries). */
|
|
76
80
|
protocol: z.enum(["openai-compat", "anthropic-style"]).optional(),
|
|
77
81
|
/** HTTP shape — documentation/future only; runtime dispatches on adapterKind. */
|
|
78
82
|
shape: z.enum(["generic-sync", "fal-queue"]).optional(),
|
|
79
|
-
displayName: z.string(),
|
|
80
|
-
description: z.string(),
|
|
81
|
-
defaultBaseUrl: z.string(),
|
|
82
|
-
defaultModel:
|
|
83
|
+
displayName: z.string().min(1).max(256),
|
|
84
|
+
description: z.string().max(4_096),
|
|
85
|
+
defaultBaseUrl: z.string().max(4_096),
|
|
86
|
+
defaultModel: shortText.optional(),
|
|
83
87
|
/** Whether this provider needs an API key (ollama/local = false). */
|
|
84
88
|
needsKey: z.boolean().optional(),
|
|
85
89
|
/**
|
|
@@ -89,8 +93,8 @@ export const catalogEntrySchema = z.object({
|
|
|
89
93
|
* `value`, so future built-in models remain visible.
|
|
90
94
|
*/
|
|
91
95
|
modelPresetsMode: z.enum(["replace", "merge"]).optional(),
|
|
92
|
-
modelPresets: z.array(modelPresetSchema).optional(),
|
|
93
|
-
signupUrl: z.string().optional(),
|
|
96
|
+
modelPresets: z.array(modelPresetSchema).max(512).optional(),
|
|
97
|
+
signupUrl: z.string().max(4_096).optional(),
|
|
94
98
|
/** Whether the 连接 card offers a "测试" button (image=true, video=false). */
|
|
95
99
|
test: z.boolean().optional(),
|
|
96
100
|
/**
|
|
@@ -98,7 +102,7 @@ export const catalogEntrySchema = z.object({
|
|
|
98
102
|
* the agent via the dynamic GenerateImage/GenerateVideo tool description so it
|
|
99
103
|
* knows what a configured model accepts (different models differ).
|
|
100
104
|
*/
|
|
101
|
-
paramsDoc:
|
|
105
|
+
paramsDoc: longText.optional(),
|
|
102
106
|
});
|
|
103
107
|
/** A user-catalog file is just an array of entries (zod-validated on load). */
|
|
104
|
-
export const userCatalogFileSchema = z.array(catalogEntrySchema);
|
|
108
|
+
export const userCatalogFileSchema = z.array(catalogEntrySchema).max(256);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { cp, lstat, mkdir, mkdtemp,
|
|
2
|
+
import { constants, existsSync } from "node:fs";
|
|
3
|
+
import { cp, lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { basename, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { extractZip, extractZipSubdirectory } from "../plugins/installer/unzip.js";
|
|
@@ -279,22 +279,34 @@ async function walkBoundedTree(root, directory, depth, budget, files, directorie
|
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
async function readManifest(sourceRoot) {
|
|
282
|
-
const file = join(sourceRoot, PANEL_APP_MANIFEST_FILE);
|
|
283
|
-
let info;
|
|
284
282
|
try {
|
|
285
|
-
|
|
283
|
+
const raw = await readBoundedPackageFile(sourceRoot, PANEL_APP_MANIFEST_FILE, MAX_MANIFEST_BYTES);
|
|
284
|
+
return PanelAppManifest.parse(JSON.parse(raw.toString("utf8")));
|
|
286
285
|
}
|
|
287
|
-
catch {
|
|
288
|
-
throw new PanelAppInstallError(`
|
|
286
|
+
catch (error) {
|
|
287
|
+
throw new PanelAppInstallError(`invalid Panel App manifest: ${error instanceof Error ? error.message : String(error)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function readBoundedPackageFile(root, relativePath, maxBytes) {
|
|
291
|
+
const candidate = join(root, ...relativePath.split("/"));
|
|
292
|
+
const metadata = await lstat(candidate);
|
|
293
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > maxBytes) {
|
|
294
|
+
throw new PanelAppInstallError(`Panel App file is not a bounded regular file: ${relativePath}`);
|
|
289
295
|
}
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
const physical = await realpath(candidate);
|
|
297
|
+
if (physical !== root && !physical.startsWith(`${root}${sep}`)) {
|
|
298
|
+
throw new PanelAppInstallError(`Panel App file escapes its package: ${relativePath}`);
|
|
292
299
|
}
|
|
300
|
+
const handle = await open(candidate, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
293
301
|
try {
|
|
294
|
-
|
|
302
|
+
const opened = await handle.stat();
|
|
303
|
+
if (!opened.isFile() || opened.size > maxBytes) {
|
|
304
|
+
throw new PanelAppInstallError(`Panel App file is not a bounded regular file: ${relativePath}`);
|
|
305
|
+
}
|
|
306
|
+
return await handle.readFile();
|
|
295
307
|
}
|
|
296
|
-
|
|
297
|
-
|
|
308
|
+
finally {
|
|
309
|
+
await handle.close();
|
|
298
310
|
}
|
|
299
311
|
}
|
|
300
312
|
async function inspectPanelAppSource(sourceRoot) {
|
|
@@ -323,10 +335,11 @@ async function inspectPanelAppSource(sourceRoot) {
|
|
|
323
335
|
if (!files.includes(skillEntry)) {
|
|
324
336
|
throw new PanelAppInstallError(`declared Panel App skill is missing: ${skillEntry}`);
|
|
325
337
|
}
|
|
326
|
-
const skillInfo = await
|
|
338
|
+
const skillInfo = await lstat(join(root, ...skillEntry.split("/")));
|
|
327
339
|
if (!skillInfo.isFile() || skillInfo.size > MAX_AGENT_SKILL_BYTES) {
|
|
328
340
|
throw new PanelAppInstallError(`declared Panel App skill must be a file no larger than 256 KiB: ${skillEntry}`);
|
|
329
341
|
}
|
|
342
|
+
await readBoundedPackageFile(root, skillEntry, MAX_AGENT_SKILL_BYTES);
|
|
330
343
|
}
|
|
331
344
|
for (const file of files) {
|
|
332
345
|
if (file === PANEL_APP_MANIFEST_FILE || file === PANEL_APP_META_FILE)
|
|
@@ -352,7 +365,7 @@ async function inspectPanelAppSource(sourceRoot) {
|
|
|
352
365
|
hash
|
|
353
366
|
.update(file)
|
|
354
367
|
.update("\0")
|
|
355
|
-
.update(await
|
|
368
|
+
.update(await readBoundedPackageFile(root, file, file === PANEL_APP_MANIFEST_FILE ? MAX_MANIFEST_BYTES : MAX_FILE_BYTES));
|
|
356
369
|
}
|
|
357
370
|
return { manifest, files, digest: hash.digest("hex") };
|
|
358
371
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const PANEL_APP_MANIFEST_FILE = ".codeshell-panel/panel.json";
|
|
3
|
-
export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"];
|
|
3
|
+
export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "agent.task", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage", "process"];
|
|
4
4
|
export declare const PANEL_APP_ICONS: readonly ["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"];
|
|
5
5
|
export declare const PanelAppAgentTool: z.ZodEffects<z.ZodObject<{
|
|
6
6
|
name: z.ZodString;
|
|
@@ -116,12 +116,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
116
116
|
icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
|
|
117
117
|
placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
|
|
118
118
|
singleton: z.ZodDefault<z.ZodBoolean>;
|
|
119
|
-
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
119
|
+
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "agent.task", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage", "process"]>, "many">>;
|
|
120
120
|
schemaVersion: z.ZodLiteral<1>;
|
|
121
121
|
}, "strict", z.ZodTypeAny, {
|
|
122
122
|
id: string;
|
|
123
123
|
version: string;
|
|
124
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
124
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
|
|
125
125
|
entry: string;
|
|
126
126
|
title: {
|
|
127
127
|
default: string;
|
|
@@ -144,7 +144,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
144
144
|
};
|
|
145
145
|
schemaVersion: 1;
|
|
146
146
|
description?: string | undefined;
|
|
147
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
147
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
|
|
148
148
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
149
149
|
placement?: "right-dock" | undefined;
|
|
150
150
|
singleton?: boolean | undefined;
|
|
@@ -230,12 +230,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
230
230
|
icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
|
|
231
231
|
placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
|
|
232
232
|
singleton: z.ZodDefault<z.ZodBoolean>;
|
|
233
|
-
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
233
|
+
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "agent.task", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage", "process"]>, "many">>;
|
|
234
234
|
schemaVersion: z.ZodLiteral<2>;
|
|
235
235
|
}, "strict", z.ZodTypeAny, {
|
|
236
236
|
id: string;
|
|
237
237
|
version: string;
|
|
238
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
238
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
|
|
239
239
|
entry: string;
|
|
240
240
|
title: {
|
|
241
241
|
default: string;
|
|
@@ -276,14 +276,14 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
276
276
|
}[] | undefined;
|
|
277
277
|
} | undefined;
|
|
278
278
|
description?: string | undefined;
|
|
279
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
279
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
|
|
280
280
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
281
281
|
placement?: "right-dock" | undefined;
|
|
282
282
|
singleton?: boolean | undefined;
|
|
283
283
|
}>]>, {
|
|
284
284
|
id: string;
|
|
285
285
|
version: string;
|
|
286
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
286
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
|
|
287
287
|
entry: string;
|
|
288
288
|
title: {
|
|
289
289
|
default: string;
|
|
@@ -298,7 +298,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
298
298
|
} | {
|
|
299
299
|
id: string;
|
|
300
300
|
version: string;
|
|
301
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
301
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
|
|
302
302
|
entry: string;
|
|
303
303
|
title: {
|
|
304
304
|
default: string;
|
|
@@ -330,7 +330,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
330
330
|
};
|
|
331
331
|
schemaVersion: 1;
|
|
332
332
|
description?: string | undefined;
|
|
333
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
333
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
|
|
334
334
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
335
335
|
placement?: "right-dock" | undefined;
|
|
336
336
|
singleton?: boolean | undefined;
|
|
@@ -354,7 +354,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
354
354
|
}[] | undefined;
|
|
355
355
|
} | undefined;
|
|
356
356
|
description?: string | undefined;
|
|
357
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
357
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
|
|
358
358
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
359
359
|
placement?: "right-dock" | undefined;
|
|
360
360
|
singleton?: boolean | undefined;
|
|
@@ -7,6 +7,7 @@ export const PANEL_APP_PERMISSIONS = [
|
|
|
7
7
|
"storage",
|
|
8
8
|
"external.open",
|
|
9
9
|
"agent.submitPrompt",
|
|
10
|
+
"agent.task",
|
|
10
11
|
"workspace.info",
|
|
11
12
|
"workspace.read",
|
|
12
13
|
"workspace.write",
|
|
@@ -14,6 +15,7 @@ export const PANEL_APP_PERMISSIONS = [
|
|
|
14
15
|
"audio.transcribe",
|
|
15
16
|
"credentials.cookies",
|
|
16
17
|
"automations.manage",
|
|
18
|
+
"process",
|
|
17
19
|
];
|
|
18
20
|
export const PANEL_APP_ICONS = [
|
|
19
21
|
"panel",
|
|
@@ -139,7 +141,7 @@ const PanelAppManifestFields = {
|
|
|
139
141
|
icon: z.enum(PANEL_APP_ICONS).default("panel"),
|
|
140
142
|
placement: z.literal("right-dock").default("right-dock"),
|
|
141
143
|
singleton: z.boolean().default(true),
|
|
142
|
-
permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(
|
|
144
|
+
permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(16).default([]),
|
|
143
145
|
};
|
|
144
146
|
/**
|
|
145
147
|
* Schema v2 keeps one installable Panel App identity while allowing it to
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { mkdir,
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { panelAppsRegistryPath } from "./paths.js";
|
|
@@ -28,47 +28,95 @@ const Registry = z
|
|
|
28
28
|
apps: z.array(RegistryEntry).max(1_024),
|
|
29
29
|
})
|
|
30
30
|
.strict();
|
|
31
|
-
|
|
31
|
+
const MAX_PANEL_APP_REGISTRY_BYTES = 4 * 1024 * 1024;
|
|
32
|
+
async function registryEntry(path) {
|
|
33
|
+
try {
|
|
34
|
+
return await lstat(path);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === "ENOENT")
|
|
38
|
+
return null;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function checkedRegistryDirectory() {
|
|
43
|
+
const directory = dirname(panelAppsRegistryPath());
|
|
44
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
45
|
+
const metadata = await lstat(directory);
|
|
46
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
47
|
+
throw new Error("Panel App registry directory must be a real directory");
|
|
48
|
+
}
|
|
49
|
+
return directory;
|
|
50
|
+
}
|
|
51
|
+
async function readRegistryUnlocked(strict = false) {
|
|
32
52
|
const path = panelAppsRegistryPath();
|
|
33
|
-
|
|
34
|
-
return [];
|
|
53
|
+
let handle;
|
|
35
54
|
try {
|
|
36
|
-
|
|
55
|
+
const metadata = await registryEntry(path);
|
|
56
|
+
if (!metadata)
|
|
57
|
+
return [];
|
|
58
|
+
if (metadata.isSymbolicLink() ||
|
|
59
|
+
!metadata.isFile() ||
|
|
60
|
+
metadata.size > MAX_PANEL_APP_REGISTRY_BYTES) {
|
|
61
|
+
throw new Error("Panel App registry must be a bounded regular file");
|
|
62
|
+
}
|
|
63
|
+
handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
64
|
+
const opened = await handle.stat();
|
|
65
|
+
if (!opened.isFile() || opened.size > MAX_PANEL_APP_REGISTRY_BYTES) {
|
|
66
|
+
throw new Error("Panel App registry must be a bounded regular file");
|
|
67
|
+
}
|
|
68
|
+
return Registry.parse(JSON.parse(await handle.readFile("utf8"))).apps;
|
|
37
69
|
}
|
|
38
|
-
catch {
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (strict) {
|
|
72
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
73
|
+
throw new Error(`Panel App registry is corrupt${detail}`, { cause: error });
|
|
74
|
+
}
|
|
39
75
|
return [];
|
|
40
76
|
}
|
|
77
|
+
finally {
|
|
78
|
+
await handle?.close().catch(() => undefined);
|
|
79
|
+
}
|
|
41
80
|
}
|
|
42
81
|
async function writeRegistryUnlocked(apps) {
|
|
43
82
|
const path = panelAppsRegistryPath();
|
|
44
|
-
await
|
|
83
|
+
await checkedRegistryDirectory();
|
|
84
|
+
const target = await registryEntry(path);
|
|
85
|
+
if (target && (target.isSymbolicLink() || !target.isFile())) {
|
|
86
|
+
throw new Error("Panel App registry target must be a regular file");
|
|
87
|
+
}
|
|
45
88
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
46
89
|
try {
|
|
47
90
|
const registry = Registry.parse({
|
|
48
91
|
version: 1,
|
|
49
92
|
apps: [...apps].sort((left, right) => left.id.localeCompare(right.id)),
|
|
50
93
|
});
|
|
51
|
-
|
|
94
|
+
const serialized = `${JSON.stringify(registry, null, 2)}\n`;
|
|
95
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_PANEL_APP_REGISTRY_BYTES) {
|
|
96
|
+
throw new Error("Panel App registry is too large");
|
|
97
|
+
}
|
|
98
|
+
await writeFile(tmp, serialized, {
|
|
52
99
|
encoding: "utf-8",
|
|
53
100
|
flag: "wx",
|
|
54
101
|
mode: 0o600,
|
|
55
102
|
});
|
|
56
103
|
await rename(tmp, path);
|
|
104
|
+
if (process.platform !== "win32")
|
|
105
|
+
await chmod(path, 0o600);
|
|
57
106
|
}
|
|
58
107
|
finally {
|
|
59
108
|
await rm(tmp, { force: true });
|
|
60
109
|
}
|
|
61
110
|
}
|
|
62
111
|
async function mutateRegistry(mutation) {
|
|
63
|
-
const directory =
|
|
64
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
112
|
+
const directory = await checkedRegistryDirectory();
|
|
65
113
|
const release = await lock(directory, {
|
|
66
114
|
realpath: true,
|
|
67
115
|
stale: 10_000,
|
|
68
116
|
retries: { retries: 8, minTimeout: 10, maxTimeout: 120, factor: 1.5 },
|
|
69
117
|
});
|
|
70
118
|
try {
|
|
71
|
-
const next = mutation(await readRegistryUnlocked());
|
|
119
|
+
const next = mutation(await readRegistryUnlocked(true));
|
|
72
120
|
await writeRegistryUnlocked(next.apps);
|
|
73
121
|
return next.result;
|
|
74
122
|
}
|
|
@@ -7,6 +7,10 @@ import type { InstalledPluginsV2, PluginInstallEntry } from "./types.js";
|
|
|
7
7
|
export declare function installedPluginsPath(): string;
|
|
8
8
|
export declare function readInstalledPlugins(): InstalledPluginsV2;
|
|
9
9
|
export declare function writeInstalledPlugins(data: InstalledPluginsV2): void;
|
|
10
|
+
export declare function mutateInstalledPlugins<R>(mutation: (current: InstalledPluginsV2) => {
|
|
11
|
+
value?: InstalledPluginsV2;
|
|
12
|
+
result?: R;
|
|
13
|
+
}): R | undefined;
|
|
10
14
|
/**
|
|
11
15
|
* Append an install entry for `<plugin>@<marketplace>`. Multiple entries
|
|
12
16
|
* for the same key are allowed (different scopes); MVP only writes scope:"user".
|