@canner-ca/cli 0.5.6 → 0.5.7
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 +2 -1
- package/package.json +1 -1
- package/src/cli.mjs +65 -5
package/README.md
CHANGED
|
@@ -76,7 +76,8 @@ canner deploy --follow
|
|
|
76
76
|
| `canner domains verify <hostname\|id> [--slug X]` | Re-check DNS and provision TLS. |
|
|
77
77
|
| `canner domains rm <hostname\|id> [--slug X]` | Detach a custom domain. |
|
|
78
78
|
| `canner jobs [list] [--slug X]` | List scheduled jobs, with the header names each sends (never a value). |
|
|
79
|
-
| `canner jobs create <name> (--path /api/cron [--branch BRANCH] \| --url URL) --schedule "CRON" [--header name=value] [--header-env name=VARIABLE] [--bearer-env VARIABLE] [--slug X]` | Create a job. `--path` calls a path in the project itself (its address is built from the project's own host each time, in production or, with `--branch`, on that preview); `--url` is for anything else. `--header-env x-cron-key=CRON_KEY` sends the project variable's value, read each time the job runs, so the secret is never copied. `--bearer-env CRON_SECRET` sends `Authorization: Bearer …`. With a `CRON_SECRET` variable and no Authorization header, Canner sends it automatically. |
|
|
79
|
+
| `canner jobs create <name> (--path /api/cron [--branch BRANCH] \| --url URL) --schedule "CRON" [--header name=value] [--header-env name=VARIABLE] [--bearer-env VARIABLE] [--slug X]` | Create a job. `--path` calls a path in the project itself (in Git Bash on Windows write `--path //api/cron`, or Git Bash rewrites the leading slash) (its address is built from the project's own host each time, in production or, with `--branch`, on that preview); `--url` is for anything else. `--header-env x-cron-key=CRON_KEY` sends the project variable's value, read each time the job runs, so the secret is never copied. `--bearer-env CRON_SECRET` sends `Authorization: Bearer …`. With a `CRON_SECRET` variable and no Authorization header, Canner sends it automatically. |
|
|
80
|
+
| `canner jobs edit <id> [--name N] [--schedule "CRON"] [--description D] [--pause \| --resume] [--path /p [--branch B \| --production] \| --url U] [--header-env name=VARIABLE] [--bearer-env VARIABLE] [--header name=value] [--remove-header name] [--clear-headers]` | Change a job in place; only the options you give change, and its run history stays. A header given by name replaces the one with that name and the others are kept. |
|
|
80
81
|
| `canner jobs run <id>` / `canner jobs runs <id>` | Run a job now and show the result, or list recent runs with what to check on a failure. |
|
|
81
82
|
| `canner jobs delete <id> --yes` | Delete a job and its run history. |
|
|
82
83
|
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
28
28
|
import { stdin as input, stdout as output } from 'node:process';
|
|
29
29
|
import { create as tarCreate } from 'tar';
|
|
30
30
|
|
|
31
|
-
const VERSION = '0.5.
|
|
31
|
+
const VERSION = '0.5.7';
|
|
32
32
|
const DEFAULT_API_BASE = 'https://api.canner.ca';
|
|
33
33
|
const CONFIG_DIR = join(homedir(), '.canner');
|
|
34
34
|
const CREDENTIALS_PATH = join(CONFIG_DIR, 'credentials');
|
|
@@ -1129,6 +1129,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
1129
1129
|
'json', 'reveal', 'sensitive', 'follow', 'dashboard', 'version', 'help',
|
|
1130
1130
|
'replace', 'yes', 'no-wait', 'enable', 'disable', 'restore', 'download', 'rm', 'delete', 'instance', 'pitr',
|
|
1131
1131
|
'read-only', 'read-write', 'replication', 'group', 'login', 'adopt', 'strict', 'dedicated',
|
|
1132
|
+
'pause', 'resume', 'production', 'clear-headers',
|
|
1132
1133
|
]);
|
|
1133
1134
|
|
|
1134
1135
|
const splitList = (v) => String(v).split(',').map((x) => x.trim()).filter(Boolean);
|
|
@@ -1370,6 +1371,11 @@ async function cmdBilling(args) {
|
|
|
1370
1371
|
// [--header "name=value" ...] [--header-env name=VARIABLE ...] [--bearer-env VARIABLE]
|
|
1371
1372
|
// (--path calls a path in the project itself: its address is built from the project's own host each time, in
|
|
1372
1373
|
// production or, with --branch, on that preview; --url is for anything else)
|
|
1374
|
+
// canner jobs edit <id> [--name N] [--schedule "CRON"] [--description D] [--pause | --resume]
|
|
1375
|
+
// [--path /p [--branch B | --production] | --url U]
|
|
1376
|
+
// [--header-env name=VARIABLE ...] [--bearer-env VARIABLE] [--header name=value ...]
|
|
1377
|
+
// [--remove-header name ...] [--clear-headers]
|
|
1378
|
+
// (edit changes only what is given. A header given by name replaces the one with that name; the others stay.)
|
|
1373
1379
|
// canner jobs run <id> run it now and show what came back
|
|
1374
1380
|
// canner jobs runs <id> recent runs, with what to check on a failure
|
|
1375
1381
|
// canner jobs delete <id> --yes
|
|
@@ -1381,6 +1387,16 @@ async function cmdBilling(args) {
|
|
|
1381
1387
|
// ---------------------------------------------------------------------------
|
|
1382
1388
|
const asList = (v) => (v === undefined || v === true ? [] : Array.isArray(v) ? v : [v]);
|
|
1383
1389
|
|
|
1390
|
+
// A path argument, as the API wants it (leading slash). Git Bash on Windows rewrites an argument that starts with
|
|
1391
|
+
// "/" into a Windows path (C:/Program Files/Git/api/cron) before any program sees it; say so instead of sending that.
|
|
1392
|
+
function pathArg(raw) {
|
|
1393
|
+
const p = String(raw);
|
|
1394
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) {
|
|
1395
|
+
die(`"${p}" is not a path in your project. Git Bash on Windows rewrites arguments that start with "/": write it with a doubled slash (--path //api/cron), or run the command with MSYS_NO_PATHCONV=1.`, 2);
|
|
1396
|
+
}
|
|
1397
|
+
return p.startsWith('//') && !p.startsWith('///') ? p.slice(1) : p.startsWith('/') ? p : `/${p}`;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1384
1400
|
function jobHeadersFromArgs(args) {
|
|
1385
1401
|
const headers = {};
|
|
1386
1402
|
const put = (name, value) => {
|
|
@@ -1449,7 +1465,7 @@ async function cmdJobs(args) {
|
|
|
1449
1465
|
const headers = jobHeadersFromArgs(args);
|
|
1450
1466
|
const body = await apiJSON(creds, 'POST', `/projects/${encodeURIComponent(slug)}/jobs`, {
|
|
1451
1467
|
name, schedule: String(args.schedule),
|
|
1452
|
-
...(args.path ? { path:
|
|
1468
|
+
...(args.path ? { path: pathArg(args.path), ...(args.branch ? { branch: String(args.branch) } : {}) } : { url: String(args.url) }),
|
|
1453
1469
|
...(args.description ? { description: String(args.description) } : {}),
|
|
1454
1470
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
1455
1471
|
});
|
|
@@ -1490,6 +1506,48 @@ async function cmdJobs(args) {
|
|
|
1490
1506
|
return;
|
|
1491
1507
|
}
|
|
1492
1508
|
|
|
1509
|
+
if (sub === 'edit' || sub === 'update') {
|
|
1510
|
+
if (!args._[1]) die('usage: canner jobs edit <id> [--schedule "CRON"] [--path /p [--branch B | --production] | --url U] [--header-env name=VARIABLE] [--bearer-env VARIABLE] [--remove-header name] [--clear-headers] [--pause | --resume]', 2);
|
|
1511
|
+
if (args.url && args.path) die('give either --url or --path, not both.', 2);
|
|
1512
|
+
if (args.branch && args.production) die('give either --branch or --production, not both.', 2);
|
|
1513
|
+
if (args.pause && args.resume) die('give either --pause or --resume, not both.', 2);
|
|
1514
|
+
if ((args.branch || args.production) && args.url) die('--branch / --production go with --path.', 2);
|
|
1515
|
+
const job = await findJob(creds, args._[1]);
|
|
1516
|
+
|
|
1517
|
+
const patchBody = {};
|
|
1518
|
+
if (args.name) patchBody.name = String(args.name);
|
|
1519
|
+
if (args.schedule) patchBody.schedule = String(args.schedule);
|
|
1520
|
+
if (args.description !== undefined && args.description !== true) patchBody.description = String(args.description);
|
|
1521
|
+
if (args.pause) patchBody.is_active = false;
|
|
1522
|
+
if (args.resume) patchBody.is_active = true;
|
|
1523
|
+
if (args.path) patchBody.path = pathArg(args.path);
|
|
1524
|
+
if (args.url) patchBody.url = String(args.url);
|
|
1525
|
+
if (args.branch) patchBody.branch = String(args.branch);
|
|
1526
|
+
if (args.production) patchBody.branch = null;
|
|
1527
|
+
|
|
1528
|
+
// Headers: start from what the job has (references as written, literal values hidden — the server keeps
|
|
1529
|
+
// a hidden value as is), drop what was asked to go, and let a header given by name replace its namesake.
|
|
1530
|
+
const added = jobHeadersFromArgs(args);
|
|
1531
|
+
const removed = asList(args['remove-header']).map((n) => String(n).toLowerCase());
|
|
1532
|
+
if (Object.keys(added).length > 0 || removed.length > 0 || args['clear-headers']) {
|
|
1533
|
+
const lower = (n) => n.toLowerCase();
|
|
1534
|
+
const replaced = new Set(Object.keys(added).map(lower));
|
|
1535
|
+
const kept = args['clear-headers']
|
|
1536
|
+
? {}
|
|
1537
|
+
: Object.fromEntries(Object.entries(job.headers ?? {}).filter(([n]) => !replaced.has(lower(n)) && !removed.includes(lower(n))));
|
|
1538
|
+
patchBody.headers = { ...kept, ...added };
|
|
1539
|
+
}
|
|
1540
|
+
if (Object.keys(patchBody).length === 0) die('nothing to change. Give at least one option (canner jobs edit <id> --help-style usage above).', 2);
|
|
1541
|
+
|
|
1542
|
+
const body = await apiJSON(creds, 'PATCH', `/projects/${encodeURIComponent(job.project_slug)}/jobs/${job.id}`, patchBody);
|
|
1543
|
+
if (JSON_MODE) return emitJSON(body);
|
|
1544
|
+
info(`${green('updated')} ${bold(body.job?.name ?? job.name)}`);
|
|
1545
|
+
info(` calls ${body.job?.url ?? job.url}`);
|
|
1546
|
+
info(` headers ${describeJobHeaders(body.job?.headers)}`);
|
|
1547
|
+
for (const w of body.warnings ?? []) info(`${yellow('warning')} ${w.message}`);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1493
1551
|
if (sub === 'delete' || sub === 'rm') {
|
|
1494
1552
|
if (!args._[1]) die('usage: canner jobs delete <id> --yes', 2);
|
|
1495
1553
|
const job = await findJob(creds, args._[1]);
|
|
@@ -1500,7 +1558,7 @@ async function cmdJobs(args) {
|
|
|
1500
1558
|
return;
|
|
1501
1559
|
}
|
|
1502
1560
|
|
|
1503
|
-
die('usage: canner jobs [list] | create <name> ... | run <id> | runs <id> | delete <id> --yes', 2);
|
|
1561
|
+
die('usage: canner jobs [list] | create <name> ... | edit <id> ... | run <id> | runs <id> | delete <id> --yes', 2);
|
|
1504
1562
|
}
|
|
1505
1563
|
|
|
1506
1564
|
async function cmdDb(args) {
|
|
@@ -2821,11 +2879,13 @@ ${bold('Databases:')}
|
|
|
2821
2879
|
Create the primary on its own dedicated instance with
|
|
2822
2880
|
point-in-time recovery. If the project has no unit, this
|
|
2823
2881
|
buys one: it shows the price and needs --yes.
|
|
2824
|
-
${bold('jobs')} [list] | create <name> (--path /p [--branch B] | --url U) --schedule "CRON" | run <id> | runs <id> | delete <id> --yes
|
|
2882
|
+
${bold('jobs')} [list] | create <name> (--path /p [--branch B] | --url U) --schedule "CRON" | edit <id> | run <id> | runs <id> | delete <id> --yes
|
|
2825
2883
|
Scheduled jobs. --path calls a path in the project itself
|
|
2826
2884
|
(production, or a preview with --branch). A header can read a
|
|
2827
2885
|
project variable: --header-env x-cron-key=CRON_KEY or
|
|
2828
|
-
--bearer-env CRON_SECRET.
|
|
2886
|
+
--bearer-env CRON_SECRET. "jobs edit <id>" changes only the
|
|
2887
|
+
options you give (--schedule, --path, --header-env,
|
|
2888
|
+
--remove-header, --pause / --resume ...).
|
|
2829
2889
|
${bold('billing')} addons | preview <addon> <quantity>
|
|
2830
2890
|
See the add-ons you own and where they are assigned, and
|
|
2831
2891
|
preview a quantity change. Read-only; nothing is charged.
|