carboncanvas-cli 0.1.1 → 0.1.2
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/CONTRACT.md +112 -0
- package/README.md +87 -6
- package/dist/canvasApi.js +102 -0
- package/dist/canvasApi.js.map +1 -0
- package/dist/canvasesCommand.js +25 -0
- package/dist/canvasesCommand.js.map +1 -0
- package/dist/constants.js +10 -4
- package/dist/constants.js.map +1 -1
- package/dist/deviceAuth.js +137 -0
- package/dist/deviceAuth.js.map +1 -0
- package/dist/envFiles.js +110 -0
- package/dist/envFiles.js.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/initCommand.js +172 -0
- package/dist/initCommand.js.map +1 -0
- package/dist/loginCommand.js +66 -7
- package/dist/loginCommand.js.map +1 -1
- package/dist/prompt.js +18 -3
- package/dist/prompt.js.map +1 -1
- package/dist/session.js +52 -0
- package/dist/session.js.map +1 -0
- package/dist/tokenStore.js +22 -4
- package/dist/tokenStore.js.map +1 -1
- package/package.json +1 -1
package/CONTRACT.md
CHANGED
|
@@ -92,3 +92,115 @@ re-checks (a hand-crafted request can skip the CLI):
|
|
|
92
92
|
server should still enforce (→ 400 / 413).
|
|
93
93
|
- **Static-ness:** `index.html` at root, no server manifest (`.next/standalone`,
|
|
94
94
|
`.output/server`, `functions/`, `server.js`/`server.mjs`, `_worker.js`).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
# CLI auth contract (`/api/cli-auth/*` — device-code flow)
|
|
99
|
+
|
|
100
|
+
Added 2026-07-08 (Wave B). Source of truth for `carboncanvas login`'s
|
|
101
|
+
browser-approval flow; the server half must match byte-for-byte. The paste-token
|
|
102
|
+
path (dashboard reveal panel → interactive prompt) remains as the fallback.
|
|
103
|
+
Designed to be reused by a future MCP grant flow — nothing in it is CLI-specific.
|
|
104
|
+
|
|
105
|
+
Works everywhere the CLI runs (laptop, Replit/Bolt cloud shell, container): the
|
|
106
|
+
CLI never receives a browser callback — it POLLS. The user approves in their
|
|
107
|
+
signed-in dashboard at `/activate`.
|
|
108
|
+
|
|
109
|
+
## 1. `POST {serviceUrl}/api/cli-auth/start` — begin a grant (public, rate-limited)
|
|
110
|
+
|
|
111
|
+
Request body: `{}` (no fields yet; senders MUST send valid JSON).
|
|
112
|
+
|
|
113
|
+
`200` →
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"deviceCode": "dc_<high-entropy secret, CLI-side only, never shown to the user>",
|
|
117
|
+
"userCode": "ABCD-1234",
|
|
118
|
+
"verifyUrl": "https://<host>/activate",
|
|
119
|
+
"expiresIn": 900,
|
|
120
|
+
"interval": 5
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
`userCode`: 8 chars from an unambiguous uppercase alphabet (no `0/O/1/I`),
|
|
124
|
+
displayed and compared case-insensitively, hyphen optional on entry. Single-use,
|
|
125
|
+
expires with the grant. The server stores only a hash of `deviceCode`.
|
|
126
|
+
|
|
127
|
+
## 2. `POST {serviceUrl}/api/cli-auth/poll` — CLI polls (public, rate-limited)
|
|
128
|
+
|
|
129
|
+
Request body: `{ "deviceCode": "dc_…" }`.
|
|
130
|
+
|
|
131
|
+
`200` responses by grant state:
|
|
132
|
+
- `{ "status": "pending" }` — keep polling at `interval` seconds.
|
|
133
|
+
- `{ "status": "approved", "token": "<session bearer token>" }` — ONE-SHOT: the
|
|
134
|
+
token is minted at claim time and the grant is consumed; a second poll after
|
|
135
|
+
this returns `{ "status": "expired" }`.
|
|
136
|
+
- `{ "status": "expired" }` — grant expired/denied/already-claimed; CLI restarts
|
|
137
|
+
or falls back to paste-token.
|
|
138
|
+
|
|
139
|
+
`429` → slow down; CLI doubles its interval for the next poll. Body error code:
|
|
140
|
+
`slow_down` on poll (device-flow convention); `start` uses the codebase-standard
|
|
141
|
+
`rate_limited`. The CLI branches only on the 429 status, never the code.
|
|
142
|
+
Unknown/garbage `deviceCode` → `{ "status": "expired" }` (no oracle).
|
|
143
|
+
|
|
144
|
+
## 3. `POST {serviceUrl}/api/cli-auth/approve` — dashboard approves (session-authed)
|
|
145
|
+
|
|
146
|
+
Called by the `/activate` dashboard page with the signed-in CREATOR's bearer
|
|
147
|
+
session (`Authorization: Bearer <token>`). Guest/reviewer sessions are rejected.
|
|
148
|
+
|
|
149
|
+
Request body: `{ "userCode": "ABCD-1234" }` (case-insensitive, hyphen optional).
|
|
150
|
+
|
|
151
|
+
- `200 { "ok": true }` — grant bound to this account; the CLI's next poll claims it.
|
|
152
|
+
- `404 { error, message }` — unknown, expired, or already-used code.
|
|
153
|
+
- `401 / 403` — no session / not a creator session.
|
|
154
|
+
|
|
155
|
+
All error bodies use the `{ error, message }` shape.
|
|
156
|
+
|
|
157
|
+
## CLI behavior (`carboncanvas login`)
|
|
158
|
+
|
|
159
|
+
1. `start` → print `verifyUrl` + `userCode` prominently; attempt to open the
|
|
160
|
+
browser locally (best-effort, never fail on error — cloud shells have no
|
|
161
|
+
browser); poll per `interval` until `approved`/`expired` (honor 429 backoff).
|
|
162
|
+
2. On `approved`: store the token in the token store keyed by `serviceUrl`
|
|
163
|
+
(existing mechanism), verify with `GET /api/entitlements`, greet.
|
|
164
|
+
3. On `expired` or user abort: offer the paste-token fallback.
|
|
165
|
+
|
|
166
|
+
## `carboncanvas init` (companion command; no new server surface)
|
|
167
|
+
|
|
168
|
+
Requires a stored token (runs the login flow inline if absent), then:
|
|
169
|
+
1. Chooses the canvas (amended 2026-07-08 — `--canvas` / `--new` / picker):
|
|
170
|
+
- `--canvas <cnv_…>` — link an EXISTING canvas instead of creating. Verified
|
|
171
|
+
against `GET /api/canvases?includeArchived=1` (existing endpoint →
|
|
172
|
+
`{ canvases: CanvasView[] }`), matched on `id`. Not in the account → exit 1
|
|
173
|
+
("check the ID in your dashboard"); archived → exit 1 (activate it first);
|
|
174
|
+
id without the `cnv_` prefix → exit 1 before any network call. Never POSTs.
|
|
175
|
+
- Plain `init`, account owns 0 non-archived canvases → create (step 1b), no
|
|
176
|
+
prompt — TTY or not (first-time fast path).
|
|
177
|
+
- Plain `init`, account owns ≥1 non-archived canvas (same GET, archived
|
|
178
|
+
excluded):
|
|
179
|
+
- TTY (stdin AND stdout): interactive picker — numbered `name id` list;
|
|
180
|
+
Enter = create new named `<resolved name>`, a number = link that one.
|
|
181
|
+
- non-TTY (agents/CI): NEVER guesses — prints the canvases (one
|
|
182
|
+
`<id> <name>` line each, stderr) and exits 1 telling the caller to
|
|
183
|
+
re-run with `--canvas <id>` or `--new`. Prevents surprise duplicates.
|
|
184
|
+
- `--new` — always create; skips the GET/picker/stop entirely. `--canvas`
|
|
185
|
+
and `--new` are mutually exclusive (exit 1).
|
|
186
|
+
1b. Create: `POST /api/canvases { name }` (name from `--name` or
|
|
187
|
+
package.json `name`) — existing endpoint, existing auth. `402/403` →
|
|
188
|
+
surface `message` (plan gate).
|
|
189
|
+
Auto-relogin (amended 2026-07-08): a `401` from either call above triggers
|
|
190
|
+
ONE inline device-code login ("Your sign-in expired — let's renew it."),
|
|
191
|
+
then a single retry of the failed call; a second `401` surfaces the error.
|
|
192
|
+
One relogin per run, never a loop.
|
|
193
|
+
2. Write `VITE_CC_CANVAS_ID=<id>` + `VITE_CC_BACKEND=<serviceUrl>` into `.env`
|
|
194
|
+
(create or append; NEVER overwrite an existing `VITE_CC_CANVAS_ID` — the id
|
|
195
|
+
is write-once; print the existing one and exit 1 instead), mirror the keys
|
|
196
|
+
into `.env.example` with placeholder values, ensure `.gitignore` covers `.env`.
|
|
197
|
+
Identical for created AND linked canvases (the write-once guard still runs
|
|
198
|
+
first, before any network call).
|
|
199
|
+
3. Print: the canvas id/name, what was written, and "tell your coding agent the
|
|
200
|
+
canvas is registered — it wires the comments prop from these env vars
|
|
201
|
+
(see <serviceUrl>/skill.md)". The id is the one machine-readable stdout line.
|
|
202
|
+
|
|
203
|
+
Sibling command (added 2026-07-08): `carboncanvas canvases` — lists the
|
|
204
|
+
account's canvases via the same `GET /api/canvases?includeArchived=1` (same
|
|
205
|
+
auth + one-shot relogin); one `<id> <name>` line per canvas on stdout, with a
|
|
206
|
+
trailing ` [archived]` marker on soft-archived ones. No new server surface.
|
package/README.md
CHANGED
|
@@ -19,20 +19,101 @@ npx carboncanvas publish
|
|
|
19
19
|
|
|
20
20
|
## `carboncanvas login`
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Signs you in via your **browser** (device-code flow): the CLI prints an
|
|
23
|
+
activation link + a short code, opens the browser when it can, and waits while
|
|
24
|
+
you approve the sign-in on the dashboard's `/activate` page. This works from
|
|
25
|
+
anywhere — laptops, cloud shells (Replit/Bolt), containers — because the CLI
|
|
26
|
+
only polls; it never needs a callback. The resulting token is verified and
|
|
27
|
+
stored in `~/.carboncanvas/config.json` (mode `0600`), keyed by backend URL.
|
|
25
28
|
|
|
26
29
|
```bash
|
|
27
|
-
carboncanvas login #
|
|
28
|
-
carboncanvas login --token cc_xxx # non-interactive
|
|
30
|
+
carboncanvas login # browser approval (link + code)
|
|
31
|
+
carboncanvas login --token cc_xxx # paste-token, non-interactive
|
|
29
32
|
```
|
|
30
33
|
|
|
34
|
+
- **Paste-token fallback:** if the browser flow expires, is denied, or the
|
|
35
|
+
backend doesn't support it, the CLI falls back to the classic hidden prompt
|
|
36
|
+
for a creator API token (dashboard → Settings → API tokens). `--token`
|
|
37
|
+
skips the browser flow entirely.
|
|
31
38
|
- `CARBON_TOKEN` overrides the stored token at use time.
|
|
32
39
|
- `--service-url <url>` / `CARBON_SERVICE_URL` selects the backend
|
|
33
|
-
(default `https://
|
|
40
|
+
(default `https://app.carboncanvas.design`).
|
|
34
41
|
- `carboncanvas logout` removes the stored token; `carboncanvas whoami` checks it.
|
|
35
42
|
|
|
43
|
+
## `carboncanvas init`
|
|
44
|
+
|
|
45
|
+
Registers (or links) a canvas for the current project and wires it into your
|
|
46
|
+
env files — run it once per project, then hand off to your coding agent.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
carboncanvas init # create or pick — see below
|
|
50
|
+
carboncanvas init --name my-app # explicit canvas name for a new canvas
|
|
51
|
+
carboncanvas init --canvas cnv_xxx # link an EXISTING canvas from your account
|
|
52
|
+
carboncanvas init --new # always create, never ask
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
What it does, in order:
|
|
56
|
+
|
|
57
|
+
1. **Ensures you're signed in** — if there's no stored token it runs the
|
|
58
|
+
browser sign-in inline first. If a stored token turns out to be expired
|
|
59
|
+
(the API answers 401), `init` renews it automatically: it runs the browser
|
|
60
|
+
sign-in once ("Your sign-in expired — let's renew it.") and retries the
|
|
61
|
+
failed call once; a second 401 surfaces as an error.
|
|
62
|
+
2. **Chooses the canvas:**
|
|
63
|
+
- `--canvas <cnv_…>` — links that existing canvas instead of creating one.
|
|
64
|
+
It must be in *your* account (checked against `GET /api/canvases`); an id
|
|
65
|
+
that isn't yours, an archived canvas, or a malformed id (no `cnv_` prefix)
|
|
66
|
+
exits 1 with a clear message. Nothing is created.
|
|
67
|
+
- Plain `init` when your account owns **no** canvases — creates one
|
|
68
|
+
(`POST /api/canvases`; the server mints the `cnv_…` id). The name comes
|
|
69
|
+
from `--name`, else the nearest `package.json` `name` (walking up from the
|
|
70
|
+
project dir), else the directory name.
|
|
71
|
+
- Plain `init` when your account **already owns canvases**:
|
|
72
|
+
- **In a terminal (TTY):** an interactive picker lists them (numbered,
|
|
73
|
+
name + id; archived ones excluded). Press Enter to create a new canvas
|
|
74
|
+
with the resolved name, or type a number to link that existing one.
|
|
75
|
+
- **Non-interactive (coding agents, CI, piped output):** `init` will
|
|
76
|
+
*not* guess. It prints your canvases (one `<id> <name>` per line) and
|
|
77
|
+
exits 1 with instructions to re-run with `--canvas <id>` or `--new` —
|
|
78
|
+
so an agent can relay the choice instead of minting a surprise
|
|
79
|
+
duplicate. First-time accounts (zero canvases) still create
|
|
80
|
+
deterministically with no prompt.
|
|
81
|
+
- `--new` — always create; skips the picker and the non-interactive stop
|
|
82
|
+
entirely (no canvas listing round-trip).
|
|
83
|
+
3. **Writes the env wiring** — carefully:
|
|
84
|
+
- `.env` — appends `VITE_CC_CANVAS_ID=<id>` and `VITE_CC_BACKEND=<url>`
|
|
85
|
+
(creates the file if absent, preserves everything already in it).
|
|
86
|
+
**Write-once:** if `VITE_CC_CANVAS_ID` already has a value, `init` touches
|
|
87
|
+
nothing, prints the existing id, and exits 1 — comments and review history
|
|
88
|
+
live on that id.
|
|
89
|
+
- `.env.example` — mirrors the two keys with placeholder values (skips keys
|
|
90
|
+
already present).
|
|
91
|
+
- `.gitignore` — adds a `.env` rule if none covers it.
|
|
92
|
+
4. **Prints the handoff** — tell your coding agent the canvas is registered; it
|
|
93
|
+
wires the `comments` prop from these env vars (instructions at
|
|
94
|
+
`<serviceUrl>/skill.md`). The canvas id is the one machine-readable line on
|
|
95
|
+
stdout.
|
|
96
|
+
|
|
97
|
+
Flags: `--name <name>`, `--dir <dir>` (project directory, default cwd),
|
|
98
|
+
`--canvas <cnv_…>` (link an existing canvas), `--new` (always create), plus the
|
|
99
|
+
common `--service-url` / `--token`. `--canvas` and `--new` are mutually
|
|
100
|
+
exclusive.
|
|
101
|
+
|
|
102
|
+
## `carboncanvas canvases`
|
|
103
|
+
|
|
104
|
+
Lists the canvases in your account — so you (or your coding agent) can pick an
|
|
105
|
+
id for `init --canvas` or `publish --canvas` at any time.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
carboncanvas canvases
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
One canvas per stdout line, machine-readable: `<id> <name>` (two spaces;
|
|
112
|
+
`(unnamed)` if the canvas has no name), with a trailing ` [archived]` marker
|
|
113
|
+
on soft-archived ones. Headers and the empty-account message go to stderr, so
|
|
114
|
+
`carboncanvas canvases | grep cnv_` sees only canvas lines. Uses the same
|
|
115
|
+
sign-in as `init`, including the automatic renewal on an expired token.
|
|
116
|
+
|
|
36
117
|
## `carboncanvas publish`
|
|
37
118
|
|
|
38
119
|
Runs, in order:
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Canvas registration + listing for `carboncanvas init` — POST /api/canvases
|
|
2
|
+
// and GET /api/canvases (existing endpoints, existing bearer auth; no new
|
|
3
|
+
// server surface — see CONTRACT.md "carboncanvas init"). The server mints the
|
|
4
|
+
// opaque `cnv_…` id on create; GET returns `{ canvases: CanvasView[] }`
|
|
5
|
+
// (server/src/routes/canvases.ts), non-archived unless `includeArchived`.
|
|
6
|
+
//
|
|
7
|
+
// Both calls throw AuthExpiredError on 401 so initCommand can run the
|
|
8
|
+
// device-code login once and retry (see initCommand's relogin wrapper).
|
|
9
|
+
import { AuthExpiredError, UserError } from './errors.js';
|
|
10
|
+
async function readErrorBody(res) {
|
|
11
|
+
try {
|
|
12
|
+
return (await res.json());
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function unreachable(base, err) {
|
|
19
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
20
|
+
return new UserError(`Could not reach the Carbon Canvas backend at ${base} (${detail}). ` +
|
|
21
|
+
'Check your network and --service-url, then retry.');
|
|
22
|
+
}
|
|
23
|
+
export async function createCanvas(serviceUrl, token, name, fetchImpl = fetch) {
|
|
24
|
+
const base = serviceUrl.replace(/\/+$/, '');
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetchImpl(`${base}/api/canvases`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${token}`,
|
|
31
|
+
'content-type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ name }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
throw unreachable(base, err);
|
|
38
|
+
}
|
|
39
|
+
if (res.status === 200 || res.status === 201) {
|
|
40
|
+
const body = (await res.json());
|
|
41
|
+
if (!body.id) {
|
|
42
|
+
throw new UserError('The server accepted the canvas but returned no id. Try again.');
|
|
43
|
+
}
|
|
44
|
+
return { id: body.id, name: body.name ?? null };
|
|
45
|
+
}
|
|
46
|
+
const body = await readErrorBody(res);
|
|
47
|
+
switch (res.status) {
|
|
48
|
+
case 401:
|
|
49
|
+
throw new AuthExpiredError();
|
|
50
|
+
case 402:
|
|
51
|
+
throw new UserError(body.message ?? 'Creating another canvas is not included in your current plan.', 'Upgrade your plan on the dashboard (or archive a canvas), then retry.');
|
|
52
|
+
case 403:
|
|
53
|
+
throw new UserError(body.message ?? 'Your account is not allowed to create canvases.');
|
|
54
|
+
default:
|
|
55
|
+
throw new UserError(body.message ?? `Canvas registration failed with HTTP ${res.status}.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The account's own canvases (GET /api/canvases). Non-archived by default;
|
|
60
|
+
* `includeArchived` adds soft-archived ones (each carries `archivedAt`) so the
|
|
61
|
+
* `--canvas` link path can tell "archived" apart from "not yours".
|
|
62
|
+
*/
|
|
63
|
+
export async function listCanvases(serviceUrl, token, fetchImpl = fetch, includeArchived = false) {
|
|
64
|
+
const base = serviceUrl.replace(/\/+$/, '');
|
|
65
|
+
const url = `${base}/api/canvases${includeArchived ? '?includeArchived=1' : ''}`;
|
|
66
|
+
let res;
|
|
67
|
+
try {
|
|
68
|
+
res = await fetchImpl(url, {
|
|
69
|
+
method: 'GET',
|
|
70
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
throw unreachable(base, err);
|
|
75
|
+
}
|
|
76
|
+
if (res.status === 200) {
|
|
77
|
+
let body;
|
|
78
|
+
try {
|
|
79
|
+
body = (await res.json());
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new UserError('The server returned an unreadable canvas list. Try again.');
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(body.canvases)) {
|
|
85
|
+
throw new UserError('The server returned an unreadable canvas list. Try again.');
|
|
86
|
+
}
|
|
87
|
+
return body.canvases
|
|
88
|
+
.filter((c) => typeof c.id === 'string')
|
|
89
|
+
.map((c) => ({
|
|
90
|
+
id: c.id,
|
|
91
|
+
name: typeof c.name === 'string' ? c.name : null,
|
|
92
|
+
archivedAt: typeof c.archivedAt === 'string' ? c.archivedAt : null,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
if (res.status === 401) {
|
|
96
|
+
await readErrorBody(res); // drain
|
|
97
|
+
throw new AuthExpiredError();
|
|
98
|
+
}
|
|
99
|
+
const body = await readErrorBody(res);
|
|
100
|
+
throw new UserError(body.message ?? `Listing your canvases failed with HTTP ${res.status}.`);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=canvasApi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvasApi.js","sourceRoot":"","sources":["../src/canvasApi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// `carboncanvas canvases` — list the account's canvases so a user (or their
|
|
2
|
+
// coding agent) can pick an id for `init --canvas` / `publish --canvas` at any
|
|
3
|
+
// time. Same GET /api/canvases the init picker uses, same inline sign-in and
|
|
4
|
+
// one-shot relogin (session.ts).
|
|
5
|
+
//
|
|
6
|
+
// Output contract: ONE canvas per stdout line — `<id> <name>` (two spaces),
|
|
7
|
+
// `(unnamed)` when the canvas has no name, a trailing ` [archived]` marker on
|
|
8
|
+
// soft-archived ones. Headers/empty-state go to stderr, so
|
|
9
|
+
// `carboncanvas canvases | grep …` sees only canvas lines.
|
|
10
|
+
import * as log from './logger.js';
|
|
11
|
+
import { listCanvases } from './canvasApi.js';
|
|
12
|
+
import { openSession } from './session.js';
|
|
13
|
+
export async function canvasesCommand(opts) {
|
|
14
|
+
const session = await openSession(opts, '`carboncanvas canvases`');
|
|
15
|
+
const owned = await session.withRelogin((tok) => listCanvases(opts.serviceUrl, tok, opts.fetchImpl, true));
|
|
16
|
+
if (owned.length === 0) {
|
|
17
|
+
log.info(`No canvases on ${opts.serviceUrl} yet. Run \`carboncanvas init\` to create one.`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
log.info(`Canvases on ${opts.serviceUrl} — <id> <name>:`);
|
|
21
|
+
for (const c of owned) {
|
|
22
|
+
log.out(`${c.id} ${c.name ?? '(unnamed)'}${c.archivedAt ? ' [archived]' : ''}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=canvasesCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvasesCommand.js","sourceRoot":"","sources":["../src/canvasesCommand.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,+EAA+E;AAC/E,6EAA6E;AAC7E,iCAAiC;AACjC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,2DAA2D;AAC3D,2DAA2D;AAE3D,OAAO,KAAK,GAAG,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAoB,MAAM,cAAc,CAAC;AAI7D,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAqB;IACzD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,yBAAyB,CAAC,CAAC;IACnE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,EAAE,CAC9C,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CACzD,CAAC;IAEF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC,kBAAkB,IAAI,CAAC,UAAU,gDAAgD,CAAC,CAAC;QAC5F,OAAO;IACT,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,UAAU,kBAAkB,CAAC,CAAC;IAC3D,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,IAAI,WAAW,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpF,CAAC;AACH,CAAC"}
|
package/dist/constants.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// Shared constants for the Carbon Canvas CLI.
|
|
2
2
|
//
|
|
3
|
-
// The prod backend
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
export const DEFAULT_SERVICE_URL = 'https://
|
|
3
|
+
// The prod backend is the hosted Carbon Canvas service (a custom domain in front
|
|
4
|
+
// of the same Railway deployment the comments client talks to). It can be
|
|
5
|
+
// overridden per-invocation with `--service-url` or `CARBON_SERVICE_URL`.
|
|
6
|
+
export const DEFAULT_SERVICE_URL = 'https://app.carboncanvas.design';
|
|
7
|
+
// LEGACY: the Railway-generated URL the service shipped on before the
|
|
8
|
+
// app.carboncanvas.design custom domain existed. It still resolves (same
|
|
9
|
+
// backend), and tokens stored under this key by older CLI versions are honored
|
|
10
|
+
// via a read-time fallback in tokenStore — but it must never be presented to
|
|
11
|
+
// customers as the service URL, and writes never use this key.
|
|
12
|
+
export const LEGACY_SERVICE_URL = 'https://quilt-production-65f6.up.railway.app';
|
|
7
13
|
// The command name a user types (bin name), used in help/error text.
|
|
8
14
|
export const BIN_NAME = 'carboncanvas';
|
|
9
15
|
// Client-side size caps (the server re-validates — these mirror plan §6b/B.3).
|
package/dist/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,iFAAiF;AACjF,0EAA0E;AAC1E,0EAA0E;AAC1E,MAAM,CAAC,MAAM,mBAAmB,GAAG,iCAAiC,CAAC;AAErE,sEAAsE;AACtE,yEAAyE;AACzE,+EAA+E;AAC/E,6EAA6E;AAC7E,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,8CAA8C,CAAC;AAEjF,qEAAqE;AACrE,MAAM,CAAC,MAAM,QAAQ,GAAG,cAAc,CAAC;AAEvC,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,2BAA2B;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,4EAA4E;AAC5E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC;AAC/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAa,CAAC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Device-code login flow — the CLI half of the `/api/cli-auth/*` contract
|
|
2
|
+
// (see CONTRACT.md "CLI auth contract"). THE SHAPE HERE IS FIXED; the server is
|
|
3
|
+
// built to match it byte-for-byte.
|
|
4
|
+
//
|
|
5
|
+
// POST {serviceUrl}/api/cli-auth/start {} → { deviceCode, userCode, verifyUrl, expiresIn, interval }
|
|
6
|
+
// POST {serviceUrl}/api/cli-auth/poll { deviceCode } → { status: "pending" }
|
|
7
|
+
// | { status: "approved", token }
|
|
8
|
+
// | { status: "expired" }
|
|
9
|
+
// 429 on poll → slow down: double the interval for the next poll.
|
|
10
|
+
//
|
|
11
|
+
// The CLI never receives a browser callback — it POLLS, so the flow works in
|
|
12
|
+
// cloud shells (Replit/Bolt/containers) where no browser can reach us back.
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
// Hard ceiling on how long we poll, whatever `expiresIn` says (~15 min).
|
|
15
|
+
export const MAX_POLL_MS = 15 * 60 * 1000;
|
|
16
|
+
/**
|
|
17
|
+
* Begin a grant. Returns null when the backend doesn't speak the device-code
|
|
18
|
+
* flow (older self-hosted deploys, network trouble) so the caller can fall back
|
|
19
|
+
* to the paste-token prompt instead of dying.
|
|
20
|
+
*/
|
|
21
|
+
export async function startDeviceGrant(serviceUrl, fetchImpl = fetch) {
|
|
22
|
+
const base = serviceUrl.replace(/\/+$/, '');
|
|
23
|
+
let res;
|
|
24
|
+
try {
|
|
25
|
+
res = await fetchImpl(`${base}/api/cli-auth/start`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'content-type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({}), // contract: senders MUST send valid JSON
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (res.status !== 200)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const body = (await res.json());
|
|
38
|
+
if (!body.deviceCode || !body.userCode || !body.verifyUrl)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
deviceCode: body.deviceCode,
|
|
42
|
+
userCode: body.userCode,
|
|
43
|
+
verifyUrl: body.verifyUrl,
|
|
44
|
+
expiresIn: typeof body.expiresIn === 'number' ? body.expiresIn : 900,
|
|
45
|
+
interval: typeof body.interval === 'number' && body.interval > 0 ? body.interval : 5,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
53
|
+
/**
|
|
54
|
+
* Poll `/api/cli-auth/poll` until the grant is approved or expired (or we hit
|
|
55
|
+
* the ~15 min ceiling). Honors 429 by doubling the poll interval.
|
|
56
|
+
*/
|
|
57
|
+
export async function pollForApproval(serviceUrl, grant, opts = {}) {
|
|
58
|
+
const base = serviceUrl.replace(/\/+$/, '');
|
|
59
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
60
|
+
const sleep = opts.sleep ?? realSleep;
|
|
61
|
+
const now = opts.now ?? Date.now;
|
|
62
|
+
const heartbeatEveryMs = opts.heartbeatEveryMs ?? 30_000;
|
|
63
|
+
const deadline = now() + Math.min(grant.expiresIn * 1000, MAX_POLL_MS);
|
|
64
|
+
let intervalMs = Math.max(1, grant.interval) * 1000;
|
|
65
|
+
let lastBeat = now();
|
|
66
|
+
while (now() < deadline) {
|
|
67
|
+
await sleep(intervalMs);
|
|
68
|
+
let res;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetchImpl(`${base}/api/cli-auth/poll`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'content-type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ deviceCode: grant.deviceCode }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
78
|
+
return { status: 'unavailable', detail };
|
|
79
|
+
}
|
|
80
|
+
if (res.status === 429) {
|
|
81
|
+
intervalMs *= 2; // contract: slow down
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (res.status !== 200) {
|
|
85
|
+
return { status: 'unavailable', detail: `HTTP ${res.status}` };
|
|
86
|
+
}
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
body = (await res.json());
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { status: 'unavailable', detail: 'unparseable poll response' };
|
|
93
|
+
}
|
|
94
|
+
if (body.status === 'approved' && body.token) {
|
|
95
|
+
return { status: 'approved', token: body.token };
|
|
96
|
+
}
|
|
97
|
+
if (body.status === 'pending') {
|
|
98
|
+
if (opts.heartbeat && now() - lastBeat >= heartbeatEveryMs) {
|
|
99
|
+
opts.heartbeat('Still waiting for approval in the browser …');
|
|
100
|
+
lastBeat = now();
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// "expired" and anything unrecognized: the grant is gone (no oracle).
|
|
105
|
+
return { status: 'expired' };
|
|
106
|
+
}
|
|
107
|
+
return { status: 'expired' };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Best-effort browser open. Cloud shells have no browser — every failure is
|
|
111
|
+
* swallowed silently; the printed link + code are the real UX.
|
|
112
|
+
*/
|
|
113
|
+
export function openBrowser(url) {
|
|
114
|
+
try {
|
|
115
|
+
let cmd;
|
|
116
|
+
let args;
|
|
117
|
+
if (process.platform === 'darwin') {
|
|
118
|
+
cmd = 'open';
|
|
119
|
+
args = [url];
|
|
120
|
+
}
|
|
121
|
+
else if (process.platform === 'win32') {
|
|
122
|
+
cmd = 'cmd';
|
|
123
|
+
args = ['/c', 'start', '""', url];
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
cmd = 'xdg-open';
|
|
127
|
+
args = [url];
|
|
128
|
+
}
|
|
129
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
130
|
+
child.on('error', () => undefined);
|
|
131
|
+
child.unref();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// No browser here — fine. The user has the link and code on screen.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=deviceAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deviceAuth.js","sourceRoot":"","sources":["../src/deviceAuth.ts"],"names":[],"mappings":"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"}
|
package/dist/envFiles.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Env-file surgery for `carboncanvas init`. This touches files in the
|
|
2
|
+
// CUSTOMER'S repo, so the rules are strict:
|
|
3
|
+
//
|
|
4
|
+
// .env — append the two VITE_CC_* lines, preserving everything
|
|
5
|
+
// else byte-for-byte (create if absent). If a
|
|
6
|
+
// VITE_CC_CANVAS_ID with a value already exists, touch
|
|
7
|
+
// NOTHING — the id is write-once (comments live on it).
|
|
8
|
+
// .env.example — mirror the two keys with placeholder values (create or
|
|
9
|
+
// append; keys already present are left alone).
|
|
10
|
+
// .gitignore — ensure a `.env` rule exists (append or create).
|
|
11
|
+
//
|
|
12
|
+
// All functions are pure fs against a project root — no network, no prompts.
|
|
13
|
+
import { promises as fs } from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
export const ENV_CANVAS_KEY = 'VITE_CC_CANVAS_ID';
|
|
16
|
+
export const ENV_BACKEND_KEY = 'VITE_CC_BACKEND';
|
|
17
|
+
async function readIfExists(p) {
|
|
18
|
+
try {
|
|
19
|
+
return await fs.readFile(p, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Find `KEY=value` in dotenv-style content. Returns the (unquoted, trimmed)
|
|
26
|
+
// value of the first assignment, or null when the key isn't assigned at all.
|
|
27
|
+
function envValue(content, key) {
|
|
28
|
+
const re = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=(.*)$`);
|
|
29
|
+
for (const line of content.split(/\r?\n/)) {
|
|
30
|
+
const m = line.match(re);
|
|
31
|
+
if (m) {
|
|
32
|
+
let value = m[1].trim();
|
|
33
|
+
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
34
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
|
|
35
|
+
value = value.slice(1, -1);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function hasEnvKey(content, key) {
|
|
43
|
+
return envValue(content, key) !== null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The write-once guard: the canvas id already wired into `.env`, if any.
|
|
47
|
+
* An assignment with an EMPTY value doesn't count — only a real id blocks init.
|
|
48
|
+
*/
|
|
49
|
+
export async function existingCanvasId(root) {
|
|
50
|
+
const content = await readIfExists(path.join(root, '.env'));
|
|
51
|
+
if (content === null)
|
|
52
|
+
return null;
|
|
53
|
+
const value = envValue(content, ENV_CANVAS_KEY);
|
|
54
|
+
return value ? value : null;
|
|
55
|
+
}
|
|
56
|
+
// Append `block` to a file, preserving existing content and making sure the
|
|
57
|
+
// block starts on its own line. Returns the action taken.
|
|
58
|
+
async function appendBlock(p, block) {
|
|
59
|
+
const existing = await readIfExists(p);
|
|
60
|
+
if (existing === null) {
|
|
61
|
+
await fs.writeFile(p, block);
|
|
62
|
+
return 'created';
|
|
63
|
+
}
|
|
64
|
+
const glue = existing === '' || existing.endsWith('\n') ? '' : '\n';
|
|
65
|
+
await fs.writeFile(p, existing + glue + block);
|
|
66
|
+
return 'appended';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Write the canvas wiring. Throws if a canvas id is already present with a
|
|
70
|
+
* value (callers should check `existingCanvasId` first — this re-check is the
|
|
71
|
+
* last line of defense against clobbering a live canvas binding).
|
|
72
|
+
*/
|
|
73
|
+
export async function writeEnvFiles(root, canvasId, serviceUrl) {
|
|
74
|
+
const already = await existingCanvasId(root);
|
|
75
|
+
if (already) {
|
|
76
|
+
throw new Error(`Refusing to write: ${ENV_CANVAS_KEY} already set (${already}).`);
|
|
77
|
+
}
|
|
78
|
+
// 1. .env — the real values.
|
|
79
|
+
const envBlock = '# Carbon Canvas (added by `carboncanvas init`)\n' +
|
|
80
|
+
`${ENV_CANVAS_KEY}=${canvasId}\n` +
|
|
81
|
+
`${ENV_BACKEND_KEY}=${serviceUrl}\n`;
|
|
82
|
+
const env = await appendBlock(path.join(root, '.env'), envBlock);
|
|
83
|
+
// 2. .env.example — placeholders only, and only for keys not already there.
|
|
84
|
+
const examplePath = path.join(root, '.env.example');
|
|
85
|
+
const exampleContent = (await readIfExists(examplePath)) ?? '';
|
|
86
|
+
const missing = [];
|
|
87
|
+
if (!hasEnvKey(exampleContent, ENV_CANVAS_KEY)) {
|
|
88
|
+
missing.push(`${ENV_CANVAS_KEY}=cnv_your_canvas_id`);
|
|
89
|
+
}
|
|
90
|
+
if (!hasEnvKey(exampleContent, ENV_BACKEND_KEY)) {
|
|
91
|
+
missing.push(`${ENV_BACKEND_KEY}=${serviceUrl}`);
|
|
92
|
+
}
|
|
93
|
+
let envExample = 'unchanged';
|
|
94
|
+
if (missing.length > 0) {
|
|
95
|
+
envExample = await appendBlock(examplePath, '# Carbon Canvas (run `carboncanvas init` to fill these in)\n' + missing.join('\n') + '\n');
|
|
96
|
+
}
|
|
97
|
+
// 3. .gitignore — make sure `.env` never gets committed.
|
|
98
|
+
const gitignorePath = path.join(root, '.gitignore');
|
|
99
|
+
const gitignoreContent = await readIfExists(gitignorePath);
|
|
100
|
+
const covered = (gitignoreContent ?? '')
|
|
101
|
+
.split(/\r?\n/)
|
|
102
|
+
.map((l) => l.trim())
|
|
103
|
+
.some((l) => l === '.env' || l === '/.env' || l === '.env*' || l === '*.env');
|
|
104
|
+
let gitignore = 'unchanged';
|
|
105
|
+
if (!covered) {
|
|
106
|
+
gitignore = await appendBlock(gitignorePath, '.env\n');
|
|
107
|
+
}
|
|
108
|
+
return { env, envExample, gitignore };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=envFiles.js.map
|