@foldrun/core 0.2.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.
- package/LICENSE +202 -0
- package/README.md +239 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +43 -0
- package/dist/src/agent-tools.d.ts +31 -0
- package/dist/src/agent-tools.js +99 -0
- package/dist/src/agents-md.d.ts +9 -0
- package/dist/src/agents-md.js +83 -0
- package/dist/src/api-tools.d.ts +38 -0
- package/dist/src/api-tools.js +313 -0
- package/dist/src/approvals.d.ts +39 -0
- package/dist/src/approvals.js +103 -0
- package/dist/src/arrange.d.ts +8 -0
- package/dist/src/arrange.js +40 -0
- package/dist/src/catalog.d.ts +108 -0
- package/dist/src/catalog.js +255 -0
- package/dist/src/completions.d.ts +61 -0
- package/dist/src/completions.js +578 -0
- package/dist/src/confine.d.ts +31 -0
- package/dist/src/confine.js +298 -0
- package/dist/src/container.d.ts +30 -0
- package/dist/src/container.js +172 -0
- package/dist/src/context-tools.d.ts +81 -0
- package/dist/src/context-tools.js +347 -0
- package/dist/src/deploy.d.ts +100 -0
- package/dist/src/deploy.js +377 -0
- package/dist/src/egress.d.ts +75 -0
- package/dist/src/egress.js +133 -0
- package/dist/src/evals.d.ts +150 -0
- package/dist/src/evals.js +651 -0
- package/dist/src/flow-lint.d.ts +16 -0
- package/dist/src/flow-lint.js +177 -0
- package/dist/src/git-hook.d.ts +1 -0
- package/dist/src/git-hook.js +70 -0
- package/dist/src/git.d.ts +37 -0
- package/dist/src/git.js +101 -0
- package/dist/src/gitrepo.d.ts +95 -0
- package/dist/src/gitrepo.js +355 -0
- package/dist/src/history.d.ts +82 -0
- package/dist/src/history.js +259 -0
- package/dist/src/kinds.d.ts +95 -0
- package/dist/src/kinds.js +421 -0
- package/dist/src/library.d.ts +94 -0
- package/dist/src/library.js +341 -0
- package/dist/src/linkable.d.ts +20 -0
- package/dist/src/linkable.js +113 -0
- package/dist/src/model-probe.d.ts +21 -0
- package/dist/src/model-probe.js +81 -0
- package/dist/src/notify.d.ts +61 -0
- package/dist/src/notify.js +252 -0
- package/dist/src/oauth-presets.d.ts +13 -0
- package/dist/src/oauth-presets.js +39 -0
- package/dist/src/okf.d.ts +220 -0
- package/dist/src/okf.js +656 -0
- package/dist/src/openapi.d.ts +66 -0
- package/dist/src/openapi.js +342 -0
- package/dist/src/paths.d.ts +33 -0
- package/dist/src/paths.js +78 -0
- package/dist/src/platform.d.ts +32 -0
- package/dist/src/platform.js +48 -0
- package/dist/src/providers.d.ts +47 -0
- package/dist/src/providers.js +114 -0
- package/dist/src/refs.d.ts +20 -0
- package/dist/src/refs.js +78 -0
- package/dist/src/run-container.d.ts +227 -0
- package/dist/src/run-container.js +760 -0
- package/dist/src/runner.d.ts +241 -0
- package/dist/src/runner.js +3215 -0
- package/dist/src/runtime.d.ts +41 -0
- package/dist/src/runtime.js +357 -0
- package/dist/src/script-tools.d.ts +62 -0
- package/dist/src/script-tools.js +296 -0
- package/dist/src/secret-files.d.ts +12 -0
- package/dist/src/secret-files.js +92 -0
- package/dist/src/secrets.d.ts +147 -0
- package/dist/src/secrets.js +626 -0
- package/dist/src/starter.d.ts +21 -0
- package/dist/src/starter.js +399 -0
- package/dist/src/step-exec.d.ts +136 -0
- package/dist/src/step-exec.js +391 -0
- package/dist/src/storage.d.ts +123 -0
- package/dist/src/storage.js +640 -0
- package/dist/src/store.d.ts +1052 -0
- package/dist/src/store.js +2378 -0
- package/dist/src/tar.d.ts +30 -0
- package/dist/src/tar.js +111 -0
- package/dist/src/tool-names.d.ts +39 -0
- package/dist/src/tool-names.js +79 -0
- package/dist/src/tool-programs.d.ts +18 -0
- package/dist/src/tool-programs.js +52 -0
- package/dist/src/tool-test.d.ts +19 -0
- package/dist/src/tool-test.js +335 -0
- package/dist/src/translator.d.ts +159 -0
- package/dist/src/translator.js +986 -0
- package/dist/src/triggers.d.ts +60 -0
- package/dist/src/triggers.js +270 -0
- package/dist/src/webhook.d.ts +47 -0
- package/dist/src/webhook.js +148 -0
- package/index.ts +44 -0
- package/package.json +64 -0
- package/src/agent-tools.ts +138 -0
- package/src/agents-md.ts +81 -0
- package/src/api-tools.ts +389 -0
- package/src/approvals.ts +129 -0
- package/src/arrange.ts +42 -0
- package/src/catalog.ts +321 -0
- package/src/completions.ts +652 -0
- package/src/confine.ts +350 -0
- package/src/container.ts +220 -0
- package/src/context-tools.ts +435 -0
- package/src/deploy.ts +458 -0
- package/src/egress.ts +176 -0
- package/src/evals.ts +812 -0
- package/src/flow-lint.ts +215 -0
- package/src/git-hook.ts +73 -0
- package/src/git.ts +117 -0
- package/src/gitrepo.ts +391 -0
- package/src/history.ts +329 -0
- package/src/kinds.ts +507 -0
- package/src/library.ts +419 -0
- package/src/linkable.ts +123 -0
- package/src/model-probe.ts +112 -0
- package/src/notify.ts +281 -0
- package/src/oauth-presets.ts +54 -0
- package/src/okf.ts +768 -0
- package/src/openapi.ts +383 -0
- package/src/paths.ts +79 -0
- package/src/platform.ts +87 -0
- package/src/providers.ts +147 -0
- package/src/refs.ts +83 -0
- package/src/run-container.ts +883 -0
- package/src/runner.ts +3446 -0
- package/src/runtime.ts +404 -0
- package/src/script-tools.ts +383 -0
- package/src/secret-files.ts +101 -0
- package/src/secrets.ts +799 -0
- package/src/starter.ts +415 -0
- package/src/step-exec.ts +498 -0
- package/src/storage.ts +771 -0
- package/src/store.ts +2985 -0
- package/src/tar.ts +124 -0
- package/src/tool-names.ts +91 -0
- package/src/tool-programs.ts +65 -0
- package/src/tool-test.ts +401 -0
- package/src/translator.ts +1029 -0
- package/src/triggers.ts +287 -0
- package/src/webhook.ts +183 -0
- package/templates/blog-desk/AGENTS.md +13 -0
- package/templates/blog-desk/agents/researcher/agent.md +11 -0
- package/templates/blog-desk/agents/writer/agent.md +8 -0
- package/templates/blog-desk/evals/writer-quality.md +10 -0
- package/templates/blog-desk/flows/publish.md +10 -0
- package/templates/blog-desk/knowledge/house-style.md +9 -0
- package/templates/blog-desk/knowledge/index.md +9 -0
- package/templates/hello/AGENTS.md +18 -0
- package/templates/hello/agents/curator/agent.md +32 -0
- package/templates/hello/agents/notetaker/agent.md +24 -0
- package/templates/hello/evals/house-style.md +16 -0
- package/templates/hello/flows/memory-review.md +7 -0
- package/templates/hello/flows/note.md +7 -0
- package/templates/hello/knowledge/house-style.md +15 -0
- package/templates/hello/knowledge/index.md +9 -0
- package/templates/hello/skills/plain-english/SKILL.md +11 -0
- package/templates/hello/state/publishing.md +7 -0
- package/templates/hello/tools/wordcount/run.py +13 -0
- package/templates/hello/tools/wordcount/tool.md +22 -0
package/src/history.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// Every change to a workspace, kept.
|
|
2
|
+
//
|
|
3
|
+
// A workspace is a few hundred kilobytes of markdown, and until now the only
|
|
4
|
+
// record of a change was the file after it. A save from the dashboard, a
|
|
5
|
+
// deploy from git, an agent writing memory — each overwrote what was there
|
|
6
|
+
// and nothing remembered the before. Two things were impossible as a result:
|
|
7
|
+
// answering "what did this look like yesterday", and attributing a change in
|
|
8
|
+
// an eval score to the change in a file that caused it.
|
|
9
|
+
//
|
|
10
|
+
// So every write records a REVISION: what changed, from what, to what, by
|
|
11
|
+
// whom, and why. A deploy from git records one revision for the whole push
|
|
12
|
+
// with the commit as its id; a dashboard save records one for the file with
|
|
13
|
+
// a local id. Observe attributes eval scores to revision ids, so the "this
|
|
14
|
+
// change made it worse" table works for a workspace that has never seen a
|
|
15
|
+
// git repository — and when one is connected later, its commits slot into
|
|
16
|
+
// the same series.
|
|
17
|
+
//
|
|
18
|
+
// No git binary, no working copy, no repository on the server. The platform
|
|
19
|
+
// image deliberately has none (deploy.ts explains why); this is a journal of
|
|
20
|
+
// content, which is all the questions above need. Connecting a repository
|
|
21
|
+
// is a separate step and pushes THIS history out, not the other way round.
|
|
22
|
+
//
|
|
23
|
+
// Storage: <data>/<tenant>/.history/<scope>/<id>.json, one file per revision,
|
|
24
|
+
// plus index.jsonl — one line each, newest last — so listing does not open
|
|
25
|
+
// every revision. Content is stored inline: the files are small, and a
|
|
26
|
+
// content-addressed store would be machinery in search of a problem.
|
|
27
|
+
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import crypto from "node:crypto";
|
|
31
|
+
import { dataRoot } from "./paths.ts";
|
|
32
|
+
import {
|
|
33
|
+
commitChanges,
|
|
34
|
+
gitAvailable,
|
|
35
|
+
headSha,
|
|
36
|
+
listCommits,
|
|
37
|
+
readCommit,
|
|
38
|
+
repoExists,
|
|
39
|
+
} from "./gitrepo.ts";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The current tree of a scope, so the first commit can be a full import.
|
|
43
|
+
*
|
|
44
|
+
* Registered by the modules that know how to list a workspace or the library
|
|
45
|
+
* (store.ts, library.ts), because this module cannot import them — they
|
|
46
|
+
* import it. A revision recorded before any commit exists imports the whole
|
|
47
|
+
* tree rather than committing one file into an empty repository, so a clone
|
|
48
|
+
* made a minute later has everything.
|
|
49
|
+
*/
|
|
50
|
+
type TreeReader = (tenant: string, scope: string) => RevisionFile[] | null;
|
|
51
|
+
let readTree: TreeReader = () => null;
|
|
52
|
+
export function registerTreeReader(fn: TreeReader) {
|
|
53
|
+
const prev = readTree;
|
|
54
|
+
readTree = (tenant, scope) => fn(tenant, scope) ?? prev(tenant, scope);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Something to do after a commit lands on main — the mirror push. Registered
|
|
58
|
+
* by mirror.ts; this module cannot import it (secrets, settings). */
|
|
59
|
+
type AfterCommit = (tenant: string, scope: string, sha: string) => void;
|
|
60
|
+
const afterCommit: AfterCommit[] = [];
|
|
61
|
+
export function registerAfterCommit(fn: AfterCommit) {
|
|
62
|
+
afterCommit.push(fn);
|
|
63
|
+
}
|
|
64
|
+
export function notifyCommit(tenant: string, scope: string, sha: string) {
|
|
65
|
+
for (const fn of afterCommit) {
|
|
66
|
+
try {
|
|
67
|
+
fn(tenant, scope, sha);
|
|
68
|
+
} catch {
|
|
69
|
+
// a mirror that fails must not fail the save that triggered it
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Journal or git? Git whenever the binary exists; the journal is the
|
|
75
|
+
* fallback for an install without one. An install that started on the
|
|
76
|
+
* journal keeps it readable — both are consulted on read. */
|
|
77
|
+
const useGit = () => process.env.FOLDRUN_HISTORY !== "journal" && gitAvailable();
|
|
78
|
+
|
|
79
|
+
// Not store's assertSafeName: store imports this module, and a cycle between
|
|
80
|
+
// the two is avoidable for one regex.
|
|
81
|
+
function assertSafeName(v: string, what: string) {
|
|
82
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._@-]{0,120}$/.test(v)) throw new Error(`illegal ${what}: ${v}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RevisionFile {
|
|
86
|
+
path: string;
|
|
87
|
+
/** Null when the file did not exist before. */
|
|
88
|
+
before: string | null;
|
|
89
|
+
/** Null when the change deleted it. */
|
|
90
|
+
after: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface Revision {
|
|
94
|
+
id: string;
|
|
95
|
+
at: string;
|
|
96
|
+
/** Who: a signed-in email, "api-key", "agent:<name>", "deploy", "system". */
|
|
97
|
+
by: string;
|
|
98
|
+
message: string;
|
|
99
|
+
/** Set when this revision IS a git commit — a deploy from a push. */
|
|
100
|
+
commit: string | null;
|
|
101
|
+
files: RevisionFile[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The index line: everything but the content. */
|
|
105
|
+
export interface RevisionSummary {
|
|
106
|
+
id: string;
|
|
107
|
+
at: string;
|
|
108
|
+
by: string;
|
|
109
|
+
message: string;
|
|
110
|
+
commit: string | null;
|
|
111
|
+
paths: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Anything over this is not a document and is recorded by name only. */
|
|
115
|
+
const MAX_INLINE = 256 * 1024;
|
|
116
|
+
|
|
117
|
+
const historyDir = (tenant: string, scope: string) => {
|
|
118
|
+
assertSafeName(tenant, "tenant");
|
|
119
|
+
// "@library" for the account shelf; a workspace name otherwise.
|
|
120
|
+
if (scope !== "@library") assertSafeName(scope, "workspace");
|
|
121
|
+
return path.join(dataRoot(), tenant, ".history", scope);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** A local revision id: sortable by time, unique enough, visibly not a sha. */
|
|
125
|
+
export function newRevisionId(now = new Date()): string {
|
|
126
|
+
return `r-${now.getTime().toString(36)}-${crypto.randomBytes(3).toString("hex")}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Record one change set. Called by the writers, never by hand.
|
|
131
|
+
*
|
|
132
|
+
* Returns null and records nothing when nothing actually changed — a save
|
|
133
|
+
* of identical content is not a revision, and a history full of "no change"
|
|
134
|
+
* entries is a history nobody reads.
|
|
135
|
+
*/
|
|
136
|
+
export function recordRevision(
|
|
137
|
+
tenant: string,
|
|
138
|
+
scope: string,
|
|
139
|
+
files: RevisionFile[],
|
|
140
|
+
meta: { by?: string; message?: string; id?: string; commit?: string | null } = {},
|
|
141
|
+
): Revision | null {
|
|
142
|
+
const changed = files.filter((f) => f.before !== f.after);
|
|
143
|
+
if (changed.length === 0) return null;
|
|
144
|
+
|
|
145
|
+
if (useGit()) {
|
|
146
|
+
// First commit ever for this scope: import the whole tree as it stands
|
|
147
|
+
// now (the writer has already written), so the repository is complete
|
|
148
|
+
// from its first commit rather than starting with one file.
|
|
149
|
+
const first = !headSha(tenant, scope);
|
|
150
|
+
// Generated bundle files (knowledge/index.md, memory/log.md) are rewritten
|
|
151
|
+
// by the same save that changed a concept, outside this call. Commit them
|
|
152
|
+
// as they now stand, or a clone's index disagrees with its own files —
|
|
153
|
+
// the exact drift the template tests exist to catch.
|
|
154
|
+
const generated = first
|
|
155
|
+
? []
|
|
156
|
+
: (readTree(tenant, scope) ?? []).filter((f) => /(^|\/)(knowledge|memory)\/(.*\/)?(index|log)\.md$/.test(f.path));
|
|
157
|
+
const toCommit = first ? (readTree(tenant, scope) ?? changed) : [...changed, ...generated];
|
|
158
|
+
const sha = commitChanges(tenant, scope, toCommit, {
|
|
159
|
+
by: meta.by,
|
|
160
|
+
// Named after what the caller changed, not the generated files that
|
|
161
|
+
// rode along — "edited knowledge/style.md", not "changed 3 files".
|
|
162
|
+
message: first ? `initial import${meta.message ? ` — ${meta.message}` : ""}` : (meta.message ?? describe(changed)),
|
|
163
|
+
});
|
|
164
|
+
if (!sha) return null;
|
|
165
|
+
notifyCommit(tenant, scope, sha);
|
|
166
|
+
return {
|
|
167
|
+
id: meta.commit ?? sha,
|
|
168
|
+
at: new Date().toISOString(),
|
|
169
|
+
by: meta.by ?? "system",
|
|
170
|
+
message: meta.message ?? describe(changed),
|
|
171
|
+
commit: sha,
|
|
172
|
+
files: changed,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const dir = historyDir(tenant, scope);
|
|
177
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
178
|
+
const rev: Revision = {
|
|
179
|
+
id: meta.id ?? meta.commit ?? newRevisionId(),
|
|
180
|
+
at: new Date().toISOString(),
|
|
181
|
+
by: meta.by ?? "system",
|
|
182
|
+
message: meta.message ?? describe(changed),
|
|
183
|
+
commit: meta.commit ?? null,
|
|
184
|
+
files: changed.map((f) => ({
|
|
185
|
+
path: f.path,
|
|
186
|
+
before: clip(f.before),
|
|
187
|
+
after: clip(f.after),
|
|
188
|
+
})),
|
|
189
|
+
};
|
|
190
|
+
assertSafeName(rev.id, "revision id");
|
|
191
|
+
fs.writeFileSync(path.join(dir, `${rev.id}.json`), JSON.stringify(rev));
|
|
192
|
+
const line: RevisionSummary = {
|
|
193
|
+
id: rev.id,
|
|
194
|
+
at: rev.at,
|
|
195
|
+
by: rev.by,
|
|
196
|
+
message: rev.message,
|
|
197
|
+
commit: rev.commit,
|
|
198
|
+
paths: rev.files.map((f) => f.path),
|
|
199
|
+
};
|
|
200
|
+
fs.appendFileSync(path.join(dir, "index.jsonl"), `${JSON.stringify(line)}\n`);
|
|
201
|
+
return rev;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const clip = (s: string | null) =>
|
|
205
|
+
s === null ? null : s.length > MAX_INLINE ? `[${s.length} bytes — too large to keep inline]` : s;
|
|
206
|
+
|
|
207
|
+
/** "edited tools/x/run.py", "created 3 files", "deleted flows/old.md". */
|
|
208
|
+
function describe(files: RevisionFile[]): string {
|
|
209
|
+
if (files.length === 1) {
|
|
210
|
+
const f = files[0];
|
|
211
|
+
const verb = f.before === null ? "created" : f.after === null ? "deleted" : "edited";
|
|
212
|
+
return `${verb} ${f.path}`;
|
|
213
|
+
}
|
|
214
|
+
const created = files.filter((f) => f.before === null).length;
|
|
215
|
+
const deleted = files.filter((f) => f.after === null).length;
|
|
216
|
+
const edited = files.length - created - deleted;
|
|
217
|
+
return [edited && `edited ${edited}`, created && `created ${created}`, deleted && `deleted ${deleted}`]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join(", ") + ` file${files.length === 1 ? "" : "s"}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Newest first. `forPath` narrows to revisions that touched one file. */
|
|
223
|
+
export function listRevisions(
|
|
224
|
+
tenant: string,
|
|
225
|
+
scope: string,
|
|
226
|
+
opts: { forPath?: string; limit?: number } = {},
|
|
227
|
+
): RevisionSummary[] {
|
|
228
|
+
if (useGit() && repoExists(tenant, scope)) {
|
|
229
|
+
const fromGit = listCommits(tenant, scope, opts);
|
|
230
|
+
// Anything the journal holds from before the repository existed comes
|
|
231
|
+
// after git's rows: older, and still worth showing.
|
|
232
|
+
return [...fromGit, ...journalRevisions(tenant, scope, opts)].slice(0, opts.limit ?? 200);
|
|
233
|
+
}
|
|
234
|
+
return journalRevisions(tenant, scope, opts);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function journalRevisions(
|
|
238
|
+
tenant: string,
|
|
239
|
+
scope: string,
|
|
240
|
+
opts: { forPath?: string; limit?: number } = {},
|
|
241
|
+
): RevisionSummary[] {
|
|
242
|
+
const file = path.join(historyDir(tenant, scope), "index.jsonl");
|
|
243
|
+
if (!fs.existsSync(file)) return [];
|
|
244
|
+
const all = fs
|
|
245
|
+
.readFileSync(file, "utf8")
|
|
246
|
+
.split("\n")
|
|
247
|
+
.filter(Boolean)
|
|
248
|
+
.flatMap((l) => {
|
|
249
|
+
try {
|
|
250
|
+
return [JSON.parse(l) as RevisionSummary];
|
|
251
|
+
} catch {
|
|
252
|
+
return []; // a torn line from a crash mid-append loses one entry, not the file
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
.reverse();
|
|
256
|
+
const narrowed = opts.forPath ? all.filter((r) => r.paths.includes(opts.forPath!)) : all;
|
|
257
|
+
return narrowed.slice(0, opts.limit ?? 200);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function readRevision(tenant: string, scope: string, id: string, forPath?: string): Revision | null {
|
|
261
|
+
assertSafeName(id, "revision id");
|
|
262
|
+
if (useGit() && repoExists(tenant, scope) && /^[0-9a-f]{7,40}$/.test(id)) {
|
|
263
|
+
const fromGit = readCommit(tenant, scope, id, forPath);
|
|
264
|
+
if (fromGit) return fromGit;
|
|
265
|
+
}
|
|
266
|
+
const file = path.join(historyDir(tenant, scope), `${id}.json`);
|
|
267
|
+
if (!fs.existsSync(file)) return null;
|
|
268
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) as Revision;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Make sure a scope's repository has its initial import, so a clone made
|
|
273
|
+
* right now is complete. A no-op once any commit exists.
|
|
274
|
+
*/
|
|
275
|
+
export function ensureImported(tenant: string, scope: string): string | null {
|
|
276
|
+
if (!useGit()) return null;
|
|
277
|
+
if (headSha(tenant, scope)) return headSha(tenant, scope);
|
|
278
|
+
const tree = readTree(tenant, scope);
|
|
279
|
+
if (!tree || tree.length === 0) return null;
|
|
280
|
+
return commitChanges(tenant, scope, tree, { by: "foldrun", message: "initial import" });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The most recent revision id for a scope — what a change is attributed
|
|
284
|
+
* to when nothing more specific (a deployed commit) is on record. */
|
|
285
|
+
export function latestRevisionId(tenant: string, scope: string): string | null {
|
|
286
|
+
return listRevisions(tenant, scope, { limit: 1 })[0]?.id ?? null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ------------------------------------------------------------------- diff
|
|
290
|
+
|
|
291
|
+
export type DiffOp = { kind: "same" | "add" | "del"; text: string };
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* A line diff, for showing a revision. Plain LCS — quadratic, and fine: the
|
|
295
|
+
* files are documents, not generated code, and a diff of two 300-line files
|
|
296
|
+
* is ninety thousand cells, which is nothing.
|
|
297
|
+
*/
|
|
298
|
+
export function diffLines(before: string, after: string): DiffOp[] {
|
|
299
|
+
const a = before.split("\n");
|
|
300
|
+
const b = after.split("\n");
|
|
301
|
+
const n = a.length;
|
|
302
|
+
const m = b.length;
|
|
303
|
+
// lcs[i][j] = length of the LCS of a[i..] and b[j..]
|
|
304
|
+
const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0));
|
|
305
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
306
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
307
|
+
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const out: DiffOp[] = [];
|
|
311
|
+
let i = 0;
|
|
312
|
+
let j = 0;
|
|
313
|
+
while (i < n && j < m) {
|
|
314
|
+
if (a[i] === b[j]) {
|
|
315
|
+
out.push({ kind: "same", text: a[i] });
|
|
316
|
+
i++;
|
|
317
|
+
j++;
|
|
318
|
+
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
319
|
+
out.push({ kind: "del", text: a[i] });
|
|
320
|
+
i++;
|
|
321
|
+
} else {
|
|
322
|
+
out.push({ kind: "add", text: b[j] });
|
|
323
|
+
j++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
while (i < n) out.push({ kind: "del", text: a[i++] });
|
|
327
|
+
while (j < m) out.push({ kind: "add", text: b[j++] });
|
|
328
|
+
return out;
|
|
329
|
+
}
|