tauri-kargo-tools 0.1.0 → 0.1.2
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/api.ts +218 -0
- package/dist/test.ts +324 -0
- package/dist/types.ts +178 -0
- package/package.json +4 -5
package/dist/api.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// dist/client.ts
|
|
2
|
+
import * as T from "./types";
|
|
3
|
+
|
|
4
|
+
export type FetchLike = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
export interface ClientOptions {
|
|
7
|
+
/** Exemple: "http://127.0.0.1:5173" ; si absent, utilise 127.0.0.1:8080 */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Port à utiliser si baseUrl est omis */
|
|
10
|
+
port?: number | string;
|
|
11
|
+
/** En-têtes par défaut à ajouter à chaque requête */
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/** fetch custom (ex. node-fetch) si l’environnement ne fournit pas fetch */
|
|
14
|
+
fetchImpl?: FetchLike;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class TauriKargoClient {
|
|
18
|
+
readonly baseUrl: string;
|
|
19
|
+
readonly headers: Record<string, string>;
|
|
20
|
+
private readonly fetchImpl: FetchLike;
|
|
21
|
+
|
|
22
|
+
constructor(opts: ClientOptions = {}) {
|
|
23
|
+
const port = opts.port ?? 8080;
|
|
24
|
+
this.baseUrl = opts.baseUrl ?? (opts.port ?`http://127.0.0.1:${port}`:'');
|
|
25
|
+
this.headers = opts.headers ?? {};
|
|
26
|
+
this.fetchImpl = opts.fetchImpl ?? (globalThis.fetch?.bind(globalThis) as FetchLike);
|
|
27
|
+
if (!this.fetchImpl) throw new Error("No fetch implementation available.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* =============== Helpers HTTP =============== */
|
|
31
|
+
|
|
32
|
+
private req(path: string, init: RequestInit): Promise<Response> {
|
|
33
|
+
const url = this.baseUrl + path;
|
|
34
|
+
const headers = { ...this.headers, ...(init.headers || {}) } as Record<string, string>;
|
|
35
|
+
return this.fetchImpl(url, { ...init, headers });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async postJson<R>(path: string, body: unknown): Promise<R> {
|
|
39
|
+
const res = await this.req(path, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
body: body == null ? undefined : JSON.stringify(body),
|
|
43
|
+
});
|
|
44
|
+
return this.parseJsonOrThrow<R>(res);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async parseJsonOrThrow<R>(res: Response): Promise<R> {
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const text = await this.safeText(res);
|
|
50
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
|
|
51
|
+
}
|
|
52
|
+
const ct = res.headers.get("content-type") || "";
|
|
53
|
+
if (ct.includes("application/json")) {
|
|
54
|
+
return (await res.json()) as R;
|
|
55
|
+
}
|
|
56
|
+
// Tolérance: certains endpoints peuvent répondre text/plain en erreur
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(text) as R;
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`Unexpected content-type: ${ct || "unknown"}; body: ${text}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async safeText(res: Response): Promise<string> {
|
|
66
|
+
try { return await res.text(); } catch { return "<no-body>"; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* =============== Routes: Embed =============== */
|
|
70
|
+
|
|
71
|
+
/** POST /api/embed */
|
|
72
|
+
embed(body: T.EmbedReqAny): Promise<T.EmbedResp> {
|
|
73
|
+
return this.postJson<T.EmbedResp>("/api/embed", body);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* =============== Routes: Config =============== */
|
|
77
|
+
|
|
78
|
+
/** POST /api/useConfig */
|
|
79
|
+
useConfig(body: T.UseConfigReq): Promise<T.UseConfigResp> {
|
|
80
|
+
return this.postJson<T.UseConfigResp>("/api/useConfig", body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** POST /api/get-config */
|
|
84
|
+
getConfig(): Promise<T.GetConfigResp> {
|
|
85
|
+
return this.postJson<T.GetConfigResp>("/api/get-config", {});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* =============== Routes: Files =============== */
|
|
89
|
+
|
|
90
|
+
/** POST /api/current-directory */
|
|
91
|
+
setCurrentDirectory(body: T.CurrentDirReq): Promise<T.CurrentDirResp> {
|
|
92
|
+
return this.postJson<T.CurrentDirResp>("/api/current-directory", body);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Lire un fichier texte relatif au répertoire courant.
|
|
97
|
+
* (POST sans corps ⇒ READ ; renvoie text/plain ou octet-stream)
|
|
98
|
+
*/
|
|
99
|
+
async readFileText(file: string, encoding: string = "utf-8"): Promise<string> {
|
|
100
|
+
const res = await this.req(`/api/file/${encodeURIComponent(file)}`, { method: "POST" });
|
|
101
|
+
if (!res.ok) throw new Error(`READ ${file} failed: ${res.status} ${await this.safeText(res)}`);
|
|
102
|
+
const ct = res.headers.get("content-type") || "";
|
|
103
|
+
if (ct.includes("application/octet-stream")) {
|
|
104
|
+
const buf = await res.arrayBuffer();
|
|
105
|
+
return new TextDecoder(encoding).decode(buf);
|
|
106
|
+
}
|
|
107
|
+
return res.text();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Lire un fichier binaire */
|
|
111
|
+
async readFileBinary(file: string): Promise<ArrayBuffer> {
|
|
112
|
+
const res = await this.req(`/api/file/${encodeURIComponent(file)}`, { method: "POST" });
|
|
113
|
+
if (!res.ok) throw new Error(`READ ${file} failed: ${res.status} ${await this.safeText(res)}`);
|
|
114
|
+
return res.arrayBuffer();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Écrire un fichier texte (UTF-8) */
|
|
118
|
+
async writeFileText(file: string, content: string): Promise<T.FileWriteResp> {
|
|
119
|
+
const res = await this.req(`/api/file/${encodeURIComponent(file)}`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
122
|
+
body: content,
|
|
123
|
+
});
|
|
124
|
+
return this.parseJsonOrThrow<T.FileWriteResp>(res);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Écrire un fichier binaire */
|
|
128
|
+
async writeFileBinary(
|
|
129
|
+
file: string,
|
|
130
|
+
data: Blob
|
|
131
|
+
): Promise<T.FileWriteResp> {
|
|
132
|
+
// Normalise en Blob (OK WebView2/WebKit/Chromium)
|
|
133
|
+
const blob = data
|
|
134
|
+
|
|
135
|
+
const res = await fetch(`${this.baseUrl}/api/file/${encodeURIComponent(file)}`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
// Laisser fetch gérer le Content-Length; Content-Type vient du blob
|
|
138
|
+
headers: { "content-type": blob.type || "application/octet-stream" },
|
|
139
|
+
body: blob,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
const text = await res.text().catch(() => "<no-body>");
|
|
144
|
+
throw new Error(`WRITE ${file} failed: ${res.status} ${text}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// La route renvoie du JSON en cas d’écriture
|
|
148
|
+
const ct = res.headers.get("content-type") || "";
|
|
149
|
+
if (ct.includes("application/json")) {
|
|
150
|
+
return (await res.json()) as T.FileWriteResp;
|
|
151
|
+
}
|
|
152
|
+
// Fallback s'il répond "text/plain" avec un JSON sérialisé
|
|
153
|
+
return JSON.parse(await res.text()) as T.FileWriteResp;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** DELETE /api/file/{file} */
|
|
157
|
+
async deleteFile(file: string): Promise<T.FileDeleteResp> {
|
|
158
|
+
const res = await this.req(`/api/file/${encodeURIComponent(file)}`, { method: "DELETE" });
|
|
159
|
+
return this.parseJsonOrThrow<T.FileDeleteResp>(res);
|
|
160
|
+
// En cas d'erreur 4xx/5xx, parseJsonOrThrow lève déjà une exception
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* =============== Routes: Run (process) =============== */
|
|
164
|
+
|
|
165
|
+
/** POST /api/run */
|
|
166
|
+
run(body: T.RunReq): Promise<T.RunResp> {
|
|
167
|
+
return this.postJson<T.RunResp>("/api/run", body);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** POST /api/run/status */
|
|
171
|
+
runStatus(body: T.ProcIdReq): Promise<T.ProcStatusResp> {
|
|
172
|
+
return this.postJson<T.ProcStatusResp>("/api/run/status", body);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** POST /api/run/stop */
|
|
176
|
+
runStop(body: T.ProcIdReq): Promise<T.ProcStopResp> {
|
|
177
|
+
return this.postJson<T.ProcStopResp>("/api/run/stop", body);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** POST /api/run/stopAll */
|
|
181
|
+
runStopAll(): Promise<T.StopAllResp> {
|
|
182
|
+
return this.postJson<T.StopAllResp>("/api/run/stopAll", {});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* =============== Routes: Explorer =============== */
|
|
186
|
+
|
|
187
|
+
/** POST /api/explorer */
|
|
188
|
+
explorer(body: T.ExplorerReq): Promise<T.ExplorerResult> {
|
|
189
|
+
return this.postJson<T.ExplorerResult>("/api/explorer", body);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* =============== Routes: Servers =============== */
|
|
193
|
+
|
|
194
|
+
/** POST /api/newServer */
|
|
195
|
+
newServer(body: T.NewServerReq): Promise<T.NewServerResp> {
|
|
196
|
+
return this.postJson<T.NewServerResp>("/api/newServer", body);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** POST /api/stop */
|
|
200
|
+
stopServer(body: T.StopServerReq = {}): Promise<T.StopServerResp> {
|
|
201
|
+
return this.postJson<T.StopServerResp>("/api/stop", body);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* =============== Petit helper factory =============== */
|
|
206
|
+
export function createClient(opts?: ClientOptions) {
|
|
207
|
+
return new TauriKargoClient(opts);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* =============== Exemple d'usage =============== */
|
|
211
|
+
/*
|
|
212
|
+
const api = createClient({ port: 5173 });
|
|
213
|
+
await api.useConfig({ code: "C:/code", executable: "C:/bin" });
|
|
214
|
+
const txt = await api.readFileText("README.md");
|
|
215
|
+
await api.writeFileText("out.txt", "hello");
|
|
216
|
+
const run = await api.run({ executableName: "mytool", arguments: ["--help"] });
|
|
217
|
+
const st = await api.runStatus({ id: run.id! });
|
|
218
|
+
*/
|
package/dist/test.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// test.ts — mini runner front sans dépendances.
|
|
2
|
+
// Usage:
|
|
3
|
+
// import { test } from "./test.ts";
|
|
4
|
+
// test("grammar compiles", () => { assertEquals(typeof parser.parse, "function"); });
|
|
5
|
+
// test("examples parse to expected AST", async (t) => {
|
|
6
|
+
// for (let i = 0; i < tests.length; i++) {
|
|
7
|
+
// const tc = tests[i];
|
|
8
|
+
// await t.step(`case #${i + 1}`, () => {
|
|
9
|
+
// const got = parser.parse(tc.source);
|
|
10
|
+
// assertEquals(got, tc.prog);
|
|
11
|
+
// });
|
|
12
|
+
// }
|
|
13
|
+
// });
|
|
14
|
+
|
|
15
|
+
type StepFn = () => void | Promise<void>;
|
|
16
|
+
type TestFn = (t: { step: (name: string, fn: StepFn) => Promise<void> }) => void | Promise<void>;
|
|
17
|
+
|
|
18
|
+
function ensureRoot(): HTMLElement {
|
|
19
|
+
const id = "test-root";
|
|
20
|
+
let el = document.getElementById(id);
|
|
21
|
+
if (!el) {
|
|
22
|
+
el = document.createElement("div");
|
|
23
|
+
el.id = id;
|
|
24
|
+
el.style.cssText = "font-family: ui-sans-serif, system-ui, Arial; line-height:1.4; padding:16px;";
|
|
25
|
+
document.body.appendChild(el);
|
|
26
|
+
}
|
|
27
|
+
return el;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createTestBlock(name: string): { block: HTMLElement; header: HTMLElement; list: HTMLUListElement } {
|
|
31
|
+
const root = ensureRoot();
|
|
32
|
+
|
|
33
|
+
const block = document.createElement("section");
|
|
34
|
+
block.style.cssText = "margin: 12px 0; padding: 12px; border: 1px solid #ddd; border-radius: 8px;";
|
|
35
|
+
|
|
36
|
+
const header = document.createElement("h3");
|
|
37
|
+
header.textContent = `Test: ${name}`;
|
|
38
|
+
header.style.cssText = "margin: 0 0 8px 0; font-size: 16px;";
|
|
39
|
+
|
|
40
|
+
const list = document.createElement("ul");
|
|
41
|
+
list.style.cssText = "margin: 8px 0 0 16px; padding: 0;";
|
|
42
|
+
|
|
43
|
+
block.appendChild(header);
|
|
44
|
+
block.appendChild(list);
|
|
45
|
+
root.appendChild(block);
|
|
46
|
+
|
|
47
|
+
return { block, header, list };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatError(e: unknown): string {
|
|
51
|
+
if (e instanceof Error) return e.stack || `${e.name}: ${e.message}`;
|
|
52
|
+
try { return JSON.stringify(e); } catch { return String(e); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function test(name: string, fn: TestFn): void {
|
|
56
|
+
const { block, header, list } = createTestBlock(name);
|
|
57
|
+
|
|
58
|
+
// status badge
|
|
59
|
+
const status = document.createElement("span");
|
|
60
|
+
status.textContent = "⏳ running…";
|
|
61
|
+
status.style.cssText = "margin-left: 8px; font-size: 12px; color: #555;";
|
|
62
|
+
header.appendChild(status);
|
|
63
|
+
|
|
64
|
+
// Collect step results
|
|
65
|
+
let failed = false;
|
|
66
|
+
|
|
67
|
+
const t = {
|
|
68
|
+
step: async (stepName: string, stepFn: StepFn): Promise<void> => {
|
|
69
|
+
const li = document.createElement("li");
|
|
70
|
+
li.style.cssText = "margin: 4px 0;";
|
|
71
|
+
li.textContent = `• ${stepName}: `;
|
|
72
|
+
const badge = document.createElement("span");
|
|
73
|
+
badge.style.marginLeft = "4px";
|
|
74
|
+
li.appendChild(badge);
|
|
75
|
+
list.appendChild(li);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await stepFn();
|
|
79
|
+
badge.textContent = "✅ ok";
|
|
80
|
+
badge.style.color = "#1a7f37";
|
|
81
|
+
} catch (e) {
|
|
82
|
+
failed = true;
|
|
83
|
+
badge.textContent = "❌ fail";
|
|
84
|
+
badge.style.color = "#c62828";
|
|
85
|
+
const pre = document.createElement("pre");
|
|
86
|
+
pre.style.cssText = "white-space: pre-wrap; background:#fff5f5; border:1px solid #f0caca; padding:8px; border-radius:6px; margin:6px 0 0 0;";
|
|
87
|
+
pre.textContent = formatError(e);
|
|
88
|
+
li.appendChild(pre);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Run test (and implicit single-step if user didn’t use t.step)
|
|
94
|
+
(async () => {
|
|
95
|
+
let usedStep = false;
|
|
96
|
+
|
|
97
|
+
// Proxy t.step to detect usage
|
|
98
|
+
const proxiedT = {
|
|
99
|
+
step: async (n: string, f: StepFn) => {
|
|
100
|
+
usedStep = true;
|
|
101
|
+
return t.step(n, f);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const r = fn(proxiedT);
|
|
107
|
+
if (r instanceof Promise) await r;
|
|
108
|
+
|
|
109
|
+
if (!usedStep) {
|
|
110
|
+
// Wrap whole test as one step
|
|
111
|
+
await t.step("default", async () => {
|
|
112
|
+
// re-run nothing; the body has already executed
|
|
113
|
+
// but we still need to mark success
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
failed = true;
|
|
118
|
+
// show top-level failure as a step
|
|
119
|
+
await t.step("default", () => { throw e; }).catch(() => void 0);
|
|
120
|
+
} finally {
|
|
121
|
+
if (failed) {
|
|
122
|
+
status.textContent = "❌ failed";
|
|
123
|
+
status.style.color = "#c62828";
|
|
124
|
+
block.style.borderColor = "#f19999";
|
|
125
|
+
} else {
|
|
126
|
+
status.textContent = "✅ passed";
|
|
127
|
+
status.style.color = "#1a7f37";
|
|
128
|
+
block.style.borderColor = "#9fd3a7";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// assert.ts
|
|
136
|
+
// Usage (Deno):
|
|
137
|
+
// import { assertEquals } from "./assert.ts";
|
|
138
|
+
// Deno.test("ex", () => assertEquals(parse("..."), expected));
|
|
139
|
+
|
|
140
|
+
// ————— Deep equality —————
|
|
141
|
+
function isTypedArray(x: unknown): x is
|
|
142
|
+
| Int8Array | Uint8Array | Uint8ClampedArray
|
|
143
|
+
| Int16Array | Uint16Array
|
|
144
|
+
| Int32Array | Uint32Array
|
|
145
|
+
| Float32Array | Float64Array
|
|
146
|
+
| BigInt64Array | BigUint64Array {
|
|
147
|
+
return ArrayBuffer.isView(x) && !(x instanceof DataView);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sameValueZero(a: unknown, b: unknown): boolean {
|
|
151
|
+
// SameValueZero: like === but NaN equals NaN, and +0/-0 considered equal
|
|
152
|
+
return (a === b) || (Number.isNaN(a as number) && Number.isNaN(b as number));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isPlainObject(x: unknown): x is Record<string, unknown> {
|
|
156
|
+
if (x === null || typeof x !== "object") return false;
|
|
157
|
+
const proto = Object.getPrototypeOf(x);
|
|
158
|
+
return proto === Object.prototype || proto === null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function deepEqual(a: unknown, b: unknown, seen = new Map<any, any>()): boolean {
|
|
162
|
+
if (sameValueZero(a, b)) return true;
|
|
163
|
+
|
|
164
|
+
// Handle null vs objects/types quickly
|
|
165
|
+
if (a === null || b === null) return false;
|
|
166
|
+
|
|
167
|
+
const ta = typeof a;
|
|
168
|
+
const tb = typeof b;
|
|
169
|
+
if (ta !== "object" && tb !== "object") return false; // two different primitives
|
|
170
|
+
if (ta !== tb) return false;
|
|
171
|
+
|
|
172
|
+
// Cycle handling
|
|
173
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
174
|
+
const prev = seen.get(a);
|
|
175
|
+
if (prev && prev === b) return true;
|
|
176
|
+
seen.set(a, b);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Dates
|
|
180
|
+
if (a instanceof Date || b instanceof Date) {
|
|
181
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// RegExp
|
|
185
|
+
if (a instanceof RegExp || b instanceof RegExp) {
|
|
186
|
+
return a instanceof RegExp && b instanceof RegExp &&
|
|
187
|
+
a.source === b.source && a.flags === b.flags;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Typed arrays
|
|
191
|
+
if (isTypedArray(a) || isTypedArray(b)) {
|
|
192
|
+
if (!isTypedArray(a) || !isTypedArray(b)) return false;
|
|
193
|
+
if (Object.getPrototypeOf(a).constructor !== Object.getPrototypeOf(b).constructor) return false;
|
|
194
|
+
if (a.length !== b.length) return false;
|
|
195
|
+
for (let i = 0; i < a.length; i++) {
|
|
196
|
+
if (!sameValueZero(a[i], b[i])) return false;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Array
|
|
202
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
203
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
204
|
+
if (a.length !== b.length) return false;
|
|
205
|
+
for (let i = 0; i < a.length; i++) {
|
|
206
|
+
if (!deepEqual(a[i], b[i], seen)) return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Set
|
|
212
|
+
if (a instanceof Set || b instanceof Set) {
|
|
213
|
+
if (!(a instanceof Set) || !(b instanceof Set)) return false;
|
|
214
|
+
if (a.size !== b.size) return false;
|
|
215
|
+
// Compare as multisets with deep equality
|
|
216
|
+
const used = new Array<boolean>(b.size).fill(false);
|
|
217
|
+
const bArr = Array.from(b);
|
|
218
|
+
outer: for (const av of a) {
|
|
219
|
+
for (let i = 0; i < bArr.length; i++) {
|
|
220
|
+
if (!used[i] && deepEqual(av, bArr[i], seen)) {
|
|
221
|
+
used[i] = true;
|
|
222
|
+
continue outer;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Map
|
|
231
|
+
if (a instanceof Map || b instanceof Map) {
|
|
232
|
+
if (!(a instanceof Map) || !(b instanceof Map)) return false;
|
|
233
|
+
if (a.size !== b.size) return false;
|
|
234
|
+
// For each entry in a, find deep-equal key in b, then compare values
|
|
235
|
+
const bEntries = Array.from(b.entries());
|
|
236
|
+
const used = new Array<boolean>(bEntries.length).fill(false);
|
|
237
|
+
outerMap: for (const [ak, av] of a.entries()) {
|
|
238
|
+
for (let i = 0; i < bEntries.length; i++) {
|
|
239
|
+
if (used[i]) continue;
|
|
240
|
+
const [bk, bv] = bEntries[i];
|
|
241
|
+
if (deepEqual(ak, bk, seen) && deepEqual(av, bv, seen)) {
|
|
242
|
+
used[i] = true;
|
|
243
|
+
continue outerMap;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Plain objects
|
|
252
|
+
if (isPlainObject(a) || isPlainObject(b)) {
|
|
253
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
254
|
+
const ka = Object.keys(a as Record<string, unknown>);
|
|
255
|
+
const kb = Object.keys(b as Record<string, unknown>);
|
|
256
|
+
if (ka.length !== kb.length) return false;
|
|
257
|
+
// keys order-insensitive
|
|
258
|
+
ka.sort(); kb.sort();
|
|
259
|
+
for (let i = 0; i < ka.length; i++) {
|
|
260
|
+
if (ka[i] !== kb[i]) return false;
|
|
261
|
+
}
|
|
262
|
+
for (const k of ka) {
|
|
263
|
+
if (!deepEqual((a as any)[k], (b as any)[k], seen)) return false;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fallback for objects with prototypes/functions/etc.
|
|
269
|
+
// Compare own enumerable props + constructor
|
|
270
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
271
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
272
|
+
const ka = Object.keys(a as any);
|
|
273
|
+
const kb = Object.keys(b as any);
|
|
274
|
+
if (ka.length !== kb.length) return false;
|
|
275
|
+
for (const k of ka) {
|
|
276
|
+
if (!(k in (b as any))) return false;
|
|
277
|
+
if (!deepEqual((a as any)[k], (b as any)[k], seen)) return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ————— Pretty printers —————
|
|
286
|
+
function safeStringify(v: unknown, maxDepth = 10): string {
|
|
287
|
+
const seen = new WeakSet<object>();
|
|
288
|
+
function helper(x: unknown, depth: number): unknown {
|
|
289
|
+
if (depth <= 0) return "[Object]";
|
|
290
|
+
if (x && typeof x === "object") {
|
|
291
|
+
if (seen.has(x as object)) return "[Circular]";
|
|
292
|
+
seen.add(x as object);
|
|
293
|
+
if (Array.isArray(x)) return (x as unknown[]).map((e) => helper(e, depth - 1));
|
|
294
|
+
if (x instanceof Map) return { __map__: Array.from(x.entries()).map(([k, val]) => [helper(k, depth - 1), helper(val, depth - 1)]) };
|
|
295
|
+
if (x instanceof Set) return { __set__: Array.from(x.values()).map((e) => helper(e, depth - 1)) };
|
|
296
|
+
if (x instanceof Date) return { __date__: (x as Date).toISOString() };
|
|
297
|
+
if (x instanceof RegExp) return { __regexp__: x.toString() };
|
|
298
|
+
if (ArrayBuffer.isView(x)) return { __typedarray__: Object.getPrototypeOf(x).constructor.name, values: Array.from(x as any) };
|
|
299
|
+
const out: Record<string, unknown> = {};
|
|
300
|
+
for (const k of Object.keys(x as Record<string, unknown>)) {
|
|
301
|
+
out[k] = helper((x as any)[k], depth - 1);
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
if (typeof x === "number" && Number.isNaN(x)) return "NaN";
|
|
306
|
+
if (Object.is(x, -0)) return "-0";
|
|
307
|
+
return x as any;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
return JSON.stringify(helper(v, maxDepth), null, 2);
|
|
311
|
+
} catch {
|
|
312
|
+
return String(v);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ————— Public assert —————
|
|
317
|
+
export function assertEquals(actual: unknown, expected: unknown, msg?: string): void {
|
|
318
|
+
if (deepEqual(actual, expected)) return;
|
|
319
|
+
const aStr = safeStringify(actual);
|
|
320
|
+
const eStr = safeStringify(expected);
|
|
321
|
+
const defaultMsg = `assertEquals failed:\nExpected:\n${eStr}\nActual:\n${aStr}`;
|
|
322
|
+
throw new Error(msg ? `${msg}\n${defaultMsg}` : defaultMsg);
|
|
323
|
+
}
|
|
324
|
+
|
package/dist/types.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** OpenAPI: 3.0.3 — Tauri Static Server API — v1.0.0 */
|
|
2
|
+
|
|
3
|
+
/* =========================
|
|
4
|
+
Schemas (components)
|
|
5
|
+
========================= */
|
|
6
|
+
|
|
7
|
+
export interface EmbedReqAny {
|
|
8
|
+
/** Code folder to package (required server-side) */
|
|
9
|
+
code: string | null;
|
|
10
|
+
/** Binary folder to package */
|
|
11
|
+
executable: string | null;
|
|
12
|
+
/** Path of the new executable */
|
|
13
|
+
output: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EmbedResp {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseConfigReq {
|
|
22
|
+
code: string;
|
|
23
|
+
executable: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UseConfigResp {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GetConfigResp {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
code: string;
|
|
34
|
+
executable: string;
|
|
35
|
+
fileBase: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CurrentDirReq {
|
|
39
|
+
/** Absolute or relative path. Empty ⇒ CWD. */
|
|
40
|
+
path: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CurrentDirResp {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
message: string;
|
|
46
|
+
current: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FileWriteResp {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
message: string;
|
|
52
|
+
path: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FileDeleteResp {
|
|
56
|
+
ok: boolean;
|
|
57
|
+
message: string;
|
|
58
|
+
path: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RunReq {
|
|
62
|
+
executableName: string;
|
|
63
|
+
/** Either an argv array or a single command-line string */
|
|
64
|
+
arguments?: string[] | string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RunResp {
|
|
68
|
+
ok: boolean;
|
|
69
|
+
/** exit status code (when available) */
|
|
70
|
+
status?: number | null;
|
|
71
|
+
message: string;
|
|
72
|
+
stdout?: string;
|
|
73
|
+
stderr?: string;
|
|
74
|
+
/** internal id of started process */
|
|
75
|
+
id?: number | null; // int64 → number
|
|
76
|
+
/** OS pid (when available) */
|
|
77
|
+
pid?: number | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ProcIdReq {
|
|
81
|
+
id: number; // int64
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ProcStatusResp {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
running: boolean;
|
|
87
|
+
status?: number | null;
|
|
88
|
+
pid?: number | null;
|
|
89
|
+
stdout: string;
|
|
90
|
+
stderr: string;
|
|
91
|
+
message: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ProcStopResp {
|
|
95
|
+
ok: boolean;
|
|
96
|
+
message: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface StopAllResp {
|
|
100
|
+
ok: boolean;
|
|
101
|
+
message: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ExplorerReq {
|
|
105
|
+
path?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type ExplorerElement = ExplorerDirectory | ExplorerFile
|
|
109
|
+
|
|
110
|
+
export interface ExplorerFile {
|
|
111
|
+
type: "file";
|
|
112
|
+
path: string;
|
|
113
|
+
name: string;
|
|
114
|
+
parent: string | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ExplorerDirectory {
|
|
118
|
+
type: "directory";
|
|
119
|
+
path: string;
|
|
120
|
+
parent: string | null;
|
|
121
|
+
content: ExplorerElement[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ExplorerError {
|
|
125
|
+
type: "error";
|
|
126
|
+
message: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface NewServerReq {
|
|
130
|
+
code: string;
|
|
131
|
+
executable: string;
|
|
132
|
+
/** 0..65535 (nullable) */
|
|
133
|
+
port?: number | null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface NewServerResp {
|
|
137
|
+
ok: boolean;
|
|
138
|
+
port?: number | null;
|
|
139
|
+
message: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface StopServerReq {
|
|
143
|
+
/** if omitted, targets current server (parent) */
|
|
144
|
+
port?: number | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface StopServerResp {
|
|
148
|
+
ok: boolean;
|
|
149
|
+
port?: number | null;
|
|
150
|
+
message: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* =========================
|
|
154
|
+
Helper union types
|
|
155
|
+
========================= */
|
|
156
|
+
|
|
157
|
+
/** Result of /api/explorer (200) */
|
|
158
|
+
export type ExplorerResult = ExplorerFile | ExplorerDirectory;
|
|
159
|
+
|
|
160
|
+
/** Error shapes from /api/explorer (404/500) */
|
|
161
|
+
export type ExplorerErrorResult = ExplorerError;
|
|
162
|
+
|
|
163
|
+
/* =========================
|
|
164
|
+
(Optionnel) Types d’IO par route
|
|
165
|
+
— utiles si tu veux typer ton client HTTP
|
|
166
|
+
========================= */
|
|
167
|
+
|
|
168
|
+
/* =========================
|
|
169
|
+
(Optionnel) Types utilitaires client
|
|
170
|
+
========================= */
|
|
171
|
+
|
|
172
|
+
export type Json =
|
|
173
|
+
| null
|
|
174
|
+
| boolean
|
|
175
|
+
| number
|
|
176
|
+
| string
|
|
177
|
+
| Json[]
|
|
178
|
+
| { [k: string]: Json };
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tauri-kargo-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "",
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
},
|
|
5
|
+
"files": ["dist"],
|
|
6
|
+
"exports": { "./*": "./dist/*" },
|
|
7
|
+
"publishConfig": { "access": "public" },
|
|
9
8
|
"repository": {
|
|
10
9
|
"type": "git",
|
|
11
10
|
"url": "git+https://github.com/blockapicoder/tauriKargoTools.git"
|