@opys/core 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/index.mjs ADDED
@@ -0,0 +1,351 @@
1
+ import { z } from "zod";
2
+ import * as napi from "@opys/core-binding";
3
+
4
+ //#region lib/fetch.ts
5
+ /**
6
+ * Wrap `fetch()` with bounded retry on transient network failures and
7
+ * 5xx-class responses. Errors that won't recover from a retry (4xx,
8
+ * `AbortError`, JSON parse failures downstream of the response) are
9
+ * surfaced unchanged so callers handle them with their own logic.
10
+ *
11
+ * Network-level retries cover the AggregateError ETIMEDOUT / ECONNRESET
12
+ * / ENOTFOUND / EAI_AGAIN / ECONNREFUSED cases that node:undici reports
13
+ * as `TypeError: fetch failed` with a populated `cause`. Without this
14
+ * one DNS hiccup mid-`opys launch` aborts the whole config evaluation.
15
+ */
16
+ /**
17
+ * Default `User-Agent` for every opys HTTP request. The bare `undici`
18
+ * agent gets rejected or rate-limited by some CDNs and by the CurseForge
19
+ * API, so we identify ourselves. The patch component is omitted so the
20
+ * string doesn't churn against `package.json` on every release.
21
+ */
22
+ const OPYS_USER_AGENT = "opys/1.0";
23
+ const DEFAULT_RETRY_STATUSES = new Set([
24
+ 408,
25
+ 425,
26
+ 429,
27
+ 500,
28
+ 502,
29
+ 503,
30
+ 504
31
+ ]);
32
+ const TRANSIENT_CODES = new Set([
33
+ "ETIMEDOUT",
34
+ "ECONNRESET",
35
+ "ECONNREFUSED",
36
+ "ENOTFOUND",
37
+ "EAI_AGAIN",
38
+ "EPIPE",
39
+ "UND_ERR_SOCKET",
40
+ "UND_ERR_CONNECT_TIMEOUT",
41
+ "UND_ERR_HEADERS_TIMEOUT",
42
+ "UND_ERR_BODY_TIMEOUT"
43
+ ]);
44
+ function isTransientFetchError(err) {
45
+ if (!err || typeof err !== "object") return false;
46
+ const cause = err.cause;
47
+ if (cause) {
48
+ const code = cause.code;
49
+ if (code && TRANSIENT_CODES.has(code)) return true;
50
+ const inner = cause.errors;
51
+ if (Array.isArray(inner) && inner.length > 0) return inner.every((e) => {
52
+ const c = e?.code;
53
+ return c !== void 0 && TRANSIENT_CODES.has(c);
54
+ });
55
+ }
56
+ const code = err.code;
57
+ if (code && TRANSIENT_CODES.has(code)) return true;
58
+ return err instanceof TypeError && typeof err.message === "string" && err.message.toLowerCase().includes("fetch failed");
59
+ }
60
+ function backoffMs(attempt, base, max) {
61
+ const exp = base * 2 ** (attempt - 1);
62
+ const capped = Math.min(exp, max);
63
+ return Math.floor(capped * (.5 + Math.random() * .5));
64
+ }
65
+ function sleep(ms, signal) {
66
+ return new Promise((resolve, reject) => {
67
+ if (signal?.aborted) {
68
+ reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
69
+ return;
70
+ }
71
+ const t = setTimeout(() => {
72
+ signal?.removeEventListener("abort", onAbort);
73
+ resolve();
74
+ }, ms);
75
+ const onAbort = () => {
76
+ clearTimeout(t);
77
+ reject(signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
78
+ };
79
+ signal?.addEventListener("abort", onAbort, { once: true });
80
+ });
81
+ }
82
+ async function fetchWithRetry(input, init = {}, retry = {}) {
83
+ const attempts = Math.max(1, retry.attempts ?? 4);
84
+ const base = retry.baseDelayMs ?? 250;
85
+ const max = retry.maxDelayMs ?? 5e3;
86
+ const retryStatuses = retry.retryStatuses instanceof Set ? retry.retryStatuses : retry.retryStatuses ? new Set(retry.retryStatuses) : DEFAULT_RETRY_STATUSES;
87
+ const signal = retry.signal ?? init.signal ?? void 0;
88
+ const headers = new Headers(init.headers);
89
+ if (!headers.has("user-agent")) headers.set("user-agent", OPYS_USER_AGENT);
90
+ let lastErr;
91
+ for (let attempt = 1; attempt <= attempts; attempt++) try {
92
+ const res = await fetch(input, {
93
+ ...init,
94
+ headers,
95
+ signal
96
+ });
97
+ if (res.ok || attempt === attempts || !retryStatuses.has(res.status)) return res;
98
+ try {
99
+ await res.body?.cancel();
100
+ } catch {}
101
+ const delay = backoffMs(attempt, base, max);
102
+ retry.onRetry?.({
103
+ attempt,
104
+ delayMs: delay,
105
+ status: res.status
106
+ });
107
+ await sleep(delay, signal);
108
+ } catch (err) {
109
+ lastErr = err;
110
+ if (signal?.aborted) throw err;
111
+ if (!isTransientFetchError(err) || attempt === attempts) throw err;
112
+ const delay = backoffMs(attempt, base, max);
113
+ retry.onRetry?.({
114
+ attempt,
115
+ delayMs: delay,
116
+ error: err
117
+ });
118
+ await sleep(delay, signal);
119
+ }
120
+ throw lastErr;
121
+ }
122
+
123
+ //#endregion
124
+ //#region lib/index.ts
125
+ /**
126
+ * `@opys/core` — the frozen-manifest contract. Behaviors are backed by the
127
+ * Rust `opys-core` crate (via napi-rs); domain types, factories and small
128
+ * sugar helpers are hand-written TS.
129
+ *
130
+ * Strategy:
131
+ * - Algorithms that touch the manifest contract (decode, encode, resolve,
132
+ * filter, interpolate, glob) → typed wrappers around the Rust binding.
133
+ * - Domain types → hand-typed here, matching the frozen wire shape. These
134
+ * are pure data shapes; consumers construct them as plain JS objects.
135
+ * - Factories / type guards / small dedup helpers → pure TS, no boundary
136
+ * crossing. They are sugar over the typed shapes.
137
+ * - `parseShortRuleset` is implemented in TS as the shorthand sugar — the
138
+ * Rust binding accepts shorthand directly via `satisfiesRuleset` so the
139
+ * TS impl exists for consumers that need expanded `Rule` objects.
140
+ */
141
+ const OsNameSchema = z.enum([
142
+ "linux",
143
+ "windows",
144
+ "osx"
145
+ ]);
146
+ const OsArchSchema = z.enum([
147
+ "x86",
148
+ "x86_64",
149
+ "arm",
150
+ "aarch64",
151
+ "any"
152
+ ]);
153
+ const OsConstraintSchema = z.object({
154
+ name: OsNameSchema.optional(),
155
+ version: z.string().optional(),
156
+ arch: OsArchSchema.optional()
157
+ });
158
+ const RuleActionSchema = z.enum(["allow", "disallow"]);
159
+ const RuleSchema = z.union([
160
+ z.object({
161
+ action: RuleActionSchema,
162
+ os: OsConstraintSchema
163
+ }),
164
+ z.object({
165
+ action: RuleActionSchema,
166
+ features: z.record(z.string(), z.boolean())
167
+ }),
168
+ z.object({ action: RuleActionSchema })
169
+ ]);
170
+ function decodeManifest(wire) {
171
+ return napi.decodeManifest(wire);
172
+ }
173
+ function encodeManifest(domain) {
174
+ return napi.encodeManifest(domain);
175
+ }
176
+ function parseManifest(input) {
177
+ return napi.parseManifest(input);
178
+ }
179
+ function filterManifest(manifest, platform, features = []) {
180
+ return napi.filterManifest(manifest, platform, features);
181
+ }
182
+ function resolveVars(vars) {
183
+ return napi.resolveVars(vars);
184
+ }
185
+ function interpolate(template, vars) {
186
+ return napi.interpolate(template, vars);
187
+ }
188
+ function resolvedArgs(launch, platform, features = []) {
189
+ return napi.resolvedArgs(launch, platform, features);
190
+ }
191
+ function resolvedEnvs(launch, platform, features = []) {
192
+ return napi.resolvedEnvs(launch, platform, features);
193
+ }
194
+ function satisfiesRuleset(rules, platform, features = []) {
195
+ return napi.satisfiesRuleset(rules, platform, features);
196
+ }
197
+ function globBase(glob) {
198
+ return napi.globBase(glob);
199
+ }
200
+ function globToRegexSource(glob) {
201
+ return napi.globToRegexSource(glob);
202
+ }
203
+ /** Compile a glob to a real `RegExp` (the binding returns the source string). */
204
+ function globToRegex(glob) {
205
+ return new RegExp(napi.globToRegexSource(glob));
206
+ }
207
+ const sourceUrl = (url) => ({
208
+ kind: "url",
209
+ url
210
+ });
211
+ const sourceFile = (file) => ({
212
+ kind: "file",
213
+ file
214
+ });
215
+ const sourceString = (string) => ({
216
+ kind: "string",
217
+ string
218
+ });
219
+ const sourcePointer = (pointer) => ({
220
+ kind: "pointer",
221
+ pointer
222
+ });
223
+ const sourceBytes = (bytes) => ({
224
+ kind: "bytes",
225
+ bytes: Buffer.from(bytes).toString("base64")
226
+ });
227
+ const isSourceUrl = (s) => s.kind === "url";
228
+ const isSourceFile = (s) => s.kind === "file";
229
+ const isSourceString = (s) => s.kind === "string";
230
+ const isSourceBytes = (s) => s.kind === "bytes";
231
+ const isSourcePointer = (s) => s.kind === "pointer";
232
+ const extractPick = (file, into) => ({
233
+ kind: "pick",
234
+ file,
235
+ into
236
+ });
237
+ const extractScan = (matches, into, opts) => ({
238
+ kind: "scan",
239
+ matches,
240
+ into,
241
+ ...opts
242
+ });
243
+ const extractDump = (into, opts) => ({
244
+ kind: "dump",
245
+ into,
246
+ ...opts
247
+ });
248
+ const emptyRuleset = () => [];
249
+ const allowOsRuleset = (name) => [{
250
+ action: "allow",
251
+ os: { name }
252
+ }];
253
+ /** Deduplicate by normalized (posix) path; later entries win. */
254
+ function deduplicateArtifacts(artifacts) {
255
+ const norm = (p) => {
256
+ const parts = p.split("/");
257
+ const stack = [];
258
+ const lead = p.startsWith("/");
259
+ for (const seg of parts) {
260
+ if (seg === "" || seg === ".") continue;
261
+ if (seg === "..") {
262
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") stack.pop();
263
+ else if (!lead) stack.push("..");
264
+ } else stack.push(seg);
265
+ }
266
+ const joined = stack.join("/");
267
+ if (lead) return "/" + joined;
268
+ return joined === "" ? "." : joined;
269
+ };
270
+ const map = /* @__PURE__ */ new Map();
271
+ for (const u of artifacts) map.set(norm(u.path), u);
272
+ return [...map.values()];
273
+ }
274
+ function parseShortRule(raw) {
275
+ if (typeof raw !== "string") return raw;
276
+ const parts = raw.split(".");
277
+ const action = parts[0];
278
+ if (action !== "allow" && action !== "disallow") throw new Error(`Unknown action '${action}'`);
279
+ const type = parts[1];
280
+ if (!type) return { action };
281
+ const rest = parts.slice(2).join(".");
282
+ switch (type) {
283
+ case "os": {
284
+ if (!rest) throw new Error("missing OS name");
285
+ const atIdx = rest.indexOf("@");
286
+ const name = atIdx === -1 ? rest : rest.slice(0, atIdx);
287
+ if (![
288
+ "linux",
289
+ "windows",
290
+ "osx"
291
+ ].includes(name)) throw new Error(`invalid os name '${name}'`);
292
+ const version = atIdx === -1 ? void 0 : rest.slice(atIdx + 1);
293
+ return version ? {
294
+ action,
295
+ os: {
296
+ name,
297
+ version
298
+ }
299
+ } : {
300
+ action,
301
+ os: { name }
302
+ };
303
+ }
304
+ case "features":
305
+ if (!rest) throw new Error("missing feature name");
306
+ return {
307
+ action,
308
+ features: { [rest]: true }
309
+ };
310
+ case "arch": {
311
+ if (!rest) throw new Error("missing arch");
312
+ const arch = rest;
313
+ if (![
314
+ "x86",
315
+ "x86_64",
316
+ "arm",
317
+ "aarch64",
318
+ "any"
319
+ ].includes(arch)) throw new Error(`invalid arch '${arch}'`);
320
+ return {
321
+ action,
322
+ os: { arch }
323
+ };
324
+ }
325
+ default: throw new Error(`unknown rule type '${type}'`);
326
+ }
327
+ }
328
+ function parseShortRuleset(raw) {
329
+ return (Array.isArray(raw) ? raw : [raw]).map(parseShortRule);
330
+ }
331
+ function parseValset(raw) {
332
+ if (!Array.isArray(raw)) throw new Error("parseValset: expected an array");
333
+ return raw.map((entry) => {
334
+ if (typeof entry === "string") return {
335
+ rules: [],
336
+ value: [entry]
337
+ };
338
+ if (entry && typeof entry === "object") {
339
+ const obj = entry;
340
+ return {
341
+ rules: obj.rules ? parseShortRuleset(obj.rules) : [],
342
+ value: Array.isArray(obj.value) ? obj.value : [obj.value]
343
+ };
344
+ }
345
+ throw new Error("parseValset: invalid entry");
346
+ });
347
+ }
348
+
349
+ //#endregion
350
+ export { OPYS_USER_AGENT, OsArchSchema, OsNameSchema, RuleSchema, allowOsRuleset, decodeManifest, deduplicateArtifacts, emptyRuleset, encodeManifest, extractDump, extractPick, extractScan, fetchWithRetry, filterManifest, globBase, globToRegex, globToRegexSource, interpolate, isSourceBytes, isSourceFile, isSourcePointer, isSourceString, isSourceUrl, parseManifest, parseShortRuleset, parseValset, resolveVars, resolvedArgs, resolvedEnvs, satisfiesRuleset, sourceBytes, sourceFile, sourcePointer, sourceString, sourceUrl };
351
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../lib/fetch.ts","../lib/index.ts"],"sourcesContent":["/**\n * Wrap `fetch()` with bounded retry on transient network failures and\n * 5xx-class responses. Errors that won't recover from a retry (4xx,\n * `AbortError`, JSON parse failures downstream of the response) are\n * surfaced unchanged so callers handle them with their own logic.\n *\n * Network-level retries cover the AggregateError ETIMEDOUT / ECONNRESET\n * / ENOTFOUND / EAI_AGAIN / ECONNREFUSED cases that node:undici reports\n * as `TypeError: fetch failed` with a populated `cause`. Without this\n * one DNS hiccup mid-`opys launch` aborts the whole config evaluation.\n */\n\n/**\n * Default `User-Agent` for every opys HTTP request. The bare `undici`\n * agent gets rejected or rate-limited by some CDNs and by the CurseForge\n * API, so we identify ourselves. The patch component is omitted so the\n * string doesn't churn against `package.json` on every release.\n */\nexport const OPYS_USER_AGENT = 'opys/1.0';\n\nexport interface FetchRetryOptions {\n /** Total attempts including the first. Default 4 (so up to 3 retries). */\n attempts?: number;\n /** Initial delay before retry #2, in ms. Default 250. */\n baseDelayMs?: number;\n /** Cap on per-attempt delay, in ms. Default 5000. */\n maxDelayMs?: number;\n /** HTTP status codes that trigger a retry. Default 408, 425, 429, 500, 502, 503, 504. */\n retryStatuses?: ReadonlySet<number> | readonly number[];\n /** Forward to the underlying `fetch`'s signal — abort cancels remaining retries. */\n signal?: AbortSignal;\n /** Callback for diagnostics / logging on each retry. */\n onRetry?: (info: {\n attempt: number;\n delayMs: number;\n error?: unknown;\n status?: number;\n }) => void;\n}\n\nconst DEFAULT_RETRY_STATUSES: ReadonlySet<number> = new Set([\n 408, 425, 429, 500, 502, 503, 504,\n]);\n\nconst TRANSIENT_CODES: ReadonlySet<string> = new Set([\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'EPIPE',\n 'UND_ERR_SOCKET',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_HEADERS_TIMEOUT',\n 'UND_ERR_BODY_TIMEOUT',\n]);\n\nfunction isTransientFetchError(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n // Direct undici/Node fetch failure surfaces as `TypeError: fetch failed`\n // with a `cause` carrying the underlying socket error (or AggregateError\n // when Happy Eyeballs raced multiple addresses).\n const cause = (err as { cause?: unknown }).cause;\n if (cause) {\n const code = (cause as { code?: string }).code;\n if (code && TRANSIENT_CODES.has(code)) return true;\n const inner = (cause as { errors?: unknown[] }).errors;\n if (Array.isArray(inner) && inner.length > 0) {\n // AggregateError — retry only if every inner error is transient.\n return inner.every((e) => {\n const c = (e as { code?: string } | undefined)?.code;\n return c !== undefined && TRANSIENT_CODES.has(c);\n });\n }\n }\n const code = (err as { code?: string }).code;\n if (code && TRANSIENT_CODES.has(code)) return true;\n return (\n err instanceof TypeError &&\n typeof (err as Error).message === 'string' &&\n (err as Error).message.toLowerCase().includes('fetch failed')\n );\n}\n\nfunction backoffMs(attempt: number, base: number, max: number): number {\n // Exponential with full jitter — `attempt` is 1-indexed; first retry uses base.\n const exp = base * 2 ** (attempt - 1);\n const capped = Math.min(exp, max);\n return Math.floor(capped * (0.5 + Math.random() * 0.5));\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason ?? new Error('aborted'));\n return;\n }\n const t = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(t);\n reject(signal?.reason ?? new Error('aborted'));\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nexport async function fetchWithRetry(\n input: string | URL,\n init: RequestInit = {},\n retry: FetchRetryOptions = {},\n): Promise<Response> {\n const attempts = Math.max(1, retry.attempts ?? 4);\n const base = retry.baseDelayMs ?? 250;\n const max = retry.maxDelayMs ?? 5000;\n const retryStatuses =\n retry.retryStatuses instanceof Set\n ? retry.retryStatuses\n : retry.retryStatuses\n ? new Set(retry.retryStatuses)\n : DEFAULT_RETRY_STATUSES;\n const signal = retry.signal ?? init.signal ?? undefined;\n\n // Default the User-Agent without clobbering a caller-supplied one.\n const headers = new Headers(init.headers);\n if (!headers.has('user-agent')) headers.set('user-agent', OPYS_USER_AGENT);\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= attempts; attempt++) {\n try {\n const res = await fetch(input, { ...init, headers, signal });\n if (res.ok || attempt === attempts || !retryStatuses.has(res.status)) {\n return res;\n }\n // Drain so the connection isn't held open across retries.\n try {\n await res.body?.cancel();\n } catch {\n /* ignore */\n }\n const delay = backoffMs(attempt, base, max);\n retry.onRetry?.({ attempt, delayMs: delay, status: res.status });\n await sleep(delay, signal);\n } catch (err) {\n lastErr = err;\n if (signal?.aborted) throw err;\n if (!isTransientFetchError(err) || attempt === attempts) throw err;\n const delay = backoffMs(attempt, base, max);\n retry.onRetry?.({ attempt, delayMs: delay, error: err });\n await sleep(delay, signal);\n }\n }\n // Defensive — the loop always returns or throws above.\n throw lastErr;\n}\n","/**\n * `@opys/core` — the frozen-manifest contract. Behaviors are backed by the\n * Rust `opys-core` crate (via napi-rs); domain types, factories and small\n * sugar helpers are hand-written TS.\n *\n * Strategy:\n * - Algorithms that touch the manifest contract (decode, encode, resolve,\n * filter, interpolate, glob) → typed wrappers around the Rust binding.\n * - Domain types → hand-typed here, matching the frozen wire shape. These\n * are pure data shapes; consumers construct them as plain JS objects.\n * - Factories / type guards / small dedup helpers → pure TS, no boundary\n * crossing. They are sugar over the typed shapes.\n * - `parseShortRuleset` is implemented in TS as the shorthand sugar — the\n * Rust binding accepts shorthand directly via `satisfiesRuleset` so the\n * TS impl exists for consumers that need expanded `Rule` objects.\n */\n\nimport { z } from 'zod';\nimport * as napi from '@opys/core-binding';\n\n// `fetchWithRetry` is a build-time HTTP utility (used by Mojang/Forge/Java\n// plugins). It's not part of the manifest contract, so it stays as TS —\n// build-time consumers can't reach into `@opys/runtime`, so it lives here.\nexport { fetchWithRetry, OPYS_USER_AGENT } from './fetch';\nexport type { FetchRetryOptions } from './fetch';\n\n// `RuleSchema` is a zod schema typed to produce a domain `Rule`. Used by\n// the Forge recipe parser to validate upstream JSON. Kept for build-time\n// consumers — the manifest contract itself doesn't need zod (the napi\n// binding does its own serde validation).\nexport const OsNameSchema = z.enum(['linux', 'windows', 'osx']);\nexport const OsArchSchema = z.enum(['x86', 'x86_64', 'arm', 'aarch64', 'any']);\nconst OsConstraintSchema = z.object({\n name: OsNameSchema.optional(),\n version: z.string().optional(),\n arch: OsArchSchema.optional(),\n});\nconst RuleActionSchema = z.enum(['allow', 'disallow']);\nexport const RuleSchema = z.union([\n z.object({ action: RuleActionSchema, os: OsConstraintSchema }),\n z.object({\n action: RuleActionSchema,\n features: z.record(z.string(), z.boolean()),\n }),\n z.object({ action: RuleActionSchema }),\n]);\n\n// ──────────────────────────────────────────────────────────────────────────\n// Behaviors — typed wrappers around the Rust binding.\n//\n// The codegen'd `.d.ts` types every return value as `Json` (≈ unknown), so\n// each wrapper carries one `as`-cast at the boundary. No `as unknown as`.\n// ──────────────────────────────────────────────────────────────────────────\n\nexport function decodeManifest(wire: unknown): Manifest {\n return napi.decodeManifest(wire) as Manifest;\n}\nexport function encodeManifest(domain: Manifest): unknown {\n return napi.encodeManifest(domain);\n}\nexport function parseManifest(input: string): Manifest {\n return napi.parseManifest(input) as Manifest;\n}\nexport function filterManifest(\n manifest: Manifest,\n platform: OsOptions,\n features: string[] = [],\n): Manifest {\n return napi.filterManifest(manifest, platform, features) as Manifest;\n}\nexport function resolveVars(\n vars: Record<string, string>,\n): Record<string, string> {\n return napi.resolveVars(vars);\n}\nexport function interpolate(\n template: string,\n vars: Record<string, string>,\n): string {\n return napi.interpolate(template, vars);\n}\nexport function resolvedArgs(\n launch: Launch,\n platform: OsOptions,\n features: string[] = [],\n): string[] {\n return napi.resolvedArgs(launch, platform, features);\n}\nexport function resolvedEnvs(\n launch: Launch,\n platform: OsOptions,\n features: string[] = [],\n): Record<string, string> {\n return napi.resolvedEnvs(launch, platform, features);\n}\nexport function satisfiesRuleset(\n rules: Ruleset | string | unknown,\n platform: OsOptions,\n features: string[] = [],\n): boolean {\n return napi.satisfiesRuleset(rules, platform, features);\n}\nexport function globBase(glob: string): string {\n return napi.globBase(glob);\n}\nexport function globToRegexSource(glob: string): string {\n return napi.globToRegexSource(glob);\n}\n\n/** Compile a glob to a real `RegExp` (the binding returns the source string). */\nexport function globToRegex(glob: string): RegExp {\n return new RegExp(napi.globToRegexSource(glob));\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Domain types — frozen wire shape.\n// ──────────────────────────────────────────────────────────────────────────\n\nexport type OsOptions = napi.OsOptions;\nexport type OsName = 'linux' | 'windows' | 'osx';\nexport type OsArch = 'x86' | 'x86_64' | 'arm' | 'aarch64' | 'any';\n\nexport interface OsConstraint {\n readonly name?: OsName;\n readonly version?: string;\n readonly arch?: OsArch;\n}\n\nexport type RuleAction = 'allow' | 'disallow';\nexport type FeatureConstraint = Record<string, boolean>;\n\nexport type Rule =\n | { action: RuleAction; os: OsConstraint }\n | { action: RuleAction; features: FeatureConstraint }\n | { action: RuleAction };\n\nexport type Ruleset = Rule[];\n\nexport type Source =\n | { readonly kind: 'url'; readonly url: string }\n | { readonly kind: 'file'; readonly file: string }\n | { readonly kind: 'string'; readonly string: string }\n | { readonly kind: 'bytes'; readonly bytes: string }\n | { readonly kind: 'pointer'; readonly pointer: string };\n\nexport type HashEntry = { sha1: string } | { sha256: string } | { md5: string };\nexport type Integrity = HashEntry | HashEntry[];\nexport type HashAlgo = 'sha1' | 'sha256' | 'md5';\n\nexport type HashRef =\n | { readonly sha256: string }\n | { readonly sha1: string }\n | { readonly md5: string };\n\nexport interface IntegrityProbes {\n readonly header?: HashRef;\n readonly url?: HashRef;\n}\nexport interface SizeProbes {\n readonly header?: string;\n}\nexport interface Discovery {\n readonly integrity?: IntegrityProbes;\n readonly size?: SizeProbes;\n}\n\nexport interface ExtractPick {\n readonly kind: 'pick';\n readonly file: string;\n readonly into: string;\n}\nexport interface ExtractScan {\n readonly kind: 'scan';\n readonly matches: string;\n readonly into: string;\n readonly strip?: string[];\n readonly includes?: string[];\n readonly excludes?: string[];\n}\nexport interface ExtractDump {\n readonly kind: 'dump';\n readonly into: string;\n readonly clean?: boolean;\n readonly includes?: string[];\n readonly excludes?: string[];\n}\nexport type ExtractRule = ExtractPick | ExtractScan | ExtractDump;\n\nexport interface Artifact {\n readonly path: string;\n readonly source: Source;\n readonly size?: number;\n readonly rules: Ruleset;\n readonly integrity?: Integrity;\n readonly discovery?: Discovery;\n readonly metadata?: unknown;\n readonly extract?: ExtractRule[];\n}\n\nexport interface Val {\n readonly rules: Ruleset;\n readonly value: string[];\n}\nexport type Valset = Val[];\n\nexport interface ConditionalVal {\n readonly value: string;\n readonly rules: Ruleset;\n}\nexport type ValDefs = Readonly<\n Record<string, string | readonly ConditionalVal[]>\n>;\n\nexport interface Launch {\n readonly command: string;\n readonly workdir: string;\n readonly args: Valset;\n readonly envs: ValDefs;\n}\n\nexport interface Manifest {\n readonly vars: ValDefs;\n readonly launch?: Launch;\n readonly artifacts: ReadonlyArray<Artifact>;\n readonly restrict?: ReadonlyArray<string>;\n}\n\nexport interface PointerDescriptor {\n readonly source: Source;\n readonly integrity?: Integrity;\n readonly size?: number;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Factories — pure TS, no boundary crossing.\n// ──────────────────────────────────────────────────────────────────────────\n\nexport const sourceUrl = (url: string): Source => ({ kind: 'url', url });\nexport const sourceFile = (file: string): Source => ({ kind: 'file', file });\nexport const sourceString = (string: string): Source => ({\n kind: 'string',\n string,\n});\nexport const sourcePointer = (pointer: string): Source => ({\n kind: 'pointer',\n pointer,\n});\nexport const sourceBytes = (bytes: Uint8Array): Source => ({\n kind: 'bytes',\n bytes: Buffer.from(bytes).toString('base64'),\n});\n\nexport const isSourceUrl = (s: Source): s is Extract<Source, { kind: 'url' }> =>\n s.kind === 'url';\nexport const isSourceFile = (\n s: Source,\n): s is Extract<Source, { kind: 'file' }> => s.kind === 'file';\nexport const isSourceString = (\n s: Source,\n): s is Extract<Source, { kind: 'string' }> => s.kind === 'string';\nexport const isSourceBytes = (\n s: Source,\n): s is Extract<Source, { kind: 'bytes' }> => s.kind === 'bytes';\nexport const isSourcePointer = (\n s: Source,\n): s is Extract<Source, { kind: 'pointer' }> => s.kind === 'pointer';\n\nexport const extractPick = (file: string, into: string): ExtractPick => ({\n kind: 'pick',\n file,\n into,\n});\nexport const extractScan = (\n matches: string,\n into: string,\n opts?: Omit<ExtractScan, 'kind' | 'matches' | 'into'>,\n): ExtractScan => ({ kind: 'scan', matches, into, ...opts });\nexport const extractDump = (\n into: string,\n opts?: Omit<ExtractDump, 'kind' | 'into'>,\n): ExtractDump => ({ kind: 'dump', into, ...opts });\n\nexport const emptyRuleset = (): Ruleset => [];\nexport const allowOsRuleset = (name: OsName): Ruleset => [\n { action: 'allow', os: { name } },\n];\n\n/** Deduplicate by normalized (posix) path; later entries win. */\nexport function deduplicateArtifacts(artifacts: Artifact[]): Artifact[] {\n const norm = (p: string): string => {\n const parts = p.split('/');\n const stack: string[] = [];\n const lead = p.startsWith('/');\n for (const seg of parts) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n if (stack.length > 0 && stack[stack.length - 1] !== '..') stack.pop();\n else if (!lead) stack.push('..');\n } else stack.push(seg);\n }\n const joined = stack.join('/');\n if (lead) return '/' + joined;\n return joined === '' ? '.' : joined;\n };\n const map = new Map<string, Artifact>();\n for (const u of artifacts) map.set(norm(u.path), u);\n return [...map.values()];\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Shorthand expansion — pure TS sugar over canonical `Rule` objects.\n// ──────────────────────────────────────────────────────────────────────────\n\ntype RawSingle = string | Rule;\ntype RawRuleset = RawSingle | RawSingle[];\n\nfunction parseShortRule(raw: RawSingle): Rule {\n if (typeof raw !== 'string') return raw;\n const parts = raw.split('.');\n const action = parts[0] as RuleAction;\n if (action !== 'allow' && action !== 'disallow') {\n throw new Error(`Unknown action '${action}'`);\n }\n const type = parts[1];\n if (!type) return { action };\n const rest = parts.slice(2).join('.');\n switch (type) {\n case 'os': {\n if (!rest) throw new Error('missing OS name');\n const atIdx = rest.indexOf('@');\n const name = (atIdx === -1 ? rest : rest.slice(0, atIdx)) as OsName;\n if (!['linux', 'windows', 'osx'].includes(name))\n throw new Error(`invalid os name '${name}'`);\n const version = atIdx === -1 ? undefined : rest.slice(atIdx + 1);\n return version\n ? { action, os: { name, version } }\n : { action, os: { name } };\n }\n case 'features': {\n if (!rest) throw new Error('missing feature name');\n return { action, features: { [rest]: true } };\n }\n case 'arch': {\n if (!rest) throw new Error('missing arch');\n const arch = rest as OsArch;\n if (!['x86', 'x86_64', 'arm', 'aarch64', 'any'].includes(arch))\n throw new Error(`invalid arch '${arch}'`);\n return { action, os: { arch } };\n }\n default:\n throw new Error(`unknown rule type '${type}'`);\n }\n}\n\nexport function parseShortRuleset(raw: RawRuleset): Ruleset {\n const arr: RawSingle[] = Array.isArray(raw) ? raw : [raw];\n return arr.map(parseShortRule);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// `parseValset` — used by the Mojang version-JSON mapper. Build-time-only;\n// not part of the napi boundary surface.\n// ──────────────────────────────────────────────────────────────────────────\n\nexport function parseValset(raw: unknown): Valset {\n if (!Array.isArray(raw)) {\n throw new Error('parseValset: expected an array');\n }\n return raw.map((entry): Val => {\n if (typeof entry === 'string') return { rules: [], value: [entry] };\n if (entry && typeof entry === 'object') {\n const obj = entry as { rules?: unknown; value: string | string[] };\n const rules = obj.rules ? parseShortRuleset(obj.rules as RawRuleset) : [];\n const value = Array.isArray(obj.value) ? obj.value : [obj.value];\n return { rules, value };\n }\n throw new Error('parseValset: invalid entry');\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAkBA,MAAa,kBAAkB;AAsB/B,MAAM,yBAA8C,IAAI,IAAI;CAC1D;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAC/B,CAAC;AAEF,MAAM,kBAAuC,IAAI,IAAI;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,sBAAsB,KAAuB;AACpD,KAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;CAI5C,MAAM,QAAS,IAA4B;AAC3C,KAAI,OAAO;EACT,MAAM,OAAQ,MAA4B;AAC1C,MAAI,QAAQ,gBAAgB,IAAI,KAAK,CAAE,QAAO;EAC9C,MAAM,QAAS,MAAiC;AAChD,MAAI,MAAM,QAAQ,MAAM,IAAI,MAAM,SAAS,EAEzC,QAAO,MAAM,OAAO,MAAM;GACxB,MAAM,IAAK,GAAqC;AAChD,UAAO,MAAM,UAAa,gBAAgB,IAAI,EAAE;IAChD;;CAGN,MAAM,OAAQ,IAA0B;AACxC,KAAI,QAAQ,gBAAgB,IAAI,KAAK,CAAE,QAAO;AAC9C,QACE,eAAe,aACf,OAAQ,IAAc,YAAY,YACjC,IAAc,QAAQ,aAAa,CAAC,SAAS,eAAe;;AAIjE,SAAS,UAAU,SAAiB,MAAc,KAAqB;CAErE,MAAM,MAAM,OAAO,MAAM,UAAU;CACnC,MAAM,SAAS,KAAK,IAAI,KAAK,IAAI;AACjC,QAAO,KAAK,MAAM,UAAU,KAAM,KAAK,QAAQ,GAAG,IAAK;;AAGzD,SAAS,MAAM,IAAY,QAAqC;AAC9D,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,MAAI,QAAQ,SAAS;AACnB,UAAO,OAAO,0BAAU,IAAI,MAAM,UAAU,CAAC;AAC7C;;EAEF,MAAM,IAAI,iBAAiB;AACzB,WAAQ,oBAAoB,SAAS,QAAQ;AAC7C,YAAS;KACR,GAAG;EACN,MAAM,gBAAgB;AACpB,gBAAa,EAAE;AACf,UAAO,QAAQ,0BAAU,IAAI,MAAM,UAAU,CAAC;;AAEhD,UAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;GAC1D;;AAGJ,eAAsB,eACpB,OACA,OAAoB,EAAE,EACtB,QAA2B,EAAE,EACV;CACnB,MAAM,WAAW,KAAK,IAAI,GAAG,MAAM,YAAY,EAAE;CACjD,MAAM,OAAO,MAAM,eAAe;CAClC,MAAM,MAAM,MAAM,cAAc;CAChC,MAAM,gBACJ,MAAM,yBAAyB,MAC3B,MAAM,gBACN,MAAM,gBACJ,IAAI,IAAI,MAAM,cAAc,GAC5B;CACR,MAAM,SAAS,MAAM,UAAU,KAAK,UAAU;CAG9C,MAAM,UAAU,IAAI,QAAQ,KAAK,QAAQ;AACzC,KAAI,CAAC,QAAQ,IAAI,aAAa,CAAE,SAAQ,IAAI,cAAc,gBAAgB;CAE1E,IAAI;AACJ,MAAK,IAAI,UAAU,GAAG,WAAW,UAAU,UACzC,KAAI;EACF,MAAM,MAAM,MAAM,MAAM,OAAO;GAAE,GAAG;GAAM;GAAS;GAAQ,CAAC;AAC5D,MAAI,IAAI,MAAM,YAAY,YAAY,CAAC,cAAc,IAAI,IAAI,OAAO,CAClE,QAAO;AAGT,MAAI;AACF,SAAM,IAAI,MAAM,QAAQ;UAClB;EAGR,MAAM,QAAQ,UAAU,SAAS,MAAM,IAAI;AAC3C,QAAM,UAAU;GAAE;GAAS,SAAS;GAAO,QAAQ,IAAI;GAAQ,CAAC;AAChE,QAAM,MAAM,OAAO,OAAO;UACnB,KAAK;AACZ,YAAU;AACV,MAAI,QAAQ,QAAS,OAAM;AAC3B,MAAI,CAAC,sBAAsB,IAAI,IAAI,YAAY,SAAU,OAAM;EAC/D,MAAM,QAAQ,UAAU,SAAS,MAAM,IAAI;AAC3C,QAAM,UAAU;GAAE;GAAS,SAAS;GAAO,OAAO;GAAK,CAAC;AACxD,QAAM,MAAM,OAAO,OAAO;;AAI9B,OAAM;;;;;;;;;;;;;;;;;;;;;AC7HR,MAAa,eAAe,EAAE,KAAK;CAAC;CAAS;CAAW;CAAM,CAAC;AAC/D,MAAa,eAAe,EAAE,KAAK;CAAC;CAAO;CAAU;CAAO;CAAW;CAAM,CAAC;AAC9E,MAAM,qBAAqB,EAAE,OAAO;CAClC,MAAM,aAAa,UAAU;CAC7B,SAAS,EAAE,QAAQ,CAAC,UAAU;CAC9B,MAAM,aAAa,UAAU;CAC9B,CAAC;AACF,MAAM,mBAAmB,EAAE,KAAK,CAAC,SAAS,WAAW,CAAC;AACtD,MAAa,aAAa,EAAE,MAAM;CAChC,EAAE,OAAO;EAAE,QAAQ;EAAkB,IAAI;EAAoB,CAAC;CAC9D,EAAE,OAAO;EACP,QAAQ;EACR,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,SAAS,CAAC;EAC5C,CAAC;CACF,EAAE,OAAO,EAAE,QAAQ,kBAAkB,CAAC;CACvC,CAAC;AASF,SAAgB,eAAe,MAAyB;AACtD,QAAO,KAAK,eAAe,KAAK;;AAElC,SAAgB,eAAe,QAA2B;AACxD,QAAO,KAAK,eAAe,OAAO;;AAEpC,SAAgB,cAAc,OAAyB;AACrD,QAAO,KAAK,cAAc,MAAM;;AAElC,SAAgB,eACd,UACA,UACA,WAAqB,EAAE,EACb;AACV,QAAO,KAAK,eAAe,UAAU,UAAU,SAAS;;AAE1D,SAAgB,YACd,MACwB;AACxB,QAAO,KAAK,YAAY,KAAK;;AAE/B,SAAgB,YACd,UACA,MACQ;AACR,QAAO,KAAK,YAAY,UAAU,KAAK;;AAEzC,SAAgB,aACd,QACA,UACA,WAAqB,EAAE,EACb;AACV,QAAO,KAAK,aAAa,QAAQ,UAAU,SAAS;;AAEtD,SAAgB,aACd,QACA,UACA,WAAqB,EAAE,EACC;AACxB,QAAO,KAAK,aAAa,QAAQ,UAAU,SAAS;;AAEtD,SAAgB,iBACd,OACA,UACA,WAAqB,EAAE,EACd;AACT,QAAO,KAAK,iBAAiB,OAAO,UAAU,SAAS;;AAEzD,SAAgB,SAAS,MAAsB;AAC7C,QAAO,KAAK,SAAS,KAAK;;AAE5B,SAAgB,kBAAkB,MAAsB;AACtD,QAAO,KAAK,kBAAkB,KAAK;;;AAIrC,SAAgB,YAAY,MAAsB;AAChD,QAAO,IAAI,OAAO,KAAK,kBAAkB,KAAK,CAAC;;AA8HjD,MAAa,aAAa,SAAyB;CAAE,MAAM;CAAO;CAAK;AACvE,MAAa,cAAc,UAA0B;CAAE,MAAM;CAAQ;CAAM;AAC3E,MAAa,gBAAgB,YAA4B;CACvD,MAAM;CACN;CACD;AACD,MAAa,iBAAiB,aAA6B;CACzD,MAAM;CACN;CACD;AACD,MAAa,eAAe,WAA+B;CACzD,MAAM;CACN,OAAO,OAAO,KAAK,MAAM,CAAC,SAAS,SAAS;CAC7C;AAED,MAAa,eAAe,MAC1B,EAAE,SAAS;AACb,MAAa,gBACX,MAC2C,EAAE,SAAS;AACxD,MAAa,kBACX,MAC6C,EAAE,SAAS;AAC1D,MAAa,iBACX,MAC4C,EAAE,SAAS;AACzD,MAAa,mBACX,MAC8C,EAAE,SAAS;AAE3D,MAAa,eAAe,MAAc,UAA+B;CACvE,MAAM;CACN;CACA;CACD;AACD,MAAa,eACX,SACA,MACA,UACiB;CAAE,MAAM;CAAQ;CAAS;CAAM,GAAG;CAAM;AAC3D,MAAa,eACX,MACA,UACiB;CAAE,MAAM;CAAQ;CAAM,GAAG;CAAM;AAElD,MAAa,qBAA8B,EAAE;AAC7C,MAAa,kBAAkB,SAA0B,CACvD;CAAE,QAAQ;CAAS,IAAI,EAAE,MAAM;CAAE,CAClC;;AAGD,SAAgB,qBAAqB,WAAmC;CACtE,MAAM,QAAQ,MAAsB;EAClC,MAAM,QAAQ,EAAE,MAAM,IAAI;EAC1B,MAAM,QAAkB,EAAE;EAC1B,MAAM,OAAO,EAAE,WAAW,IAAI;AAC9B,OAAK,MAAM,OAAO,OAAO;AACvB,OAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,OAAI,QAAQ,MACV;QAAI,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,OAAO,KAAM,OAAM,KAAK;aAC5D,CAAC,KAAM,OAAM,KAAK,KAAK;SAC3B,OAAM,KAAK,IAAI;;EAExB,MAAM,SAAS,MAAM,KAAK,IAAI;AAC9B,MAAI,KAAM,QAAO,MAAM;AACvB,SAAO,WAAW,KAAK,MAAM;;CAE/B,MAAM,sBAAM,IAAI,KAAuB;AACvC,MAAK,MAAM,KAAK,UAAW,KAAI,IAAI,KAAK,EAAE,KAAK,EAAE,EAAE;AACnD,QAAO,CAAC,GAAG,IAAI,QAAQ,CAAC;;AAU1B,SAAS,eAAe,KAAsB;AAC5C,KAAI,OAAO,QAAQ,SAAU,QAAO;CACpC,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,MAAM,SAAS,MAAM;AACrB,KAAI,WAAW,WAAW,WAAW,WACnC,OAAM,IAAI,MAAM,mBAAmB,OAAO,GAAG;CAE/C,MAAM,OAAO,MAAM;AACnB,KAAI,CAAC,KAAM,QAAO,EAAE,QAAQ;CAC5B,MAAM,OAAO,MAAM,MAAM,EAAE,CAAC,KAAK,IAAI;AACrC,SAAQ,MAAR;EACE,KAAK,MAAM;AACT,OAAI,CAAC,KAAM,OAAM,IAAI,MAAM,kBAAkB;GAC7C,MAAM,QAAQ,KAAK,QAAQ,IAAI;GAC/B,MAAM,OAAQ,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,MAAM;AACxD,OAAI,CAAC;IAAC;IAAS;IAAW;IAAM,CAAC,SAAS,KAAK,CAC7C,OAAM,IAAI,MAAM,oBAAoB,KAAK,GAAG;GAC9C,MAAM,UAAU,UAAU,KAAK,SAAY,KAAK,MAAM,QAAQ,EAAE;AAChE,UAAO,UACH;IAAE;IAAQ,IAAI;KAAE;KAAM;KAAS;IAAE,GACjC;IAAE;IAAQ,IAAI,EAAE,MAAM;IAAE;;EAE9B,KAAK;AACH,OAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAClD,UAAO;IAAE;IAAQ,UAAU,GAAG,OAAO,MAAM;IAAE;EAE/C,KAAK,QAAQ;AACX,OAAI,CAAC,KAAM,OAAM,IAAI,MAAM,eAAe;GAC1C,MAAM,OAAO;AACb,OAAI,CAAC;IAAC;IAAO;IAAU;IAAO;IAAW;IAAM,CAAC,SAAS,KAAK,CAC5D,OAAM,IAAI,MAAM,iBAAiB,KAAK,GAAG;AAC3C,UAAO;IAAE;IAAQ,IAAI,EAAE,MAAM;IAAE;;EAEjC,QACE,OAAM,IAAI,MAAM,sBAAsB,KAAK,GAAG;;;AAIpD,SAAgB,kBAAkB,KAA0B;AAE1D,SADyB,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,IAAI,EAC9C,IAAI,eAAe;;AAQhC,SAAgB,YAAY,KAAsB;AAChD,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,iCAAiC;AAEnD,QAAO,IAAI,KAAK,UAAe;AAC7B,MAAI,OAAO,UAAU,SAAU,QAAO;GAAE,OAAO,EAAE;GAAE,OAAO,CAAC,MAAM;GAAE;AACnE,MAAI,SAAS,OAAO,UAAU,UAAU;GACtC,MAAM,MAAM;AAGZ,UAAO;IAAE,OAFK,IAAI,QAAQ,kBAAkB,IAAI,MAAoB,GAAG,EAAE;IAEzD,OADF,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,MAAM;IACzC;;AAEzB,QAAM,IAAI,MAAM,6BAA6B;GAC7C"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@opys/core",
3
+ "version": "0.1.2",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.js",
6
+ "peerDependencies": {
7
+ "zod": "^4.0.0"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsdown lib/index.ts --format esm,cjs --dts --clean",
20
+ "typecheck": "tsc --noEmit -p tsconfig.json",
21
+ "test": "vitest run tests/unit"
22
+ },
23
+ "type": "module",
24
+ "dependencies": {
25
+ "@opys/core-binding": "^0.1.2",
26
+ "@opys/mojang-rules": "^0.1.2"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/harmoniya-net/opys.git",
34
+ "directory": "packages/core"
35
+ }
36
+ }