clickroom 0.2.0 → 0.3.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 +10 -4
- package/bin/clickroom.js +7 -5
- package/cli/login.js +48 -19
- package/cli/share.js +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,9 +102,15 @@ In a review, the project name in the toolbar is a switcher with the same list. `
|
|
|
102
102
|
|
|
103
103
|
On clickroom.dev every team has a workspace at its own address, `<workspace>.clickroom.dev`, with its own people, projects, comments and builds. Each review is at `<workspace>.clickroom.dev/<project>`.
|
|
104
104
|
|
|
105
|
-
- **
|
|
105
|
+
- **The quickest start:** `npx clickroom` in a project's folder, nothing set up. It opens clickroom.dev, where you sign in (Google, or a link by email). New people name their workspace there, filled in for them (`olga@acme.com` → *Acme*, `acme.clickroom.dev`; a personal address → *Olga's workspace*); people with a workspace pick it. That one click connects the computer. The browser lands on the workspace's setup page, which opens v1 when the terminal has pushed it.
|
|
106
|
+
- **Signing up:** on clickroom.dev, *Start a workspace* signs you in with your email address (a link, or Google). Someone without a workspace goes straight to step 1, its name and address (filled in the same way). The rest of the setup is at `<workspace>.clickroom.dev/welcome`.
|
|
107
|
+
Step 2, share your first prototype: one command in the project's folder, `npx clickroom --workspace <slug>`. The first run opens the site to connect the computer (one click), then builds the project and pushes v1. The page ticks off when the computer connects and when v1 arrives.
|
|
108
|
+
|
|
109
|
+
Teammates and clients are invited later, from the dashboard.
|
|
110
|
+
|
|
111
|
+
Until the owner finishes or skips the setup, the dashboard offers to continue it. Teammates land on the same page from their invite, with only the install-and-connect step.
|
|
106
112
|
- **Your workspaces:** clickroom.dev lists every workspace your address is in, as an owner, teammate or client. *Open* signs you in there. Each workspace's dashboard has *Switch* next to its name.
|
|
107
|
-
- **Sharing from a computer:** `clickroom
|
|
113
|
+
- **Sharing from a computer:** `npx clickroom --workspace <workspace>` in a project's folder: it connects the computer the first time, then shares the project. `clickroom login <workspace>` only connects. Builds are uploaded. Live dev servers (the relay) belong to the first workspace only.
|
|
108
114
|
- **Separate by design:** a sign-in, a CLI key or a share link of one workspace opens nothing in another, even for a project with the same name (`test/workspaces.test.js`). The admin key and the Slack webhook belong to the first workspace.
|
|
109
115
|
|
|
110
116
|
How it's set up:
|
|
@@ -146,12 +152,12 @@ Teammates share their own builds with you through your clickroom (for example cl
|
|
|
146
152
|
|
|
147
153
|
```sh
|
|
148
154
|
npm install -g clickroom
|
|
149
|
-
clickroom login # opens clickroom in the browser: approve this computer
|
|
155
|
+
clickroom login # opens clickroom in the browser: sign in, approve this computer
|
|
150
156
|
cd ~/code/their-app
|
|
151
157
|
clickroom # builds it, uploads it as v1; p pushes the next version
|
|
152
158
|
```
|
|
153
159
|
|
|
154
|
-
- **`clickroom login`** asks clickroom for a login and opens a page where they approve this computer. They sign in
|
|
160
|
+
- **`clickroom login`** asks clickroom for a login and opens a page where they approve this computer. They sign in there with the address they were invited with, and pick the workspace if they're in several (`clickroom login acme` goes straight to acme). Clients can't connect the command. The page and the terminal show the same code to compare.
|
|
155
161
|
- The terminal then gets a key of its own in `~/.clickroom/credentials.json` (readable only by them). It lasts 90 days from its last use.
|
|
156
162
|
- `clickroom logout` ends it. *Revoke* on the People panel ends it as well, along with their browser sign-ins.
|
|
157
163
|
- `clickroom login https://other.example` connects to another clickroom server; `clickroom whoami` shows who's connected.
|
package/bin/clickroom.js
CHANGED
|
@@ -7,11 +7,12 @@ import { call } from '../cli/client.js';
|
|
|
7
7
|
|
|
8
8
|
const HELP = `clickroom: multiplayer review for locally built prototypes
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
clickroom
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
Share a project with your workspace
|
|
11
|
+
npx clickroom in a project: the first time it opens clickroom.dev to connect this
|
|
12
|
+
computer (sign in, pick or name your workspace), then builds the project,
|
|
13
|
+
uploads it as v1 and keeps the session open (p pushes the next version)
|
|
14
|
+
npx clickroom --workspace acme the same, straight to acme.clickroom.dev
|
|
15
|
+
clickroom login [workspace] only connect; then run clickroom in a project
|
|
15
16
|
clickroom logout | whoami
|
|
16
17
|
|
|
17
18
|
Share the project you're in (run inside any project folder)
|
|
@@ -80,6 +81,7 @@ const { values: opts, positionals } = parseArgs({
|
|
|
80
81
|
build: { type: 'string' },
|
|
81
82
|
live: { type: 'boolean' },
|
|
82
83
|
'no-open': { type: 'boolean' },
|
|
84
|
+
workspace: { type: 'string', short: 'w' },
|
|
83
85
|
print: { type: 'boolean' },
|
|
84
86
|
'no-comments': { type: 'boolean' },
|
|
85
87
|
'no-questions': { type: 'boolean' },
|
package/cli/login.js
CHANGED
|
@@ -6,15 +6,46 @@ import { bold, dim } from './dev.js';
|
|
|
6
6
|
export const DEFAULT_URL = 'https://clickroom.dev';
|
|
7
7
|
|
|
8
8
|
// `clickroom login [workspace|url]`: connects this computer to a clickroom workspace as you
|
|
9
|
-
// ("acme" means https://acme.clickroom.dev). It opens a page in your
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// ("acme" means https://acme.clickroom.dev; none: clickroom.dev asks). It opens a page in your
|
|
10
|
+
// browser to approve this computer, then keeps a key of its own in ~/.clickroom/credentials.json.
|
|
11
|
+
// "acme" → https://acme.clickroom.dev; a URL stays as it is.
|
|
12
|
+
export function workspaceUrl(arg) {
|
|
13
|
+
if (/^[a-z][a-z0-9-]{1,19}$/.test(arg)) return `https://${arg}.${new URL(DEFAULT_URL).hostname}`;
|
|
14
|
+
return String(arg).replace(/\/$/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
export async function login(arg) {
|
|
13
|
-
if (arg
|
|
14
|
-
const
|
|
18
|
+
if (arg) arg = workspaceUrl(arg);
|
|
19
|
+
const known = arg || process.env.CLICKROOM_URL || credentials()?.url;
|
|
20
|
+
// No workspace given (and none from before): clickroom.dev asks in the browser, where you sign
|
|
21
|
+
// in and pick yours, or name a new one.
|
|
22
|
+
if (!known) return connect();
|
|
23
|
+
const url = known.replace(/\/$/, '');
|
|
15
24
|
if (!/^https?:\/\//.test(url)) throw new Error('usage: clickroom login <workspace> (or a URL: clickroom login https://acme.clickroom.dev)');
|
|
16
25
|
if (adminToken() && !process.env.CLICKROOM_TOKEN) console.log(dim(' This computer has the admin key (.dev.vars), which the clickroom command uses first.'));
|
|
17
|
-
const
|
|
26
|
+
const start = await begin(`${url}/__ps/api/cli-login`, url);
|
|
27
|
+
console.log(`\n Approve this computer in your browser:\n ${bold(start.url)}\n`);
|
|
28
|
+
console.log(` Check that the page shows ${bold(start.code)}.`);
|
|
29
|
+
console.log(dim(' Not signed in there? Sign in with the email address you were invited with.\n'));
|
|
30
|
+
openUrl(start.url);
|
|
31
|
+
const d = await wait(`${url}/__ps/api/cli-login/poll`, start);
|
|
32
|
+
done(url, d);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The first `npx clickroom` on a computer: started on clickroom.dev, approved in the browser
|
|
36
|
+
// (signing in with Google is most of it), connected to the workspace picked or made there.
|
|
37
|
+
async function connect() {
|
|
38
|
+
const auth = (process.env.CLICKROOM_AUTH_URL || DEFAULT_URL).replace(/\/$/, '');
|
|
39
|
+
const start = await begin(`${auth}/__ps/api/connect`, auth);
|
|
40
|
+
console.log(`\n Opening clickroom in your browser to connect this computer:\n ${bold(start.url)}\n`);
|
|
41
|
+
console.log(dim(` Sign in there (new here? you name your workspace next). It shows ${start.code}.\n`));
|
|
42
|
+
openUrl(start.url);
|
|
43
|
+
const d = await wait(`${auth}/__ps/api/connect/poll`, start);
|
|
44
|
+
done(d.url.replace(/\/$/, ''), d);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function begin(endpoint, url) {
|
|
48
|
+
const res = await fetch(endpoint, {
|
|
18
49
|
method: 'POST',
|
|
19
50
|
headers: { 'content-type': 'application/json' },
|
|
20
51
|
body: JSON.stringify({ label: `${os.userInfo().username} on ${os.hostname().replace(/\.local$/, '')}` }),
|
|
@@ -22,16 +53,14 @@ export async function login(arg) {
|
|
|
22
53
|
if (!res) throw new Error(`Can't reach ${url}.`);
|
|
23
54
|
const start = await res.json().catch(() => ({}));
|
|
24
55
|
if (!res.ok) throw new Error(start.error || `${url} answered ${res.status}`);
|
|
56
|
+
return start;
|
|
57
|
+
}
|
|
25
58
|
|
|
26
|
-
|
|
27
|
-
console.log(` Check that the page shows ${bold(start.code)}.`);
|
|
28
|
-
console.log(dim(' Not signed in there? Sign in with the email address you were invited with.\n'));
|
|
29
|
-
openUrl(start.url);
|
|
30
|
-
|
|
59
|
+
async function wait(endpoint, start) {
|
|
31
60
|
const until = Date.now() + start.expiresIn * 1000;
|
|
32
61
|
while (Date.now() < until) {
|
|
33
62
|
await new Promise((r) => setTimeout(r, 2000));
|
|
34
|
-
const r = await fetch(
|
|
63
|
+
const r = await fetch(endpoint, {
|
|
35
64
|
method: 'POST',
|
|
36
65
|
headers: { 'content-type': 'application/json' },
|
|
37
66
|
body: JSON.stringify({ poll: start.poll }),
|
|
@@ -39,14 +68,14 @@ export async function login(arg) {
|
|
|
39
68
|
if (!r?.ok) continue;
|
|
40
69
|
const d = await r.json();
|
|
41
70
|
if (d.status === 'expired') break;
|
|
42
|
-
if (d.status === 'approved')
|
|
43
|
-
saveCredentials({ url, token: d.token, name: d.me.name, email: d.me.email || null, at: new Date().toISOString() });
|
|
44
|
-
console.log(`${bold('✔')} Signed in to ${url} as ${bold(d.me.name)}${d.me.email ? ` (${d.me.email})` : ''}.`);
|
|
45
|
-
console.log(dim(' Run `clickroom` in a project to share it.'));
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
71
|
+
if (d.status === 'approved') return d;
|
|
48
72
|
}
|
|
49
|
-
throw new Error('The approval link expired. Run
|
|
73
|
+
throw new Error('The approval link expired. Run it again.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function done(url, d) {
|
|
77
|
+
saveCredentials({ url, token: d.token, name: d.me.name, email: d.me.email || null, at: new Date().toISOString() });
|
|
78
|
+
console.log(`${bold('✔')} Connected to ${bold(url.replace(/^https?:\/\//, ''))} as ${bold(d.me.name)}${d.me.email ? ` (${d.me.email})` : ''}.`);
|
|
50
79
|
}
|
|
51
80
|
|
|
52
81
|
export async function logout() {
|
package/cli/share.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import readline from 'node:readline';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { readLocal, writeLocal, copyBuild, cloudUrl, baseUrl, isTeammate, adminToken, ROOT } from './config.js';
|
|
5
|
+
import { readLocal, writeLocal, copyBuild, cloudUrl, baseUrl, isTeammate, adminToken, credentials, ROOT } from './config.js';
|
|
6
6
|
import { call, isUp, signinLink } from './client.js';
|
|
7
7
|
import { projectRoot, projectName, parseTarget, detectTarget } from './detect.js';
|
|
8
8
|
import { startServer, bold, dim } from './dev.js';
|
|
@@ -26,10 +26,18 @@ export async function share(arg, opts) {
|
|
|
26
26
|
const port = Number(opts.port || cfg.port || 8787);
|
|
27
27
|
// In cloud mode comments, versions and links live on the deployed worker; this Mac only
|
|
28
28
|
// relays Live views and API calls (the local server, usually the background service).
|
|
29
|
+
// Not connected to a workspace yet (`npx clickroom` in a fresh project): connect first (signing in
|
|
30
|
+
// in the browser; --workspace acme skips picking it there), then share, in the same run.
|
|
31
|
+
const hasServer = fs.existsSync(path.join(ROOT, 'node_modules', '.bin', 'wrangler'));
|
|
32
|
+
if (!opts.local && !adminToken() && !process.env.CLICKROOM_TOKEN) {
|
|
33
|
+
const { login, workspaceUrl } = await import('./login.js');
|
|
34
|
+
const want = opts.workspace ? workspaceUrl(opts.workspace) : null;
|
|
35
|
+
if (want ? credentials()?.url !== want : !credentials() && !hasServer) await login(opts.workspace);
|
|
36
|
+
}
|
|
29
37
|
// A teammate's computer (`clickroom login`): their builds go to the team's clickroom server.
|
|
30
38
|
const teammate = isTeammate() && !opts.local;
|
|
31
|
-
if (!teammate && !adminToken() && !
|
|
32
|
-
throw new Error('Not
|
|
39
|
+
if (!teammate && !adminToken() && !hasServer) {
|
|
40
|
+
throw new Error('Not connected to a workspace. Run `npx clickroom` (it opens clickroom in your browser).');
|
|
33
41
|
}
|
|
34
42
|
const cloud = opts.local ? null : cloudUrl();
|
|
35
43
|
if (!cloud) process.env.CLICKROOM_URL = `http://localhost:${port}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clickroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Share prototype builds for review: pins, region comments, threads, versions, and agent hand-off (MCP). Teammates push builds to their team’s clickroom with `clickroom login`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|