@treeseed/sdk 0.6.7 → 0.6.9
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/copilot.d.ts +15 -0
- package/dist/copilot.js +75 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/managed-dependencies.d.ts +56 -0
- package/dist/managed-dependencies.js +668 -0
- package/dist/operations/providers/default.js +30 -1
- package/dist/operations/services/commit-message-provider.d.ts +33 -0
- package/dist/operations/services/commit-message-provider.js +319 -0
- package/dist/operations/services/config-runtime.js +50 -23
- package/dist/operations/services/git-remote-policy.d.ts +9 -0
- package/dist/operations/services/git-remote-policy.js +55 -0
- package/dist/operations/services/git-workflow.js +22 -3
- package/dist/operations/services/github-api.js +9 -4
- package/dist/operations/services/knowledge-coop-launch.js +4 -0
- package/dist/operations/services/local-dev.js +7 -2
- package/dist/operations/services/package-reference-policy.d.ts +70 -0
- package/dist/operations/services/package-reference-policy.js +330 -0
- package/dist/operations/services/project-platform.d.ts +4 -0
- package/dist/operations/services/project-platform.js +28 -4
- package/dist/operations/services/railway-deploy.d.ts +4 -1
- package/dist/operations/services/railway-deploy.js +76 -38
- package/dist/operations/services/repository-save-orchestrator.d.ts +172 -0
- package/dist/operations/services/repository-save-orchestrator.js +1462 -0
- package/dist/operations/services/workspace-dependency-mode.d.ts +70 -0
- package/dist/operations/services/workspace-dependency-mode.js +404 -0
- package/dist/operations/services/workspace-preflight.js +5 -0
- package/dist/operations/services/workspace-save.js +10 -6
- package/dist/operations-registry.js +5 -0
- package/dist/operations-types.d.ts +1 -0
- package/dist/platform/books-data.js +4 -1
- package/dist/platform/env.yaml +6 -3
- package/dist/reconcile/builtin-adapters.js +37 -7
- package/dist/scripts/cleanup-markdown.js +4 -0
- package/dist/scripts/prepare.js +14 -0
- package/dist/scripts/publish-package.js +5 -0
- package/dist/scripts/tenant-workflow-action.js +11 -2
- package/dist/verification.js +46 -13
- package/dist/workflow/operations.d.ts +381 -55
- package/dist/workflow/operations.js +725 -261
- package/dist/workflow-state.d.ts +40 -1
- package/dist/workflow-state.js +220 -17
- package/dist/workflow-support.d.ts +3 -0
- package/dist/workflow-support.js +34 -0
- package/dist/workflow.d.ts +19 -3
- package/dist/workflow.js +3 -3
- package/dist/wrangler-d1.js +6 -1
- package/package.json +17 -1
- package/templates/github/deploy.workflow.yml +59 -14
|
@@ -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 =
|
|
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] =
|
|
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 =
|
|
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];
|
package/dist/platform/env.yaml
CHANGED
|
@@ -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
|
|
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
|