@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.
@@ -0,0 +1,18 @@
1
+ export type TreeseedExportResult = {
2
+ directory: string;
3
+ tenantRoot: string;
4
+ outputPath: string;
5
+ branch: string;
6
+ timestamp: string;
7
+ includedBundlePaths: string[];
8
+ ignorePatterns: string[];
9
+ summary: {
10
+ totalFiles: number;
11
+ totalCharacters: number;
12
+ totalTokens: number;
13
+ outputFiles: string[];
14
+ };
15
+ };
16
+ export declare function exportTreeseedCodebase({ directory, }?: {
17
+ directory?: string;
18
+ }): Promise<TreeseedExportResult>;
@@ -0,0 +1,136 @@
1
+ import { existsSync, mkdirSync, statSync } from "node:fs";
2
+ import { resolve, relative } from "node:path";
3
+ import { runDefaultAction, setLogLevel } from "repomix";
4
+ import { loadTreeseedDeployConfigFromPath, resolveTreeseedDeployConfigPathFromRoot } from "../../platform/deploy-config.js";
5
+ import { findNearestTreeseedRoot } from "./workspace-tools.js";
6
+ import { currentBranch, repoRoot } from "./workspace-save.js";
7
+ function ensureDirectory(directory) {
8
+ if (!existsSync(directory)) {
9
+ throw new Error(`Treeseed export directory does not exist: ${directory}`);
10
+ }
11
+ const stats = statSync(directory);
12
+ if (!stats.isDirectory()) {
13
+ throw new Error(`Treeseed export directory must be a directory: ${directory}`);
14
+ }
15
+ }
16
+ function formatExportTimestamp(date = /* @__PURE__ */ new Date()) {
17
+ const year = date.getFullYear();
18
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
19
+ const day = `${date.getDate()}`.padStart(2, "0");
20
+ const hours = `${date.getHours()}`.padStart(2, "0");
21
+ const minutes = `${date.getMinutes()}`.padStart(2, "0");
22
+ const seconds = `${date.getSeconds()}`.padStart(2, "0");
23
+ return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
24
+ }
25
+ function sanitizeBranchSegment(branch) {
26
+ const sanitized = String(branch ?? "").trim().replaceAll(/[\\/]+/g, "-").replaceAll(/[^a-zA-Z0-9._-]+/g, "-").replaceAll(/-+/g, "-").replaceAll(/^-|-$/g, "");
27
+ return sanitized || "detached";
28
+ }
29
+ function resolveGitBranch(directory) {
30
+ try {
31
+ const gitRoot = repoRoot(directory);
32
+ const branch = currentBranch(gitRoot);
33
+ return sanitizeBranchSegment(branch);
34
+ } catch {
35
+ return "detached";
36
+ }
37
+ }
38
+ function resolveExportOutputPath(directory, branch, timestamp) {
39
+ const exportsRoot = resolve(directory, ".treeseed", "exports");
40
+ mkdirSync(exportsRoot, { recursive: true });
41
+ return resolve(exportsRoot, `${branch}-${timestamp}.md`);
42
+ }
43
+ function resolveConfiguredBundlePaths(directory, tenantRoot, config) {
44
+ const includedBundlePaths = [];
45
+ const seen = /* @__PURE__ */ new Set();
46
+ for (const configuredPath of config.export?.bundledPaths ?? []) {
47
+ const absolutePath = resolve(tenantRoot, configuredPath);
48
+ if (!existsSync(absolutePath)) {
49
+ continue;
50
+ }
51
+ const relativeToDirectory = relative(directory, absolutePath);
52
+ if (!relativeToDirectory || !relativeToDirectory.startsWith("..") && relativeToDirectory !== ".") {
53
+ continue;
54
+ }
55
+ if (seen.has(absolutePath)) {
56
+ continue;
57
+ }
58
+ seen.add(absolutePath);
59
+ includedBundlePaths.push(absolutePath);
60
+ }
61
+ return includedBundlePaths.sort((left, right) => left.localeCompare(right));
62
+ }
63
+ function resolveIgnorePatterns(config) {
64
+ return [
65
+ ".treeseed/exports",
66
+ ".treeseed/exports/**",
67
+ "**/.treeseed/exports",
68
+ "**/.treeseed/exports/**",
69
+ ...config.export?.ignore ?? []
70
+ ];
71
+ }
72
+ function toRepomixDirectories(directory, includedBundlePaths) {
73
+ return [".", ...includedBundlePaths.map((bundlePath) => relative(directory, bundlePath) || ".")];
74
+ }
75
+ function normalizePackResultOutputFiles(packResult, outputPath) {
76
+ return packResult.outputFiles && packResult.outputFiles.length > 0 ? packResult.outputFiles : [outputPath];
77
+ }
78
+ async function withCleanNodeExecArgv(action) {
79
+ const previousExecArgv = [...process.execArgv];
80
+ process.execArgv = previousExecArgv.filter(
81
+ (arg) => !arg.startsWith("--test") && !arg.startsWith("--input-type") && !arg.startsWith("--experimental-test") && !arg.startsWith("--watch")
82
+ );
83
+ try {
84
+ return await action();
85
+ } finally {
86
+ process.execArgv = previousExecArgv;
87
+ }
88
+ }
89
+ async function exportTreeseedCodebase({
90
+ directory = process.cwd()
91
+ } = {}) {
92
+ const resolvedDirectory = resolve(directory);
93
+ ensureDirectory(resolvedDirectory);
94
+ const tenantRoot = findNearestTreeseedRoot(resolvedDirectory);
95
+ if (!tenantRoot) {
96
+ throw new Error(`Treeseed export requires a Treeseed project. No ancestor containing treeseed.site.yaml was found from ${resolvedDirectory}.`);
97
+ }
98
+ const deployConfig = loadTreeseedDeployConfigFromPath(resolveTreeseedDeployConfigPathFromRoot(tenantRoot));
99
+ const includedBundlePaths = resolveConfiguredBundlePaths(resolvedDirectory, tenantRoot, deployConfig);
100
+ const ignorePatterns = resolveIgnorePatterns(deployConfig);
101
+ const branch = resolveGitBranch(resolvedDirectory);
102
+ const timestamp = formatExportTimestamp();
103
+ const outputPath = resolveExportOutputPath(resolvedDirectory, branch, timestamp);
104
+ const options = {
105
+ output: outputPath,
106
+ style: "markdown",
107
+ ignore: ignorePatterns.join(","),
108
+ quiet: true,
109
+ skipLocalConfig: true,
110
+ copy: false,
111
+ stdout: false
112
+ };
113
+ setLogLevel(0);
114
+ const result = await withCleanNodeExecArgv(
115
+ () => runDefaultAction(toRepomixDirectories(resolvedDirectory, includedBundlePaths), resolvedDirectory, options)
116
+ );
117
+ const outputFiles = normalizePackResultOutputFiles(result.packResult, outputPath);
118
+ return {
119
+ directory: resolvedDirectory,
120
+ tenantRoot,
121
+ outputPath,
122
+ branch,
123
+ timestamp,
124
+ includedBundlePaths,
125
+ ignorePatterns,
126
+ summary: {
127
+ totalFiles: result.packResult.totalFiles,
128
+ totalCharacters: result.packResult.totalCharacters,
129
+ totalTokens: result.packResult.totalTokens,
130
+ outputFiles
131
+ }
132
+ };
133
+ }
134
+ export {
135
+ exportTreeseedCodebase
136
+ };
@@ -37,6 +37,7 @@ export interface TemplateProductDefinition extends SdkTemplateCatalogEntry {
37
37
  artifactRoot: string;
38
38
  artifactManifestPath: string;
39
39
  templateRoot: string;
40
+ fulfillmentMode: 'packaged' | 'git';
40
41
  }
