@canner-ca/cli 0.3.0 → 0.4.1

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 +52 -4
  2. package/package.json +1 -1
  3. package/src/cli.mjs +76 -12
package/README.md CHANGED
@@ -38,6 +38,7 @@ canner deploy --follow
38
38
  | Command | What it does |
39
39
  |---|---|
40
40
  | `canner login` | Prompt for an API token, store it at `~/.canner/credentials` (mode 0600). |
41
+ | `canner logout` | Delete `~/.canner/credentials` from this machine. |
41
42
  | `canner whoami` | Show the email + plan this CLI is signed in as. |
42
43
 
43
44
  ### Projects
@@ -67,7 +68,7 @@ canner deploy --follow
67
68
  | `canner token list` | List your `cnr_` API tokens. |
68
69
  | `canner token create <name>` | Mint a new token. The secret is printed once — copy it immediately. |
69
70
  | `canner token revoke <id\|name>` | Revoke a token. |
70
- | `canner env [list] [--slug X]` | List a project's environment variables. |
71
+ | `canner env [list] [--reveal] [--slug X]` | List a project's environment variables. Values marked sensitive are redacted (`••••••`, or `null` in `--json`) unless `--reveal` is passed. |
71
72
  | `canner env set KEY=VALUE [--env production\|preview] [--sensitive] [--slug X]` | Add an environment variable. |
72
73
  | `canner env rm <KEY\|id> [--env ...] [--slug X]` | Delete an environment variable. |
73
74
  | `canner domains [list] [--slug X]` | List a project's custom domains. |
@@ -148,12 +149,59 @@ canner deploy --follow
148
149
 
149
150
  The CLI uploads under the same per-tier compressed-archive cap as the dashboard:
150
151
 
151
- - **Starter** — 50 MB
152
- - **Basic** — 200 MB
153
- - **Pro** — 500 MB
152
+ <!-- BEGIN:GENERATED:cli-caps -->
153
+ - **Starter** — 100 MB
154
+ - **Live** — 500 MB
155
+ - **Dedicated** — 1 GB
156
+ <!-- END:GENERATED:cli-caps -->
154
157
 
155
158
  If your project is bigger than that compressed, drop heavier directories into `.cannerignore` or upgrade your plan. `node_modules` is already excluded; the typical thing that pushes a project over the cap is a `public/` folder full of media.
156
159
 
