@treeseed/sdk 0.6.16 → 0.6.18

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 (47) hide show
  1. package/dist/db/d1.d.ts +3493 -0
  2. package/dist/db/d1.js +8 -0
  3. package/dist/db/index.d.ts +2 -0
  4. package/dist/db/index.js +2 -0
  5. package/dist/db/node-sqlite.d.ts +3544 -0
  6. package/dist/db/node-sqlite.js +119 -0
  7. package/dist/db/schema.d.ts +6272 -0
  8. package/dist/db/schema.js +231 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/operations/providers/default.js +1 -0
  12. package/dist/operations/services/commit-message-provider.d.ts +33 -1
  13. package/dist/operations/services/commit-message-provider.js +228 -51
  14. package/dist/operations/services/config-runtime.js +0 -1
  15. package/dist/operations/services/deploy.d.ts +19 -5
  16. package/dist/operations/services/deploy.js +75 -36
  17. package/dist/operations/services/github-actions-verification.d.ts +123 -0
  18. package/dist/operations/services/github-actions-verification.js +440 -0
  19. package/dist/operations/services/mailpit-runtime.d.ts +5 -0
  20. package/dist/operations/services/mailpit-runtime.js +2 -2
  21. package/dist/operations/services/repository-save-orchestrator.js +64 -8
  22. package/dist/operations/services/runtime-tools.d.ts +6 -0
  23. package/dist/operations/services/runtime-tools.js +11 -0
  24. package/dist/operations-registry.js +1 -0
  25. package/dist/platform/contracts.d.ts +6 -0
  26. package/dist/platform/deploy-config.js +17 -0
  27. package/dist/reconcile/builtin-adapters.js +2 -16
  28. package/dist/reconcile/contracts.d.ts +1 -1
  29. package/dist/reconcile/desired-state.d.ts +6 -0
  30. package/dist/reconcile/desired-state.js +1 -13
  31. package/dist/reconcile/engine.d.ts +12 -0
  32. package/dist/reconcile/state.js +2 -1
  33. package/dist/reconcile/units.js +0 -1
  34. package/dist/scripts/tenant-d1-migrate-local.js +5 -2
  35. package/dist/scripts/tenant-destroy.js +3 -1
  36. package/dist/sdk.js +2 -6
  37. package/dist/types/cloudflare.d.ts +0 -1
  38. package/dist/workflow/operations.d.ts +2 -1
  39. package/dist/workflow/operations.js +115 -35
  40. package/dist/workflow-support.d.ts +1 -0
  41. package/dist/workflow-support.js +6 -0
  42. package/dist/workflow.d.ts +24 -2
  43. package/dist/workflow.js +6 -0
  44. package/package.json +19 -5
  45. package/templates/github/deploy.workflow.yml +4 -0
  46. package/dist/wrangler-d1.d.ts +0 -25
  47. package/dist/wrangler-d1.js +0 -89