41
42
  export interface ResolvedTemplateDefinition {
42
43
  product: TemplateProductDefinition;
@@ -82,6 +83,7 @@ export declare function serializeTemplateRegistryEntry(product: Pick<TemplatePro
82
83
  templateApiVersion: number;
83
84
  minCliVersion: string;
84
85
  minCoreVersion: string | undefined;
86
+ fulfillmentMode: "git" | "packaged";
85
87
  source: import("../../sdk-types.ts").SdkTemplateCatalogSource;
86
88
  };
87
89
  export declare function exportTemplateCatalogYaml(options?: TemplateCatalogOptions): Promise<string>;
@@ -1,4 +1,5 @@
1
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { basename, dirname, relative, resolve } from "node:path";
3
4
  import { RemoteTemplateCatalogClient } from "../../template-catalog.js";
4
5
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
@@ -53,10 +54,10 @@ function validateTemplateProductShape(product) {
53
54
  if (product.status !== "draft" && product.status !== "live" && product.status !== "archived") {
54
55
  throw new Error(`Template product ${product.id} uses unsupported status "${product.status}".`);
55
56
  }
56
- if (!existsSync(product.artifactManifestPath)) {
57
+ if (product.fulfillmentMode === "packaged" && !existsSync(product.artifactManifestPath)) {
57
58
  throw new Error(`Template product ${product.id} points to a missing artifact manifest: ${product.artifactManifestPath}`);
58
59
  }
59
- if (!existsSync(product.templateRoot)) {
60
+ if (product.fulfillmentMode === "packaged" && !existsSync(product.templateRoot)) {
60
61
  throw new Error(`Template product ${product.id} points to a missing template payload: ${product.templateRoot}`);
61
62
  }
62
63
  }
@@ -101,9 +102,55 @@ function normalizeTemplateProduct(remoteProduct) {
101
102
  contentPath: `${remoteProduct.fulfillment.source.repoUrl}#${remoteProduct.id}`,
102
103
  artifactRoot,
103
104
  artifactManifestPath: resolve(artifactRoot, "template.config.json"),
105
+ templateRoot: resolve(artifactRoot, "template"),
106
+ fulfillmentMode: remoteProduct.fulfillment.mode ?? "packaged"
107
+ };
108
+ }
109
+ function sanitizeCacheSegment(value) {
110
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "source";
111
+ }
112
+ function resolveTemplateSourceCacheRoot(product, options) {
113
+ const cachePath = resolveTreeseedTemplateCatalogCachePath(options.cwd ?? process.cwd());
114
+ return resolve(dirname(cachePath), "templates", sanitizeCacheSegment(product.id), sanitizeCacheSegment(product.fulfillment.source.ref));
115
+ }
116
+ function runGit(commandArgs, cwd) {
117
+ const result = spawnSync("git", commandArgs, {
118
+ cwd,
119
+ stdio: "pipe",
120
+ encoding: "utf8"
121
+ });
122
+ if (result.status !== 0) {
123
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `git ${commandArgs.join(" ")} failed`);
124
+ }
125
+ }
126
+ function materializeGitTemplateSource(product, options) {
127
+ const cacheRoot = resolveTemplateSourceCacheRoot(product, options);
128
+ const repoRoot = resolve(cacheRoot, "repo");
129
+ const source = product.fulfillment.source;
130
+ if (!existsSync(resolve(repoRoot, ".git"))) {
131
+ rmSync(cacheRoot, { recursive: true, force: true });
132
+ mkdirSync(cacheRoot, { recursive: true });
133
+ runGit(["clone", "--no-checkout", source.repoUrl, repoRoot]);
134
+ }
135
+ runGit(["fetch", "--all", "--tags"], repoRoot);
136
+ runGit(["checkout", "--force", source.ref], repoRoot);
137
+ const artifactRoot = resolve(repoRoot, source.directory);
138
+ return {
139
+ artifactRoot,
140
+ manifestPath: resolve(artifactRoot, "template.config.json"),
104
141
  templateRoot: resolve(artifactRoot, "template")
105
142
  };
106
143
  }
144
+ function resolveTemplateDefinitionPaths(product, options) {
145
+ if (existsSync(product.artifactManifestPath) && existsSync(product.templateRoot)) {
146
+ return {
147
+ artifactRoot: product.artifactRoot,
148
+ manifestPath: product.artifactManifestPath,
149
+ templateRoot: product.templateRoot
150
+ };
151
+ }
152
+ return materializeGitTemplateSource(product, options);
153
+ }
107
154
  function readTemplateCatalogCache(cachePath) {
108
155
  if (!existsSync(cachePath)) {
109
156
  return null;
@@ -268,11 +315,12 @@ async function resolveTemplateDefinition(id, options = {}, category) {
268
315
  throw new Error(`Unable to resolve template "${id}" in category "${category}".`);
269
316
  }
270
317
  validateTemplateProductShape(product);
271
- const manifest = loadJsonFile(product.artifactManifestPath);
318
+ const resolvedPaths = resolveTemplateDefinitionPaths(product, options);
319
+ const manifest = loadJsonFile(resolvedPaths.manifestPath);
272
320
  const definition = {
273
321
  product,
274
- manifestPath: product.artifactManifestPath,
275
- templateRoot: product.templateRoot,
322
+ manifestPath: resolvedPaths.manifestPath,
323
+ templateRoot: resolvedPaths.templateRoot,
276
324
  manifest
277
325
  };
278
326
  validateTemplateManifest(definition);
@@ -387,6 +435,7 @@ function serializeTemplateRegistryEntry(product) {
387
435
  templateApiVersion: product.templateApiVersion,
388
436
  minCliVersion: product.minCliVersion,
389
437
  minCoreVersion: product.minCoreVersion,
438
+ fulfillmentMode: product.fulfillment.mode ?? "packaged",
390
439
  source: product.fulfillment.source
391
440
  };
392
441
  }
@@ -17,6 +17,7 @@ const TRESEED_OPERATION_SPECS = [
17
17
  operation({ id: "template.sync", name: "sync", aliases: [], group: "Validation", summary: "Validate or reconcile the managed template surface for the current site.", description: "Use remote template metadata plus the local scaffold artifact to check or apply updates to the managed scaffold surface.", provider: "default", related: ["template", "init", "status"] }),
18
18
  operation({ id: "project.init", name: "init", aliases: [], group: "Workflow", summary: "Scaffold a new Treeseed tenant project.", description: "Create a new Treeseed tenant directory from a remote-catalog template backed by the packaged scaffold artifact.", provider: "default", related: ["config", "switch", "dev"] }),
19
19
  operation({ id: "project.config", name: "config", aliases: [], group: "Workflow", summary: "Configure and test the runtime foundation.", description: "Apply safe repairs, collect environment values, write local machine config, generate local env files, initialize environments, sync providers, and run doctor-style checks.", provider: "default", related: ["status", "switch", "dev"] }),
20
+ operation({ id: "project.export", name: "export", aliases: [], group: "Utilities", summary: "Export a Markdown snapshot of the current codebase.", description: "Generate a Markdown codebase snapshot for the selected directory using the SDK-owned repomix integration and store it under .treeseed/exports.", provider: "default", related: ["status", "config"] }),
20
21
  operation({ id: "deploy.release", name: "release", aliases: [], group: "Workflow", summary: "Promote staging to production with a version bump.", description: "Validate staging, apply one version bump, tag the release, merge staging into main, push, and rely on production deploy automation.", provider: "default", related: ["stage", "status", "rollback"] }),
21
22
  operation({ id: "deploy.destroy", name: "destroy", aliases: [], group: "Workflow", summary: "Destroy a persistent environment and its local state.", description: "Delete the selected persistent environment resources and remove the local deploy state after confirmation.", provider: "default", related: ["config", "status"] }),
22
23
  operation({ id: "local.dev", name: "dev", aliases: [], group: "Local Development", summary: "Start the unified local Treeseed development environment.", description: "Start the unified local Treeseed development environment.", provider: "default" }),
@@ -1,5 +1,5 @@
1
1
  export type TreeseedOperationGroup = 'Workflow' | 'Local Development' | 'Validation' | 'Release Utilities' | 'Utilities' | 'Passthrough';
2
- export type TreeseedOperationId = 'workspace.status' | 'workspace.doctor' | 'branch.tasks' | 'branch.switch' | 'branch.save' | 'branch.close' | 'branch.stage' | 'deploy.release' | 'deploy.rollback' | 'deploy.destroy' | 'template.list' | 'template.show' | 'template.validate' | 'template.sync' | 'project.init' | 'project.config' | 'local.dev' | 'local.devWatch' | 'local.build' | 'local.check' | 'local.preview' | 'local.lint' | 'local.test' | 'validation.testUnit' | 'validation.preflight' | 'validation.authCheck' | 'auth.login' | 'auth.logout' | 'auth.whoami' | 'release.testE2e' | 'release.testE2eLocal' | 'release.testE2eStaging' | 'release.testE2eFull' | 'release.testFast' | 'release.verify' | 'release.publishChanged' | 'services.mailpitUp' | 'services.mailpitDown' | 'services.mailpitLogs' | 'data.d1MigrateLocal' | 'content.cleanupMarkdown' | 'content.cleanupMarkdownCheck' | 'tools.astro' | 'tools.syncDevvars' | 'tools.starlightPatch' | 'agents.run';
2
+ export type TreeseedOperationId = 'workspace.status' | 'workspace.doctor' | 'branch.tasks' | 'branch.switch' | 'branch.save' | 'branch.close' | 'branch.stage' | 'deploy.release' | 'deploy.rollback' | 'deploy.destroy' | 'template.list' | 'template.show' | 'template.validate' | 'template.sync' | 'project.init' | 'project.config' | 'local.dev' | 'local.devWatch' | 'local.build' | 'local.check' | 'local.preview' | 'local.lint' | 'local.test' | 'validation.testUnit' | 'validation.preflight' | 'validation.authCheck' | 'auth.login' | 'auth.logout' | 'auth.whoami' | 'release.testE2e' | 'release.testE2eLocal' | 'release.testE2eStaging' | 'release.testE2eFull' | 'release.testFast' | 'release.verify' | 'release.publishChanged' | 'services.mailpitUp' | 'services.mailpitDown' | 'services.mailpitLogs' | 'data.d1MigrateLocal' | 'content.cleanupMarkdown' | 'content.cleanupMarkdownCheck' | 'project.export' | 'tools.astro' | 'tools.syncDevvars' | 'tools.starlightPatch' | 'agents.run';
3
3
  export type TreeseedOperationProviderId = 'default';
4
4
  export type TreeseedOperationMetadata = {
5
5
  id: TreeseedOperationId;
@@ -0,0 +1,78 @@
1
+ export type TreeseedBookExportFileEntry = {
2
+ fileId: string;
3
+ bookId: string;
4
+ memberBookId: string;
5
+ absolutePath: string;
6
+ projectRelativePath: string;
7
+ bookRelativePath: string;
8
+ rootRelativePath: string;
9
+ rootPath: string;
10
+ ordinal: number;
11
+ frontmatterOrder: number | null;
12
+ sourceType: 'md' | 'mdx' | 'text';
13
+ chunkId: string;
14
+ markerId: string;
15
+ };
16
+ export type TreeseedBookExportMemberSummary = {
17
+ bookId: string;
18
+ slug: string;
19
+ title: string;
20
+ order: number;
21
+ downloadFileName: string;
22
+ downloadHref: string;
23
+ sourceFileCount: number;
24
+ markdownPath?: string;
25
+ indexPath?: string;
26
+ };
27
+ export type TreeseedBookExportManifest = {
28
+ packageKind: 'book' | 'library';
29
+ packageVersion: number;
30
+ packageId: string;
31
+ generatedAt: string;
32
+ tenantRoot: string;
33
+ tenantId: string;
34
+ book: {
35
+ id: string;
36
+ slug: string;
37
+ title: string;
38
+ order: number;
39
+ basePath: string;
40
+ downloadFileName: string;
41
+ downloadHref: string;
42
+ downloadTitle: string;
43
+ resolvedRoots: string[];
44
+ };
45
+ files: TreeseedBookExportFileEntry[];
46
+ members?: TreeseedBookExportMemberSummary[];
47
+ };
48
+ export type TreeseedBookPackageResult = {
49
+ manifest: TreeseedBookExportManifest;
50
+ markdownPath: string;
51
+ indexPath: string;
52
+ sourceFileCount: number;
53
+ includedRoots: string[];
54
+ };
55
+ export type TreeseedBookLibraryPackageResult = {
56
+ manifest: TreeseedBookExportManifest;
57
+ markdownPath: string;
58
+ indexPath: string;
59
+ memberPackages: TreeseedBookPackageResult[];
60
+ sourceFileCount: number;
61
+ includedRoots: string[];
62
+ };
63
+ export declare function buildBookExportManifest(bookId: string, options?: {
64
+ projectRoot?: string;
65
+ }): TreeseedBookExportManifest;
66
+ export declare function exportBookPackage(bookId: string, options?: {
67
+ projectRoot?: string;
68
+ }): Promise<TreeseedBookPackageResult>;
69
+ export declare function exportBookLibrary(options?: {
70
+ projectRoot?: string;
71
+ }): Promise<TreeseedBookLibraryPackageResult>;
72
+ export declare function exportTenantBookPackages(options?: {
73
+ projectRoot?: string;
74
+ }): Promise<{
75
+ projectRoot: string;
76
+ bookPackages: TreeseedBookPackageResult[];
77
+ libraryPackage: TreeseedBookLibraryPackageResult;
78
+ }>;