@treeseed/sdk 0.4.7 → 0.4.9
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 +1 -1
- package/dist/dispatch.d.ts +4 -0
- package/dist/dispatch.js +180 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +25 -3
- package/dist/operations/providers/default.js +1 -0
- package/dist/operations/services/config-runtime.d.ts +121 -26
- package/dist/operations/services/config-runtime.js +332 -198
- package/dist/operations/services/deploy.d.ts +0 -3
- package/dist/operations/services/deploy.js +1 -1
- package/dist/operations/services/export-runtime.d.ts +18 -0
- package/dist/operations/services/export-runtime.js +136 -0
- package/dist/operations/services/railway-deploy.js +2 -2
- package/dist/operations/services/runtime-tools.d.ts +0 -1
- package/dist/operations/services/runtime-tools.js +1 -2
- package/dist/operations-registry.js +1 -0
- package/dist/operations-types.d.ts +1 -1
- package/dist/platform/book-export.d.ts +78 -0
- package/dist/platform/book-export.js +449 -0
- package/dist/platform/contracts.d.ts +6 -2
- package/dist/platform/deploy-config.d.ts +2 -0
- package/dist/platform/deploy-config.js +30 -2
- package/dist/platform/env.yaml +5 -0
- package/dist/platform/environment.d.ts +10 -1
- package/dist/platform/environment.js +82 -6
- package/dist/remote.d.ts +65 -9
- package/dist/remote.js +104 -28
- package/dist/scripts/aggregate-book.js +13 -118
- package/dist/scripts/config-treeseed.js +18 -27
- package/dist/sdk-dispatch.d.ts +12 -0
- package/dist/sdk-dispatch.js +142 -0
- package/dist/sdk-types.d.ts +137 -4
- package/dist/sdk-types.js +16 -0
- package/dist/sdk.d.ts +7 -1
- package/dist/sdk.js +69 -0
- package/dist/workflow/operations.d.ts +59 -15
- package/dist/workflow/operations.js +61 -81
- package/dist/workflow-state.js +2 -1
- package/dist/workflow-support.d.ts +2 -1
- package/dist/workflow-support.js +14 -6
- package/dist/workflow.d.ts +11 -1
- package/dist/workflow.js +6 -0
- package/package.json +6 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import path, { dirname, extname, relative, resolve } from "node:path";
|
|
13
|
+
import { runDefaultAction, setLogLevel } from "repomix";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
15
|
+
import { buildTenantBookRuntime } from "./books-data.js";
|
|
16
|
+
import { loadTreeseedManifest } from "./tenant-config.js";
|
|
17
|
+
const BOOK_EXPORT_PACKAGE_VERSION = 1;
|
|
18
|
+
function sha1(value) {
|
|
19
|
+
return createHash("sha1").update(value).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
function sortPaths(paths) {
|
|
22
|
+
return [...paths].sort((left, right) => left.localeCompare(right, void 0, { numeric: true, sensitivity: "base" }));
|
|
23
|
+
}
|
|
24
|
+
function collectMarkdownFiles(rootPath) {
|
|
25
|
+
if (!existsSync(rootPath)) {
|
|
26
|
+
throw new Error(`Book export root not found: ${rootPath}`);
|
|
27
|
+
}
|
|
28
|
+
const stats = statSync(rootPath);
|
|
29
|
+
if (stats.isFile()) {
|
|
30
|
+
return [rootPath];
|
|
31
|
+
}
|
|
32
|
+
return sortPaths(
|
|
33
|
+
readdirSync(rootPath, { withFileTypes: true }).flatMap((entry) => {
|
|
34
|
+
const fullPath = path.join(rootPath, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
return collectMarkdownFiles(fullPath);
|
|
37
|
+
}
|
|
38
|
+
if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".mdx"))) {
|
|
39
|
+
return [fullPath];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function frontmatter(filePath) {
|
|
46
|
+
const raw = readFileSync(filePath, "utf8");
|
|
47
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
48
|
+
return match ? parseYaml(match[1]) : {};
|
|
49
|
+
}
|
|
50
|
+
function frontmatterOrder(filePath) {
|
|
51
|
+
const order = frontmatter(filePath).order;
|
|
52
|
+
return typeof order === "number" && Number.isFinite(order) ? order : null;
|
|
53
|
+
}
|
|
54
|
+
function contentTypeFor(filePath) {
|
|
55
|
+
const extension = extname(filePath).toLowerCase();
|
|
56
|
+
if (extension === ".md") return "md";
|
|
57
|
+
if (extension === ".mdx") return "mdx";
|
|
58
|
+
return "text";
|
|
59
|
+
}
|
|
60
|
+
function inferExportRoots(book, tenantConfig) {
|
|
61
|
+
const knowledgePrefix = "/knowledge/";
|
|
62
|
+
const normalizedBasePath = String(book.basePath || "").trim();
|
|
63
|
+
if (!normalizedBasePath.startsWith(knowledgePrefix)) {
|
|
64
|
+
throw new Error(`Book basePath must start with "${knowledgePrefix}" to infer exports: ${book.basePath}`);
|
|
65
|
+
}
|
|
66
|
+
const relativeKnowledgePath = normalizedBasePath.slice(knowledgePrefix.length).replace(/^\/+|\/+$/g, "");
|
|
67
|
+
if (!relativeKnowledgePath) {
|
|
68
|
+
throw new Error(`Book basePath must identify a knowledge directory: ${book.basePath}`);
|
|
69
|
+
}
|
|
70
|
+
return [path.join(tenantConfig.content.docs, relativeKnowledgePath)];
|
|
71
|
+
}
|
|
72
|
+
function resolveBookRoots(book, projectRoot, tenantConfig) {
|
|
73
|
+
const configuredRoots = Array.isArray(book.exportRoots) && book.exportRoots.length > 0 ? book.exportRoots.map((entry) => resolve(projectRoot, entry)) : inferExportRoots(book, tenantConfig).map((entry) => resolve(entry));
|
|
74
|
+
return configuredRoots;
|
|
75
|
+
}
|
|
76
|
+
function rootKeyFor(book, rootPath, projectRoot, index, total) {
|
|
77
|
+
if (total === 1) {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
const rel = relative(projectRoot, rootPath).replaceAll(path.sep, "/");
|
|
81
|
+
const cleaned = rel.replaceAll(/[^a-zA-Z0-9/_-]+/g, "-").replaceAll(/-+/g, "-").replace(/^\/+|\/+$/g, "");
|
|
82
|
+
return cleaned || `root-${String(index + 1).padStart(2, "0")}`;
|
|
83
|
+
}
|
|
84
|
+
function orderedBookFiles(book, tenantConfig, projectRoot) {
|
|
85
|
+
const resolvedRoots = resolveBookRoots(book, projectRoot, tenantConfig);
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
const files = resolvedRoots.flatMap(
|
|
88
|
+
(rootPath, rootIndex) => collectMarkdownFiles(rootPath).sort((left, right) => {
|
|
89
|
+
const orderDelta = (frontmatterOrder(left) ?? Number.POSITIVE_INFINITY) - (frontmatterOrder(right) ?? Number.POSITIVE_INFINITY);
|
|
90
|
+
if (orderDelta !== 0) {
|
|
91
|
+
return orderDelta;
|
|
92
|
+
}
|
|
93
|
+
return left.localeCompare(right, void 0, { numeric: true, sensitivity: "base" });
|
|
94
|
+
}).map((absolutePath) => {
|
|
95
|
+
if (seen.has(absolutePath)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
seen.add(absolutePath);
|
|
99
|
+
const rootRelativePath = relative(rootPath, absolutePath).replaceAll(path.sep, "/");
|
|
100
|
+
const rootKey = rootKeyFor(book, rootPath, projectRoot, rootIndex, resolvedRoots.length);
|
|
101
|
+
const bookRelativePath = rootKey ? `${rootKey}/${rootRelativePath}` : rootRelativePath;
|
|
102
|
+
return {
|
|
103
|
+
absolutePath,
|
|
104
|
+
rootPath,
|
|
105
|
+
rootRelativePath,
|
|
106
|
+
bookRelativePath
|
|
107
|
+
};
|
|
108
|
+
}).filter(Boolean)
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
resolvedRoots,
|
|
112
|
+
files: files.map((file, index) => {
|
|
113
|
+
const projectRelativePath = relative(projectRoot, file.absolutePath).replaceAll(path.sep, "/");
|
|
114
|
+
const markerId = `marker:${book.id}:${String(index + 1).padStart(4, "0")}`;
|
|
115
|
+
return {
|
|
116
|
+
fileId: sha1(`${book.id}:${projectRelativePath}`),
|
|
117
|
+
bookId: book.id ?? book.slug,
|
|
118
|
+
memberBookId: book.id ?? book.slug,
|
|
119
|
+
absolutePath: file.absolutePath,
|
|
120
|
+
projectRelativePath,
|
|
121
|
+
bookRelativePath: file.bookRelativePath,
|
|
122
|
+
rootRelativePath: file.rootRelativePath,
|
|
123
|
+
rootPath: file.rootPath,
|
|
124
|
+
ordinal: index + 1,
|
|
125
|
+
frontmatterOrder: frontmatterOrder(file.absolutePath),
|
|
126
|
+
sourceType: contentTypeFor(file.absolutePath),
|
|
127
|
+
chunkId: sha1(`chunk:${book.id}:${projectRelativePath}:${index + 1}`),
|
|
128
|
+
markerId
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function loadTenant(projectRoot = process.cwd()) {
|
|
134
|
+
const manifestPath = resolve(projectRoot, "src", "manifest.yaml");
|
|
135
|
+
const tenantConfig = loadTreeseedManifest(manifestPath);
|
|
136
|
+
const runtime = buildTenantBookRuntime(tenantConfig, { projectRoot });
|
|
137
|
+
return {
|
|
138
|
+
projectRoot,
|
|
139
|
+
tenantConfig,
|
|
140
|
+
runtime
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function findBookOrThrow(bookId, books) {
|
|
144
|
+
const book = books.find((entry) => entry.id === bookId || entry.slug === bookId);
|
|
145
|
+
if (!book) {
|
|
146
|
+
throw new Error(`Unknown book export target: ${bookId}`);
|
|
147
|
+
}
|
|
148
|
+
return book;
|
|
149
|
+
}
|
|
150
|
+
function buildBookManifestFromBook(book, tenantConfig, projectRoot) {
|
|
151
|
+
const ordered = orderedBookFiles(book, tenantConfig, projectRoot);
|
|
152
|
+
return {
|
|
153
|
+
packageKind: "book",
|
|
154
|
+
packageVersion: BOOK_EXPORT_PACKAGE_VERSION,
|
|
155
|
+
packageId: `book:${book.id}`,
|
|
156
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
157
|
+
tenantRoot: projectRoot,
|
|
158
|
+
tenantId: tenantConfig.id,
|
|
159
|
+
book: {
|
|
160
|
+
id: book.id ?? book.slug,
|
|
161
|
+
slug: book.slug,
|
|
162
|
+
title: book.title,
|
|
163
|
+
order: book.order,
|
|
164
|
+
basePath: book.basePath,
|
|
165
|
+
downloadFileName: book.downloadFileName,
|
|
166
|
+
downloadHref: book.downloadHref,
|
|
167
|
+
downloadTitle: book.downloadTitle,
|
|
168
|
+
resolvedRoots: ordered.resolvedRoots.map((entry) => relative(projectRoot, entry).replaceAll(path.sep, "/"))
|
|
169
|
+
},
|
|
170
|
+
files: ordered.files
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function buildBookExportManifest(bookId, options = {}) {
|
|
174
|
+
const { projectRoot, tenantConfig, runtime } = loadTenant(resolve(options.projectRoot ?? process.cwd()));
|
|
175
|
+
const book = findBookOrThrow(bookId, runtime.BOOKS);
|
|
176
|
+
return buildBookManifestFromBook(book, tenantConfig, projectRoot);
|
|
177
|
+
}
|
|
178
|
+
function stageManifest(manifest, stageRoot) {
|
|
179
|
+
rmSync(stageRoot, { recursive: true, force: true });
|
|
180
|
+
mkdirSync(resolve(stageRoot, "manifest"), { recursive: true });
|
|
181
|
+
mkdirSync(resolve(stageRoot, "content"), { recursive: true });
|
|
182
|
+
writeFileSync(resolve(stageRoot, "manifest", "book.json"), `${JSON.stringify({
|
|
183
|
+
packageKind: manifest.packageKind,
|
|
184
|
+
packageVersion: manifest.packageVersion,
|
|
185
|
+
packageId: manifest.packageId,
|
|
186
|
+
generatedAt: manifest.generatedAt,
|
|
187
|
+
book: manifest.book,
|
|
188
|
+
members: manifest.members ?? []
|
|
189
|
+
}, null, 2)}
|
|
190
|
+
`, "utf8");
|
|
191
|
+
writeFileSync(resolve(stageRoot, "manifest", "files.json"), `${JSON.stringify(manifest.files, null, 2)}
|
|
192
|
+
`, "utf8");
|
|
193
|
+
for (const file of manifest.files) {
|
|
194
|
+
const stagedPath = resolve(
|
|
195
|
+
stageRoot,
|
|
196
|
+
"content",
|
|
197
|
+
String(file.ordinal).padStart(4, "0"),
|
|
198
|
+
file.bookRelativePath
|
|
199
|
+
);
|
|
200
|
+
mkdirSync(dirname(stagedPath), { recursive: true });
|
|
201
|
+
copyFileSync(file.absolutePath, stagedPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function withCleanNodeExecArgv(action) {
|
|
205
|
+
const previousExecArgv = [...process.execArgv];
|
|
206
|
+
process.execArgv = previousExecArgv.filter(
|
|
207
|
+
(arg) => !arg.startsWith("--test") && !arg.startsWith("--input-type") && !arg.startsWith("--experimental-test") && !arg.startsWith("--watch")
|
|
208
|
+
);
|
|
209
|
+
try {
|
|
210
|
+
return await action();
|
|
211
|
+
} finally {
|
|
212
|
+
process.execArgv = previousExecArgv;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function renderRepomixMarkdown(stageRoot, outputPath) {
|
|
216
|
+
const options = {
|
|
217
|
+
output: outputPath,
|
|
218
|
+
style: "markdown",
|
|
219
|
+
ignore: "",
|
|
220
|
+
quiet: true,
|
|
221
|
+
skipLocalConfig: true,
|
|
222
|
+
copy: false,
|
|
223
|
+
stdout: false
|
|
224
|
+
};
|
|
225
|
+
setLogLevel(0);
|
|
226
|
+
const result = await withCleanNodeExecArgv(() => runDefaultAction(["."], stageRoot, options));
|
|
227
|
+
return {
|
|
228
|
+
markdown: readFileSync(outputPath, "utf8"),
|
|
229
|
+
packResult: result.packResult
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function normalizePackResultOutputFiles(packResult, outputPath) {
|
|
233
|
+
return packResult.outputFiles && packResult.outputFiles.length > 0 ? packResult.outputFiles : [outputPath];
|
|
234
|
+
}
|
|
235
|
+
function jsonFence(value) {
|
|
236
|
+
return ["```json", JSON.stringify(value, null, 2), "```"].join("\n");
|
|
237
|
+
}
|
|
238
|
+
function renderBookPackageMarkdown(manifest, repomixMarkdown) {
|
|
239
|
+
const header = {
|
|
240
|
+
packageKind: manifest.packageKind,
|
|
241
|
+
packageVersion: manifest.packageVersion,
|
|
242
|
+
packageId: manifest.packageId,
|
|
243
|
+
book: manifest.book,
|
|
244
|
+
sourceFileCount: manifest.files.length
|
|
245
|
+
};
|
|
246
|
+
const fileIndex = manifest.files.map((file) => ({
|
|
247
|
+
fileId: file.fileId,
|
|
248
|
+
ordinal: file.ordinal,
|
|
249
|
+
projectRelativePath: file.projectRelativePath,
|
|
250
|
+
bookRelativePath: file.bookRelativePath,
|
|
251
|
+
sourceType: file.sourceType,
|
|
252
|
+
chunkId: file.chunkId,
|
|
253
|
+
markerId: file.markerId
|
|
254
|
+
}));
|
|
255
|
+
return [
|
|
256
|
+
`# ${manifest.book.downloadTitle}`,
|
|
257
|
+
"",
|
|
258
|
+
"> Auto-generated Treeseed AI package. Treeseed owns the manifest and file ordering; Repomix owns the content serialization payload.",
|
|
259
|
+
"",
|
|
260
|
+
"<!-- TRESEED_PACKAGE_HEADER_BEGIN -->",
|
|
261
|
+
jsonFence(header),
|
|
262
|
+
"<!-- TRESEED_PACKAGE_HEADER_END -->",
|
|
263
|
+
"",
|
|
264
|
+
"<!-- TRESEED_PACKAGE_MANIFEST_BEGIN -->",
|
|
265
|
+
jsonFence(manifest),
|
|
266
|
+
"<!-- TRESEED_PACKAGE_MANIFEST_END -->",
|
|
267
|
+
"",
|
|
268
|
+
"<!-- TRESEED_PACKAGE_FILE_INDEX_BEGIN -->",
|
|
269
|
+
jsonFence(fileIndex),
|
|
270
|
+
"<!-- TRESEED_PACKAGE_FILE_INDEX_END -->",
|
|
271
|
+
"",
|
|
272
|
+
"<!-- TRESEED_PACKAGE_CONTENT_BEGIN -->",
|
|
273
|
+
repomixMarkdown.trim(),
|
|
274
|
+
"<!-- TRESEED_PACKAGE_CONTENT_END -->",
|
|
275
|
+
""
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
function buildBookIndexPayload(manifest, markdownPath) {
|
|
279
|
+
return {
|
|
280
|
+
packageKind: manifest.packageKind,
|
|
281
|
+
packageVersion: manifest.packageVersion,
|
|
282
|
+
packageId: manifest.packageId,
|
|
283
|
+
markdownPath,
|
|
284
|
+
book: manifest.book,
|
|
285
|
+
files: manifest.files.map((file) => ({
|
|
286
|
+
fileId: file.fileId,
|
|
287
|
+
memberBookId: file.memberBookId,
|
|
288
|
+
ordinal: file.ordinal,
|
|
289
|
+
projectRelativePath: file.projectRelativePath,
|
|
290
|
+
bookRelativePath: file.bookRelativePath,
|
|
291
|
+
rootRelativePath: file.rootRelativePath,
|
|
292
|
+
sourceType: file.sourceType,
|
|
293
|
+
chunkId: file.chunkId,
|
|
294
|
+
markerId: file.markerId,
|
|
295
|
+
relations: {
|
|
296
|
+
book: manifest.book.id,
|
|
297
|
+
nextFileId: manifest.files.find((candidate) => candidate.ordinal === file.ordinal + 1)?.fileId ?? null
|
|
298
|
+
}
|
|
299
|
+
}))
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function booksOutputRoot(projectRoot) {
|
|
303
|
+
return resolve(projectRoot, "public", "books");
|
|
304
|
+
}
|
|
305
|
+
function sidecarIndexPath(markdownPath) {
|
|
306
|
+
return markdownPath.replace(/\.md$/u, ".json");
|
|
307
|
+
}
|
|
308
|
+
async function exportManifestPackage(manifest, markdownPath, indexPath) {
|
|
309
|
+
const tempRoot = resolve(manifest.tenantRoot, ".treeseed", "tmp", "book-exports", manifest.packageId.replaceAll(":", "-"));
|
|
310
|
+
const repomixOutputPath = resolve(tempRoot, "..", `${manifest.packageId.replaceAll(":", "-")}.repomix.md`);
|
|
311
|
+
stageManifest(manifest, tempRoot);
|
|
312
|
+
const { markdown: repomixMarkdown, packResult } = await renderRepomixMarkdown(tempRoot, repomixOutputPath);
|
|
313
|
+
const wrappedMarkdown = renderBookPackageMarkdown(manifest, repomixMarkdown);
|
|
314
|
+
mkdirSync(dirname(markdownPath), { recursive: true });
|
|
315
|
+
writeFileSync(markdownPath, wrappedMarkdown, "utf8");
|
|
316
|
+
writeFileSync(indexPath, `${JSON.stringify({
|
|
317
|
+
...buildBookIndexPayload(manifest, markdownPath),
|
|
318
|
+
repomixSummary: {
|
|
319
|
+
totalFiles: packResult.totalFiles,
|
|
320
|
+
totalCharacters: packResult.totalCharacters,
|
|
321
|
+
totalTokens: packResult.totalTokens,
|
|
322
|
+
outputFiles: normalizePackResultOutputFiles(packResult, repomixOutputPath)
|
|
323
|
+
}
|
|
324
|
+
}, null, 2)}
|
|
325
|
+
`, "utf8");
|
|
326
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
327
|
+
rmSync(repomixOutputPath, { force: true });
|
|
328
|
+
}
|
|
329
|
+
async function exportBookPackage(bookId, options = {}) {
|
|
330
|
+
const manifest = buildBookExportManifest(bookId, options);
|
|
331
|
+
const markdownPath = resolve(booksOutputRoot(manifest.tenantRoot), manifest.book.downloadFileName);
|
|
332
|
+
const indexPath = sidecarIndexPath(markdownPath);
|
|
333
|
+
await exportManifestPackage(manifest, markdownPath, indexPath);
|
|
334
|
+
return {
|
|
335
|
+
manifest,
|
|
336
|
+
markdownPath,
|
|
337
|
+
indexPath,
|
|
338
|
+
sourceFileCount: manifest.files.length,
|
|
339
|
+
includedRoots: manifest.book.resolvedRoots
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function renderLibraryMarkdown(manifest, memberPackages) {
|
|
343
|
+
return [
|
|
344
|
+
`# ${manifest.book.downloadTitle}`,
|
|
345
|
+
"",
|
|
346
|
+
"> Auto-generated Treeseed AI library package. Each member section embeds a reconstructable per-book package.",
|
|
347
|
+
"",
|
|
348
|
+
"<!-- TRESEED_LIBRARY_HEADER_BEGIN -->",
|
|
349
|
+
jsonFence({
|
|
350
|
+
packageKind: manifest.packageKind,
|
|
351
|
+
packageVersion: manifest.packageVersion,
|
|
352
|
+
packageId: manifest.packageId,
|
|
353
|
+
book: manifest.book,
|
|
354
|
+
memberCount: memberPackages.length
|
|
355
|
+
}),
|
|
356
|
+
"<!-- TRESEED_LIBRARY_HEADER_END -->",
|
|
357
|
+
"",
|
|
358
|
+
"<!-- TRESEED_LIBRARY_MANIFEST_BEGIN -->",
|
|
359
|
+
jsonFence(manifest),
|
|
360
|
+
"<!-- TRESEED_LIBRARY_MANIFEST_END -->",
|
|
361
|
+
"",
|
|
362
|
+
...memberPackages.flatMap((member) => [
|
|
363
|
+
`<!-- TRESEED_AGGREGATE_MEMBER_BEGIN ${member.manifest.book.id} -->`,
|
|
364
|
+
readFileSync(member.markdownPath, "utf8").trim(),
|
|
365
|
+
`<!-- TRESEED_AGGREGATE_MEMBER_END ${member.manifest.book.id} -->`,
|
|
366
|
+
""
|
|
367
|
+
])
|
|
368
|
+
].join("\n");
|
|
369
|
+
}
|
|
370
|
+
async function exportBookLibrary(options = {}) {
|
|
371
|
+
const { projectRoot, tenantConfig, runtime } = loadTenant(resolve(options.projectRoot ?? process.cwd()));
|
|
372
|
+
const memberPackages = [];
|
|
373
|
+
for (const book of runtime.BOOKS) {
|
|
374
|
+
memberPackages.push(await exportBookPackage(book.id ?? book.slug, { projectRoot }));
|
|
375
|
+
}
|
|
376
|
+
const files = memberPackages.flatMap((entry) => entry.manifest.files);
|
|
377
|
+
const manifest = {
|
|
378
|
+
packageKind: "library",
|
|
379
|
+
packageVersion: BOOK_EXPORT_PACKAGE_VERSION,
|
|
380
|
+
packageId: "library:books",
|
|
381
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
382
|
+
tenantRoot: projectRoot,
|
|
383
|
+
tenantId: tenantConfig.id,
|
|
384
|
+
book: {
|
|
385
|
+
id: "library",
|
|
386
|
+
slug: "library",
|
|
387
|
+
title: runtime.TREESEED_LIBRARY_DOWNLOAD.downloadTitle,
|
|
388
|
+
order: 0,
|
|
389
|
+
basePath: "/books/",
|
|
390
|
+
downloadFileName: runtime.TREESEED_LIBRARY_DOWNLOAD.downloadFileName,
|
|
391
|
+
downloadHref: runtime.TREESEED_LIBRARY_DOWNLOAD.downloadHref,
|
|
392
|
+
downloadTitle: runtime.TREESEED_LIBRARY_DOWNLOAD.downloadTitle,
|
|
393
|
+
resolvedRoots: Array.from(new Set(memberPackages.flatMap((entry) => entry.includedRoots))).sort((left, right) => left.localeCompare(right))
|
|
394
|
+
},
|
|
395
|
+
files,
|
|
396
|
+
members: memberPackages.map((entry) => ({
|
|
397
|
+
bookId: entry.manifest.book.id,
|
|
398
|
+
slug: entry.manifest.book.slug,
|
|
399
|
+
title: entry.manifest.book.title,
|
|
400
|
+
order: entry.manifest.book.order,
|
|
401
|
+
downloadFileName: entry.manifest.book.downloadFileName,
|
|
402
|
+
downloadHref: entry.manifest.book.downloadHref,
|
|
403
|
+
sourceFileCount: entry.sourceFileCount,
|
|
404
|
+
markdownPath: entry.markdownPath,
|
|
405
|
+
indexPath: entry.indexPath
|
|
406
|
+
}))
|
|
407
|
+
};
|
|
408
|
+
const markdownPath = resolve(booksOutputRoot(projectRoot), runtime.TREESEED_LIBRARY_DOWNLOAD.downloadFileName);
|
|
409
|
+
const indexPath = sidecarIndexPath(markdownPath);
|
|
410
|
+
mkdirSync(dirname(markdownPath), { recursive: true });
|
|
411
|
+
writeFileSync(markdownPath, renderLibraryMarkdown(manifest, memberPackages), "utf8");
|
|
412
|
+
writeFileSync(indexPath, `${JSON.stringify({
|
|
413
|
+
packageKind: manifest.packageKind,
|
|
414
|
+
packageVersion: manifest.packageVersion,
|
|
415
|
+
packageId: manifest.packageId,
|
|
416
|
+
markdownPath,
|
|
417
|
+
book: manifest.book,
|
|
418
|
+
members: manifest.members,
|
|
419
|
+
files: buildBookIndexPayload(manifest, markdownPath).files
|
|
420
|
+
}, null, 2)}
|
|
421
|
+
`, "utf8");
|
|
422
|
+
return {
|
|
423
|
+
manifest,
|
|
424
|
+
markdownPath,
|
|
425
|
+
indexPath,
|
|
426
|
+
memberPackages,
|
|
427
|
+
sourceFileCount: files.length,
|
|
428
|
+
includedRoots: manifest.book.resolvedRoots
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function exportTenantBookPackages(options = {}) {
|
|
432
|
+
const { projectRoot } = loadTenant(resolve(options.projectRoot ?? process.cwd()));
|
|
433
|
+
const libraryPackage = await exportBookLibrary({ projectRoot });
|
|
434
|
+
const legacyOutputFile = resolve(projectRoot, "public", "book.md");
|
|
435
|
+
if (existsSync(legacyOutputFile)) {
|
|
436
|
+
rmSync(legacyOutputFile, { force: true });
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
projectRoot,
|
|
440
|
+
bookPackages: libraryPackage.memberPackages,
|
|
441
|
+
libraryPackage
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
export {
|
|
445
|
+
buildBookExportManifest,
|
|
446
|
+
exportBookLibrary,
|
|
447
|
+
exportBookPackage,
|
|
448
|
+
exportTenantBookPackages
|
|
449
|
+
};
|
|
@@ -83,7 +83,7 @@ export interface TreeseedPluginReference {
|
|
|
83
83
|
enabled?: boolean;
|
|
84
84
|
config?: Record<string, unknown>;
|
|
85
85
|
}
|
|
86
|
-
export type TreeseedPlatformSurfaceName = 'web' | 'api' |
|
|
86
|
+
export type TreeseedPlatformSurfaceName = 'web' | 'api' | (string & {});
|
|
87
87
|
export type TreeseedPlatformResourceKind = 'pages' | 'styles' | 'components' | 'routes' | 'middleware' | 'handlers' | 'config';
|
|
88
88
|
export interface TreeseedPlatformLayerDefinition {
|
|
89
89
|
root: string;
|
|
@@ -135,7 +135,6 @@ export interface TreeseedManagedServicesConfig {
|
|
|
135
135
|
export interface TreeseedPlatformSurfacesConfig {
|
|
136
136
|
web?: TreeseedPlatformSurfaceConfig;
|
|
137
137
|
api?: TreeseedPlatformSurfaceConfig;
|
|
138
|
-
gateway?: TreeseedPlatformSurfaceConfig;
|
|
139
138
|
[key: string]: TreeseedPlatformSurfaceConfig | undefined;
|
|
140
139
|
}
|
|
141
140
|
export interface TreeseedProviderSelections {
|
|
@@ -155,6 +154,10 @@ export interface TreeseedProviderSelections {
|
|
|
155
154
|
};
|
|
156
155
|
site?: string;
|
|
157
156
|
}
|
|
157
|
+
export interface TreeseedExportConfig {
|
|
158
|
+
ignore?: string[];
|
|
159
|
+
bundledPaths?: string[];
|
|
160
|
+
}
|
|
158
161
|
export interface TreeseedDeployConfig {
|
|
159
162
|
name: string;
|
|
160
163
|
slug: string;
|
|
@@ -174,6 +177,7 @@ export interface TreeseedDeployConfig {
|
|
|
174
177
|
turnstile?: {
|
|
175
178
|
enabled?: boolean;
|
|
176
179
|
};
|
|
180
|
+
export?: TreeseedExportConfig;
|
|
177
181
|
}
|
|
178
182
|
export interface TreeseedTenantConfig {
|
|
179
183
|
id: string;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { TreeseedDeployConfig } from './contracts.ts';
|
|
2
2
|
export declare function resolveTreeseedDeployConfigPath(configPath?: string): string;
|
|
3
|
+
export declare function resolveTreeseedDeployConfigPathFromRoot(tenantRoot: string, configPath?: string): string;
|
|
3
4
|
export declare function deriveCloudflareWorkerName(config: TreeseedDeployConfig): string;
|
|
4
5
|
export declare function loadTreeseedDeployConfig(configPath?: string): TreeseedDeployConfig;
|
|
6
|
+
export declare function loadTreeseedDeployConfigFromPath(resolvedConfigPath: string): TreeseedDeployConfig;
|
|
@@ -41,6 +41,15 @@ function optionalBoolean(value, label) {
|
|
|
41
41
|
}
|
|
42
42
|
return value;
|
|
43
43
|
}
|
|
44
|
+
function optionalStringArray(value, label) {
|
|
45
|
+
if (value === void 0) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(value)) {
|
|
49
|
+
throw new Error(`Invalid deploy config: expected ${label} to be an array of strings when provided.`);
|
|
50
|
+
}
|
|
51
|
+
return value.map((entry, index) => expectString(entry, `${label}[${index}]`)).filter(Boolean);
|
|
52
|
+
}
|
|
44
53
|
function optionalRecord(value, label) {
|
|
45
54
|
if (value === void 0 || value === null) {
|
|
46
55
|
return void 0;
|
|
@@ -185,6 +194,16 @@ function parsePlatformSurfacesConfig(value) {
|
|
|
185
194
|
])
|
|
186
195
|
);
|
|
187
196
|
}
|
|
197
|
+
function parseExportConfig(value) {
|
|
198
|
+
const record = optionalRecord(value, "export");
|
|
199
|
+
if (!record) {
|
|
200
|
+
return void 0;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
ignore: optionalStringArray(record.ignore, "export.ignore"),
|
|
204
|
+
bundledPaths: optionalStringArray(record.bundledPaths, "export.bundledPaths")
|
|
205
|
+
};
|
|
206
|
+
}
|
|
188
207
|
function parseDeployConfig(raw) {
|
|
189
208
|
const parsed = normalizeAliasedRecord(
|
|
190
209
|
deployConfigFieldAliases,
|
|
@@ -215,11 +234,15 @@ function parseDeployConfig(raw) {
|
|
|
215
234
|
},
|
|
216
235
|
turnstile: {
|
|
217
236
|
enabled: true
|
|
218
|
-
}
|
|
237
|
+
},
|
|
238
|
+
export: parseExportConfig(parsed.export)
|
|
219
239
|
};
|
|
220
240
|
}
|
|
221
241
|
function resolveTreeseedDeployConfigPath(configPath = "treeseed.site.yaml") {
|
|
222
242
|
const tenantRoot = resolveTreeseedTenantRoot();
|
|
243
|
+
return resolveTreeseedDeployConfigPathFromRoot(tenantRoot, configPath);
|
|
244
|
+
}
|
|
245
|
+
function resolveTreeseedDeployConfigPathFromRoot(tenantRoot, configPath = "treeseed.site.yaml") {
|
|
223
246
|
const candidate = resolve(tenantRoot, configPath);
|
|
224
247
|
if (!existsSync(candidate)) {
|
|
225
248
|
throw new Error(`Unable to resolve Treeseed deploy config at "${candidate}".`);
|
|
@@ -231,6 +254,9 @@ function deriveCloudflareWorkerName(config) {
|
|
|
231
254
|
}
|
|
232
255
|
function loadTreeseedDeployConfig(configPath = "treeseed.site.yaml") {
|
|
233
256
|
const resolvedConfigPath = resolveTreeseedDeployConfigPath(configPath);
|
|
257
|
+
return loadTreeseedDeployConfigFromPath(resolvedConfigPath);
|
|
258
|
+
}
|
|
259
|
+
function loadTreeseedDeployConfigFromPath(resolvedConfigPath) {
|
|
234
260
|
const tenantRoot = dirname(resolvedConfigPath);
|
|
235
261
|
const parsed = parseDeployConfig(readFileSync(resolvedConfigPath, "utf8"));
|
|
236
262
|
Object.defineProperty(parsed, "__tenantRoot", {
|
|
@@ -246,5 +272,7 @@ function loadTreeseedDeployConfig(configPath = "treeseed.site.yaml") {
|
|
|
246
272
|
export {
|
|
247
273
|
deriveCloudflareWorkerName,
|
|
248
274
|
loadTreeseedDeployConfig,
|
|
249
|
-
|
|
275
|
+
loadTreeseedDeployConfigFromPath,
|
|
276
|
+
resolveTreeseedDeployConfigPath,
|
|
277
|
+
resolveTreeseedDeployConfigPathFromRoot
|
|
250
278
|
};
|
package/dist/platform/env.yaml
CHANGED
|
@@ -12,6 +12,7 @@ entries:
|
|
|
12
12
|
- local
|
|
13
13
|
- staging
|
|
14
14
|
- prod
|
|
15
|
+
storage: shared
|
|
15
16
|
requirement: required
|
|
16
17
|
purposes:
|
|
17
18
|
- save
|
|
@@ -34,6 +35,7 @@ entries:
|
|
|
34
35
|
- local
|
|
35
36
|
- staging
|
|
36
37
|
- prod
|
|
38
|
+
storage: shared
|
|
37
39
|
requirement: required
|
|
38
40
|
purposes:
|
|
39
41
|
- save
|
|
@@ -58,6 +60,7 @@ entries:
|
|
|
58
60
|
- local
|
|
59
61
|
- staging
|
|
60
62
|
- prod
|
|
63
|
+
storage: shared
|
|
61
64
|
requirement: conditional
|
|
62
65
|
purposes:
|
|
63
66
|
- deploy
|
|
@@ -83,6 +86,7 @@ entries:
|
|
|
83
86
|
- local
|
|
84
87
|
- staging
|
|
85
88
|
- prod
|
|
89
|
+
storage: shared
|
|
86
90
|
requirement: optional
|
|
87
91
|
purposes:
|
|
88
92
|
- deploy
|
|
@@ -107,6 +111,7 @@ entries:
|
|
|
107
111
|
scopes:
|
|
108
112
|
- staging
|
|
109
113
|
- prod
|
|
114
|
+
storage: shared
|
|
110
115
|
requirement: required
|
|
111
116
|
purposes:
|
|
112
117
|
- save
|
|
@@ -5,18 +5,20 @@ export declare const TREESEED_ENVIRONMENT_REQUIREMENTS: readonly ["required", "c
|
|
|
5
5
|
export declare const TREESEED_ENVIRONMENT_TARGETS: readonly ["local-file", "wrangler-dev-vars", "github-secret", "github-variable", "cloudflare-secret", "cloudflare-var", "railway-secret", "railway-var", "config-file"];
|
|
6
6
|
export declare const TREESEED_ENVIRONMENT_PURPOSES: readonly ["dev", "save", "deploy", "destroy", "config"];
|
|
7
7
|
export declare const TREESEED_ENVIRONMENT_SENSITIVITY: readonly ["secret", "plain", "derived"];
|
|
8
|
+
export declare const TREESEED_ENVIRONMENT_STORAGE: readonly ["scoped", "shared"];
|
|
8
9
|
export type TreeseedEnvironmentScope = (typeof TREESEED_ENVIRONMENT_SCOPES)[number];
|
|
9
10
|
export type TreeseedEnvironmentRequirement = (typeof TREESEED_ENVIRONMENT_REQUIREMENTS)[number];
|
|
10
11
|
export type TreeseedEnvironmentTarget = (typeof TREESEED_ENVIRONMENT_TARGETS)[number];
|
|
11
12
|
export type TreeseedEnvironmentPurpose = (typeof TREESEED_ENVIRONMENT_PURPOSES)[number];
|
|
12
13
|
export type TreeseedEnvironmentSensitivity = (typeof TREESEED_ENVIRONMENT_SENSITIVITY)[number];
|
|
14
|
+
export type TreeseedEnvironmentStorage = (typeof TREESEED_ENVIRONMENT_STORAGE)[number];
|
|
13
15
|
export type TreeseedEnvironmentValidation = {
|
|
14
16
|
kind: 'string' | 'nonempty' | 'boolean' | 'number' | 'url' | 'email';
|
|
15
17
|
} | {
|
|
16
18
|
kind: 'enum';
|
|
17
19
|
values: string[];
|
|
18
20
|
};
|
|
19
|
-
export type TreeseedEnvironmentValueResolver = string | ((context: TreeseedEnvironmentContext, scope: TreeseedEnvironmentScope) => string);
|
|
21
|
+
export type TreeseedEnvironmentValueResolver = string | ((context: TreeseedEnvironmentContext, scope: TreeseedEnvironmentScope, values?: Record<string, string | undefined>) => string | undefined);
|
|
20
22
|
export type TreeseedMachineSecretPayload = {
|
|
21
23
|
algorithm: 'aes-256-gcm';
|
|
22
24
|
iv: string;
|
|
@@ -39,6 +41,10 @@ export type TreeseedMachineConfig = {
|
|
|
39
41
|
cloudflare: boolean;
|
|
40
42
|
};
|
|
41
43
|
};
|
|
44
|
+
shared: {
|
|
45
|
+
values: Record<string, string>;
|
|
46
|
+
secrets: Record<string, TreeseedMachineSecretPayload>;
|
|
47
|
+
};
|
|
42
48
|
environments: Record<TreeseedEnvironmentScope, {
|
|
43
49
|
values: Record<string, string>;
|
|
44
50
|
secrets: Record<string, TreeseedMachineSecretPayload>;
|
|
@@ -61,6 +67,7 @@ export type TreeseedEnvironmentEntry = {
|
|
|
61
67
|
scopes: TreeseedEnvironmentScope[];
|
|
62
68
|
requirement: TreeseedEnvironmentRequirement;
|
|
63
69
|
purposes: TreeseedEnvironmentPurpose[];
|
|
70
|
+
storage?: TreeseedEnvironmentStorage;
|
|
64
71
|
validation?: TreeseedEnvironmentValidation;
|
|
65
72
|
sourcePriority?: string[];
|
|
66
73
|
defaultValue?: TreeseedEnvironmentValueResolver;
|
|
@@ -119,7 +126,9 @@ export declare function getTreeseedEnvironmentSuggestedValues(options: {
|
|
|
119
126
|
deployConfig?: TreeseedDeployConfig;
|
|
120
127
|
tenantConfig?: TreeseedTenantConfig;
|
|
121
128
|
plugins?: LoadedTreeseedPluginEntry[];
|
|
129
|
+
values?: Record<string, string | undefined>;
|
|
122
130
|
}): Record<string, string>;
|
|
131
|
+
export declare function isTreeseedEnvironmentEntryRequired(entry: TreeseedEnvironmentEntry, context: TreeseedEnvironmentContext, scope: TreeseedEnvironmentScope, purpose?: TreeseedEnvironmentPurpose): boolean;
|
|
123
132
|
export declare function validateTreeseedEnvironmentValues(options: {
|
|
124
133
|
values: Record<string, string | undefined>;
|
|
125
134
|
scope: TreeseedEnvironmentScope;
|