@treeseed/sdk 0.12.8 → 0.12.10
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.
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
ensureGitHubBranchFromBase
|
|
49
49
|
} from "./github-api.js";
|
|
50
50
|
import { resolveGitHubCredentialForRepository } from "./github-credentials.js";
|
|
51
|
-
import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
|
|
51
|
+
import { loadCliDeployConfig, packageDistScriptRoot, packageScriptPath, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
|
|
52
52
|
import { PRODUCTION_BRANCH, STAGING_BRANCH } from "./git-workflow.js";
|
|
53
53
|
import {
|
|
54
54
|
createTreeseedManagedToolEnv,
|
|
@@ -82,6 +82,7 @@ const TEMPLATE_CATALOG_CACHE_RELATIVE_PATH = "treeseed/cache/template-catalog.js
|
|
|
82
82
|
const TENANT_ENVIRONMENT_OVERLAY_PATH = "src/env.yaml";
|
|
83
83
|
const CLOUDFLARE_ACCOUNT_ID_PLACEHOLDER = "replace-with-cloudflare-account-id";
|
|
84
84
|
const TREESEED_KEY_AGENT_AUTOPROMPT_ENV = "TREESEED_KEY_AGENT_AUTOPROMPT";
|
|
85
|
+
const KEY_AGENT_COMMAND_TIMEOUT_MS = 1e4;
|
|
85
86
|
const DEFAULT_TREESEED_API_BASE_URL = "https://api.treeseed.dev";
|
|
86
87
|
const DEFAULT_TEMPLATE_CATALOG_URL = "https://api.treeseed.dev/search/templates";
|
|
87
88
|
const TREESEED_TEMPLATE_CATALOG_URL_ENV = "TREESEED_TEMPLATE_CATALOG_URL";
|
|
@@ -335,7 +336,12 @@ function resolveManagedWorktreeMachineConfigRoot(tenantRoot) {
|
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
338
|
function keyAgentScriptPath() {
|
|
338
|
-
|
|
339
|
+
const distScriptPath = resolve(packageDistScriptRoot, "key-agent.js");
|
|
340
|
+
return existsSync(distScriptPath) ? distScriptPath : packageScriptPath("key-agent.ts");
|
|
341
|
+
}
|
|
342
|
+
function keyAgentNodeArgs() {
|
|
343
|
+
const scriptPath = keyAgentScriptPath();
|
|
344
|
+
return scriptPath.endsWith(".ts") ? ["--import", "tsx", scriptPath] : [scriptPath];
|
|
339
345
|
}
|
|
340
346
|
function keyAgentScriptCwd() {
|
|
341
347
|
return dirname(dirname(keyAgentScriptPath()));
|
|
@@ -372,11 +378,10 @@ function withTreeseedKeyAgentAutopromptDisabled(action) {
|
|
|
372
378
|
function startTreeseedKeyAgentDaemon(tenantRoot) {
|
|
373
379
|
const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
374
380
|
const { socketPath } = getTreeseedKeyAgentPaths();
|
|
381
|
+
const scriptArgs = keyAgentNodeArgs();
|
|
375
382
|
const command = [
|
|
376
383
|
shellQuote(process.execPath),
|
|
377
|
-
|
|
378
|
-
"tsx",
|
|
379
|
-
shellQuote(keyAgentScriptPath()),
|
|
384
|
+
...scriptArgs.map((arg) => shellQuote(arg)),
|
|
380
385
|
"serve",
|
|
381
386
|
"--key-path",
|
|
382
387
|
shellQuote(keyPath),
|
|
@@ -396,9 +401,7 @@ function startTreeseedKeyAgentDaemon(tenantRoot) {
|
|
|
396
401
|
}
|
|
397
402
|
function runTreeseedKeyAgentCommand(args, options = {}) {
|
|
398
403
|
const result = spawnSync(process.execPath, [
|
|
399
|
-
|
|
400
|
-
"tsx",
|
|
401
|
-
keyAgentScriptPath(),
|
|
404
|
+
...keyAgentNodeArgs(),
|
|
402
405
|
...args
|
|
403
406
|
], {
|
|
404
407
|
cwd: keyAgentScriptCwd(),
|
|
@@ -408,8 +411,18 @@ function runTreeseedKeyAgentCommand(args, options = {}) {
|
|
|
408
411
|
...options.env ?? {}
|
|
409
412
|
},
|
|
410
413
|
stdio: options.input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
411
|
-
input: options.input
|
|
414
|
+
input: options.input,
|
|
415
|
+
timeout: KEY_AGENT_COMMAND_TIMEOUT_MS,
|
|
416
|
+
killSignal: "SIGTERM"
|
|
412
417
|
});
|
|
418
|
+
if (result.error) {
|
|
419
|
+
const timedOut = result.error.code === "ETIMEDOUT";
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
code: "daemon_unavailable",
|
|
423
|
+
message: timedOut ? `Treeseed key-agent command timed out after ${KEY_AGENT_COMMAND_TIMEOUT_MS}ms.` : result.error.message || "Treeseed key-agent command failed."
|
|
424
|
+
};
|
|
425
|
+
}
|
|
413
426
|
if (result.status !== 0 && (!result.stdout || result.stdout.trim().length === 0)) {
|
|
414
427
|
return {
|
|
415
428
|
ok: false,
|
|
@@ -57,6 +57,27 @@ function extractConfirmationUrl(body) {
|
|
|
57
57
|
const relative = body.match(/(?:\/auth\/confirm-email\?[^"' <>\n]+|\/team-invites\/[^"' <>\n]+\/accept)/u)?.[0];
|
|
58
58
|
return relative ?? null;
|
|
59
59
|
}
|
|
60
|
+
async function sleep(ms) {
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
}
|
|
63
|
+
function isRetryableNavigationError(error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
65
|
+
return /Timeout|ERR_CONNECTION|ECONNRESET|ECONNREFUSED|ETIMEDOUT|503|502|504/iu.test(message);
|
|
66
|
+
}
|
|
67
|
+
async function navigateScenePage(page, url) {
|
|
68
|
+
let lastError = null;
|
|
69
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
70
|
+
try {
|
|
71
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
72
|
+
return;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
lastError = error;
|
|
75
|
+
if (!isRetryableNavigationError(error) || attempt >= 3) break;
|
|
76
|
+
await sleep(500 * attempt);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? `Navigation failed for ${url}`));
|
|
80
|
+
}
|
|
60
81
|
async function assertionReport(kind, action, selector) {
|
|
61
82
|
const startedAt = /* @__PURE__ */ new Date();
|
|
62
83
|
try {
|
|
@@ -85,7 +106,7 @@ function createBuiltInTreeseedScenePlugins() {
|
|
|
85
106
|
summary: "Navigate to a route or absolute URL.",
|
|
86
107
|
async run({ action, context }) {
|
|
87
108
|
if (!("goto" in action)) return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.invalid_action", "Expected goto action.", "workflow.action.goto")] };
|
|
88
|
-
await context.session.page
|
|
109
|
+
await navigateScenePage(context.session.page, context.resolveUrl(action.goto));
|
|
89
110
|
return { ok: true, diagnostics: [] };
|
|
90
111
|
}
|
|
91
112
|
},
|
|
@@ -219,18 +240,18 @@ function createBuiltInTreeseedScenePlugins() {
|
|
|
219
240
|
search.searchParams.set("query", `to:${email}`);
|
|
220
241
|
context.timeline.push("mailpit.inbox.open", { messageId: id, email, url: search.toString() }, step.id);
|
|
221
242
|
context.progress?.push("mailpit.inbox.open", { messageId: id, email }, { stepId: step.id });
|
|
222
|
-
await context.session.page
|
|
243
|
+
await navigateScenePage(context.session.page, search.toString());
|
|
223
244
|
await context.sleep(displayInboxSeconds * 1e3);
|
|
224
245
|
}
|
|
225
246
|
if (displayMessageSeconds && displayMessageSeconds > 0) {
|
|
226
247
|
const view = new URL(`view/${encodeURIComponent(id)}`, mailpitBase);
|
|
227
248
|
context.timeline.push("mailpit.message.open", { messageId: id, email, url: view.toString() }, step.id);
|
|
228
249
|
context.progress?.push("mailpit.message.open", { messageId: id, email }, { stepId: step.id });
|
|
229
|
-
await context.session.page
|
|
250
|
+
await navigateScenePage(context.session.page, view.toString());
|
|
230
251
|
await context.sleep(displayMessageSeconds * 1e3);
|
|
231
252
|
}
|
|
232
253
|
context.timeline.push("mailpit.confirm.open", { messageId: id, email, url: resolvedUrl }, step.id);
|
|
233
|
-
await context.session.page
|
|
254
|
+
await navigateScenePage(context.session.page, resolvedUrl);
|
|
234
255
|
return { ok: true, diagnostics: [] };
|
|
235
256
|
} catch (error) {
|
|
236
257
|
return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.mailpit_unavailable", error instanceof Error ? error.message : String(error ?? "Mailpit confirmation failed."), `workflow.${step.id}.action.mailpitConfirmLatest`)] };
|
package/dist/scenes/types.d.ts
CHANGED
|
@@ -1809,7 +1809,10 @@ export type TreeseedSceneLocator = {
|
|
|
1809
1809
|
isVisible(): Promise<boolean>;
|
|
1810
1810
|
};
|
|
1811
1811
|
export type TreeseedScenePage = {
|
|
1812
|
-
goto(url: string
|
|
1812
|
+
goto(url: string, options?: {
|
|
1813
|
+
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
|
|
1814
|
+
timeout?: number;
|
|
1815
|
+
}): Promise<void>;
|
|
1813
1816
|
url(): string;
|
|
1814
1817
|
locator(selector: string): TreeseedSceneLocator;
|
|
1815
1818
|
getByTestId(testId: string): TreeseedSceneLocator;
|
|
@@ -58,6 +58,22 @@ function serviceHeaders(input) {
|
|
|
58
58
|
"x-treeseed-service-secret": configuredValue(input, "TREESEED_API_WEB_SERVICE_SECRET") ?? configuredValue(input, "TREESEED_WEB_SERVICE_SECRET") ?? "treeseed-web-service-dev-secret"
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
async function sleep(ms) {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
64
|
+
function retryDelayMs(attempt) {
|
|
65
|
+
return Math.min(500 * attempt, 2e3);
|
|
66
|
+
}
|
|
67
|
+
function isRetryableStatus(status) {
|
|
68
|
+
return [408, 425, 429, 500, 502, 503, 504].includes(status);
|
|
69
|
+
}
|
|
70
|
+
async function gotoSceneFixturePage(page, url) {
|
|
71
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
72
|
+
await page.waitForLoadState?.("networkidle", { timeout: 5e3 }).catch(() => void 0);
|
|
73
|
+
}
|
|
74
|
+
function isSignInUrl(value) {
|
|
75
|
+
return /\/auth\/sign-in/u.test(value);
|
|
76
|
+
}
|
|
61
77
|
function seedActorsForRoles(roles) {
|
|
62
78
|
const actors = {};
|
|
63
79
|
for (const role of roles) {
|
|
@@ -76,32 +92,33 @@ function seedActorsForRoles(roles) {
|
|
|
76
92
|
async function seedVisualAuditFixtures(input) {
|
|
77
93
|
const actors = seedActorsForRoles(input.roles);
|
|
78
94
|
if (Object.keys(actors).length === 0) return [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
let lastFailure = null;
|
|
96
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(new URL("/v1/acceptance/seed", input.baseUrl).toString(), {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: serviceHeaders(input),
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
namespace: "visual-audit",
|
|
103
|
+
password: TREESEED_VISUAL_AUDIT_PASSWORD,
|
|
104
|
+
actors
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
const text = await response.text();
|
|
108
|
+
if (response.ok) return [];
|
|
109
|
+
lastFailure = `HTTP ${response.status}: ${text.slice(0, 500)}`;
|
|
110
|
+
if (!isRetryableStatus(response.status) || attempt >= 3) break;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
lastFailure = error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable");
|
|
113
|
+
if (attempt >= 3) break;
|
|
96
114
|
}
|
|
97
|
-
|
|
98
|
-
} catch (error) {
|
|
99
|
-
return [sceneWarningDiagnostic(
|
|
100
|
-
"scene.visual_audit_fixture_unavailable",
|
|
101
|
-
`Visual audit API fixture seed failed against ${input.baseUrl}: ${error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable")}.`,
|
|
102
|
-
"roles"
|
|
103
|
-
)];
|
|
115
|
+
await sleep(retryDelayMs(attempt));
|
|
104
116
|
}
|
|
117
|
+
return [sceneWarningDiagnostic(
|
|
118
|
+
"scene.visual_audit_fixture_unavailable",
|
|
119
|
+
`Visual audit API fixture seed failed against ${input.baseUrl}: ${lastFailure ?? "local fixture API is unavailable"}.`,
|
|
120
|
+
"roles"
|
|
121
|
+
)];
|
|
105
122
|
}
|
|
106
123
|
async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
|
|
107
124
|
const diagnostics = [];
|
|
@@ -165,40 +182,54 @@ async function signInTreeseedSceneVisualAuditRole(input) {
|
|
|
165
182
|
return [sceneErrorDiagnostic("scene.visual_audit_role_unknown", `Visual audit role "${input.role}" has no fixture login.`, "role")];
|
|
166
183
|
}
|
|
167
184
|
const apiBaseUrl = input.apiBaseUrl?.trim() || input.baseUrl;
|
|
185
|
+
let lastError = null;
|
|
168
186
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
188
|
+
const session = await clientFor(apiBaseUrl).webSignIn({ login: user.email, password: user.password });
|
|
189
|
+
const accessToken = session.payload.accessToken;
|
|
190
|
+
if (accessToken) {
|
|
191
|
+
const webUrl = new URL(input.baseUrl);
|
|
192
|
+
await input.page.context().addCookies([{
|
|
193
|
+
name: "ts_market_api_access",
|
|
194
|
+
value: accessToken,
|
|
195
|
+
domain: webUrl.hostname,
|
|
196
|
+
path: "/",
|
|
197
|
+
httpOnly: true,
|
|
198
|
+
secure: webUrl.protocol === "https:",
|
|
199
|
+
sameSite: "Lax",
|
|
200
|
+
expires: Math.floor(Date.now() / 1e3) + Number(session.payload.expiresInSeconds ?? 900)
|
|
201
|
+
}]);
|
|
202
|
+
await gotoSceneFixturePage(input.page, new URL("/app/", input.baseUrl).toString());
|
|
203
|
+
if (!isSignInUrl(input.page.url())) return [];
|
|
204
|
+
}
|
|
205
|
+
await sleep(retryDelayMs(attempt));
|
|
185
206
|
}
|
|
186
207
|
} catch {
|
|
187
208
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
210
|
+
try {
|
|
211
|
+
const signInUrl = new URL("/auth/sign-in", input.baseUrl);
|
|
212
|
+
signInUrl.searchParams.set("returnTo", "/app/");
|
|
213
|
+
await gotoSceneFixturePage(input.page, signInUrl.toString());
|
|
214
|
+
await input.page.locator('input[name="login"], input[name="email"], input[name="emailOrUsername"], input[name="username"]').first().fill(user.email, { timeout: 1e4 });
|
|
215
|
+
await input.page.locator('input[name="password"]').first().fill(user.password, { timeout: 1e4 });
|
|
216
|
+
await input.page.getByRole("button", { name: /sign in/i }).click({ timeout: 1e4 });
|
|
217
|
+
await input.page.waitForURL?.((url) => !isSignInUrl(url.pathname), { timeout: 15e3 }).catch(() => void 0);
|
|
218
|
+
await input.page.waitForLoadState?.("networkidle", { timeout: 5e3 }).catch(() => void 0);
|
|
219
|
+
if (!isSignInUrl(input.page.url())) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
lastError = new Error(`Visual audit login for ${input.role} did not leave the sign-in page. Ensure deterministic fixture user ${user.email} exists.`);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
lastError = error;
|
|
197
225
|
}
|
|
198
|
-
|
|
199
|
-
} catch (error) {
|
|
200
|
-
return [sceneWarningDiagnostic("scene.visual_audit_role_login_failed", error instanceof Error ? error.message : String(error ?? `Visual audit login for ${input.role} failed.`), "roles")];
|
|
226
|
+
await sleep(retryDelayMs(attempt));
|
|
201
227
|
}
|
|
228
|
+
return [sceneWarningDiagnostic(
|
|
229
|
+
"scene.visual_audit_role_login_failed",
|
|
230
|
+
lastError instanceof Error ? lastError.message : String(lastError ?? `Visual audit login for ${input.role} failed.`),
|
|
231
|
+
"roles"
|
|
232
|
+
)];
|
|
202
233
|
}
|
|
203
234
|
export {
|
|
204
235
|
TREESEED_VISUAL_AUDIT_PASSWORD,
|