@treeseed/core 0.4.10 → 0.4.11

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.
Files changed (55) hide show
  1. package/dist/api/auth/rbac.d.ts +2 -2
  2. package/dist/api/auth/rbac.js +2 -1
  3. package/dist/components/site/RouteNotFound.astro +25 -0
  4. package/dist/content-config.d.ts +1 -0
  5. package/dist/content.d.ts +1 -0
  6. package/dist/content.js +177 -1
  7. package/dist/dev.d.ts +7 -2
  8. package/dist/dev.js +59 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +9 -1
  11. package/dist/middleware/editorial-preview.d.ts +26 -0
  12. package/dist/middleware/editorial-preview.js +37 -0
  13. package/dist/middleware/starlightRouteData.js +15 -4
  14. package/dist/pages/[slug].astro +12 -10
  15. package/dist/pages/agents/[slug].astro +28 -21
  16. package/dist/pages/books/[slug].astro +19 -12
  17. package/dist/pages/feed.xml.js +6 -4
  18. package/dist/pages/index.astro +43 -14
  19. package/dist/pages/notes/[slug].astro +19 -12
  20. package/dist/pages/objectives/[slug].astro +30 -23
  21. package/dist/pages/people/[slug].astro +28 -21
  22. package/dist/pages/questions/[slug].astro +30 -23
  23. package/dist/scripts/build-dist.js +6 -1
  24. package/dist/scripts/dev-platform.js +9 -1
  25. package/dist/services/agents.d.ts +22 -0
  26. package/dist/services/agents.js +29 -0
  27. package/dist/services/index.d.ts +3 -0
  28. package/dist/services/index.js +11 -0
  29. package/dist/services/manager.d.ts +247 -0
  30. package/dist/services/manager.js +1129 -0
  31. package/dist/services/remote-runner.d.ts +7 -0
  32. package/dist/services/remote-runner.js +6 -0
  33. package/dist/services/workday-content.d.ts +53 -0
  34. package/dist/services/workday-content.js +190 -0
  35. package/dist/services/workday-report.d.ts +160 -2
  36. package/dist/services/workday-report.js +3 -26
  37. package/dist/services/workday-start.d.ts +170 -1
  38. package/dist/services/workday-start.js +3 -7
  39. package/dist/services/worker-pool-scaler.d.ts +27 -0
  40. package/dist/services/worker-pool-scaler.js +109 -0
  41. package/dist/services/worker.d.ts +7 -0
  42. package/dist/services/worker.js +3 -0
  43. package/dist/site.js +43 -27
  44. package/dist/templates.d.ts +98 -0
  45. package/dist/templates.js +170 -0
  46. package/dist/tenant/runtime-config.d.ts +4 -0
  47. package/dist/tenant/runtime-config.js +34 -1
  48. package/dist/utils/hub-content.js +35 -0
  49. package/dist/utils/published-content.js +60 -0
  50. package/dist/utils/site-models.d.ts +6 -0
  51. package/dist/utils/site-models.js +16 -0
  52. package/dist/utils/starlight-nav.js +50 -0
  53. package/package.json +20 -2
  54. package/templates/github/deploy.workflow.yml +404 -9
  55. 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>;
@@ -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.pattern.startsWith("/agents") && tenantConfig.features?.agents === false) continue;
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: deployConfig.surfaces?.web?.provider === "cloudflare" || deployConfig.providers.deploy === "cloudflare" ? cloudflare() : void 0,
235
- output: deployConfig.surfaces?.web?.provider === "cloudflare" || deployConfig.providers.deploy === "cloudflare" ? "server" : "static",
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
+ };
@@ -0,0 +1,4 @@
1
+ import type { TreeseedTenantConfig } from '@treeseed/sdk/platform/contracts';
2
+ export declare const RUNTIME_PROJECT_ROOT: string;
3
+ export declare const RUNTIME_TENANT: TreeseedTenantConfig;
4
+ export declare const RUNTIME_SITE_CONFIG: any;
@@ -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"));