@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canner-ca/cli",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Canner deploy CLI — git push from any terminal, no GitHub required",
5
5
  "type": "module",
6
6
  "bin": {
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.6';
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?.platform_ip) muted(`Point an A record at ${body.platform_ip}, then run \`canner domains verify ${hostname}\`.`);
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: String(args.path), ...(args.branch ? { branch: String(args.branch) } : {}) } : { url: String(args.url) }),
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.