@supanadit/morse-web 0.11.0 → 0.13.0
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/server.mjs +434 -81
- package/dist/server.mjs.map +4 -4
- package/dist/ui/3rdpartylicenses.txt +34 -34
- package/dist/ui/{chunk-DA4X1qo9.js → chunk-CVTlMVVu.js} +1 -1
- package/dist/ui/{chunk-DD8oHaM3.js → chunk-D1etuzzZ.js} +2 -2
- package/dist/ui/{chunk-C9z992Mq.js → chunk-ourdPesa.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/ui/main-TLDRX4Y6.js +120 -0
- package/dist/ui/webview.manifest.json +3 -3
- package/package.json +1 -1
- package/dist/ui/main-5C6SRPAD.js +0 -120
package/dist/server.mjs
CHANGED
|
@@ -79,7 +79,7 @@ function hasKnownType(value, allowed) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// ../protocol/dist/version.js
|
|
82
|
-
var PROTOCOL_VERSION =
|
|
82
|
+
var PROTOCOL_VERSION = 21;
|
|
83
83
|
var FRONTEND_MANIFEST_FILE = "webview.manifest.json";
|
|
84
84
|
var DEFAULT_WS_PATH = "/ws";
|
|
85
85
|
|
|
@@ -1710,11 +1710,11 @@ function promptDescription(raw) {
|
|
|
1710
1710
|
}
|
|
1711
1711
|
body = raw.slice(frontmatter[0].length);
|
|
1712
1712
|
}
|
|
1713
|
-
const
|
|
1714
|
-
if (
|
|
1713
|
+
const firstLine = body.split("\n").find((line) => line.trim().length > 0);
|
|
1714
|
+
if (firstLine === void 0) {
|
|
1715
1715
|
return void 0;
|
|
1716
1716
|
}
|
|
1717
|
-
const trimmed =
|
|
1717
|
+
const trimmed = firstLine.trim();
|
|
1718
1718
|
return trimmed.length > 60 ? `${trimmed.slice(0, 60)}...` : trimmed;
|
|
1719
1719
|
}
|
|
1720
1720
|
function unquote(value) {
|
|
@@ -2145,6 +2145,17 @@ function createPiRpcAdapter(config, logger) {
|
|
|
2145
2145
|
};
|
|
2146
2146
|
}
|
|
2147
2147
|
|
|
2148
|
+
// ../host-runtime/dist/terminal-buffer.js
|
|
2149
|
+
var TERMINAL_BUFFER_CHARS = 256 * 1024;
|
|
2150
|
+
function trimTail(text, maxChars = TERMINAL_BUFFER_CHARS) {
|
|
2151
|
+
if (text.length <= maxChars) {
|
|
2152
|
+
return text;
|
|
2153
|
+
}
|
|
2154
|
+
const tail = text.slice(-maxChars);
|
|
2155
|
+
const newline = tail.indexOf("\n");
|
|
2156
|
+
return newline === -1 ? tail : tail.slice(newline + 1);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2148
2159
|
// ../host-runtime/dist/transcript-projector.js
|
|
2149
2160
|
var TranscriptProjector = class {
|
|
2150
2161
|
options;
|
|
@@ -2671,8 +2682,15 @@ var HostSessionController = class {
|
|
|
2671
2682
|
loadingOlderHistory = false;
|
|
2672
2683
|
/** Discriminates history item ids across pages (see `historyItems`). */
|
|
2673
2684
|
historyPageSeq = 0;
|
|
2674
|
-
/**
|
|
2685
|
+
/** This client's live attachments to shells, keyed by the terminal id it named. */
|
|
2675
2686
|
terminals = /* @__PURE__ */ new Map();
|
|
2687
|
+
/**
|
|
2688
|
+
* The ids this connection has already opened. It separates a *first* open — a
|
|
2689
|
+
* page reattaching to a shell the host still runs — from a repeat open (the
|
|
2690
|
+
* Restart button), which must replace the shell and clear its scrollback
|
|
2691
|
+
* instead of replaying it into the emulator that already shows it.
|
|
2692
|
+
*/
|
|
2693
|
+
openedTerminals = /* @__PURE__ */ new Set();
|
|
2676
2694
|
/**
|
|
2677
2695
|
* Bumped whenever a terminal id is opened or closed. A shell's late `exit`
|
|
2678
2696
|
* (a replaced one, a killed one) compares its generation and stays silent, so
|
|
@@ -2721,11 +2739,12 @@ var HostSessionController = class {
|
|
|
2721
2739
|
return;
|
|
2722
2740
|
}
|
|
2723
2741
|
this.disposed = true;
|
|
2724
|
-
for (const
|
|
2725
|
-
|
|
2742
|
+
for (const session of this.terminals.values()) {
|
|
2743
|
+
session.detach();
|
|
2726
2744
|
}
|
|
2727
2745
|
this.terminals.clear();
|
|
2728
2746
|
this.terminalGenerations.clear();
|
|
2747
|
+
this.openedTerminals.clear();
|
|
2729
2748
|
this.detachRegistry?.();
|
|
2730
2749
|
this.detachRegistry = void 0;
|
|
2731
2750
|
this.detachTranscripts?.();
|
|
@@ -2830,23 +2849,38 @@ var HostSessionController = class {
|
|
|
2830
2849
|
}
|
|
2831
2850
|
}
|
|
2832
2851
|
/**
|
|
2833
|
-
*
|
|
2834
|
-
* (or the draft's), never an arbitrary path the client
|
|
2835
|
-
* still re-checks it against the host policy before
|
|
2852
|
+
* Attaches this client to a shell for the bottom panel. The directory is the
|
|
2853
|
+
* viewing session's (or the draft's), never an arbitrary path the client
|
|
2854
|
+
* names — the backend still re-checks it against the host policy before
|
|
2855
|
+
* spawning.
|
|
2856
|
+
*
|
|
2857
|
+
* A *first* open on this connection may land on a shell that is already running
|
|
2858
|
+
* (a reloaded page reattaching); a repeat open (the Restart button) replaces
|
|
2859
|
+
* it, scrollback and all, so the fresh shell does not replay the old one.
|
|
2836
2860
|
*/
|
|
2837
2861
|
async openTerminal(payload) {
|
|
2838
|
-
|
|
2862
|
+
const backend = this.options.terminal;
|
|
2863
|
+
if (backend === void 0) {
|
|
2839
2864
|
this.emitTerminalExit(payload.terminalId, void 0, "This host has no terminal.");
|
|
2840
2865
|
return;
|
|
2841
2866
|
}
|
|
2842
|
-
this.
|
|
2843
|
-
|
|
2867
|
+
const previous = this.terminals.get(payload.terminalId);
|
|
2868
|
+
if (previous !== void 0) {
|
|
2869
|
+
previous.detach();
|
|
2870
|
+
this.terminals.delete(payload.terminalId);
|
|
2871
|
+
backend.close(payload.terminalId);
|
|
2872
|
+
} else if (this.openedTerminals.has(payload.terminalId)) {
|
|
2873
|
+
backend.close(payload.terminalId);
|
|
2874
|
+
}
|
|
2875
|
+
this.openedTerminals.add(payload.terminalId);
|
|
2876
|
+
const generation = (this.terminalGenerations.get(payload.terminalId) ?? 0) + 1;
|
|
2877
|
+
this.terminalGenerations.set(payload.terminalId, generation);
|
|
2844
2878
|
const cwd = payload.cwd ?? this.currentState().workspace.cwd;
|
|
2845
2879
|
const cols = clampGeometry(payload.cols, 80);
|
|
2846
2880
|
const rows = clampGeometry(payload.rows, 24);
|
|
2847
|
-
let
|
|
2881
|
+
let session;
|
|
2848
2882
|
try {
|
|
2849
|
-
|
|
2883
|
+
session = await backend.attach(payload.terminalId, { cwd, cols, rows }, {
|
|
2850
2884
|
output: (data) => {
|
|
2851
2885
|
if (this.terminalGenerations.get(payload.terminalId) === generation) {
|
|
2852
2886
|
this.emitTerminalOutput(payload.terminalId, data);
|
|
@@ -2866,19 +2900,25 @@ var HostSessionController = class {
|
|
|
2866
2900
|
return;
|
|
2867
2901
|
}
|
|
2868
2902
|
if (!this.disposed && this.terminalGenerations.get(payload.terminalId) === generation) {
|
|
2869
|
-
this.terminals.set(payload.terminalId,
|
|
2903
|
+
this.terminals.set(payload.terminalId, session);
|
|
2904
|
+
if (session.replay.length > 0) {
|
|
2905
|
+
this.emitTerminalOutput(payload.terminalId, session.replay);
|
|
2906
|
+
}
|
|
2870
2907
|
} else {
|
|
2871
|
-
|
|
2908
|
+
session.detach();
|
|
2872
2909
|
}
|
|
2873
2910
|
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Ends a shell for good: the reader closed the pane. Bumps the generation first,
|
|
2913
|
+
* so a late `exit` for the shell being replaced is not reported as news.
|
|
2914
|
+
*/
|
|
2874
2915
|
closeTerminal(terminalId) {
|
|
2875
2916
|
this.terminalGenerations.set(terminalId, (this.terminalGenerations.get(terminalId) ?? 0) + 1);
|
|
2876
|
-
const
|
|
2877
|
-
if (process2 === void 0) {
|
|
2878
|
-
return;
|
|
2879
|
-
}
|
|
2917
|
+
const session = this.terminals.get(terminalId);
|
|
2880
2918
|
this.terminals.delete(terminalId);
|
|
2881
|
-
|
|
2919
|
+
this.openedTerminals.delete(terminalId);
|
|
2920
|
+
session?.detach();
|
|
2921
|
+
this.options.terminal?.close(terminalId);
|
|
2882
2922
|
}
|
|
2883
2923
|
emitTerminalOutput(terminalId, data) {
|
|
2884
2924
|
if (data.length > 0 && !this.disposed) {
|
|
@@ -3769,7 +3809,50 @@ var ServerProjectPolicy = class {
|
|
|
3769
3809
|
// ../server/src/internal/terminal/terminal.service.ts
|
|
3770
3810
|
import { spawn as spawnPty } from "node-pty";
|
|
3771
3811
|
import { Inject as Inject2, Injectable } from "@nestjs/common";
|
|
3812
|
+
|
|
3813
|
+
// ../server/src/internal/terminal/terminal-log.ts
|
|
3814
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3815
|
+
import { join as join5 } from "node:path";
|
|
3816
|
+
var LOG_DIR = "terminals";
|
|
3817
|
+
var LOG_MAX_CHARS = TERMINAL_BUFFER_CHARS * 4;
|
|
3818
|
+
function readTerminalLog(dataDir, terminalId) {
|
|
3819
|
+
try {
|
|
3820
|
+
const raw = readFileSync(logPath(dataDir, terminalId), "utf8");
|
|
3821
|
+
return trimTail(raw, LOG_MAX_CHARS);
|
|
3822
|
+
} catch {
|
|
3823
|
+
return "";
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
function writeTerminalLog(dataDir, terminalId, text) {
|
|
3827
|
+
const trimmed = trimTail(text, LOG_MAX_CHARS);
|
|
3828
|
+
const target = logPath(dataDir, terminalId);
|
|
3829
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
3830
|
+
try {
|
|
3831
|
+
mkdirSync(join5(dataDir, LOG_DIR), { recursive: true });
|
|
3832
|
+
writeFileSync(temp, trimmed);
|
|
3833
|
+
renameSync(temp, target);
|
|
3834
|
+
} catch {
|
|
3835
|
+
try {
|
|
3836
|
+
writeFileSync(target, trimmed);
|
|
3837
|
+
} catch {
|
|
3838
|
+
rmSync(temp, { force: true });
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
function removeTerminalLog(dataDir, terminalId) {
|
|
3843
|
+
rmSync(logPath(dataDir, terminalId), { force: true });
|
|
3844
|
+
}
|
|
3845
|
+
function safeTerminalName(terminalId) {
|
|
3846
|
+
const safe = terminalId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 100);
|
|
3847
|
+
return safe.length > 0 ? safe : "terminal";
|
|
3848
|
+
}
|
|
3849
|
+
function logPath(dataDir, terminalId) {
|
|
3850
|
+
return join5(dataDir, LOG_DIR, `${safeTerminalName(terminalId)}.log`);
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// ../server/src/internal/terminal/terminal.service.ts
|
|
3772
3854
|
var SIGKILL_AFTER_MS = 1500;
|
|
3855
|
+
var FLUSH_DEBOUNCE_MS = 2e3;
|
|
3773
3856
|
function defaultShell() {
|
|
3774
3857
|
const configured = process.env.SHELL?.trim();
|
|
3775
3858
|
if (configured !== void 0 && configured.length > 0) {
|
|
@@ -3778,17 +3861,95 @@ function defaultShell() {
|
|
|
3778
3861
|
return process.platform === "win32" ? "powershell.exe" : "/bin/bash";
|
|
3779
3862
|
}
|
|
3780
3863
|
var ServerTerminalBackend = class {
|
|
3781
|
-
constructor(policy, logger) {
|
|
3864
|
+
constructor(policy, logger, config) {
|
|
3782
3865
|
this.policy = policy;
|
|
3783
3866
|
this.logger = logger;
|
|
3867
|
+
this.config = config;
|
|
3868
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
3784
3869
|
}
|
|
3785
|
-
|
|
3870
|
+
attach(terminalId, options, sink) {
|
|
3871
|
+
const entry = this.entryFor(terminalId);
|
|
3872
|
+
if (entry.pty === void 0) {
|
|
3873
|
+
this.spawn(terminalId, entry, options);
|
|
3874
|
+
} else {
|
|
3875
|
+
this.resize(entry, options.cols, options.rows);
|
|
3876
|
+
}
|
|
3877
|
+
entry.sinks.add(sink);
|
|
3878
|
+
this.cancelIdle(entry);
|
|
3879
|
+
return {
|
|
3880
|
+
replay: entry.output,
|
|
3881
|
+
write: (data) => entry.pty?.write(data),
|
|
3882
|
+
resize: (cols, rows) => this.resize(entry, cols, rows),
|
|
3883
|
+
detach: () => this.detach(terminalId, entry, sink)
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
/** Ends a shell for good, drops its scrollback, and tells any other viewer. */
|
|
3887
|
+
close(terminalId) {
|
|
3888
|
+
const entry = this.entries.get(terminalId);
|
|
3889
|
+
this.entries.delete(terminalId);
|
|
3890
|
+
if (entry !== void 0) {
|
|
3891
|
+
this.stopFlush(entry);
|
|
3892
|
+
this.cancelIdle(entry);
|
|
3893
|
+
entry.generation += 1;
|
|
3894
|
+
const sinks = [...entry.sinks];
|
|
3895
|
+
entry.sinks.clear();
|
|
3896
|
+
this.killPty(entry.pty);
|
|
3897
|
+
entry.pty = void 0;
|
|
3898
|
+
for (const sink of sinks) {
|
|
3899
|
+
sink.exit();
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
removeTerminalLog(this.config.dataDir, terminalId);
|
|
3903
|
+
}
|
|
3904
|
+
onModuleDestroy() {
|
|
3905
|
+
for (const [terminalId, entry] of this.entries) {
|
|
3906
|
+
entry.generation += 1;
|
|
3907
|
+
this.persist(terminalId, entry);
|
|
3908
|
+
this.killPty(entry.pty);
|
|
3909
|
+
entry.pty = void 0;
|
|
3910
|
+
this.stopFlush(entry);
|
|
3911
|
+
this.cancelIdle(entry);
|
|
3912
|
+
}
|
|
3913
|
+
this.entries.clear();
|
|
3914
|
+
}
|
|
3915
|
+
/** SIGTERM, then SIGKILL for a shell that ignores it (a hung process group). */
|
|
3916
|
+
killPty(pty) {
|
|
3917
|
+
if (pty === void 0) {
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
pty.kill();
|
|
3922
|
+
} catch {
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
const timer = setTimeout(() => {
|
|
3926
|
+
try {
|
|
3927
|
+
pty.kill("SIGKILL");
|
|
3928
|
+
} catch {
|
|
3929
|
+
}
|
|
3930
|
+
}, SIGKILL_AFTER_MS);
|
|
3931
|
+
timer.unref?.();
|
|
3932
|
+
}
|
|
3933
|
+
entryFor(terminalId) {
|
|
3934
|
+
const existing = this.entries.get(terminalId);
|
|
3935
|
+
if (existing !== void 0) {
|
|
3936
|
+
return existing;
|
|
3937
|
+
}
|
|
3938
|
+
const entry = {
|
|
3939
|
+
sinks: /* @__PURE__ */ new Set(),
|
|
3940
|
+
output: readTerminalLog(this.config.dataDir, terminalId),
|
|
3941
|
+
dirty: false,
|
|
3942
|
+
generation: 0
|
|
3943
|
+
};
|
|
3944
|
+
this.entries.set(terminalId, entry);
|
|
3945
|
+
return entry;
|
|
3946
|
+
}
|
|
3947
|
+
spawn(terminalId, entry, options) {
|
|
3786
3948
|
if (!this.policy.canOpen(options.cwd)) {
|
|
3787
|
-
throw new UnsupportedByHostError(
|
|
3788
|
-
`Morse is not allowed to run a terminal in ${options.cwd}.`
|
|
3789
|
-
);
|
|
3949
|
+
throw new UnsupportedByHostError(`Morse is not allowed to run a terminal in ${options.cwd}.`);
|
|
3790
3950
|
}
|
|
3791
3951
|
const shell = defaultShell();
|
|
3952
|
+
const generation = ++entry.generation;
|
|
3792
3953
|
const pty = spawnPty(shell, [], {
|
|
3793
3954
|
name: "xterm-256color",
|
|
3794
3955
|
cols: options.cols,
|
|
@@ -3796,41 +3957,105 @@ var ServerTerminalBackend = class {
|
|
|
3796
3957
|
cwd: options.cwd,
|
|
3797
3958
|
env: { ...process.env, TERM: "xterm-256color", MORSE_TERMINAL: "1" }
|
|
3798
3959
|
});
|
|
3960
|
+
entry.pty = pty;
|
|
3799
3961
|
this.logger.info(`Terminal opened in ${options.cwd} (${shell}, pid ${pty.pid})`);
|
|
3800
|
-
|
|
3801
|
-
|
|
3962
|
+
pty.onData((chunk) => {
|
|
3963
|
+
if (entry.generation !== generation) {
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
entry.output += chunk;
|
|
3967
|
+
if (entry.output.length > TERMINAL_BUFFER_CHARS * 1.5) {
|
|
3968
|
+
entry.output = trimTail(entry.output, TERMINAL_BUFFER_CHARS);
|
|
3969
|
+
}
|
|
3970
|
+
entry.dirty = true;
|
|
3971
|
+
this.scheduleFlush(terminalId, entry);
|
|
3972
|
+
for (const sink of entry.sinks) {
|
|
3973
|
+
sink.output(chunk);
|
|
3974
|
+
}
|
|
3975
|
+
});
|
|
3802
3976
|
pty.onExit(({ exitCode }) => {
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3977
|
+
if (entry.generation !== generation) {
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
entry.pty = void 0;
|
|
3981
|
+
this.persist(terminalId, entry);
|
|
3982
|
+
for (const sink of [...entry.sinks]) {
|
|
3983
|
+
sink.exit(exitCode);
|
|
3984
|
+
}
|
|
3806
3985
|
});
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3986
|
+
}
|
|
3987
|
+
resize(entry, cols, rows) {
|
|
3988
|
+
try {
|
|
3989
|
+
entry.pty?.resize(cols, rows);
|
|
3990
|
+
} catch {
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
detach(terminalId, entry, sink) {
|
|
3994
|
+
entry.sinks.delete(sink);
|
|
3995
|
+
if (entry.sinks.size > 0) {
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
this.persist(terminalId, entry);
|
|
3999
|
+
this.scheduleIdle(terminalId, entry);
|
|
4000
|
+
}
|
|
4001
|
+
/**
|
|
4002
|
+
* Reclaims a shell nobody has reattached to. The log stays (the output should
|
|
4003
|
+
* still be there next time); only the process goes.
|
|
4004
|
+
*/
|
|
4005
|
+
scheduleIdle(terminalId, entry) {
|
|
4006
|
+
if (this.config.terminalIdleMs <= 0) {
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
this.cancelIdle(entry);
|
|
4010
|
+
entry.idleTimer = setTimeout(() => {
|
|
4011
|
+
entry.idleTimer = void 0;
|
|
4012
|
+
if (entry.sinks.size > 0) {
|
|
4013
|
+
return;
|
|
3826
4014
|
}
|
|
3827
|
-
|
|
4015
|
+
entry.generation += 1;
|
|
4016
|
+
this.killPty(entry.pty);
|
|
4017
|
+
entry.pty = void 0;
|
|
4018
|
+
this.persist(terminalId, entry);
|
|
4019
|
+
this.entries.delete(terminalId);
|
|
4020
|
+
}, this.config.terminalIdleMs);
|
|
4021
|
+
entry.idleTimer.unref?.();
|
|
4022
|
+
}
|
|
4023
|
+
cancelIdle(entry) {
|
|
4024
|
+
if (entry.idleTimer !== void 0) {
|
|
4025
|
+
clearTimeout(entry.idleTimer);
|
|
4026
|
+
entry.idleTimer = void 0;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
scheduleFlush(terminalId, entry) {
|
|
4030
|
+
if (entry.flushTimer !== void 0) {
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
entry.flushTimer = setTimeout(() => {
|
|
4034
|
+
entry.flushTimer = void 0;
|
|
4035
|
+
this.persist(terminalId, entry);
|
|
4036
|
+
}, FLUSH_DEBOUNCE_MS);
|
|
4037
|
+
entry.flushTimer.unref?.();
|
|
4038
|
+
}
|
|
4039
|
+
stopFlush(entry) {
|
|
4040
|
+
if (entry.flushTimer !== void 0) {
|
|
4041
|
+
clearTimeout(entry.flushTimer);
|
|
4042
|
+
entry.flushTimer = void 0;
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
persist(terminalId, entry) {
|
|
4046
|
+
this.stopFlush(entry);
|
|
4047
|
+
if (!entry.dirty) {
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
entry.dirty = false;
|
|
4051
|
+
writeTerminalLog(this.config.dataDir, terminalId, entry.output);
|
|
3828
4052
|
}
|
|
3829
4053
|
};
|
|
3830
4054
|
ServerTerminalBackend = __decorateClass([
|
|
3831
4055
|
Injectable(),
|
|
3832
4056
|
__decorateParam(0, Inject2(MORSE_PROJECT_POLICY)),
|
|
3833
|
-
__decorateParam(1, Inject2(MORSE_LOGGER))
|
|
4057
|
+
__decorateParam(1, Inject2(MORSE_LOGGER)),
|
|
4058
|
+
__decorateParam(2, Inject2(MORSE_CONFIG))
|
|
3834
4059
|
], ServerTerminalBackend);
|
|
3835
4060
|
|
|
3836
4061
|
// ../server/src/internal/registry.ts
|
|
@@ -3872,7 +4097,7 @@ import { Inject as Inject4, Injectable as Injectable3 } from "@nestjs/common";
|
|
|
3872
4097
|
// ../server/src/internal/uploads/upload-store.ts
|
|
3873
4098
|
import { existsSync } from "node:fs";
|
|
3874
4099
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3875
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
4100
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join6, relative, sep as sep3 } from "node:path";
|
|
3876
4101
|
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
3877
4102
|
var DEFAULT_UPLOAD_DIR = ".morse/uploads";
|
|
3878
4103
|
var UploadError = class extends Error {
|
|
@@ -3892,7 +4117,7 @@ async function saveUpload(cwd, request, uploadDir = DEFAULT_UPLOAD_DIR) {
|
|
|
3892
4117
|
`That file is ${formatMb(bytes.length)} MB; the limit is ${formatMb(MAX_UPLOAD_BYTES)} MB.`
|
|
3893
4118
|
);
|
|
3894
4119
|
}
|
|
3895
|
-
const directory = isAbsolute3(uploadDir) ? uploadDir :
|
|
4120
|
+
const directory = isAbsolute3(uploadDir) ? uploadDir : join6(cwd, uploadDir);
|
|
3896
4121
|
await mkdir(directory, { recursive: true });
|
|
3897
4122
|
const target = uniquePath(directory, name);
|
|
3898
4123
|
await writeFile(target, bytes);
|
|
@@ -3911,10 +4136,10 @@ function uniquePath(directory, name) {
|
|
|
3911
4136
|
const dot = name.lastIndexOf(".");
|
|
3912
4137
|
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
3913
4138
|
const extension = dot > 0 ? name.slice(dot) : "";
|
|
3914
|
-
let candidate =
|
|
4139
|
+
let candidate = join6(directory, name);
|
|
3915
4140
|
let suffix = 1;
|
|
3916
4141
|
while (existsSync(candidate)) {
|
|
3917
|
-
candidate =
|
|
4142
|
+
candidate = join6(directory, `${stem}-${suffix}${extension}`);
|
|
3918
4143
|
suffix += 1;
|
|
3919
4144
|
}
|
|
3920
4145
|
return candidate;
|
|
@@ -3931,7 +4156,7 @@ function formatMb(bytes) {
|
|
|
3931
4156
|
// ../server/src/internal/workspace/directory-browser.ts
|
|
3932
4157
|
import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
|
|
3933
4158
|
import { homedir as homedir3 } from "node:os";
|
|
3934
|
-
import { basename as basename3, dirname, isAbsolute as isAbsolute4, join as
|
|
4159
|
+
import { basename as basename3, dirname, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "node:path";
|
|
3935
4160
|
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
3936
4161
|
"node_modules",
|
|
3937
4162
|
"dist",
|
|
@@ -3955,7 +4180,7 @@ async function browseDirectory(path, options) {
|
|
|
3955
4180
|
throw new Error(`A folder path must be absolute: ${requested}`);
|
|
3956
4181
|
}
|
|
3957
4182
|
const entries = await readdir3(start, { withFileTypes: true });
|
|
3958
|
-
const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path:
|
|
4183
|
+
const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path: join7(start, entry.name) })).sort((left, right) => left.name.localeCompare(right.name));
|
|
3959
4184
|
const parent = dirname(start);
|
|
3960
4185
|
return {
|
|
3961
4186
|
path: start,
|
|
@@ -3970,7 +4195,7 @@ function shouldSkip(name) {
|
|
|
3970
4195
|
}
|
|
3971
4196
|
async function isGitRepo(path) {
|
|
3972
4197
|
try {
|
|
3973
|
-
await stat3(
|
|
4198
|
+
await stat3(join7(path, ".git"));
|
|
3974
4199
|
return true;
|
|
3975
4200
|
} catch {
|
|
3976
4201
|
return false;
|
|
@@ -4253,7 +4478,10 @@ async function commitGit(cwd, message) {
|
|
|
4253
4478
|
return { ok: false, message: "This project is not a git repository." };
|
|
4254
4479
|
}
|
|
4255
4480
|
const result = await gitExec(["commit", "-m", text], cwd, 6e4);
|
|
4256
|
-
|
|
4481
|
+
if (result.ok) {
|
|
4482
|
+
return { ok: true };
|
|
4483
|
+
}
|
|
4484
|
+
return { ok: false, message: describeCommitFailure(result.output) };
|
|
4257
4485
|
}
|
|
4258
4486
|
async function checkoutGit(cwd, branch, create) {
|
|
4259
4487
|
const name = branch.trim();
|
|
@@ -4269,13 +4497,21 @@ async function checkoutGit(cwd, branch, create) {
|
|
|
4269
4497
|
cwd,
|
|
4270
4498
|
6e4
|
|
4271
4499
|
);
|
|
4272
|
-
return { ok: result.ok, message: result.ok ? void 0 :
|
|
4500
|
+
return { ok: result.ok, message: result.ok ? void 0 : gitRefusal(result.output) };
|
|
4273
4501
|
}
|
|
4274
4502
|
function isBranchName(name) {
|
|
4275
4503
|
return name.length > 0 && !name.startsWith("-") && !name.includes("..") && !name.includes("@{") && !name.endsWith("/") && !/\s/.test(name) && !/[~^:?*\[\\]/.test(name);
|
|
4276
4504
|
}
|
|
4277
|
-
function
|
|
4278
|
-
|
|
4505
|
+
function describeCommitFailure(output) {
|
|
4506
|
+
if (/nothing to commit|no changes added to commit|nothing added to commit/i.test(output)) {
|
|
4507
|
+
return "Nothing is staged to commit. Stage a change first.";
|
|
4508
|
+
}
|
|
4509
|
+
return gitRefusal(output);
|
|
4510
|
+
}
|
|
4511
|
+
function gitRefusal(output) {
|
|
4512
|
+
const line = output.split("\n").map((candidate) => candidate.trim()).find(
|
|
4513
|
+
(candidate) => candidate.length > 0 && !candidate.startsWith("Command failed:") && !/^On branch /.test(candidate) && !/^Your branch /.test(candidate)
|
|
4514
|
+
);
|
|
4279
4515
|
if (line === void 0) {
|
|
4280
4516
|
return void 0;
|
|
4281
4517
|
}
|
|
@@ -4359,9 +4595,12 @@ function gitExec(args, cwd, timeout = 5e3) {
|
|
|
4359
4595
|
}
|
|
4360
4596
|
},
|
|
4361
4597
|
(error, stdout, stderr) => {
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4598
|
+
if (error === null) {
|
|
4599
|
+
resolve6({ ok: true, output: stdout.trim() });
|
|
4600
|
+
return;
|
|
4601
|
+
}
|
|
4602
|
+
const output = [stderr.trim(), stdout.trim()].filter((part) => part.length > 0).join("\n");
|
|
4603
|
+
resolve6({ ok: false, output: output.length > 0 ? output : error.message.trim() });
|
|
4365
4604
|
}
|
|
4366
4605
|
);
|
|
4367
4606
|
});
|
|
@@ -4370,7 +4609,7 @@ function gitExec(args, cwd, timeout = 5e3) {
|
|
|
4370
4609
|
// ../server/src/internal/workspace/workspace-index.ts
|
|
4371
4610
|
import { execFile as execFile2 } from "node:child_process";
|
|
4372
4611
|
import { readdir as readdir4 } from "node:fs/promises";
|
|
4373
|
-
import { join as
|
|
4612
|
+
import { join as join8, relative as relative4 } from "node:path";
|
|
4374
4613
|
var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set([
|
|
4375
4614
|
"node_modules",
|
|
4376
4615
|
"dist",
|
|
@@ -4481,7 +4720,7 @@ async function walk(root, directory, files) {
|
|
|
4481
4720
|
if (files.length >= MAX_INDEXED_FILES) {
|
|
4482
4721
|
return;
|
|
4483
4722
|
}
|
|
4484
|
-
const full =
|
|
4723
|
+
const full = join8(directory, entry.name);
|
|
4485
4724
|
if (entry.isDirectory()) {
|
|
4486
4725
|
if (SKIPPED_DIRECTORIES2.has(entry.name) || entry.name.startsWith(".")) {
|
|
4487
4726
|
continue;
|
|
@@ -4498,6 +4737,81 @@ function toPosix3(path) {
|
|
|
4498
4737
|
return path.split("\\").join("/");
|
|
4499
4738
|
}
|
|
4500
4739
|
|
|
4740
|
+
// ../server/src/internal/workspace/workbench-store.ts
|
|
4741
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4742
|
+
import { join as join9 } from "node:path";
|
|
4743
|
+
var LAYOUT_FILE = "workbench.json";
|
|
4744
|
+
var DRAFTS_FILE = "drafts.json";
|
|
4745
|
+
var LAYOUT_MAX_BYTES = 256 * 1024;
|
|
4746
|
+
var DRAFTS_MAX_BYTES = 32 * 1024 * 1024;
|
|
4747
|
+
function readWorkbench(dataDir) {
|
|
4748
|
+
return readSlot(join9(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
|
|
4749
|
+
}
|
|
4750
|
+
function saveWorkbench(dataDir, snapshot) {
|
|
4751
|
+
return saveSlot(dataDir, LAYOUT_FILE, snapshot, LAYOUT_MAX_BYTES);
|
|
4752
|
+
}
|
|
4753
|
+
function readDrafts(dataDir) {
|
|
4754
|
+
return readSlot(join9(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
|
|
4755
|
+
}
|
|
4756
|
+
function saveDrafts(dataDir, snapshot) {
|
|
4757
|
+
return saveSlot(dataDir, DRAFTS_FILE, snapshot, DRAFTS_MAX_BYTES);
|
|
4758
|
+
}
|
|
4759
|
+
function readSlot(path, maxBytes) {
|
|
4760
|
+
let raw;
|
|
4761
|
+
try {
|
|
4762
|
+
raw = readFileSync2(path, "utf8");
|
|
4763
|
+
} catch {
|
|
4764
|
+
return void 0;
|
|
4765
|
+
}
|
|
4766
|
+
if (raw.length > maxBytes) {
|
|
4767
|
+
return void 0;
|
|
4768
|
+
}
|
|
4769
|
+
let parsed;
|
|
4770
|
+
try {
|
|
4771
|
+
parsed = JSON.parse(raw);
|
|
4772
|
+
} catch {
|
|
4773
|
+
return void 0;
|
|
4774
|
+
}
|
|
4775
|
+
return asSnapshot(parsed);
|
|
4776
|
+
}
|
|
4777
|
+
function saveSlot(dataDir, file, snapshot, maxBytes) {
|
|
4778
|
+
const valid = asSnapshot(snapshot);
|
|
4779
|
+
if (valid === void 0) {
|
|
4780
|
+
throw new UnsupportedByHostError(`Saving ${file} needs a { version, data } snapshot.`);
|
|
4781
|
+
}
|
|
4782
|
+
const serialized = JSON.stringify(valid);
|
|
4783
|
+
if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
|
|
4784
|
+
throw new UnsupportedByHostError(`That ${file} snapshot is too large to save.`);
|
|
4785
|
+
}
|
|
4786
|
+
const target = join9(dataDir, file);
|
|
4787
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
4788
|
+
try {
|
|
4789
|
+
mkdirSync2(dataDir, { recursive: true });
|
|
4790
|
+
writeFileSync2(temp, serialized);
|
|
4791
|
+
renameSync2(temp, target);
|
|
4792
|
+
return true;
|
|
4793
|
+
} catch {
|
|
4794
|
+
try {
|
|
4795
|
+
writeFileSync2(target, serialized);
|
|
4796
|
+
return true;
|
|
4797
|
+
} catch {
|
|
4798
|
+
rmSync2(temp, { force: true });
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
function asSnapshot(value) {
|
|
4804
|
+
if (typeof value !== "object" || value === null) {
|
|
4805
|
+
return void 0;
|
|
4806
|
+
}
|
|
4807
|
+
const candidate = value;
|
|
4808
|
+
const version = candidate["version"];
|
|
4809
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
|
|
4810
|
+
return void 0;
|
|
4811
|
+
}
|
|
4812
|
+
return { version, data: candidate["data"] };
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4501
4815
|
// ../server/src/internal/ws/session-factory.service.ts
|
|
4502
4816
|
var MorseSessionFactory = class {
|
|
4503
4817
|
constructor(registry, transcripts, policy, logger, config, terminal) {
|
|
@@ -4532,6 +4846,10 @@ var MorseSessionFactory = class {
|
|
|
4532
4846
|
// the bottom panel's terminal is this host's. It runs in the viewing
|
|
4533
4847
|
// session's directory and is gated by `ProjectPolicy`, like a session.
|
|
4534
4848
|
terminal: true,
|
|
4849
|
+
// The browser host restores the tabs, panel, terminals and half-written
|
|
4850
|
+
// prompts the reader left open, kept under `<MORSE_HOME>`. VS Code has its
|
|
4851
|
+
// own tab restoration and leaves this off.
|
|
4852
|
+
workbench: true,
|
|
4535
4853
|
// A browser cannot hand a dragged file's path to pi, so the host takes the
|
|
4536
4854
|
// bytes and writes them next to the session; the frontend then `@mentions`
|
|
4537
4855
|
// the path it gets back.
|
|
@@ -4595,13 +4913,25 @@ var MorseSessionFactory = class {
|
|
|
4595
4913
|
});
|
|
4596
4914
|
return { ...listing, canOpen: this.policy.canOpen(listing.path) };
|
|
4597
4915
|
}
|
|
4916
|
+
case "readWorkbench": {
|
|
4917
|
+
return readWorkbench(this.config.dataDir) ?? null;
|
|
4918
|
+
}
|
|
4919
|
+
case "saveWorkbench": {
|
|
4920
|
+
return { ok: saveWorkbench(this.config.dataDir, args) };
|
|
4921
|
+
}
|
|
4922
|
+
case "readDrafts": {
|
|
4923
|
+
return readDrafts(this.config.dataDir) ?? null;
|
|
4924
|
+
}
|
|
4925
|
+
case "saveDrafts": {
|
|
4926
|
+
return { ok: saveDrafts(this.config.dataDir, args) };
|
|
4927
|
+
}
|
|
4598
4928
|
case "listFiles": {
|
|
4599
4929
|
const cwd = this.requireWritableCwd(context);
|
|
4600
4930
|
const files = await workspaceFiles(cwd, this.logger, { fresh: args?.fresh === true });
|
|
4601
4931
|
return { files };
|
|
4602
4932
|
}
|
|
4603
4933
|
case "readFile": {
|
|
4604
|
-
const cwd = this.
|
|
4934
|
+
const cwd = this.commandCwd(context, args);
|
|
4605
4935
|
const path = typeof args?.path === "string" ? args.path : "";
|
|
4606
4936
|
if (path.length === 0) {
|
|
4607
4937
|
throw new UnsupportedByHostError('readFile needs a "path" argument.');
|
|
@@ -4625,7 +4955,7 @@ var MorseSessionFactory = class {
|
|
|
4625
4955
|
return readGitStatus(cwd);
|
|
4626
4956
|
}
|
|
4627
4957
|
case "gitDiff": {
|
|
4628
|
-
const cwd = this.
|
|
4958
|
+
const cwd = this.commandCwd(context, args);
|
|
4629
4959
|
const path = typeof args?.path === "string" ? args.path : "";
|
|
4630
4960
|
if (path.length === 0) {
|
|
4631
4961
|
throw new UnsupportedByHostError('gitDiff needs a "path" argument.');
|
|
@@ -4646,7 +4976,7 @@ var MorseSessionFactory = class {
|
|
|
4646
4976
|
return readCommitFiles(cwd, hash);
|
|
4647
4977
|
}
|
|
4648
4978
|
case "gitCommitDiff": {
|
|
4649
|
-
const cwd = this.
|
|
4979
|
+
const cwd = this.commandCwd(context, args);
|
|
4650
4980
|
const hash = typeof args?.hash === "string" ? args.hash : "";
|
|
4651
4981
|
const path = typeof args?.path === "string" ? args.path : "";
|
|
4652
4982
|
if (path.length === 0) {
|
|
@@ -4699,6 +5029,22 @@ var MorseSessionFactory = class {
|
|
|
4699
5029
|
}
|
|
4700
5030
|
return cwd;
|
|
4701
5031
|
}
|
|
5032
|
+
/**
|
|
5033
|
+
* The directory a file command works in. A restored preview tab carries the
|
|
5034
|
+
* project it was read from (`args.cwd`), because the host may still be on
|
|
5035
|
+
* another session when that tab is loaded; the policy checks it exactly like a
|
|
5036
|
+
* terminal's directory. Without one, the viewing session's directory is used.
|
|
5037
|
+
*/
|
|
5038
|
+
commandCwd(context, args) {
|
|
5039
|
+
const requested = args?.cwd;
|
|
5040
|
+
if (typeof requested === "string" && requested.length > 0) {
|
|
5041
|
+
if (!this.policy.canOpen(requested)) {
|
|
5042
|
+
throw new UnsupportedByHostError(`Morse is not allowed to work in ${requested}.`);
|
|
5043
|
+
}
|
|
5044
|
+
return requested;
|
|
5045
|
+
}
|
|
5046
|
+
return this.requireWritableCwd(context);
|
|
5047
|
+
}
|
|
4702
5048
|
/** The cwd of the session this connection is showing. */
|
|
4703
5049
|
activeCwd() {
|
|
4704
5050
|
const key = this.registry.activeKeyOf();
|
|
@@ -4771,18 +5117,19 @@ MorseGateway = __decorateClass([
|
|
|
4771
5117
|
], MorseGateway);
|
|
4772
5118
|
|
|
4773
5119
|
// ../server/src/app/config.ts
|
|
4774
|
-
import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute6, join as
|
|
4775
|
-
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
5120
|
+
import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute6, join as join10 } from "node:path";
|
|
5121
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
5122
|
+
import { homedir as homedir4 } from "node:os";
|
|
4776
5123
|
import { fileURLToPath } from "node:url";
|
|
4777
5124
|
function loadConfig(env = process.env) {
|
|
4778
5125
|
const workspacePath = resolveWorkspacePath(env);
|
|
4779
5126
|
const configuredUiDir = env.MORSE_UI_DIR;
|
|
4780
5127
|
const moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
4781
|
-
const bundledUiDir =
|
|
4782
|
-
const monorepoUiDir =
|
|
4783
|
-
const defaultUiDir = existsSync2(
|
|
5128
|
+
const bundledUiDir = join10(moduleDir, "ui");
|
|
5129
|
+
const monorepoUiDir = join10(moduleDir, "..", "..", "..", "ui-angular", "dist");
|
|
5130
|
+
const defaultUiDir = existsSync2(join10(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
|
|
4784
5131
|
const defaultHost = "127.0.0.1";
|
|
4785
|
-
const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir :
|
|
5132
|
+
const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join10(process.cwd(), configuredUiDir) : defaultUiDir;
|
|
4786
5133
|
return {
|
|
4787
5134
|
host: env.MORSE_HOST ?? defaultHost,
|
|
4788
5135
|
port: parsePositiveInt(env.MORSE_PORT, 4399),
|
|
@@ -4797,6 +5144,8 @@ function loadConfig(env = process.env) {
|
|
|
4797
5144
|
hotSessions: parsePositiveInt(env.MORSE_HOT_SESSIONS, 4),
|
|
4798
5145
|
projects: parseList(env.MORSE_PROJECTS),
|
|
4799
5146
|
uploadDir: env.MORSE_UPLOAD_DIR?.trim() || DEFAULT_UPLOAD_DIR,
|
|
5147
|
+
dataDir: resolveDataDir(env),
|
|
5148
|
+
terminalIdleMs: parsePositiveInt(env.MORSE_TERMINAL_IDLE_MS, 30 * 6e4),
|
|
4800
5149
|
updateCheck: !isOff(env.MORSE_UPDATE_CHECK),
|
|
4801
5150
|
instance: env.MORSE_INSTANCE?.trim() || void 0
|
|
4802
5151
|
};
|
|
@@ -4804,12 +5153,16 @@ function loadConfig(env = process.env) {
|
|
|
4804
5153
|
function readFrontendIdentity(uiDir) {
|
|
4805
5154
|
try {
|
|
4806
5155
|
return frontendIdentity(
|
|
4807
|
-
parseFrontendManifest(
|
|
5156
|
+
parseFrontendManifest(readFileSync3(join10(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
|
|
4808
5157
|
);
|
|
4809
5158
|
} catch {
|
|
4810
5159
|
return void 0;
|
|
4811
5160
|
}
|
|
4812
5161
|
}
|
|
5162
|
+
function resolveDataDir(env) {
|
|
5163
|
+
const explicit = env.MORSE_HOME?.trim();
|
|
5164
|
+
return explicit && explicit.length > 0 ? explicit : join10(homedir4(), ".morse");
|
|
5165
|
+
}
|
|
4813
5166
|
function parsePositiveInt(value, fallback) {
|
|
4814
5167
|
if (!value) {
|
|
4815
5168
|
return fallback;
|
|
@@ -4835,7 +5188,7 @@ function resolveWorkspacePath(env) {
|
|
|
4835
5188
|
const start = process.cwd();
|
|
4836
5189
|
let directory = start;
|
|
4837
5190
|
for (; ; ) {
|
|
4838
|
-
const manifest =
|
|
5191
|
+
const manifest = join10(directory, "package.json");
|
|
4839
5192
|
if (existsSync2(manifest) && declaresWorkspaces(manifest)) {
|
|
4840
5193
|
return directory;
|
|
4841
5194
|
}
|
|
@@ -4848,7 +5201,7 @@ function resolveWorkspacePath(env) {
|
|
|
4848
5201
|
}
|
|
4849
5202
|
function declaresWorkspaces(manifestPath) {
|
|
4850
5203
|
try {
|
|
4851
|
-
const manifest = JSON.parse(
|
|
5204
|
+
const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
4852
5205
|
return manifest.workspaces !== void 0;
|
|
4853
5206
|
} catch {
|
|
4854
5207
|
return false;
|