@treeseed/sdk 0.10.11 → 0.10.13

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 (52) hide show
  1. package/README.md +2 -2
  2. package/dist/api/auth/d1-store.js +20 -1
  3. package/dist/capacity-provider.d.ts +53 -1
  4. package/dist/capacity.d.ts +80 -1
  5. package/dist/capacity.js +687 -8
  6. package/dist/db/d1.d.ts +109 -3227
  7. package/dist/db/index.d.ts +1 -0
  8. package/dist/db/index.js +1 -0
  9. package/dist/db/market-schema.d.ts +43769 -0
  10. package/dist/db/market-schema.js +1878 -0
  11. package/dist/db/node-sqlite.d.ts +109 -3227
  12. package/dist/db/schema.d.ts +226 -5757
  13. package/dist/db/schema.js +35 -226
  14. package/dist/index.d.ts +6 -4
  15. package/dist/index.js +32 -0
  16. package/dist/market-client.d.ts +135 -0
  17. package/dist/market-client.js +134 -1
  18. package/dist/operations/services/commit-message-provider.js +1 -1
  19. package/dist/operations/services/d1-migration.js +0 -59
  20. package/dist/operations/services/deploy.js +5 -1
  21. package/dist/operations/services/github-api.d.ts +83 -0
  22. package/dist/operations/services/github-api.js +167 -0
  23. package/dist/operations/services/local-dev.js +3 -3
  24. package/dist/operations/services/mailpit-runtime.d.ts +13 -2
  25. package/dist/operations/services/mailpit-runtime.js +19 -14
  26. package/dist/operations/services/project-web-monitor.d.ts +15 -0
  27. package/dist/operations/services/project-web-monitor.js +260 -0
  28. package/dist/operations/services/railway-api.js +2 -2
  29. package/dist/operations/services/release-candidate.js +9 -9
  30. package/dist/operations/services/runtime-paths.d.ts +1 -1
  31. package/dist/operations/services/runtime-paths.js +2 -2
  32. package/dist/operations/services/template-registry.js +10 -1
  33. package/dist/operations.d.ts +1 -0
  34. package/dist/operations.js +11 -1
  35. package/dist/platform-operation-store.d.ts +4 -0
  36. package/dist/platform-operation-store.js +29 -3
  37. package/dist/platform-operations.d.ts +8 -0
  38. package/dist/platform-operations.js +19 -0
  39. package/dist/remote.js +6 -6
  40. package/dist/scripts/tenant-d1-migrate-local.js +2 -3
  41. package/dist/scripts/test-cloudflare-local.js +1 -1
  42. package/dist/scripts/workspace-command-e2e.js +3 -1
  43. package/dist/sdk-types.d.ts +281 -3
  44. package/dist/sdk-types.js +5 -1
  45. package/dist/seeds/normalize.js +6 -0
  46. package/dist/seeds/schema.js +61 -1
  47. package/dist/seeds/types.d.ts +32 -0
  48. package/drizzle/d1/0000_treeseed_d1.sql +37 -0
  49. package/drizzle/market/0000_market_control_plane.sql +2929 -0
  50. package/drizzle/market/0001_capacity_budget_mode_default.sql +4 -0
  51. package/drizzle/market/0002_user_email_addresses.sql +26 -0
  52. package/package.json +8 -1