160
+ ## What this CLI stores and sends
161
+
162
+ **Written to your machine — two files, both plain text you can read:**
163
+
164
+ | Path | Contents | Written by |
165
+ |---|---|---|
166
+ | `~/.canner/credentials` | `{ api_base, token }`, mode `0600` | `canner login` |
167
+ | `./canner.json` | Your project slug, in the project directory | `canner init` |
168
+
169
+ Nothing else is written. No system settings, shell profiles, PATH entries, or
170
+ startup items are modified.
171
+
172
+ **Sent over the network:** requests go only to `https://api.canner.ca` (or
173
+ whatever `CANNER_API_BASE` is set to). `canner deploy` uploads a tar of your
174
+ project directory — minus `node_modules`, `.git`, build output, and anything in
175
+ your `.cannerignore` — so that we can build and host it.
176
+
177
+ **Telemetry: none.** There is no usage tracking, analytics, crash reporting, or
178
+ phone-home of any kind. The CLI makes no network request you didn't ask for by
179
+ running a command.
180
+
181
+ ## Removing the CLI
182
+
183
+ ```bash
184
+ canner logout # delete ~/.canner/credentials
185
+ npm uninstall -g @canner-ca/cli # remove the package
186
+ ```
187
+
188
+ `canner logout` removes the stored credential from **this machine only**. The
189
+ token itself stays valid — the same token may be in use in CI or on another
190
+ machine, so logging out on your laptop deliberately doesn't kill it. To revoke
191
+ it everywhere, run `canner token revoke <name>` before logging out, or revoke it
192
+ from your dashboard's API tokens page.
193
+
194
+ If you've already uninstalled the package and want to clear the leftover
195
+ credential, delete `~/.canner/credentials` by hand. Removing it affects nothing
196
+ else; `canner.json` in a project directory is just a slug and is safe to keep or
197
+ delete.
198
+
199
+ ## Terms
200
+
201
+ Use of the CLI and the Canner service is governed by the
202
+ [Terms of Service](https://canner.ca/terms) and the
203
+ [Privacy Policy](https://canner.ca/privacy).
204
+
157
205
  ## License
158
206
 
159
207
  MIT.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canner-ca/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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
@@ -9,6 +9,7 @@
9
9
  //
10
10
  // Commands:
11
11
  // canner login store an API token
12
+ // canner logout remove the stored token from this machine
12
13
  // canner whoami show the account this CLI is signed in as
13
14
  // canner init [--slug X] create a project, write canner.json
14
15
  // canner deploy [path] [--slug X] [--follow]
@@ -27,7 +28,7 @@ import { randomUUID } from 'node:crypto';
27
28
  import { stdin as input, stdout as output } from 'node:process';
28
29
  import { create as tarCreate } from 'tar';
29
30
 
30
- const VERSION = '0.3.0';
31
+ const VERSION = '0.4.0';
31
32
  const DEFAULT_API_BASE = 'https://api.canner.ca';
32
33
  const CONFIG_DIR = join(homedir(), '.canner');
33
34
  const CREDENTIALS_PATH = join(CONFIG_DIR, 'credentials');
@@ -311,6 +312,53 @@ async function cmdWhoami() {
311
312
  }
312
313
  }
313
314
 
315
+ // Remove the stored credential from this machine.
316
+ //
317
+ // `npm uninstall -g @canner-ca/cli` leaves ~/.canner/credentials behind — a
318
+ // live bearer token sitting in the user's home directory with no obvious way
319
+ // to clear it. That's the gap this closes: uninstalling should not leave a
320
+ // working credential on disk.
321
+ //
322
+ // Deliberately local-only. We delete the file we wrote; we do NOT revoke the
323
+ // token server-side, because the same token may be in use by CI, the MCP
324
+ // server, or another machine, and silently killing those from a `logout` on
325
+ // one laptop would be a surprising side effect. The output says so plainly and
326
+ // points at `canner token revoke` for people who do want it gone everywhere.
327
+ async function cmdLogout() {
328
+ const creds = await loadCredentials();
329
+ let removed = false;
330
+ try {
331
+ await unlink(CREDENTIALS_PATH);
332
+ removed = true;
333
+ } catch (err) {
334
+ // ENOENT just means there was nothing stored — that's a success, not an
335
+ // error. Anything else (EACCES, EPERM) the user needs to know about.
336
+ if (err?.code !== 'ENOENT') {
337
+ die(`could not remove ${CREDENTIALS_PATH}: ${err.message}`);
338
+ }
339
+ }
340
+ if (JSON_MODE) {
341
+ return emitJSON({ removed, path: CREDENTIALS_PATH });
342
+ }
343
+ if (!removed) {
344
+ info('Nothing to remove — no stored credential on this machine.');
345
+ // A token in the environment survives logout, and a user who doesn't know
346
+ // that will think they've signed out when they haven't.
347
+ if (process.env.CANNER_TOKEN) {
348
+ muted('Note: CANNER_TOKEN is set in your environment and still applies.');
349
+ }
350
+ return;
351
+ }
352
+ info(`${green('Signed out.')} Removed ${CREDENTIALS_PATH}`);
353
+ if (creds.token) {
354
+ muted('The token itself is still valid. To revoke it everywhere, run');
355
+ muted(` canner token revoke <name> ${dim('(or use the dashboard)')}`);
356
+ }
357
+ if (process.env.CANNER_TOKEN) {
358
+ muted('Note: CANNER_TOKEN is set in your environment and still applies.');
359
+ }
360
+ }
361
+
314
362
  async function cmdInit(args) {
315
363
  const cwd = process.cwd();
316
364
  const existing = await loadProjectConfig(cwd);
@@ -342,7 +390,7 @@ async function cmdInit(args) {
342
390
  if (!body) die('project creation returned no body.');
343
391
  await writeProjectConfig(cwd, { slug });
344
392
  info(`${green('Created')} ${bold(slug)}`);
345
- muted(`Will be served at https://${slug}.app.canner.ca after the first deploy.`);
393
+ muted(`Will be served at https://${slug}.canner.app after the first deploy.`);
346
394
  muted(`Wrote ${PROJECT_CONFIG_FILE}. Run \`canner deploy\` to ship.`);
347
395
  }
348
396
 
@@ -400,7 +448,7 @@ async function cmdProjects() {
400
448
  }
401
449
  info(`${pad(bold('SLUG'), 24)}${pad(bold('STATUS'), 14)}${pad(bold('URL'), 40)}${bold('LAST REQUEST')}`);
402
450
  for (const p of rows) {
403
- const url = `https://${p.slug}.app.canner.ca`;
451
+ const url = `https://${p.slug}.canner.app`;
404
452
  const lastReq = relTime(p.last_request_at);
405
453
  info(`${pad(p.slug, 24)}${pad(colorStatus(p.status), 14)}${pad(dim(url), 40)}${dim(lastReq)}`);
406
454
  }
@@ -419,7 +467,7 @@ async function cmdStatus(args) {
419
467
  if (JSON_MODE) return emitJSON(p);
420
468
  info(`${bold(p.slug)} ${dim(`(${p.name ?? ''})`)}`);
421
469
  info(` status: ${colorStatus(p.status)}${p.is_cold ? dim(' (cold)') : ''}`);
422
- info(` url: ${cyan(`https://${p.slug}.app.canner.ca`)}`);
470
+ info(` url: ${cyan(`https://${p.slug}.canner.app`)}`);
423
471
  info(` source: ${p.is_connected ? `${dim('github')} ${p.repo_url ?? ''} (branch ${p.repo_branch})` : dim('upload-only')}`);
424
472
  info(` memory: ${p.memory_limit_mb}M${p.always_warm ? ` ${dim('(always-warm)')}` : ''}`);
425
473
  info(` last request: ${relTime(p.last_request_at)}`);
@@ -630,7 +678,8 @@ async function resolveTokenId(creds, ref) {
630
678
  }
631
679
 
632
680
  // ---------------------------------------------------------------------------
633
- // canner env [--slug X] — list env vars
681
+ // canner env [--reveal] [--slug X] — list env vars
682
+ // (sensitive values redacted by default, in text AND --json; --reveal opts out)
634
683
  // canner env set KEY=VALUE [--env production|preview] [--sensitive] [--slug X]
635
684
  // canner env rm <KEY|id> [--env ...] [--slug X] — delete an env var
636
685
  // ---------------------------------------------------------------------------
@@ -644,11 +693,21 @@ async function cmdEnv(args) {
644
693
  if (sub === 'list') {
645
694
  const body = await apiJSON(creds, 'GET', base);
646
695
  const rows = body?.env_vars ?? [];
647
- if (JSON_MODE) return emitJSON(rows);
696
+ // Sensitive values are redacted by default in BOTH output modes — the API
697
+ // returns them decrypted (the owner can read their own secrets back), but
698
+ // a plain `env list` (human or --json) is a read-only config check, not a
699
+ // secret-reveal action. --reveal is the explicit opt-in for scripts that
700
+ // actually need the value (e.g. syncing it elsewhere).
701
+ const reveal = args.reveal === true;
702
+ if (JSON_MODE) {
703
+ return emitJSON(
704
+ reveal ? rows : rows.map((v) => (v.is_sensitive ? { ...v, value: null } : v))
705
+ );
706
+ }
648
707
  if (rows.length === 0) { muted('no environment variables.'); return; }
649
708
  info(`${pad(bold('KEY'), 28)}${pad(bold('ENV'), 14)}${pad(bold('SENSITIVE'), 12)}${bold('VALUE')}`);
650
709
  for (const v of rows) {
651
- const val = v.is_sensitive ? dim('••••••') : (v.value ?? '').split('\n')[0].slice(0, 48);
710
+ const val = v.is_sensitive && !reveal ? dim('••••••') : (v.value ?? '').split('\n')[0].slice(0, 48);
652
711
  info(`${pad(v.key, 28)}${pad(dim(v.environment ?? 'all'), 14)}${pad(v.is_sensitive ? yellow('yes') : dim('no'), 12)}${dim(val)}`);
653
712
  }
654
713
  return;
@@ -809,7 +868,7 @@ async function cmdOpen(args) {
809
868
  const dashboardOrigin = (creds.apiBase || DEFAULT_API_BASE).replace(/^https?:\/\/api\./, 'https://');
810
869
  const url = args.dashboard
811
870
  ? `${dashboardOrigin}/dashboard/projects/${slug}`
812
- : `https://${slug}.app.canner.ca`;
871
+ : `https://${slug}.canner.app`;
813
872
  info(`opening ${dim(url)}`);
814
873
  const { spawn } = await import('node:child_process');
815
874
  const platform = process.platform;
@@ -913,7 +972,7 @@ async function cmdDeploy(args) {
913
972
  info(`${dim('packaged')} ${fileCount} files (${sizeMb} MB compressed)`);
914
973
 
915
974
  // Upload. fs.openAsBlob() backs the Blob by the on-disk file lazily —
916
- // bytes only flow when fetch consumes them, so a 500 MB Pro upload
975
+ // bytes only flow when fetch consumes them, so a 500 MB Live upload
917
976
  // doesn't buffer into RAM on the user's machine. Node fetch has no
918
977
  // upload-progress hook as of 2026, so we just print the size we're
919
978
  // sending; the dashboard shows real-time progress for browser uploads.
@@ -1011,7 +1070,7 @@ async function tailDeploymentLogs(creds, slug, deploymentId) {
1011
1070
  }
1012
1071
 
1013
1072
  if (lastStatus === 'live') {
1014
- info(`\n${green('live')} at https://${slug}.app.canner.ca`);
1073
+ info(`\n${green('live')} at https://${slug}.canner.app`);
1015
1074
  return 0;
1016
1075
  }
1017
1076
  if (lastStatus === 'failed') {
@@ -1078,6 +1137,7 @@ ${bold('Usage:')}
1078
1137
 
1079
1138
  ${bold('Setup:')}
1080
1139
  ${bold('login')} Store your Canner API token (one-time).
1140
+ ${bold('logout')} Remove the stored token from this machine.
1081
1141
  ${bold('whoami')} Show the account this CLI is signed in as.
1082
1142
 
1083
1143
  ${bold('Projects:')}
@@ -1108,10 +1168,12 @@ ${bold('Deploys:')}
1108
1168
  ${bold('Resources:')}
1109
1169
  ${bold('token')} list | create <name> | revoke <id|name>
1110
1170
  Manage the cnr_ API tokens that authenticate this CLI.
1111
- ${bold('env')} [list] [--slug X]
1171
+ ${bold('env')} [list] [--reveal] [--slug X]
1112
1172
  ${bold('env')} set KEY=VALUE [--env production|preview] [--sensitive] [--slug X]
1113
1173
  ${bold('env')} rm <KEY|id> [--env ...] [--slug X]
1114
- Manage a project's environment variables.
1174
+ Manage a project's environment variables. Sensitive
1175
+ values are redacted as •••••• (or null in --json) on
1176
+ list; pass --reveal to see them in cleartext.
1115
1177
  ${bold('domains')} [list] [--slug X]
1116
1178
  ${bold('domains')} add <hostname> [--slug X]
1117
1179
  ${bold('domains')} verify <hostname|id> [--slug X]
@@ -1135,6 +1197,7 @@ ${bold('Examples:')}
1135
1197
  canner open --dashboard ${dim('# open dashboard project page')}
1136
1198
  canner token create ci ${dim('# mint a token named "ci"')}
1137
1199
  canner env set API_KEY=secret --sensitive ${dim('# add a hidden env var')}
1200
+ canner env list --reveal ${dim('# show sensitive values in cleartext')}
1138
1201
  canner domains add www.example.com ${dim('# attach a custom domain')}
1139
1202
  canner projects --json | jq '.[].slug' ${dim('# machine-readable output')}
1140
1203
 
@@ -1175,6 +1238,7 @@ if (!command || args.help || args.h || command === 'help') {
1175
1238
 
1176
1239
  const dispatch = {
1177
1240
  login: cmdLogin,
1241
+ logout: cmdLogout,
1178
1242
  whoami: cmdWhoami,
1179
1243
  init: cmdInit,
1180
1244
  deploy: cmdDeploy,