@@ -0,0 +1,440 @@
1
+ import { Buffer } from "node:buffer";
2
+ import {
3
+ createGitHubApiClient,
4
+ parseGitHubRepositorySlug
5
+ } from "./github-api.js";
6
+ function normalizeWorkflowRun(run) {
7
+ return {
8
+ id: Number(run.id ?? 0),
9
+ status: typeof run.status === "string" ? run.status : null,
10
+ conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
11
+ url: typeof run.html_url === "string" ? run.html_url : null,
12
+ headSha: typeof run.head_sha === "string" ? run.head_sha : null,
13
+ headBranch: typeof run.head_branch === "string" ? run.head_branch : null,
14
+ createdAt: typeof run.created_at === "string" ? run.created_at : null,
15
+ updatedAt: typeof run.updated_at === "string" ? run.updated_at : null
16
+ };
17
+ }
18
+ function normalizeWorkflowJobStep(step) {
19
+ return {
20
+ name: String(step.name ?? ""),
21
+ number: typeof step.number === "number" ? step.number : null,
22
+ status: typeof step.status === "string" ? step.status : null,
23
+ conclusion: typeof step.conclusion === "string" ? step.conclusion : null,
24
+ startedAt: typeof step.started_at === "string" ? step.started_at : null,
25
+ completedAt: typeof step.completed_at === "string" ? step.completed_at : null
26
+ };
27
+ }
28
+ function isSuccessfulConclusion(conclusion) {
29
+ return conclusion === "success" || conclusion === "skipped" || conclusion === "neutral";
30
+ }
31
+ function isFailedConclusion(conclusion) {
32
+ return Boolean(conclusion && !isSuccessfulConclusion(conclusion));
33
+ }
34
+ function normalizeWorkflowJob(job) {
35
+ const steps = Array.isArray(job.steps) ? job.steps.map((step) => normalizeWorkflowJobStep(step)) : [];
36
+ return {
37
+ id: Number(job.id ?? 0),
38
+ name: String(job.name ?? ""),
39
+ status: typeof job.status === "string" ? job.status : null,
40
+ conclusion: typeof job.conclusion === "string" ? job.conclusion : null,
41
+ url: typeof job.html_url === "string" ? job.html_url : null,
42
+ steps,
43
+ failedSteps: steps.filter((step) => isFailedConclusion(step.conclusion))
44
+ };
45
+ }
46
+ function workflowStateForRun(run) {
47
+ if (!run) return "missing";
48
+ if (run.status !== "completed") return "pending";
49
+ return isSuccessfulConclusion(run.conclusion) ? "success" : "failure";
50
+ }
51
+ function aggregateWorkflowState(states) {
52
+ if (states.includes("error")) return "error";
53
+ if (states.includes("not_pushed")) return "not_pushed";
54
+ if (states.includes("failure")) return "failure";
55
+ if (states.includes("missing")) return "missing";
56
+ if (states.includes("pending")) return "pending";
57
+ return "success";
58
+ }
59
+ function cappedLogExcerpt(value, maxLines) {
60
+ const text = typeof value === "string" ? value : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : Buffer.isBuffer(value) ? value.toString("utf8") : String(value ?? "");
61
+ const lines = text.split(/\r?\n/u);
62
+ return lines.slice(Math.max(0, lines.length - maxLines)).join("\n").trim();
63
+ }
64
+ async function downloadJobLogExcerpt(client, repository, jobId, maxLines) {
65
+ const { owner, name } = parseGitHubRepositorySlug(repository);
66
+ const response = await client.request("GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs", {
67
+ owner,
68
+ repo: name,
69
+ job_id: jobId
70
+ });
71
+ return cappedLogExcerpt(response.data, maxLines);
72
+ }
73
+ async function loadWorkflowJobs(client, repository, runId, options) {
74
+ const { owner, name } = parseGitHubRepositorySlug(repository);
75
+ const jobs = await client.rest.actions.listJobsForWorkflowRun({
76
+ owner,
77
+ repo: name,
78
+ run_id: runId,
79
+ per_page: 100
80
+ });
81
+ const normalized = jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
82
+ if (!options.includeLogs) {
83
+ return normalized;
84
+ }
85
+ await Promise.all(normalized.filter((job) => isFailedConclusion(job.conclusion) && job.id > 0).map(async (job) => {
86
+ try {
87
+ job.logExcerpt = await downloadJobLogExcerpt(client, repository, job.id, options.logLines);
88
+ } catch (error) {
89
+ job.logExcerpt = `Unable to fetch job log: ${error instanceof Error ? error.message : String(error)}`;
90
+ }
91
+ }));
92
+ return normalized;
93
+ }
94
+ async function resolveRemoteBranchHead(client, repository, branch) {
95
+ const { owner, name } = parseGitHubRepositorySlug(repository);
96
+ const remote = await client.rest.repos.getBranch({
97
+ owner,
98
+ repo: name,
99
+ branch
100
+ });
101
+ return remote.data.commit.sha;
102
+ }
103
+ async function findWorkflowRun(client, repository, workflow, branch, headSha) {
104
+ const { owner, name } = parseGitHubRepositorySlug(repository);
105
+ const listed = await client.rest.actions.listWorkflowRuns({
106
+ owner,
107
+ repo: name,
108
+ workflow_id: workflow,
109
+ branch: branch ?? void 0,
110
+ head_sha: headSha ?? void 0,
111
+ per_page: 20
112
+ });
113
+ const runs = listed.data.workflow_runs.map((run) => normalizeWorkflowRun(run)).filter((run) => (!headSha || run.headSha === headSha) && (!branch || run.headBranch === branch));
114
+ return runs[0] ?? null;
115
+ }
116
+ function inspectCommand(repository, runId) {
117
+ return repository && runId ? `gh run view ${runId} --repo ${repository} --log-failed` : null;
118
+ }
119
+ function workflowMessage(workflow, state, conclusion) {
120
+ switch (state) {
121
+ case "success":
122
+ return `${workflow} completed successfully.`;
123
+ case "failure":
124
+ return `${workflow} completed with conclusion ${conclusion ?? "unknown"}.`;
125
+ case "pending":
126
+ return `${workflow} is still running or queued.`;
127
+ case "missing":
128
+ return `${workflow} has no run for this branch HEAD.`;
129
+ case "not_pushed":
130
+ return `${workflow} cannot be checked because the local HEAD is not the remote branch HEAD.`;
131
+ case "error":
132
+ return `${workflow} could not be inspected.`;
133
+ }
134
+ }
135
+ function isGitHubNotFoundError(error) {
136
+ const status = typeof error?.status === "number" ? Number(error.status) : null;
137
+ const message = error instanceof Error ? error.message : String(error ?? "");
138
+ return status === 404 || /not found/iu.test(message);
139
+ }
140
+ async function inspectWorkflow(client, target, workflow, options) {
141
+ if (!target.repository || !target.branch || !target.headSha) {
142
+ return {
143
+ workflow,
144
+ state: "error",
145
+ status: null,
146
+ conclusion: null,
147
+ runId: null,
148
+ url: null,
149
+ headSha: target.headSha,
150
+ branch: target.branch,
151
+ createdAt: null,
152
+ updatedAt: null,
153
+ jobs: [],
154
+ failedJobs: [],
155
+ inspectCommand: null,
156
+ message: "Repository, branch, or head SHA is unavailable."
157
+ };
158
+ }
159
+ try {
160
+ const run = await findWorkflowRun(client, target.repository, workflow, target.branch, target.headSha);
161
+ const state = workflowStateForRun(run);
162
+ const jobs = run?.id && run.status === "completed" ? await loadWorkflowJobs(client, target.repository, run.id, options) : [];
163
+ const failedJobs = jobs.filter((job) => isFailedConclusion(job.conclusion));
164
+ return {
165
+ workflow,
166
+ state,
167
+ status: run?.status ?? null,
168
+ conclusion: run?.conclusion ?? null,
169
+ runId: run?.id ?? null,
170
+ url: run?.url ?? null,
171
+ headSha: run?.headSha ?? target.headSha,
172
+ branch: run?.headBranch ?? target.branch,
173
+ createdAt: run?.createdAt ?? null,
174
+ updatedAt: run?.updatedAt ?? null,
175
+ jobs,
176
+ failedJobs,
177
+ inspectCommand: inspectCommand(target.repository, run?.id ?? null),
178
+ message: workflowMessage(workflow, state, run?.conclusion ?? null)
179
+ };
180
+ } catch (error) {
181
+ if (isGitHubNotFoundError(error)) {
182
+ return {
183
+ workflow,
184
+ state: "missing",
185
+ status: null,
186
+ conclusion: null,
187
+ runId: null,
188
+ url: null,
189
+ headSha: target.headSha,
190
+ branch: target.branch,
191
+ createdAt: null,
192
+ updatedAt: null,
193
+ jobs: [],
194
+ failedJobs: [],
195
+ inspectCommand: null,
196
+ message: `${workflow} is missing or has no run for this branch HEAD.`
197
+ };
198
+ }
199
+ return {
200
+ workflow,
201
+ state: "error",
202
+ status: null,
203
+ conclusion: null,
204
+ runId: null,
205
+ url: null,
206
+ headSha: target.headSha,
207
+ branch: target.branch,
208
+ createdAt: null,
209
+ updatedAt: null,
210
+ jobs: [],
211
+ failedJobs: [],
212
+ inspectCommand: null,
213
+ message: error instanceof Error ? error.message : String(error)
214
+ };
215
+ }
216
+ }
217
+ function repositoryFailure(target) {
218
+ return {
219
+ type: "repository",
220
+ repository: target.repository,
221
+ repoName: target.name,
222
+ workflow: null,
223
+ runId: null,
224
+ jobId: null,
225
+ jobName: null,
226
+ state: target.state,
227
+ conclusion: null,
228
+ url: null,
229
+ inspectCommand: null,
230
+ message: target.message ?? `${target.name} requires attention.`,
231
+ failedSteps: []
232
+ };
233
+ }
234
+ function workflowFailure(repo, workflow) {
235
+ return {
236
+ type: "workflow",
237
+ repository: repo.repository,
238
+ repoName: repo.name,
239
+ workflow: workflow.workflow,
240
+ runId: workflow.runId,
241
+ jobId: null,
242
+ jobName: null,
243
+ state: workflow.state,
244
+ conclusion: workflow.conclusion,
245
+ url: workflow.url,
246
+ inspectCommand: workflow.inspectCommand,
247
+ message: workflow.message ?? `${repo.name} ${workflow.workflow} requires attention.`,
248
+ failedSteps: []
249
+ };
250
+ }
251
+ function jobFailure(repo, workflow, job) {
252
+ return {
253
+ type: "job",
254
+ repository: repo.repository,
255
+ repoName: repo.name,
256
+ workflow: workflow.workflow,
257
+ runId: workflow.runId,
258
+ jobId: job.id,
259
+ jobName: job.name,
260
+ state: workflow.state,
261
+ conclusion: job.conclusion,
262
+ url: job.url ?? workflow.url,
263
+ inspectCommand: workflow.inspectCommand,
264
+ message: `${repo.name} ${workflow.workflow} job ${job.name || job.id} completed with conclusion ${job.conclusion ?? "unknown"}.`,
265
+ failedSteps: job.failedSteps,
266
+ logExcerpt: job.logExcerpt ?? null
267
+ };
268
+ }
269
+ function collectFailures(repositories) {
270
+ const failures = [];
271
+ for (const repo of repositories) {
272
+ if (repo.state === "not_pushed" || repo.state === "error") {
273
+ failures.push(repositoryFailure(repo));
274
+ continue;
275
+ }
276
+ for (const workflow of repo.workflows) {
277
+ if (workflow.state === "failure" && workflow.failedJobs.length > 0) {
278
+ failures.push(...workflow.failedJobs.map((job) => jobFailure(repo, workflow, job)));
279
+ continue;
280
+ }
281
+ if (workflow.state === "failure" || workflow.state === "missing" || workflow.state === "error") {
282
+ failures.push(workflowFailure(repo, workflow));
283
+ }
284
+ }
285
+ }
286
+ return failures;
287
+ }
288
+ function summarize(repositories, failures) {
289
+ const workflows = repositories.flatMap((repo) => repo.workflows);
290
+ return {
291
+ repositories: repositories.length,
292
+ workflows: workflows.length,
293
+ success: workflows.filter((workflow) => workflow.state === "success").length,
294
+ failure: workflows.filter((workflow) => workflow.state === "failure").length,
295
+ pending: workflows.filter((workflow) => workflow.state === "pending").length,
296
+ missing: workflows.filter((workflow) => workflow.state === "missing").length,
297
+ notPushed: repositories.filter((repo) => repo.state === "not_pushed").length,
298
+ error: workflows.filter((workflow) => workflow.state === "error").length + repositories.filter((repo) => repo.state === "error").length,
299
+ failures: failures.length
300
+ };
301
+ }
302
+ async function inspectTarget(client, target, options) {
303
+ if (!target.repository || !target.branch || !target.headSha) {
304
+ return {
305
+ name: target.name,
306
+ repoPath: target.repoPath,
307
+ repository: target.repository,
308
+ kind: target.kind ?? "package",
309
+ branch: target.branch,
310
+ headSha: target.headSha,
311
+ remoteHeadSha: null,
312
+ remoteSynced: false,
313
+ state: "error",
314
+ message: "Repository, branch, or head SHA is unavailable.",
315
+ workflows: []
316
+ };
317
+ }
318
+ try {
319
+ const remoteHeadSha = await resolveRemoteBranchHead(client, target.repository, target.branch);
320
+ if (remoteHeadSha !== target.headSha) {
321
+ return {
322
+ name: target.name,
323
+ repoPath: target.repoPath,
324
+ repository: target.repository,
325
+ kind: target.kind ?? "package",
326
+ branch: target.branch,
327
+ headSha: target.headSha,
328
+ remoteHeadSha,
329
+ remoteSynced: false,
330
+ state: "not_pushed",
331
+ message: `Local HEAD ${target.headSha.slice(0, 12)} does not match origin/${target.branch} ${remoteHeadSha.slice(0, 12)}.`,
332
+ workflows: target.workflows.map((workflow) => ({
333
+ workflow,
334
+ state: "not_pushed",
335
+ status: null,
336
+ conclusion: null,
337
+ runId: null,
338
+ url: null,
339
+ headSha: target.headSha,
340
+ branch: target.branch,
341
+ createdAt: null,
342
+ updatedAt: null,
343
+ jobs: [],
344
+ failedJobs: [],
345
+ inspectCommand: null,
346
+ message: workflowMessage(workflow, "not_pushed", null)
347
+ }))
348
+ };
349
+ }
350
+ const workflows = await Promise.all(target.workflows.map((workflow) => inspectWorkflow(client, target, workflow, options)));
351
+ return {
352
+ name: target.name,
353
+ repoPath: target.repoPath,
354
+ repository: target.repository,
355
+ kind: target.kind ?? "package",
356
+ branch: target.branch,
357
+ headSha: target.headSha,
358
+ remoteHeadSha,
359
+ remoteSynced: true,
360
+ state: aggregateWorkflowState(workflows.map((workflow) => workflow.state)),
361
+ message: null,
362
+ workflows
363
+ };
364
+ } catch (error) {
365
+ return {
366
+ name: target.name,
367
+ repoPath: target.repoPath,
368
+ repository: target.repository,
369
+ kind: target.kind ?? "package",
370
+ branch: target.branch,
371
+ headSha: target.headSha,
372
+ remoteHeadSha: null,
373
+ remoteSynced: false,
374
+ state: "error",
375
+ message: error instanceof Error ? error.message : String(error),
376
+ workflows: []
377
+ };
378
+ }
379
+ }
380
+ async function inspectGitHubActionsVerification(targets, {
381
+ client = createGitHubApiClient(),
382
+ includeLogs = false,
383
+ logLines = 120
384
+ } = {}) {
385
+ const resolvedLogLines = Math.max(20, Math.min(1e3, Math.floor(logLines)));
386
+ const repositories = await Promise.all(targets.map((target) => inspectTarget(client, target, {
387
+ includeLogs,
388
+ logLines: resolvedLogLines
389
+ })));
390
+ const failures = collectFailures(repositories);
391
+ return {
392
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
393
+ repositories,
394
+ failures,
395
+ summary: summarize(repositories, failures)
396
+ };
397
+ }
398
+ function skippedGitHubActionsGate(gate, reason) {
399
+ return {
400
+ name: gate.name,
401
+ repository: gate.repository ?? null,
402
+ workflow: gate.workflow,
403
+ branch: gate.branch,
404
+ headSha: gate.headSha,
405
+ status: "skipped",
406
+ reason,
407
+ conclusion: null,
408
+ runId: null,
409
+ url: null
410
+ };
411
+ }
412
+ function formatGitHubActionsGateFailure(gate, result) {
413
+ const repository = String(result.repository ?? gate.repository ?? gate.name);
414
+ const runId = typeof result.runId === "number" || typeof result.runId === "string" ? String(result.runId) : "";
415
+ const url = typeof result.url === "string" && result.url ? `
416
+ ${result.url}` : "";
417
+ const failedJobs = Array.isArray(result.failedJobs) ? result.failedJobs.map((job) => typeof job?.name === "string" ? String(job.name) : "").filter(Boolean) : [];
418
+ const jobLine = failedJobs.length > 0 ? `
419
+ Failed jobs: ${failedJobs.join(", ")}` : "";
420
+ const command = runId ? `
421
+ Inspect with: gh run view ${runId} --repo ${repository} --log-failed` : "";
422
+ return `${gate.name} ${gate.workflow} completed with conclusion ${String(result.conclusion ?? "unknown")} in ${repository}.${url}${jobLine}${command}`;
423
+ }
424
+ async function waitForGitHubActionsGate(gate, options = {}) {
425
+ const { waitForGitHubWorkflowCompletion } = await import("./github-automation.js");
426
+ return await waitForGitHubWorkflowCompletion(gate.repoPath, {
427
+ repository: gate.repository,
428
+ workflow: gate.workflow,
429
+ headSha: gate.headSha,
430
+ branch: gate.branch,
431
+ timeoutSeconds: options.timeoutSeconds,
432
+ pollSeconds: options.pollSeconds
433
+ });
434
+ }
435
+ export {
436
+ formatGitHubActionsGateFailure,
437
+ inspectGitHubActionsVerification,
438
+ skippedGitHubActionsGate,
439
+ waitForGitHubActionsGate
440
+ };
@@ -1,3 +1,8 @@
1
+ export type TreeseedMailpitContainer = {
2
+ name: string;
3
+ image: string;
4
+ ports: string;
5
+ };
1
6
  export declare function dockerIsAvailable(): boolean;
