@treeseed/sdk 0.6.6 → 0.6.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.
Files changed (48) hide show
  1. package/dist/copilot.d.ts +15 -0
  2. package/dist/copilot.js +75 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +18 -0
  5. package/dist/managed-dependencies.d.ts +56 -0
  6. package/dist/managed-dependencies.js +668 -0
  7. package/dist/operations/providers/default.js +30 -1
  8. package/dist/operations/services/commit-message-provider.d.ts +33 -0
  9. package/dist/operations/services/commit-message-provider.js +319 -0
  10. package/dist/operations/services/config-runtime.js +41 -20
  11. package/dist/operations/services/git-remote-policy.d.ts +9 -0
  12. package/dist/operations/services/git-remote-policy.js +55 -0
  13. package/dist/operations/services/git-workflow.js +22 -3
  14. package/dist/operations/services/github-api.js +9 -4
  15. package/dist/operations/services/knowledge-coop-launch.js +4 -0
  16. package/dist/operations/services/local-dev.js +7 -2
  17. package/dist/operations/services/package-reference-policy.d.ts +70 -0
  18. package/dist/operations/services/package-reference-policy.js +314 -0
  19. package/dist/operations/services/project-platform.d.ts +4 -0
  20. package/dist/operations/services/project-platform.js +30 -4
  21. package/dist/operations/services/railway-deploy.d.ts +4 -1
  22. package/dist/operations/services/railway-deploy.js +76 -38
  23. package/dist/operations/services/repository-save-orchestrator.d.ts +172 -0
  24. package/dist/operations/services/repository-save-orchestrator.js +1462 -0
  25. package/dist/operations/services/workspace-dependency-mode.d.ts +70 -0
  26. package/dist/operations/services/workspace-dependency-mode.js +404 -0
  27. package/dist/operations/services/workspace-preflight.js +5 -0
  28. package/dist/operations/services/workspace-save.js +10 -6
  29. package/dist/operations-registry.js +5 -0
  30. package/dist/operations-types.d.ts +1 -0
  31. package/dist/platform/books-data.js +4 -1
  32. package/dist/platform/env.yaml +6 -3
  33. package/dist/reconcile/builtin-adapters.js +37 -7
  34. package/dist/scripts/cleanup-markdown.js +4 -0
  35. package/dist/scripts/publish-package.js +5 -0
  36. package/dist/scripts/tenant-workflow-action.js +11 -2
  37. package/dist/verification.js +24 -12
  38. package/dist/workflow/operations.d.ts +381 -55
  39. package/dist/workflow/operations.js +718 -258
  40. package/dist/workflow-state.d.ts +40 -1
  41. package/dist/workflow-state.js +220 -17
  42. package/dist/workflow-support.d.ts +3 -0
  43. package/dist/workflow-support.js +34 -0
  44. package/dist/workflow.d.ts +19 -3
  45. package/dist/workflow.js +3 -3
  46. package/dist/wrangler-d1.js +6 -1
  47. package/package.json +17 -1
  48. package/templates/github/deploy.workflow.yml +28 -13
