@treeseed/sdk 0.4.6 → 0.4.8
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/dist/operations/providers/default.js +1 -0
- package/dist/operations/services/config-runtime.d.ts +121 -26
- package/dist/operations/services/config-runtime.js +330 -196
- package/dist/operations/services/export-runtime.d.ts +18 -0
- package/dist/operations/services/export-runtime.js +136 -0
- package/dist/operations/services/template-registry.d.ts +2 -0
- package/dist/operations/services/template-registry.js +55 -6
- 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 +5 -0
- 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/scripts/aggregate-book.js +13 -118
- package/dist/scripts/config-treeseed.js +18 -27
- package/dist/sdk-types.d.ts +1 -0
- package/dist/template-catalog.js +1 -0
- package/dist/workflow/operations.d.ts +293 -177
- package/dist/workflow/operations.js +302 -188
- package/dist/workflow/policy.d.ts +12 -0
- package/dist/workflow/policy.js +58 -0
- package/dist/workflow-state.d.ts +19 -1
- package/dist/workflow-state.js +57 -39
- package/dist/workflow-support.d.ts +2 -1
- package/dist/workflow-support.js +14 -6
- package/dist/workflow.d.ts +14 -1
- package/dist/workflow.js +8 -1
- 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
|
+
};
|
|
@@ -155,6 +155,10 @@ export interface TreeseedProviderSelections {
|
|
|
155
155
|
};
|
|
156
156
|
site?: string;
|
|
157
157
|
}
|
|
158
|
+
export interface TreeseedExportConfig {
|
|
159
|
+
ignore?: string[];
|
|
160
|
+
bundledPaths?: string[];
|
|
161
|
+
}
|
|
158
162
|
export interface TreeseedDeployConfig {
|
|
159
163
|
name: string;
|
|
160
164
|
slug: string;
|
|
@@ -174,6 +178,7 @@ export interface TreeseedDeployConfig {
|
|
|
174
178
|
turnstile?: {
|
|
175
179
|
enabled?: boolean;
|
|
176
180
|
};
|
|
181
|
+
export?: TreeseedExportConfig;
|
|
177
182
|
}
|
|
178
183
|
export interface TreeseedTenantConfig {
|
|
179
184
|
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;
|