@foldrun/core 1.4.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/index.d.ts +0 -3
  2. package/dist/index.js +0 -3
  3. package/dist/src/catalog.d.ts +7 -0
  4. package/dist/src/catalog.js +16 -1
  5. package/dist/src/deploy.d.ts +6 -27
  6. package/dist/src/deploy.js +14 -56
  7. package/dist/src/history.d.ts +30 -10
  8. package/dist/src/history.js +232 -99
  9. package/dist/src/library.d.ts +0 -13
  10. package/dist/src/library.js +0 -50
  11. package/dist/src/model-credential.js +1 -1
  12. package/dist/src/model-env.js +1 -1
  13. package/dist/src/platform.d.ts +1 -3
  14. package/dist/src/platform.js +0 -1
  15. package/dist/src/run-container.d.ts +31 -5
  16. package/dist/src/run-container.js +71 -3
  17. package/dist/src/runner.d.ts +2 -0
  18. package/dist/src/runner.js +20 -12
  19. package/dist/src/secrets.js +1 -12
  20. package/dist/src/starter.js +2 -2
  21. package/dist/src/step-exec.d.ts +23 -5
  22. package/dist/src/step-exec.js +30 -23
  23. package/dist/src/store.d.ts +61 -22
  24. package/dist/src/store.js +105 -25
  25. package/dist/src/tool-loop.d.ts +4 -1
  26. package/dist/src/tool-loop.js +2 -0
  27. package/dist/src/translator.d.ts +12 -0
  28. package/dist/src/translator.js +36 -13
  29. package/index.ts +0 -3
  30. package/package.json +1 -3
  31. package/src/catalog.ts +18 -2
  32. package/src/deploy.ts +13 -74
  33. package/src/history.ts +230 -108
  34. package/src/library.ts +0 -44
  35. package/src/model-credential.ts +1 -1
  36. package/src/model-env.ts +1 -1
  37. package/src/platform.ts +1 -4
  38. package/src/run-container.ts +78 -5
  39. package/src/runner.ts +22 -12
  40. package/src/secrets.ts +1 -11
  41. package/src/starter.ts +2 -2
  42. package/src/step-exec.ts +44 -27
  43. package/src/store.ts +145 -37
  44. package/src/tool-loop.ts +6 -1
  45. package/src/translator.ts +39 -15
  46. package/dist/src/git-hook.d.ts +0 -1
  47. package/dist/src/git-hook.js +0 -70
  48. package/dist/src/git.d.ts +0 -37
  49. package/dist/src/git.js +0 -101
  50. package/dist/src/gitrepo.d.ts +0 -113
  51. package/dist/src/gitrepo.js +0 -404
  52. package/dist/src/tar.d.ts +0 -30
  53. package/dist/src/tar.js +0 -112
  54. package/src/git-hook.ts +0 -73
  55. package/src/git.ts +0 -117
  56. package/src/gitrepo.ts +0 -439
  57. package/src/tar.ts +0 -125
package/src/translator.ts CHANGED
@@ -22,7 +22,9 @@
22
22
  // and their results, streaming — and drops what a Chat-Completions endpoint
23
23
  // has no word for (cache_control, top_k, server-side tools, thinking on
24
24
  // endpoints that lack reasoning_effort). Anything dropped is said once in
25
- // the run trace. The translation is pure functions over JSON so the tests
25
+ // the run trace. Those endpoints cache prompt prefixes on their own side;
26
+ // the cached share they report comes back as cache_read_input_tokens, so a
27
+ // step's record says how much of its context was re-read. The translation is pure functions over JSON so the tests
26
28
  // can drive every mapping without a network; the server around them is a
27
29
  // few dozen lines.
28
30
 
@@ -37,6 +39,34 @@ import { captureRefusalHeaders, isRefusalStatus, refusalLine } from "./refusal.t
37
39
 
38
40
  type Json = Record<string, unknown>;
39
41
 