2
7
  export declare function findRunningMailpitContainer(): any;
3
8
  export declare function stopKnownMailpitContainers(): boolean;
@@ -35,11 +35,11 @@ function stopKnownMailpitContainers() {
35
35
  if (!container) {
36
36
  return true;
37
37
  }
38
- const stopResult = runDocker(["stop", container.name], { stdio: "inherit" });
38
+ const stopResult = runDocker(["stop", container.name]);
39
39
  if (stopResult.status !== 0) {
40
40
  return false;
41
41
  }
42
- const removeResult = runDocker(["rm", "-f", container.name], { stdio: "inherit" });
42
+ const removeResult = runDocker(["rm", "-f", container.name]);
43
43
  return removeResult.status === 0;
44
44
  }
45
45
  function streamKnownMailpitLogs() {
@@ -505,6 +505,9 @@ async function commitMessageFor(node, options, context) {
505
505
  provider: options.commitMessageProvider
506
506
  });
507
507
  }
508
+ function commitSubject(message) {
509
+ return String(message ?? "").split(/\r?\n/u)[0]?.trim() || null;
510
+ }
508
511
  function gitDiffSummary(repoDir) {
509
512
  const changedFiles = run("git", ["status", "--porcelain"], { cwd: repoDir, capture: true });
510
513
  const diff = run("git", ["diff", "--cached"], { cwd: repoDir, capture: true });
@@ -884,19 +887,68 @@ function ensurePackageTagReady(node, options, tagName, branch, workflowRunId) {
884
887
  }
885
888
  return message;
886
889
  }
