@treeseed/core 0.4.10 → 0.4.12
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/api/auth/rbac.d.ts +2 -2
- package/dist/api/auth/rbac.js +2 -1
- package/dist/components/site/RouteNotFound.astro +25 -0
- package/dist/content-config.d.ts +1 -0
- package/dist/content.d.ts +1 -0
- package/dist/content.js +177 -1
- package/dist/dev.d.ts +7 -2
- package/dist/dev.js +59 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -1
- package/dist/middleware/editorial-preview.d.ts +26 -0
- package/dist/middleware/editorial-preview.js +37 -0
- package/dist/middleware/starlightRouteData.js +15 -4
- package/dist/pages/[slug].astro +12 -10
- package/dist/pages/agents/[slug].astro +28 -21
- package/dist/pages/books/[slug].astro +19 -12
- package/dist/pages/feed.xml.js +6 -4
- package/dist/pages/index.astro +43 -14
- package/dist/pages/notes/[slug].astro +19 -12
- package/dist/pages/objectives/[slug].astro +30 -23
- package/dist/pages/people/[slug].astro +28 -21
- package/dist/pages/questions/[slug].astro +30 -23
- package/dist/scripts/build-dist.js +6 -1
- package/dist/scripts/dev-platform.js +9 -1
- package/dist/services/agents.d.ts +22 -0
- package/dist/services/agents.js +29 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +11 -0
- package/dist/services/manager.d.ts +247 -0
- package/dist/services/manager.js +1129 -0
- package/dist/services/remote-runner.d.ts +7 -0
- package/dist/services/remote-runner.js +6 -0
- package/dist/services/workday-content.d.ts +53 -0
- package/dist/services/workday-content.js +190 -0
- package/dist/services/workday-report.d.ts +160 -2
- package/dist/services/workday-report.js +3 -26
- package/dist/services/workday-start.d.ts +170 -1
- package/dist/services/workday-start.js +3 -7
- package/dist/services/worker-pool-scaler.d.ts +27 -0
- package/dist/services/worker-pool-scaler.js +109 -0
- package/dist/services/worker.d.ts +7 -0
- package/dist/services/worker.js +3 -0
- package/dist/site.js +43 -27
- package/dist/templates.d.ts +98 -0
- package/dist/templates.js +170 -0
- package/dist/tenant/runtime-config.d.ts +4 -0
- package/dist/tenant/runtime-config.js +34 -1
- package/dist/utils/hub-content.js +35 -0
- package/dist/utils/published-content.js +60 -0
- package/dist/utils/site-models.d.ts +6 -0
- package/dist/utils/site-models.js +16 -0
- package/dist/utils/starlight-nav.js +50 -0
- package/package.json +20 -2
- package/templates/github/deploy.workflow.yml +404 -9
- package/templates/github/hosted-project.workflow.yml +77 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ScaleDecision, WorkerPoolScaleResult, WorkerPoolScaler } from '@treeseed/sdk';
|
|
2
|
+
export type WorkerPoolScalerKind = 'noop' | 'railway';
|
|
3
|
+
export interface RailwayWorkerPoolScalerOptions {
|
|
4
|
+
apiToken?: string | null;
|
|
5
|
+
apiUrl?: string | null;
|
|
6
|
+
serviceId?: string | null;
|
|
7
|
+
environmentId?: string | null;
|
|
8
|
+
projectId?: string | null;
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
mutation?: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare class NoopWorkerPoolScaler implements WorkerPoolScaler {
|
|
13
|
+
scale(decision: ScaleDecision): Promise<WorkerPoolScaleResult>;
|
|
14
|
+
}
|
|
15
|
+
export declare class RailwayWorkerPoolScaler implements WorkerPoolScaler {
|
|
16
|
+
private readonly apiToken;
|
|
17
|
+
private readonly apiUrl;
|
|
18
|
+
private readonly serviceId;
|
|
19
|
+
private readonly environmentId;
|
|
20
|
+
private readonly projectId;
|
|
21
|
+
private readonly fetchImpl;
|
|
22
|
+
private readonly mutation;
|
|
23
|
+
constructor(options?: RailwayWorkerPoolScalerOptions);
|
|
24
|
+
private configured;
|
|
25
|
+
scale(decision: ScaleDecision): Promise<WorkerPoolScaleResult>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createWorkerPoolScaler(kind?: WorkerPoolScalerKind | null, options?: RailwayWorkerPoolScalerOptions): WorkerPoolScaler;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function envValue(name) {
|
|
2
|
+
const value = process.env[name]?.trim();
|
|
3
|
+
return value ? value : "";
|
|
4
|
+
}
|
|
5
|
+
const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
|
|
6
|
+
const DEFAULT_SCALE_MUTATION = `
|
|
7
|
+
mutation TreeseedScaleService($serviceId: String!, $environmentId: String!, $replicas: Int!) {
|
|
8
|
+
serviceInstanceUpdate(
|
|
9
|
+
serviceId: $serviceId
|
|
10
|
+
environmentId: $environmentId
|
|
11
|
+
input: { numReplicas: $replicas }
|
|
12
|
+
) {
|
|
13
|
+
id
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`.trim();
|
|
17
|
+
class NoopWorkerPoolScaler {
|
|
18
|
+
async scale(decision) {
|
|
19
|
+
return {
|
|
20
|
+
applied: false,
|
|
21
|
+
provider: "noop",
|
|
22
|
+
desiredWorkers: decision.desiredWorkers,
|
|
23
|
+
metadata: {
|
|
24
|
+
reason: "scaler_unconfigured"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class RailwayWorkerPoolScaler {
|
|
30
|
+
apiToken;
|
|
31
|
+
apiUrl;
|
|
32
|
+
serviceId;
|
|
33
|
+
environmentId;
|
|
34
|
+
projectId;
|
|
35
|
+
fetchImpl;
|
|
36
|
+
mutation;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.apiToken = options.apiToken?.trim() || envValue("RAILWAY_API_TOKEN") || envValue("RAILWAY_TOKEN") || null;
|
|
39
|
+
this.apiUrl = options.apiUrl?.trim() || envValue("TREESEED_RAILWAY_API_URL") || DEFAULT_RAILWAY_API_URL;
|
|
40
|
+
this.serviceId = options.serviceId?.trim() || envValue("TREESEED_RAILWAY_WORKER_SERVICE_ID") || envValue("TREESEED_WORKER_SERVICE_ID") || null;
|
|
41
|
+
this.environmentId = options.environmentId?.trim() || envValue("TREESEED_RAILWAY_ENVIRONMENT_ID") || null;
|
|
42
|
+
this.projectId = options.projectId?.trim() || envValue("TREESEED_RAILWAY_PROJECT_ID") || null;
|
|
43
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
44
|
+
this.mutation = options.mutation?.trim() || envValue("TREESEED_RAILWAY_SCALE_MUTATION") || DEFAULT_SCALE_MUTATION;
|
|
45
|
+
}
|
|
46
|
+
configured() {
|
|
47
|
+
return Boolean(this.apiToken && this.serviceId && this.environmentId);
|
|
48
|
+
}
|
|
49
|
+
async scale(decision) {
|
|
50
|
+
if (!this.configured() || !this.apiToken || !this.serviceId || !this.environmentId) {
|
|
51
|
+
return {
|
|
52
|
+
applied: false,
|
|
53
|
+
provider: "railway",
|
|
54
|
+
desiredWorkers: decision.desiredWorkers,
|
|
55
|
+
metadata: {
|
|
56
|
+
reason: "railway_scaler_unconfigured",
|
|
57
|
+
projectId: this.projectId,
|
|
58
|
+
serviceId: this.serviceId,
|
|
59
|
+
environmentId: this.environmentId
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const response = await this.fetchImpl(this.apiUrl, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
authorization: `Bearer ${this.apiToken}`,
|
|
67
|
+
"content-type": "application/json"
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
query: this.mutation,
|
|
71
|
+
variables: {
|
|
72
|
+
serviceId: this.serviceId,
|
|
73
|
+
environmentId: this.environmentId,
|
|
74
|
+
replicas: decision.desiredWorkers
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
const payload = await response.json().catch(() => ({}));
|
|
79
|
+
if (!response.ok || Array.isArray(payload.errors) && payload.errors.length > 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
payload.errors?.[0]?.message ?? `Railway worker scale request failed with ${response.status}.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
applied: true,
|
|
86
|
+
provider: "railway",
|
|
87
|
+
desiredWorkers: decision.desiredWorkers,
|
|
88
|
+
metadata: {
|
|
89
|
+
projectId: this.projectId,
|
|
90
|
+
serviceId: this.serviceId,
|
|
91
|
+
environmentId: this.environmentId
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function createWorkerPoolScaler(kind, options = {}) {
|
|
97
|
+
const configuredKind = envValue("TREESEED_WORKER_POOL_SCALER") || null;
|
|
98
|
+
const inferredKind = envValue("RAILWAY_API_TOKEN") && (envValue("TREESEED_RAILWAY_WORKER_SERVICE_ID") || envValue("TREESEED_WORKER_SERVICE_ID")) ? "railway" : "noop";
|
|
99
|
+
const resolvedKind = kind ?? configuredKind ?? inferredKind;
|
|
100
|
+
if (resolvedKind === "railway") {
|
|
101
|
+
return new RailwayWorkerPoolScaler(options);
|
|
102
|
+
}
|
|
103
|
+
return new NoopWorkerPoolScaler();
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
NoopWorkerPoolScaler,
|
|
107
|
+
RailwayWorkerPoolScaler,
|
|
108
|
+
createWorkerPoolScaler
|
|
109
|
+
};
|
|
@@ -2,5 +2,12 @@
|
|
|
2
2
|
export declare function runWorkerCycle(): Promise<{
|
|
3
3
|
ok: boolean;
|
|
4
4
|
processed: number;
|
|
5
|
+
idle: boolean;
|
|
6
|
+
reason: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
processed: number;
|
|
10
|
+
idle?: undefined;
|
|
11
|
+
reason?: undefined;
|
|
5
12
|
}>;
|
|
6
13
|
export declare function startWorkerLoop(): Promise<void>;
|
package/dist/services/worker.js
CHANGED
|
@@ -6,6 +6,9 @@ async function runWorkerCycle() {
|
|
|
6
6
|
const queue = createQueueClient();
|
|
7
7
|
const config = resolveWorkerConfig();
|
|
8
8
|
if (!queue) {
|
|
9
|
+
if (process.env.TREESEED_LOCAL_DEV_MODE?.trim()) {
|
|
10
|
+
return { ok: true, processed: 0, idle: true, reason: "queue_unconfigured" };
|
|
11
|
+
}
|
|
9
12
|
throw new Error("Worker requires CLOUDFLARE_ACCOUNT_ID, TREESEED_QUEUE_ID, and TREESEED_QUEUE_PULL_TOKEN.");
|
|
10
13
|
}
|
|
11
14
|
const pulled = await queue.pull({
|
package/dist/site.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveTreeseedSiteResource,
|
|
19
19
|
resolveTreeseedStyleEntrypoint
|
|
20
20
|
} from "./site-resources.js";
|
|
21
|
+
import { isSiteRenderedModel } from "./utils/site-models.js";
|
|
21
22
|
const TENANT_THEME_VIRTUAL_ID = "virtual:treeseed/tenant-theme.css";
|
|
22
23
|
const RESOLVED_TENANT_THEME_VIRTUAL_ID = "\0treeseed:tenant-theme.css";
|
|
23
24
|
function packageFile(relativePath) {
|
|
@@ -37,20 +38,20 @@ const PACKAGE_ROUTE_ENTRIES = [
|
|
|
37
38
|
{ pattern: "/", resourcePath: "pages/index.astro" },
|
|
38
39
|
{ pattern: "/404", resourcePath: "pages/404.astro" },
|
|
39
40
|
{ pattern: "/contact", resourcePath: "pages/contact.astro" },
|
|
40
|
-
{ pattern: "/feed.xml", resourcePath: "pages/feed.xml" },
|
|
41
|
-
{ pattern: "/[slug]", resourcePath: "pages/[slug].astro" },
|
|
42
|
-
{ pattern: "/agents", resourcePath: "pages/agents/index.astro" },
|
|
43
|
-
{ pattern: "/agents/[slug]", resourcePath: "pages/agents/[slug].astro" },
|
|
44
|
-
{ pattern: "/books", resourcePath: "pages/books/index.astro" },
|
|
45
|
-
{ pattern: "/books/[slug]", resourcePath: "pages/books/[slug].astro" },
|
|
46
|
-
{ pattern: "/notes", resourcePath: "pages/notes/index.astro" },
|
|
47
|
-
{ pattern: "/notes/[slug]", resourcePath: "pages/notes/[slug].astro" },
|
|
48
|
-
{ pattern: "/objectives", resourcePath: "pages/objectives/index.astro" },
|
|
49
|
-
{ pattern: "/objectives/[slug]", resourcePath: "pages/objectives/[slug].astro" },
|
|
50
|
-
{ pattern: "/people", resourcePath: "pages/people/index.astro" },
|
|
51
|
-
{ pattern: "/people/[slug]", resourcePath: "pages/people/[slug].astro" },
|
|
52
|
-
{ pattern: "/questions", resourcePath: "pages/questions/index.astro" },
|
|
53
|
-
{ pattern: "/questions/[slug]", resourcePath: "pages/questions/[slug].astro" }
|
|
41
|
+
{ pattern: "/feed.xml", resourcePath: "pages/feed.xml", model: "notes" },
|
|
42
|
+
{ pattern: "/[slug]", resourcePath: "pages/[slug].astro", model: "pages" },
|
|
43
|
+
{ pattern: "/agents", resourcePath: "pages/agents/index.astro", model: "agents" },
|
|
44
|
+
{ pattern: "/agents/[slug]", resourcePath: "pages/agents/[slug].astro", model: "agents" },
|
|
45
|
+
{ pattern: "/books", resourcePath: "pages/books/index.astro", model: "books" },
|
|
46
|
+
{ pattern: "/books/[slug]", resourcePath: "pages/books/[slug].astro", model: "books" },
|
|
47
|
+
{ pattern: "/notes", resourcePath: "pages/notes/index.astro", model: "notes" },
|
|
48
|
+
{ pattern: "/notes/[slug]", resourcePath: "pages/notes/[slug].astro", model: "notes" },
|
|
49
|
+
{ pattern: "/objectives", resourcePath: "pages/objectives/index.astro", model: "objectives" },
|
|
50
|
+
{ pattern: "/objectives/[slug]", resourcePath: "pages/objectives/[slug].astro", model: "objectives" },
|
|
51
|
+
{ pattern: "/people", resourcePath: "pages/people/index.astro", model: "people" },
|
|
52
|
+
{ pattern: "/people/[slug]", resourcePath: "pages/people/[slug].astro", model: "people" },
|
|
53
|
+
{ pattern: "/questions", resourcePath: "pages/questions/index.astro", model: "questions" },
|
|
54
|
+
{ pattern: "/questions/[slug]", resourcePath: "pages/questions/[slug].astro", model: "questions" }
|
|
54
55
|
];
|
|
55
56
|
function createTreeseedRoutesIntegration(tenantConfig, routes = []) {
|
|
56
57
|
return {
|
|
@@ -58,11 +59,7 @@ function createTreeseedRoutesIntegration(tenantConfig, routes = []) {
|
|
|
58
59
|
hooks: {
|
|
59
60
|
"astro:config:setup"({ injectRoute }) {
|
|
60
61
|
for (const route of routes) {
|
|
61
|
-
if (route.
|
|
62
|
-
if (route.pattern.startsWith("/books") && tenantConfig.features?.books === false) continue;
|
|
63
|
-
if (route.pattern.startsWith("/notes") && tenantConfig.features?.notes === false) continue;
|
|
64
|
-
if (route.pattern.startsWith("/objectives") && tenantConfig.features?.objectives === false) continue;
|
|
65
|
-
if (route.pattern.startsWith("/questions") && tenantConfig.features?.questions === false) continue;
|
|
62
|
+
if (route.model && !isSiteRenderedModel(tenantConfig, route.model)) continue;
|
|
66
63
|
injectRoute(route);
|
|
67
64
|
}
|
|
68
65
|
}
|
|
@@ -80,14 +77,16 @@ function resolveRouteEntry(route, siteLayers) {
|
|
|
80
77
|
return {
|
|
81
78
|
pattern: route.pattern,
|
|
82
79
|
entrypoint: route.entrypoint,
|
|
83
|
-
resourcePath: route.resourcePath
|
|
80
|
+
resourcePath: route.resourcePath,
|
|
81
|
+
model: route.model
|
|
84
82
|
};
|
|
85
83
|
}
|
|
86
84
|
if (route.resourcePath) {
|
|
87
85
|
return {
|
|
88
86
|
pattern: route.pattern,
|
|
89
87
|
entrypoint: resolveTreeseedPageEntrypoint(siteLayers, route.resourcePath),
|
|
90
|
-
resourcePath: route.resourcePath
|
|
88
|
+
resourcePath: route.resourcePath,
|
|
89
|
+
model: route.model
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
throw new Error(`Treeseed route "${route.pattern}" must define either entrypoint or resourcePath.`);
|
|
@@ -208,6 +207,8 @@ function createTreeseedSite(tenantConfig, { starlight }) {
|
|
|
208
207
|
const deployConfig = loadTreeseedDeployConfig();
|
|
209
208
|
const pluginRuntime = loadTreeseedPluginRuntime(deployConfig);
|
|
210
209
|
const bookRuntime = buildTenantBookRuntime(tenantConfig, { projectRoot });
|
|
210
|
+
const docsRendered = isSiteRenderedModel(tenantConfig, "docs");
|
|
211
|
+
const booksRendered = isSiteRenderedModel(tenantConfig, "books");
|
|
211
212
|
const tenantThemeCss = buildTenantThemeCss(siteConfig.site.theme);
|
|
212
213
|
const siteLayers = buildTreeseedSiteLayers(pluginRuntime, {
|
|
213
214
|
coreRoot: fileURLToPath(new URL(".", import.meta.url)),
|
|
@@ -230,10 +231,16 @@ function createTreeseedSite(tenantConfig, { starlight }) {
|
|
|
230
231
|
const injectedSiteConfig = JSON.stringify(siteConfig);
|
|
231
232
|
const injectedDeployConfig = JSON.stringify(deployConfig);
|
|
232
233
|
const resolvedGlobalCss = resolveTreeseedStyleEntrypoint(siteLayers, "styles/global.css");
|
|
234
|
+
const serverRendered = deployConfig.surfaces?.web?.provider === "cloudflare" || deployConfig.providers.deploy === "cloudflare";
|
|
233
235
|
return defineConfig({
|
|
234
|
-
adapter:
|
|
235
|
-
output:
|
|
236
|
+
adapter: serverRendered ? cloudflare({ imageService: "compile" }) : void 0,
|
|
237
|
+
output: serverRendered ? "server" : "static",
|
|
236
238
|
site: siteConfig.site.siteUrl,
|
|
239
|
+
image: {
|
|
240
|
+
service: {
|
|
241
|
+
entrypoint: "astro/assets/services/sharp"
|
|
242
|
+
}
|
|
243
|
+
},
|
|
237
244
|
vite: {
|
|
238
245
|
define: {
|
|
239
246
|
__TREESEED_TENANT_CONFIG__: injectedTenantConfig,
|
|
@@ -247,7 +254,7 @@ function createTreeseedSite(tenantConfig, { starlight }) {
|
|
|
247
254
|
...siteExtensions.vitePlugins
|
|
248
255
|
],
|
|
249
256
|
ssr: {
|
|
250
|
-
external: ["node:fs", "node:path", "node:url"]
|
|
257
|
+
external: ["node:async_hooks", "node:crypto", "node:fs", "node:module", "node:path", "node:url"]
|
|
251
258
|
}
|
|
252
259
|
},
|
|
253
260
|
markdown: {
|
|
@@ -268,6 +275,13 @@ function createTreeseedSite(tenantConfig, { starlight }) {
|
|
|
268
275
|
TREESEED_SMTP_FROM: envField.string({ context: "server", access: "secret", optional: true }),
|
|
269
276
|
TREESEED_SMTP_REPLY_TO: envField.string({ context: "server", access: "secret", optional: true }),
|
|
270
277
|
TREESEED_FORM_TOKEN_SECRET: envField.string({ context: "server", access: "secret", optional: true }),
|
|
278
|
+
TREESEED_EDITORIAL_PREVIEW_SECRET: envField.string({ context: "server", access: "secret", optional: true }),
|
|
279
|
+
TREESEED_EDITORIAL_PREVIEW_ROOT: envField.string({ context: "server", access: "secret", optional: true }),
|
|
280
|
+
TREESEED_EDITORIAL_PREVIEW_TTL_HOURS: envField.number({ context: "server", access: "secret", optional: true }),
|
|
281
|
+
TREESEED_CONTENT_BUCKET_NAME: envField.string({ context: "server", access: "secret", optional: true }),
|
|
282
|
+
TREESEED_CONTENT_DEFAULT_TEAM_ID: envField.string({ context: "server", access: "secret", optional: true }),
|
|
283
|
+
TREESEED_CONTENT_MANIFEST_KEY_TEMPLATE: envField.string({ context: "server", access: "secret", optional: true }),
|
|
284
|
+
TREESEED_CONTENT_PREVIEW_ROOT_TEMPLATE: envField.string({ context: "server", access: "secret", optional: true }),
|
|
271
285
|
TREESEED_LOCAL_DEV_MODE: envField.enum({ values: ["cloudflare"], context: "server", access: "secret", optional: true }),
|
|
272
286
|
TREESEED_FORMS_LOCAL_BYPASS_TURNSTILE: envField.boolean({ context: "server", access: "secret", optional: true }),
|
|
273
287
|
TREESEED_FORMS_LOCAL_BYPASS_CLOUDFLARE_GUARDS: envField.boolean({ context: "server", access: "secret", optional: true }),
|
|
@@ -279,7 +293,9 @@ function createTreeseedSite(tenantConfig, { starlight }) {
|
|
|
279
293
|
},
|
|
280
294
|
integrations: [
|
|
281
295
|
createTreeseedRoutesIntegration(tenantConfig, resolvedRoutes),
|
|
282
|
-
starlight({
|
|
296
|
+
...docsRendered ? [starlight({
|
|
297
|
+
prerender: !serverRendered,
|
|
298
|
+
pagefind: !serverRendered,
|
|
283
299
|
disable404Route: true,
|
|
284
300
|
expressiveCode: false,
|
|
285
301
|
customCss: [resolvedGlobalCss, TENANT_THEME_VIRTUAL_ID, ...siteExtensions.customCss],
|
|
@@ -303,9 +319,9 @@ function createTreeseedSite(tenantConfig, { starlight }) {
|
|
|
303
319
|
ThemeSelect: resolveCoreComponentEntrypoint(siteLayers, "components/docs/ThemeSelect.astro", "./components/docs/ThemeSelect.astro"),
|
|
304
320
|
...siteExtensions.starlightComponents
|
|
305
321
|
},
|
|
306
|
-
sidebar: getStarlightSidebarConfigFromRuntime(bookRuntime),
|
|
322
|
+
sidebar: booksRendered ? getStarlightSidebarConfigFromRuntime(bookRuntime) : [],
|
|
307
323
|
routeMiddleware: [packageModuleFile("./middleware/starlightRouteData"), ...siteExtensions.routeMiddleware]
|
|
308
|
-
}),
|
|
324
|
+
})] : [],
|
|
309
325
|
...siteExtensions.integrations
|
|
310
326
|
]
|
|
311
327
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CatalogItem, CatalogItemOfferMode } from '@treeseed/sdk/types';
|
|
2
|
+
export interface TemplateContentEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
data: {
|
|
5
|
+
slug: string;
|
|
6
|
+
title: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
description: string;
|
|
9
|
+
status: string;
|
|
10
|
+
category: string;
|
|
11
|
+
featured?: boolean;
|
|
12
|
+
publisher: {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
templateVersion: string;
|
|
16
|
+
templateApiVersion: number;
|
|
17
|
+
minCliVersion: string;
|
|
18
|
+
minCoreVersion: string;
|
|
19
|
+
offer?: {
|
|
20
|
+
priceModel?: CatalogItemOfferMode | string;
|
|
21
|
+
};
|
|
22
|
+
fulfillment: {
|
|
23
|
+
mode?: string;
|
|
24
|
+
source: {
|
|
25
|
+
kind: 'git';
|
|
26
|
+
repoUrl: string;
|
|
27
|
+
directory: string;
|
|
28
|
+
ref: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'r2';
|
|
31
|
+
objectKey: string;
|
|
32
|
+
version: string;
|
|
33
|
+
publicUrl?: string;
|
|
34
|
+
};
|
|
35
|
+
hooksPolicy?: string;
|
|
36
|
+
supportsReconcile?: boolean;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
export interface TemplateCatalogProviderContext {
|
|
42
|
+
locals?: object | null | undefined;
|
|
43
|
+
}
|
|
44
|
+
export interface TemplateCatalogProvider {
|
|
45
|
+
listItems?(context: TemplateCatalogProviderContext): Promise<CatalogItem[]>;
|
|
46
|
+
getItemBySlug?(slug: string, context: TemplateCatalogProviderContext): Promise<CatalogItem | null>;
|
|
47
|
+
}
|
|
48
|
+
export interface TemplateSiteCard {
|
|
49
|
+
slug: string;
|
|
50
|
+
title: string;
|
|
51
|
+
summary: string;
|
|
52
|
+
category: string;
|
|
53
|
+
featured: boolean;
|
|
54
|
+
publisherName: string;
|
|
55
|
+
templateVersion?: string;
|
|
56
|
+
priceModel?: CatalogItemOfferMode | string;
|
|
57
|
+
source: 'catalog' | 'content';
|
|
58
|
+
}
|
|
59
|
+
export interface TemplateSiteDetail extends TemplateSiteCard {
|
|
60
|
+
description: string;
|
|
61
|
+
compatibility: {
|
|
62
|
+
templateVersion?: string;
|
|
63
|
+
templateApiVersion?: number | string;
|
|
64
|
+
minCliVersion?: string;
|
|
65
|
+
minCoreVersion?: string;
|
|
66
|
+
};
|
|
67
|
+
fulfillment: {
|
|
68
|
+
mode: string;
|
|
69
|
+
sourceLabel: string;
|
|
70
|
+
artifactKey?: string | null;
|
|
71
|
+
manifestKey?: string | null;
|
|
72
|
+
repoUrl?: string;
|
|
73
|
+
directory?: string;
|
|
74
|
+
ref?: string;
|
|
75
|
+
objectKey?: string;
|
|
76
|
+
version?: string;
|
|
77
|
+
publicUrl?: string;
|
|
78
|
+
hooksPolicy?: string | null;
|
|
79
|
+
supportsReconcile?: boolean | null;
|
|
80
|
+
};
|
|
81
|
+
contentEntry: TemplateContentEntry | null;
|
|
82
|
+
catalogItem: CatalogItem | null;
|
|
83
|
+
}
|
|
84
|
+
type TemplateSourceOptions = TemplateCatalogProviderContext & {
|
|
85
|
+
catalogProvider?: TemplateCatalogProvider | null;
|
|
86
|
+
listLocalEntries?: (() => Promise<TemplateContentEntry[]>) | null;
|
|
87
|
+
};
|
|
88
|
+
export interface TemplateSiteListingResult {
|
|
89
|
+
rendered: boolean;
|
|
90
|
+
items: TemplateSiteCard[];
|
|
91
|
+
}
|
|
92
|
+
export interface ResolvedSiteTemplateResult {
|
|
93
|
+
rendered: boolean;
|
|
94
|
+
item: TemplateSiteDetail | null;
|
|
95
|
+
}
|
|
96
|
+
export declare function listSiteTemplates(context?: TemplateSourceOptions): Promise<TemplateSiteListingResult>;
|
|
97
|
+
export declare function resolveSiteTemplate(slug: string, context?: TemplateSourceOptions): Promise<ResolvedSiteTemplateResult>;
|
|
98
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { getCollection } from "astro:content";
|
|
2
|
+
import { RUNTIME_TENANT } from "./tenant/runtime-config.js";
|
|
3
|
+
import { siteModelRendered } from "./utils/site-models.js";
|
|
4
|
+
function sortTemplateCards(entries) {
|
|
5
|
+
return [...entries].sort((left, right) => {
|
|
6
|
+
const featuredDelta = Number(Boolean(right.featured)) - Number(Boolean(left.featured));
|
|
7
|
+
if (featuredDelta !== 0) {
|
|
8
|
+
return featuredDelta;
|
|
9
|
+
}
|
|
10
|
+
return String(left.title ?? "").localeCompare(String(right.title ?? ""), void 0, { sensitivity: "base" });
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async function listLocalTemplateEntries(listLocalEntries) {
|
|
14
|
+
if (typeof listLocalEntries === "function") {
|
|
15
|
+
return listLocalEntries();
|
|
16
|
+
}
|
|
17
|
+
if (!RUNTIME_TENANT.content.templates) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
return getCollection("templates");
|
|
21
|
+
}
|
|
22
|
+
function catalogString(metadata, key) {
|
|
23
|
+
const value = metadata?.[key];
|
|
24
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
25
|
+
}
|
|
26
|
+
function catalogNumber(metadata, key) {
|
|
27
|
+
const value = metadata?.[key];
|
|
28
|
+
return typeof value === "number" ? value : void 0;
|
|
29
|
+
}
|
|
30
|
+
function contentCardFromEntry(entry) {
|
|
31
|
+
if (entry.data.status !== "live") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
slug: entry.data.slug,
|
|
36
|
+
title: entry.data.title,
|
|
37
|
+
summary: entry.data.summary,
|
|
38
|
+
category: entry.data.category,
|
|
39
|
+
featured: entry.data.featured === true,
|
|
40
|
+
publisherName: entry.data.publisher.name,
|
|
41
|
+
templateVersion: entry.data.templateVersion,
|
|
42
|
+
priceModel: entry.data.offer?.priceModel,
|
|
43
|
+
source: "content"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function detailFromContentEntry(entry) {
|
|
47
|
+
const source = entry.data.fulfillment.source;
|
|
48
|
+
return {
|
|
49
|
+
...contentCardFromEntry(entry),
|
|
50
|
+
description: entry.data.description,
|
|
51
|
+
compatibility: {
|
|
52
|
+
templateVersion: entry.data.templateVersion,
|
|
53
|
+
templateApiVersion: entry.data.templateApiVersion,
|
|
54
|
+
minCliVersion: entry.data.minCliVersion,
|
|
55
|
+
minCoreVersion: entry.data.minCoreVersion
|
|
56
|
+
},
|
|
57
|
+
fulfillment: source.kind === "r2" ? {
|
|
58
|
+
mode: entry.data.fulfillment.mode ?? "r2",
|
|
59
|
+
sourceLabel: "R2 artifact",
|
|
60
|
+
objectKey: source.objectKey,
|
|
61
|
+
version: source.version,
|
|
62
|
+
publicUrl: source.publicUrl,
|
|
63
|
+
hooksPolicy: entry.data.fulfillment.hooksPolicy,
|
|
64
|
+
supportsReconcile: entry.data.fulfillment.supportsReconcile
|
|
65
|
+
} : {
|
|
66
|
+
mode: entry.data.fulfillment.mode ?? "git",
|
|
67
|
+
sourceLabel: "Git",
|
|
68
|
+
repoUrl: source.repoUrl,
|
|
69
|
+
directory: source.directory,
|
|
70
|
+
ref: source.ref,
|
|
71
|
+
hooksPolicy: entry.data.fulfillment.hooksPolicy,
|
|
72
|
+
supportsReconcile: entry.data.fulfillment.supportsReconcile
|
|
73
|
+
},
|
|
74
|
+
contentEntry: entry,
|
|
75
|
+
catalogItem: null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function cardFromCatalogItem(item) {
|
|
79
|
+
return {
|
|
80
|
+
slug: item.slug,
|
|
81
|
+
title: item.title,
|
|
82
|
+
summary: item.summary ?? "",
|
|
83
|
+
category: catalogString(item.metadata, "category") ?? "Template",
|
|
84
|
+
featured: item.metadata?.featured === true,
|
|
85
|
+
publisherName: catalogString(item.metadata, "publisherName") ?? item.teamId,
|
|
86
|
+
templateVersion: catalogString(item.metadata, "templateVersion"),
|
|
87
|
+
priceModel: item.offerMode,
|
|
88
|
+
source: "catalog"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function detailFromCatalogItem(item) {
|
|
92
|
+
return {
|
|
93
|
+
...cardFromCatalogItem(item),
|
|
94
|
+
description: item.summary ?? "This template is managed through the central market catalog.",
|
|
95
|
+
compatibility: {
|
|
96
|
+
templateVersion: catalogString(item.metadata, "templateVersion"),
|
|
97
|
+
templateApiVersion: catalogNumber(item.metadata, "templateApiVersion"),
|
|
98
|
+
minCliVersion: catalogString(item.metadata, "minCliVersion"),
|
|
99
|
+
minCoreVersion: catalogString(item.metadata, "minCoreVersion")
|
|
100
|
+
},
|
|
101
|
+
fulfillment: {
|
|
102
|
+
mode: catalogString(item.metadata, "fulfillmentMode") ?? "r2",
|
|
103
|
+
sourceLabel: item.artifactKey ? "R2 artifact" : "Catalog metadata",
|
|
104
|
+
artifactKey: item.artifactKey,
|
|
105
|
+
manifestKey: item.manifestKey
|
|
106
|
+
},
|
|
107
|
+
contentEntry: null,
|
|
108
|
+
catalogItem: item
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function selectCatalogItemBySlug(slug, context) {
|
|
112
|
+
if (!context.catalogProvider) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (typeof context.catalogProvider.getItemBySlug === "function") {
|
|
116
|
+
return context.catalogProvider.getItemBySlug(slug, { locals: context.locals });
|
|
117
|
+
}
|
|
118
|
+
if (typeof context.catalogProvider.listItems === "function") {
|
|
119
|
+
const items = await context.catalogProvider.listItems({ locals: context.locals });
|
|
120
|
+
return items.find((item) => item.slug === slug) ?? null;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
async function listSiteTemplates(context = {}) {
|
|
125
|
+
if (!siteModelRendered("templates")) {
|
|
126
|
+
return {
|
|
127
|
+
rendered: false,
|
|
128
|
+
items: []
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const catalogItems = context.catalogProvider && typeof context.catalogProvider.listItems === "function" ? await context.catalogProvider.listItems({ locals: context.locals }) : [];
|
|
132
|
+
if (catalogItems.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
rendered: true,
|
|
135
|
+
items: sortTemplateCards(catalogItems.map(cardFromCatalogItem))
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const entries = await listLocalTemplateEntries(context.listLocalEntries);
|
|
139
|
+
return {
|
|
140
|
+
rendered: true,
|
|
141
|
+
items: sortTemplateCards(
|
|
142
|
+
entries.map(contentCardFromEntry).filter((entry) => Boolean(entry))
|
|
143
|
+
)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function resolveSiteTemplate(slug, context = {}) {
|
|
147
|
+
if (!siteModelRendered("templates")) {
|
|
148
|
+
return {
|
|
149
|
+
rendered: false,
|
|
150
|
+
item: null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const catalogItem = await selectCatalogItemBySlug(slug, context);
|
|
154
|
+
if (catalogItem) {
|
|
155
|
+
return {
|
|
156
|
+
rendered: true,
|
|
157
|
+
item: detailFromCatalogItem(catalogItem)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const entries = await listLocalTemplateEntries(context.listLocalEntries);
|
|
161
|
+
const entry = entries.find((candidate) => candidate.data.slug === slug && candidate.data.status === "live") ?? null;
|
|
162
|
+
return {
|
|
163
|
+
rendered: true,
|
|
164
|
+
item: entry ? detailFromContentEntry(entry) : null
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
listSiteTemplates,
|
|
169
|
+
resolveSiteTemplate
|
|
170
|
+
};
|
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
import { loadTreeseedManifest } from "@treeseed/sdk/platform/tenant-config";
|
|
3
4
|
import { parseSiteConfig } from "../utils/site-config-schema.js";
|
|
4
5
|
const injectedTenantConfig = typeof __TREESEED_TENANT_CONFIG__ !== "undefined" ? __TREESEED_TENANT_CONFIG__ : null;
|
|
5
6
|
const injectedProjectRoot = typeof __TREESEED_PROJECT_ROOT__ !== "undefined" ? __TREESEED_PROJECT_ROOT__ : null;
|
|
6
7
|
const injectedSiteConfig = typeof __TREESEED_SITE_CONFIG__ !== "undefined" ? __TREESEED_SITE_CONFIG__ : null;
|
|
7
|
-
const RUNTIME_TENANT = injectedTenantConfig ?? loadTreeseedManifest();
|
|
8
8
|
const RUNTIME_PROJECT_ROOT = injectedProjectRoot ?? process.cwd();
|
|
9
|
+
function fallbackTenantConfig(projectRoot) {
|
|
10
|
+
return {
|
|
11
|
+
id: "treeseed-runtime",
|
|
12
|
+
siteConfigPath: resolve(projectRoot, "treeseed.site.yaml"),
|
|
13
|
+
content: {
|
|
14
|
+
pages: resolve(projectRoot, "src/content/pages"),
|
|
15
|
+
notes: resolve(projectRoot, "src/content/notes"),
|
|
16
|
+
questions: resolve(projectRoot, "src/content/questions"),
|
|
17
|
+
objectives: resolve(projectRoot, "src/content/objectives"),
|
|
18
|
+
people: resolve(projectRoot, "src/content/people"),
|
|
19
|
+
agents: resolve(projectRoot, "src/content/agents"),
|
|
20
|
+
books: resolve(projectRoot, "src/content/books"),
|
|
21
|
+
docs: resolve(projectRoot, "src/content/knowledge"),
|
|
22
|
+
templates: resolve(projectRoot, "src/content/templates"),
|
|
23
|
+
knowledge_packs: resolve(projectRoot, "src/content/knowledge-packs"),
|
|
24
|
+
workdays: resolve(projectRoot, "src/content/workdays")
|
|
25
|
+
},
|
|
26
|
+
features: {
|
|
27
|
+
docs: true,
|
|
28
|
+
books: true
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const RUNTIME_TENANT = (() => {
|
|
33
|
+
if (injectedTenantConfig) {
|
|
34
|
+
return injectedTenantConfig;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return loadTreeseedManifest();
|
|
38
|
+
} catch {
|
|
39
|
+
return fallbackTenantConfig(RUNTIME_PROJECT_ROOT);
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
9
42
|
const RUNTIME_SITE_CONFIG = injectedSiteConfig ?? (() => {
|
|
10
43
|
try {
|
|
11
44
|
return parseSiteConfig(readFileSync(RUNTIME_TENANT.siteConfigPath, "utf8"));
|