42
+ /**
43
+ * An OpenAI-shaped usage object as Anthropic's: fresh input, cached input,
44
+ * output. Both OpenAI shapes count the cached part INSIDE the prompt total
45
+ * (Chat's prompt_tokens_details.cached_tokens, Responses'
46
+ * input_tokens_details.cached_tokens; DeepSeek says prompt_cache_hit_tokens),
47
+ * so it is taken out of input here or the step would count it twice.
48
+ */
49
+ export function anthropicUsage(u: Json | undefined, wire: TranslatorWire): { input: number; cached: number; output: number } {
50
+ if (!u) return { input: 0, cached: 0, output: 0 };
51
+ const total = Number((wire === "chat" ? u.prompt_tokens : u.input_tokens) ?? 0);
52
+ const details = (wire === "chat" ? u.prompt_tokens_details : u.input_tokens_details) as Json | undefined;
53
+ const raw = Number(details?.cached_tokens ?? u.prompt_cache_hit_tokens ?? 0);
54
+ const cached = Number.isFinite(raw) ? Math.max(0, Math.min(raw, total)) : 0;
55
+ return {
56
+ input: total - cached,
57
+ cached,
58
+ output: Number((wire === "chat" ? u.completion_tokens : u.output_tokens) ?? 0),
59
+ };
60
+ }
61
+
62
+ function usageBlock(u: { input: number; cached: number; output: number } | null): Json {
63
+ return {
64
+ input_tokens: u?.input ?? 0,
65
+ output_tokens: u?.output ?? 0,
66
+ ...(u && u.cached > 0 ? { cache_read_input_tokens: u.cached } : {}),
67
+ };
68
+ }
69
+
40
70
  export type TranslatorWire = "chat" | "responses";
41
71
 
42
72
  export interface TranslatorSpec {
@@ -493,10 +523,7 @@ export function fromChatCompletion(res: Json, requestedModel: string, stopSequen
493
523
  content,
494
524
  stop_reason: stop.reason,
495
525
  stop_sequence: stop.sequence,
496
- usage: {
497
- input_tokens: Number(usage.prompt_tokens ?? 0),
498
- output_tokens: Number(usage.completion_tokens ?? 0),
499
- },
526
+ usage: usageBlock(anthropicUsage(usage, "chat")),
500
527
  };
501
528
  }
502
529
 
