@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canner-ca/cli",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
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.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: String(args.path), ...(args.branch ? { branch: String(args.branch) } : {}) } : { url: String(args.url) }),
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.