@@ -0,0 +1,70 @@
1
+ export type DependencyResolutionMode = 'local-workspace' | 'git-dev' | 'stable-registry';
2
+ export type WorkspaceLinksMode = 'auto' | 'off';
3
+ type WorkspaceLink = {
4
+ packageName: string;
5
+ linkPath: string;
6
+ targetPath: string;
7
+ scope: 'root' | 'package';
8
+ ownerPath: string;
9
+ };
10
+ export type WorkspaceDependencyModeReport = {
11
+ mode: DependencyResolutionMode;
12
+ enabled: boolean;
13
+ root: string;
14
+ links: Array<WorkspaceLink & {
15
+ exists: boolean;
16
+ linked: boolean;
17
+ targetMatches: boolean;
18
+ currentTarget: string | null;
19
+ }>;
20
+ created: string[];
21
+ removed: string[];
22
+ issues: string[];
23
+ };
24
+ export type DeploymentLockfileWorkspaceIssue = {
25
+ filePath: string;
26
+ packageName: string;
27
+ reason: string;
28
+ };
29
+ export declare function discoverWorkspaceLinks(root?: any): WorkspaceLink[];
30
+ export declare function ensureLocalWorkspaceLinks(root?: any, options?: {
31
+ mode?: WorkspaceLinksMode;
32
+ env?: NodeJS.ProcessEnv;
33
+ }): {
34
+ created: string[];
35
+ removed: never[];
36
+ mode: DependencyResolutionMode;
37
+ enabled: boolean;
38
+ root: string;
39
+ links: Array<WorkspaceLink & {
40
+ exists: boolean;
41
+ linked: boolean;
42
+ targetMatches: boolean;
43
+ currentTarget: string | null;
44
+ }>;
45
+ issues: string[];
46
+ };
47
+ export declare function unlinkLocalWorkspaceLinks(root?: any, options?: {
48
+ mode?: WorkspaceLinksMode;
49
+ env?: NodeJS.ProcessEnv;
50
+ }): {
51
+ enabled: boolean;
52
+ removed: string[];
53
+ mode: DependencyResolutionMode;
54
+ root: string;
55
+ links: Array<WorkspaceLink & {
56
+ exists: boolean;
57
+ linked: boolean;
58
+ targetMatches: boolean;
59
+ currentTarget: string | null;
60
+ }>;
61
+ created: string[];
62
+ issues: string[];
63
+ };
64
+ export declare function collectDeploymentLockfileWorkspaceIssues(root?: any): DeploymentLockfileWorkspaceIssue[];
65
+ export declare function assertNoWorkspaceLinksInDeploymentLockfiles(root?: any): void;
66
+ export declare function inspectWorkspaceDependencyMode(root?: any, options?: {
67
+ mode?: WorkspaceLinksMode;
68
+ env?: NodeJS.ProcessEnv;
69
+ }): WorkspaceDependencyModeReport;
70
+ export {};
@@ -0,0 +1,404 @@
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { workspacePackages, workspaceRoot } from "./workspace-tools.js";
4
+ const METADATA_VERSION = 1;
5
+ const INTERNAL_DEPENDENCY_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"];
6
+ function metadataPath(root) {
7
+ return resolve(root, ".treeseed", "workspace-links.json");
8
+ }
9
+ function workspaceLinksEnabled(mode = "auto", env = process.env) {
10
+ if (mode === "off") return false;
11
+ const envMode = String(env.TREESEED_WORKSPACE_LINKS ?? "auto").trim().toLowerCase();
12
+ return envMode !== "off" && envMode !== "false" && envMode !== "0";
13
+ }
14
+ function readJson(filePath) {
15
+ return JSON.parse(readFileSync(filePath, "utf8"));
16
+ }
17
+ function packageDirName(packageName) {
18
+ const parts = packageName.split("/");
19
+ return parts.at(-1) ?? packageName;
20
+ }
21
+ function linkPathFor(ownerPath, packageName) {
22
+ const scope = packageName.startsWith("@") ? packageName.split("/")[0] : null;
23
+ const name = packageDirName(packageName);
24
+ return scope ? resolve(ownerPath, "node_modules", scope, name) : resolve(ownerPath, "node_modules", name);
25
+ }
26
+ function safeLstat(path) {
27
+ try {
28
+ return lstatSync(path);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function safeReadlink(path) {
34
+ try {
35
+ const link = readlinkSync(path);
36
+ return resolve(dirname(path), link);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ function pathKey(path) {
42
+ return resolve(path);
43
+ }
44
+ function readMetadata(root) {
45
+ const filePath = metadataPath(root);
46
+ if (!existsSync(filePath)) return /* @__PURE__ */ new Set();
47
+ try {
48
+ const value = readJson(filePath);
49
+ const links = Array.isArray(value.links) ? value.links : [];
50
+ return new Set(links.map((entry) => entry && typeof entry === "object" ? String(entry.linkPath ?? "") : "").filter(Boolean).map(pathKey));
51
+ } catch {
52
+ return /* @__PURE__ */ new Set();
53
+ }
54
+ }
55
+ function writeMetadata(root, links) {
56
+ const filePath = metadataPath(root);
57
+ mkdirSync(dirname(filePath), { recursive: true });
58
+ writeFileSync(filePath, `${JSON.stringify({
59
+ version: METADATA_VERSION,
60
+ root,
61
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
62
+ links
63
+ }, null, 2)}
64
+ `, "utf8");
65
+ }
66
+ function gitInfoExcludePath(repoPath) {
67
+ const gitPath = resolve(repoPath, ".git");
68
+ const stat = safeLstat(gitPath);
69
+ if (!stat) return null;
70
+ if (stat.isDirectory()) {
71
+ return resolve(gitPath, "info", "exclude");
72
+ }
73
+ if (stat.isFile()) {
74
+ try {
75
+ const content = readFileSync(gitPath, "utf8").trim();
76
+ const match = /^gitdir:\s*(.+)$/iu.exec(content);
77
+ if (!match) return null;
78
+ const gitDir = resolve(repoPath, match[1]);
79
+ return resolve(gitDir, "info", "exclude");
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ function ensureGitInfoExcludes(root, links) {
87
+ const patternsByRepo = /* @__PURE__ */ new Map();
88
+ const addPattern = (repoPath, pattern) => {
89
+ const set = patternsByRepo.get(repoPath) ?? /* @__PURE__ */ new Set();
90
+ set.add(pattern.replaceAll("\\", "/"));
91
+ patternsByRepo.set(repoPath, set);
92
+ };
93
+ addPattern(root, ".treeseed/workspace-links.json");
94
+ for (const link of links) {
95
+ addPattern(link.ownerPath, relative(link.ownerPath, link.linkPath));
96
+ }
97
+ for (const [repoPath, patterns] of patternsByRepo) {
98
+ const excludePath = gitInfoExcludePath(repoPath);
99
+ if (!excludePath) continue;
100
+ mkdirSync(dirname(excludePath), { recursive: true });
101
+ const current = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
102
+ const currentLines = new Set(current.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean));
103
+ const missing = [...patterns].filter((pattern) => !currentLines.has(pattern));
104
+ if (missing.length === 0) continue;
105
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
106
+ writeFileSync(excludePath, `${current}${prefix}${missing.join("\n")}
107
+ `, "utf8");
108
+ }
109
+ }
110
+ function internalDependencyNames(packageJson, packageNames) {
111
+ const names = /* @__PURE__ */ new Set();
112
+ for (const field of INTERNAL_DEPENDENCY_FIELDS) {
113
+ const deps = packageJson[field];
114
+ if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
115
+ for (const name of Object.keys(deps)) {
116
+ if (packageNames.has(name)) names.add(name);
117
+ }
118
+ }
119
+ return [...names].sort();
120
+ }
121
+ function dependencyNames(packageJson, filter) {
122
+ const names = /* @__PURE__ */ new Set();
123
+ if (!packageJson) return names;
124
+ for (const field of INTERNAL_DEPENDENCY_FIELDS) {
125
+ const deps = packageJson[field];
126
+ if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
127
+ for (const name of Object.keys(deps)) {
128
+ if (filter(name)) names.add(name);
129
+ }
130
+ }
131
+ return names;
132
+ }
133
+ function dependencySpec(packageJson, field, packageName) {
134
+ const deps = packageJson[field];
135
+ if (!deps || typeof deps !== "object" || Array.isArray(deps)) return null;
136
+ const value = deps[packageName];
137
+ return typeof value === "string" ? value : null;
138
+ }
139
+ function normalizedPathValue(value) {
140
+ return value.replaceAll("\\", "/").replace(/^\.\//u, "").replace(/\/$/u, "");
141
+ }
142
+ function rootLockfileAllowsWorkspaceLink(root, filePath, packageName, entry, workspacePackageByName) {
143
+ if (filePath !== resolve(root, "package-lock.json")) return false;
144
+ const workspacePackage = workspacePackageByName.get(packageName);
145
+ if (!workspacePackage) return false;
146
+ return normalizedPathValue(String(entry.resolved ?? "")) === normalizedPathValue(workspacePackage.relativeDir);
147
+ }
148
+ function collectPackageLockConsistencyIssues(filePath, packageJson, packages, workspacePackageByName) {
149
+ const issues = [];
150
+ const rootLockEntry = packages[""];
151
+ const declaredWorkspaces = Array.isArray(packageJson.workspaces) ? packageJson.workspaces.map(String) : [];
152
+ if (declaredWorkspaces.length > 0) {
153
+ const lockWorkspaces = Array.isArray(rootLockEntry?.workspaces) ? rootLockEntry.workspaces.map(String) : [];
154
+ if (JSON.stringify(lockWorkspaces) !== JSON.stringify(declaredWorkspaces)) {
155
+ issues.push({
156
+ filePath,
157
+ packageName: String(packageJson.name ?? "(root)"),
158
+ reason: `root-workspaces-mismatch:package.json=${JSON.stringify(declaredWorkspaces)} lockfile=${JSON.stringify(lockWorkspaces)}`
159
+ });
160
+ }
161
+ }
162
+ for (const [packageName, workspacePackage] of workspacePackageByName) {
163
+ const relativeDir = normalizedPathValue(workspacePackage.relativeDir);
164
+ const packageEntry = packages[relativeDir];
165
+ if (!packageEntry) {
166
+ issues.push({ filePath, packageName, reason: `missing-workspace-package-entry:${relativeDir}` });
167
+ continue;
168
+ }
169
+ if (packageEntry.name !== packageName) {
170
+ issues.push({ filePath, packageName, reason: `workspace-package-name-mismatch:${relativeDir}` });
171
+ }
172
+ const expectedVersion = workspacePackage.packageJson.version;
173
+ if (typeof expectedVersion === "string" && packageEntry.version !== expectedVersion) {
174
+ issues.push({ filePath, packageName, reason: `workspace-package-version-mismatch:${packageEntry.version ?? "(missing)"}!=${expectedVersion}` });
175
+ }
176
+ const linkEntry = packages[`node_modules/${packageName}`];
177
+ if (!linkEntry || linkEntry.link !== true || normalizedPathValue(String(linkEntry.resolved ?? "")) !== relativeDir) {
178
+ issues.push({ filePath, packageName, reason: `workspace-link-entry-mismatch:${relativeDir}` });
179
+ }
180
+ }
181
+ for (const field of INTERNAL_DEPENDENCY_FIELDS) {
182
+ for (const packageName of workspacePackageByName.keys()) {
183
+ const manifestSpec = dependencySpec(packageJson, field, packageName);
184
+ if (!manifestSpec) continue;
185
+ const lockSpec = dependencySpec(rootLockEntry ?? {}, field, packageName);
186
+ if (lockSpec !== manifestSpec) {
187
+ issues.push({
188
+ filePath,
189
+ packageName,
190
+ reason: `root-dependency-spec-mismatch:${field}:${lockSpec ?? "(missing)"}!=${manifestSpec}`
191
+ });
192
+ }
193
+ }
194
+ }
195
+ return issues;
196
+ }
197
+ function discoverWorkspaceLinks(root = workspaceRoot()) {
198
+ if (!existsSync(resolve(root, "package.json"))) {
199
+ return [];
200
+ }
201
+ const packages = workspacePackages(root).filter((pkg) => typeof pkg.name === "string" && pkg.name.startsWith("@treeseed/"));
202
+ const packageByName = new Map(packages.map((pkg) => [String(pkg.name), pkg]));
203
+ const packageNames = new Set(packageByName.keys());
204
+ const links = [];
205
+ for (const pkg of packages) {
206
+ links.push({
207
+ packageName: String(pkg.name),
208
+ linkPath: linkPathFor(root, String(pkg.name)),
209
+ targetPath: pkg.dir,
210
+ scope: "root",
211
+ ownerPath: root
212
+ });
213
+ }
214
+ for (const owner of packages) {
215
+ for (const dependencyName of internalDependencyNames(owner.packageJson, packageNames)) {
216
+ const dependency = packageByName.get(dependencyName);
217
+ if (!dependency) continue;
218
+ links.push({
219
+ packageName: dependencyName,
220
+ linkPath: linkPathFor(owner.dir, dependencyName),
221
+ targetPath: dependency.dir,
222
+ scope: "package",
223
+ ownerPath: owner.dir
224
+ });
225
+ }
226
+ }
227
+ return links;
228
+ }
229
+ function isInstalledTreeseedPackage(path, packageName) {
230
+ try {
231
+ const packageJson = readJson(resolve(path, "package.json"));
232
+ return packageJson.name === packageName;
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+ function isEmptyDirectory(path) {
238
+ try {
239
+ return safeLstat(path)?.isDirectory() === true && readdirSync(path).length === 0;
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
244
+ function removeLinkCandidate(link, managedLinks) {
245
+ const stat = safeLstat(link.linkPath);
246
+ if (!stat) return true;
247
+ const currentTarget = stat.isSymbolicLink() ? safeReadlink(link.linkPath) : null;
248
+ const managed = managedLinks.has(pathKey(link.linkPath)) || currentTarget === pathKey(link.targetPath);
249
+ if (stat.isSymbolicLink()) {
250
+ if (!managed && currentTarget !== pathKey(link.targetPath)) {
251
+ throw new Error(`Refusing to remove unmanaged workspace link ${link.linkPath}.`);
252
+ }
253
+ unlinkSync(link.linkPath);
254
+ return true;
255
+ }
256
+ if (isInstalledTreeseedPackage(link.linkPath, link.packageName)) {
257
+ rmSync(link.linkPath, { recursive: true, force: true });
258
+ return true;
259
+ }
260
+ if (isEmptyDirectory(link.linkPath)) {
261
+ rmSync(link.linkPath, { recursive: true, force: true });
262
+ return true;
263
+ }
264
+ throw new Error(`Refusing to replace unmanaged path ${link.linkPath}.`);
265
+ }
266
+ function createLink(link) {
267
+ mkdirSync(dirname(link.linkPath), { recursive: true });
268
+ const relativeTarget = relative(dirname(link.linkPath), link.targetPath) || ".";
269
+ symlinkSync(relativeTarget, link.linkPath, process.platform === "win32" ? "junction" : "dir");
270
+ }
271
+ function ensureLocalWorkspaceLinks(root = workspaceRoot(), options = {}) {
272
+ const enabled = workspaceLinksEnabled(options.mode, options.env);
273
+ const links = discoverWorkspaceLinks(root);
274
+ const report = inspectWorkspaceDependencyMode(root, { mode: options.mode, env: options.env });
275
+ if (!enabled) return { ...report, enabled: false, created: [], removed: [] };
276
+ ensureGitInfoExcludes(root, links);
277
+ const managedLinks = readMetadata(root);
278
+ const created = [];
279
+ for (const link of links) {
280
+ const stat = safeLstat(link.linkPath);
281
+ const currentTarget = stat?.isSymbolicLink() ? safeReadlink(link.linkPath) : null;
282
+ if (stat?.isSymbolicLink() && currentTarget === pathKey(link.targetPath)) continue;
283
+ removeLinkCandidate(link, managedLinks);
284
+ createLink(link);
285
+ created.push(link.linkPath);
286
+ }
287
+ writeMetadata(root, links);
288
+ return {
289
+ ...inspectWorkspaceDependencyMode(root, { mode: options.mode, env: options.env }),
290
+ created,
291
+ removed: []
292
+ };
293
+ }
294
+ function unlinkLocalWorkspaceLinks(root = workspaceRoot(), options = {}) {
295
+ const enabled = workspaceLinksEnabled(options.mode, options.env);
296
+ const links = discoverWorkspaceLinks(root);
297
+ const managedLinks = readMetadata(root);
298
+ const removed = [];
299
+ if (!enabled) return { ...inspectWorkspaceDependencyMode(root, { mode: options.mode, env: options.env }), enabled: false, removed };
300
+ for (const link of links) {
301
+ const stat = safeLstat(link.linkPath);
302
+ if (!stat) continue;
303
+ const currentTarget = stat.isSymbolicLink() ? safeReadlink(link.linkPath) : null;
304
+ const managed = managedLinks.has(pathKey(link.linkPath)) || currentTarget === pathKey(link.targetPath);
305
+ if (!stat.isSymbolicLink() || !managed) continue;
306
+ unlinkSync(link.linkPath);
307
+ removed.push(link.linkPath);
308
+ }
309
+ return {
310
+ ...inspectWorkspaceDependencyMode(root, { mode: options.mode, env: options.env }),
311
+ removed,
312
+ created: []
313
+ };
314
+ }
315
+ function collectDeploymentLockfileWorkspaceIssues(root = workspaceRoot()) {
316
+ if (!existsSync(resolve(root, "package.json"))) {
317
+ return [];
318
+ }
319
+ const rootPackageJson = readJson(resolve(root, "package.json"));
320
+ const workspacePkgs = workspacePackages(root).filter((pkg) => typeof pkg.name === "string" && pkg.name.startsWith("@treeseed/"));
321
+ const workspacePackageByName = new Map(workspacePkgs.map((pkg) => [String(pkg.name), {
322
+ relativeDir: pkg.relativeDir,
323
+ packageJson: pkg.packageJson
324
+ }]));
325
+ const packageNames = workspacePackageByName.size > 0 ? new Set(workspacePackageByName.keys()) : dependencyNames(rootPackageJson, (name) => name.startsWith("@treeseed/"));
326
+ const lockRoots = workspacePkgs.length > 0 ? [root, ...workspacePkgs.map((pkg) => pkg.dir)] : [root];
327
+ const issues = [];
328
+ for (const dir of lockRoots) {
329
+ const filePath = resolve(dir, "package-lock.json");
330
+ if (!existsSync(filePath)) continue;
331
+ let lock;
332
+ try {
333
+ lock = readJson(filePath);
334
+ } catch {
335
+ continue;
336
+ }
337
+ const packages = lock.packages && typeof lock.packages === "object" && !Array.isArray(lock.packages) ? lock.packages : {};
338
+ if (dir === root && workspacePackageByName.size > 0) {
339
+ issues.push(...collectPackageLockConsistencyIssues(filePath, rootPackageJson, packages, workspacePackageByName));
340
+ }
341
+ const checkedPackageNames = dir === root ? packageNames : dependencyNames(
342
+ existsSync(resolve(dir, "package.json")) ? readJson(resolve(dir, "package.json")) : null,
343
+ (name) => name.startsWith("@treeseed/")
344
+ );
345
+ const lockPackageNames = new Set(Object.keys(packages).map((key) => /^node_modules\/(@treeseed\/[^/]+)$/u.exec(key)?.[1] ?? null).filter((name) => Boolean(name)));
346
+ for (const packageName of /* @__PURE__ */ new Set([...packageNames, ...checkedPackageNames, ...lockPackageNames])) {
347
+ const entry = packages[`node_modules/${packageName}`];
348
+ if (!entry) continue;
349
+ const resolvedValue = String(entry.resolved ?? "");
350
+ if (entry.link === true) {
351
+ if (!rootLockfileAllowsWorkspaceLink(root, filePath, packageName, entry, workspacePackageByName)) {
352
+ issues.push({ filePath, packageName, reason: "workspace-link-lock-entry" });
353
+ }
354
+ } else if (/^(?:\.\.?\/|packages\/|file:)/u.test(resolvedValue)) {
355
+ issues.push({ filePath, packageName, reason: `local-lock-resolved:${resolvedValue}` });
356
+ }
357
+ }
358
+ }
359
+ return issues.filter((issue, index, all) => all.findIndex((candidate) => candidate.filePath === issue.filePath && candidate.packageName === issue.packageName && candidate.reason === issue.reason) === index);
360
+ }
361
+ function assertNoWorkspaceLinksInDeploymentLockfiles(root = workspaceRoot()) {
362
+ const issues = collectDeploymentLockfileWorkspaceIssues(root);
363
+ if (issues.length === 0) return;
364
+ throw new Error(`Deployment lockfile validation failed.
365
+ ${issues.map((issue) => `${issue.filePath}: ${issue.packageName} ${issue.reason}`).join("\n")}`);
366
+ }
367
+ function inspectWorkspaceDependencyMode(root = workspaceRoot(), options = {}) {
368
+ const enabled = workspaceLinksEnabled(options.mode, options.env);
369
+ const links = discoverWorkspaceLinks(root);
370
+ const inspected = links.map((link) => {
371
+ const stat = safeLstat(link.linkPath);
372
+ const currentTarget = stat?.isSymbolicLink() ? safeReadlink(link.linkPath) : null;
373
+ const targetMatches = currentTarget === pathKey(link.targetPath);
374
+ return {
375
+ ...link,
376
+ exists: Boolean(stat),
377
+ linked: stat?.isSymbolicLink() === true,
378
+ targetMatches,
379
+ currentTarget
380
+ };
381
+ });
382
+ const lockIssues = collectDeploymentLockfileWorkspaceIssues(root).map((issue) => `${issue.filePath}: ${issue.packageName} ${issue.reason}`);
383
+ const allLinked = inspected.length > 0 && inspected.every((link) => link.linked && link.targetMatches);
384
+ return {
385
+ mode: allLinked ? "local-workspace" : "git-dev",
386
+ enabled,
387
+ root,
388
+ links: inspected,
389
+ created: [],
390
+ removed: [],
391
+ issues: [
392
+ ...inspected.filter((link) => link.exists && (!link.linked || !link.targetMatches)).map((link) => `${link.linkPath} is not linked to ${link.targetPath}.`),
393
+ ...lockIssues
394
+ ]
395
+ };
396
+ }
397
+ export {
398
+ assertNoWorkspaceLinksInDeploymentLockfiles,
399
+ collectDeploymentLockfileWorkspaceIssues,
400
+ discoverWorkspaceLinks,
401
+ ensureLocalWorkspaceLinks,
402
+ inspectWorkspaceDependencyMode,
403
+ unlinkLocalWorkspaceLinks
404
+ };
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { collectTreeseedConfigSeedValues } from "./config-runtime.js";
5
5
  import { createTempDir } from "./workspace-tools.js";
6
+ import { resolveTreeseedToolBinary } from "../../managed-dependencies.js";
6
7
  function runCapture(command, args, options = {}) {
7
8
  const result = spawnSync(command, args, {
8
9
  cwd: options.cwd ?? process.cwd(),
@@ -20,6 +21,10 @@ function runCapture(command, args, options = {}) {
20
21
  };
21
22
  }
22
23
  function locateBinary(candidate) {
24
+ const managed = resolveTreeseedToolBinary(candidate);
25
+ if (managed) {
26
+ return managed;
27
+ }
23
28
  const result = runCapture("bash", ["-lc", `command -v ${candidate}`]);
24
29
  return result.status === 0 ? result.stdout.trim() : null;
25
30
  }
@@ -3,14 +3,15 @@ import { resolve } from "node:path";
3
3
  import { changedWorkspacePackages, publishableWorkspacePackages, run, sortWorkspacePackages, workspacePackages, workspaceRoot } from "./workspace-tools.js";
4
4
  const MERGE_CONFLICT_EXIT_CODE = 12;
5
5
  function parseSemver(version) {
6
- const match = String(version).trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
6
+ const match = String(version).trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/);
7
7
  if (!match) {
8
8
  throw new Error(`Unsupported version "${version}". Expected x.y.z.`);
9
9
  }
10
10
  return {
11
11
  major: Number(match[1]),
12
12
  minor: Number(match[2]),
13
- patch: Number(match[3])
13
+ patch: Number(match[3]),
14
+ prerelease: String(version).includes("-")
14
15
  };
15
16
  }
16
17
  function incrementPatchVersion(version) {
@@ -26,6 +27,9 @@ function incrementVersion(version, level = "patch") {
26
27
  return `${parsed.major}.${parsed.minor + 1}.0`;
27
28
  }
28
29
  if (level === "patch") {
30
+ if (parsed.prerelease) {
31
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
32
+ }
29
33
  return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
30
34
  }
31
35
  throw new Error(`Unsupported release bump "${level}". Expected major, minor, or patch.`);
@@ -71,7 +75,7 @@ function planWorkspaceVersionChanges(root = workspaceRoot()) {
71
75
  if (!versions.has(depName)) {
72
76
  continue;
73
77
  }
74
- const nextSpec = `^${versions.get(depName)}`;
78
+ const nextSpec = `${versions.get(depName)}`;
75
79
  if (pkg.packageJson[field][depName] === nextSpec) {
76
80
  continue;
77
81
  }
@@ -143,7 +147,7 @@ function planWorkspaceReleaseBump(level = "patch", root = workspaceRoot(), optio
143
147
  if (!versions.has(depName)) {
144
148
  continue;
145
149
  }
146
- pkg.packageJson[field][depName] = `^${versions.get(depName)}`;
150
+ pkg.packageJson[field][depName] = `${versions.get(depName)}`;
147
151
  touched.add(pkg.name);
148
152
  }
149
153
  }
@@ -169,7 +173,7 @@ function collectWorkspaceVersionConsistencyIssues(root = workspaceRoot()) {
169
173
  if (!versions.has(depName)) {
170
174
  continue;
171
175
  }
172
- const expectedSpec = `^${versions.get(depName)}`;
176
+ const expectedSpec = `${versions.get(depName)}`;
173
177
  if (currentSpec !== expectedSpec) {
174
178
  issues.push({
175
179
  packageName: pkg.name,
@@ -246,7 +250,7 @@ function collectMergeConflictReport(repoDir) {
246
250
  }
247
251
  function formatMergeConflictReport(report, repoDir, targetBranch = "main") {
248
252
  const lines = [
249
- `Treeseed save failed due to merge conflicts during \`git pull --rebase origin ${targetBranch}\`.`,
253
+ `Treeseed save failed due to merge conflicts during \`git pull --rebase --recurse-submodules=no origin ${targetBranch}\`.`,
250
254
  `Repository root: ${repoDir}`,
251
255
  `Branch: ${report.branch}`,
252
256
  `Rebase in progress: ${report.rebaseInProgress ? "yes" : "no"}`,
@@ -1,6 +1,7 @@
1
1
  function operation(spec) {
2
2
  return spec;
3
3
  }
4
+ const workspaceCommand = (name) => `workspace${":"}${name}`;
4
5
  const TRESEED_OPERATION_SPECS = [
5
6
  operation({ id: "workspace.status", name: "status", aliases: [], group: "Workflow", summary: "Show Treeseed project health and the current task state.", description: "Report branch/task state, runtime readiness, preview/deploy state, auth readiness, and recommended next operations.", provider: "default", related: ["tasks", "switch", "config"] }),
6
7
  operation({ id: "branch.tasks", name: "tasks", aliases: [], group: "Workflow", summary: "List task branches plus package branch alignment.", description: "List task branches from market and checked-out package repos, including preview metadata and branch/pointer alignment state.", provider: "default", related: ["status", "switch", "close"] }),
@@ -10,8 +11,12 @@ const TRESEED_OPERATION_SPECS = [
10
11
  operation({ id: "branch.stage", name: "stage", aliases: [], group: "Workflow", summary: "Squash a task branch into staging across market and packages.", description: "Auto-save if needed, squash-merge package task branches into staging first, update market submodule pointers, then squash-merge market into staging and clean up the task branch.", provider: "default", related: ["save", "release", "close"] }),
11
12
  operation({ id: "workspace.resume", name: "resume", aliases: [], group: "Workflow", summary: "Resume an interrupted workflow run.", description: "Continue a failed journaled workflow run from its next incomplete step after validating current workspace preconditions.", provider: "default", related: ["recover", "status"] }),
12
13
  operation({ id: "workspace.recover", name: "recover", aliases: [], group: "Workflow", summary: "Inspect active workflow locks and interrupted runs.", description: "List active workflow locks, resumable interrupted runs, and the exact commands needed to continue or recover the workspace state.", provider: "default", related: ["resume", "status"] }),
14
+ operation({ id: "workspace.links.status", name: workspaceCommand("status"), aliases: [], group: "Utilities", summary: "Inspect local workspace dependency links.", description: "Inspect whether Treeseed package dependencies are linked to local package checkouts or resolved through deployment references.", provider: "default", related: ["dev", "save"] }),
15
+ operation({ id: "workspace.links.link", name: workspaceCommand("link"), aliases: [], group: "Utilities", summary: "Link local Treeseed workspace packages.", description: "Create local node_modules links from internal Treeseed dependencies to checked-out package repositories for integrated development.", provider: "default", related: ["dev", workspaceCommand("status")] }),
16
+ operation({ id: "workspace.links.unlink", name: workspaceCommand("unlink"), aliases: [], group: "Utilities", summary: "Remove managed local workspace package links.", description: "Remove Treeseed-managed local workspace links before deployment-style installs or lockfile repair.", provider: "default", related: ["save", workspaceCommand("status")] }),
13
17
  operation({ id: "deploy.rollback", name: "rollback", aliases: [], group: "Workflow", summary: "Roll back staging or production to a recorded deployment.", description: "Redeploy a previously recorded staging or production commit using a temporary checkout of that revision.", provider: "default", related: ["status", "release"] }),
14
18
  operation({ id: "workspace.doctor", name: "doctor", aliases: [], group: "Validation", summary: "Diagnose Treeseed tooling, auth, and workflow readiness.", description: "Collect doctor-style diagnostics for workspace readiness and optional safe repairs.", provider: "default", related: ["status", "config"] }),
19
+ operation({ id: "workspace.install", name: "install", aliases: [], group: "Utilities", summary: "Install Treeseed-managed local dependencies.", description: "Install or repair Treeseed-managed CLI dependencies including GitHub CLI, Wrangler, Railway, Copilot, and optional gh-act support.", provider: "default", related: ["config", "doctor"] }),
15
20
  operation({ id: "auth.login", name: "auth:login", aliases: [], group: "Validation", summary: "Authenticate against the configured Treeseed API.", description: "Start the device login flow against the active Treeseed API host and persist the returned session locally.", provider: "default", related: ["auth:check", "auth:whoami", "auth:logout"] }),
16
21
  operation({ id: "auth.logout", name: "auth:logout", aliases: [], group: "Validation", summary: "Clear locally stored Treeseed API credentials.", description: "Remove the persisted local device-flow session for the active Treeseed API host.", provider: "default", related: ["auth:login", "auth:whoami"] }),
17
22
  operation({ id: "auth.whoami", name: "auth:whoami", aliases: [], group: "Validation", summary: "Inspect the active Treeseed API identity.", description: "Use the persisted local remote session to query the active Treeseed API principal.", provider: "default", related: ["auth:login", "status"] }),
@@ -28,6 +28,7 @@ export type TreeseedOperationResult<TPayload = Record<string, unknown>> = {
28
28
  exitCode?: number;
29
29
  stdout?: string[];
30
30
  stderr?: string[];
31
+ report?: Record<string, unknown> | null;
31
32
  };
32
33
  export type TreeseedOperationWriter = (output: string, stream?: 'stdout' | 'stderr') => void;
33
34
  export type TreeseedOperationPrompt = (message: string) => Promise<string> | string;
@@ -1,4 +1,4 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  import { getTenantContentRoot } from "./tenant-config.js";
@@ -26,6 +26,9 @@ function sortPaths(paths) {
26
26
  return [...paths].sort((left, right) => left.localeCompare(right, void 0, { numeric: true, sensitivity: "base" }));
27
27
  }
28
28
  function collectMarkdownFiles(rootPath) {
29
+ if (!existsSync(rootPath)) {
30
+ return [];
31
+ }
29
32
  const stats = statSync(rootPath);
30
33
  if (stats.isFile()) {
31
34
  return [rootPath];
@@ -90,13 +90,14 @@ entries:
90
90
  CLOUDFLARE_API_TOKEN:
91
91
  label: Cloudflare API token
92
92
  group: auth
93
- description: Account-level Cloudflare API token used by Wrangler and CI to manage Pages, Workers, Workers KV, D1, Queues, DNS, secrets, and deploys.
94
- howToGet: "Create a Cloudflare account-level API token scoped to the target domain and account. Required permissions: Account Cloudflare Pages edit, Account Workers Scripts edit, Account Workers KV Storage edit, Account D1 edit, Account Queues edit, Zone DNS edit. Then paste it here as CLOUDFLARE_API_TOKEN."
93
+ description: Account-level Cloudflare API token used by Wrangler, Workers AI, and CI to manage Pages, Workers, Workers KV, D1, Queues, DNS, secrets, and deploys.
94
+ howToGet: "Create a Cloudflare account-level API token scoped to the target domain and account. Required permissions: Account Cloudflare Pages edit, Account Workers Scripts edit, Account Workers KV Storage edit, Account D1 edit, Account Queues edit, Zone DNS edit, Workers AI Read, and Workers AI Edit for model execution. If TREESEED_COMMIT_AI_GATEWAY_ID is configured, also grant AI Gateway Read/Run access for the gateway. Then paste it here as CLOUDFLARE_API_TOKEN."
95
95
  sensitivity: secret
96
96
  targets:
97
97
  - local-runtime
98
98
  - github-secret
99
99
  scopes:
100
+ - local
100
101
  - staging
101
102
  - prod
102
103
  storage: shared
@@ -167,14 +168,16 @@ entries:
167
168
  CLOUDFLARE_ACCOUNT_ID:
168
169
  label: Cloudflare account ID
169
170
  group: cloudflare
170
- description: Identifies the Cloudflare account Treeseed should provision and deploy into.
171
+ description: Identifies the Cloudflare account Treeseed should provision, deploy into, and use for Workers AI commit message generation.
171
172
  howToGet: In the Cloudflare dashboard, open Workers & Pages or Account Home and copy the account ID.
172
173
  startupProfile: core
173
174
  sensitivity: plain
174
175
  targets:
176
+ - local-runtime
175
177
  - github-variable
176
178
  - config-file
177
179
  scopes:
180
+ - local
178
181
  - staging
179
182
  - prod
180
183
  storage: shared