@@ -515,7 +542,7 @@ export class StreamTranslator {
515
542
  private tools = new Map<number, { block: number; argsSeen: boolean }>();
516
543
  /** The last choice's stop fields: finish_reason and any matched stop. */
517
544
  private finish: Json = {};
518
- private usage: { input: number; output: number } | null = null;
545
+ private usage: { input: number; cached: number; output: number } | null = null;
519
546
  private id = `msg_${crypto.randomBytes(8).toString("hex")}`;
520
547
  private readonly model: string;
521
548
  private readonly stops: string[];
@@ -561,7 +588,7 @@ export class StreamTranslator {
561
588
  const out = this.start(chunk);
562
589
  const u = chunk.usage as Json | undefined;
563
590
  if (u && (u.prompt_tokens !== undefined || u.completion_tokens !== undefined)) {
564
- this.usage = { input: Number(u.prompt_tokens ?? 0), output: Number(u.completion_tokens ?? 0) };
591
+ this.usage = anthropicUsage(u, "chat");
565
592
  }
566
593
  const choice = ((chunk.choices as Json[] | undefined) ?? [])[0];
567
594
  if (!choice) return out;
@@ -650,7 +677,7 @@ export class StreamTranslator {
650
677
  this.event("message_delta", {
651
678
  type: "message_delta",
652
679
  delta: { stop_reason: stop.reason, stop_sequence: stop.sequence },
653
- usage: { input_tokens: this.usage?.input ?? 0, output_tokens: this.usage?.output ?? 0 },
680
+ usage: usageBlock(this.usage),
654
681
  }),
655
682
  );
656
683
  out.push(this.event("message_stop", { type: "message_stop" }));
@@ -691,10 +718,7 @@ export function fromResponses(res: Json, requestedModel: string): Json {
691
718
  content,
692
719
  stop_reason: stop,
693
720
  stop_sequence: null,
694
- usage: {
695
- input_tokens: Number(usage.input_tokens ?? 0),
696
- output_tokens: Number(usage.output_tokens ?? 0),
697
- },
721
+ usage: usageBlock(anthropicUsage(usage, "responses")),
698
722
  };
699
723
  }
700
724
 
@@ -710,7 +734,7 @@ export class ResponsesStreamTranslator {
710
734
  private nextIndex = 0;
711
735
  /** Responses output_index → Anthropic block index and kind. */
712
736
  private blocks = new Map<number, { block: number; kind: "text" | "tool"; argsSeen: boolean }>();
713
- private usage: { input: number; output: number } | null = null;
737
+ private usage: { input: number; cached: number; output: number } | null = null;
714
738
  private incomplete: string | null = null;
715
739
  private sawTool = false;
716
740
  private sawRefusal = false;
@@ -812,7 +836,7 @@ export class ResponsesStreamTranslator {
812
836
  if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
813
837
  const r = (ev.response as Json | undefined) ?? {};
814
838
  const u = r.usage as Json | undefined;
815
- if (u) this.usage = { input: Number(u.input_tokens ?? 0), output: Number(u.output_tokens ?? 0) };
839
+ if (u) this.usage = anthropicUsage(u, "responses");
816
840
  const reason = (r.incomplete_details as Json | undefined)?.reason;
817
841
  if (typeof reason === "string") this.incomplete = reason;
818
842
  return out;
@@ -834,7 +858,7 @@ export class ResponsesStreamTranslator {
834
858
  this.event("message_delta", {
835
859
  type: "message_delta",
836
860
  delta: { stop_reason: reason, stop_sequence: null },
837
- usage: { input_tokens: this.usage?.input ?? 0, output_tokens: this.usage?.output ?? 0 },
861
+ usage: usageBlock(this.usage),
838
862
  }),
839
863
  );
840
864
  out.push(this.event("message_stop", { type: "message_stop" }));
@@ -1 +0,0 @@
1
- export {};
@@ -1,70 +0,0 @@
1
- // The pre-receive check, run by git inside a push.
2
- //
3
- // git feeds "<old> <new> <ref>" lines on stdin. For a push to main, read the
4
- // tree at the new commit straight from the object store and run the deploy
5
- // checks on it. Any issue: print them and exit non-zero — the push is refused
6
- // and the person sees exactly what a dashboard deploy would have told them,
7
- // in their terminal, before anything changed.
8
- //
9
- // Runs as its own process under git's environment. GIT_DIR is set by git.
10
- import fs from "node:fs";
11
- import path from "node:path";
12
- import { spawnSync } from "node:child_process";
13
- import { deployIssues, runsInFlight } from "./deploy.js";
14
- const dir = process.env.GIT_DIR;
15
- if (!dir)
16
- process.exit(0);
17
- // `_library.git` is the account shelf, not a workspace: it holds tools,
18
- // skills and knowledge and has no agents by definition. The workspace rules
19
- // refused every push to it with "no agents". Its sync (syncLibraryFromTree)
20
- // validates what it accepts on receive; nothing here to add yet.
21
- // Inside a hook git sets GIT_DIR relative to the repo ("."), so resolve it
22
- // against the cwd before asking for its name.
23
- if (path.basename(path.resolve(process.cwd(), dir)) === "_library.git")
24
- process.exit(0);
25
- const input = fs.readFileSync(0, "utf8");
26
- const git = (args) => {
27
- const r = spawnSync("git", ["--git-dir", dir, ...args], { maxBuffer: 64 * 1024 * 1024 });
28
- return r.status === 0 ? r.stdout.toString() : null;
29
- };
30
- // The client is gone by the time the deploy runs, so this is the only moment
31
- // a person can be told their push will not take effect yet. A warning, not a
32
- // refusal: the commit is fine and the scheduler applies it when the runs end.
33
- const here = path.basename(path.resolve(process.cwd(), dir));
34
- if (here.endsWith(".git")) {
35
- const workspace = here.slice(0, -4);
36
- const tenant = path.basename(path.resolve(process.cwd(), dir, "..", ".."));
37
- try {
38
- const live = runsInFlight(tenant, workspace);
39
- if (live.length) {
40
- process.stderr.write(`\nfoldrun: ${live.length} run${live.length === 1 ? " is" : "s are"} in flight, so this push is not live yet.\n` +
41
- ` It is on main and will be applied automatically when they finish.\n\n`);
42
- }
43
- }
44
- catch {
45
- // A warning that cannot be computed must never fail a push.
46
- }
47
- }
48
- let refused = false;
49
- for (const line of input.split("\n").filter(Boolean)) {
50
- const [, newSha, ref] = line.split(" ");
51
- if (ref !== "refs/heads/main" || /^0+$/.test(newSha))
52
- continue;
53
- const tree = git(["ls-tree", "-r", "--name-only", newSha]) ?? "";
54
- const files = tree
55
- .split("\n")
56
- .filter(Boolean)
57
- .flatMap((p) => {
58
- const content = git(["show", `${newSha}:${p}`]);
59
- return content === null ? [] : [{ path: p, content }];
60
- });
61
- const issues = deployIssues(files);
62
- if (issues.length) {
63
- refused = true;
64
- process.stderr.write(`\nfoldrun: refusing this push — ${issues.length} problem${issues.length === 1 ? "" : "s"} the checks would have stopped at deploy:\n`);
65
- for (const i of issues)
66
- process.stderr.write(` ${i.where}: ${i.message}\n`);
67
- process.stderr.write(`\nFix and push again. (foldrun check runs the same rules locally.)\n\n`);
68
- }
69
- }
70
- process.exit(refused ? 1 : 0);
package/dist/src/git.d.ts DELETED
@@ -1,37 +0,0 @@
1
- /**
2
- * The secret a repository signs its pushes with.
3
- *
4
- * Derived rather than stored, exactly like a flow's webhook token: HMAC of the
5
- * workspace's identity under the install key. Nothing new to persist, unique
6
- * per workspace, and rotating FOLDRUN_SECRET_KEY invalidates every connection
7
- * at once.
8
- */
9
- export declare function gitSecret(tenant: string, workspace: string): string;
10
- export declare function gitHookPath(tenant: string, workspace: string): string;
11
- /**
12
- * Whether a request really came from the repository it claims to.
13
- *
14
- * Compared in constant time: a byte-by-byte early return leaks the expected
15
- * signature to anyone willing to time enough requests. The raw body is
16
- * required — re-serializing parsed JSON changes bytes GitHub signed.
17
- */
18
- export declare function verifySignature(secret: string, rawBody: string, header: string | null): boolean;
19
- export interface PushEvent {
20
- repo: string;
21
- /** Full ref, e.g. refs/heads/main. */
22
- ref: string;
23
- branch: string;
24
- commit: string;
25
- message: string | null;
26
- pusher: string | null;
27
- }
28
- /** What we need out of a GitHub push payload, or null if it isn't one. */
29
- export declare function parsePush(payload: unknown): PushEvent | null;
30
- /**
31
- * The repository at one commit, as a gzipped tarball.
32
- *
33
- * A token is only needed for a private repository. It is read from the
34
- * environment rather than taken as an argument so a caller cannot be tricked
35
- * into forwarding one somewhere else.
36
- */
37
- export declare function fetchTarball(repo: string, commit: string): Promise<Buffer>;
package/dist/src/git.js DELETED
@@ -1,101 +0,0 @@
1
- // git push → deployed.
2
- //
3
- // A workspace is markdown, so there is no build to run: a push is a fetch of
4
- // the tree at that commit, a check, and a swap. This module is the GitHub half
5
- // — the secret a repository signs with, the payload shape, and pulling the
6
- // tarball. `deploy.ts` decides whether the result is allowed to go live.
7
- //
8
- // The tarball, not a clone: one HTTP call, no git binary, no working copy kept
9
- // on the server. A markdown workspace is a few hundred kilobytes.
10
- import crypto from "node:crypto";
11
- import { installKey } from "./webhook.js";
12
- /** GitHub refuses to fetch anything larger, and neither should we. */
13
- const MAX_TARBALL = 25 * 1024 * 1024;
14
- /**
15
- * The secret a repository signs its pushes with.
16
- *
17
- * Derived rather than stored, exactly like a flow's webhook token: HMAC of the
18
- * workspace's identity under the install key. Nothing new to persist, unique
19
- * per workspace, and rotating FOLDRUN_SECRET_KEY invalidates every connection
20
- * at once.
21
- */
22
- export function gitSecret(tenant, workspace) {
23
- return crypto
24
- .createHmac("sha256", installKey())
25
- .update(`git/${tenant}/${workspace}`)
26
- .digest("hex");
27
- }
28
- export function gitHookPath(tenant, workspace) {
29
- return `/api/git/${tenant}/${workspace}`;
30
- }
31
- /**
32
- * Whether a request really came from the repository it claims to.
33
- *
34
- * Compared in constant time: a byte-by-byte early return leaks the expected
35
- * signature to anyone willing to time enough requests. The raw body is
36
- * required — re-serializing parsed JSON changes bytes GitHub signed.
37
- */
38
- export function verifySignature(secret, rawBody, header) {
39
- if (!header)
40
- return false;
41
- const expected = `sha256=${crypto.createHmac("sha256", secret).update(rawBody).digest("hex")}`;
42
- const a = Buffer.from(expected);
43
- const b = Buffer.from(header);
44
- return a.length === b.length && crypto.timingSafeEqual(a, b);
45
- }
46
- /** What we need out of a GitHub push payload, or null if it isn't one. */
47
- export function parsePush(payload) {
48
- if (!payload || typeof payload !== "object")
49
- return null;
50
- const p = payload;
51
- const repo = p.repository?.full_name;
52
- const ref = p.ref;
53
- const commit = p.after;
54
- if (typeof repo !== "string" || typeof ref !== "string" || typeof commit !== "string") {
55
- return null;
56
- }
57
- // A deleted branch reports all zeros and ships no tree.
58
- if (/^0+$/.test(commit))
59
- return null;
60
- return {
61
- repo,
62
- ref,
63
- branch: ref.replace(/^refs\/heads\//, ""),
64
- commit,
65
- message: typeof p.head_commit?.message === "string" ? p.head_commit.message : null,
66
- pusher: typeof p.pusher?.name === "string" ? p.pusher.name : null,
67
- };
68
- }
69
- /**
70
- * The repository at one commit, as a gzipped tarball.
71
- *
72
- * A token is only needed for a private repository. It is read from the
73
- * environment rather than taken as an argument so a caller cannot be tricked
74
- * into forwarding one somewhere else.
75
- */
76
- export async function fetchTarball(repo, commit) {
77
- if (!/^[\w.-]+\/[\w.-]+$/.test(repo))
78
- throw new Error(`refusing to fetch "${repo}"`);
79
- if (!/^[0-9a-f]{7,40}$/i.test(commit))
80
- throw new Error(`refusing to fetch commit "${commit}"`);
81
- const headers = {
82
- accept: "application/vnd.github+json",
83
- "user-agent": "foldrun",
84
- "x-github-api-version": "2022-11-28",
85
- };
86
- const token = process.env.GITHUB_TOKEN;
87
- if (token)
88
- headers.authorization = `Bearer ${token}`;
89
- const res = await fetch(`https://api.github.com/repos/${repo}/tarball/${commit}`, { headers });
90
- if (!res.ok) {
91
- throw new Error(`GitHub returned ${res.status} for ${repo}@${commit.slice(0, 8)}` +
92
- (res.status === 404 && !token
93
- ? " — a private repository needs GITHUB_TOKEN set on the server"
94
- : ""));
95
- }
96
- const buf = Buffer.from(await res.arrayBuffer());
97
- if (buf.length > MAX_TARBALL) {
98
- throw new Error(`${repo} is ${Math.round(buf.length / 1e6)}MB — too large to deploy`);
99
- }
100
- return buf;
101
- }
@@ -1,113 +0,0 @@
1
- import type { DiffOp, Revision, RevisionFile, RevisionSummary } from "./history.ts";
2
- /** Where a workspace's bare repository lives. `@library` is the account shelf. */
3
- export declare function repoDir(tenant: string, scope: string): string;
4
- /** Is there a git binary? Checked once; an install without one keeps the journal. */
5
- export declare function gitAvailable(): boolean;
6
- export declare function repoExists(tenant: string, scope: string): boolean;
7
- /** Create the repository if it is not there. Idempotent. */
8
- export declare function ensureRepo(tenant: string, scope: string): string;
9
- export declare function headSha(tenant: string, scope: string, ref?: string): string | null;
10
- /**
11
- * Apply a change set as one commit on main, using plumbing against a scratch
12
- * index seeded from HEAD. No working copy, no checkout: each changed path is
13
- * hashed straight from its content into the object store.
14
- *
15
- * Returns the new commit, or null when the tree did not change.
16
- */
17
- export declare function commitChanges(tenant: string, scope: string, files: RevisionFile[], meta?: {
18
- by?: string;
19
- message?: string;
20
- at?: string;
21
- }): string | null;
22
- /** Commits on main, newest first, as the same summary the journal gave. */
23
- export declare function listCommits(tenant: string, scope: string, opts?: {
24
- forPath?: string;
25
- limit?: number;
26
- ref?: string;
27
- }): RevisionSummary[];
28
- /** One commit in full: every changed file with its before and after. */
29
- export declare function readCommit(tenant: string, scope: string, sha: string, forPath?: string): Revision | null;
30
- export interface RefInfo {
31
- name: string;
32
- sha: string;
33
- /** Commit subject and time, for a branch list that says what each is. */
34
- subject: string;
35
- at: string;
36
- }
37
- export declare function listRefs(tenant: string, scope: string, kind: "heads" | "tags"): RefInfo[];
38
- /**
39
- * The commit a restore means by `to`, on main's history:
40
- *
41
- * a sha or a prefix of one that commit
42
- * a branch or tag name where it points
43
- * an ISO date or time the last commit on main at or before it
44
- * "3d", "12h", "90m", "2w" the same, that long ago
45
- *
46
- * Null when nothing matches — before the first commit, or a name that is
47
- * not there. Throws on a `to` that cannot be any of these, so a typo is a
48
- * 400 rather than a silent "nothing found".
49
- */
50
- export declare function resolveRestorePoint(tenant: string, scope: string, to: string, now?: number): {
51
- sha: string;
52
- at: string;
53
- by: string;
54
- message: string;
55
- } | null;
56
- /** The files at a commit, for a browser or a deploy. */
57
- export declare function listTree(tenant: string, scope: string, ref?: string): {
58
- path: string;
59
- mode: string;
60
- }[];
61
- export declare function readBlob(tenant: string, scope: string, ref: string, p: string): string | null;
62
- /** The whole tree at a ref as deploy files — what a push turns into a workspace. */
63
- export declare function filesAt(tenant: string, scope: string, ref: string): {
64
- path: string;
65
- content: string;
66
- }[];
67
- /**
68
- * What a ref would change on main: every file that differs, with both sides.
69
- * The branch's own commits are `listCommits({ ref })`; this is the net
70
- * effect, which is what a reviewer reads.
71
- */
72
- export declare function diffRefs(tenant: string, scope: string, base: string, head: string): RevisionFile[];
73
- /**
74
- * Merge a branch into main and return the new main. A fast-forward when it
75
- * is one; otherwise a merge commit whose tree is the BRANCH's — the branch
76
- * is what the person reviewed and chose, and a three-way textual merge of
77
- * markdown with no one to resolve conflicts would deploy text nobody wrote.
78
- * Both parents are recorded, so history shows the merge for what it was.
79
- */
80
- export declare function mergeBranch(tenant: string, scope: string, branch: string, meta?: {
81
- by?: string;
82
- }): {
83
- sha: string;
84
- fastForward: boolean;
85
- };
86
- export declare function deleteBranch(tenant: string, scope: string, branch: string): void;
87
- /** Push main to another remote — a GitHub or GitLab mirror. Blocking; callers
88
- * run it off the request path. The token never touches disk: it is in the
89
- * URL for this one process and nowhere else. */
90
- export declare function pushMirror(tenant: string, scope: string, url: string): {
91
- ok: boolean;
92
- detail: string;
93
- };
94
- /**
95
- * Where the compiled pre-receive check is, found on disk rather than through
96
- * require.resolve — a bundler rewrites require.resolve("pkg") into a module
97
- * id, and the hook was once told to run a script called "51113". Searched
98
- * from the working directory upward: the monorepo layout, then an installed
99
- * package. Empty when there is no compiled check (a dev server on the source
100
- * tree), in which case a push is checked at deploy instead of refused — the
101
- * same rules, one step later.
102
- */
103
- export declare function hookScriptPath(): string;
104
- /**
105
- * The pre-receive hook: a push that fails the deploy checks is refused with
106
- * the errors in the pusher's terminal, instead of accepted and then not
107
- * deployed. The hook shells out to node with the compiled check, so it runs
108
- * the same rules as a dashboard deploy; which node and which script are
109
- * passed in the environment by the HTTP route, because a hook inherits git's
110
- * environment and nothing else.
111
- */
112
- export declare function installHooks(dir: string): void;
113
- export { type DiffOp };