@sibylline-labs/eris 0.0.0-stage → 0.3.1

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.
Files changed (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +146 -2
  4. package/bin/eris.ts +42 -0
  5. package/docs/ARCHITECTURE.md +188 -0
  6. package/docs/COMPLETION.md +106 -0
  7. package/docs/ERRORS.md +632 -0
  8. package/docs/OUTPUT-CONTRACT.md +219 -0
  9. package/docs/README.md +30 -0
  10. package/docs/UPDATE.md +159 -0
  11. package/docs/YAK.md +395 -0
  12. package/docs/dev/ADDING-A-COMMAND.md +79 -0
  13. package/docs/dev/DEVELOPMENT.md +88 -0
  14. package/docs/dev/LAYOUT.md +74 -0
  15. package/docs/dev/PLUGINS.md +382 -0
  16. package/docs/dev/RELEASING.md +157 -0
  17. package/docs/dev/TESTING.md +84 -0
  18. package/docs/dev/surfaces/secrets.md +95 -0
  19. package/package.json +55 -4
  20. package/packages/yak-contract/package.json +28 -0
  21. package/packages/yak-contract/schema/yak-v2.schema.json +416 -0
  22. package/packages/yak-contract/src/index.ts +1 -0
  23. package/packages/yak-contract/src/v2.ts +375 -0
  24. package/plugins/index.ts +12 -0
  25. package/plugins/yak/handlers.ts +128 -0
  26. package/plugins/yak/index.ts +51 -0
  27. package/plugins/yak/login.ts +300 -0
  28. package/plugins/yak/yak-context.ts +280 -0
  29. package/plugins/yak/yak-identity.ts +399 -0
  30. package/plugins/yak/yak-redact.ts +686 -0
  31. package/plugins/yak/yak-setup.ts +133 -0
  32. package/plugins/yak/yak-submit.ts +1166 -0
  33. package/plugins/yak/yak.ts +485 -0
  34. package/src/build.ts +248 -0
  35. package/src/cli/args.ts +434 -0
  36. package/src/cli/commands/completion/bash.ts +24 -0
  37. package/src/cli/commands/completion/domain.ts +203 -0
  38. package/src/cli/commands/completion/emit.ts +246 -0
  39. package/src/cli/commands/completion/shell.ts +24 -0
  40. package/src/cli/commands/completion/zsh.ts +24 -0
  41. package/src/cli/commands/completion.ts +48 -0
  42. package/src/cli/commands/update/github.ts +328 -0
  43. package/src/cli/commands/update/opt-in.ts +32 -0
  44. package/src/cli/commands/update/refusal.ts +21 -0
  45. package/src/cli/commands/update/release.ts +351 -0
  46. package/src/cli/commands/update/replace.ts +143 -0
  47. package/src/cli/commands/update/semver.ts +48 -0
  48. package/src/cli/commands/update.ts +47 -0
  49. package/src/cli/commands/usage.ts +68 -0
  50. package/src/cli/commands/version.ts +29 -0
  51. package/src/cli/compose.ts +273 -0
  52. package/src/cli/output.ts +81 -0
  53. package/src/cli/plugin.ts +85 -0
  54. package/src/cli/registry.ts +180 -0
  55. package/src/cli/router.ts +31 -0
  56. package/src/cli/ts-verb.ts +126 -0
  57. package/src/main.ts +223 -0
  58. package/src/runtime/env.ts +16 -0
  59. package/src/runtime/eris-assets.d.ts +5 -0
  60. package/src/runtime/home.ts +306 -0
  61. package/src/runtime/logging.ts +43 -0
  62. package/src/runtime/process.ts +388 -0
  63. package/src/runtime/settings.ts +191 -0
  64. package/src/sdk.ts +62 -0
  65. package/testing/build-child.ts +55 -0
  66. package/testing/fake-runner.ts +23 -0
  67. package/testing/index.ts +16 -0
  68. package/ts/lib/eris-fail.ts +180 -0
  69. package/ts/lib/pool.ts +63 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * build-child.ts — `build()` in a child bun process, for a suite that needs a
3
+ * compiled binary.
4
+ *
5
+ * A compile in the test process is not repeatable across test files: on Linux
6
+ * (bun 1.3.13, linux-x64 runner and a linux container) a later file's
7
+ * `Bun.build` fails with `EBADF` / `Unexpected reading file` on a module an
8
+ * earlier file's compile read, while each file alone passes. One process per
9
+ * build shares no bundler state with the suite or with another build.
10
+ *
11
+ * buildInChild({ module: join(root, "build.ts"), outfile, tag: "v0.0.1" })
12
+ *
13
+ * `module` is the file of a build, an absolute path to a module that exports
14
+ * `BUILD`, the `BuildOptions` to build with (examples/distribution/build.ts and
15
+ * scripts/build.ts are two). The rest of the spec is passed to `build()` over
16
+ * those. Refusals that throw before bundling still run in-process: they never
17
+ * reach `Bun.build`.
18
+ *
19
+ * Like `bun test` itself this is for a checkout: the child runs the build
20
+ * module under the bun that runs the tests. Never import it from the program.
21
+ */
22
+
23
+ import { spawnSync } from "node:child_process";
24
+ import { dirname, isAbsolute } from "node:path";
25
+ import { build, type BuildOptions, type BuildResult } from "../src/build";
26
+
27
+ export interface ChildBuild {
28
+ /** Absolute path of a module exporting `BUILD: BuildOptions`. */
29
+ readonly module: string;
30
+ readonly outfile: string;
31
+ readonly tag?: string;
32
+ readonly releaseRepo?: string;
33
+ readonly versionExtra?: string;
34
+ readonly docsBase?: string;
35
+ }
36
+
37
+ /** Run one build in a fresh bun; its result, or the child's stderr as the error. */
38
+ export function buildInChild(spec: ChildBuild): BuildResult {
39
+ if (!isAbsolute(spec.module)) throw new Error(`buildInChild: module must be an absolute path: ${JSON.stringify(spec.module)}`);
40
+ const r = spawnSync(process.execPath, ["--no-env-file", "--config=/dev/null", import.meta.path, JSON.stringify(spec)], {
41
+ cwd: dirname(spec.module),
42
+ encoding: "utf8",
43
+ timeout: 170_000,
44
+ });
45
+ if (r.status !== 0) throw new Error(`build in a child bun failed (status ${r.status}, signal ${r.signal}):\n${r.stderr}`);
46
+ return JSON.parse(r.stdout) as BuildResult;
47
+ }
48
+
49
+ if (import.meta.main) {
50
+ const { module, ...rest } = JSON.parse(process.argv[2] ?? "{}") as ChildBuild;
51
+ const { BUILD } = (await import(module)) as { BUILD?: BuildOptions };
52
+ if (BUILD === undefined) throw new Error(`${module} does not export BUILD`);
53
+ const r = await build({ ...BUILD, ...rest });
54
+ process.stdout.write(`${JSON.stringify(r)}\n`);
55
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * fake-runner.ts — a `Runner` that records every call and answers from a
3
+ * function, so a test injects one instead of stubbing PATH
4
+ * (src/runtime/process.ts holds the real adapter and the interface).
5
+ */
6
+
7
+ import type { RunOptions, RunResult, Runner } from "../src/runtime/process";
8
+
9
+ export type FakeRunner = Runner & { calls: Array<{ argv: readonly string[]; opts: RunOptions }> };
10
+
11
+ /** A Runner that records every call and answers from `respond`. */
12
+ export function createFakeRunner(
13
+ respond: (argv: readonly string[], opts: RunOptions) => RunResult | Promise<RunResult>,
14
+ ): FakeRunner {
15
+ const calls: FakeRunner["calls"] = [];
16
+ return {
17
+ calls,
18
+ async run(argv, opts) {
19
+ calls.push({ argv, opts });
20
+ return respond(argv, opts);
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `eris/sdk/testing`: the test helpers a distribution's own suites reuse
3
+ * instead of copying (docs/dev/PLUGINS.md#testing-a-distribution).
4
+ *
5
+ * - `createFakeRunner`, `FakeRunner`: a `Runner` (eris/sdk) that records its
6
+ * calls and answers from a function;
7
+ * - `buildInChild`: `build()` in a child bun, one process per compile.
8
+ *
9
+ * Separate from `eris/sdk` and outside src/ so a binary never carries it: the
10
+ * `runtime-imports-no-testing` rule keeps src/, ts/ and plugins/ from importing
11
+ * it, and `distribution-imports-sdk-only` does the same for a distribution's
12
+ * source in this repository.
13
+ */
14
+
15
+ export { buildInChild, type ChildBuild } from "./build-child";
16
+ export { createFakeRunner, type FakeRunner } from "./fake-runner";
@@ -0,0 +1,180 @@
1
+ /**
2
+ * eris-fail.ts — the `ERROR`/`slug:`/`fix:`/`docs:` failure block, in TS.
3
+ *
4
+ * The block predates this module: an earlier Bash implementation of eris
5
+ * printed it (`eris::report`), and this module reproduces it BYTE FOR BYTE
6
+ * for the same inputs, because `docs/OUTPUT-CONTRACT.md` §4 says the shape is
7
+ * a **contract**: consumers line-prefix-parse it, and a re-implementation is
8
+ * required to *reproduce* the grammar, never to re-derive it.
9
+ * `tests/eris-fail.test.ts` proves byte-identity against what that Bash
10
+ * implementation printed, captured in a real bash subprocess and frozen
11
+ * (tests/helpers/frozen-oracle.ts) — not by eyeballing the two
12
+ * implementations side by side.
13
+ *
14
+ * The grammar (docs/OUTPUT-CONTRACT.md §4), reproduced here exactly:
15
+ *
16
+ * ERROR <what failed>
17
+ * slug: <slug>
18
+ * fix: <line> — zero or more, one per line of the fix text
19
+ * docs: <local path> — $ERIS_HOME/<relpath>
20
+ * docs: <github url> — <docs base>/<relpath>
21
+ *
22
+ * The docs base is https://github.com/sibylline-labs/eris/blob/main unless a
23
+ * distribution named its own: baked by `build({ docsBase })` (src/build.ts), or
24
+ * handed to `runProgram({ identity })` for a run from source (`identity()` in
25
+ * src/runtime/home.ts).
26
+ *
27
+ * All on stderr; stdout is never touched by anything in this file. Field
28
+ * order is fixed, every prefix carries its single trailing space, and the
29
+ * two `docs:` lines are always both present (local first). `<relpath>`
30
+ * defaults to `docs/ERRORS.md#<slug>`, exactly matching the Bash
31
+ * `eris::report`'s `docs="${4:-docs/ERRORS.md#$1}"`.
32
+ *
33
+ * The `fix:` line-splitting mirrors a bash quirk deliberately, not
34
+ * accidentally: `eris::report` feeds `$fix` through
35
+ * `<<< "$fix"` into a `while IFS= read -r _line || [[ -n "$_line" ]]`
36
+ * loop. A here-string ALWAYS appends one `\n` to its content before
37
+ * feeding it to the loop, so the loop's real input is `${fix}\n` — and
38
+ * `read` terminates the instant it hits EOF with a truly empty buffer,
39
+ * never on the placeholder `\n` it appended. Reproducing that requires the
40
+ * same transform: append one `\n`, split on `\n`, and drop exactly the
41
+ * one trailing empty element the appended `\n` introduces (see
42
+ * `splitFixLines` below). That transform is equal to a plain
43
+ * `fix.split("\n")` for every non-empty `fix` (a trailing `\n` yields one
44
+ * trailing empty `fix: ` line either way — mutation-checked); the
45
+ * only case that needs care is the EMPTY fix, which emits no `fix:` line at
46
+ * all. `tests/eris-fail-golden.test.ts` compares every documented slug,
47
+ * including a trailing-newline fix and an empty fix, against the real bash.
48
+ */
49
+
50
+ import { docsRoot, identity, installRoot, installRootIsUnwritten } from "../../src/runtime/home";
51
+
52
+ export interface ErisFailureInput {
53
+ /** Stable machine key, `^[a-z0-9]+(-[a-z0-9]+)+$` — a `docs/ERRORS.md`
54
+ * heading anchor. `tests/errors-doc.test.ts` harvests every `slug: "…"`
55
+ * literal and fails when its heading is missing. */
56
+ slug: string;
57
+ /** The `ERROR <what>` line's message. Never a secret value — name the
58
+ * slot/path being resolved, per `docs/OUTPUT-CONTRACT.md` §4. */
59
+ what: string;
60
+ /** Zero or more `fix:` lines, `\n`-joined — mirrors `eris::report`'s
61
+ * third positional exactly (multi-line fixes are literal, indentation
62
+ * included). Omit or pass `""` for no fix lines at all. */
63
+ fix?: string;
64
+ /** Repo-relative doc path, optionally with a `#anchor`. Defaults to
65
+ * `docs/ERRORS.md#<slug>`, same as bash's `"${4:-docs/ERRORS.md#$1}"`. */
66
+ docs?: string;
67
+ }
68
+
69
+ /**
70
+ * `$ERIS_HOME`: `ERIS_DIR` wins when set; otherwise the install root (src/runtime/home.ts): the
71
+ * checkout this file sits in, a distribution's own root, or a compiled binary's
72
+ * asset home. The local `docs:` line names the doc under that root, or under this
73
+ * package's when a distribution run from source does not carry it (`docsRoot`), so it
74
+ * names a file that exists every way eris runs.
75
+ */
76
+ export function erisHome(env: Record<string, string | undefined> = process.env): string {
77
+ const override = env.ERIS_DIR;
78
+ if (override && override.length > 0) return override;
79
+ return installRoot();
80
+ }
81
+
82
+ /**
83
+ * Split a `fix:` block into its per-line renderings, reproducing
84
+ * `eris::report`'s here-string + `read` loop byte for byte (see file
85
+ * header). Returns `[]` for an absent/empty `fix` — matching bash's
86
+ * `if [[ -n "$fix" ]]` guard, which skips the loop entirely rather than
87
+ * emitting one empty `fix: ` line.
88
+ */
89
+ export function splitFixLines(fix: string | undefined): string[] {
90
+ if (!fix || fix.length === 0) return [];
91
+ // Simulate the here-string's appended `\n`, split, then drop the ONE
92
+ // trailing empty element that append introduces — never a real line.
93
+ const withTrailingNewline = `${fix}\n`;
94
+ const parts = withTrailingNewline.split("\n");
95
+ parts.pop();
96
+ return parts;
97
+ }
98
+
99
+ /** The failure block's lines, in order, with NO trailing `\n` on any of
100
+ * them — `renderFailureBlock` is what adds one per line. Exposed
101
+ * separately so a test can assert on individual lines without
102
+ * re-parsing the joined block.
103
+ *
104
+ * With no local copy of the docs to name (`home` null, or the install root of a
105
+ * compiled binary whose asset home could not be written, `eris-cache-unwritable`),
106
+ * the first `docs:` line is the repo-relative
107
+ * path, so the block keeps its two `docs:` lines and the GitHub one stays the
108
+ * readable one. */
109
+ export function failureLines(input: ErisFailureInput, home: string | null): string[] {
110
+ const lines: string[] = [];
111
+ lines.push(`ERROR ${input.what}`);
112
+ lines.push(`slug: ${input.slug}`);
113
+ for (const fixLine of splitFixLines(input.fix)) {
114
+ lines.push(`fix: ${fixLine}`);
115
+ }
116
+ const docsRel = input.docs && input.docs.length > 0 ? input.docs : `docs/ERRORS.md#${input.slug}`;
117
+ const local = home === null || (installRootIsUnwritten() && home === installRoot()) ? null : home;
118
+ lines.push(`docs: ${local === null ? docsRel : `${docsRoot(local, docsRel)}/${docsRel}`}`);
119
+ lines.push(`docs: ${identity().docsBase}/${docsRel}`);
120
+ return lines;
121
+ }
122
+
123
+ /** The full block, `\n`-terminated on every line — byte-identical to what
124
+ * `eris::report`/`eris::fail` writes to stderr for the same inputs. */
125
+ export function renderFailureBlock(input: ErisFailureInput, home: string | null = erisHome()): string {
126
+ return failureLines(input, home)
127
+ .map((line) => `${line}\n`)
128
+ .join("");
129
+ }
130
+
131
+ /** A write-only stream: process.stderr, or a capture in tests. */
132
+ export interface FailureSink {
133
+ write(chunk: string): unknown;
134
+ }
135
+
136
+ /**
137
+ * The TS `eris::report`: write the block and RETURN 1
138
+ * instead of exiting, for a caller that owns something which must run after
139
+ * the failure (e.g. a failed WAL result after a failed write). Prefer
140
+ * `emitFailureAndExit`; the caller of this adds its own summary.
141
+ * `tests/eris-fail-golden.test.ts` pins it against every documented slug.
142
+ */
143
+ export function reportFailure(
144
+ input: ErisFailureInput,
145
+ home: string = erisHome(),
146
+ sink: FailureSink = process.stderr,
147
+ ): 1 {
148
+ sink.write(renderFailureBlock(input, home));
149
+ return 1;
150
+ }
151
+
152
+ /** Write the block to stderr and exit 1 — the TS `eris::fail` equivalent.
153
+ * Stdout is never touched (§1 of the output contract: a failure writes
154
+ * nothing to stdout). */
155
+ export function emitFailureAndExit(
156
+ input: ErisFailureInput,
157
+ home: string = erisHome(),
158
+ sink: FailureSink = process.stderr,
159
+ exit: (code: number) => never = (code) => process.exit(code),
160
+ ): never {
161
+ sink.write(renderFailureBlock(input, home));
162
+ return exit(1);
163
+ }
164
+
165
+ /**
166
+ * The `WARN` line `eris yak` emits for its environment/config/
167
+ * transport outcomes: concise, on
168
+ * stderr, exit 0 — never the four-line failure block, which is reserved
169
+ * for agent misuse. Two lines, not a bare `WARN <reason>`: the slug alone
170
+ * is what makes an unset-token/unreachable-endpoint/timeout warning
171
+ * machine-legible on stderr without promoting it to the exit-1 failure
172
+ * contract, and `docs/ERRORS.md`'s per-slug headings apply.
173
+ */
174
+ export function renderWarnLines(reason: string, slug: string): string {
175
+ return `WARN ${reason}\nslug: ${slug}\n`;
176
+ }
177
+
178
+ export function emitWarn(reason: string, slug: string): void {
179
+ process.stderr.write(renderWarnLines(reason, slug));
180
+ }
package/ts/lib/pool.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * pool.ts — env-driven concurrency/deadline resolution, and the in-process
3
+ * bounded-concurrency runner.
4
+ *
5
+ * `ERIS_BATCH_CONCURRENCY` caps how many tasks run at once and
6
+ * `ERIS_BATCH_TIMEOUT` (seconds) bounds each one. An unset, non-numeric, zero
7
+ * or negative value CLAMPS to the default rather than disabling the bound:
8
+ * a typo in an env var must never turn a large batch into an unbounded
9
+ * fan-out.
10
+ */
11
+
12
+ /** Digits only — no sign, no decimal point. An empty, non-numeric, zero or
13
+ * negative value clamps to 8 rather than disabling the cap. */
14
+ export function resolveConcurrency(raw: string | undefined): number {
15
+ if (raw !== undefined && /^[0-9]+$/.test(raw)) {
16
+ const n = Number(raw);
17
+ if (n > 0) return n;
18
+ }
19
+ return 8;
20
+ }
21
+
22
+ /** Same integer-seconds grammar, converted to milliseconds for `AbortSignal`.
23
+ * Defaults to 60 seconds. Invalid
24
+ * input clamps to the default rather than producing NaN (which would mean
25
+ * "no deadline at all" — the opposite of the safety property a timeout
26
+ * exists to provide). */
27
+ export function resolveDeadlineMs(raw: string | undefined): number {
28
+ if (raw !== undefined && /^[0-9]+$/.test(raw)) {
29
+ const n = Number(raw);
30
+ if (n > 0) return n * 1000;
31
+ }
32
+ return 60_000;
33
+ }
34
+
35
+ /**
36
+ * Run `worker` over every item in `items`, at most `concurrency` in flight
37
+ * at once, preserving input order in the returned array regardless of
38
+ * completion order. A thrown worker error propagates (rejects the whole
39
+ * pool) rather than being swallowed — callers that want per-item
40
+ * ok/error/absent results encode that in R itself, so a THROW here always
41
+ * means a programming error, not a classified task failure.
42
+ */
43
+ export async function runPool<T, R>(
44
+ items: readonly T[],
45
+ concurrency: number,
46
+ worker: (item: T, index: number) => Promise<R>
47
+ ): Promise<R[]> {
48
+ const results: R[] = new Array(items.length);
49
+ if (items.length === 0) return results;
50
+
51
+ let next = 0;
52
+ async function runner(): Promise<void> {
53
+ while (true) {
54
+ const i = next++;
55
+ if (i >= items.length) return;
56
+ results[i] = await worker(items[i] as T, i);
57
+ }
58
+ }
59
+
60
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
61
+ await Promise.all(Array.from({ length: workerCount }, () => runner()));
62
+ return results;
63
+ }