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 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
- - **Signing up:** on clickroom.dev, *Start a workspace* signs you in with your email address (a link, or Google) and asks for a name and an address. You're its owner and signed in there right away.
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 login <workspace>`, then `clickroom` in a project. Builds are uploaded. Live dev servers (the relay) belong to the first workspace only.
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 from their invite there, as a teammate. Clients can't connect the command. The page and the terminal show the same code to compare.
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
- On a teammate's computer
11
- clickroom login <workspace> connect this computer to your team's workspace (acme → acme.clickroom.dev):
12
- approve it in the browser where you're signed in from your invite
13
- clickroom then, in a project: builds it, uploads it as v1 and keeps the session open
14
- (p pushes the next version). The team sees it on the dashboard.
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 browser (where you're signed
10
- // in from your invite) to approve this computer, then keeps a key of its own in
11
- // ~/.clickroom/credentials.json.
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 && /^[a-z][a-z0-9-]{1,19}$/.test(arg)) arg = `https://${arg}.${new URL(DEFAULT_URL).hostname}`;
14
- const url = (arg || process.env.CLICKROOM_URL || credentials()?.url || DEFAULT_URL).replace(/\/$/, '');
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 res = await fetch(`${url}/__ps/api/cli-login`, {
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
- console.log(`\n Approve this computer in your browser:\n ${bold(start.url)}\n`);
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(`${url}/__ps/api/cli-login/poll`, {
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 `clickroom login` again.');
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() && !fs.existsSync(path.join(ROOT, 'node_modules', '.bin', 'wrangler'))) {
32
- throw new Error('Not signed in. Run `clickroom login` first (it opens clickroom in your browser).');
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.2.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": {