@sweny-ai/core 0.1.171 → 0.1.172
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/cli/check.js +12 -12
- package/dist/cli/config.js +24 -24
- package/dist/cli/e2e.js +17 -17
- package/dist/cli/marketplace.js +3 -3
- package/dist/cli/new.js +8 -8
- package/dist/cli/publish.js +2 -2
- package/dist/cli/record.d.ts +66 -0
- package/dist/cli/record.js +344 -0
- package/dist/cli/setup.js +7 -7
- package/dist/cli/try.d.ts +4 -0
- package/dist/cli/try.js +15 -2
- package/dist/cli/upgrade.js +1 -1
- package/package.json +1 -1
package/dist/cli/check.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function checkProviderConnectivity(config, opts = {}) {
|
|
|
63
63
|
results.push(await checkSentry(creds));
|
|
64
64
|
}
|
|
65
65
|
else if (provider === "file") {
|
|
66
|
-
results.push({ name: "Observability (file)", status: "skip", detail: "File provider
|
|
66
|
+
results.push({ name: "Observability (file)", status: "skip", detail: "File provider - no network check needed" });
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
69
|
results.push({
|
|
@@ -89,7 +89,7 @@ export async function checkProviderConnectivity(config, opts = {}) {
|
|
|
89
89
|
results.push({
|
|
90
90
|
name: "Issue tracker (file)",
|
|
91
91
|
status: "skip",
|
|
92
|
-
detail: "File provider
|
|
92
|
+
detail: "File provider - no network check needed",
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
else {
|
|
@@ -118,7 +118,7 @@ export async function checkProviderConnectivity(config, opts = {}) {
|
|
|
118
118
|
results.push({
|
|
119
119
|
name: "Source control (file)",
|
|
120
120
|
status: "skip",
|
|
121
|
-
detail: "File provider
|
|
121
|
+
detail: "File provider - no network check needed",
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
else {
|
|
@@ -145,7 +145,7 @@ async function checkAnthropic(apiKey) {
|
|
|
145
145
|
return {
|
|
146
146
|
name,
|
|
147
147
|
status: "fail",
|
|
148
|
-
detail: "401 Unauthorized
|
|
148
|
+
detail: "401 Unauthorized - check ANTHROPIC_API_KEY at https://console.anthropic.com",
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
return { name, status: "fail", detail: `Unexpected HTTP ${res.status}` };
|
|
@@ -220,7 +220,7 @@ export async function checkDatadog(creds) {
|
|
|
220
220
|
return {
|
|
221
221
|
name,
|
|
222
222
|
status: "fail",
|
|
223
|
-
detail: `Invalid DD_SITE "${site}"
|
|
223
|
+
detail: `Invalid DD_SITE "${site}" - expected a hostname like datadoghq.com or datadoghq.eu`,
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
try {
|
|
@@ -235,7 +235,7 @@ export async function checkDatadog(creds) {
|
|
|
235
235
|
return {
|
|
236
236
|
name,
|
|
237
237
|
status: "fail",
|
|
238
|
-
detail: "403 Forbidden
|
|
238
|
+
detail: "403 Forbidden - check DD_API_KEY and DD_APP_KEY at https://app.datadoghq.com/organization-settings/api-keys",
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
return { name, status: "fail", detail: `Unexpected HTTP ${res.status}` };
|
|
@@ -262,7 +262,7 @@ async function checkSentry(creds) {
|
|
|
262
262
|
return {
|
|
263
263
|
name,
|
|
264
264
|
status: "fail",
|
|
265
|
-
detail: "401 Unauthorized
|
|
265
|
+
detail: "401 Unauthorized - check SENTRY_AUTH_TOKEN at https://sentry.io/settings/auth-tokens/",
|
|
266
266
|
};
|
|
267
267
|
}
|
|
268
268
|
return { name, status: "fail", detail: `Unexpected HTTP ${res.status}` };
|
|
@@ -286,7 +286,7 @@ async function checkLinear(apiKey) {
|
|
|
286
286
|
if (res.status === 200) {
|
|
287
287
|
const body = (await res.json());
|
|
288
288
|
if (body.errors?.length) {
|
|
289
|
-
return { name, status: "fail", detail: "GraphQL error
|
|
289
|
+
return { name, status: "fail", detail: "GraphQL error - check LINEAR_API_KEY" };
|
|
290
290
|
}
|
|
291
291
|
const displayName = body.data?.viewer?.name ?? "authenticated";
|
|
292
292
|
return { name, status: "ok", detail: `Authenticated as ${displayName}` };
|
|
@@ -295,7 +295,7 @@ async function checkLinear(apiKey) {
|
|
|
295
295
|
return {
|
|
296
296
|
name,
|
|
297
297
|
status: "fail",
|
|
298
|
-
detail: "401 Unauthorized
|
|
298
|
+
detail: "401 Unauthorized - check LINEAR_API_KEY at https://linear.app/settings/api",
|
|
299
299
|
};
|
|
300
300
|
}
|
|
301
301
|
return { name, status: "fail", detail: `Unexpected HTTP ${res.status}` };
|
|
@@ -321,7 +321,7 @@ async function checkGitHub(token, name) {
|
|
|
321
321
|
return {
|
|
322
322
|
name,
|
|
323
323
|
status: "fail",
|
|
324
|
-
detail: "401 Bad credentials
|
|
324
|
+
detail: "401 Bad credentials - check GITHUB_TOKEN at https://github.com/settings/tokens",
|
|
325
325
|
};
|
|
326
326
|
}
|
|
327
327
|
return { name, status: "fail", detail: `Unexpected HTTP ${res.status}` };
|
|
@@ -335,11 +335,11 @@ function networkErrorMessage(err) {
|
|
|
335
335
|
// explicit abort surfaces as "AbortError". Either means we gave up waiting.
|
|
336
336
|
const errName = err instanceof Error ? err.name : "";
|
|
337
337
|
if (errName === "TimeoutError" || errName === "AbortError") {
|
|
338
|
-
return `Connection timed out after ${CHECK_TIMEOUT_MS}ms
|
|
338
|
+
return `Connection timed out after ${CHECK_TIMEOUT_MS}ms - host unreachable or too slow`;
|
|
339
339
|
}
|
|
340
340
|
const msg = err instanceof Error ? err.message : String(err);
|
|
341
341
|
if (/ENOTFOUND|ETIMEDOUT|ECONNREFUSED/i.test(msg)) {
|
|
342
|
-
return `Network error
|
|
342
|
+
return `Network error - check your internet connection (${msg})`;
|
|
343
343
|
}
|
|
344
344
|
return msg;
|
|
345
345
|
}
|
package/dist/cli/config.js
CHANGED
|
@@ -149,7 +149,7 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
149
149
|
const fromEnv = env.SWENY_CLOUD_TOKEN || "";
|
|
150
150
|
const fromFile = f("cloud-token") || "";
|
|
151
151
|
if (!fromEnv && fromFile) {
|
|
152
|
-
console.warn("\x1b[33m⚠ cloud-token is set in .sweny.yml
|
|
152
|
+
console.warn("\x1b[33m⚠ cloud-token is set in .sweny.yml - consider using SWENY_CLOUD_TOKEN env var instead to avoid accidental commits.\x1b[0m");
|
|
153
153
|
}
|
|
154
154
|
return fromEnv || fromFile;
|
|
155
155
|
})(),
|
|
@@ -229,7 +229,7 @@ export function validateInputs(config) {
|
|
|
229
229
|
switch (config.codingAgentProvider) {
|
|
230
230
|
case "claude":
|
|
231
231
|
if (!config.anthropicApiKey && !config.claudeOauthToken) {
|
|
232
|
-
errors.push("Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN
|
|
232
|
+
errors.push("Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN - get a key at https://console.anthropic.com");
|
|
233
233
|
}
|
|
234
234
|
break;
|
|
235
235
|
case "codex":
|
|
@@ -262,13 +262,13 @@ export function validateInputs(config) {
|
|
|
262
262
|
switch (provider) {
|
|
263
263
|
case "datadog":
|
|
264
264
|
if (!creds.apiKey)
|
|
265
|
-
errors.push("Missing: DD_API_KEY
|
|
265
|
+
errors.push("Missing: DD_API_KEY - find API keys at https://app.datadoghq.com/organization-settings/api-keys");
|
|
266
266
|
if (!creds.appKey)
|
|
267
|
-
errors.push("Missing: DD_APP_KEY
|
|
267
|
+
errors.push("Missing: DD_APP_KEY - find application keys at https://app.datadoghq.com/organization-settings/application-keys");
|
|
268
268
|
break;
|
|
269
269
|
case "sentry":
|
|
270
270
|
if (!creds.authToken)
|
|
271
|
-
errors.push("Missing: SENTRY_AUTH_TOKEN
|
|
271
|
+
errors.push("Missing: SENTRY_AUTH_TOKEN - create a token at https://sentry.io/settings/auth-tokens/");
|
|
272
272
|
if (!creds.organization)
|
|
273
273
|
errors.push("Missing: --sentry-org is required for sentry provider");
|
|
274
274
|
if (!creds.project)
|
|
@@ -342,33 +342,33 @@ export function validateInputs(config) {
|
|
|
342
342
|
break;
|
|
343
343
|
case "vercel":
|
|
344
344
|
if (!creds.token)
|
|
345
|
-
errors.push("Missing: VERCEL_TOKEN
|
|
345
|
+
errors.push("Missing: VERCEL_TOKEN - create a token at https://vercel.com/account/tokens");
|
|
346
346
|
if (!creds.projectId)
|
|
347
|
-
errors.push("Missing: VERCEL_PROJECT_ID
|
|
347
|
+
errors.push("Missing: VERCEL_PROJECT_ID - find it in your Vercel project settings");
|
|
348
348
|
break;
|
|
349
349
|
case "supabase":
|
|
350
350
|
if (!creds.managementApiKey)
|
|
351
|
-
errors.push("Missing: SUPABASE_MANAGEMENT_KEY
|
|
351
|
+
errors.push("Missing: SUPABASE_MANAGEMENT_KEY - create a token at https://supabase.com/dashboard/account/tokens");
|
|
352
352
|
if (!creds.projectRef)
|
|
353
|
-
errors.push("Missing: SUPABASE_PROJECT_REF
|
|
353
|
+
errors.push("Missing: SUPABASE_PROJECT_REF - find it in your Supabase project settings > General");
|
|
354
354
|
break;
|
|
355
355
|
case "netlify":
|
|
356
356
|
if (!creds.token)
|
|
357
|
-
errors.push("Missing: NETLIFY_TOKEN
|
|
357
|
+
errors.push("Missing: NETLIFY_TOKEN - create a token at https://app.netlify.com/user/applications#personal-access-tokens");
|
|
358
358
|
if (!creds.siteId)
|
|
359
|
-
errors.push("Missing: NETLIFY_SITE_ID
|
|
359
|
+
errors.push("Missing: NETLIFY_SITE_ID - find it in Site Settings > General > Site details");
|
|
360
360
|
break;
|
|
361
361
|
case "fly":
|
|
362
362
|
if (!creds.token)
|
|
363
|
-
errors.push("Missing: FLY_TOKEN
|
|
363
|
+
errors.push("Missing: FLY_TOKEN - create a token at https://fly.io/user/personal_access_tokens");
|
|
364
364
|
if (!creds.appName)
|
|
365
|
-
errors.push("Missing: FLY_APP_NAME
|
|
365
|
+
errors.push("Missing: FLY_APP_NAME - the name of your Fly.io application");
|
|
366
366
|
break;
|
|
367
367
|
case "render":
|
|
368
368
|
if (!creds.apiKey)
|
|
369
|
-
errors.push("Missing: RENDER_API_KEY
|
|
369
|
+
errors.push("Missing: RENDER_API_KEY - create an API key at https://dashboard.render.com/u/settings");
|
|
370
370
|
if (!creds.serviceId)
|
|
371
|
-
errors.push("Missing: RENDER_SERVICE_ID
|
|
371
|
+
errors.push("Missing: RENDER_SERVICE_ID - find it in your service's Settings page (srv-...)");
|
|
372
372
|
break;
|
|
373
373
|
default:
|
|
374
374
|
errors.push(`Unknown --observability-provider "${provider}". Valid values: none, datadog, sentry, cloudwatch, splunk, elastic, newrelic, loki, prometheus, pagerduty, honeycomb, heroku, opsgenie, axiom, betterstack, vercel, supabase, netlify, fly, render, file`);
|
|
@@ -378,17 +378,17 @@ export function validateInputs(config) {
|
|
|
378
378
|
switch (config.issueTrackerProvider) {
|
|
379
379
|
case "linear":
|
|
380
380
|
if (!config.linearApiKey)
|
|
381
|
-
errors.push("Missing: LINEAR_API_KEY
|
|
381
|
+
errors.push("Missing: LINEAR_API_KEY - find API keys at https://linear.app/settings/api");
|
|
382
382
|
if (!config.linearTeamId)
|
|
383
|
-
errors.push("Missing: LINEAR_TEAM_ID
|
|
383
|
+
errors.push("Missing: LINEAR_TEAM_ID - find it in Linear > Settings > Workspace > Teams > [your team] > copy the ID from the URL");
|
|
384
384
|
break;
|
|
385
385
|
case "jira":
|
|
386
386
|
if (!config.jiraBaseUrl)
|
|
387
|
-
errors.push("Missing: JIRA_BASE_URL
|
|
387
|
+
errors.push("Missing: JIRA_BASE_URL - set to your Atlassian domain, e.g. https://your-org.atlassian.net");
|
|
388
388
|
if (!config.jiraEmail)
|
|
389
|
-
errors.push("Missing: JIRA_EMAIL
|
|
389
|
+
errors.push("Missing: JIRA_EMAIL - your Atlassian account email address");
|
|
390
390
|
if (!config.jiraApiToken)
|
|
391
|
-
errors.push("Missing: JIRA_API_TOKEN
|
|
391
|
+
errors.push("Missing: JIRA_API_TOKEN - create a token at https://id.atlassian.com/manage-profile/security/api-tokens");
|
|
392
392
|
break;
|
|
393
393
|
case "github-issues":
|
|
394
394
|
case "file":
|
|
@@ -401,13 +401,13 @@ export function validateInputs(config) {
|
|
|
401
401
|
switch (config.sourceControlProvider) {
|
|
402
402
|
case "github":
|
|
403
403
|
if (!config.githubToken && !config.botToken)
|
|
404
|
-
errors.push("Missing: GITHUB_TOKEN
|
|
404
|
+
errors.push("Missing: GITHUB_TOKEN - create a Personal Access Token at https://github.com/settings/tokens");
|
|
405
405
|
break;
|
|
406
406
|
case "gitlab":
|
|
407
407
|
if (!config.gitlabToken)
|
|
408
|
-
errors.push("Missing: GITLAB_TOKEN
|
|
408
|
+
errors.push("Missing: GITLAB_TOKEN - create a Personal Access Token at https://gitlab.com/-/user_settings/personal_access_tokens (api + read_repository + write_repository scopes)");
|
|
409
409
|
if (!config.gitlabProjectId)
|
|
410
|
-
errors.push("Missing: GITLAB_PROJECT_ID
|
|
410
|
+
errors.push("Missing: GITLAB_PROJECT_ID - find it in GitLab > [your project] > Settings > General (numeric ID at the top)");
|
|
411
411
|
break;
|
|
412
412
|
case "file":
|
|
413
413
|
// No external credentials needed
|
|
@@ -670,7 +670,7 @@ export function registerImplementCommand(program) {
|
|
|
670
670
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
671
671
|
.option("--issue-tracker-provider <provider>", "Issue tracker (linear|jira|github-issues|file)")
|
|
672
672
|
.option("--source-control-provider <provider>", "Source control (github|gitlab|file)")
|
|
673
|
-
.option("--dry-run", "Skip creating PR
|
|
673
|
+
.option("--dry-run", "Skip creating PR - report only", false)
|
|
674
674
|
.option("--stage", "Run normally, but preview the PR and issue comment instead of writing them", false)
|
|
675
675
|
.option("--max-implement-turns <n>", "Max coding agent turns (default: 40)")
|
|
676
676
|
.option("--base-branch <branch>", "Base branch for PRs (default: main)")
|
package/dist/cli/e2e.js
CHANGED
|
@@ -87,17 +87,17 @@ This returns element references like @e1, @e2, @e3.
|
|
|
87
87
|
Use those refs with: agent-browser click @e5, agent-browser fill @e7 "text", etc.
|
|
88
88
|
|
|
89
89
|
Available commands:
|
|
90
|
-
- agent-browser open <url>
|
|
91
|
-
- agent-browser snapshot
|
|
92
|
-
- agent-browser click <ref>
|
|
93
|
-
- agent-browser fill <ref> <text>
|
|
94
|
-
- agent-browser press <key>
|
|
95
|
-
- agent-browser get url
|
|
96
|
-
- agent-browser get text <ref>
|
|
97
|
-
- agent-browser screenshot <path>
|
|
98
|
-
- agent-browser scroll <direction> <pixels>
|
|
99
|
-
- agent-browser scrollintoview <ref>
|
|
100
|
-
- agent-browser select <ref> <value>
|
|
90
|
+
- agent-browser open <url> - navigate to URL
|
|
91
|
+
- agent-browser snapshot - get accessibility tree with @refs
|
|
92
|
+
- agent-browser click <ref> - click an element
|
|
93
|
+
- agent-browser fill <ref> <text> - clear input and fill with text
|
|
94
|
+
- agent-browser press <key> - press keyboard key (Enter, Tab, Escape)
|
|
95
|
+
- agent-browser get url - get current page URL
|
|
96
|
+
- agent-browser get text <ref> - get element text content
|
|
97
|
+
- agent-browser screenshot <path> - save screenshot to file
|
|
98
|
+
- agent-browser scroll <direction> <pixels> - scroll page
|
|
99
|
+
- agent-browser scrollintoview <ref> - scroll element into view
|
|
100
|
+
- agent-browser select <ref> <value> - select dropdown option`,
|
|
101
101
|
skills: [],
|
|
102
102
|
output: {
|
|
103
103
|
type: "object",
|
|
@@ -142,7 +142,7 @@ Use curl to call the Supabase Admin API:
|
|
|
142
142
|
2. Filter for emails starting with "e2e-" and ending with "@yourapp.test"
|
|
143
143
|
3. Delete each matching user: DELETE {SUPABASE_URL}/auth/v1/admin/users/{user_id}
|
|
144
144
|
|
|
145
|
-
If no test users are found, that's fine
|
|
145
|
+
If no test users are found, that's fine - skip gracefully.
|
|
146
146
|
If the service role key is not available, skip cleanup gracefully.`;
|
|
147
147
|
break;
|
|
148
148
|
case "firebase":
|
|
@@ -501,7 +501,7 @@ const CLEANUP_ENV_VARS = {
|
|
|
501
501
|
*/
|
|
502
502
|
export function buildE2eEnvTemplate(selections) {
|
|
503
503
|
const lines = [];
|
|
504
|
-
lines.push("# E2E Testing
|
|
504
|
+
lines.push("# E2E Testing - SWEny");
|
|
505
505
|
lines.push("# Fill in values, then run: sweny workflow run");
|
|
506
506
|
lines.push("");
|
|
507
507
|
lines.push(`E2E_BASE_URL=${selections.baseUrl}`);
|
|
@@ -640,7 +640,7 @@ export async function runE2eInit(options = {}) {
|
|
|
640
640
|
if (p.isCancel(overwrite))
|
|
641
641
|
cancel();
|
|
642
642
|
if (!overwrite) {
|
|
643
|
-
p.cancel("Setup cancelled
|
|
643
|
+
p.cancel("Setup cancelled - existing files preserved.");
|
|
644
644
|
process.exit(0);
|
|
645
645
|
}
|
|
646
646
|
}
|
|
@@ -741,7 +741,7 @@ export async function runE2eInit(options = {}) {
|
|
|
741
741
|
p.log.success(`Appended E2E vars to ${chalk.cyan(".env")}`);
|
|
742
742
|
}
|
|
743
743
|
else {
|
|
744
|
-
p.log.warn(".env already has E2E vars
|
|
744
|
+
p.log.warn(".env already has E2E vars - skipped");
|
|
745
745
|
}
|
|
746
746
|
}
|
|
747
747
|
else {
|
|
@@ -930,10 +930,10 @@ export async function runE2eRun(options) {
|
|
|
930
930
|
const elapsed = ((Date.now() - enterTime) / 1000).toFixed(1);
|
|
931
931
|
const status = event.result.data?.status || event.result.status;
|
|
932
932
|
if (isTTY) {
|
|
933
|
-
process.stderr.write(`\x1B[1A\x1B[2K ${icon} ${event.node}
|
|
933
|
+
process.stderr.write(`\x1B[1A\x1B[2K ${icon} ${event.node} - ${status} (${elapsed}s)\n`);
|
|
934
934
|
}
|
|
935
935
|
else {
|
|
936
|
-
process.stderr.write(` ${icon} ${event.node}
|
|
936
|
+
process.stderr.write(` ${icon} ${event.node} - ${status} (${elapsed}s)\n`);
|
|
937
937
|
}
|
|
938
938
|
break;
|
|
939
939
|
}
|
package/dist/cli/marketplace.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function fetchMarketplaceWorkflow(id) {
|
|
|
22
22
|
res = await fetch(url);
|
|
23
23
|
}
|
|
24
24
|
catch {
|
|
25
|
-
const err = new Error(`Could not reach github.com
|
|
25
|
+
const err = new Error(`Could not reach github.com - check your connection`);
|
|
26
26
|
err.kind = "network";
|
|
27
27
|
throw err;
|
|
28
28
|
}
|
|
@@ -147,8 +147,8 @@ export async function adaptWorkflowInteractive(workflow, options) {
|
|
|
147
147
|
const action = await p.select({
|
|
148
148
|
message: "Looks good?",
|
|
149
149
|
options: [
|
|
150
|
-
{ value: "accept", label: "Yes
|
|
151
|
-
{ value: "refine", label: "Refine
|
|
150
|
+
{ value: "accept", label: "Yes - use this workflow" },
|
|
151
|
+
{ value: "refine", label: "Refine - describe what to change" },
|
|
152
152
|
{ value: "cancel", label: "Cancel" },
|
|
153
153
|
],
|
|
154
154
|
});
|
package/dist/cli/new.js
CHANGED
|
@@ -328,7 +328,7 @@ export function inferObservability(skills) {
|
|
|
328
328
|
*/
|
|
329
329
|
export function buildSwenyYml(sourceControl, observability, issueTracker) {
|
|
330
330
|
const lines = [];
|
|
331
|
-
lines.push("# .sweny.yml
|
|
331
|
+
lines.push("# .sweny.yml - SWEny project configuration");
|
|
332
332
|
lines.push("# Secrets (API keys, tokens) go in .env (gitignored).");
|
|
333
333
|
lines.push("# Docs: https://docs.sweny.ai/cli");
|
|
334
334
|
lines.push("");
|
|
@@ -345,7 +345,7 @@ export function buildSwenyYml(sourceControl, observability, issueTracker) {
|
|
|
345
345
|
*/
|
|
346
346
|
export function buildEnvTemplate(credentials) {
|
|
347
347
|
const lines = [];
|
|
348
|
-
lines.push("# .env
|
|
348
|
+
lines.push("# .env - SWEny credentials (DO NOT COMMIT)");
|
|
349
349
|
lines.push("# Fill in each value, then run: sweny check");
|
|
350
350
|
lines.push("");
|
|
351
351
|
for (const cred of credentials) {
|
|
@@ -746,7 +746,7 @@ export async function runNew(options) {
|
|
|
746
746
|
summaryLines.push("");
|
|
747
747
|
}
|
|
748
748
|
if (hasExistingConfig) {
|
|
749
|
-
summaryLines.push(chalk.dim("(.sweny.yml already exists
|
|
749
|
+
summaryLines.push(chalk.dim("(.sweny.yml already exists - keeping your existing config)"));
|
|
750
750
|
summaryLines.push("");
|
|
751
751
|
}
|
|
752
752
|
summaryLines.push(chalk.bold(hasExistingConfig ? "Workflow uses:" : "Inferred from workflow:"));
|
|
@@ -773,7 +773,7 @@ export async function runNew(options) {
|
|
|
773
773
|
p.log.success("Created .sweny.yml");
|
|
774
774
|
}
|
|
775
775
|
else {
|
|
776
|
-
p.log.info(".sweny.yml already exists
|
|
776
|
+
p.log.info(".sweny.yml already exists - keeping existing config");
|
|
777
777
|
}
|
|
778
778
|
// 2. Append missing .env keys
|
|
779
779
|
const envExistedBefore = fs.existsSync(path.join(cwd, ".env"));
|
|
@@ -786,7 +786,7 @@ export async function runNew(options) {
|
|
|
786
786
|
p.log.success(`Appended ${addedCount} new key(s) to .env`);
|
|
787
787
|
}
|
|
788
788
|
else {
|
|
789
|
-
p.log.info(".env already contains all required keys
|
|
789
|
+
p.log.info(".env already contains all required keys - skipped");
|
|
790
790
|
}
|
|
791
791
|
// The pack's Actions trigger (#474): never overwritten, skipped with --no-ci.
|
|
792
792
|
const installTrigger = () => {
|
|
@@ -815,7 +815,7 @@ export async function runNew(options) {
|
|
|
815
815
|
if (p.isCancel(overwrite))
|
|
816
816
|
cancel();
|
|
817
817
|
if (!overwrite) {
|
|
818
|
-
p.log.info("Workflow file preserved
|
|
818
|
+
p.log.info("Workflow file preserved - no changes");
|
|
819
819
|
p.outro("Done.");
|
|
820
820
|
return;
|
|
821
821
|
}
|
|
@@ -903,8 +903,8 @@ async function runCustomWorkflowBuilder(skills) {
|
|
|
903
903
|
const action = await p.select({
|
|
904
904
|
message: "Looks good?",
|
|
905
905
|
options: [
|
|
906
|
-
{ value: "accept", label: "Yes
|
|
907
|
-
{ value: "refine", label: "Refine
|
|
906
|
+
{ value: "accept", label: "Yes - use this workflow" },
|
|
907
|
+
{ value: "refine", label: "Refine - describe what to change" },
|
|
908
908
|
{ value: "cancel", label: "Cancel" },
|
|
909
909
|
],
|
|
910
910
|
});
|
package/dist/cli/publish.js
CHANGED
|
@@ -216,7 +216,7 @@ async function publishWorkflow() {
|
|
|
216
216
|
p.log.error(`Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
217
217
|
return null;
|
|
218
218
|
}
|
|
219
|
-
p.log.success(`Validated: ${chalk.cyan(validation.name)}
|
|
219
|
+
p.log.success(`Validated: ${chalk.cyan(validation.name)} - ${validation.nodeCount} nodes, ${validation.edgeCount} edges`);
|
|
220
220
|
if (validation.warnings.length > 0) {
|
|
221
221
|
for (const warn of validation.warnings) {
|
|
222
222
|
p.log.warn(warn);
|
|
@@ -312,7 +312,7 @@ async function selectOutputMode(type, id, sourcePath, meta) {
|
|
|
312
312
|
const hasGh = ghAvailable();
|
|
313
313
|
const options = [
|
|
314
314
|
...(hasGh
|
|
315
|
-
? [{ value: "pr", label: "Open GitHub PR", hint: "recommended
|
|
315
|
+
? [{ value: "pr", label: "Open GitHub PR", hint: "recommended - forks marketplace repo and opens a PR" }]
|
|
316
316
|
: []),
|
|
317
317
|
{ value: "save", label: "Save submission file", hint: "saves to ./sweny-publish/" },
|
|
318
318
|
];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny try --record <file.svg>`: the demo as an animated terminal SVG.
|
|
3
|
+
*
|
|
4
|
+
* Runs the real `sweny try` replay (instant, no TTY) with color forced on,
|
|
5
|
+
* captures the ANSI text, and lays it out as a slate terminal window whose
|
|
6
|
+
* lines appear on a CSS keyframe timeline: the command, a spinner per node
|
|
7
|
+
* until it settles, the answer, the receipt ticket with its stamp
|
|
8
|
+
* landing last. Then it loops.
|
|
9
|
+
*
|
|
10
|
+
* Self-contained by construction: inline CSS only, a monospace font stack, no
|
|
11
|
+
* external fonts, images, links or scripts. Deterministic: no clock, no ids,
|
|
12
|
+
* no randomness; the same fixture and theme always yield the same bytes, so
|
|
13
|
+
* CI can regenerate the file and fail when the committed copy is stale.
|
|
14
|
+
* `prefers-reduced-motion` shows the finished frame with no animation.
|
|
15
|
+
*/
|
|
16
|
+
import { type TryFixture } from "./try.js";
|
|
17
|
+
/** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
|
|
18
|
+
export declare const RECORD_COLUMNS = 80;
|
|
19
|
+
/** Lines longer than this wrap in the window, like a terminal would. */
|
|
20
|
+
export declare const RECORD_WRAP = 96;
|
|
21
|
+
/** Width of one terminal column in the window, in px. Every segment is placed at column * CELL_W. */
|
|
22
|
+
export declare const CELL_W = 7.85;
|
|
23
|
+
/** Left edge of column 0. */
|
|
24
|
+
export declare const TEXT_X = 24;
|
|
25
|
+
export interface Style {
|
|
26
|
+
fg?: string;
|
|
27
|
+
bold?: boolean;
|
|
28
|
+
dim?: boolean;
|
|
29
|
+
underline?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface Cell {
|
|
32
|
+
ch: string;
|
|
33
|
+
style: Style;
|
|
34
|
+
}
|
|
35
|
+
/** One line of ANSI text as styled cells. Cursor and erase sequences are dropped. */
|
|
36
|
+
export declare function parseAnsiLine(line: string): Cell[];
|
|
37
|
+
/** Soft-wrap a row of cells at `max` columns, on spaces, continuation indented two past the first. */
|
|
38
|
+
export declare function wrapCells(cells: Cell[], max: number): Cell[][];
|
|
39
|
+
export interface Row {
|
|
40
|
+
cells: Cell[];
|
|
41
|
+
/** Vertical slot. A spinner shares the slot of the line that replaces it. */
|
|
42
|
+
slot: number;
|
|
43
|
+
/** Seconds at which the row appears. */
|
|
44
|
+
from: number;
|
|
45
|
+
/** Seconds at which it disappears again (spinners). Absent: stays until the loop restarts. */
|
|
46
|
+
to?: number;
|
|
47
|
+
spinner?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Lay captured output lines on the timeline. Pure. */
|
|
50
|
+
export declare function buildTimeline(lines: string[], fixture: TryFixture): {
|
|
51
|
+
rows: Row[];
|
|
52
|
+
slots: number;
|
|
53
|
+
total: number;
|
|
54
|
+
};
|
|
55
|
+
/** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
|
|
56
|
+
export declare function renderRecordingSvg(lines: string[], fixture: TryFixture): string;
|
|
57
|
+
/**
|
|
58
|
+
* The PR-comment preview (heading plus DAG) sits between the answer and the
|
|
59
|
+
* ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
|
|
60
|
+
* answer, ticket.
|
|
61
|
+
*/
|
|
62
|
+
export declare function dropCommentPreview(lines: string[]): string[];
|
|
63
|
+
/** Run the demo and return the SVG text. */
|
|
64
|
+
export declare function recordTrySvg(fixture?: TryFixture): Promise<string>;
|
|
65
|
+
/** Record to `file`, creating its directory. Returns the byte size written. */
|
|
66
|
+
export declare function recordTrySvgToFile(file: string, fixture?: TryFixture): Promise<number>;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny try --record <file.svg>`: the demo as an animated terminal SVG.
|
|
3
|
+
*
|
|
4
|
+
* Runs the real `sweny try` replay (instant, no TTY) with color forced on,
|
|
5
|
+
* captures the ANSI text, and lays it out as a slate terminal window whose
|
|
6
|
+
* lines appear on a CSS keyframe timeline: the command, a spinner per node
|
|
7
|
+
* until it settles, the answer, the receipt ticket with its stamp
|
|
8
|
+
* landing last. Then it loops.
|
|
9
|
+
*
|
|
10
|
+
* Self-contained by construction: inline CSS only, a monospace font stack, no
|
|
11
|
+
* external fonts, images, links or scripts. Deterministic: no clock, no ids,
|
|
12
|
+
* no randomness; the same fixture and theme always yield the same bytes, so
|
|
13
|
+
* CI can regenerate the file and fail when the committed copy is stale.
|
|
14
|
+
* `prefers-reduced-motion` shows the finished frame with no animation.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { GLYPHS, PALETTE, ROLE_COLORS, SPINNER_FRAMES, SURFACE_COLORS } from "../theme.js";
|
|
20
|
+
import { loadTryFixture, runTry } from "./try.js";
|
|
21
|
+
/** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
|
|
22
|
+
export const RECORD_COLUMNS = 80;
|
|
23
|
+
/** Lines longer than this wrap in the window, like a terminal would. */
|
|
24
|
+
export const RECORD_WRAP = 96;
|
|
25
|
+
const FONT_SIZE = 13;
|
|
26
|
+
/** Width of one terminal column in the window, in px. Every segment is placed at column * CELL_W. */
|
|
27
|
+
export const CELL_W = 7.85;
|
|
28
|
+
/** Left edge of column 0. */
|
|
29
|
+
export const TEXT_X = 24;
|
|
30
|
+
const NBSP = "\u00A0";
|
|
31
|
+
const LINE_H = 19;
|
|
32
|
+
const PAD_X = TEXT_X;
|
|
33
|
+
const BAR_H = 38;
|
|
34
|
+
const PAD_TOP = 18;
|
|
35
|
+
const PAD_BOTTOM = 22;
|
|
36
|
+
const WINDOW_W = Math.ceil(RECORD_WRAP * CELL_W + PAD_X * 2);
|
|
37
|
+
const FONT_STACK = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace";
|
|
38
|
+
const SECONDS_PER_LINE = 0.06;
|
|
39
|
+
const SECONDS_PER_BLANK = 0.25;
|
|
40
|
+
const SECONDS_STAMP_DELAY = 0.5;
|
|
41
|
+
const SECONDS_HOLD = 5;
|
|
42
|
+
const BASIC_COLORS = {
|
|
43
|
+
30: PALETTE.slate900,
|
|
44
|
+
31: ROLE_COLORS.error,
|
|
45
|
+
32: ROLE_COLORS.success,
|
|
46
|
+
33: ROLE_COLORS.warning,
|
|
47
|
+
34: ROLE_COLORS.brand,
|
|
48
|
+
35: PALETTE.blue300,
|
|
49
|
+
36: ROLE_COLORS.info,
|
|
50
|
+
37: SURFACE_COLORS.dark.text,
|
|
51
|
+
90: ROLE_COLORS.muted,
|
|
52
|
+
91: ROLE_COLORS.error,
|
|
53
|
+
92: ROLE_COLORS.success,
|
|
54
|
+
93: ROLE_COLORS.warning,
|
|
55
|
+
94: ROLE_COLORS.info,
|
|
56
|
+
95: PALETTE.blue300,
|
|
57
|
+
96: ROLE_COLORS.info,
|
|
58
|
+
97: PALETTE.white,
|
|
59
|
+
};
|
|
60
|
+
const hex = (n) => n.toString(16).padStart(2, "0");
|
|
61
|
+
/** Apply one SGR parameter list to a style. */
|
|
62
|
+
function applySgr(style, params) {
|
|
63
|
+
const next = { ...style };
|
|
64
|
+
for (let i = 0; i < params.length; i++) {
|
|
65
|
+
const p = params[i];
|
|
66
|
+
if (p === 0)
|
|
67
|
+
return {};
|
|
68
|
+
else if (p === 1)
|
|
69
|
+
next.bold = true;
|
|
70
|
+
else if (p === 2)
|
|
71
|
+
next.dim = true;
|
|
72
|
+
else if (p === 22) {
|
|
73
|
+
delete next.bold;
|
|
74
|
+
delete next.dim;
|
|
75
|
+
}
|
|
76
|
+
else if (p === 4)
|
|
77
|
+
next.underline = true;
|
|
78
|
+
else if (p === 24)
|
|
79
|
+
delete next.underline;
|
|
80
|
+
else if (p === 39)
|
|
81
|
+
delete next.fg;
|
|
82
|
+
else if (p === 38 && params[i + 1] === 2) {
|
|
83
|
+
next.fg = `#${hex(params[i + 2] ?? 0)}${hex(params[i + 3] ?? 0)}${hex(params[i + 4] ?? 0)}`;
|
|
84
|
+
i += 4;
|
|
85
|
+
}
|
|
86
|
+
else if (p === 38 && params[i + 1] === 5) {
|
|
87
|
+
next.fg = SURFACE_COLORS.dark.text;
|
|
88
|
+
i += 2;
|
|
89
|
+
}
|
|
90
|
+
else if (BASIC_COLORS[p])
|
|
91
|
+
next.fg = BASIC_COLORS[p];
|
|
92
|
+
}
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
/** One line of ANSI text as styled cells. Cursor and erase sequences are dropped. */
|
|
96
|
+
export function parseAnsiLine(line) {
|
|
97
|
+
const cells = [];
|
|
98
|
+
let style = {};
|
|
99
|
+
const re = /\x1B\[([0-9;]*)([A-Za-z])/g;
|
|
100
|
+
let last = 0;
|
|
101
|
+
const push = (text) => {
|
|
102
|
+
for (const ch of text.replace(/\r/g, ""))
|
|
103
|
+
cells.push({ ch, style });
|
|
104
|
+
};
|
|
105
|
+
for (let m = re.exec(line); m; m = re.exec(line)) {
|
|
106
|
+
push(line.slice(last, m.index));
|
|
107
|
+
if (m[2] === "m")
|
|
108
|
+
style = applySgr(style, m[1] === "" ? [0] : m[1].split(";").map(Number));
|
|
109
|
+
last = m.index + m[0].length;
|
|
110
|
+
}
|
|
111
|
+
push(line.slice(last));
|
|
112
|
+
return cells;
|
|
113
|
+
}
|
|
114
|
+
const sameStyle = (a, b) => a.fg === b.fg && !!a.bold === !!b.bold && !!a.dim === !!b.dim && !!a.underline === !!b.underline;
|
|
115
|
+
/** Soft-wrap a row of cells at `max` columns, on spaces, continuation indented two past the first. */
|
|
116
|
+
export function wrapCells(cells, max) {
|
|
117
|
+
const rows = [];
|
|
118
|
+
const indent = cells.findIndex((c) => c.ch !== " ");
|
|
119
|
+
const pad = " ".repeat(Math.max(0, indent) + 2);
|
|
120
|
+
let rest = cells;
|
|
121
|
+
while (rest.length > max) {
|
|
122
|
+
let cut = -1;
|
|
123
|
+
for (let i = max; i > pad.length; i--) {
|
|
124
|
+
if (rest[i].ch === " ") {
|
|
125
|
+
cut = i;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (cut < 0)
|
|
130
|
+
cut = max;
|
|
131
|
+
rows.push(rest.slice(0, cut));
|
|
132
|
+
let tail = rest.slice(cut);
|
|
133
|
+
while (tail.length > 0 && tail[0].ch === " ")
|
|
134
|
+
tail = tail.slice(1);
|
|
135
|
+
rest = [...[...pad].map((ch) => ({ ch, style: {} })), ...tail];
|
|
136
|
+
}
|
|
137
|
+
rows.push(rest);
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
const plainText = (cells) => cells.map((c) => c.ch).join("");
|
|
141
|
+
function textCells(text, style = {}) {
|
|
142
|
+
return [...text].map((ch) => ({ ch, style }));
|
|
143
|
+
}
|
|
144
|
+
/** Lay captured output lines on the timeline. Pure. */
|
|
145
|
+
export function buildTimeline(lines, fixture) {
|
|
146
|
+
const rows = [];
|
|
147
|
+
const nodeIds = Object.keys(fixture.nodes);
|
|
148
|
+
const settledPrefix = (id) => ` ${GLYPHS.unicode.success} ${id}`;
|
|
149
|
+
const spinnerGlyph = SPINNER_FRAMES.unicode[3];
|
|
150
|
+
let t = 0.5;
|
|
151
|
+
let slot = 0;
|
|
152
|
+
// The command being typed.
|
|
153
|
+
rows.push({
|
|
154
|
+
cells: [...textCells("$ ", { fg: ROLE_COLORS.muted }), ...textCells("sweny try", { bold: true })],
|
|
155
|
+
slot: slot++,
|
|
156
|
+
from: t,
|
|
157
|
+
});
|
|
158
|
+
t += 0.9;
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const cells = parseAnsiLine(line);
|
|
161
|
+
const text = plainText(cells);
|
|
162
|
+
const id = nodeIds.find((n) => text.startsWith(settledPrefix(n)));
|
|
163
|
+
if (id) {
|
|
164
|
+
const seconds = 0.8 + Math.min(1.6, fixture.nodes[id].duration_ms / 12000);
|
|
165
|
+
rows.push({
|
|
166
|
+
cells: [...textCells(" "), ...textCells(spinnerGlyph, { fg: ROLE_COLORS.brand }), ...textCells(` ${id}`)],
|
|
167
|
+
slot,
|
|
168
|
+
from: t,
|
|
169
|
+
to: t + seconds,
|
|
170
|
+
spinner: true,
|
|
171
|
+
});
|
|
172
|
+
t += seconds;
|
|
173
|
+
}
|
|
174
|
+
else if (/\[ (ENV|SANDBOXED|UNSANDBOXED)\b/.test(text)) {
|
|
175
|
+
// The policy stamp lands a beat after the card.
|
|
176
|
+
t += SECONDS_STAMP_DELAY;
|
|
177
|
+
}
|
|
178
|
+
for (const part of wrapCells(cells, RECORD_WRAP)) {
|
|
179
|
+
rows.push({ cells: part, slot, from: t });
|
|
180
|
+
slot++;
|
|
181
|
+
}
|
|
182
|
+
t += text.trim() === "" ? SECONDS_PER_BLANK : SECONDS_PER_LINE;
|
|
183
|
+
}
|
|
184
|
+
return { rows, slots: slot, total: t + SECONDS_HOLD };
|
|
185
|
+
}
|
|
186
|
+
// ── SVG ─────────────────────────────────────────────────────────
|
|
187
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
188
|
+
const pct = (seconds, total) => ((seconds / total) * 100).toFixed(2);
|
|
189
|
+
/** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
|
|
190
|
+
export function renderRecordingSvg(lines, fixture) {
|
|
191
|
+
const { rows, slots, total } = buildTimeline(lines, fixture);
|
|
192
|
+
const height = BAR_H + PAD_TOP + slots * LINE_H + PAD_BOTTOM;
|
|
193
|
+
const dark = SURFACE_COLORS.dark;
|
|
194
|
+
// Color classes by first use, so the CSS is small and the order is stable.
|
|
195
|
+
const colorClass = new Map();
|
|
196
|
+
const classFor = (style) => {
|
|
197
|
+
const names = [];
|
|
198
|
+
if (style.fg) {
|
|
199
|
+
let c = colorClass.get(style.fg);
|
|
200
|
+
if (!c) {
|
|
201
|
+
c = `c${colorClass.size}`;
|
|
202
|
+
colorClass.set(style.fg, c);
|
|
203
|
+
}
|
|
204
|
+
names.push(c);
|
|
205
|
+
}
|
|
206
|
+
if (style.bold)
|
|
207
|
+
names.push("b");
|
|
208
|
+
if (style.dim)
|
|
209
|
+
names.push("d");
|
|
210
|
+
if (style.underline)
|
|
211
|
+
names.push("u");
|
|
212
|
+
return names.join(" ");
|
|
213
|
+
};
|
|
214
|
+
// Keyframes by (from, to), numbered by first use.
|
|
215
|
+
const frames = new Map();
|
|
216
|
+
const frameFor = (from, to) => {
|
|
217
|
+
const key = `${pct(from, total)}:${to === undefined ? "" : pct(to, total)}`;
|
|
218
|
+
let name = frames.get(key);
|
|
219
|
+
if (!name) {
|
|
220
|
+
name = `k${frames.size}`;
|
|
221
|
+
frames.set(key, name);
|
|
222
|
+
}
|
|
223
|
+
return name;
|
|
224
|
+
};
|
|
225
|
+
const body = [];
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
const y = BAR_H + PAD_TOP + row.slot * LINE_H + FONT_SIZE;
|
|
228
|
+
const spans = [];
|
|
229
|
+
let i = 0;
|
|
230
|
+
while (i < row.cells.length) {
|
|
231
|
+
let j = i + 1;
|
|
232
|
+
while (j < row.cells.length && sameStyle(row.cells[i].style, row.cells[j].style))
|
|
233
|
+
j++;
|
|
234
|
+
const raw = row.cells
|
|
235
|
+
.slice(i, j)
|
|
236
|
+
.map((c) => c.ch)
|
|
237
|
+
.join("");
|
|
238
|
+
// Layout must not depend on whitespace handling (rsvg, proxies and some viewers collapse
|
|
239
|
+
// spaces): each segment sits at an explicit x for its column, edge spaces become offset
|
|
240
|
+
// or nothing, and inner spaces are non-breaking.
|
|
241
|
+
const lead = raw.length - raw.trimStart().length;
|
|
242
|
+
const body_ = raw.trim();
|
|
243
|
+
if (body_ !== "") {
|
|
244
|
+
const x = (TEXT_X + (i + lead) * CELL_W).toFixed(2);
|
|
245
|
+
const text = esc(body_).replace(/ /g, NBSP);
|
|
246
|
+
const cls = classFor(row.cells[i].style);
|
|
247
|
+
spans.push(`<tspan x="${x}"${cls ? ` class="${cls}"` : ""}>${text}</tspan>`);
|
|
248
|
+
}
|
|
249
|
+
i = j;
|
|
250
|
+
}
|
|
251
|
+
const cls = `r ${frameFor(row.from, row.to)}${row.spinner ? " sp" : ""}`;
|
|
252
|
+
body.push(`<text class="${cls}" x="${TEXT_X}" y="${y}" xml:space="preserve">${spans.join("")}</text>`);
|
|
253
|
+
}
|
|
254
|
+
const css = [
|
|
255
|
+
`text{font-family:${FONT_STACK};font-size:${FONT_SIZE}px;fill:${dark.text};white-space:pre}`,
|
|
256
|
+
// No-animation default is the FINAL frame: lines visible, spinner frames hidden. Keyframes drive the replay.
|
|
257
|
+
`.r{opacity:1}`,
|
|
258
|
+
`.sp{opacity:0}`,
|
|
259
|
+
`.b{font-weight:700}`,
|
|
260
|
+
`.d{fill-opacity:.65}`,
|
|
261
|
+
`.u{text-decoration:underline}`,
|
|
262
|
+
`.t{fill:${dark.textSecondary};font-size:12px}`,
|
|
263
|
+
];
|
|
264
|
+
for (const [fg, name] of colorClass)
|
|
265
|
+
css.push(`.${name}{fill:${fg}}`);
|
|
266
|
+
for (const [key, name] of frames) {
|
|
267
|
+
const [from, to] = key.split(":");
|
|
268
|
+
const a = Number(from);
|
|
269
|
+
const keyframe = to === ""
|
|
270
|
+
? `0%,${from}%{opacity:0}${(a + 0.01).toFixed(2)}%,98%{opacity:1}100%{opacity:0}`
|
|
271
|
+
: `0%,${from}%{opacity:0}${(a + 0.01).toFixed(2)}%,${to}%{opacity:1}${(Number(to) + 0.01).toFixed(2)}%,100%{opacity:0}`;
|
|
272
|
+
css.push(`@keyframes ${name}{${keyframe}}`);
|
|
273
|
+
css.push(`.${name}{animation:${name} ${total.toFixed(2)}s linear infinite}`);
|
|
274
|
+
}
|
|
275
|
+
css.push(`@media (prefers-reduced-motion:reduce){.r{animation:none!important}.sp{display:none}}`);
|
|
276
|
+
const label = "Recorded demo of sweny try: two nodes run, then the answer and the receipt ticket.";
|
|
277
|
+
return [
|
|
278
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WINDOW_W} ${height}" width="${WINDOW_W}" height="${height}" xml:space="preserve" role="img" aria-label="${esc(label)}">`,
|
|
279
|
+
`<title>sweny try</title>`,
|
|
280
|
+
`<style>${css.join("")}</style>`,
|
|
281
|
+
`<rect x="0.5" y="0.5" width="${WINDOW_W - 1}" height="${height - 1}" rx="10" fill="${dark.background}" stroke="${dark.border}"/>`,
|
|
282
|
+
`<path d="M0.5 ${BAR_H}V10.5A10 10 0 0 1 10.5 0.5H${WINDOW_W - 10.5}A10 10 0 0 1 ${WINDOW_W - 0.5} 10.5V${BAR_H}Z" fill="${PALETTE.slate900}" stroke="${dark.border}"/>`,
|
|
283
|
+
`<circle cx="20" cy="19" r="5.5" fill="${ROLE_COLORS.error}"/>`,
|
|
284
|
+
`<circle cx="40" cy="19" r="5.5" fill="${ROLE_COLORS.warning}"/>`,
|
|
285
|
+
`<circle cx="60" cy="19" r="5.5" fill="${ROLE_COLORS.success}"/>`,
|
|
286
|
+
`<text class="t" x="${WINDOW_W / 2}" y="23" text-anchor="middle">sweny try</text>`,
|
|
287
|
+
...body,
|
|
288
|
+
`</svg>`,
|
|
289
|
+
``,
|
|
290
|
+
].join("\n");
|
|
291
|
+
}
|
|
292
|
+
// ── Entry points ────────────────────────────────────────────────
|
|
293
|
+
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
294
|
+
/**
|
|
295
|
+
* The PR-comment preview (heading plus DAG) sits between the answer and the
|
|
296
|
+
* ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
|
|
297
|
+
* answer, ticket.
|
|
298
|
+
*/
|
|
299
|
+
export function dropCommentPreview(lines) {
|
|
300
|
+
const from = lines.findIndex((l) => stripAnsi(l).includes("On a pull request, CI posts"));
|
|
301
|
+
const to = lines.findIndex((l, i) => i > from && stripAnsi(l).startsWith(" \u256D"));
|
|
302
|
+
if (from < 0 || to < 0)
|
|
303
|
+
return lines;
|
|
304
|
+
return [...lines.slice(0, from), ...lines.slice(to)];
|
|
305
|
+
}
|
|
306
|
+
/** Run the demo and return the SVG text. */
|
|
307
|
+
export async function recordTrySvg(fixture = loadTryFixture()) {
|
|
308
|
+
let out = "";
|
|
309
|
+
// The DAG renderer paints through the default chalk; pin it so the capture carries color on any host.
|
|
310
|
+
const prevLevel = chalk.level;
|
|
311
|
+
chalk.level = 3;
|
|
312
|
+
try {
|
|
313
|
+
const code = await runTry({
|
|
314
|
+
fast: true,
|
|
315
|
+
tty: false,
|
|
316
|
+
color: true,
|
|
317
|
+
rich: true,
|
|
318
|
+
unicode: true,
|
|
319
|
+
columns: RECORD_COLUMNS,
|
|
320
|
+
fixture,
|
|
321
|
+
write: (s) => {
|
|
322
|
+
out += s;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
if (code !== 0)
|
|
326
|
+
throw new Error(`the demo run exited ${code}`);
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
chalk.level = prevLevel;
|
|
330
|
+
}
|
|
331
|
+
const lines = out.split("\n");
|
|
332
|
+
while (lines.length > 0 && lines[0].trim() === "")
|
|
333
|
+
lines.shift();
|
|
334
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "")
|
|
335
|
+
lines.pop();
|
|
336
|
+
return renderRecordingSvg(dropCommentPreview(lines), fixture);
|
|
337
|
+
}
|
|
338
|
+
/** Record to `file`, creating its directory. Returns the byte size written. */
|
|
339
|
+
export async function recordTrySvgToFile(file, fixture) {
|
|
340
|
+
const svg = await recordTrySvg(fixture);
|
|
341
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
342
|
+
fs.writeFileSync(file, svg);
|
|
343
|
+
return Buffer.byteLength(svg);
|
|
344
|
+
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -12,17 +12,17 @@ const SIGNAL_LABELS = [
|
|
|
12
12
|
{
|
|
13
13
|
name: "agent-needs-input",
|
|
14
14
|
color: "CA8A04",
|
|
15
|
-
description: "Agent hit a decision point
|
|
15
|
+
description: "Agent hit a decision point - needs human clarification before it can proceed",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: "agent-error",
|
|
19
19
|
color: "B91C1C",
|
|
20
|
-
description: "Unexpected technical failure during agent execution
|
|
20
|
+
description: "Unexpected technical failure during agent execution - needs human investigation",
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
name: "human-only",
|
|
24
24
|
color: "6B7280",
|
|
25
|
-
description: "Guard rail
|
|
25
|
+
description: "Guard rail - automation must not touch this issue or PR",
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
name: "needs-review",
|
|
@@ -52,7 +52,7 @@ const WORK_TYPE_LABELS = [
|
|
|
52
52
|
{
|
|
53
53
|
name: "research",
|
|
54
54
|
color: "D97706",
|
|
55
|
-
description: "Spike, investigation, or report by the agent
|
|
55
|
+
description: "Spike, investigation, or report by the agent - exploratory work without a direct code change",
|
|
56
56
|
isWorkType: true,
|
|
57
57
|
},
|
|
58
58
|
{
|
|
@@ -64,7 +64,7 @@ const WORK_TYPE_LABELS = [
|
|
|
64
64
|
{
|
|
65
65
|
name: "spec",
|
|
66
66
|
color: "BE185D",
|
|
67
|
-
description: "Spec generation
|
|
67
|
+
description: "Spec generation - agent converted non-technical input into a structured spec",
|
|
68
68
|
isWorkType: true,
|
|
69
69
|
},
|
|
70
70
|
{
|
|
@@ -77,7 +77,7 @@ const WORK_TYPE_LABELS = [
|
|
|
77
77
|
const AGENT_PARENT = {
|
|
78
78
|
name: "agent",
|
|
79
79
|
color: "7C3AED",
|
|
80
|
-
description: "Parent group
|
|
80
|
+
description: "Parent group - marks all autonomous agent work",
|
|
81
81
|
};
|
|
82
82
|
const BUG_LABEL = {
|
|
83
83
|
name: "bug",
|
|
@@ -254,7 +254,7 @@ ${chalk.cyan(` source-control-provider: github
|
|
|
254
254
|
pr-labels: agent,triage,needs-review
|
|
255
255
|
issue-labels: agent`)}
|
|
256
256
|
|
|
257
|
-
${chalk.dim(" GitHub labels are referenced by name
|
|
257
|
+
${chalk.dim(" GitHub labels are referenced by name - no UUIDs needed.")}
|
|
258
258
|
`);
|
|
259
259
|
}
|
|
260
260
|
// ── Command registration ───────────────────────────────────────────────────
|
package/dist/cli/try.d.ts
CHANGED
|
@@ -86,6 +86,10 @@ export interface TryOptions {
|
|
|
86
86
|
tty?: boolean;
|
|
87
87
|
/** Draw with Unicode. Default: what the terminal supports. */
|
|
88
88
|
unicode?: boolean;
|
|
89
|
+
/** Force ANSI color on or off. Default: on for a TTY that allows color. */
|
|
90
|
+
color?: boolean;
|
|
91
|
+
/** Force the receipt ticket (true) or the plain receipt line (false). Default: `tty`. */
|
|
92
|
+
rich?: boolean;
|
|
89
93
|
/** Terminal width. Default: stdout's. */
|
|
90
94
|
columns?: number;
|
|
91
95
|
fixture?: TryFixture;
|
package/dist/cli/try.js
CHANGED
|
@@ -189,7 +189,7 @@ export async function runTry(opts = {}) {
|
|
|
189
189
|
}
|
|
190
190
|
const env = process.env;
|
|
191
191
|
const unicode = opts.unicode ?? supportsUnicode(env);
|
|
192
|
-
const color = tty && colorEnabled({ isTTY: true }, env);
|
|
192
|
+
const color = opts.color ?? (tty && colorEnabled({ isTTY: true }, env));
|
|
193
193
|
const paint = createPaint(color);
|
|
194
194
|
const g = glyphsFor(unicode);
|
|
195
195
|
const columns = opts.columns ?? terminalColumns(process.stdout, env);
|
|
@@ -263,7 +263,7 @@ export async function runTry(opts = {}) {
|
|
|
263
263
|
runId: "demo",
|
|
264
264
|
stream: { write, isTTY: tty, columns },
|
|
265
265
|
env,
|
|
266
|
-
rich: tty,
|
|
266
|
+
rich: opts.rich ?? tty,
|
|
267
267
|
color,
|
|
268
268
|
unicode,
|
|
269
269
|
animate: !opts.fast,
|
|
@@ -287,8 +287,21 @@ export function registerTryCommand(program) {
|
|
|
287
287
|
.option("--fast", "Replay instantly instead of pacing the progress")
|
|
288
288
|
.option("--pace <ms>", `Milliseconds each node takes to replay (default ${DEFAULT_TRY_PACE_MS})`)
|
|
289
289
|
.option("--comment-file <path>", "Also write the PR-comment markdown to <path>")
|
|
290
|
+
.option("--record <file.svg>", "Write the demo as an animated, self-contained SVG terminal recording to <file.svg>")
|
|
290
291
|
.addHelpText("after", `\n${TRY_HELP_NOTE}\n`)
|
|
291
292
|
.action(async (options) => {
|
|
293
|
+
if (options.record) {
|
|
294
|
+
try {
|
|
295
|
+
const { recordTrySvgToFile } = await import("./record.js");
|
|
296
|
+
const bytes = await recordTrySvgToFile(options.record);
|
|
297
|
+
process.stdout.write(` wrote ${options.record} (${bytes} bytes)\n`);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
console.error(`\n could not record the demo: ${err instanceof Error ? err.message : err}\n`);
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
292
305
|
let paceMs;
|
|
293
306
|
if (options.pace !== undefined) {
|
|
294
307
|
paceMs = Number(options.pace);
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function runUpgrade(options, deps) {
|
|
|
154
154
|
latest = await deps.fetchLatestVersion(tag);
|
|
155
155
|
}
|
|
156
156
|
catch (err) {
|
|
157
|
-
stderr.write(chalk.red(` Error: couldn't reach the npm registry
|
|
157
|
+
stderr.write(chalk.red(` Error: couldn't reach the npm registry - ${err instanceof Error ? err.message : String(err)}`) +
|
|
158
158
|
"\n");
|
|
159
159
|
stderr.write(chalk.dim(" Check your network connection and try again.") + "\n");
|
|
160
160
|
exit(1);
|