887
- function refreshSubmodulePointers(node, finalizedCommits) {
888
- let changed = false;
889
- for (const [repoName] of finalizedCommits.entries()) {
890
+ function treeCommitForPath(repoDir, ref, path) {
891
+ try {
892
+ const output = run("git", ["ls-tree", ref, "--", path], { cwd: repoDir, capture: true }).trim();
893
+ const match = output.match(/^\d+\s+commit\s+([0-9a-f]{40})\t/u);
894
+ return match?.[1] ?? null;
895
+ } catch {
896
+ return null;
897
+ }
898
+ }
899
+ function collectSubmodulePointerChanges(node, finalizedCommits) {
900
+ const changes = [];
901
+ for (const [repoName, finalizedCommit] of finalizedCommits.entries()) {
890
902
  const childRelativePath = node.id === "." ? repoName : repoName.startsWith(`${node.id}/`) ? repoName.slice(node.id.length + 1) : null;
891
903
  if (!childRelativePath) continue;
892
904
  const childPath = resolve(node.path, childRelativePath);
893
905
  if (!existsSync(childPath) || !isGitRepo(childPath)) continue;
894
906
  const status = run("git", ["status", "--porcelain", "--", childRelativePath], { cwd: node.path, capture: true });
895
- if (status.trim().length > 0) {
896
- changed = true;
907
+ const oldSha = treeCommitForPath(node.path, "HEAD", childRelativePath);
908
+ const newSha = finalizedCommit || headCommit(childPath);
909
+ if (status.trim().length > 0 || oldSha && newSha && oldSha !== newSha) {
910
+ changes.push({
911
+ path: childRelativePath,
912
+ oldSha,
913
+ newSha
914
+ });
897
915
  }
898
916
  }
899
- return changed;
917
+ return changes;
918
+ }
919
+ function commitContextDependencyUpdates(updates) {
920
+ return updates.map((update) => ({
921
+ packageName: update.packageName,
922
+ field: update.field,
923
+ from: update.from,
924
+ to: update.to,
925
+ tagName: update.tagName
926
+ }));
927
+ }
928
+ function commitContextPackageChanges(node, state, submodulePointers) {
929
+ const pointersByPath = new Map(submodulePointers.map((pointer) => [pointer.path, pointer]));
930
+ const changes = [];
931
+ for (const [relativePath, report] of state.reports.entries()) {
932
+ if (relativePath === node.id || relativePath === ".") continue;
933
+ const childRelativePath = node.id === "." ? relativePath : relativePath.startsWith(`${node.id}/`) ? relativePath.slice(node.id.length + 1) : null;
934
+ if (!childRelativePath) continue;
935
+ const pointer = pointersByPath.get(childRelativePath);
936
+ if (!pointer && !report.committed && !report.tagName && !report.dependencySpec) continue;
937
+ changes.push({
938
+ name: report.name,
939
+ path: childRelativePath,
940
+ oldSha: pointer?.oldSha ?? null,
941
+ newSha: pointer?.newSha ?? report.commitSha,
942
+ tagName: report.tagName,
943
+ version: report.version,
944
+ dependencySpec: report.dependencySpec,
945
+ commitSubject: commitSubject(report.commitMessage)
946
+ });
947
+ if (pointer) {
948
+ pointer.packageName = report.name;
949
+ }
950
+ }
951
+ return changes;
900
952
  }
901
953
  function syncBranchBeforeSave(node, options, branch) {
902
954
  checkoutOrCreateBranch(node, options, branch);
@@ -1201,7 +1253,8 @@ async function saveOneRepository(node, options, state) {
1201
1253
  const dependencyUpdates = updateDependencyReferences(node, state.finalizedReferences);
1202
1254
  const dependencyChanged = dependencyUpdates.length > 0;
1203
1255
  const gitDependencyRefreshSpecs = dependencyUpdates.map((update) => state.finalizedReferences.get(update.packageName)).filter((reference) => Boolean(reference) && reference.mode === "dev-git-tag").map((reference) => `${reference.packageName}@${reference.installSpec ?? reference.spec}`);
1204
- const submodulesChanged = refreshSubmodulePointers(node, state.finalizedCommits);
1256
+ const submodulePointers = collectSubmodulePointerChanges(node, state.finalizedCommits);
1257
+ const submodulesChanged = submodulePointers.length > 0;
1205
1258
  const packageNeedsVersion = node.kind === "package" && (hasMeaningfulChanges(node.path) || dependencyChanged || submodulesChanged);
1206
1259
  let plannedVersion = null;
1207
1260
  if (packageNeedsVersion) {
@@ -1294,7 +1347,10 @@ async function saveOneRepository(node, options, state) {
1294
1347
  changedFiles,
1295
1348
  diff,
1296
1349
  plannedVersion: plannedVersion ?? report.version,
1297
- plannedTag: node.plannedTag ?? report.tagName
1350
+ plannedTag: node.plannedTag ?? report.tagName,
1351
+ dependencyUpdates: commitContextDependencyUpdates(dependencyUpdates),
1352
+ submodulePointers,
1353
+ packageChanges: commitContextPackageChanges(node, state, submodulePointers)
1298
1354
  });
1299
1355
  report.commitMessage = messageResult.message;
1300
1356
  report.commitMessageProvider = messageResult.provider;
@@ -109,6 +109,9 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
109
109
  rootDir: string | undefined;
110
110
  publicBaseUrl: string | undefined;
111
111
  localBaseUrl: string | undefined;
112
+ local: {
113
+ runtime: string | undefined;
114
+ } | undefined;
112
115
  environments: {
113
116
  local: {
114
117
  baseUrl: string | undefined;
@@ -160,6 +163,9 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
160
163
  provider: string | undefined;
161
164
  rootDir: string | undefined;
162
165
  publicBaseUrl: string | undefined;
166
+ local: {
167
+ runtime: string | undefined;
168
+ } | undefined;
163
169
  cloudflare: {
164
170
  workerName: string | undefined;
165
171
  };
@@ -112,6 +112,15 @@ function parseServiceEnvironmentConfig(value) {
112
112
  railwayEnvironment: optionalString(record.railwayEnvironment)
113
113
  };
114
114
  }
115
+ function parseLocalRuntimeConfig(value, label) {
116
+ const record = optionalRecord(value, label);
117
+ if (!record) {
118
+ return void 0;
119
+ }
120
+ return {
121
+ runtime: optionalEnum(record.runtime ?? record.runtime_mode ?? record.runtimeMode, `${label}.runtime`, ["auto", "provider", "local"])
122
+ };
123
+ }
115
124
  function parseManagedServiceConfig(value, label) {
116
125
  const record = optionalRecord(value, label);
117
126
  if (!record) {
@@ -125,6 +134,7 @@ function parseManagedServiceConfig(value, label) {
125
134
  provider: optionalString(record.provider),
126
135
  rootDir: optionalString(record.rootDir),
127
136
  publicBaseUrl: optionalString(record.publicBaseUrl),
137
+ local: parseLocalRuntimeConfig(record.local, `${label}.local`),
128
138
  cloudflare: {
129
139
  workerName: optionalString(cloudflare.workerName)
130
140
  },
@@ -212,6 +222,7 @@ function parseSurfaceConfig(value, label) {
212
222
  rootDir: optionalString(record.rootDir),
213
223
  publicBaseUrl: optionalString(record.publicBaseUrl),
214
224
  localBaseUrl: optionalString(record.localBaseUrl),
225
+ local: parseLocalRuntimeConfig(record.local, `${label}.local`),
215
226
  environments: (() => {
216
227
  const environments = optionalRecord(record.environments, `${label}.environments`);
217
228
  if (!environments) {
@@ -4,6 +4,7 @@ function operation(spec) {
4
4
  const workspaceCommand = (name) => `workspace${":"}${name}`;
5
5
  const TRESEED_OPERATION_SPECS = [
6
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"] }),
7
+ operation({ id: "workspace.ci", name: "ci", aliases: [], group: "Validation", summary: "Inspect remote GitHub Actions status for the active workspace commits.", description: "Check the latest GitHub Actions workflow runs for the current branch heads across market and checked-out package repositories, including failed jobs and inspect commands.", provider: "default", related: ["status", "save", "stage"] }),
7
8
  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"] }),
8
9
  operation({ id: "branch.switch", name: "switch", aliases: [], group: "Workflow", summary: "Create or resume a recursive task branch.", description: "Create or resume the task branch in market and any checked-out package repos, with optional preview provisioning.", provider: "default", related: ["tasks", "dev", "save"] }),
9
10
  operation({ id: "branch.save", name: "save", aliases: [], group: "Workflow", summary: "Recursively verify, commit, and push the current task checkpoint.", description: "Save dirty package repos in dependency order, then verify, commit, push, and optionally refresh preview for market.", provider: "default", related: ["switch", "stage", "status"] }),
@@ -122,6 +122,7 @@ export interface TreeseedPlatformSurfaceConfig {
122
122
  rootDir?: string;
123
123
  publicBaseUrl?: string;
124
124
  localBaseUrl?: string;
125
+ local?: TreeseedLocalRuntimeConfig;
125
126
  environments?: Partial<Record<'local' | 'staging' | 'prod', TreeseedManagedServiceEnvironmentConfig>>;
126
127
  cache?: TreeseedWebSurfaceCacheConfig;
127
128
  }
@@ -182,6 +183,10 @@ export interface TreeseedManagedServiceEnvironmentConfig {
182
183
  domain?: string;
183
184
  railwayEnvironment?: string;
184
185
  }
186
+ export type TreeseedLocalRuntimeMode = 'auto' | 'provider' | 'local';
187
+ export interface TreeseedLocalRuntimeConfig {
188
+ runtime?: TreeseedLocalRuntimeMode;
189
+ }
185
190
  export interface TreeseedManagedServiceCloudflareConfig {
186
191
  workerName?: string;
187
192
  }
@@ -207,6 +212,7 @@ export interface TreeseedManagedServiceConfig {
207
212
  publicBaseUrl?: string;
208
213
  cloudflare?: TreeseedManagedServiceCloudflareConfig;
209
214
  railway?: TreeseedManagedServiceRailwayConfig;
215
+ local?: TreeseedLocalRuntimeConfig;
210
216
  environments?: Partial<Record<'local' | 'staging' | 'prod', TreeseedManagedServiceEnvironmentConfig>>;
211
217
  }
212
218
  export interface TreeseedManagedServicesConfig {