@treeseed/sdk 0.10.28 → 0.11.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/README.md +207 -6
- package/dist/capacity-provider.d.ts +3 -1
- package/dist/capacity-provider.js +25 -5
- package/dist/control-plane.d.ts +1 -0
- package/dist/control-plane.js +38 -13
- package/dist/db/market-schema.d.ts +8860 -6172
- package/dist/db/market-schema.js +108 -0
- package/dist/db/node-sqlite.js +7 -2
- package/dist/hosting/apps.d.ts +12 -0
- package/dist/hosting/apps.js +107 -0
- package/dist/hosting/builtins.d.ts +25 -0
- package/dist/hosting/builtins.js +791 -0
- package/dist/hosting/contracts.d.ts +207 -0
- package/dist/hosting/contracts.js +0 -0
- package/dist/hosting/graph.d.ts +192 -0
- package/dist/hosting/graph.js +1106 -0
- package/dist/hosting/index.d.ts +4 -0
- package/dist/hosting/index.js +4 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +63 -6
- package/dist/managed-dependencies.js +1 -2
- package/dist/market-client.d.ts +63 -3
- package/dist/market-client.js +83 -11
- package/dist/operations/services/bootstrap-runner.d.ts +3 -1
- package/dist/operations/services/bootstrap-runner.js +22 -2
- package/dist/operations/services/config-runtime.d.ts +10 -5
- package/dist/operations/services/config-runtime.js +209 -66
- package/dist/operations/services/deploy.d.ts +70 -7
- package/dist/operations/services/deploy.js +579 -64
- package/dist/operations/services/deployment-readiness.d.ts +30 -0
- package/dist/operations/services/deployment-readiness.js +175 -0
- package/dist/operations/services/git-workflow.d.ts +2 -1
- package/dist/operations/services/git-workflow.js +9 -3
- package/dist/operations/services/github-actions-verification.d.ts +1 -0
- package/dist/operations/services/github-actions-verification.js +1 -0
- package/dist/operations/services/github-api.js +1 -1
- package/dist/operations/services/github-automation.d.ts +1 -1
- package/dist/operations/services/github-automation.js +4 -3
- package/dist/operations/services/github-credentials.d.ts +13 -0
- package/dist/operations/services/github-credentials.js +58 -0
- package/dist/operations/services/hosted-service-checks.d.ts +63 -0
- package/dist/operations/services/hosted-service-checks.js +327 -0
- package/dist/operations/services/hub-provider-launch.js +3 -3
- package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
- package/dist/operations/services/live-hosted-service-checks.js +350 -0
- package/dist/operations/services/managed-host-security.js +1 -1
- package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
- package/dist/operations/services/operations-runner-smoke.js +180 -0
- package/dist/operations/services/package-adapters.d.ts +95 -0
- package/dist/operations/services/package-adapters.js +288 -0
- package/dist/operations/services/package-reference-policy.d.ts +1 -0
- package/dist/operations/services/package-reference-policy.js +15 -2
- package/dist/operations/services/project-platform.d.ts +80 -22
- package/dist/operations/services/project-platform.js +49 -8
- package/dist/operations/services/project-web-monitor.js +26 -4
- package/dist/operations/services/railway-api.d.ts +88 -5
- package/dist/operations/services/railway-api.js +626 -35
- package/dist/operations/services/railway-deploy.d.ts +46 -40
- package/dist/operations/services/railway-deploy.js +261 -293
- package/dist/operations/services/release-candidate.d.ts +19 -0
- package/dist/operations/services/release-candidate.js +375 -38
- package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
- package/dist/operations/services/repository-save-orchestrator.js +279 -66
- package/dist/operations/services/runtime-tools.d.ts +1 -0
- package/dist/operations/services/runtime-tools.js +10 -9
- package/dist/operations/services/verification-cache.d.ts +25 -0
- package/dist/operations/services/verification-cache.js +71 -0
- package/dist/operations/services/workspace-dependency-mode.js +9 -1
- package/dist/operations/services/workspace-save.js +1 -1
- package/dist/operations/services/workspace-tools.js +2 -1
- package/dist/platform/contracts.d.ts +32 -1
- package/dist/platform/deploy-config.js +73 -8
- package/dist/platform/env.yaml +163 -35
- package/dist/platform/environment.d.ts +1 -0
- package/dist/platform/environment.js +74 -5
- package/dist/platform/plugin.d.ts +9 -0
- package/dist/platform-operation-store.js +2 -2
- package/dist/platform-operations.js +1 -1
- package/dist/reconcile/bootstrap-systems.js +2 -2
- package/dist/reconcile/builtin-adapters.js +372 -189
- package/dist/reconcile/contracts.d.ts +9 -5
- package/dist/reconcile/desired-state.d.ts +1 -0
- package/dist/reconcile/desired-state.js +5 -5
- package/dist/reconcile/engine.d.ts +5 -2
- package/dist/reconcile/engine.js +53 -32
- package/dist/reconcile/index.d.ts +2 -0
- package/dist/reconcile/index.js +2 -0
- package/dist/reconcile/live-acceptance.d.ts +79 -0
- package/dist/reconcile/live-acceptance.js +1615 -0
- package/dist/reconcile/platform.d.ts +104 -0
- package/dist/reconcile/platform.js +100 -0
- package/dist/reconcile/state.js +4 -4
- package/dist/reconcile/units.js +2 -2
- package/dist/scripts/deployment-readiness.js +20 -0
- package/dist/scripts/generate-treedx-openapi-types.js +186 -0
- package/dist/scripts/operations-runner-smoke.js +16 -0
- package/dist/scripts/release-verify.js +4 -1
- package/dist/scripts/tenant-workflow-action.js +10 -1
- package/dist/sdk-types.d.ts +169 -4
- package/dist/sdk-types.js +20 -2
- package/dist/sdk.d.ts +35 -24
- package/dist/sdk.js +186 -17
- package/dist/template-launch-requirements.js +9 -0
- package/dist/treedx/adapters.d.ts +6 -0
- package/dist/treedx/adapters.js +36 -0
- package/dist/treedx/client.d.ts +222 -0
- package/dist/treedx/client.js +871 -0
- package/dist/treedx/errors.d.ts +13 -0
- package/dist/treedx/errors.js +17 -0
- package/dist/treedx/federated-client.d.ts +27 -0
- package/dist/treedx/federated-client.js +158 -0
- package/dist/treedx/generated/openapi-types.d.ts +3558 -0
- package/dist/treedx/generated/openapi-types.js +0 -0
- package/dist/treedx/graph-adapter.d.ts +33 -0
- package/dist/treedx/graph-adapter.js +156 -0
- package/dist/treedx/index.d.ts +14 -0
- package/dist/treedx/index.js +48 -0
- package/dist/treedx/market-integration.d.ts +27 -0
- package/dist/treedx/market-integration.js +131 -0
- package/dist/treedx/ports.d.ts +166 -0
- package/dist/treedx/ports.js +231 -0
- package/dist/treedx/query-adapter.d.ts +19 -0
- package/dist/treedx/query-adapter.js +62 -0
- package/dist/treedx/registry-client.d.ts +11 -0
- package/dist/treedx/registry-client.js +19 -0
- package/dist/treedx/repository-adapter.d.ts +45 -0
- package/dist/treedx/repository-adapter.js +308 -0
- package/dist/treedx/sdk-integration.d.ts +27 -0
- package/dist/treedx/sdk-integration.js +63 -0
- package/dist/treedx/types.d.ts +1084 -0
- package/dist/treedx/types.js +8 -0
- package/dist/treedx/workspace-adapter.d.ts +27 -0
- package/dist/treedx/workspace-adapter.js +65 -0
- package/dist/treedx-backends.d.ts +218 -0
- package/dist/treedx-backends.js +632 -0
- package/dist/treedx-client.d.ts +86 -0
- package/dist/treedx-client.js +175 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +23 -23
- package/dist/workflow/operations.d.ts +119 -13
- package/dist/workflow/operations.js +309 -53
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.js +43 -26
- package/dist/workflow-support.d.ts +11 -3
- package/dist/workflow-support.js +67 -3
- package/dist/workflow.d.ts +5 -0
- package/drizzle/market/0004_treedx_market_integration.sql +99 -0
- package/package.json +34 -3
- package/templates/github/deploy-web.workflow.yml +39 -6
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { TreeDxApiError, TreeDxClient } from "./treedx-client.js";
|
|
3
|
+
import { parseFrontmatterDocument, serializeFrontmatterDocument } from "./frontmatter.js";
|
|
4
|
+
import { resolveModelDefinition } from "./model-registry.js";
|
|
5
|
+
import { applyFilters, applySort } from "./sdk-filters.js";
|
|
6
|
+
import {
|
|
7
|
+
canonicalizeFrontmatter,
|
|
8
|
+
normalizeFilterFields,
|
|
9
|
+
normalizeMutationData,
|
|
10
|
+
normalizeRecordToCanonicalShape,
|
|
11
|
+
normalizeSortFields,
|
|
12
|
+
readCanonicalFieldValue
|
|
13
|
+
} from "./sdk-fields.js";
|
|
14
|
+
import { assertExpectedVersion } from "./sdk-version.js";
|
|
15
|
+
class TreeDxContentRepositoryConfigError extends Error {
|
|
16
|
+
constructor(message = 'TreeDX content repository is not configured. Set TREESEED_TREEDX_BASE_URL or pass treeDx.baseUrl, or select contentRepository.adapter = "local".') {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "TreeDxContentRepositoryConfigError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function compactObject(input) {
|
|
22
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
|
|
23
|
+
}
|
|
24
|
+
function stringValue(value) {
|
|
25
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
26
|
+
}
|
|
27
|
+
function arrayFromPayload(value) {
|
|
28
|
+
if (Array.isArray(value)) return value;
|
|
29
|
+
if (value && typeof value === "object") {
|
|
30
|
+
const record = value;
|
|
31
|
+
for (const key of ["items", "repositories", "repos", "data"]) {
|
|
32
|
+
if (Array.isArray(record[key])) return record[key];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
function repoIdFromRepository(repository) {
|
|
38
|
+
return stringValue(repository.id) ?? stringValue(repository.repoId) ?? stringValue(repository.repo_id) ?? stringValue(repository.name) ?? stringValue(repository.slug);
|
|
39
|
+
}
|
|
40
|
+
function repositoryMatchesHint(repository, hint) {
|
|
41
|
+
const name = stringValue(repository.name) ?? stringValue(repository.slug);
|
|
42
|
+
const owner = stringValue(repository.owner) ?? stringValue(repository.metadata?.owner);
|
|
43
|
+
const remoteUrl = stringValue(repository.remoteUrl) ?? stringValue(repository.remote_url) ?? stringValue(repository.cloneUrl) ?? stringValue(repository.clone_url) ?? stringValue(repository.url);
|
|
44
|
+
const defaultBranch = stringValue(repository.defaultBranch) ?? stringValue(repository.default_branch) ?? stringValue(repository.branch);
|
|
45
|
+
const metadata = repository.metadata && typeof repository.metadata === "object" ? repository.metadata : {};
|
|
46
|
+
if (hint.purpose && metadata.purpose !== hint.purpose && repository.purpose !== hint.purpose) return false;
|
|
47
|
+
if (hint.name && name !== hint.name) return false;
|
|
48
|
+
if (hint.owner && owner !== hint.owner) return false;
|
|
49
|
+
if (hint.remoteUrl && remoteUrl !== hint.remoteUrl) return false;
|
|
50
|
+
if (hint.defaultBranch && defaultBranch !== hint.defaultBranch) return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function normalizePathPattern(pattern) {
|
|
54
|
+
return pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
55
|
+
}
|
|
56
|
+
function segment(value) {
|
|
57
|
+
return encodeURIComponent(value);
|
|
58
|
+
}
|
|
59
|
+
function isMarkdownPath(value) {
|
|
60
|
+
return /\.(md|mdx)$/i.test(value);
|
|
61
|
+
}
|
|
62
|
+
function inferSlug(filePath, pathPattern) {
|
|
63
|
+
const normalizedPath = normalizePathPattern(filePath);
|
|
64
|
+
const root = normalizePathPattern(pathPattern).replace(/\*\*.*$/u, "").replace(/\/?$/u, "/");
|
|
65
|
+
const withoutRoot = normalizedPath.startsWith(root) ? normalizedPath.slice(root.length) : normalizedPath;
|
|
66
|
+
return withoutRoot.replace(/\.(md|mdx)$/i, "");
|
|
67
|
+
}
|
|
68
|
+
function normalizePathList(value) {
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return value.flatMap((entry) => {
|
|
71
|
+
if (typeof entry === "string") return [entry];
|
|
72
|
+
if (entry && typeof entry === "object") {
|
|
73
|
+
const record = entry;
|
|
74
|
+
const candidate = stringValue(record.path) ?? stringValue(record.file) ?? stringValue(record.key);
|
|
75
|
+
return candidate ? [candidate] : [];
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (value && typeof value === "object") {
|
|
81
|
+
const record = value;
|
|
82
|
+
for (const key of ["paths", "items", "files", "results", "data"]) {
|
|
83
|
+
const normalized = normalizePathList(record[key]);
|
|
84
|
+
if (normalized.length > 0) return normalized;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
function extractTextPayload(value) {
|
|
90
|
+
if (typeof value === "string") return value;
|
|
91
|
+
if (value && typeof value === "object") {
|
|
92
|
+
const record = value;
|
|
93
|
+
for (const key of ["content", "body", "text", "source", "data"]) {
|
|
94
|
+
const candidate = record[key];
|
|
95
|
+
if (typeof candidate === "string") return candidate;
|
|
96
|
+
if (candidate && typeof candidate === "object") {
|
|
97
|
+
const nested = extractTextPayload(candidate);
|
|
98
|
+
if (nested) return nested;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
function normalizeFilesPayload(value) {
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return value.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
107
|
+
}
|
|
108
|
+
if (value && typeof value === "object") {
|
|
109
|
+
const record = value;
|
|
110
|
+
for (const key of ["results", "files", "items", "data"]) {
|
|
111
|
+
const normalized = normalizeFilesPayload(record[key]);
|
|
112
|
+
if (normalized.length > 0) return normalized;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
function entryFromTreeDxPayload(definition, file, contentRoot) {
|
|
118
|
+
const filePath = stringValue(file.path) ?? stringValue(file.file) ?? stringValue(file.key) ?? "";
|
|
119
|
+
const parsed = file.frontmatter && typeof file.frontmatter === "object" ? {
|
|
120
|
+
frontmatter: file.frontmatter,
|
|
121
|
+
body: typeof file.body === "string" ? file.body : extractTextPayload(file)
|
|
122
|
+
} : parseFrontmatterDocument(extractTextPayload(file));
|
|
123
|
+
const slug = inferSlug(filePath, contentRoot);
|
|
124
|
+
return {
|
|
125
|
+
id: slug,
|
|
126
|
+
slug,
|
|
127
|
+
model: definition.name,
|
|
128
|
+
title: titleForEntry(definition, parsed.frontmatter),
|
|
129
|
+
path: filePath,
|
|
130
|
+
body: parsed.body,
|
|
131
|
+
frontmatter: parsed.frontmatter,
|
|
132
|
+
createdAt: dateForEntry(definition, parsed.frontmatter, "created_at"),
|
|
133
|
+
updatedAt: dateForEntry(definition, parsed.frontmatter, "updated_at")
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function entryMatchesIdentity(entry, request) {
|
|
137
|
+
return [request.id, request.slug, request.key].filter(Boolean).includes(entry.id) || [request.id, request.slug, request.key].filter(Boolean).includes(entry.slug);
|
|
138
|
+
}
|
|
139
|
+
function sanitizeFrontmatterInput(data) {
|
|
140
|
+
const next = { ...data };
|
|
141
|
+
delete next.body;
|
|
142
|
+
delete next.branchPrefix;
|
|
143
|
+
return next;
|
|
144
|
+
}
|
|
145
|
+
function ensureMutationAllowed(definition, operation) {
|
|
146
|
+
if (!definition.operations.includes(operation)) {
|
|
147
|
+
throw new Error(`Model "${definition.name}" does not allow ${operation}.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function titleForEntry(definition, frontmatter) {
|
|
151
|
+
const titleField = definition.fields.title ? readCanonicalFieldValue(definition, { frontmatter }, "title") : void 0;
|
|
152
|
+
const nameField = definition.fields.name ? readCanonicalFieldValue(definition, { frontmatter }, "name") : void 0;
|
|
153
|
+
return typeof titleField === "string" ? String(titleField) : typeof nameField === "string" ? String(nameField) : void 0;
|
|
154
|
+
}
|
|
155
|
+
function dateForEntry(definition, frontmatter, field) {
|
|
156
|
+
const value = definition.fields[field] ? readCanonicalFieldValue(definition, { frontmatter }, field) : void 0;
|
|
157
|
+
return typeof value === "string" ? String(value) : null;
|
|
158
|
+
}
|
|
159
|
+
function relativeContentDir(repoRoot, definition) {
|
|
160
|
+
if (!definition.contentDir) return void 0;
|
|
161
|
+
return normalizePathPattern(path.relative(repoRoot, definition.contentDir));
|
|
162
|
+
}
|
|
163
|
+
function makeDefaultPathRule(repoRoot, definition) {
|
|
164
|
+
const relative = relativeContentDir(repoRoot, definition) ?? `src/content/${definition.contentCollection ?? definition.name}`;
|
|
165
|
+
return { paths: [`${relative.replace(/\/$/u, "")}/**`] };
|
|
166
|
+
}
|
|
167
|
+
function normalizePathRule(input, fallback) {
|
|
168
|
+
if (!input) return fallback;
|
|
169
|
+
if (typeof input === "string") return { paths: [input] };
|
|
170
|
+
return input;
|
|
171
|
+
}
|
|
172
|
+
function mutationPath(definition, repoRoot, slug) {
|
|
173
|
+
const relative = relativeContentDir(repoRoot, definition) ?? `src/content/${definition.contentCollection ?? definition.name}`;
|
|
174
|
+
const extension = definition.name === "knowledge" ? ".md" : ".mdx";
|
|
175
|
+
return `${relative.replace(/\/$/u, "")}/${slug}${extension}`;
|
|
176
|
+
}
|
|
177
|
+
function resolveTreeDxOptions(input) {
|
|
178
|
+
const baseUrl = stringValue(input?.baseUrl) ?? stringValue(process.env.TREESEED_TREEDX_BASE_URL);
|
|
179
|
+
if (!baseUrl) return null;
|
|
180
|
+
return {
|
|
181
|
+
baseUrl,
|
|
182
|
+
token: stringValue(input?.token) ?? stringValue(process.env.TREESEED_TREEDX_TOKEN),
|
|
183
|
+
repoId: stringValue(input?.repoId),
|
|
184
|
+
ref: stringValue(input?.ref) ?? stringValue(process.env.TREESEED_TREEDX_REF),
|
|
185
|
+
workspaceId: stringValue(input?.workspaceId) ?? stringValue(process.env.TREESEED_TREEDX_WORKSPACE_ID),
|
|
186
|
+
contentPathMap: input?.contentPathMap,
|
|
187
|
+
repositoryHints: input?.repositoryHints ?? [],
|
|
188
|
+
fetchImpl: input?.fetchImpl
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function createTreeDxClientFromAgentOptions(options) {
|
|
192
|
+
const config = compactObject({
|
|
193
|
+
baseUrl: options.baseUrl,
|
|
194
|
+
token: options.token,
|
|
195
|
+
fetchImpl: options.fetchImpl
|
|
196
|
+
});
|
|
197
|
+
return new TreeDxClient(config);
|
|
198
|
+
}
|
|
199
|
+
class TreeDxPortfolioResolver {
|
|
200
|
+
constructor(options) {
|
|
201
|
+
this.options = options;
|
|
202
|
+
}
|
|
203
|
+
options;
|
|
204
|
+
repositoryCache = null;
|
|
205
|
+
candidateCache = /* @__PURE__ */ new Map();
|
|
206
|
+
async listRepositories() {
|
|
207
|
+
this.repositoryCache ??= this.options.client.repositories.list().then(
|
|
208
|
+
(response) => arrayFromPayload(response).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)))
|
|
209
|
+
);
|
|
210
|
+
return this.repositoryCache;
|
|
211
|
+
}
|
|
212
|
+
async resolveCandidates(rule) {
|
|
213
|
+
if (this.options.repoId) {
|
|
214
|
+
return rule.paths.map((path2) => ({
|
|
215
|
+
repoId: this.options.repoId,
|
|
216
|
+
ref: this.options.ref,
|
|
217
|
+
path: path2,
|
|
218
|
+
repository: { repoId: this.options.repoId }
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
const cacheKey = JSON.stringify({
|
|
222
|
+
paths: rule.paths,
|
|
223
|
+
hints: [...this.options.repositoryHints ?? [], ...rule.repositoryHints ?? []],
|
|
224
|
+
ref: this.options.ref
|
|
225
|
+
});
|
|
226
|
+
const cached = this.candidateCache.get(cacheKey);
|
|
227
|
+
if (cached) return cached;
|
|
228
|
+
const promise = this.resolveCandidatesUncached(rule);
|
|
229
|
+
this.candidateCache.set(cacheKey, promise);
|
|
230
|
+
return promise;
|
|
231
|
+
}
|
|
232
|
+
async resolveCandidatesUncached(rule) {
|
|
233
|
+
const repositories = await this.listRepositories();
|
|
234
|
+
const hints = [...this.options.repositoryHints ?? [], ...rule.repositoryHints ?? []].filter((hint) => hint.purpose !== "site_code" && hint.purpose !== "optional_project");
|
|
235
|
+
const matching = hints.length > 0 ? repositories.filter((repository) => hints.some((hint) => repositoryMatchesHint(repository, hint))) : repositories.filter((repository) => {
|
|
236
|
+
const metadata = repository.metadata && typeof repository.metadata === "object" ? repository.metadata : {};
|
|
237
|
+
return repository.purpose !== "site_code" && repository.purpose !== "optional_project" && metadata.purpose !== "site_code" && metadata.purpose !== "optional_project";
|
|
238
|
+
});
|
|
239
|
+
return matching.flatMap((repository) => {
|
|
240
|
+
const repoId = repoIdFromRepository(repository);
|
|
241
|
+
if (!repoId) return [];
|
|
242
|
+
return rule.paths.map((rulePath) => ({
|
|
243
|
+
repoId,
|
|
244
|
+
ref: this.options.ref,
|
|
245
|
+
path: normalizePathPattern(rulePath),
|
|
246
|
+
repository
|
|
247
|
+
}));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
class LocalContentBackend {
|
|
252
|
+
constructor(store) {
|
|
253
|
+
this.store = store;
|
|
254
|
+
}
|
|
255
|
+
store;
|
|
256
|
+
list(model) {
|
|
257
|
+
return this.store.list(model);
|
|
258
|
+
}
|
|
259
|
+
get(request) {
|
|
260
|
+
return this.store.get(request);
|
|
261
|
+
}
|
|
262
|
+
search(request) {
|
|
263
|
+
return this.store.search(request);
|
|
264
|
+
}
|
|
265
|
+
follow(request) {
|
|
266
|
+
return this.store.follow(request);
|
|
267
|
+
}
|
|
268
|
+
pick(request) {
|
|
269
|
+
return this.store.pick(request);
|
|
270
|
+
}
|
|
271
|
+
create(request) {
|
|
272
|
+
return this.store.create(request);
|
|
273
|
+
}
|
|
274
|
+
update(request) {
|
|
275
|
+
return this.store.update(request);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
class MissingTreeDxContentBackend {
|
|
279
|
+
fail() {
|
|
280
|
+
throw new TreeDxContentRepositoryConfigError();
|
|
281
|
+
}
|
|
282
|
+
list() {
|
|
283
|
+
this.fail();
|
|
284
|
+
}
|
|
285
|
+
get() {
|
|
286
|
+
this.fail();
|
|
287
|
+
}
|
|
288
|
+
search() {
|
|
289
|
+
this.fail();
|
|
290
|
+
}
|
|
291
|
+
follow() {
|
|
292
|
+
this.fail();
|
|
293
|
+
}
|
|
294
|
+
pick() {
|
|
295
|
+
this.fail();
|
|
296
|
+
}
|
|
297
|
+
create() {
|
|
298
|
+
this.fail();
|
|
299
|
+
}
|
|
300
|
+
update() {
|
|
301
|
+
this.fail();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
class TreeDxContentBackend {
|
|
305
|
+
constructor(options) {
|
|
306
|
+
this.options = options;
|
|
307
|
+
}
|
|
308
|
+
options;
|
|
309
|
+
definition(model) {
|
|
310
|
+
const definition = resolveModelDefinition(model, this.options.models);
|
|
311
|
+
if (definition.storage !== "content") {
|
|
312
|
+
throw new Error(`Model "${model}" is not content-backed.`);
|
|
313
|
+
}
|
|
314
|
+
return definition;
|
|
315
|
+
}
|
|
316
|
+
pathRule(definition) {
|
|
317
|
+
const configured = this.options.contentPathMap?.[definition.name];
|
|
318
|
+
return normalizePathRule(configured, makeDefaultPathRule(this.options.repoRoot, definition));
|
|
319
|
+
}
|
|
320
|
+
async readEntry(definition, candidate, filePath) {
|
|
321
|
+
const payload = await this.options.client.query.readFile(candidate.repoId, compactObject({
|
|
322
|
+
ref: candidate.ref,
|
|
323
|
+
path: filePath
|
|
324
|
+
}));
|
|
325
|
+
const parsed = parseFrontmatterDocument(extractTextPayload(payload));
|
|
326
|
+
const payloadRecord = payload && typeof payload === "object" ? payload : {};
|
|
327
|
+
const payloadFile = payloadRecord.file && typeof payloadRecord.file === "object" ? payloadRecord.file : {};
|
|
328
|
+
const resolvedPath = stringValue(payloadFile.path) ?? stringValue(payloadRecord.path) ?? filePath;
|
|
329
|
+
const slug = inferSlug(resolvedPath, candidate.path);
|
|
330
|
+
return {
|
|
331
|
+
id: slug,
|
|
332
|
+
slug,
|
|
333
|
+
model: definition.name,
|
|
334
|
+
title: titleForEntry(definition, parsed.frontmatter),
|
|
335
|
+
path: resolvedPath,
|
|
336
|
+
body: parsed.body,
|
|
337
|
+
frontmatter: parsed.frontmatter,
|
|
338
|
+
createdAt: dateForEntry(definition, parsed.frontmatter, "created_at"),
|
|
339
|
+
updatedAt: dateForEntry(definition, parsed.frontmatter, "updated_at")
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async list(model) {
|
|
343
|
+
const definition = this.definition(model);
|
|
344
|
+
const rule = this.pathRule(definition);
|
|
345
|
+
const candidates = await this.options.resolver.resolveCandidates(rule);
|
|
346
|
+
const entries = (await Promise.all(candidates.map(async (candidate) => {
|
|
347
|
+
const listPayload = await this.options.client.query.listPaths(candidate.repoId, compactObject({
|
|
348
|
+
ref: candidate.ref,
|
|
349
|
+
path: candidate.path
|
|
350
|
+
}));
|
|
351
|
+
const paths = normalizePathList(listPayload).filter(isMarkdownPath);
|
|
352
|
+
return await Promise.all(paths.map((filePath) => this.readEntry(definition, candidate, filePath)));
|
|
353
|
+
}))).flat();
|
|
354
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
const existing = deduped.get(entry.id);
|
|
357
|
+
if (!existing || new Date(entry.updatedAt ?? 0).valueOf() >= new Date(existing.updatedAt ?? 0).valueOf()) {
|
|
358
|
+
deduped.set(entry.id, entry);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return [...deduped.values()];
|
|
362
|
+
}
|
|
363
|
+
async get(request) {
|
|
364
|
+
if (this.options.directRepoId) {
|
|
365
|
+
const definition = this.definition(request.model);
|
|
366
|
+
const rule = this.pathRule(definition);
|
|
367
|
+
const identities = [request.id, request.slug, request.key].filter((value) => Boolean(value));
|
|
368
|
+
for (const identity of identities) {
|
|
369
|
+
for (const basePath of rule.paths) {
|
|
370
|
+
for (const extension of [".md", ".mdx"]) {
|
|
371
|
+
try {
|
|
372
|
+
return await this.readEntry(definition, {
|
|
373
|
+
repoId: this.options.directRepoId,
|
|
374
|
+
ref: this.options.ref,
|
|
375
|
+
path: basePath,
|
|
376
|
+
repository: { repoId: this.options.directRepoId }
|
|
377
|
+
}, `${basePath.replace(/\/$/u, "")}/${identity}${extension}`);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const entries = await this.list(request.model);
|
|
386
|
+
return entries.find((entry) => entryMatchesIdentity(entry, request)) ?? null;
|
|
387
|
+
}
|
|
388
|
+
async search(request) {
|
|
389
|
+
const definition = this.definition(request.model);
|
|
390
|
+
if (this.options.directRepoId) {
|
|
391
|
+
const rule = this.pathRule(definition);
|
|
392
|
+
const payload = await this.options.client.query.searchFiles(this.options.directRepoId, compactObject({
|
|
393
|
+
ref: this.options.ref,
|
|
394
|
+
paths: rule.paths.map((path2) => `${path2.replace(/\/$/u, "")}/**`),
|
|
395
|
+
query: "",
|
|
396
|
+
limit: request.limit,
|
|
397
|
+
includeBody: true,
|
|
398
|
+
includeFrontmatter: true
|
|
399
|
+
}));
|
|
400
|
+
const results = normalizeFilesPayload(payload);
|
|
401
|
+
return results.map((file) => entryFromTreeDxPayload(definition, file, rule.paths[0] ?? ""));
|
|
402
|
+
}
|
|
403
|
+
const items = await this.list(definition.name);
|
|
404
|
+
const filtered = applyFilters(items, normalizeFilterFields(definition, request.filters), definition);
|
|
405
|
+
const sorted = applySort(filtered, normalizeSortFields(definition, request.sort), definition);
|
|
406
|
+
return sorted.slice(0, request.limit ?? sorted.length);
|
|
407
|
+
}
|
|
408
|
+
async follow(request) {
|
|
409
|
+
const items = await this.search({
|
|
410
|
+
model: request.model,
|
|
411
|
+
filters: [
|
|
412
|
+
...request.filters ?? [],
|
|
413
|
+
{ field: "updatedAt", op: "updated_since", value: request.since }
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
return { items, since: request.since };
|
|
417
|
+
}
|
|
418
|
+
pick(request) {
|
|
419
|
+
return this.options.localLeaseStore.pick(request);
|
|
420
|
+
}
|
|
421
|
+
async resolveWritePath(definition, slug) {
|
|
422
|
+
if (this.options.workspaceId) return mutationPath(definition, this.options.repoRoot, slug);
|
|
423
|
+
const candidates = await this.options.resolver.resolveCandidates(this.pathRule(definition));
|
|
424
|
+
const repositoryIds = new Set(candidates.map((candidate) => candidate.repoId));
|
|
425
|
+
if (repositoryIds.size === 0) {
|
|
426
|
+
throw new Error(`No TreeDX repository candidate found for ${definition.name} content.`);
|
|
427
|
+
}
|
|
428
|
+
if (repositoryIds.size > 1) {
|
|
429
|
+
throw new Error(`Ambiguous TreeDX repository candidates for ${definition.name} content. Configure repositoryHints or provide a workspaceId.`);
|
|
430
|
+
}
|
|
431
|
+
return mutationPath(definition, this.options.repoRoot, slug);
|
|
432
|
+
}
|
|
433
|
+
requireWorkspaceId() {
|
|
434
|
+
if (!this.options.workspaceId) {
|
|
435
|
+
throw new Error("TreeDX content writes require treeDx.workspaceId in Phase 9.");
|
|
436
|
+
}
|
|
437
|
+
return this.options.workspaceId;
|
|
438
|
+
}
|
|
439
|
+
async createDirectWorkspace() {
|
|
440
|
+
if (!this.options.directRepoId) return null;
|
|
441
|
+
const response = await this.options.client.transport.request({
|
|
442
|
+
method: "POST",
|
|
443
|
+
path: `/api/v1/repos/${segment(this.options.directRepoId)}/workspaces`,
|
|
444
|
+
body: { mode: "writable" }
|
|
445
|
+
});
|
|
446
|
+
const payload = response.data;
|
|
447
|
+
return stringValue(payload.workspaceId) ?? stringValue(payload.id) ?? null;
|
|
448
|
+
}
|
|
449
|
+
async commitDirectWorkspace(workspaceId, filePath) {
|
|
450
|
+
await this.options.client.transport.request({
|
|
451
|
+
method: "POST",
|
|
452
|
+
path: `/api/v1/workspaces/${segment(workspaceId)}/commit`,
|
|
453
|
+
body: {
|
|
454
|
+
message: `Update ${filePath}`,
|
|
455
|
+
paths: [filePath]
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
async create(request) {
|
|
460
|
+
const definition = this.definition(request.model);
|
|
461
|
+
ensureMutationAllowed(definition, "create");
|
|
462
|
+
const slug = String(request.data.slug ?? request.data.id ?? cryptoRandomId());
|
|
463
|
+
const body = typeof request.data.body === "string" ? request.data.body : "";
|
|
464
|
+
const mutationData = normalizeMutationData(definition, sanitizeFrontmatterInput(request.data));
|
|
465
|
+
const frontmatter = canonicalizeFrontmatter(definition, {}, {
|
|
466
|
+
...mutationData,
|
|
467
|
+
slug,
|
|
468
|
+
updated_at: mutationData.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
469
|
+
});
|
|
470
|
+
const filePath = await this.resolveWritePath(definition, slug);
|
|
471
|
+
const workspaceId = this.options.workspaceId ?? await this.createDirectWorkspace() ?? this.requireWorkspaceId();
|
|
472
|
+
await this.options.client.files.write(workspaceId, {
|
|
473
|
+
path: filePath,
|
|
474
|
+
content: serializeFrontmatterDocument(frontmatter, body)
|
|
475
|
+
});
|
|
476
|
+
if (!this.options.workspaceId && this.options.directRepoId) {
|
|
477
|
+
await this.commitDirectWorkspace(workspaceId, filePath);
|
|
478
|
+
return {
|
|
479
|
+
item: await this.readEntry(definition, {
|
|
480
|
+
repoId: this.options.directRepoId,
|
|
481
|
+
ref: this.options.ref,
|
|
482
|
+
path: this.pathRule(definition).paths[0] ?? "",
|
|
483
|
+
repository: { repoId: this.options.directRepoId }
|
|
484
|
+
}, filePath),
|
|
485
|
+
git: null
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
item: {
|
|
490
|
+
id: slug,
|
|
491
|
+
slug,
|
|
492
|
+
model: definition.name,
|
|
493
|
+
title: titleForEntry(definition, frontmatter),
|
|
494
|
+
path: filePath,
|
|
495
|
+
body,
|
|
496
|
+
frontmatter,
|
|
497
|
+
createdAt: dateForEntry(definition, frontmatter, "created_at"),
|
|
498
|
+
updatedAt: dateForEntry(definition, frontmatter, "updated_at")
|
|
499
|
+
},
|
|
500
|
+
git: null
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async update(request) {
|
|
504
|
+
const definition = this.definition(request.model);
|
|
505
|
+
ensureMutationAllowed(definition, "update");
|
|
506
|
+
const workspaceId = this.requireWorkspaceId();
|
|
507
|
+
const existing = await this.get(request);
|
|
508
|
+
if (!existing) {
|
|
509
|
+
throw new Error(`No ${request.model} entry found for update.`);
|
|
510
|
+
}
|
|
511
|
+
assertExpectedVersion(request.expectedVersion, existing, `${definition.name} "${existing.slug}"`);
|
|
512
|
+
const mutationData = normalizeMutationData(definition, sanitizeFrontmatterInput(request.data));
|
|
513
|
+
const nextFrontmatter = canonicalizeFrontmatter(
|
|
514
|
+
definition,
|
|
515
|
+
normalizeRecordToCanonicalShape(definition, existing.frontmatter),
|
|
516
|
+
{
|
|
517
|
+
...mutationData,
|
|
518
|
+
updated_at: mutationData.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
const nextBody = typeof request.data.body === "string" ? request.data.body : existing.body;
|
|
522
|
+
await this.options.client.files.patch(workspaceId, {
|
|
523
|
+
path: existing.path,
|
|
524
|
+
content: serializeFrontmatterDocument(nextFrontmatter, nextBody)
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
item: {
|
|
528
|
+
...existing,
|
|
529
|
+
title: titleForEntry(definition, nextFrontmatter),
|
|
530
|
+
body: nextBody,
|
|
531
|
+
frontmatter: nextFrontmatter,
|
|
532
|
+
updatedAt: dateForEntry(definition, nextFrontmatter, "updated_at")
|
|
533
|
+
},
|
|
534
|
+
git: null
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function cryptoRandomId() {
|
|
539
|
+
return globalThis.crypto?.randomUUID?.() ?? `treedx-${Date.now()}`;
|
|
540
|
+
}
|
|
541
|
+
class LocalGraphBackend {
|
|
542
|
+
constructor(runtime) {
|
|
543
|
+
this.runtime = runtime;
|
|
544
|
+
}
|
|
545
|
+
runtime;
|
|
546
|
+
refresh(request) {
|
|
547
|
+
return this.runtime.refresh(request);
|
|
548
|
+
}
|
|
549
|
+
queryGraph(request) {
|
|
550
|
+
return this.runtime.queryGraph(request);
|
|
551
|
+
}
|
|
552
|
+
buildContextPack(request) {
|
|
553
|
+
return this.runtime.buildContextPack(request);
|
|
554
|
+
}
|
|
555
|
+
parseGraphDsl(source) {
|
|
556
|
+
return this.runtime.parseGraphDsl(source);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
class TreeDxGraphBackend {
|
|
560
|
+
constructor(options) {
|
|
561
|
+
this.options = options;
|
|
562
|
+
}
|
|
563
|
+
options;
|
|
564
|
+
async refresh(request) {
|
|
565
|
+
const candidates = await this.options.resolver.resolveCandidates({
|
|
566
|
+
paths: request?.paths?.length ? request.paths : ["**"]
|
|
567
|
+
});
|
|
568
|
+
return Promise.all(candidates.map(
|
|
569
|
+
(candidate) => this.options.client.graph.refresh(candidate.repoId, compactObject({
|
|
570
|
+
ref: candidate.ref ?? this.options.ref,
|
|
571
|
+
paths: request?.paths
|
|
572
|
+
}))
|
|
573
|
+
));
|
|
574
|
+
}
|
|
575
|
+
async repoIds() {
|
|
576
|
+
const repositories = await this.options.resolver.listRepositories();
|
|
577
|
+
return repositories.flatMap((repository) => {
|
|
578
|
+
const repoId = repoIdFromRepository(repository);
|
|
579
|
+
return repoId ? [repoId] : [];
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async queryGraph(request) {
|
|
583
|
+
return this.options.client.federation.graphQuery({
|
|
584
|
+
...request,
|
|
585
|
+
repoIds: await this.repoIds(),
|
|
586
|
+
ref: this.options.ref
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
async buildContextPack(request) {
|
|
590
|
+
return this.options.client.federation.contextBuild({
|
|
591
|
+
...request,
|
|
592
|
+
repoIds: await this.repoIds(),
|
|
593
|
+
ref: this.options.ref
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
parseGraphDsl(source) {
|
|
597
|
+
return this.options.localRuntime.parseGraphDsl(source);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
class LocalExecBackend {
|
|
601
|
+
async run(input) {
|
|
602
|
+
return input;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
class TreeDxExecBackend {
|
|
606
|
+
constructor(client, workspaceId) {
|
|
607
|
+
this.client = client;
|
|
608
|
+
this.workspaceId = workspaceId;
|
|
609
|
+
}
|
|
610
|
+
client;
|
|
611
|
+
workspaceId;
|
|
612
|
+
run(input) {
|
|
613
|
+
if (!this.workspaceId) {
|
|
614
|
+
throw new Error("TreeDX exec requires treeDx.workspaceId.");
|
|
615
|
+
}
|
|
616
|
+
return this.client.exec.run(this.workspaceId, input);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
export {
|
|
620
|
+
LocalContentBackend,
|
|
621
|
+
LocalExecBackend,
|
|
622
|
+
LocalGraphBackend,
|
|
623
|
+
MissingTreeDxContentBackend,
|
|
624
|
+
TreeDxApiError,
|
|
625
|
+
TreeDxContentBackend,
|
|
626
|
+
TreeDxContentRepositoryConfigError,
|
|
627
|
+
TreeDxExecBackend,
|
|
628
|
+
TreeDxGraphBackend,
|
|
629
|
+
TreeDxPortfolioResolver,
|
|
630
|
+
createTreeDxClientFromAgentOptions,
|
|
631
|
+
resolveTreeDxOptions
|
|
632
|
+
};
|