@tryarcanist/cli 0.1.130 → 0.1.131
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/README.md +32 -0
- package/dist/index.js +181 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -235,6 +235,38 @@ arcanist sessions usage abc123
|
|
|
235
235
|
arcanist sessions usage abc123 --json
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
+
### `arcanist automations create <repo-url> [prompt]`
|
|
239
|
+
|
|
240
|
+
Creates a scheduled automation for a GitHub repo. Create/delete require a write-scoped CLI token; read-scoped tokens can list automations. The server validates repo access, normalizes cron expressions, dedupes retries by repo+cron+prompt, and caps each business at 20 enabled rules.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
arcanist automations create your-org/your-repo "summarize new regressions" --cron "*/15 * * * *"
|
|
244
|
+
printf "summarize new regressions" | arcanist automations create your-org/your-repo --prompt-stdin --cron "*/15 * * * *" --json
|
|
245
|
+
arcanist automations create https://github.com/your-org/your-repo - --cron "0 14 * * 1-5" --name "Weekday summary"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
JSON mode prints the created rule. Human-readable mode prints the rule ID, repo, normalized cron, and next fire time.
|
|
249
|
+
|
|
250
|
+
### `arcanist automations list`
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
arcanist automations list
|
|
254
|
+
arcanist automations list --limit 20 --json
|
|
255
|
+
arcanist automations list --cursor <cursor>
|
|
256
|
+
arcanist automations list --all --json
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Default output reads one page. `--all` follows pagination until completion. JSON mode returns `{items, nextCursor}`.
|
|
260
|
+
|
|
261
|
+
### `arcanist automations delete <id>`
|
|
262
|
+
|
|
263
|
+
Deletes a scheduled automation. Interactive mode prompts for confirmation; pass `--yes` for non-interactive use. `--json` requires `--yes`.
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
arcanist automations delete rule_123
|
|
267
|
+
arcanist automations delete rule_123 --yes --json
|
|
268
|
+
```
|
|
269
|
+
|
|
238
270
|
### `arcanist tokens list`
|
|
239
271
|
|
|
240
272
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -378,6 +378,161 @@ async function whoamiCommand(options, command) {
|
|
|
378
378
|
if (payload.tokenScope) console.log(`Token scope: ${String(payload.tokenScope)}`);
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
// ../../shared/github/repo-url.ts
|
|
382
|
+
function parseGithubRepoFullName(value) {
|
|
383
|
+
const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
|
|
384
|
+
if (shorthand) return { owner: shorthand[1], repo: shorthand[2] };
|
|
385
|
+
const ssh = value.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
386
|
+
if (ssh) return { owner: ssh[1], repo: ssh[2] };
|
|
387
|
+
const https = value.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
388
|
+
if (https) return { owner: https[1], repo: https[2] };
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/commands/automations.ts
|
|
393
|
+
var AUTOMATION_ERROR_HINTS = {
|
|
394
|
+
invalid_cron: "Use a standard five-field cron expression, for example: */15 * * * *",
|
|
395
|
+
repo_not_available: "Check that the token owner has GitHub access and Arcanist is installed for that repo.",
|
|
396
|
+
duplicate_rule: "An enabled automation already exists for this repo, cron, and prompt.",
|
|
397
|
+
rule_cap_reached: "Delete or disable an existing automation before creating another one. The cap is 20 enabled rules.",
|
|
398
|
+
installation_unresolved: "Reconnect or reinstall the GitHub integration for that repository, then retry."
|
|
399
|
+
};
|
|
400
|
+
async function createAutomationCommand(repoUrl, promptArg, options, command) {
|
|
401
|
+
const repo = parseAutomationRepo(repoUrl);
|
|
402
|
+
const prompt = await resolvePromptInput(promptArg, options);
|
|
403
|
+
const runtime = getRuntimeOptions(command, options);
|
|
404
|
+
const config = requireConfig(runtime);
|
|
405
|
+
const body = {
|
|
406
|
+
repoOwner: repo.owner,
|
|
407
|
+
repoName: repo.repo,
|
|
408
|
+
cron: options.cron,
|
|
409
|
+
prompt
|
|
410
|
+
};
|
|
411
|
+
if (options.name !== void 0) body.name = options.name;
|
|
412
|
+
const payload = await automationApiFetch(config, "/api/automation/schedules", {
|
|
413
|
+
method: "POST",
|
|
414
|
+
body: JSON.stringify(body)
|
|
415
|
+
});
|
|
416
|
+
if (isJson(command, options)) {
|
|
417
|
+
writeJson(payload.data);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
printAutomationRule(payload.data);
|
|
421
|
+
}
|
|
422
|
+
async function listAutomationsCommand(options, command) {
|
|
423
|
+
const runtime = getRuntimeOptions(command, options);
|
|
424
|
+
const config = requireConfig(runtime);
|
|
425
|
+
const items = [];
|
|
426
|
+
let cursor = options.cursor;
|
|
427
|
+
let nextCursor = null;
|
|
428
|
+
do {
|
|
429
|
+
const query = new URLSearchParams();
|
|
430
|
+
if (options.limit) query.set("limit", options.limit);
|
|
431
|
+
if (cursor) query.set("cursor", cursor);
|
|
432
|
+
const payload = await automationApiFetch(
|
|
433
|
+
config,
|
|
434
|
+
`/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
|
|
435
|
+
);
|
|
436
|
+
items.push(...payload.data.items);
|
|
437
|
+
nextCursor = payload.data.nextCursor;
|
|
438
|
+
cursor = nextCursor ?? void 0;
|
|
439
|
+
} while (options.all === true && nextCursor);
|
|
440
|
+
const output = { items, nextCursor: options.all === true ? null : nextCursor };
|
|
441
|
+
if (isJson(command, options)) {
|
|
442
|
+
writeJson(output);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (items.length === 0) {
|
|
446
|
+
console.log("No automations found.");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
for (const rule of items) {
|
|
450
|
+
console.log(
|
|
451
|
+
`${rule.id} ${rule.repoOwner}/${rule.repoName} ${rule.normalizedCron} ${String(rule.enabled)} ${formatTime(rule.nextFireAt)}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (output.nextCursor) console.log(`Next cursor: ${output.nextCursor}`);
|
|
455
|
+
}
|
|
456
|
+
async function deleteAutomationCommand(id, options, command) {
|
|
457
|
+
if (isJson(command, options) && options.yes !== true) {
|
|
458
|
+
throw new CliError("user", "`automations delete --json` requires --yes.");
|
|
459
|
+
}
|
|
460
|
+
if (options.yes !== true) {
|
|
461
|
+
await confirmOrThrow(`Delete automation ${id}?`);
|
|
462
|
+
}
|
|
463
|
+
const runtime = getRuntimeOptions(command, options);
|
|
464
|
+
const config = requireConfig(runtime);
|
|
465
|
+
await automationApiFetchText(config, `/api/automation/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
466
|
+
if (isJson(command, options)) {
|
|
467
|
+
writeJson({ ok: true, id });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
console.log(`Deleted automation ${id}.`);
|
|
471
|
+
}
|
|
472
|
+
function parseAutomationRepo(value) {
|
|
473
|
+
if (/^git@/i.test(value) && !/^git@github\.com:/i.test(value)) {
|
|
474
|
+
throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
|
|
475
|
+
}
|
|
476
|
+
const parsed = parseGithubRepoFullName(value);
|
|
477
|
+
if (!parsed) throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
|
|
478
|
+
return { owner: parsed.owner, repo: parsed.repo };
|
|
479
|
+
}
|
|
480
|
+
async function automationApiFetch(config, path, init) {
|
|
481
|
+
try {
|
|
482
|
+
return await apiFetch(config, path, init);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
throw mapAutomationApiError(err);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function automationApiFetchText(config, path, init) {
|
|
488
|
+
try {
|
|
489
|
+
return await apiFetchText(config, path, init);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
throw mapAutomationApiError(err);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function mapAutomationApiError(err) {
|
|
495
|
+
if (!(err instanceof ApiError)) {
|
|
496
|
+
return err instanceof CliError ? err : new CliError("server", err instanceof Error ? err.message : String(err));
|
|
497
|
+
}
|
|
498
|
+
const parsed = parseApiErrorBody2(err.body);
|
|
499
|
+
const serverCode = parsed?.error;
|
|
500
|
+
return new CliError(codeForStatus(err.status), parsed?.message || serverCode || err.message, {
|
|
501
|
+
exitCode: err.exitCode,
|
|
502
|
+
hint: serverCode ? AUTOMATION_ERROR_HINTS[serverCode] : void 0,
|
|
503
|
+
requestId: err.requestId
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function parseApiErrorBody2(body) {
|
|
507
|
+
try {
|
|
508
|
+
const parsed = JSON.parse(body);
|
|
509
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
510
|
+
const record = parsed;
|
|
511
|
+
return {
|
|
512
|
+
error: typeof record.error === "string" ? record.error : void 0,
|
|
513
|
+
message: typeof record.message === "string" ? record.message : void 0
|
|
514
|
+
};
|
|
515
|
+
} catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function codeForStatus(status) {
|
|
520
|
+
if (status === 401 || status === 403) return "auth";
|
|
521
|
+
if (status === 404) return "not_found";
|
|
522
|
+
if (status === 409) return "conflict";
|
|
523
|
+
if (status >= 500) return "server";
|
|
524
|
+
return "user";
|
|
525
|
+
}
|
|
526
|
+
function printAutomationRule(rule) {
|
|
527
|
+
console.log(`ID: ${rule.id}`);
|
|
528
|
+
console.log(`Repo: ${rule.repoOwner}/${rule.repoName}`);
|
|
529
|
+
console.log(`Cron: ${rule.normalizedCron}`);
|
|
530
|
+
console.log(`Next fire: ${formatTime(rule.nextFireAt)}`);
|
|
531
|
+
}
|
|
532
|
+
function formatTime(value) {
|
|
533
|
+
return typeof value === "number" ? new Date(value).toISOString() : "none";
|
|
534
|
+
}
|
|
535
|
+
|
|
381
536
|
// ../../shared/agent/agent-runtime-backend.ts
|
|
382
537
|
var CODEX_AGENT_RUNTIME_BACKEND = "codex";
|
|
383
538
|
var CLAUDE_CODE_AGENT_RUNTIME_BACKEND = "claude_code";
|
|
@@ -2551,17 +2706,6 @@ import { execFileSync } from "child_process";
|
|
|
2551
2706
|
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2552
2707
|
import { dirname } from "path";
|
|
2553
2708
|
|
|
2554
|
-
// ../../shared/github/repo-url.ts
|
|
2555
|
-
function parseGithubRepoFullName(value) {
|
|
2556
|
-
const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
|
|
2557
|
-
if (shorthand) return { owner: shorthand[1], repo: shorthand[2] };
|
|
2558
|
-
const ssh = value.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
2559
|
-
if (ssh) return { owner: ssh[1], repo: ssh[2] };
|
|
2560
|
-
const https = value.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
2561
|
-
if (https) return { owner: https[1], repo: https[2] };
|
|
2562
|
-
return null;
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
2709
|
// ../../shared/sandbox-layer/parser.ts
|
|
2566
2710
|
import { isMap, isScalar, isSeq, parseDocument } from "yaml";
|
|
2567
2711
|
|
|
@@ -4129,6 +4273,32 @@ Examples:
|
|
|
4129
4273
|
arcanist sessions usage <session-id> --json
|
|
4130
4274
|
`
|
|
4131
4275
|
).action((sessionId, options, command) => usageCommand(sessionId, options, command));
|
|
4276
|
+
var automations = program.command("automations").description("Automation commands");
|
|
4277
|
+
automations.command("create").description("Create a scheduled automation").argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to run, or '-' to read stdin").requiredOption("--cron <expr>", "Cron expression").option("--name <name>", "Automation display name").option("--prompt-stdin", "Read prompt from stdin").addHelpText(
|
|
4278
|
+
"after",
|
|
4279
|
+
`
|
|
4280
|
+
Examples:
|
|
4281
|
+
arcanist automations create tryarcanist/arcanist "summarize regressions" --cron "*/15 * * * *"
|
|
4282
|
+
printf "summarize regressions" | arcanist automations create tryarcanist/arcanist --prompt-stdin --cron "*/15 * * * *" --json
|
|
4283
|
+
`
|
|
4284
|
+
).action((repoUrl, prompt, options, command) => createAutomationCommand(repoUrl, prompt, options, command));
|
|
4285
|
+
automations.command("list").description("List scheduled automations").option("--limit <n>", "Maximum automations to return").option("--cursor <cursor>", "Pagination cursor").option("--all", "Fetch all pages").addHelpText(
|
|
4286
|
+
"after",
|
|
4287
|
+
`
|
|
4288
|
+
Examples:
|
|
4289
|
+
arcanist automations list
|
|
4290
|
+
arcanist automations list --limit 20 --json
|
|
4291
|
+
arcanist automations list --all --json
|
|
4292
|
+
`
|
|
4293
|
+
).action((options, command) => listAutomationsCommand(options, command));
|
|
4294
|
+
automations.command("delete").description("Delete a scheduled automation").argument("<id>", "Automation ID").option("--yes", "Confirm deletion without prompting").addHelpText(
|
|
4295
|
+
"after",
|
|
4296
|
+
`
|
|
4297
|
+
Examples:
|
|
4298
|
+
arcanist automations delete auto_123
|
|
4299
|
+
arcanist automations delete auto_123 --yes --json
|
|
4300
|
+
`
|
|
4301
|
+
).action((id, options, command) => deleteAutomationCommand(id, options, command));
|
|
4132
4302
|
var sandbox = program.command("sandbox").description("Sandbox layer commands");
|
|
4133
4303
|
sandbox.command("init").description("Create sandbox layer source files").action((options, command) => sandboxInitCommand(options, command));
|
|
4134
4304
|
sandbox.command("validate").description("Validate local sandbox layer source files").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").action((options, command) => sandboxValidateCommand(options, command));
|