@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.
- package/README.md +52 -4
- package/package.json +1 -1
- 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
|
-
-
|
|
152
|
-
- **
|
|
153
|
-
- **
|
|
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
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.
|
|
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}.
|
|
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}.
|
|
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}.
|
|
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]
|
|
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
|
-
|
|
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}.
|
|
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
|
|
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}.
|
|
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,
|