@@ -0,0 +1,15 @@
1
+ import { type GitHubApiClient } from './github-api.ts';
2
+ import type { ProjectDeploymentEnvironment, ProjectWebMonitorResult } from '../../sdk-types.ts';
3
+ export declare function buildProjectWebMonitorResult(input: {
4
+ environment: ProjectDeploymentEnvironment;
5
+ action?: string | null;
6
+ repository?: Record<string, unknown> | null;
7
+ workflowFile?: string | null;
8
+ target?: Record<string, unknown> | null;
9
+ externalWorkflow?: Record<string, unknown> | null;
10
+ workflowResult?: Record<string, any> | null;
11
+ githubClient?: GitHubApiClient | null;
12
+ fetchImpl?: typeof fetch | null;
13
+ mockExternal?: boolean;
14
+ dryRun?: boolean;
15
+ }): Promise<ProjectWebMonitorResult>;
@@ -0,0 +1,260 @@
1
+ import {
2
+ getGitHubWorkflowFileStatus,
3
+ getLatestGitHubWorkflowRun
4
+ } from "./github-api.js";
5
+ const FORBIDDEN_MONITOR_FIELDS = /* @__PURE__ */ new Set([
6
+ "capacityProviderId",
7
+ "laneId",
8
+ "grantId",
9
+ "workerPoolId",
10
+ "runtimeHostId",
11
+ "railwayServiceId",
12
+ "runnerToken"
13
+ ]);
14
+ function text(value, fallback = "") {
15
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
16
+ }
17
+ function objectValue(value) {
18
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
19
+ }
20
+ function repositorySlug(repository) {
21
+ const record = objectValue(repository);
22
+ const owner = text(record.owner);
23
+ const name = text(record.name);
24
+ return owner && name ? `${owner}/${name}` : null;
25
+ }
26
+ function externalUrl(...values) {
27
+ for (const value of values) {
28
+ const raw = text(value);
29
+ if (!raw) continue;
30
+ try {
31
+ const url = new URL(raw);
32
+ if (url.protocol === "http:" || url.protocol === "https:") return url.href;
33
+ } catch {
34
+ continue;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ function redact(value) {
40
+ if (Array.isArray(value)) return value.map((entry) => redact(entry));
41
+ if (!value || typeof value !== "object") return value;
42
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !FORBIDDEN_MONITOR_FIELDS.has(key)).filter(([key]) => !/(?:secret|token|password|apiKey|privateKey)/iu.test(key)).map(([key, entry]) => [key, redact(entry)]));
43
+ }
44
+ function check(input) {
45
+ return redact(input);
46
+ }
47
+ function workflowStatusFromConclusion(conclusion) {
48
+ if (conclusion === "success") return "passed";
49
+ if (conclusion === "cancelled" || conclusion === "failure" || conclusion === "timed_out") return "failed";
50
+ if (conclusion) return "warning";
51
+ return "warning";
52
+ }
53
+ function statusFromChecks(checks) {
54
+ if (checks.some((entry) => entry.status === "failed")) return "failed";
55
+ if (checks.some((entry) => entry.status === "warning")) return "degraded";
56
+ if (checks.some((entry) => entry.status === "passed")) return "healthy";
57
+ return "unknown";
58
+ }
59
+ function workflowRunCheck(input) {
60
+ const run = input.workflowResult?.runId ? input.workflowResult : input.latestWorkflowRun;
61
+ if (!run) {
62
+ return check({
63
+ key: "latest_workflow",
64
+ label: "Latest workflow",
65
+ status: "skipped",
66
+ source: "github",
67
+ summary: input.repository ? "No workflow run was recorded for this deployment yet." : "Repository is unavailable, so workflow state was skipped."
68
+ });
69
+ }
70
+ const conclusion = text(run.conclusion, "");
71
+ const runStatus = text(run.status, "");
72
+ const status = runStatus === "completed" ? workflowStatusFromConclusion(conclusion) : "warning";
73
+ const runId = Number(run.runId ?? run.id);
74
+ const inspectCommand = input.repository && Number.isFinite(runId) && runId > 0 ? `gh run view ${runId} --repo ${input.repository} --log-failed` : void 0;
75
+ return check({
76
+ key: "latest_workflow",
77
+ label: "Latest workflow",
78
+ status,
79
+ source: "github",
80
+ summary: status === "passed" ? `${input.workflowFile} completed successfully.` : runStatus === "completed" ? `${input.workflowFile} completed with conclusion ${conclusion || "unknown"}.` : `${input.workflowFile} is ${runStatus || "active"}.`,
81
+ url: externalUrl(run.url, run.runUrl),
82
+ ...inspectCommand ? { inspectCommand } : {}
83
+ });
84
+ }
85
+ async function workflowFileCheck(input) {
86
+ if (!input.repository) {
87
+ return check({
88
+ key: "workflow_file",
89
+ label: "Workflow file",
90
+ status: "failed",
91
+ source: "github",
92
+ summary: "GitHub repository is not configured."
93
+ });
94
+ }
95
+ if (input.mockExternal || input.dryRun) {
96
+ return check({
97
+ key: "workflow_file",
98
+ label: "Workflow file",
99
+ status: "passed",
100
+ source: "github",
101
+ summary: `${input.workflowFile} is assumed present in mock mode.`
102
+ });
103
+ }
104
+ if (!input.githubClient) {
105
+ return check({
106
+ key: "workflow_file",
107
+ label: "Workflow file",
108
+ status: "warning",
109
+ source: "github",
110
+ summary: "GitHub credentials are unavailable, so workflow file presence could not be verified."
111
+ });
112
+ }
113
+ const status = await getGitHubWorkflowFileStatus(input.repository, input.workflowFile, { client: input.githubClient }).catch((error) => ({
114
+ ok: false,
115
+ exists: null,
116
+ repository: input.repository,
117
+ workflow: input.workflowFile,
118
+ url: null,
119
+ message: error instanceof Error ? error.message : String(error)
120
+ }));
121
+ return check({
122
+ key: "workflow_file",
123
+ label: "Workflow file",
124
+ status: status.exists === true ? "passed" : status.exists === false ? "failed" : "warning",
125
+ source: "github",
126
+ summary: status.message,
127
+ ...status.url ? { url: status.url } : {}
128
+ });
129
+ }
130
+ async function fetchWithTimeout(fetchImpl, url, timeoutMs) {
131
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
132
+ return await fetchImpl(url, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
133
+ }
134
+ return await fetchImpl(url, { method: "GET" });
135
+ }
136
+ async function httpCheck(input) {
137
+ if (!input.url) {
138
+ return check({
139
+ key: "http_response",
140
+ label: "HTTP response",
141
+ status: "skipped",
142
+ source: "http",
143
+ summary: "No public URL is available for HTTP probing."
144
+ });
145
+ }
146
+ if (input.mockExternal || input.dryRun || !input.fetchImpl) {
147
+ return check({
148
+ key: "http_response",
149
+ label: "HTTP response",
150
+ status: input.mockExternal ? "passed" : "skipped",
151
+ source: "http",
152
+ url: input.url,
153
+ summary: input.mockExternal ? "Mock HTTP probe passed." : "HTTP probe skipped for this monitor run."
154
+ });
155
+ }
156
+ try {
157
+ const response = await fetchWithTimeout(input.fetchImpl, input.url, input.timeoutMs ?? 1e4);
158
+ const status = response.status >= 200 && response.status < 400 ? "passed" : response.status === 401 || response.status === 403 || response.status >= 500 ? "warning" : "failed";
159
+ return check({
160
+ key: "http_response",
161
+ label: "HTTP response",
162
+ status,
163
+ source: "http",
164
+ url: input.url,
165
+ summary: `HTTP probe returned ${response.status}.`
166
+ });
167
+ } catch (error) {
168
+ return check({
169
+ key: "http_response",
170
+ label: "HTTP response",
171
+ status: "failed",
172
+ source: "http",
173
+ url: input.url,
174
+ summary: error instanceof Error ? error.message : "HTTP probe failed."
175
+ });
176
+ }
177
+ }
178
+ async function buildProjectWebMonitorResult(input) {
179
+ const repository = repositorySlug(input.repository);
180
+ const workflowFile = text(input.workflowFile ?? input.repository?.workflowFile, "deploy-web.yml");
181
+ const target = objectValue(input.target);
182
+ const targetUrl = externalUrl(target.url, target.baseUrl, target.previewUrl, target.lastDeploymentUrl);
183
+ const latestWorkflowRun = !input.workflowResult && repository && input.githubClient ? await getLatestGitHubWorkflowRun(repository, {
184
+ client: input.githubClient,
185
+ workflow: workflowFile,
186
+ branch: text(input.repository?.branch, "") || null
187
+ }).catch(() => null) : null;
188
+ const checks = [
189
+ workflowRunCheck({
190
+ workflowResult: input.workflowResult ?? input.externalWorkflow ?? null,
191
+ latestWorkflowRun,
192
+ repository,
193
+ workflowFile
194
+ }),
195
+ await workflowFileCheck({
196
+ repository,
197
+ workflowFile,
198
+ githubClient: input.githubClient,
199
+ mockExternal: input.mockExternal,
200
+ dryRun: input.dryRun
201
+ }),
202
+ check({
203
+ key: "web_host",
204
+ label: "Web host",
205
+ status: Object.keys(target).length > 0 ? "passed" : "failed",
206
+ source: "market",
207
+ summary: Object.keys(target).length > 0 ? "Web host target is configured." : "Web host target is missing."
208
+ }),
209
+ check({
210
+ key: "target_url",
211
+ label: "Public URL",
212
+ status: targetUrl ? "passed" : "failed",
213
+ source: "market",
214
+ ...targetUrl ? { url: targetUrl } : {},
215
+ summary: targetUrl ? "Deployment target URL is recorded." : "Deployment target URL is missing."
216
+ }),
217
+ await httpCheck({
218
+ url: targetUrl,
219
+ fetchImpl: input.fetchImpl,
220
+ mockExternal: input.mockExternal,
221
+ dryRun: input.dryRun
222
+ }),
223
+ check({
224
+ key: "content_publish",
225
+ label: "Content publish",
226
+ status: input.action === "publish_content" ? "passed" : "skipped",
227
+ source: "sdk",
228
+ summary: input.action === "publish_content" ? "Content publish workflow completed." : "Content publish was not part of this action."
229
+ }),
230
+ check({
231
+ key: "d1_migration",
232
+ label: "D1 migration",
233
+ status: "skipped",
234
+ source: "sdk",
235
+ summary: "No D1 migration result was reported for this deployment."
236
+ }),
237
+ check({
238
+ key: "form_api_route",
239
+ label: "Form/API route",
240
+ status: "skipped",
241
+ source: "http",
242
+ summary: "No form/API route probe was configured for this project."
243
+ })
244
+ ];
245
+ const status = statusFromChecks(checks);
246
+ const warnings = checks.filter((entry) => entry.status === "warning").map((entry) => entry.summary);
247
+ const urls = [...new Set(checks.map((entry) => entry.url).filter((url) => Boolean(url)))];
248
+ const result = {
249
+ environment: input.environment,
250
+ status,
251
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
252
+ checks,
253
+ urls,
254
+ warnings
255
+ };
256
+ return redact(result);
257
+ }
258
+ export {
259
+ buildProjectWebMonitorResult
260
+ };
@@ -283,8 +283,8 @@ async function railwayGraphqlRequest({
283
283
  apiToken,
284
284
  apiUrl,
285
285
  fetchImpl = fetch,
286
- timeoutMs = 15e3,
287
- retries = 5
286
+ timeoutMs = 3e4,
287
+ retries = 8
288
288
  }) {
289
289
  const token = apiToken || resolveRailwayApiToken(env);
290
290
  if (!token) {
@@ -543,30 +543,30 @@ async function configParityChecks(root, failures) {
543
543
  };
544
544
  }
545
545
  function migrationCompatibilityChecks(root, failures) {
546
- if (!existsSync(resolve(root, "migrations"))) {
546
+ if (!existsSync(resolve(root, "packages/sdk/package.json"))) {
547
547
  return {
548
548
  name: "migration-compatibility",
549
549
  status: "skipped",
550
- detail: "No migrations directory is present in this workspace."
550
+ detail: "No SDK package checkout is present in this workspace."
551
551
  };
552
552
  }
553
- const requiredMigrations = [
554
- "migrations/0007_site_web_sessions.sql",
555
- "migrations/0014_better_auth_integer_timestamps.sql"
553
+ const requiredArtifacts = [
554
+ "packages/sdk/drizzle/d1/0000_treeseed_d1.sql",
555
+ "packages/sdk/drizzle/market/0000_market_control_plane.sql"
556
556
  ];
557
- const missing = requiredMigrations.filter((path) => !existsSync(resolve(root, path)));
557
+ const missing = requiredArtifacts.filter((path) => !existsSync(resolve(root, path)));
558
558
  for (const path of missing) {
559
559
  addFailure(failures, {
560
- code: "missing_migration_fixture",
560
+ code: "missing_drizzle_migration_artifact",
561
561
  scope: "@treeseed/market",
562
- message: `Migration compatibility check is missing ${path}.`,
562
+ message: `Drizzle migration compatibility check is missing ${path}.`,
563
563
  details: { path }
564
564
  });
565
565
  }
566
566
  return {
567
567
  name: "migration-compatibility",
568
568
  status: missing.length > 0 ? "failed" : "passed",
569
- detail: "Checked required legacy web session and Better Auth migration coverage."
569
+ detail: "Checked required Drizzle migration artifacts for Market PostgreSQL and SDK D1."
570
570
  };
571
571
  }
572
572
  async function runReleaseCandidateGate(input) {
@@ -11,7 +11,7 @@ export declare const servicesRoot: string;
11
11
  export declare const mailpitComposeFile: string;
12
12
  export declare const fixtureRoot: string;
13
13
  export declare const fixtureWranglerConfig: string;
14
- export declare const fixtureMigrationsRoot: string;
14
+ export declare const sdkD1MigrationsRoot: string;
15
15
  export declare const fixtureSrcRoot: string;
16
16
  export declare const templateCatalogRoot: string;
17
17
  export declare const localTemplateArtifactsRoot: string;
@@ -25,7 +25,7 @@ const servicesRoot = resolve(cliRuntimeRoot, "services");
25
25
  const mailpitComposeFile = resolve(servicesRoot, "compose.yml");
26
26
  const fixtureRoot = resolve(corePackageRoot, "fixture");
27
27
  const fixtureWranglerConfig = resolve(fixtureRoot, "wrangler.toml");
28
- const fixtureMigrationsRoot = resolve(fixtureRoot, "migrations");
28
+ const sdkD1MigrationsRoot = resolve(sdkPackageRoot, "drizzle", "d1");
29
29
  const fixtureSrcRoot = resolve(fixtureRoot, "src");
30
30
  const templateCatalogRoot = resolve(cliRuntimeRoot, "template-catalog");
31
31
  const localTemplateArtifactsRoot = resolve(templateCatalogRoot, "templates");
@@ -40,7 +40,6 @@ export {
40
40
  corePackageRoot,
41
41
  corePackageVersion,
42
42
  examplesRoot,
43
- fixtureMigrationsRoot,
44
43
  fixtureRoot,
45
44
  fixtureSrcRoot,
46
45
  fixtureWranglerConfig,
@@ -49,6 +48,7 @@ export {
49
48
  mailpitComposeFile,
50
49
  packageRoot,
51
50
  referenceAppsRoot,
51
+ sdkD1MigrationsRoot,
52
52
  sdkPackageVersion,
53
53
  servicesRoot,
54
54
  templateCatalogRoot,
@@ -126,11 +126,20 @@ function runGit(commandArgs, cwd) {
126
126
  throw new Error(result.stderr?.trim() || result.stdout?.trim() || `git ${commandArgs.join(" ")} failed`);
127
127
  }
128
128
  }
129
+ function readGitOutput(commandArgs, cwd) {
130
+ const result = spawnSync("git", commandArgs, {
131
+ cwd,
132
+ stdio: "pipe",
133
+ encoding: "utf8"
134
+ });
135
+ return result.status === 0 ? result.stdout.trim() : null;
136
+ }
129
137
  function materializeGitTemplateSource(product, options) {
130
138
  const cacheRoot = resolveTemplateSourceCacheRoot(product, options);
131
139
  const repoRoot = resolve(cacheRoot, "repo");
132
140
  const source = product.fulfillment.source;
133
- if (!existsSync(resolve(repoRoot, ".git"))) {
141
+ const cachedOrigin = existsSync(resolve(repoRoot, ".git")) ? readGitOutput(["config", "--get", "remote.origin.url"], repoRoot) : null;
142
+ if (cachedOrigin !== source.repoUrl) {
134
143
  rmSync(cacheRoot, { recursive: true, force: true });
135
144
  mkdirSync(cacheRoot, { recursive: true });
136
145
  runGit(["clone", "--no-checkout", source.repoUrl, repoRoot]);
@@ -2,6 +2,7 @@ export { TRESEED_OPERATION_SPECS, findTreeseedOperation, listTreeseedOperationNa
2
2
  export { collectTreeseedConfigSeedValues } from './operations/services/config-runtime.ts';
3
3
  export { createKnowledgeHubRepositories, defaultHubContentResolutionPolicy, executeKnowledgeHubLaunch, normalizeKnowledgeHubLaunchIntent, planKnowledgeHubLaunch, planKnowledgeHubRepositories, validateRepositoryHost, } from './operations/services/hub-launch.ts';
4
4
  export { TreeseedOperationsSdk } from './operations/runtime.ts';
5
+ export { cancelGitHubWorkflowRun, dispatchGitHubWorkflowRun, formatGitHubWorkflowFailure, waitForGitHubWorkflowRunCompletion, type GitHubWorkflowCancellationResult, type GitHubWorkflowDispatchResult, type GitHubWorkflowFailureSummary, type GitHubWorkflowFailureSummaryInput, type GitHubWorkflowProgressEvent, } from './operations/services/github-api.ts';
5
6
  export type { HubContentResolutionPolicy, KnowledgeHubLaunchIntent, KnowledgeHubLaunchPhase, KnowledgeHubLaunchPlan, KnowledgeHubLaunchResult, KnowledgeHubRepositoryPlan, RepositoryHost, } from './operations/services/hub-launch.ts';
6
7
  export type { TreeseedOperationContext, TreeseedOperationImplementation, TreeseedOperationId, TreeseedOperationMetadata, TreeseedOperationProvider, TreeseedOperationProviderId, TreeseedOperationRequest, TreeseedOperationResult, TreeseedOperationGroup, } from './operations-types.ts';
7
8
  export { TreeseedOperationError } from './operations-types.ts';
@@ -14,6 +14,12 @@ import {
14
14
  validateRepositoryHost
15
15
  } from "./operations/services/hub-launch.js";
16
16
  import { TreeseedOperationsSdk } from "./operations/runtime.js";
17
+ import {
18
+ cancelGitHubWorkflowRun,
19
+ dispatchGitHubWorkflowRun,
20
+ formatGitHubWorkflowFailure,
21
+ waitForGitHubWorkflowRunCompletion
22
+ } from "./operations/services/github-api.js";
17
23
  import { TreeseedOperationError } from "./operations-types.js";
18
24
  import {
19
25
  AGENT_OPERATION_MODES,
@@ -32,19 +38,23 @@ export {
32
38
  TreeseedOperationError,
33
39
  TreeseedOperationsSdk,
34
40
  TreeseedWorkflowSdk,
41
+ cancelGitHubWorkflowRun,
35
42
  collectTreeseedConfigSeedValues,
36
43
  createAgentOperationEvent,
37
44
  createKnowledgeHubRepositories,
38
45
  decideAgentOperationPermission,
39
46
  defaultHubContentResolutionPolicy,
40
47
  deniedAgentOperationResult,
48
+ dispatchGitHubWorkflowRun,
41
49
  executeKnowledgeHubLaunch,
42
50
  findTreeseedOperation,
51
+ formatGitHubWorkflowFailure,
43
52
  isAgentOperationName,
44
53
  listTreeseedOperationNames,
45
54
  normalizeKnowledgeHubLaunchIntent,
46
55
  planKnowledgeHubLaunch,
47
56
  planKnowledgeHubRepositories,
48
57
  resolveAgentOperationGrant,
49
- validateRepositoryHost
58
+ validateRepositoryHost,
59
+ waitForGitHubWorkflowRunCompletion
50
60
  };
@@ -84,6 +84,10 @@ export declare class PlatformOperationStore {
84
84
  ok: true;
85
85
  operation: PlatformOperation;
86
86
  }>;
87
+ cancel(operationId: string, request: PlatformRunnerJobUpdateRequest): Promise<{
88
+ ok: true;
89
+ operation: PlatformOperation;
90
+ }>;
87
91
  private upsertRepositoryClaim;
88
92
  private renewRepositoryClaimsForRunner;
89
93
  private releaseRepositoryClaimsForRunner;
@@ -131,6 +131,9 @@ function repositoryWorkspacePath(workspaceRoot, repository = {}) {
131
131
  const root = String(workspaceRoot ?? "/data").replace(/\/+$/u, "") || "/data";
132
132
  return `${root}/repositories/${repositoryKey(repository)}/repo`;
133
133
  }
134
+ function normalizeOperationCapabilities(capabilities) {
135
+ return Array.isArray(capabilities) ? capabilities.map((entry) => String(entry ?? "").trim()).filter(Boolean) : [];
136
+ }
134
137
  function convertQuestionPlaceholders(query) {
135
138
  let index = 0;
136
139
  return query.replace(/\?/gu, () => `$${++index}`);
@@ -311,20 +314,27 @@ class PlatformOperationStore {
311
314
  const leaseSeconds = Math.max(30, Math.min(Number(input.leaseSeconds ?? 300), 3600));
312
315
  const now = isoNow(this.now);
313
316
  const leaseExpiresAt = new Date(this.now().getTime() + leaseSeconds * 1e3).toISOString();
317
+ const capabilities = normalizeOperationCapabilities(input.capabilities);
318
+ const capabilityWhere = capabilities.length > 0 ? ` AND (${capabilities.map(() => `(namespace || ':' || operation) = ?`).join(" OR ")})` : "";
319
+ const capabilityParams = capabilities;
314
320
  const rows = input.operationId ? await this.database.all(
315
321
  `SELECT * FROM platform_operations
316
322
  WHERE id = ? AND (
317
323
  status = 'queued'
318
324
  OR (status = 'leased' AND lease_expires_at IS NOT NULL AND lease_expires_at < ?)
319
325
  )
326
+ ${capabilityWhere}
320
327
  ORDER BY created_at ASC LIMIT 1`,
321
- [input.operationId, now]
328
+ [input.operationId, now, ...capabilityParams]
322
329
  ) : await this.database.all(
323
330
  `SELECT * FROM platform_operations
324
- WHERE status = 'queued'
331
+ WHERE (
332
+ status = 'queued'
325
333
  OR (status = 'leased' AND lease_expires_at IS NOT NULL AND lease_expires_at < ?)
334
+ )
335
+ ${capabilityWhere}
326
336
  ORDER BY created_at ASC LIMIT 1`,
327
- [now]
337
+ [now, ...capabilityParams]
328
338
  );
329
339
  const row = rows[0];
330
340
  if (!row) return { ok: true, operation: null };
@@ -424,6 +434,22 @@ class PlatformOperationStore {
424
434
  });
425
435
  return this.getOperation(operationId);
426
436
  }
437
+ async cancel(operationId, request) {
438
+ await this.assertRunnerUpdate(operationId, request.runnerId);
439
+ const timestamp = isoNow(this.now);
440
+ await this.database.run(
441
+ `UPDATE platform_operations
442
+ SET status = 'cancelled', error_json = ?, lease_expires_at = NULL, cancelled_at = COALESCE(cancelled_at, ?), updated_at = ?, finished_at = COALESCE(finished_at, ?)
443
+ WHERE id = ?`,
444
+ [JSON.stringify(request.error ?? { message: "Platform operation was cancelled." }), timestamp, timestamp, timestamp, operationId]
445
+ );
446
+ await this.appendPlatformOperationEvent(operationId, request.event?.kind ?? "runner.cancelled", request.event?.data ?? {});
447
+ await this.releaseRepositoryClaimsForRunner(request.runnerId, {
448
+ claimState: "released",
449
+ metadata: { operationId, status: "cancelled" }
450
+ });
451
+ return this.getOperation(operationId);
452
+ }
427
453
  async upsertRepositoryClaim(input) {
428
454
  const repositoryKeyValue = repositoryKey(input.repository);
429
455
  const timestamp = isoNow(this.now);
@@ -154,6 +154,10 @@ export interface PlatformOperationRunnerCoreClient {
154
154
  ok: true;
155
155
  operation: PlatformOperation;
156
156
  }>;
157
+ cancel?(operationId: string, request: PlatformRunnerJobUpdateRequest): Promise<{
158
+ ok: true;
159
+ operation: PlatformOperation;
160
+ }>;
157
161
  }
158
162
  export interface PlatformOperationRunnerCoreOptions {
159
163
  client: PlatformOperationRunnerCoreClient;
@@ -262,4 +266,8 @@ export declare class PlatformRunnerClient {
262
266
  ok: true;
263
267
  operation: PlatformOperation;
264
268
  }>;
269
+ cancel(operationId: string, request: PlatformRunnerJobUpdateRequest): Promise<{
270
+ ok: true;
271
+ operation: PlatformOperation;
272
+ }>;
265
273
  }
@@ -190,6 +190,7 @@ async function runPlatformOperationOnce(options) {
190
190
  const claimed = await options.client.claimJob({
191
191
  runnerId: options.runnerId,
192
192
  operationId: options.operationId ?? void 0,
193
+ capabilities: registry.keys(),
193
194
  limit: options.limit ?? 1,
194
195
  leaseSeconds: options.leaseSeconds ?? 300
195
196
  });
@@ -259,6 +260,14 @@ async function runPlatformOperationOnce(options) {
259
260
  message: error instanceof Error ? error.message : String(error)
260
261
  };
261
262
  const eventKind = failure.message.toLowerCase().includes("cancel") ? "runner.cancelled" : "runner.retry_safe_failure";
263
+ if (eventKind === "runner.cancelled" && options.client.cancel) {
264
+ const cancelled = await options.client.cancel(operation.id, {
265
+ runnerId: options.runnerId,
266
+ error: failure,
267
+ event: { kind: eventKind, data: failure }
268
+ });
269
+ return { ok: false, claimed: true, operation: cancelled.operation, error: failure };
270
+ }
262
271
  if (eventKind === "runner.cancelled" && options.client.getOperation) {
263
272
  await options.client.appendEvent(operation.id, {
264
273
  runnerId: options.runnerId,
@@ -399,6 +408,16 @@ class PlatformRunnerClient {
399
408
  return response;
400
409
  });
401
410
  }
411
+ cancel(operationId, request) {
412
+ return this.requestJson(`${PLATFORM_OPERATION_ENDPOINTS.runnerJob(operationId)}/cancel`, {
413
+ method: "POST",
414
+ body: { ...request, marketId: this.marketId }
415
+ }).then((response) => {
416
+ assertPlatformOperationOkEnvelope(response, "Platform operation cancellation response");
417
+ assertPlatformOperation(response.operation);
418
+ return response;
419
+ });
420
+ }
402
421
  }
403
422
  export {
404
423
  PLATFORM_OPERATION_ENDPOINTS,
package/dist/remote.js CHANGED
@@ -56,10 +56,10 @@ class RemoteTreeseedClient {
56
56
  }
57
57
  }
58
58
  class RemoteTreeseedAuthClient {
59
+ client;
59
60
  constructor(client) {
60
61
  this.client = client;
61
62
  }
62
- client;
63
63
  startDeviceFlow(request) {
64
64
  return this.client.requestJson("/auth/device/start", {
65
65
  method: "POST",
@@ -85,10 +85,10 @@ class RemoteTreeseedAuthClient {
85
85
  }
86
86
  }
87
87
  class RemoteTreeseedSdkClient {
88
+ client;
88
89
  constructor(client) {
89
90
  this.client = client;
90
91
  }
91
- client;
92
92
  execute(operation, request) {
93
93
  return this.client.requestJson(`/sdk/${encodeURIComponent(operation)}`, {
94
94
  method: "POST",
@@ -182,10 +182,10 @@ class CloudflareQueuePushClient {
182
182
  }
183
183
  }
184
184
  class RemoteTreeseedOperationsClient {
185
+ client;
185
186
  constructor(client) {
186
187
  this.client = client;
187
188
  }
188
- client;
189
189
  execute(operation, request) {
190
190
  return this.client.requestJson(`/operations/${encodeURIComponent(operation)}`, {
191
191
  method: "POST",
@@ -195,10 +195,10 @@ class RemoteTreeseedOperationsClient {
195
195
  }
196
196
  }
197
197
  class RemoteTreeseedDispatchClient {
198
+ client;
198
199
  constructor(client) {
199
200
  this.client = client;
200
201
  }
201
- client;
202
202
  dispatch(projectId, request) {
203
203
  return this.client.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/dispatch`, {
204
204
  method: "POST",
@@ -208,10 +208,10 @@ class RemoteTreeseedDispatchClient {
208
208
  }
209
209
  }
210
210
  class RemoteTreeseedJobsClient {
211
+ client;
211
212
  constructor(client) {
212
213
  this.client = client;
213
214
  }
214
- client;
215
215
  get(jobId) {
216
216
  return this.client.requestJson(`/v1/jobs/${encodeURIComponent(jobId)}`, {
217
217
  requireAuth: true
@@ -230,10 +230,10 @@ class RemoteTreeseedJobsClient {
230
230
  }
231
231
  }
232
232
  class RemoteTreeseedRunnerClient {
233
+ client;
233
234
  constructor(client) {
234
235
  this.client = client;
235
236
  }
236
- client;
237
237
  pull(projectId, request = {}) {
238
238
  return this.client.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/runner/jobs/pull`, {
239
239
  method: "POST",
@@ -1,8 +1,7 @@
1
- import { resolve } from 'node:path';
2
1
  import { runLocalD1Migrations } from '../operations/services/d1-migration.js';
3
2
  import { createPersistentDeployTarget, ensureGeneratedWranglerConfig } from '../operations/services/deploy.js';
3
+ import { sdkD1MigrationsRoot } from '../operations/services/runtime-paths.js';
4
4
  const tenantRoot = process.cwd();
5
- const migrationsRoot = resolve(tenantRoot, 'migrations');
6
5
  const { wranglerPath: wranglerConfig } = ensureGeneratedWranglerConfig(tenantRoot, {
7
6
  target: createPersistentDeployTarget('local'),
8
7
  env: process.env,
@@ -10,7 +9,7 @@ const { wranglerPath: wranglerConfig } = ensureGeneratedWranglerConfig(tenantRoo
10
9
  runLocalD1Migrations({
11
10
  cwd: tenantRoot,
12
11
  wranglerConfig,
13
- migrationsRoot,
12
+ migrationsRoot: sdkD1MigrationsRoot,
14
13
  persistTo: process.env.TREESEED_API_D1_LOCAL_PERSIST_TO?.trim() || undefined,
15
14
  });
16
15
  process.exit(0);
@@ -111,7 +111,7 @@ async function querySubscribers() {
111
111
  const child = spawnProcess('wrangler', [
112
112
  'd1',
113
113
  'execute',
114
- 'karyon-docs-site-data',
114
+ 'docs-site-data',
115
115
  '--local',
116
116
  '--config',
117
117
  fixtureWranglerConfig,