@xbzbing/dsh-git-panel 1.8.0 → 1.8.1
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.en.md +4 -0
- package/README.md +4 -0
- package/lib/client.js +126 -32
- package/lib/host/actions.js +44 -2
- package/lib/host/core.d.ts +8 -2
- package/lib/host/core.js +15 -6
- package/lib/host/index.d.ts +8 -2
- package/lib/host/index.js +52 -5
- package/lib/host/open-file.d.ts +16 -0
- package/lib/host/open-file.js +29 -0
- package/lib/host/queries.d.ts +8 -0
- package/lib/host/queries.js +1 -0
- package/lib/host/suggest.js +5 -1
- package/lib/host/types.d.ts +35 -2
- package/lib/host/types.js +2 -0
- package/package.json +1 -1
package/lib/host/actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, sep } from "node:path";
|
|
2
2
|
import { mapWorkspaceFailure, resolveWorkspace, runCommand, snapshotForSession } from "./core.js";
|
|
3
|
+
import { parseStatus } from "./parser.js";
|
|
3
4
|
import { isSafeBranchName, isSafePath, isSafeRemoteName, isSafeRev } from "./validate.js";
|
|
4
5
|
function withPaths(prefixes, paths) {
|
|
5
6
|
if (paths.length === 0) return { error: "invalid-path", message: "no paths given" };
|
|
@@ -20,6 +21,8 @@ function planAction(action, unborn) {
|
|
|
20
21
|
return unborn ? { argv: [["git", "rm", "--cached", "-r", "--", "."]] } : { argv: [["git", "restore", "--staged", "--", "."]] };
|
|
21
22
|
case "discard":
|
|
22
23
|
return withPaths([["git", "restore", "--"]], action.paths);
|
|
24
|
+
case "discard-all":
|
|
25
|
+
return { error: "invalid-path", message: "discard-all must be resolved to paths before planning" };
|
|
23
26
|
case "commit": {
|
|
24
27
|
const message = action.message.trim();
|
|
25
28
|
const amend = action.amend === true;
|
|
@@ -93,6 +96,14 @@ function isIndexBusy(outcome) {
|
|
|
93
96
|
if (!("run" in outcome) || outcome.run.exitCode === 0) return false;
|
|
94
97
|
return /index\.lock|Unable to create.*index|another git process/i.test(outcome.run.stderr + outcome.run.stdout);
|
|
95
98
|
}
|
|
99
|
+
function probeFailure(outcome) {
|
|
100
|
+
if ("failure" in outcome) {
|
|
101
|
+
return { ok: false, error: { code: "git-unavailable", message: outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure) } };
|
|
102
|
+
}
|
|
103
|
+
if (outcome.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
104
|
+
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
96
107
|
function scrubCredentials(text) {
|
|
97
108
|
return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/gi, "$1");
|
|
98
109
|
}
|
|
@@ -105,8 +116,8 @@ function classifyActionFailure(stdout, stderr, exitCode) {
|
|
|
105
116
|
if (/index\.lock|Unable to create.*index|another git process/i.test(combined)) {
|
|
106
117
|
return { code: "index-busy", message: err };
|
|
107
118
|
}
|
|
108
|
-
if (/nothing to commit|no changes added/i.test(combined)) {
|
|
109
|
-
return { code: "
|
|
119
|
+
if (/nothing to commit|nothing added to commit|no changes added/i.test(combined)) {
|
|
120
|
+
return { code: "nothing-to-commit", message: err };
|
|
110
121
|
}
|
|
111
122
|
if (/No such ref|not a valid reference|is not a stash|no tag|tag .* not found|unknown revision|bad revision|Could not parse object|ambiguous argument|did not match any file\(s\) known to git/i.test(combined)) {
|
|
112
123
|
return { code: "not-found", message: err || "not found" };
|
|
@@ -137,6 +148,16 @@ async function runAction(deps, config, request) {
|
|
|
137
148
|
const root = workspace.root;
|
|
138
149
|
const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
|
|
139
150
|
const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
|
|
151
|
+
if (request.action.kind === "publish") {
|
|
152
|
+
const headBroke = probeFailure(headProbe);
|
|
153
|
+
if (headBroke !== null) return headBroke;
|
|
154
|
+
if (unborn) return { ok: false, error: { code: "no-branch", message: "no commits to publish" } };
|
|
155
|
+
const branchProbe = await runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "publish-branch", deps.signal);
|
|
156
|
+
const branchBroke = probeFailure(branchProbe);
|
|
157
|
+
if (branchBroke !== null) return branchBroke;
|
|
158
|
+
const branch = "run" in branchProbe && branchProbe.run.exitCode === 0 ? branchProbe.run.stdout.trim() : "";
|
|
159
|
+
if (branch === "") return { ok: false, error: { code: "no-branch", message: "detached HEAD: no branch to publish" } };
|
|
160
|
+
}
|
|
140
161
|
const act = request.action;
|
|
141
162
|
if (act.kind === "stash-apply" || act.kind === "stash-pop" || act.kind === "stash-drop") {
|
|
142
163
|
if (!Number.isInteger(act.index) || act.index < 0) {
|
|
@@ -148,6 +169,27 @@ async function runAction(deps, config, request) {
|
|
|
148
169
|
return { ok: false, error: { code: "not-found", message: "stash entry changed; refresh and retry" } };
|
|
149
170
|
}
|
|
150
171
|
}
|
|
172
|
+
if (request.action.kind === "discard-all") {
|
|
173
|
+
const status = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--untracked-files=all"], root, "discard-all-status", deps.signal);
|
|
174
|
+
if ("failure" in status) return { ok: false, error: { code: "git-unavailable", message: status.failure instanceof Error ? status.failure.message : String(status.failure) } };
|
|
175
|
+
if (status.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
176
|
+
if (status.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
177
|
+
if (status.run.exitCode !== 0) {
|
|
178
|
+
const failure = classifyActionFailure(status.run.stdout, status.run.stderr, status.run.exitCode ?? -1);
|
|
179
|
+
return { ok: false, error: failure };
|
|
180
|
+
}
|
|
181
|
+
if (status.run.stdoutLossy) {
|
|
182
|
+
return { ok: false, error: { code: "status-lossy", message: "status output was clipped; refusing to discard an incomplete change set" } };
|
|
183
|
+
}
|
|
184
|
+
const all = parseStatus(status.run.stdout);
|
|
185
|
+
const paths = [...new Set(all.filter((c) => !c.staged).map((c) => c.path))].sort();
|
|
186
|
+
if (paths.length === 0) {
|
|
187
|
+
const snapshot2 = await snapshotForSession(deps, config, request.sessionId);
|
|
188
|
+
if (!snapshot2.ok) return { ok: false, error: { code: "git-error", message: "snapshot after action failed" } };
|
|
189
|
+
return { ok: true, snapshot: snapshot2.value, output: "" };
|
|
190
|
+
}
|
|
191
|
+
request = { ...request, action: { kind: "discard", paths } };
|
|
192
|
+
}
|
|
151
193
|
if (request.action.kind === "discard") {
|
|
152
194
|
const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
|
|
153
195
|
if (removed !== null) {
|
package/lib/host/core.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GitRunner } from './git.js';
|
|
2
2
|
import type { AgentDefaultModelFace, LlmFace, SessionModelSelection } from './llm-face.js';
|
|
3
|
-
import type { DiffViewMode, GraphStyle, GitChange, GitErrorCode, GitRemote, GitSnapshotResult, SuggestLanguage } from './types.js';
|
|
3
|
+
import type { DiffViewMode, FontDelta, GraphStyle, GitChange, GitErrorCode, GitRemote, GitSnapshotResult, SuggestLanguage } from './types.js';
|
|
4
4
|
export interface GitPanelConfig {
|
|
5
5
|
readonly timeoutMs: number;
|
|
6
6
|
/** Per-command stdout cap; also the per-side image-diff payload cap. */
|
|
@@ -13,6 +13,8 @@ export interface GitPanelConfig {
|
|
|
13
13
|
readonly defaultDiffView: DiffViewMode;
|
|
14
14
|
/** Commit-graph line style (parallel = VSCode-like, compact = git log). */
|
|
15
15
|
readonly graphStyle: GraphStyle;
|
|
16
|
+
/** Panel font-size step: -1 / 0 / +1. */
|
|
17
|
+
readonly fontDelta: FontDelta;
|
|
16
18
|
/** Whether the "AI suggest" button shows in the commit box. */
|
|
17
19
|
readonly suggestEnabled: boolean;
|
|
18
20
|
/** Language the generated message is written in; `auto` follows repo history. */
|
|
@@ -36,6 +38,8 @@ export declare function normalizeConfig(raw: unknown): GitPanelConfig;
|
|
|
36
38
|
export declare function readDiffView(value: unknown, fallback: DiffViewMode): DiffViewMode;
|
|
37
39
|
/** Read the commit-graph style field, unwrapping a schemastery volatile ref. */
|
|
38
40
|
export declare function readGraphStyle(value: unknown, fallback: GraphStyle): GraphStyle;
|
|
41
|
+
/** Read the panel font-size step, unwrapping a schemastery volatile ref. */
|
|
42
|
+
export declare function readFontDelta(value: unknown): FontDelta;
|
|
39
43
|
/**
|
|
40
44
|
* Read the commit-message language field, unwrapping a schemastery volatile
|
|
41
45
|
* ref. An unknown string (a hand-edited profile, or a value from a newer
|
|
@@ -162,6 +166,8 @@ export declare function mapWorkspaceFailure(failure: GitSnapshotResult & {
|
|
|
162
166
|
message?: string;
|
|
163
167
|
};
|
|
164
168
|
/** Produce a full snapshot for a session. */
|
|
165
|
-
export declare function snapshotForSession(deps: SnapshotDeps, config: GitPanelConfig, sessionId: string
|
|
169
|
+
export declare function snapshotForSession(deps: SnapshotDeps, config: GitPanelConfig, sessionId: string, opts?: {
|
|
170
|
+
readonly fullChanges?: boolean;
|
|
171
|
+
}): Promise<GitSnapshotResult>;
|
|
166
172
|
/** Max mtime among changed files (epoch ms), capped for large sets. */
|
|
167
173
|
export declare function maxChangeMtime(deps: SnapshotDeps, root: string, changes: readonly GitChange[], cap?: number): Promise<number | null>;
|
package/lib/host/core.js
CHANGED
|
@@ -3,7 +3,7 @@ import { commitFromFields, parseStatus, sumNumstat } from "./parser.js";
|
|
|
3
3
|
import { parseRemote } from "./remote.js";
|
|
4
4
|
import { snapshotFingerprint } from "./snapshot-fingerprint.js";
|
|
5
5
|
import { isSafePath } from "./validate.js";
|
|
6
|
-
import { SUGGEST_LANGUAGE_DEFAULT, SUGGEST_LANGUAGES, SUGGEST_LIMITS } from "./types.js";
|
|
6
|
+
import { SUGGEST_LANGUAGE_DEFAULT, SUGGEST_LANGUAGES, SUGGEST_LIMITS, FULL_CHANGES_MAX } from "./types.js";
|
|
7
7
|
const DEFAULT_CONFIG = {
|
|
8
8
|
timeoutMs: 8e3,
|
|
9
9
|
maxBytes: 4 * 1024 * 1024,
|
|
@@ -12,6 +12,7 @@ const DEFAULT_CONFIG = {
|
|
|
12
12
|
showInputPill: true,
|
|
13
13
|
defaultDiffView: "unified",
|
|
14
14
|
graphStyle: "parallel",
|
|
15
|
+
fontDelta: 0,
|
|
15
16
|
suggestEnabled: true,
|
|
16
17
|
suggestLanguage: SUGGEST_LANGUAGE_DEFAULT,
|
|
17
18
|
suggestMaxBytes: SUGGEST_LIMITS.maxBytes.default,
|
|
@@ -41,6 +42,7 @@ function normalizeConfig(raw) {
|
|
|
41
42
|
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
|
|
42
43
|
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
|
|
43
44
|
graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
|
|
45
|
+
fontDelta: readFontDelta(c.fontDelta),
|
|
44
46
|
suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
|
|
45
47
|
suggestLanguage: readSuggestLanguage(c.suggestLanguage, DEFAULT_CONFIG.suggestLanguage),
|
|
46
48
|
suggestMaxBytes: clampLimit(c.suggestMaxBytes, SUGGEST_LIMITS.maxBytes),
|
|
@@ -60,6 +62,10 @@ function readGraphStyle(value, fallback) {
|
|
|
60
62
|
const raw = unwrapVolatile(value);
|
|
61
63
|
return raw === "parallel" || raw === "compact" ? raw : fallback;
|
|
62
64
|
}
|
|
65
|
+
function readFontDelta(value) {
|
|
66
|
+
const raw = unwrapVolatile(value);
|
|
67
|
+
return raw === -1 || raw === 1 ? raw : 0;
|
|
68
|
+
}
|
|
63
69
|
function readSuggestLanguage(value, fallback) {
|
|
64
70
|
const raw = unwrapVolatile(value);
|
|
65
71
|
return typeof raw === "string" && SUGGEST_LANGUAGES.includes(raw) ? raw : fallback;
|
|
@@ -175,7 +181,8 @@ function mapWorkspaceFailure(failure) {
|
|
|
175
181
|
const message = "detail" in error ? error.detail : void 0;
|
|
176
182
|
return { code: error.code, ...message !== void 0 ? { message } : {} };
|
|
177
183
|
}
|
|
178
|
-
async function snapshotForSession(deps, config, sessionId) {
|
|
184
|
+
async function snapshotForSession(deps, config, sessionId, opts) {
|
|
185
|
+
const changeLimit = opts?.fullChanges === true ? Math.max(config.maxChanges, FULL_CHANGES_MAX) : config.maxChanges;
|
|
179
186
|
const workspace = await resolveWorkspace(deps, sessionId);
|
|
180
187
|
if (!workspace.ok) {
|
|
181
188
|
const failure = workspace.failure;
|
|
@@ -214,7 +221,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
214
221
|
let untrackedCollapsed = false;
|
|
215
222
|
const untrackedEntries = allChanges.filter((c) => c.status === "untracked");
|
|
216
223
|
const collapseCandidate = untrackedEntries.some((c) => c.path.includes("/"));
|
|
217
|
-
if (statusLossy || untrackedEntries.length >
|
|
224
|
+
if (statusLossy || untrackedEntries.length > changeLimit && collapseCandidate) {
|
|
218
225
|
const collapsed = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--untracked-files=normal"], root, "status-collapsed", deps.signal);
|
|
219
226
|
if ("run" in collapsed && !collapsed.run.timedOut && collapsed.run.exitCode === 0) {
|
|
220
227
|
const list = parseStatus(collapsed.run.stdout);
|
|
@@ -223,9 +230,9 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
223
230
|
statusLossy = collapsed.run.stdoutLossy;
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
|
-
const truncated = allChanges.length >
|
|
227
|
-
const truncatedReason = statusLossy ? "lossy" : allChanges.length >
|
|
228
|
-
const changes = allChanges.length >
|
|
233
|
+
const truncated = allChanges.length > changeLimit || statusLossy;
|
|
234
|
+
const truncatedReason = statusLossy ? "lossy" : allChanges.length > changeLimit ? "cap" : void 0;
|
|
235
|
+
const changes = allChanges.length > changeLimit ? allChanges.slice(0, changeLimit) : allChanges;
|
|
229
236
|
let staged = 0;
|
|
230
237
|
let modified = 0;
|
|
231
238
|
let untracked = 0;
|
|
@@ -300,6 +307,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
300
307
|
showInputPill: config.showInputPill,
|
|
301
308
|
defaultDiffView: config.defaultDiffView,
|
|
302
309
|
graphStyle: config.graphStyle,
|
|
310
|
+
fontDelta: config.fontDelta,
|
|
303
311
|
suggestEnabled: config.suggestEnabled,
|
|
304
312
|
checkedAt: Date.now()
|
|
305
313
|
};
|
|
@@ -328,6 +336,7 @@ export {
|
|
|
328
336
|
parsePrimaryRemote,
|
|
329
337
|
readBool,
|
|
330
338
|
readDiffView,
|
|
339
|
+
readFontDelta,
|
|
331
340
|
readGraphStyle,
|
|
332
341
|
readSuggestLanguage,
|
|
333
342
|
resolveBrowseRoot,
|
package/lib/host/index.d.ts
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import Schema from '@deepseek-ai/schemastery';
|
|
12
|
-
import type { GitActionRequest, GitActionResult, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult, GitSuggestRequest, GitSuggestResult, GitVersionInfo, GitVersionRequest } from './types.js';
|
|
13
|
-
export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, StashEntry, GitVersionRequest, GitVersionInfo, DiffViewMode, GraphStyle, DirEntry, ResetMode, GitSuggestRequest, GitSuggestResult, GitRemote, RemoteHostKind, SuggestLanguage, ResolvedLanguage, ModelCatalogGroup, ModelCatalogModel, ModelCatalogEffort, ModelCatalogFailure, } from './types.js';
|
|
12
|
+
import type { GitActionRequest, GitActionResult, GitOpenFileRequest, GitOpenFileResult, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult, GitSuggestRequest, GitSuggestResult, GitVersionInfo, GitVersionRequest } from './types.js';
|
|
13
|
+
export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, StashEntry, GitVersionRequest, GitVersionInfo, DiffViewMode, GraphStyle, DirEntry, ResetMode, GitSuggestRequest, GitSuggestResult, GitRemote, RemoteHostKind, SuggestLanguage, ResolvedLanguage, GitOpenFileRequest, GitOpenFileResult, ModelCatalogGroup, ModelCatalogModel, ModelCatalogEffort, ModelCatalogFailure, } from './types.js';
|
|
14
14
|
export { normalizeConfig, DEFAULT_CONFIG, snapshotForSession, resolveWorkspace, parsePrimaryRemote, readSuggestLanguage } from './core.js';
|
|
15
15
|
export { createGitRunner } from './git.js';
|
|
16
16
|
export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from './parser.js';
|
|
17
17
|
export { parseRemote, classifyHost } from './remote.js';
|
|
18
18
|
export { isSafePath, planAction, runAction, classifyActionFailure } from './actions.js';
|
|
19
|
+
export { openerArgv, canOpenWithSystem, openWithSystem } from './open-file.js';
|
|
19
20
|
export { runQuery } from './queries.js';
|
|
20
21
|
export { runSuggest, resolveSuggestLanguage, parseStyleHistory, isCjkText, compactLongLines, LONG_LINE_MAX_CHARS } from './suggest.js';
|
|
21
22
|
export { readVersionInfo, checkLatestVersion, compareVersions, parseRepository } from './version.js';
|
|
@@ -31,6 +32,7 @@ export declare class GitPanelService extends TypertRemoteService {
|
|
|
31
32
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
32
33
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
33
34
|
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
35
|
+
fontDelta: Schema<0 | 1 | -1, 0 | 1 | -1, "volatile-defined">;
|
|
34
36
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
35
37
|
suggestLanguage: Schema<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
|
|
36
38
|
suggestMaxBytes: Schema<number, number, "volatile-defined">;
|
|
@@ -43,6 +45,7 @@ export declare class GitPanelService extends TypertRemoteService {
|
|
|
43
45
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
44
46
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
45
47
|
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
48
|
+
fontDelta: Schema<0 | 1 | -1, 0 | 1 | -1, "volatile-defined">;
|
|
46
49
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
47
50
|
suggestLanguage: Schema<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
|
|
48
51
|
suggestMaxBytes: Schema<number, number, "volatile-defined">;
|
|
@@ -63,6 +66,9 @@ export declare class GitPanelService extends TypertRemoteService {
|
|
|
63
66
|
suggest(request: GitSuggestRequest, signal?: AbortSignal): Promise<GitSuggestResult>;
|
|
64
67
|
version(request: GitVersionRequest): Promise<GitVersionInfo>;
|
|
65
68
|
/** Re-read config so a live-edited volatile field (showInputPill) is current. */
|
|
69
|
+
/** Runs one OS-opener argv; null when the host has no subprocess service. */
|
|
70
|
+
private spawnOpener;
|
|
71
|
+
openFile(request: GitOpenFileRequest): Promise<GitOpenFileResult>;
|
|
66
72
|
private liveConfig;
|
|
67
73
|
private withSignal;
|
|
68
74
|
}
|
package/lib/host/index.js
CHANGED
|
@@ -46,12 +46,13 @@ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use
|
|
|
46
46
|
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
47
47
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
48
48
|
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
49
|
-
var _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
49
|
+
var _openFile_dec, _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
50
50
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
51
51
|
import Schema from "@deepseek-ai/schemastery";
|
|
52
52
|
import { createGitRunner } from "./git.js";
|
|
53
53
|
import { createFsDeps } from "./fs-deps.js";
|
|
54
|
-
import { normalizeConfig, snapshotForSession, DEFAULT_CONFIG } from "./core.js";
|
|
54
|
+
import { normalizeConfig, resolveBrowseRoot, snapshotForSession, DEFAULT_CONFIG } from "./core.js";
|
|
55
|
+
import { canOpenWithSystem, openWithSystem } from "./open-file.js";
|
|
55
56
|
import { runAction } from "./actions.js";
|
|
56
57
|
import { runQuery } from "./queries.js";
|
|
57
58
|
import { runSuggest } from "./suggest.js";
|
|
@@ -61,16 +62,38 @@ import { createGitRunner as createGitRunner2 } from "./git.js";
|
|
|
61
62
|
import { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from "./parser.js";
|
|
62
63
|
import { parseRemote, classifyHost } from "./remote.js";
|
|
63
64
|
import { isSafePath, planAction, runAction as runAction2, classifyActionFailure } from "./actions.js";
|
|
65
|
+
import { openerArgv, canOpenWithSystem as canOpenWithSystem2, openWithSystem as openWithSystem2 } from "./open-file.js";
|
|
64
66
|
import { runQuery as runQuery2 } from "./queries.js";
|
|
65
67
|
import { runSuggest as runSuggest2, resolveSuggestLanguage, parseStyleHistory, isCjkText, compactLongLines, LONG_LINE_MAX_CHARS } from "./suggest.js";
|
|
66
68
|
import { readVersionInfo as readVersionInfo2, checkLatestVersion as checkLatestVersion2, compareVersions, parseRepository } from "./version.js";
|
|
67
|
-
|
|
69
|
+
async function spawnDetached(subprocess, argv) {
|
|
70
|
+
try {
|
|
71
|
+
const handle = subprocess.spawn({
|
|
72
|
+
argv,
|
|
73
|
+
cwd: process.cwd(),
|
|
74
|
+
stdio: {
|
|
75
|
+
stdin: "ignore",
|
|
76
|
+
stdout: { collect: { maxBytes: 4096 } },
|
|
77
|
+
stderr: { collect: { maxBytes: 4096 } }
|
|
78
|
+
},
|
|
79
|
+
graceMs: 200
|
|
80
|
+
});
|
|
81
|
+
const outcome = await handle.done;
|
|
82
|
+
return outcome.exitCode;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _suggest_dec = [Remote("suggest")], _version_dec = [Remote("version")], _openFile_dec = [Remote("openFile")], _a) {
|
|
68
88
|
constructor(ctx, config) {
|
|
69
89
|
super(ctx, "gitPanel");
|
|
70
90
|
__runInitializers(_init, 5, this);
|
|
71
91
|
__publicField(this, "deps");
|
|
72
92
|
__publicField(this, "config");
|
|
73
93
|
__publicField(this, "rawConfig");
|
|
94
|
+
/** Re-read config so a live-edited volatile field (showInputPill) is current. */
|
|
95
|
+
/** Runs one OS-opener argv; null when the host has no subprocess service. */
|
|
96
|
+
__publicField(this, "spawnOpener", null);
|
|
74
97
|
this.rawConfig = config;
|
|
75
98
|
this.config = normalizeConfig(config);
|
|
76
99
|
this.deps = this.buildDeps(ctx, this.config);
|
|
@@ -84,6 +107,7 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
|
|
|
84
107
|
const run = subprocess === void 0 ? { run: async () => {
|
|
85
108
|
throw new Error("subprocess service unavailable");
|
|
86
109
|
} } : createGitRunner(subprocess, config.timeoutMs, config.maxBytes);
|
|
110
|
+
this.spawnOpener = subprocess === void 0 ? null : (argv) => spawnDetached(subprocess, argv);
|
|
87
111
|
const sessions = get("sessions");
|
|
88
112
|
const persistence = get("sessionPersistence");
|
|
89
113
|
const projections = get("sessionProjections");
|
|
@@ -131,7 +155,7 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
|
|
|
131
155
|
};
|
|
132
156
|
}
|
|
133
157
|
async snapshot(request, signal) {
|
|
134
|
-
return snapshotForSession(this.withSignal(signal), this.liveConfig(), request.sessionId);
|
|
158
|
+
return snapshotForSession(this.withSignal(signal), this.liveConfig(), request.sessionId, { fullChanges: request.fullChanges === true });
|
|
135
159
|
}
|
|
136
160
|
async run(request, signal) {
|
|
137
161
|
return runAction(this.withSignal(signal), this.liveConfig(), request);
|
|
@@ -149,7 +173,21 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
|
|
|
149
173
|
return { current: "0.0.0", updateAvailable: false, checkedRemote: false, error: error instanceof Error ? error.message : "version unavailable" };
|
|
150
174
|
}
|
|
151
175
|
}
|
|
152
|
-
|
|
176
|
+
async openFile(request) {
|
|
177
|
+
if (!canOpenWithSystem(process.platform)) {
|
|
178
|
+
return { ok: false, error: { code: "git-error", message: "this host cannot open files in a desktop application" } };
|
|
179
|
+
}
|
|
180
|
+
if (this.spawnOpener === null) {
|
|
181
|
+
return { ok: false, error: { code: "git-error", message: "subprocess service unavailable" } };
|
|
182
|
+
}
|
|
183
|
+
const browse = await resolveBrowseRoot(this.withSignal(void 0), request.sessionId);
|
|
184
|
+
if (!browse.ok) return { ok: false, error: browse.error };
|
|
185
|
+
try {
|
|
186
|
+
return await openWithSystem(this.deps, browse.root, request.path, this.spawnOpener);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : String(error) } };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
153
191
|
liveConfig() {
|
|
154
192
|
this.config = normalizeConfig(this.rawConfig);
|
|
155
193
|
return this.config;
|
|
@@ -165,6 +203,7 @@ __decorateElement(_init, 1, "run", _run_dec, GitPanelService);
|
|
|
165
203
|
__decorateElement(_init, 1, "query", _query_dec, GitPanelService);
|
|
166
204
|
__decorateElement(_init, 1, "suggest", _suggest_dec, GitPanelService);
|
|
167
205
|
__decorateElement(_init, 1, "version", _version_dec, GitPanelService);
|
|
206
|
+
__decorateElement(_init, 1, "openFile", _openFile_dec, GitPanelService);
|
|
168
207
|
__decoratorMetadata(_init, GitPanelService);
|
|
169
208
|
__publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
170
209
|
/**
|
|
@@ -183,6 +222,11 @@ __publicField(GitPanelService, "Config", Schema.object({
|
|
|
183
222
|
Schema.const("parallel").description("\u5E73\u884C\u7EBF\uFF08VSCode \u98CE\u683C\uFF0C\u5206\u652F\u5404\u5360\u4E00\u5217\uFF09"),
|
|
184
223
|
Schema.const("compact").description("\u7D27\u51D1\uFF08git log \u98CE\u683C\uFF0C\u5171\u4EAB\u7956\u5148\u5408\u5E76\u4E3A\u4E00\u5217\uFF09")
|
|
185
224
|
]).default("parallel").volatile().description("\u63D0\u4EA4\u5386\u53F2\u56FE\u7EBF\u6761\u6837\u5F0F"),
|
|
225
|
+
fontDelta: Schema.union([
|
|
226
|
+
Schema.const(-1).description("\u8F83\u5C0F"),
|
|
227
|
+
Schema.const(0).description("\u9ED8\u8BA4"),
|
|
228
|
+
Schema.const(1).description("\u8F83\u5927")
|
|
229
|
+
]).default(0).volatile().description("\u9762\u677F\u5B57\u53F7"),
|
|
186
230
|
suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE"),
|
|
187
231
|
suggestLanguage: Schema.union([
|
|
188
232
|
Schema.const("auto").description("\u81EA\u52A8\u8DDF\u968F\u4ED3\u5E93\u5386\u53F2\u63D0\u4EA4\u7684\u8BED\u8A00"),
|
|
@@ -206,6 +250,7 @@ export {
|
|
|
206
250
|
DEFAULT_CONFIG2 as DEFAULT_CONFIG,
|
|
207
251
|
GitPanelService,
|
|
208
252
|
LONG_LINE_MAX_CHARS,
|
|
253
|
+
canOpenWithSystem2 as canOpenWithSystem,
|
|
209
254
|
checkLatestVersion2 as checkLatestVersion,
|
|
210
255
|
classifyActionFailure,
|
|
211
256
|
classifyHost,
|
|
@@ -216,6 +261,8 @@ export {
|
|
|
216
261
|
isCjkText,
|
|
217
262
|
isSafePath,
|
|
218
263
|
normalizeConfig2 as normalizeConfig,
|
|
264
|
+
openWithSystem2 as openWithSystem,
|
|
265
|
+
openerArgv,
|
|
219
266
|
parseBranches,
|
|
220
267
|
parseGraphLog,
|
|
221
268
|
parseNameStatus,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SnapshotDeps } from './core.js';
|
|
2
|
+
import type { GitOpenFileResult } from './types.js';
|
|
3
|
+
/** Platform argv that opens one file with its default handler. */
|
|
4
|
+
export declare function openerArgv(platform: string, target: string): readonly string[] | null;
|
|
5
|
+
/** Whether this host can hand a file to the desktop at all. */
|
|
6
|
+
export declare function canOpenWithSystem(platform: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve `path` inside `root` and open it with the OS default application.
|
|
9
|
+
*
|
|
10
|
+
* @param deps Host fs face (realpath containment).
|
|
11
|
+
* @param root Browse root for the session.
|
|
12
|
+
* @param path Browse-root-relative path from the client (untrusted).
|
|
13
|
+
* @param spawn Runs one argv and resolves with its exit code, or null on failure.
|
|
14
|
+
* @returns The absolute path on success, a typed failure otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export declare function openWithSystem(deps: SnapshotDeps, root: string, path: string, spawn: (argv: readonly string[]) => Promise<number | null>, platform?: string): Promise<GitOpenFileResult>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { isSafePath } from "./validate.js";
|
|
3
|
+
import { isInsideRoot } from "./queries.js";
|
|
4
|
+
function openerArgv(platform, target) {
|
|
5
|
+
if (platform === "darwin") return ["open", target];
|
|
6
|
+
if (platform === "win32") return ["rundll32", "url.dll,FileProtocolHandler", target];
|
|
7
|
+
if (platform === "linux") return ["xdg-open", target];
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
function canOpenWithSystem(platform) {
|
|
11
|
+
return openerArgv(platform, "x") !== null;
|
|
12
|
+
}
|
|
13
|
+
async function openWithSystem(deps, root, path, spawn, platform = process.platform) {
|
|
14
|
+
if (!isSafePath(path)) return { ok: false, error: { code: "invalid-path", message: path } };
|
|
15
|
+
const file = join(root, path);
|
|
16
|
+
const inside = await isInsideRoot(deps, root, file, true);
|
|
17
|
+
if (inside === "missing") return { ok: false, error: { code: "file-not-found", message: path } };
|
|
18
|
+
if (inside !== "inside") return { ok: false, error: { code: "invalid-path", message: path } };
|
|
19
|
+
const argv = openerArgv(platform, file);
|
|
20
|
+
if (argv === null) return { ok: false, error: { code: "git-error", message: "no system opener on this platform" } };
|
|
21
|
+
const code = await spawn(argv);
|
|
22
|
+
if (code !== 0) return { ok: false, error: { code: "git-error", message: `the system opener exited ${String(code)}` } };
|
|
23
|
+
return { ok: true, absolutePath: file };
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
canOpenWithSystem,
|
|
27
|
+
openWithSystem,
|
|
28
|
+
openerArgv
|
|
29
|
+
};
|
package/lib/host/queries.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { SnapshotDeps, GitPanelConfig } from './core.js';
|
|
2
2
|
import type { GitQueryRequest, GitQueryResponse } from './types.js';
|
|
3
3
|
export declare function runQuery(deps: SnapshotDeps, config: GitPanelConfig, request: GitQueryRequest): Promise<GitQueryResponse>;
|
|
4
|
+
/** Whether a path resolves inside the browse root, outside its .git tree, or
|
|
5
|
+
* is absent (a deleted entry still listed by a stale tree). */
|
|
6
|
+
export type PathState = 'inside' | 'outside' | 'missing';
|
|
7
|
+
/**
|
|
8
|
+
* Where `path` sits relative to `root` after symlinks are followed: the one
|
|
9
|
+
* containment check shared by every endpoint that touches a browsable file.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isInsideRoot(deps: SnapshotDeps, root: string, path: string, hideGit?: boolean): Promise<PathState>;
|
package/lib/host/queries.js
CHANGED
package/lib/host/suggest.js
CHANGED
|
@@ -60,6 +60,9 @@ function systemPrompt(language) {
|
|
|
60
60
|
}
|
|
61
61
|
const UNTRACKED_FILE_CAP = 16;
|
|
62
62
|
const PROMPT_FILE_CAP = 200;
|
|
63
|
+
function routeNeedsSessionId(provider) {
|
|
64
|
+
return provider.toLowerCase().includes("opencode");
|
|
65
|
+
}
|
|
63
66
|
function resolveRoute(config, sessionModel, agentDefaultModel) {
|
|
64
67
|
if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
|
|
65
68
|
return {
|
|
@@ -279,7 +282,8 @@ async function runSuggest(deps, config, request) {
|
|
|
279
282
|
}],
|
|
280
283
|
maxTokens: config.suggestMaxOutputTokens,
|
|
281
284
|
temperature: 0.3,
|
|
282
|
-
signal: deadline.signal
|
|
285
|
+
signal: deadline.signal,
|
|
286
|
+
...routeNeedsSessionId(route.provider) ? { sessionId: request.sessionId } : {}
|
|
283
287
|
};
|
|
284
288
|
let output = "";
|
|
285
289
|
let finish;
|
package/lib/host/types.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type DiffViewMode = 'unified' | 'split';
|
|
|
9
9
|
* lane (git log --graph); `parallel` = keep each merge's first parent in its
|
|
10
10
|
* own lane, converging only at the ancestor (VSCode / GUI tools). */
|
|
11
11
|
export type GraphStyle = 'compact' | 'parallel';
|
|
12
|
+
/** Panel font-size step: -1 smaller, 0 default, +1 larger (CSS zoom on .gp-panel). */
|
|
13
|
+
export type FontDelta = -1 | 0 | 1;
|
|
12
14
|
/**
|
|
13
15
|
* Diff context (-U) values shared by both halves. `DEFAULT` matches git's own
|
|
14
16
|
* default and is what the host applies when a request omits `context`.
|
|
@@ -29,9 +31,16 @@ export declare const DIFF_EXPAND_CONTEXT = 2000;
|
|
|
29
31
|
* size the count reports 0 (unknown) and no trailing control is offered.
|
|
30
32
|
*/
|
|
31
33
|
export declare const DIFF_COUNT_READ_MAX: number;
|
|
32
|
-
/**
|
|
34
|
+
/** Hard ceiling for a `fullChanges` snapshot (the client's "expand all"):
|
|
35
|
+
* well above the default `maxChanges` (1000) so a big-but-sane change set
|
|
36
|
+
* expands completely, while an absurd one stays bounded on the wire. */
|
|
37
|
+
export declare const FULL_CHANGES_MAX = 10000;
|
|
38
|
+
/** Wire request: the browser sends only session identity, never a path.
|
|
39
|
+
* `fullChanges` asks the host to skip the `maxChanges` slice (still bounded
|
|
40
|
+
* by `FULL_CHANGES_MAX`) so the client can expand a truncated list once. */
|
|
33
41
|
export interface GitSnapshotRequest {
|
|
34
42
|
readonly sessionId: string;
|
|
43
|
+
readonly fullChanges?: boolean;
|
|
35
44
|
}
|
|
36
45
|
export type GitSnapshotResult = {
|
|
37
46
|
readonly ok: true;
|
|
@@ -116,6 +125,8 @@ export interface GitSnapshot {
|
|
|
116
125
|
readonly defaultDiffView: DiffViewMode;
|
|
117
126
|
/** Commit-graph line style (user preference). */
|
|
118
127
|
readonly graphStyle: GraphStyle;
|
|
128
|
+
/** Panel font-size step (user preference). */
|
|
129
|
+
readonly fontDelta: FontDelta;
|
|
119
130
|
/** Whether the commit box shows the AI-suggest button (user preference). */
|
|
120
131
|
readonly suggestEnabled: boolean;
|
|
121
132
|
/** SHA-256 of the final business snapshot, excluding checkedAt and itself.
|
|
@@ -173,6 +184,8 @@ export type GitAction = {
|
|
|
173
184
|
} | {
|
|
174
185
|
readonly kind: 'discard';
|
|
175
186
|
readonly paths: readonly string[];
|
|
187
|
+
} | {
|
|
188
|
+
readonly kind: 'discard-all';
|
|
176
189
|
} | {
|
|
177
190
|
readonly kind: 'commit';
|
|
178
191
|
readonly message: string;
|
|
@@ -228,7 +241,7 @@ export type GitAction = {
|
|
|
228
241
|
/** `git reset` modes, from least to most destructive. Single source so the
|
|
229
242
|
* client dialog and the command builder cannot drift. */
|
|
230
243
|
export type ResetMode = 'soft' | 'mixed' | 'hard';
|
|
231
|
-
export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'invalid-index' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'local-changes-block' | 'not-ff' | 'push-rejected' | 'auth-failed' | 'conflict' | 'revert-conflict' | 'revert-stuck' | 'revert-merge' | 'not-found' | 'file-not-found' | 'branch-in-use' | 'stash-switch-failed' | 'index-busy' | 'nested-repo' | 'nested-repo-partial' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
|
|
244
|
+
export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'invalid-index' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'nothing-to-commit' | 'local-changes-block' | 'not-ff' | 'push-rejected' | 'auth-failed' | 'conflict' | 'revert-conflict' | 'revert-stuck' | 'revert-merge' | 'not-found' | 'file-not-found' | 'branch-in-use' | 'stash-switch-failed' | 'index-busy' | 'nested-repo' | 'nested-repo-partial' | 'status-lossy' | 'no-branch' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
|
|
232
245
|
export type GitActionResult = {
|
|
233
246
|
readonly ok: true;
|
|
234
247
|
readonly snapshot: GitSnapshot;
|
|
@@ -649,6 +662,26 @@ export type GitSuggestResult = {
|
|
|
649
662
|
export declare const IMAGE_MIME: Readonly<Record<string, string>>;
|
|
650
663
|
/** MIME for a path's extension; null when it is not a served image type. */
|
|
651
664
|
export declare function imageMimeFor(path: string): string | null;
|
|
665
|
+
/**
|
|
666
|
+
* Hand one browsable file to the operating system so it opens outside the
|
|
667
|
+
* panel. The path is validated and confirmed inside the browse root before the
|
|
668
|
+
* OS sees it; `sessionId` picks that root.
|
|
669
|
+
*/
|
|
670
|
+
export interface GitOpenFileRequest {
|
|
671
|
+
readonly sessionId: string;
|
|
672
|
+
/** Browse-root-relative path of the file to open. */
|
|
673
|
+
readonly path: string;
|
|
674
|
+
}
|
|
675
|
+
export type GitOpenFileResult = {
|
|
676
|
+
readonly ok: true;
|
|
677
|
+
readonly absolutePath: string;
|
|
678
|
+
} | {
|
|
679
|
+
readonly ok: false;
|
|
680
|
+
readonly error: {
|
|
681
|
+
readonly code: GitErrorCode;
|
|
682
|
+
readonly message?: string;
|
|
683
|
+
};
|
|
684
|
+
};
|
|
652
685
|
export interface GitVersionRequest {
|
|
653
686
|
/** True → query the GitHub releases API; false/absent → local view only. */
|
|
654
687
|
readonly check?: boolean;
|
package/lib/host/types.js
CHANGED
|
@@ -2,6 +2,7 @@ const DIFF_CONTEXT_DEFAULT = 3;
|
|
|
2
2
|
const DIFF_CONTEXT_MAX = 1e5;
|
|
3
3
|
const DIFF_EXPAND_CONTEXT = 2e3;
|
|
4
4
|
const DIFF_COUNT_READ_MAX = 8 * 1024 * 1024;
|
|
5
|
+
const FULL_CHANGES_MAX = 1e4;
|
|
5
6
|
function isHistoryHashSearch(search) {
|
|
6
7
|
return /^[0-9a-fA-F]{7,40}$/.test(search.trim());
|
|
7
8
|
}
|
|
@@ -41,6 +42,7 @@ export {
|
|
|
41
42
|
DIFF_COUNT_READ_MAX,
|
|
42
43
|
DIFF_EXPAND_CONTEXT,
|
|
43
44
|
FILE_COPY_MAX_BYTES,
|
|
45
|
+
FULL_CHANGES_MAX,
|
|
44
46
|
IMAGE_MIME,
|
|
45
47
|
SUGGEST_LANGUAGES,
|
|
46
48
|
SUGGEST_LANGUAGE_DEFAULT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbzbing/dsh-git-panel",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "DeepSeek Harness (dsh) plugin: an IDE-style Git panel in the Web GUI — branch/history overview, uncommitted changes with commit & amend, diff stats, and a zsh-style branch marker in the input bar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|