@nghyane/arcane-natives 0.1.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/README.md +61 -0
- package/native/arcane_natives.darwin-arm64.node +0 -0
- package/native/pi_natives.darwin-arm64.node +0 -0
- package/package.json +163 -0
- package/src/bindings.ts +23 -0
- package/src/clipboard/index.ts +92 -0
- package/src/clipboard/types.ts +27 -0
- package/src/embedded-addon.ts +15 -0
- package/src/glob/index.ts +43 -0
- package/src/glob/types.ts +69 -0
- package/src/grep/index.ts +72 -0
- package/src/grep/types.ts +185 -0
- package/src/highlight/index.ts +9 -0
- package/src/highlight/types.ts +56 -0
- package/src/html/index.ts +19 -0
- package/src/html/types.ts +24 -0
- package/src/image/index.ts +13 -0
- package/src/image/types.ts +65 -0
- package/src/index.ts +125 -0
- package/src/keys/index.ts +9 -0
- package/src/keys/types.ts +75 -0
- package/src/native.ts +278 -0
- package/src/ps/index.ts +10 -0
- package/src/ps/types.ts +24 -0
- package/src/pty/index.ts +10 -0
- package/src/pty/types.ts +57 -0
- package/src/shell/index.ts +26 -0
- package/src/shell/types.ts +100 -0
- package/src/text/index.ts +44 -0
- package/src/text/types.ts +84 -0
- package/src/work/index.ts +11 -0
- package/src/work/types.ts +31 -0
package/src/native.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native addon loader and bindings.
|
|
3
|
+
*
|
|
4
|
+
* Each module extends NativeBindings via declaration merging in its types.ts.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { $env } from "@nghyane/arcane-utils";
|
|
11
|
+
import { getNativesDir } from "@nghyane/arcane-utils/dirs";
|
|
12
|
+
import packageJson from "../package.json";
|
|
13
|
+
import type { NativeBindings } from "./bindings";
|
|
14
|
+
import { embeddedAddon } from "./embedded-addon";
|
|
15
|
+
import "./clipboard/types";
|
|
16
|
+
import "./glob/types";
|
|
17
|
+
import "./grep/types";
|
|
18
|
+
import "./highlight/types";
|
|
19
|
+
import "./html/types";
|
|
20
|
+
import "./image/types";
|
|
21
|
+
import "./keys/types";
|
|
22
|
+
import "./ps/types";
|
|
23
|
+
import "./pty/types";
|
|
24
|
+
import "./shell/types";
|
|
25
|
+
import "./text/types";
|
|
26
|
+
import "./work/types";
|
|
27
|
+
|
|
28
|
+
export type { NativeBindings, TsFunc } from "./bindings";
|
|
29
|
+
|
|
30
|
+
type CpuVariant = "modern" | "baseline";
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const platformTag = `${process.platform}-${process.arch}`;
|
|
33
|
+
const packageVersion = (packageJson as { version: string }).version;
|
|
34
|
+
const nativeDir = path.join(import.meta.dir, "..", "native");
|
|
35
|
+
const execDir = path.dirname(process.execPath);
|
|
36
|
+
const versionedDir = path.join(getNativesDir(), packageVersion);
|
|
37
|
+
const userDataDir =
|
|
38
|
+
process.platform === "win32"
|
|
39
|
+
? path.join(Bun.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "arcane")
|
|
40
|
+
: path.join(os.homedir(), ".local", "bin");
|
|
41
|
+
const isCompiledBinary =
|
|
42
|
+
Bun.env.ARCANE_COMPILED ||
|
|
43
|
+
import.meta.url.includes("$bunfs") ||
|
|
44
|
+
import.meta.url.includes("~BUN") ||
|
|
45
|
+
import.meta.url.includes("%7EBUN");
|
|
46
|
+
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
|
|
47
|
+
|
|
48
|
+
const variantOverride = getVariantOverride();
|
|
49
|
+
const selectedVariant = resolveCpuVariant(variantOverride);
|
|
50
|
+
const addonFilenames = getAddonFilenames(platformTag, selectedVariant);
|
|
51
|
+
const addonLabel = selectedVariant ? `${platformTag} (${selectedVariant})` : platformTag;
|
|
52
|
+
|
|
53
|
+
const debugCandidates = [
|
|
54
|
+
path.join(nativeDir, "arcane_natives.dev.node"),
|
|
55
|
+
path.join(execDir, "arcane_natives.dev.node"),
|
|
56
|
+
];
|
|
57
|
+
const baseReleaseCandidates = addonFilenames.flatMap(filename => [
|
|
58
|
+
path.join(nativeDir, filename),
|
|
59
|
+
path.join(execDir, filename),
|
|
60
|
+
]);
|
|
61
|
+
const compiledCandidates = addonFilenames.flatMap(filename => [
|
|
62
|
+
path.join(versionedDir, filename),
|
|
63
|
+
path.join(userDataDir, filename),
|
|
64
|
+
]);
|
|
65
|
+
const releaseCandidates = isCompiledBinary ? [...compiledCandidates, ...baseReleaseCandidates] : baseReleaseCandidates;
|
|
66
|
+
const candidates = $env.ARCANE_DEV ? [...debugCandidates, ...releaseCandidates] : releaseCandidates;
|
|
67
|
+
const dedupedCandidates = [...new Set(candidates)];
|
|
68
|
+
|
|
69
|
+
function runCommand(command: string, args: string[]): string | null {
|
|
70
|
+
try {
|
|
71
|
+
const result = Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
72
|
+
if (result.exitCode !== 0) return null;
|
|
73
|
+
return result.stdout.toString("utf-8").trim();
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getVariantOverride(): CpuVariant | null {
|
|
80
|
+
const value = Bun.env.ARCANE_NATIVE_VARIANT;
|
|
81
|
+
if (!value) return null;
|
|
82
|
+
if (value === "modern" || value === "baseline") return value;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectAvx2Support(): boolean {
|
|
87
|
+
if (process.arch !== "x64") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (process.platform === "linux") {
|
|
92
|
+
try {
|
|
93
|
+
const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
|
|
94
|
+
return /\bavx2\b/i.test(cpuInfo);
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (process.platform === "darwin") {
|
|
101
|
+
const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
|
|
102
|
+
if (leaf7 && /\bAVX2\b/i.test(leaf7)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
|
|
106
|
+
return Boolean(features && /\bAVX2\b/i.test(features));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (process.platform === "win32") {
|
|
110
|
+
const output = runCommand("powershell.exe", [
|
|
111
|
+
"-NoProfile",
|
|
112
|
+
"-NonInteractive",
|
|
113
|
+
"-Command",
|
|
114
|
+
"[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
|
|
115
|
+
]);
|
|
116
|
+
return output?.toLowerCase() === "true";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveCpuVariant(override: CpuVariant | null): CpuVariant | null {
|
|
123
|
+
if (process.arch !== "x64") return null;
|
|
124
|
+
if (override) return override;
|
|
125
|
+
return detectAvx2Support() ? "modern" : "baseline";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getAddonFilenames(tag: string, variant: CpuVariant | null): string[] {
|
|
129
|
+
const defaultFilename = `arcane_natives.${tag}.node`;
|
|
130
|
+
if (process.arch !== "x64" || !variant) return [defaultFilename];
|
|
131
|
+
const baselineFilename = `arcane_natives.${tag}-baseline.node`;
|
|
132
|
+
const modernFilename = `arcane_natives.${tag}-modern.node`;
|
|
133
|
+
if (variant === "modern") {
|
|
134
|
+
return [modernFilename, baselineFilename, defaultFilename];
|
|
135
|
+
}
|
|
136
|
+
return [baselineFilename, defaultFilename];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function selectEmbeddedAddonFile(): { filename: string; filePath: string } | null {
|
|
140
|
+
if (!embeddedAddon) return null;
|
|
141
|
+
const defaultFile = embeddedAddon.files.find(file => file.variant === "default") ?? null;
|
|
142
|
+
if (process.arch !== "x64") return defaultFile ?? embeddedAddon.files[0] ?? null;
|
|
143
|
+
if (selectedVariant === "modern") {
|
|
144
|
+
return (
|
|
145
|
+
embeddedAddon.files.find(file => file.variant === "modern") ??
|
|
146
|
+
embeddedAddon.files.find(file => file.variant === "baseline") ??
|
|
147
|
+
null
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return embeddedAddon.files.find(file => file.variant === "baseline") ?? null;
|
|
151
|
+
}
|
|
152
|
+
function maybeExtractEmbeddedAddon(errors: string[]): string | null {
|
|
153
|
+
if (!isCompiledBinary || !embeddedAddon) return null;
|
|
154
|
+
if (embeddedAddon.platformTag !== platformTag || embeddedAddon.version !== packageVersion) return null;
|
|
155
|
+
|
|
156
|
+
const selectedEmbeddedFile = selectEmbeddedAddonFile();
|
|
157
|
+
if (!selectedEmbeddedFile) return null;
|
|
158
|
+
const targetPath = path.join(versionedDir, selectedEmbeddedFile.filename);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
fs.mkdirSync(versionedDir, { recursive: true });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
errors.push(`embedded addon dir: ${message}`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (fs.existsSync(targetPath)) {
|
|
169
|
+
return targetPath;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
|
|
174
|
+
fs.writeFileSync(targetPath, buffer);
|
|
175
|
+
return targetPath;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
178
|
+
errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function loadNative(): NativeBindings {
|
|
183
|
+
const errors: string[] = [];
|
|
184
|
+
const embeddedCandidate = maybeExtractEmbeddedAddon(errors);
|
|
185
|
+
const runtimeCandidates = embeddedCandidate ? [embeddedCandidate, ...dedupedCandidates] : dedupedCandidates;
|
|
186
|
+
for (const candidate of runtimeCandidates) {
|
|
187
|
+
try {
|
|
188
|
+
const bindings = require(candidate) as NativeBindings;
|
|
189
|
+
validateNative(bindings, candidate);
|
|
190
|
+
if ($env.ARCANE_DEV) {
|
|
191
|
+
console.log(`Loaded native addon from ${candidate}`);
|
|
192
|
+
}
|
|
193
|
+
return bindings;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if ($env.ARCANE_DEV) {
|
|
196
|
+
console.error(`Error loading native addon from ${candidate}:`, err);
|
|
197
|
+
}
|
|
198
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
199
|
+
errors.push(`${candidate}: ${message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Check if this is an unsupported platform
|
|
203
|
+
if (!SUPPORTED_PLATFORMS.includes(platformTag)) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Unsupported platform: ${platformTag}\n` +
|
|
206
|
+
`Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}\n` +
|
|
207
|
+
"If you need support for this platform, please open an issue.",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const details = errors.map(error => `- ${error}`).join("\n");
|
|
211
|
+
let helpMessage: string;
|
|
212
|
+
if (isCompiledBinary) {
|
|
213
|
+
const expectedPaths = addonFilenames.map(filename => ` ${path.join(versionedDir, filename)}`).join("\n");
|
|
214
|
+
const downloadHints = addonFilenames
|
|
215
|
+
.map(filename => {
|
|
216
|
+
const downloadUrl = `https://github.com/nghyane/arcane/releases/latest/download/${filename}`;
|
|
217
|
+
const targetPath = path.join(versionedDir, filename);
|
|
218
|
+
return ` curl -fsSL "${downloadUrl}" -o "${targetPath}"`;
|
|
219
|
+
})
|
|
220
|
+
.join("\n");
|
|
221
|
+
helpMessage =
|
|
222
|
+
`The compiled binary should extract one of:\n${expectedPaths}\n\n` +
|
|
223
|
+
`If missing, delete ${versionedDir} and re-run, or download manually:\n${downloadHints}`;
|
|
224
|
+
} else {
|
|
225
|
+
helpMessage =
|
|
226
|
+
"If installed via npm/bun, try reinstalling: bun install @nghyane/arcane-natives\n" +
|
|
227
|
+
"If developing locally, build with: bun --cwd=packages/natives run build:native\n" +
|
|
228
|
+
"Optional x64 variants: TARGET_VARIANT=baseline|modern bun --cwd=packages/natives run build:native";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Failed to load arcane_natives native addon for ${addonLabel}.\n\nTried:\n${details}\n\n${helpMessage}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function validateNative(bindings: NativeBindings, source: string): void {
|
|
236
|
+
const missing: string[] = [];
|
|
237
|
+
const checkFn = (name: keyof NativeBindings) => {
|
|
238
|
+
if (typeof bindings[name] !== "function") {
|
|
239
|
+
missing.push(name);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
checkFn("copyToClipboard");
|
|
243
|
+
checkFn("readImageFromClipboard");
|
|
244
|
+
checkFn("glob");
|
|
245
|
+
checkFn("fuzzyFind");
|
|
246
|
+
checkFn("grep");
|
|
247
|
+
checkFn("search");
|
|
248
|
+
checkFn("hasMatch");
|
|
249
|
+
checkFn("htmlToMarkdown");
|
|
250
|
+
checkFn("highlightCode");
|
|
251
|
+
checkFn("supportsLanguage");
|
|
252
|
+
checkFn("getSupportedLanguages");
|
|
253
|
+
checkFn("truncateToWidth");
|
|
254
|
+
checkFn("sanitizeText");
|
|
255
|
+
checkFn("wrapTextWithAnsi");
|
|
256
|
+
checkFn("sliceWithWidth");
|
|
257
|
+
checkFn("extractSegments");
|
|
258
|
+
checkFn("matchesKittySequence");
|
|
259
|
+
checkFn("executeShell");
|
|
260
|
+
checkFn("PtySession");
|
|
261
|
+
checkFn("Shell");
|
|
262
|
+
checkFn("parseKey");
|
|
263
|
+
checkFn("matchesLegacySequence");
|
|
264
|
+
checkFn("parseKittySequence");
|
|
265
|
+
checkFn("matchesKey");
|
|
266
|
+
checkFn("visibleWidth");
|
|
267
|
+
checkFn("killTree");
|
|
268
|
+
checkFn("listDescendants");
|
|
269
|
+
checkFn("getWorkProfile");
|
|
270
|
+
checkFn("invalidateFsScanCache");
|
|
271
|
+
if (missing.length) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Native addon missing exports (${source}). Missing: ${missing.join(", ")}. ` +
|
|
274
|
+
"Rebuild with `bun --cwd=packages/natives run build:native`.",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export const native = loadNative();
|
package/src/ps/index.ts
ADDED
package/src/ps/types.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for process management.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {};
|
|
6
|
+
|
|
7
|
+
declare module "../bindings" {
|
|
8
|
+
/** Native process-management bindings implemented in arcane-natives. */
|
|
9
|
+
interface NativeBindings {
|
|
10
|
+
/**
|
|
11
|
+
* Kill a process and all its descendants using platform-native APIs.
|
|
12
|
+
* @param pid Root process id.
|
|
13
|
+
* @param signal Signal number (ignored on Windows).
|
|
14
|
+
* @returns Number of processes successfully killed.
|
|
15
|
+
*/
|
|
16
|
+
killTree(pid: number, signal: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* List all descendant PIDs of a process (children, grandchildren, etc.).
|
|
19
|
+
* @param pid Root process id.
|
|
20
|
+
* @returns Empty array when the process has no children or doesn't exist.
|
|
21
|
+
*/
|
|
22
|
+
listDescendants(pid: number): number[];
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/pty/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY-backed interactive execution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { native } from "../native";
|
|
6
|
+
|
|
7
|
+
export type { PtyRunResult, PtySessionConstructor, PtyStartOptions } from "./types";
|
|
8
|
+
|
|
9
|
+
export const { PtySession } = native;
|
|
10
|
+
export type PtySession = import("./types").PtySession;
|
package/src/pty/types.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for PTY-backed interactive execution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Cancellable, TsFunc } from "../bindings";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for starting a command in a pseudo-terminal session.
|
|
9
|
+
*/
|
|
10
|
+
export interface PtyStartOptions extends Cancellable {
|
|
11
|
+
/** Command to execute. */
|
|
12
|
+
command: string;
|
|
13
|
+
/** Working directory for command execution. */
|
|
14
|
+
cwd?: string;
|
|
15
|
+
/** Environment variables for this command. */
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
/** PTY column count. */
|
|
18
|
+
cols?: number;
|
|
19
|
+
/** PTY row count. */
|
|
20
|
+
rows?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Result of a PTY command run.
|
|
25
|
+
*/
|
|
26
|
+
export interface PtyRunResult {
|
|
27
|
+
/** Exit code of the command, if available. */
|
|
28
|
+
exitCode?: number;
|
|
29
|
+
/** Whether the command was cancelled by abort signal or kill request. */
|
|
30
|
+
cancelled: boolean;
|
|
31
|
+
/** Whether the command timed out. */
|
|
32
|
+
timedOut: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Stateful PTY session instance. */
|
|
36
|
+
export interface PtySession {
|
|
37
|
+
/** Start command execution and stream output while it runs. */
|
|
38
|
+
start(options: PtyStartOptions, onChunk?: TsFunc<string>): Promise<PtyRunResult>;
|
|
39
|
+
/** Write raw input bytes to PTY stdin. */
|
|
40
|
+
write(data: string): void;
|
|
41
|
+
/** Resize active PTY. */
|
|
42
|
+
resize(cols: number, rows: number): void;
|
|
43
|
+
/** Force-kill active command. */
|
|
44
|
+
kill(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Native PTY session constructor. */
|
|
48
|
+
export interface PtySessionConstructor {
|
|
49
|
+
new (): PtySession;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare module "../bindings" {
|
|
53
|
+
interface NativeBindings {
|
|
54
|
+
/** Stateful PTY session constructor for interactive terminal passthrough. */
|
|
55
|
+
PtySession: PtySessionConstructor;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native shell execution via brush-core.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { native } from "../native";
|
|
6
|
+
import type { ShellExecuteOptions, ShellExecuteResult } from "./types";
|
|
7
|
+
|
|
8
|
+
export type { ShellExecuteOptions, ShellExecuteResult, ShellOptions, ShellRunOptions, ShellRunResult } from "./types";
|
|
9
|
+
|
|
10
|
+
export const { Shell } = native;
|
|
11
|
+
export type Shell = import("./types").Shell;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute a shell command using brush-core.
|
|
15
|
+
*
|
|
16
|
+
* @param options - Execution options including command, cwd, env, timeout
|
|
17
|
+
* @param onChunk - Optional callback for streaming output chunks
|
|
18
|
+
* @returns Promise resolving to execution result with exit code and status
|
|
19
|
+
*/
|
|
20
|
+
export async function executeShell(
|
|
21
|
+
options: ShellExecuteOptions,
|
|
22
|
+
onChunk?: (chunk: string) => void,
|
|
23
|
+
): Promise<ShellExecuteResult> {
|
|
24
|
+
const wrappedCallback = onChunk ? (err: Error | null, chunk: string) => !err && onChunk(chunk) : undefined;
|
|
25
|
+
return native.executeShell(options, wrappedCallback);
|
|
26
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for shell execution via brush-core.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Cancellable, TsFunc } from "../bindings";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for a persistent brush-core shell session.
|
|
9
|
+
*/
|
|
10
|
+
export interface ShellOptions {
|
|
11
|
+
/** Environment variables to set once per session. */
|
|
12
|
+
sessionEnv?: Record<string, string>;
|
|
13
|
+
/** Optional snapshot path to source for bash sessions. */
|
|
14
|
+
snapshotPath?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for running a single shell command.
|
|
19
|
+
*/
|
|
20
|
+
export interface ShellRunOptions extends Cancellable {
|
|
21
|
+
/** The command to execute. */
|
|
22
|
+
command: string;
|
|
23
|
+
/** Working directory for command execution. */
|
|
24
|
+
cwd?: string;
|
|
25
|
+
/** Environment variables to apply for this command. */
|
|
26
|
+
env?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Result of running a shell command via brush-core.
|
|
31
|
+
*/
|
|
32
|
+
export interface ShellRunResult {
|
|
33
|
+
/** Exit code of the command (undefined if cancelled or timed out). */
|
|
34
|
+
exitCode?: number;
|
|
35
|
+
/** Whether the command was cancelled via abort. */
|
|
36
|
+
cancelled: boolean;
|
|
37
|
+
/** Whether the command timed out. */
|
|
38
|
+
timedOut: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Internal options for the native brush-core binding.
|
|
43
|
+
*/
|
|
44
|
+
export interface ShellExecuteOptions extends Cancellable {
|
|
45
|
+
/** The command to execute. */
|
|
46
|
+
command: string;
|
|
47
|
+
/** Working directory for command execution. */
|
|
48
|
+
cwd?: string;
|
|
49
|
+
/** Environment variables to apply for this command. */
|
|
50
|
+
env?: Record<string, string>;
|
|
51
|
+
/** Environment variables to set once per session. */
|
|
52
|
+
sessionEnv?: Record<string, string>;
|
|
53
|
+
/** Optional snapshot path to source for bash sessions. */
|
|
54
|
+
snapshotPath?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
/** Internal result from the native brush-core binding. */
|
|
59
|
+
export interface ShellExecuteResult extends ShellRunResult {}
|
|
60
|
+
|
|
61
|
+
/** Native Shell class instance. */
|
|
62
|
+
export interface Shell {
|
|
63
|
+
/**
|
|
64
|
+
* Run a command in the shell.
|
|
65
|
+
* @param options Command execution options.
|
|
66
|
+
* @param onChunk Optional callback for streamed output.
|
|
67
|
+
* @returns Promise resolving to the command result.
|
|
68
|
+
*/
|
|
69
|
+
run(options: ShellRunOptions, onChunk?: TsFunc<string>): Promise<ShellRunResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Abort all running commands in this session.
|
|
72
|
+
* @param reason Optional reason for the abort.
|
|
73
|
+
*/
|
|
74
|
+
abort(reason?: string): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Native Shell class constructor. */
|
|
78
|
+
export interface ShellConstructor {
|
|
79
|
+
/**
|
|
80
|
+
* Create a new shell session.
|
|
81
|
+
* @param options Optional session configuration.
|
|
82
|
+
*/
|
|
83
|
+
new (options?: ShellOptions): Shell;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declare module "../bindings" {
|
|
87
|
+
/** Native bindings exposed by the shell module. */
|
|
88
|
+
interface NativeBindings {
|
|
89
|
+
/**
|
|
90
|
+
* Execute a shell command with explicit session metadata.
|
|
91
|
+
* @param options Execution options including session identifiers.
|
|
92
|
+
* @param onChunk Optional callback for streamed output.
|
|
93
|
+
* @returns Promise resolving to the command result.
|
|
94
|
+
*/
|
|
95
|
+
executeShell(options: ShellExecuteOptions, onChunk?: TsFunc<string>): Promise<ShellExecuteResult>;
|
|
96
|
+
|
|
97
|
+
/** Shell class constructor for creating sessions. */
|
|
98
|
+
Shell: ShellConstructor;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI-aware text utilities powered by native bindings.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Ellipsis, type SliceWithWidthResult } from "@nghyane/arcane-natives";
|
|
6
|
+
import { native } from "../native";
|
|
7
|
+
|
|
8
|
+
export type { ExtractSegmentsResult, SliceWithWidthResult } from "./types";
|
|
9
|
+
export { Ellipsis } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Truncate text to fit within a maximum visible width, adding ellipsis if needed.
|
|
13
|
+
* Optionally pad with spaces to reach exactly maxWidth.
|
|
14
|
+
* Properly handles ANSI escape codes (they don't count toward width).
|
|
15
|
+
*
|
|
16
|
+
* @param text - Text to truncate (may contain ANSI codes)
|
|
17
|
+
* @param maxWidth - Maximum visible width
|
|
18
|
+
* @param ellipsis - Ellipsis kind to append when truncating (default: Unicode "…")
|
|
19
|
+
* @param pad - If true, pad result with spaces to exactly maxWidth (default: false)
|
|
20
|
+
* @returns Truncated text, optionally padded to exactly maxWidth
|
|
21
|
+
*/
|
|
22
|
+
export function truncateToWidth(
|
|
23
|
+
text: string,
|
|
24
|
+
maxWidth: number,
|
|
25
|
+
ellipsis: Ellipsis = Ellipsis.Unicode,
|
|
26
|
+
pad = false,
|
|
27
|
+
): string {
|
|
28
|
+
return native.truncateToWidth(text, maxWidth, ellipsis, pad);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Slice a range of visible columns from a line.
|
|
33
|
+
* @param line - The line to slice
|
|
34
|
+
* @param startCol - The starting column
|
|
35
|
+
* @param length - The length of the slice
|
|
36
|
+
* @param strict - Whether to strictly enforce the length
|
|
37
|
+
* @returns The sliced line
|
|
38
|
+
*/
|
|
39
|
+
export function sliceWithWidth(line: string, startCol: number, length: number, strict = false): SliceWithWidthResult {
|
|
40
|
+
if (length <= 0) return { text: "", width: 0 };
|
|
41
|
+
return native.sliceWithWidth(line, startCol, length, strict);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const { wrapTextWithAnsi, visibleWidth, extractSegments, sanitizeText } = native;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for ANSI-aware text utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Result of slicing a line by visible columns. */
|
|
6
|
+
export interface SliceWithWidthResult {
|
|
7
|
+
/** UTF-16 slice containing the selected text. */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Visible width of the slice in terminal cells. */
|
|
10
|
+
width: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Result of extracting before/after overlay segments. */
|
|
14
|
+
export interface ExtractSegmentsResult {
|
|
15
|
+
/** UTF-16 content before the overlay region. */
|
|
16
|
+
before: string;
|
|
17
|
+
/** Visible width of the `before` segment. */
|
|
18
|
+
beforeWidth: number;
|
|
19
|
+
/** UTF-16 content after the overlay region. */
|
|
20
|
+
after: string;
|
|
21
|
+
/** Visible width of the `after` segment. */
|
|
22
|
+
afterWidth: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Ellipsis strategy for truncation. */
|
|
26
|
+
export const enum Ellipsis {
|
|
27
|
+
/** Use a single Unicode ellipsis character ("…"). */
|
|
28
|
+
Unicode = 0,
|
|
29
|
+
/** Use three ASCII dots ("..."). */
|
|
30
|
+
Ascii = 1,
|
|
31
|
+
/** Omit ellipsis entirely. */
|
|
32
|
+
Omit = 2,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare module "../bindings" {
|
|
36
|
+
interface NativeBindings {
|
|
37
|
+
/**
|
|
38
|
+
* Truncate text to a visible width, optionally padding with spaces.
|
|
39
|
+
* @param text UTF-16 input text.
|
|
40
|
+
* @param maxWidth Maximum visible width in terminal cells.
|
|
41
|
+
* @param ellipsisKind Ellipsis strategy (see {@link Ellipsis}).
|
|
42
|
+
* @param pad Whether to pad the output to `maxWidth`.
|
|
43
|
+
*/
|
|
44
|
+
truncateToWidth(text: string, maxWidth: number, ellipsisKind: number, pad: boolean): string;
|
|
45
|
+
/**
|
|
46
|
+
* Sanitize text output: strip ANSI codes, remove binary garbage, normalize line endings.
|
|
47
|
+
*/
|
|
48
|
+
sanitizeText(text: string): string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wrap text to a visible width, preserving ANSI codes across line breaks.
|
|
52
|
+
* @param text UTF-16 input text with optional ANSI escapes.
|
|
53
|
+
* @param width Maximum visible width per line.
|
|
54
|
+
*/
|
|
55
|
+
wrapTextWithAnsi(text: string, width: number): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Slice a range of visible columns from a line.
|
|
58
|
+
* @param line UTF-16 input line with optional ANSI escapes.
|
|
59
|
+
* @param startCol Starting column in terminal cells.
|
|
60
|
+
* @param length Number of visible cells to include.
|
|
61
|
+
* @param strict Whether to drop graphemes that overflow the range.
|
|
62
|
+
*/
|
|
63
|
+
sliceWithWidth(line: string, startCol: number, length: number, strict: boolean): SliceWithWidthResult;
|
|
64
|
+
/**
|
|
65
|
+
* Measure the visible width of text (excluding ANSI codes).
|
|
66
|
+
* @param text UTF-16 input text with optional ANSI escapes.
|
|
67
|
+
*/
|
|
68
|
+
visibleWidth(text: string): number;
|
|
69
|
+
/** Extract before/after segments around an overlay region.
|
|
70
|
+
* @param line UTF-16 input line with optional ANSI escapes.
|
|
71
|
+
* @param beforeEnd Column where the "before" segment ends.
|
|
72
|
+
* @param afterStart Column where the "after" segment starts.
|
|
73
|
+
* @param afterLen Visible width of the "after" segment.
|
|
74
|
+
* @param strictAfter Whether to drop graphemes that overflow `afterLen`.
|
|
75
|
+
*/
|
|
76
|
+
extractSegments(
|
|
77
|
+
line: string,
|
|
78
|
+
beforeEnd: number,
|
|
79
|
+
afterStart: number,
|
|
80
|
+
afterLen: number,
|
|
81
|
+
strictAfter: boolean,
|
|
82
|
+
): ExtractSegmentsResult;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work scheduling profiling via native instrumentation.
|
|
3
|
+
*
|
|
4
|
+
* Always-on profiling - samples are collected into a circular buffer.
|
|
5
|
+
* Call `getWorkProfile()` to retrieve recent activity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { native } from "../native";
|
|
9
|
+
|
|
10
|
+
export type { WorkProfile } from "./types";
|
|
11
|
+
export const { getWorkProfile } = native;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for work scheduling profiling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Profiling results from work scheduling instrumentation.
|
|
7
|
+
*/
|
|
8
|
+
export interface WorkProfile {
|
|
9
|
+
/** Folded stack format for flamegraph tools. */
|
|
10
|
+
folded: string;
|
|
11
|
+
/** Markdown summary of profiling results. */
|
|
12
|
+
summary: string;
|
|
13
|
+
/** SVG flamegraph (if generation succeeded). */
|
|
14
|
+
svg: string | null;
|
|
15
|
+
/** Total work time in milliseconds. */
|
|
16
|
+
totalMs: number;
|
|
17
|
+
/** Number of samples collected. */
|
|
18
|
+
sampleCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare module "../bindings" {
|
|
22
|
+
interface NativeBindings {
|
|
23
|
+
/**
|
|
24
|
+
* Get work profile data from the last N seconds.
|
|
25
|
+
*
|
|
26
|
+
* Always-on profiling - samples are collected into a circular buffer.
|
|
27
|
+
* Call this to retrieve recent activity.
|
|
28
|
+
*/
|
|
29
|
+
getWorkProfile(lastSeconds: number): WorkProfile;
|
|
30
|
+
}
|
|
31
|
+
}
|