@canner-ca/cli 0.3.0 → 0.4.0
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 +51 -3
- package/package.json +1 -1
- package/src/cli.mjs +57 -7
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
|
|
@@ -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)}`);
|
|
@@ -809,7 +857,7 @@ async function cmdOpen(args) {
|
|
|
809
857
|
const dashboardOrigin = (creds.apiBase || DEFAULT_API_BASE).replace(/^https?:\/\/api\./, 'https://');
|
|
810
858
|
const url = args.dashboard
|
|
811
859
|
? `${dashboardOrigin}/dashboard/projects/${slug}`
|
|
812
|
-
: `https://${slug}.
|
|
860
|
+
: `https://${slug}.canner.app`;
|
|
813
861
|
info(`opening ${dim(url)}`);
|
|
814
862
|
const { spawn } = await import('node:child_process');
|
|
815
863
|
const platform = process.platform;
|
|
@@ -913,7 +961,7 @@ async function cmdDeploy(args) {
|
|
|
913
961
|
info(`${dim('packaged')} ${fileCount} files (${sizeMb} MB compressed)`);
|
|
914
962
|
|
|
915
963
|
// Upload. fs.openAsBlob() backs the Blob by the on-disk file lazily —
|
|
916
|
-
// bytes only flow when fetch consumes them, so a 500 MB
|
|
964
|
+
// bytes only flow when fetch consumes them, so a 500 MB Live upload
|
|
917
965
|
// doesn't buffer into RAM on the user's machine. Node fetch has no
|
|
918
966
|
// upload-progress hook as of 2026, so we just print the size we're
|
|
919
967
|
// sending; the dashboard shows real-time progress for browser uploads.
|
|
@@ -1011,7 +1059,7 @@ async function tailDeploymentLogs(creds, slug, deploymentId) {
|
|
|
1011
1059
|
}
|
|
1012
1060
|
|
|
1013
1061
|
if (lastStatus === 'live') {
|
|
1014
|
-
info(`\n${green('live')} at https://${slug}.
|
|
1062
|
+
info(`\n${green('live')} at https://${slug}.canner.app`);
|
|
1015
1063
|
return 0;
|
|
1016
1064
|
}
|
|
1017
1065
|
if (lastStatus === 'failed') {
|
|
@@ -1078,6 +1126,7 @@ ${bold('Usage:')}
|
|
|
1078
1126
|
|
|
1079
1127
|
${bold('Setup:')}
|
|
1080
1128
|
${bold('login')} Store your Canner API token (one-time).
|
|
1129
|
+
${bold('logout')} Remove the stored token from this machine.
|
|
1081
1130
|
${bold('whoami')} Show the account this CLI is signed in as.
|
|
1082
1131
|
|
|
1083
1132
|
${bold('Projects:')}
|
|
@@ -1175,6 +1224,7 @@ if (!command || args.help || args.h || command === 'help') {
|
|
|
1175
1224
|
|
|
1176
1225
|
const dispatch = {
|
|
1177
1226
|
login: cmdLogin,
|
|
1227
|
+
logout: cmdLogout,
|
|
1178
1228
|
whoami: cmdWhoami,
|
|
1179
1229
|
init: cmdInit,
|
|
1180
1230
|
deploy: cmdDeploy,
|