@canner-ca/cli 0.5.6 → 0.5.8
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 +72 -6
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.8';
|
|
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');
|
|
@@ -828,7 +828,13 @@ async function cmdDomains(args) {
|
|
|
828
828
|
if (JSON_MODE) return emitJSON(body);
|
|
829
829
|
const d = body?.domain ?? {};
|
|
830
830
|
info(`${green('added')} ${bold(d.hostname ?? hostname)} ${dim(`(${d.status ?? 'pending'})`)}`);
|
|
831
|
-
if (body?.
|
|
831
|
+
if (body?.acme_cname) {
|
|
832
|
+
// A wildcard (*.example.com) needs two records: the wildcard itself, and the certificate delegation.
|
|
833
|
+
const host = d.hostname ?? hostname;
|
|
834
|
+
muted(`Add these two DNS records, then run \`canner domains verify '${host}'\`:`);
|
|
835
|
+
muted(` CNAME ${host} -> ${body.cname_target}`);
|
|
836
|
+
muted(` CNAME ${body.acme_cname.name} -> ${body.acme_cname.target} (lets us issue and renew the certificate; keep it in place)`);
|
|
837
|
+
} else if (body?.platform_ip) muted(`Point an A record at ${body.platform_ip}, then run \`canner domains verify ${hostname}\`.`);
|
|
832
838
|
return;
|
|
833
839
|
}
|
|
834
840
|
|
|
@@ -1129,6 +1135,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
1129
1135
|
'json', 'reveal', 'sensitive', 'follow', 'dashboard', 'version', 'help',
|
|
1130
1136
|
'replace', 'yes', 'no-wait', 'enable', 'disable', 'restore', 'download', 'rm', 'delete', 'instance', 'pitr',
|
|
1131
1137
|
'read-only', 'read-write', 'replication', 'group', 'login', 'adopt', 'strict', 'dedicated',
|
|
1138
|
+
'pause', 'resume', 'production', 'clear-headers',
|
|
1132
1139
|
]);
|
|
1133
1140
|
|
|
1134
1141
|
const splitList = (v) => String(v).split(',').map((x) => x.trim()).filter(Boolean);
|
|
@@ -1370,6 +1377,11 @@ async function cmdBilling(args) {
|
|
|
1370
1377
|
// [--header "name=value" ...] [--header-env name=VARIABLE ...] [--bearer-env VARIABLE]
|
|
1371
1378
|
// (--path calls a path in the project itself: its address is built from the project's own host each time, in
|
|
1372
1379
|
// production or, with --branch, on that preview; --url is for anything else)
|
|
1380
|
+
// canner jobs edit <id> [--name N] [--schedule "CRON"] [--description D] [--pause | --resume]
|
|
1381
|
+
// [--path /p [--branch B | --production] | --url U]
|
|
1382
|
+
// [--header-env name=VARIABLE ...] [--bearer-env VARIABLE] [--header name=value ...]
|
|
1383
|
+
// [--remove-header name ...] [--clear-headers]
|
|
1384
|
+
// (edit changes only what is given. A header given by name replaces the one with that name; the others stay.)
|
|
1373
1385
|
// canner jobs run <id> run it now and show what came back
|
|
1374
1386
|
// canner jobs runs <id> recent runs, with what to check on a failure
|
|
1375
1387
|
// canner jobs delete <id> --yes
|
|
@@ -1381,6 +1393,16 @@ async function cmdBilling(args) {
|
|
|
1381
1393
|
// ---------------------------------------------------------------------------
|
|
1382
1394
|
const asList = (v) => (v === undefined || v === true ? [] : Array.isArray(v) ? v : [v]);
|
|
1383
1395
|
|
|
1396
|
+
// A path argument, as the API wants it (leading slash). Git Bash on Windows rewrites an argument that starts with
|
|
1397
|
+
// "/" into a Windows path (C:/Program Files/Git/api/cron) before any program sees it; say so instead of sending that.
|
|
1398
|
+
function pathArg(raw) {
|
|
1399
|
+
const p = String(raw);
|
|
1400
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) {
|
|
1401
|
+
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);
|
|
1402
|
+
}
|
|
1403
|
+
return p.startsWith('//') && !p.startsWith('///') ? p.slice(1) : p.startsWith('/') ? p : `/${p}`;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1384
1406
|
function jobHeadersFromArgs(args) {
|
|
1385
1407
|
const headers = {};
|
|
1386
1408
|
const put = (name, value) => {
|
|
@@ -1449,7 +1471,7 @@ async function cmdJobs(args) {
|
|
|
1449
1471
|
const headers = jobHeadersFromArgs(args);
|
|
1450
1472
|
const body = await apiJSON(creds, 'POST', `/projects/${encodeURIComponent(slug)}/jobs`, {
|
|
1451
1473
|
name, schedule: String(args.schedule),
|
|
1452
|
-
...(args.path ? { path:
|
|
1474
|
+
...(args.path ? { path: pathArg(args.path), ...(args.branch ? { branch: String(args.branch) } : {}) } : { url: String(args.url) }),
|
|
1453
1475
|
...(args.description ? { description: String(args.description) } : {}),
|
|
1454
1476
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
1455
1477
|
});
|
|
@@ -1490,6 +1512,48 @@ async function cmdJobs(args) {
|
|
|
1490
1512
|
return;
|
|
1491
1513
|
}
|
|
1492
1514
|
|
|
1515
|
+
if (sub === 'edit' || sub === 'update') {
|
|
1516
|
+
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);
|
|
1517
|
+
if (args.url && args.path) die('give either --url or --path, not both.', 2);
|
|
1518
|
+
if (args.branch && args.production) die('give either --branch or --production, not both.', 2);
|
|
1519
|
+
if (args.pause && args.resume) die('give either --pause or --resume, not both.', 2);
|
|
1520
|
+
if ((args.branch || args.production) && args.url) die('--branch / --production go with --path.', 2);
|
|
1521
|
+
const job = await findJob(creds, args._[1]);
|
|
1522
|
+
|
|
1523
|
+
const patchBody = {};
|
|
1524
|
+
if (args.name) patchBody.name = String(args.name);
|
|
1525
|
+
if (args.schedule) patchBody.schedule = String(args.schedule);
|
|
1526
|
+
if (args.description !== undefined && args.description !== true) patchBody.description = String(args.description);
|
|
1527
|
+
if (args.pause) patchBody.is_active = false;
|
|
1528
|
+
if (args.resume) patchBody.is_active = true;
|
|
1529
|
+
if (args.path) patchBody.path = pathArg(args.path);
|
|
1530
|
+
if (args.url) patchBody.url = String(args.url);
|
|
1531
|
+
if (args.branch) patchBody.branch = String(args.branch);
|
|
1532
|
+
if (args.production) patchBody.branch = null;
|
|
1533
|
+
|
|
1534
|
+
// Headers: start from what the job has (references as written, literal values hidden — the server keeps
|
|
1535
|
+
// a hidden value as is), drop what was asked to go, and let a header given by name replace its namesake.
|
|
1536
|
+
const added = jobHeadersFromArgs(args);
|
|
1537
|
+
const removed = asList(args['remove-header']).map((n) => String(n).toLowerCase());
|
|
1538
|
+
if (Object.keys(added).length > 0 || removed.length > 0 || args['clear-headers']) {
|
|
1539
|
+
const lower = (n) => n.toLowerCase();
|
|
1540
|
+
const replaced = new Set(Object.keys(added).map(lower));
|
|
1541
|
+
const kept = args['clear-headers']
|
|
1542
|
+
? {}
|
|
1543
|
+
: Object.fromEntries(Object.entries(job.headers ?? {}).filter(([n]) => !replaced.has(lower(n)) && !removed.includes(lower(n))));
|
|
1544
|
+
patchBody.headers = { ...kept, ...added };
|
|
1545
|
+
}
|
|
1546
|
+
if (Object.keys(patchBody).length === 0) die('nothing to change. Give at least one option (canner jobs edit <id> --help-style usage above).', 2);
|
|
1547
|
+
|
|
1548
|
+
const body = await apiJSON(creds, 'PATCH', `/projects/${encodeURIComponent(job.project_slug)}/jobs/${job.id}`, patchBody);
|
|
1549
|
+
if (JSON_MODE) return emitJSON(body);
|
|
1550
|
+
info(`${green('updated')} ${bold(body.job?.name ?? job.name)}`);
|
|
1551
|
+
info(` calls ${body.job?.url ?? job.url}`);
|
|
1552
|
+
info(` headers ${describeJobHeaders(body.job?.headers)}`);
|
|
1553
|
+
for (const w of body.warnings ?? []) info(`${yellow('warning')} ${w.message}`);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1493
1557
|
if (sub === 'delete' || sub === 'rm') {
|
|
1494
1558
|
if (!args._[1]) die('usage: canner jobs delete <id> --yes', 2);
|
|
1495
1559
|
const job = await findJob(creds, args._[1]);
|
|
@@ -1500,7 +1564,7 @@ async function cmdJobs(args) {
|
|
|
1500
1564
|
return;
|
|
1501
1565
|
}
|
|
1502
1566
|
|
|
1503
|
-
die('usage: canner jobs [list] | create <name> ... | run <id> | runs <id> | delete <id> --yes', 2);
|
|
1567
|
+
die('usage: canner jobs [list] | create <name> ... | edit <id> ... | run <id> | runs <id> | delete <id> --yes', 2);
|
|
1504
1568
|
}
|
|
1505
1569
|
|
|
1506
1570
|
async function cmdDb(args) {
|
|
@@ -2821,11 +2885,13 @@ ${bold('Databases:')}
|
|
|
2821
2885
|
Create the primary on its own dedicated instance with
|
|
2822
2886
|
point-in-time recovery. If the project has no unit, this
|
|
2823
2887
|
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
|
|
2888
|
+
${bold('jobs')} [list] | create <name> (--path /p [--branch B] | --url U) --schedule "CRON" | edit <id> | run <id> | runs <id> | delete <id> --yes
|
|
2825
2889
|
Scheduled jobs. --path calls a path in the project itself
|
|
2826
2890
|
(production, or a preview with --branch). A header can read a
|
|
2827
2891
|
project variable: --header-env x-cron-key=CRON_KEY or
|
|
2828
|
-
--bearer-env CRON_SECRET.
|
|
2892
|
+
--bearer-env CRON_SECRET. "jobs edit <id>" changes only the
|
|
2893
|
+
options you give (--schedule, --path, --header-env,
|
|
2894
|
+
--remove-header, --pause / --resume ...).
|
|
2829
2895
|
${bold('billing')} addons | preview <addon> <quantity>
|
|
2830
2896
|
See the add-ons you own and where they are assigned, and
|
|
2831
2897
|
preview a quantity change. Read-only; nothing is charged.
|