@olivervea/pipelines 2.0.0-beta.3 → 2.0.0-beta.30
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 +163 -78
- package/manifest.schema.json +300 -16
- package/package.json +6 -4
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +98 -64
- package/src/build/cli.ts +33 -23
- package/src/build/elf.ts +16 -7
- package/src/build/hashes.ts +40 -51
- package/src/build/project.ts +55 -52
- package/src/builder/pipeline.ts +348 -89
- package/src/builder/refs.ts +15 -5
- package/src/core/brands.ts +6 -5
- package/src/core/config-refs.ts +103 -0
- package/src/core/config.ts +224 -0
- package/src/core/external.ts +86 -0
- package/src/core/handlers.ts +62 -15
- package/src/core/matrix.ts +150 -0
- package/src/core/models.ts +37 -15
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +113 -38
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +119 -42
- package/src/ctx/index.ts +103 -38
- package/src/ctx/links.ts +30 -0
- package/src/ctx/sh.ts +104 -44
- package/src/ctx/types.ts +24 -4
- package/src/describe/config-schema.ts +98 -0
- package/src/describe/context.ts +187 -0
- package/src/describe/describe.ts +63 -355
- package/src/describe/failure-handlers.ts +34 -0
- package/src/describe/manifest-schema.ts +304 -61
- package/src/describe/mapper-source.ts +252 -105
- package/src/describe/record.ts +67 -37
- package/src/describe/schemas.ts +71 -0
- package/src/describe/stages.ts +166 -0
- package/src/describe/step.ts +544 -0
- package/src/describe/triggers.ts +72 -0
- package/src/dev/cli.ts +62 -35
- package/src/harness/env.ts +47 -25
- package/src/harness/execute.ts +74 -30
- package/src/harness/exit.ts +1 -1
- package/src/harness/index.ts +19 -8
- package/src/harness/main.ts +234 -98
- package/src/harness/result.ts +45 -24
- package/src/harness/validate.ts +82 -15
- package/src/index.ts +139 -10
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +115 -0
- package/src/library/github-source.ts +100 -47
- package/src/library/helm-deploy-ssh.ts +426 -112
- package/src/library/helm-deploy.ts +529 -0
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +510 -162
- package/src/library/index.ts +48 -14
- package/src/library/models.ts +35 -14
- package/src/library/ssh.ts +44 -18
- package/src/testing/index.ts +100 -35
- package/src/util/canonical.ts +7 -4
- package/src/util/code-hash.ts +17 -0
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +83 -38
- package/src/library/script.ts +0 -85
- package/src/util/handler-hash.ts +0 -22
package/src/build/cli.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// olve-build <pipelineDir> --out <dir> [--target bun-linux-x64|bun-linux-x64-musl] [--upload-url <url>] [--sentinels] [--hash-only]
|
|
3
|
-
import { parseArgs } from
|
|
4
|
-
import { canonicalJson } from
|
|
5
|
-
import { build, BuildError, type Target } from
|
|
6
|
-
import {
|
|
7
|
-
import { harnessInfo, loadProject, ProjectError } from
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { canonicalJson } from '../util/canonical.ts';
|
|
5
|
+
import { build, BuildError, type Target } from './build.ts';
|
|
6
|
+
import { codeHash } from './hashes.ts';
|
|
7
|
+
import { harnessInfo, loadProject, ProjectError } from './project.ts';
|
|
8
8
|
|
|
9
|
-
const USAGE =
|
|
9
|
+
const USAGE =
|
|
10
|
+
'usage: olve-build <pipelineDir> --out <dir> [--target bun-linux-x64|bun-linux-x64-musl] [--upload-url <url>] [--sentinels]\n olve-build <pipelineDir> --hash-only';
|
|
10
11
|
|
|
11
12
|
async function main(): Promise<number> {
|
|
12
13
|
let parsed;
|
|
@@ -14,12 +15,12 @@ async function main(): Promise<number> {
|
|
|
14
15
|
parsed = parseArgs({
|
|
15
16
|
allowPositionals: true,
|
|
16
17
|
options: {
|
|
17
|
-
out: { type:
|
|
18
|
-
target: { type:
|
|
19
|
-
|
|
20
|
-
sentinels: { type:
|
|
21
|
-
|
|
22
|
-
help: { type:
|
|
18
|
+
out: { type: 'string' },
|
|
19
|
+
target: { type: 'string' },
|
|
20
|
+
'upload-url': { type: 'string' },
|
|
21
|
+
sentinels: { type: 'boolean', default: false },
|
|
22
|
+
'hash-only': { type: 'boolean', default: false },
|
|
23
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
23
24
|
},
|
|
24
25
|
});
|
|
25
26
|
} catch (e) {
|
|
@@ -27,23 +28,32 @@ async function main(): Promise<number> {
|
|
|
27
28
|
return 64;
|
|
28
29
|
}
|
|
29
30
|
const { values, positionals } = parsed;
|
|
30
|
-
if (values.help) return console.log(USAGE), 0;
|
|
31
|
-
if (positionals.length !== 1) return console.error(USAGE), 64;
|
|
31
|
+
if (values.help) return (console.log(USAGE), 0);
|
|
32
|
+
if (positionals.length !== 1) return (console.error(USAGE), 64);
|
|
32
33
|
const dir = positionals[0]!;
|
|
33
34
|
try {
|
|
34
|
-
if (values[
|
|
35
|
-
const
|
|
36
|
-
await Bun.write(
|
|
35
|
+
if (values['hash-only']) {
|
|
36
|
+
const code = await codeHash(await loadProject(dir), harnessInfo());
|
|
37
|
+
await Bun.write(
|
|
38
|
+
Bun.stdout,
|
|
39
|
+
canonicalJson({ harness: harnessInfo(), codeHash: code }),
|
|
40
|
+
);
|
|
37
41
|
return 0;
|
|
38
42
|
}
|
|
39
|
-
if (!values.out) return console.error(`--out is required\n${USAGE}`), 64;
|
|
40
|
-
const target = (values.target ??
|
|
41
|
-
if (target !==
|
|
42
|
-
|
|
43
|
+
if (!values.out) return (console.error(`--out is required\n${USAGE}`), 64);
|
|
44
|
+
const target = (values.target ?? 'bun-linux-x64') as Target;
|
|
45
|
+
if (target !== 'bun-linux-x64' && target !== 'bun-linux-x64-musl')
|
|
46
|
+
return (console.error(`unsupported target ${target}`), 64);
|
|
47
|
+
await build(dir, {
|
|
48
|
+
out: values.out,
|
|
49
|
+
target,
|
|
50
|
+
uploadUrl: values['upload-url'],
|
|
51
|
+
sentinels: values.sentinels,
|
|
52
|
+
});
|
|
43
53
|
return 0;
|
|
44
54
|
} catch (e) {
|
|
45
|
-
if (e instanceof BuildError) return console.error(e.message), e.exitCode;
|
|
46
|
-
if (e instanceof ProjectError) return console.error(e.message), 78;
|
|
55
|
+
if (e instanceof BuildError) return (console.error(e.message), e.exitCode);
|
|
56
|
+
if (e instanceof ProjectError) return (console.error(e.message), 78);
|
|
47
57
|
console.error((e as Error)?.stack ?? String(e));
|
|
48
58
|
return 1;
|
|
49
59
|
}
|
package/src/build/elf.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { closeSync, openSync, readSync } from
|
|
1
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
export const GLIBC_INTERP =
|
|
4
|
-
export const MUSL_INTERP =
|
|
3
|
+
export const GLIBC_INTERP = '/lib64/ld-linux-x86-64.so.2';
|
|
4
|
+
export const MUSL_INTERP = '/lib/ld-musl-x86-64.so.1';
|
|
5
5
|
|
|
6
6
|
function read(fd: number, pos: number, len: number): Buffer {
|
|
7
7
|
const b = Buffer.alloc(len);
|
|
8
8
|
const n = readSync(fd, b, 0, len, pos);
|
|
9
|
-
if (n !== len) throw new Error(
|
|
9
|
+
if (n !== len) throw new Error('truncated ELF file');
|
|
10
10
|
return b;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** PT_INTERP of an x86-64 ELF executable, or null if it has none (static). */
|
|
14
14
|
export function readInterpreter(path: string): string | null {
|
|
15
|
-
const fd = openSync(path,
|
|
15
|
+
const fd = openSync(path, 'r');
|
|
16
16
|
try {
|
|
17
17
|
const h = read(fd, 0, 64);
|
|
18
18
|
if (h.readUInt32BE(0) !== 0x7f454c46) throw new Error(`${path} is not an ELF file`);
|
|
19
|
-
if (h[4] !== 2 || h[5] !== 1)
|
|
19
|
+
if (h[4] !== 2 || h[5] !== 1)
|
|
20
|
+
throw new Error(`${path} is not a 64-bit little-endian ELF file`);
|
|
20
21
|
const phoff = Number(h.readBigUInt64LE(0x20));
|
|
21
22
|
const phentsize = h.readUInt16LE(0x36);
|
|
22
23
|
const phnum = h.readUInt16LE(0x38);
|
|
@@ -25,7 +26,7 @@ export function readInterpreter(path: string): string | null {
|
|
|
25
26
|
if (ph.readUInt32LE(0) === 3 /* PT_INTERP */) {
|
|
26
27
|
const off = Number(ph.readBigUInt64LE(0x08));
|
|
27
28
|
const size = Number(ph.readBigUInt64LE(0x20));
|
|
28
|
-
return read(fd, off, size).toString(
|
|
29
|
+
return read(fd, off, size).toString('latin1').replace(/\0+$/, '');
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
return null;
|
|
@@ -33,3 +34,11 @@ export function readInterpreter(path: string): string | null {
|
|
|
33
34
|
closeSync(fd);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
37
|
+
|
|
38
|
+
/** Why `binary` won't start through `target`'s loader (the harness runs it via /olve/lib/<loader>), or undefined. */
|
|
39
|
+
export function loaderProblem(binary: string, target: string): string | undefined {
|
|
40
|
+
const expected = target === 'bun-linux-x64' ? GLIBC_INTERP : MUSL_INTERP;
|
|
41
|
+
const interp = readInterpreter(binary);
|
|
42
|
+
if (interp === expected) return undefined;
|
|
43
|
+
return `compiled binary has PT_INTERP ${interp ?? '(none)'}, expected ${expected} — compile on a glibc Bun image (oven/bun:<version>-slim), not alpine`;
|
|
44
|
+
}
|
package/src/build/hashes.ts
CHANGED
|
@@ -1,67 +1,56 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
7
|
-
import { composeHandlerHash, type HarnessInfo } from "../util/handler-hash.ts";
|
|
8
|
-
import { sha256Hex } from "../util/hash.ts";
|
|
9
|
-
import type { Project } from "./project.ts";
|
|
1
|
+
// The build's code hash: everything the pipeline runs, bundled once with the SDK external (the
|
|
2
|
+
// harness identity stands for it).
|
|
3
|
+
import type { BunPlugin } from 'bun';
|
|
4
|
+
import { sha256Hex } from '../util/hash.ts';
|
|
5
|
+
import { composeCodeHash, type HarnessInfo } from '../util/code-hash.ts';
|
|
6
|
+
import type { Project } from './project.ts';
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function externals(project: Project): BunPlugin {
|
|
21
|
-
const contracts = new Set(project.contractFiles);
|
|
22
|
-
return {
|
|
23
|
-
name: "olve-handler-hash-externals",
|
|
24
|
-
setup(b) {
|
|
25
|
-
b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({ path: args.path, external: true }));
|
|
26
|
-
b.onResolve({ filter: /^\.{1,2}\// }, (args) => {
|
|
27
|
-
const base = resolve(dirname(args.importer), args.path);
|
|
28
|
-
for (const cand of [base, `${base}.ts`, join(base, "index.ts")]) if (contracts.has(cand)) return { path: args.path, external: true };
|
|
29
|
-
return undefined;
|
|
30
|
-
});
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
8
|
+
const sdkExternal: BunPlugin = {
|
|
9
|
+
name: 'olve-code-hash-external',
|
|
10
|
+
setup(b) {
|
|
11
|
+
b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({
|
|
12
|
+
path: args.path,
|
|
13
|
+
external: true,
|
|
14
|
+
}));
|
|
15
|
+
},
|
|
16
|
+
};
|
|
34
17
|
|
|
35
|
-
|
|
18
|
+
async function bundleHash(project: Project): Promise<string> {
|
|
19
|
+
const entry = project.pipelineFile;
|
|
36
20
|
const r = await Bun.build({
|
|
37
21
|
entrypoints: [entry],
|
|
38
|
-
target:
|
|
39
|
-
format:
|
|
22
|
+
target: 'bun',
|
|
23
|
+
format: 'esm',
|
|
40
24
|
root: project.dir,
|
|
41
|
-
naming: {
|
|
42
|
-
|
|
25
|
+
naming: {
|
|
26
|
+
entry: '[dir]/[name].[ext]',
|
|
27
|
+
chunk: '[dir]/[name].[ext]',
|
|
28
|
+
asset: '[dir]/[name].[ext]',
|
|
29
|
+
},
|
|
30
|
+
plugins: [sdkExternal],
|
|
43
31
|
throw: false,
|
|
44
32
|
});
|
|
45
|
-
if (!r.success)
|
|
33
|
+
if (!r.success)
|
|
34
|
+
throw new AggregateError(
|
|
35
|
+
r.logs,
|
|
36
|
+
`bundling ${entry} failed:\n${r.logs.map(String).join('\n')}`,
|
|
37
|
+
);
|
|
46
38
|
const parts: string[] = [];
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
for (const o of [...r.outputs].sort((a, b) =>
|
|
40
|
+
a.path < b.path ? -1 : a.path > b.path ? 1 : 0,
|
|
41
|
+
)) {
|
|
42
|
+
const name = o.path.replace(/^\.\//, '');
|
|
51
43
|
const bytes = new Uint8Array(await o.arrayBuffer());
|
|
52
44
|
// Bun prefixes each module with a `// <path>` comment relative to the *process cwd*; drop those
|
|
53
45
|
// so the hash depends only on code, not on where the repo or the build tool runs.
|
|
54
|
-
const content = name.endsWith(
|
|
46
|
+
const content = name.endsWith('.js')
|
|
47
|
+
? new TextDecoder().decode(bytes).replace(/^\/\/ \S+\n/gm, '')
|
|
48
|
+
: bytes;
|
|
55
49
|
parts.push(`${name}\0${sha256Hex(content)}`);
|
|
56
50
|
}
|
|
57
|
-
return
|
|
51
|
+
return 'sha256:' + sha256Hex(parts.join('\n'));
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
export async function
|
|
61
|
-
|
|
62
|
-
for (const [name, file] of project.handlers) {
|
|
63
|
-
const b = await bundleHash(project, file);
|
|
64
|
-
out[name] = { bundleHash: b.hash, handlerHash: composeHandlerHash(b.hash, harness), outputs: b.outputs };
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
54
|
+
export async function codeHash(project: Project, harness: HarnessInfo): Promise<string> {
|
|
55
|
+
return composeCodeHash(await bundleHash(project), harness);
|
|
67
56
|
}
|
package/src/build/project.ts
CHANGED
|
@@ -1,59 +1,53 @@
|
|
|
1
1
|
// Layout of a pipeline directory and the identity of the SDK building it.
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync } from
|
|
3
|
-
import { dirname, join, relative, resolve } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import type { HarnessInfo } from "../util/handler-hash.ts";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { sha256Hex } from '../util/hash.ts';
|
|
5
|
+
import type { HarnessInfo } from '../util/code-hash.ts';
|
|
7
6
|
|
|
8
|
-
export const SDK_ROOT = resolve(import.meta.dir,
|
|
7
|
+
export const SDK_ROOT = resolve(import.meta.dir, '../..');
|
|
9
8
|
|
|
10
9
|
export interface Project {
|
|
11
10
|
dir: string;
|
|
12
11
|
pipelineFile: string;
|
|
13
|
-
/** Contract files (excluded from handler bundles): pipeline.ts, models.ts. */
|
|
14
|
-
contractFiles: string[];
|
|
15
|
-
/** step name → absolute handler file, sorted by step name. The step is the one the file's `implement(step, …)` names. */
|
|
16
|
-
handlers: [string, string][];
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
export class ProjectError extends Error {
|
|
20
|
-
override name =
|
|
15
|
+
override name = 'ProjectError';
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
export async function loadProject(dirArg: string): Promise<Project> {
|
|
24
19
|
const dir = resolve(dirArg);
|
|
25
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const contractFiles = ["pipeline.ts", "models.ts"].map((f) => join(dir, f)).filter((f) => existsSync(f));
|
|
29
|
-
const hdir = join(dir, "handlers");
|
|
30
|
-
const files = existsSync(hdir)
|
|
31
|
-
? readdirSync(hdir)
|
|
32
|
-
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts") && statSync(join(hdir, f)).isFile())
|
|
33
|
-
.sort()
|
|
34
|
-
: [];
|
|
35
|
-
return { dir, pipelineFile, contractFiles, handlers: await resolveHandlers(hdir, files) };
|
|
20
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
21
|
+
throw new ProjectError(`${dirArg} is not a directory`);
|
|
22
|
+
return { dir, pipelineFile: entryOf(dirArg, dir) };
|
|
36
23
|
}
|
|
37
24
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
25
|
+
/** The file `main` in the package's package.json names. */
|
|
26
|
+
function entryOf(dirArg: string, dir: string): string {
|
|
27
|
+
const pkg = join(dir, 'package.json');
|
|
28
|
+
const main = existsSync(pkg)
|
|
29
|
+
? (JSON.parse(readFileSync(pkg, 'utf8')) as { main?: string }).main
|
|
30
|
+
: undefined;
|
|
31
|
+
if (!main)
|
|
32
|
+
throw new ProjectError(
|
|
33
|
+
`${join(dirArg, 'package.json')} must name the entry point in \`main\` (a file that default-exports pipeline({...}))`,
|
|
34
|
+
);
|
|
35
|
+
const entry = join(dir, main);
|
|
36
|
+
if (!existsSync(entry))
|
|
37
|
+
throw new ProjectError(
|
|
38
|
+
`${join(dirArg, main)} not found (it must default-export pipeline({...}))`,
|
|
39
|
+
);
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The default export of the entry point. */
|
|
44
|
+
export async function loadPipeline(project: Project): Promise<unknown> {
|
|
45
|
+
const mod = (await import(project.pipelineFile)) as { default?: unknown };
|
|
46
|
+
if (!mod.default)
|
|
47
|
+
throw new ProjectError(
|
|
48
|
+
`${project.pipelineFile} has no default export (export default pipeline({...})...)`,
|
|
49
|
+
);
|
|
50
|
+
return mod.default;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
function listFiles(dir: string): string[] {
|
|
@@ -73,28 +67,37 @@ let cached: HarnessInfo | undefined;
|
|
|
73
67
|
*/
|
|
74
68
|
export function harnessInfo(): HarnessInfo {
|
|
75
69
|
if (cached) return cached;
|
|
76
|
-
const pkg = JSON.parse(readFileSync(join(SDK_ROOT,
|
|
77
|
-
|
|
70
|
+
const pkg = JSON.parse(readFileSync(join(SDK_ROOT, 'package.json'), 'utf8')) as {
|
|
71
|
+
version: string;
|
|
72
|
+
};
|
|
73
|
+
const src = join(SDK_ROOT, 'src');
|
|
78
74
|
const files = listFiles(src)
|
|
79
|
-
.map((f) => relative(src, f).split(
|
|
80
|
-
.filter(
|
|
75
|
+
.map((f) => relative(src, f).split('\\').join('/'))
|
|
76
|
+
.filter(
|
|
77
|
+
(f) =>
|
|
78
|
+
!f.startsWith('build/') &&
|
|
79
|
+
!f.startsWith('dev/') &&
|
|
80
|
+
!f.startsWith('testing/') &&
|
|
81
|
+
f.endsWith('.ts'),
|
|
82
|
+
)
|
|
81
83
|
.sort();
|
|
82
|
-
const h = new Bun.CryptoHasher(
|
|
84
|
+
const h = new Bun.CryptoHasher('sha256');
|
|
83
85
|
for (const f of files) h.update(`${f}\0${sha256Hex(readFileSync(join(src, f)))}\n`);
|
|
84
|
-
let zodVersion =
|
|
86
|
+
let zodVersion = 'unknown';
|
|
85
87
|
try {
|
|
86
|
-
const zodPkg = Bun.resolveSync(
|
|
87
|
-
zodVersion = (JSON.parse(readFileSync(zodPkg,
|
|
88
|
+
const zodPkg = Bun.resolveSync('zod/package.json', SDK_ROOT);
|
|
89
|
+
zodVersion = (JSON.parse(readFileSync(zodPkg, 'utf8')) as { version: string })
|
|
90
|
+
.version;
|
|
88
91
|
} catch {
|
|
89
92
|
/* keep unknown */
|
|
90
93
|
}
|
|
91
94
|
h.update(`zod@${zodVersion}\n`);
|
|
92
|
-
cached = { bun: Bun.version, sdk: pkg.version, sdkHash:
|
|
95
|
+
cached = { bun: Bun.version, sdk: pkg.version, sdkHash: 'sha256:' + h.digest('hex') };
|
|
93
96
|
return cached;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
export const relPosix = (from: string, to: string) => {
|
|
97
|
-
const r = relative(from, to).split(
|
|
98
|
-
return r.startsWith(
|
|
100
|
+
const r = relative(from, to).split('\\').join('/');
|
|
101
|
+
return r.startsWith('.') ? r : `./${r}`;
|
|
99
102
|
};
|
|
100
103
|
export { dirname };
|